From a66efe9d15603cf390685d772690179a1dde5211 Mon Sep 17 00:00:00 2001 From: SIKTEC Date: Tue, 10 Nov 2026 21:04:41 -0500 Subject: [PATCH 1/2] fix(wave-1): ECMA-404 JSON.parse, process exit/rejections, worker Date/exit, CLI flags (v0.10.9) --- Cargo.lock | 10 +- Cargo.toml | 2 +- crates/slice-cas/src/archive/builder.rs | 12 +- crates/slice-cas/src/bundler/graph_bundler.rs | 4 +- crates/slice-cas/src/bundler/source_map.rs | 16 +- crates/slice-cas/src/compiler/mod.rs | 4 +- crates/slice-cas/src/compiler/transpiler.rs | 42 + crates/slice-cas/src/lib.rs | 5 +- crates/slice-cas/src/linker/ast_walker.rs | 1 - .../slice-cas/src/linker/builtins_and_ids.rs | 3 +- crates/slice-cas/src/linker/esm_transform.rs | 6 +- .../tests/bundler_and_resolver_tests.rs | 15 +- crates/slice-cli/src/commands/init.rs | 13 +- crates/slice-cli/src/commands/mod.rs | 2 +- crates/slice-cli/src/commands/run.rs | 345 +++++- crates/slice-cli/src/main.rs | 29 +- .../tests/cli_security_and_perf_tests.rs | 106 ++ crates/slice-engine/src/bindings/core.rs | 63 ++ crates/slice-engine/src/isolate/exec_boa.rs | 349 +++--- .../slice-engine/src/isolate/exec_bytecode.rs | 2 + crates/slice-engine/src/isolate/mod.rs | 32 + .../src/preludes/00_ultra_core.js | 30 +- .../src/preludes/08_workers_sockets.js | 12 +- crates/slice-engine/src/preludes/18_intl.js | 45 +- .../slice-engine/src/preludes/js_preludes.rs | 7 + crates/slice-engine/src/runtime/boa_realm.rs | 76 +- crates/slice-engine/src/runtime/event_loop.rs | 432 +++++++- crates/slice-engine/src/runtime/workers.rs | 10 +- .../tests/async_webcrypto_and_node_tests.rs | 43 +- .../workers_sockets_and_pillars_tests.rs | 489 +++++++++ vendor/boa_engine/src/builtins/json/mod.rs | 998 ++++++++++-------- 31 files changed, 2519 insertions(+), 684 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6a5f199..803b588 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2005,7 +2005,7 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "slice-cas" -version = "0.10.8" +version = "0.10.9" dependencies = [ "aho-corasick", "anyhow", @@ -2026,7 +2026,7 @@ dependencies = [ [[package]] name = "slice-cli" -version = "0.10.8" +version = "0.10.9" dependencies = [ "anyhow", "criterion", @@ -2047,7 +2047,7 @@ dependencies = [ [[package]] name = "slice-engine" -version = "0.10.8" +version = "0.10.9" dependencies = [ "aho-corasick", "anyhow", @@ -2077,7 +2077,7 @@ dependencies = [ [[package]] name = "slice-io" -version = "0.10.8" +version = "0.10.9" dependencies = [ "anyhow", "criterion", @@ -2088,7 +2088,7 @@ dependencies = [ [[package]] name = "slice-sandbox" -version = "0.10.8" +version = "0.10.9" dependencies = [ "anyhow", "blake3", diff --git a/Cargo.toml b/Cargo.toml index 1dcd0ed..7f62d38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "0.10.8" +version = "0.10.9" edition = "2021" authors = ["Slice Runtime Team"] license = "MIT OR Apache-2.0" diff --git a/crates/slice-cas/src/archive/builder.rs b/crates/slice-cas/src/archive/builder.rs index f58264b..6d92fd6 100644 --- a/crates/slice-cas/src/archive/builder.rs +++ b/crates/slice-cas/src/archive/builder.rs @@ -51,8 +51,7 @@ impl CasBuilder { let lock_path = dir.join(lock_name); if lock_path.is_file() { if let Ok(bytes) = std::fs::read(&lock_path) { - self.lockfile_blake3 = - Some(blake3::hash(&bytes).to_hex().to_string()); + self.lockfile_blake3 = Some(blake3::hash(&bytes).to_hex().to_string()); break; } } @@ -63,12 +62,9 @@ impl CasBuilder { for section in ["dependencies", "devDependencies"] { if let Some(obj) = val.get(section).and_then(|v| v.as_object()) { for (name, spec_val) in obj { - let declared = - spec_val.as_str().unwrap_or("*").to_string(); - let resolved_pkg = dir - .join("node_modules") - .join(name) - .join("package.json"); + let declared = spec_val.as_str().unwrap_or("*").to_string(); + let resolved_pkg = + dir.join("node_modules").join(name).join("package.json"); let version = std::fs::read_to_string(&resolved_pkg) .ok() .and_then(|s| { diff --git a/crates/slice-cas/src/bundler/graph_bundler.rs b/crates/slice-cas/src/bundler/graph_bundler.rs index 988dd56..6b2b232 100644 --- a/crates/slice-cas/src/bundler/graph_bundler.rs +++ b/crates/slice-cas/src/bundler/graph_bundler.rs @@ -337,7 +337,9 @@ pub fn bundle_entrypoint_with_deps(entry_path: &Path) -> Result { bundle.push_str("var __h = null;\n"); bundle.push_str(&format!("__run = __rc[\"{}\"] = {} {{\n", rc_key, fn_kw)); bundle.push_str("if (__h && globalThis.__slice_active_http_server && typeof __slice_http_is_active === 'function' && __slice_http_is_active()) { globalThis.__slice_dispatch_fetch(__h); return; }\n"); - bundle.push_str("var require = __slice_require, exports = {}, module = { exports: exports };\n"); + bundle.push_str( + "var require = __slice_require, exports = {}, module = { exports: exports };\n", + ); bundle.push_str(&entry_body); bundle.push_str("\nif (globalThis.__slice_active_http_server) __h = globalThis.__slice_active_http_server._handler;\n};\n}\n__run();\n"); } else { diff --git a/crates/slice-cas/src/bundler/source_map.rs b/crates/slice-cas/src/bundler/source_map.rs index 98f32e3..bd4dff8 100644 --- a/crates/slice-cas/src/bundler/source_map.rs +++ b/crates/slice-cas/src/bundler/source_map.rs @@ -151,8 +151,8 @@ pub fn map_bundled_error_location(bundled_js: &str, error_msg: &str) -> String { } if !active_short.is_empty() && b_line > marker_line_idx { let rel_idx_0 = count_code_lines_between(&lines, marker_line_idx, b_line); - let mut resolved_line = lookup_embedded_line_map(active_line_map, rel_idx_0) - .unwrap_or(rel_idx_0 + 1); + let mut resolved_line = + lookup_embedded_line_map(active_line_map, rel_idx_0).unwrap_or(rel_idx_0 + 1); if let Some(stmt_text) = lines.get(b_line - 1).map(|s| s.trim()) { if !stmt_text.is_empty() { if let Ok(orig_src) = std::fs::read_to_string(active_full) { @@ -209,8 +209,8 @@ pub fn map_bundled_error_location(bundled_js: &str, error_msg: &str) -> String { })); if matches_line { let rel_idx_0 = count_code_lines_between(&lines, marker_line_idx, line_1); - let mut resolved_line = lookup_embedded_line_map(active_line_map, rel_idx_0) - .unwrap_or(rel_idx_0 + 1); + let mut resolved_line = + lookup_embedded_line_map(active_line_map, rel_idx_0).unwrap_or(rel_idx_0 + 1); if let Ok(orig_src) = std::fs::read_to_string(active_full) { if let Some((orig_idx, _)) = orig_src.lines().enumerate().find(|(_, ol)| { ol.contains(core_msg) @@ -380,10 +380,7 @@ pub fn map_bundled_stack_trace(bundled_js: &str, raw_stack: &str) -> String { let mut resolved: Option<(&str, usize)> = None; if let Some(fn_name) = fn_name_opt { - let bare_fn = fn_name - .rsplit_once('.') - .map(|(_, m)| m) - .unwrap_or(fn_name); + let bare_fn = fn_name.rsplit_once('.').map(|(_, m)| m).unwrap_or(fn_name); let fn_pat_1 = format!("function {}(", bare_fn); let fn_pat_2 = format!("function {} (", bare_fn); let fn_pat_3 = format!("{} = function", bare_fn); @@ -438,8 +435,7 @@ pub fn map_bundled_stack_trace(bundled_js: &str, raw_stack: &str) -> String { } } } else if let Some(callee) = prev_callee_fn { - let bare_callee = - callee.rsplit_once('.').map(|(_, m)| m).unwrap_or(callee); + let bare_callee = callee.rsplit_once('.').map(|(_, m)| m).unwrap_or(callee); let call_pat = format!("{}(", bare_callee); for l_1 in d_line_1..=fn_end_1 { if lines[l_1 - 1].contains(&call_pat) { diff --git a/crates/slice-cas/src/compiler/mod.rs b/crates/slice-cas/src/compiler/mod.rs index 1f7113e..e1a05e5 100644 --- a/crates/slice-cas/src/compiler/mod.rs +++ b/crates/slice-cas/src/compiler/mod.rs @@ -12,6 +12,6 @@ pub mod transpiler; pub use pipeline_fusion::fuse_array_pipelines_ast; pub use semantics::{analyze_typescript_semantics, source_contains_jsx, TypeDirectedHints}; pub use transpiler::{ - rewrite_cjs_require, strip_js_comments, strip_typescript_and_normalize, strip_typescript_raw, - strip_typescript_raw_with_ext, + check_syntax_source, rewrite_cjs_require, strip_js_comments, strip_typescript_and_normalize, + strip_typescript_raw, strip_typescript_raw_with_ext, }; diff --git a/crates/slice-cas/src/compiler/transpiler.rs b/crates/slice-cas/src/compiler/transpiler.rs index fb46d36..cfb6db0 100644 --- a/crates/slice-cas/src/compiler/transpiler.rs +++ b/crates/slice-cas/src/compiler/transpiler.rs @@ -350,3 +350,45 @@ pub fn rewrite_cjs_require(source: &str) -> String { } source.replace("require(", "__slice_cjs_require(") } + +/// Validates JS/TS/JSX/TSX syntax using Oxc's parser (`oxc::parser::Parser`). +/// Returns `Ok(())` if the source has no syntax errors, or `Err(SyntaxError message)` if invalid. +pub fn check_syntax_source(source: &str, path_hint: Option<&Path>) -> Result<(), String> { + let allocator = oxc::allocator::Allocator::default(); + let parse_opts = oxc::parser::ParseOptions { + allow_return_outside_function: true, + ..Default::default() + }; + let is_jsx_ext = path_hint + .and_then(|p| p.extension()) + .and_then(|e| e.to_str()) + .is_some_and(|ext| ext.eq_ignore_ascii_case("tsx") || ext.eq_ignore_ascii_case("jsx")); + let src_type = if is_jsx_ext { + oxc::span::SourceType::tsx().with_module(true) + } else { + oxc::span::SourceType::ts().with_module(true) + }; + let parsed = oxc::parser::Parser::new(&allocator, source, src_type) + .with_options(parse_opts) + .parse(); + if parsed.diagnostics.is_empty() { + return Ok(()); + } + if !is_jsx_ext { + let retry_tsx = oxc::parser::Parser::new( + &allocator, + source, + oxc::span::SourceType::tsx().with_module(true), + ) + .with_options(parse_opts) + .parse(); + if retry_tsx.diagnostics.is_empty() { + return Ok(()); + } + } + let first_err = &parsed.diagnostics[0]; + let file_label = path_hint + .map(|p| p.display().to_string()) + .unwrap_or_else(|| "".to_string()); + Err(format!("SyntaxError in {}: {}", file_label, first_err)) +} diff --git a/crates/slice-cas/src/lib.rs b/crates/slice-cas/src/lib.rs index 6df6a9e..c0e9ba1 100644 --- a/crates/slice-cas/src/lib.rs +++ b/crates/slice-cas/src/lib.rs @@ -32,8 +32,9 @@ pub use bundler::{ resolve_project_entrypoint, }; pub use compiler::{ - analyze_typescript_semantics, fuse_array_pipelines_ast, strip_typescript_and_normalize, - strip_typescript_raw, strip_typescript_raw_with_ext, TypeDirectedHints, + analyze_typescript_semantics, check_syntax_source, fuse_array_pipelines_ast, + strip_typescript_and_normalize, strip_typescript_raw, strip_typescript_raw_with_ext, + TypeDirectedHints, }; pub use linker::{has_top_level_await, wrap_top_level_await_script}; diff --git a/crates/slice-cas/src/linker/ast_walker.rs b/crates/slice-cas/src/linker/ast_walker.rs index 21d54fa..bf939e4 100644 --- a/crates/slice-cas/src/linker/ast_walker.rs +++ b/crates/slice-cas/src/linker/ast_walker.rs @@ -1376,4 +1376,3 @@ pub fn collect_live_import_reference_edits( walk_stmt(stmt, &top_shadow, live_map, edits); } } - diff --git a/crates/slice-cas/src/linker/builtins_and_ids.rs b/crates/slice-cas/src/linker/builtins_and_ids.rs index bee9a0b..17f8461 100644 --- a/crates/slice-cas/src/linker/builtins_and_ids.rs +++ b/crates/slice-cas/src/linker/builtins_and_ids.rs @@ -155,7 +155,8 @@ fn warn_if_declared_in_package_json_but_uninstalled(spec: &str, from_dir: &Path) let Some(pkg_name) = bare_package_name(spec) else { return; }; - let already_warned = WARNED_MISSING_PACKAGES.with(|w| !w.borrow_mut().insert(pkg_name.to_string())); + let already_warned = + WARNED_MISSING_PACKAGES.with(|w| !w.borrow_mut().insert(pkg_name.to_string())); if already_warned { return; } diff --git a/crates/slice-cas/src/linker/esm_transform.rs b/crates/slice-cas/src/linker/esm_transform.rs index 2260d72..d61688f 100644 --- a/crates/slice-cas/src/linker/esm_transform.rs +++ b/crates/slice-cas/src/linker/esm_transform.rs @@ -377,10 +377,8 @@ fn try_transform_esm_to_cjs_ast( if live_import_map.contains_key(loc_s) { continue; } - named_specs.push(( - named.imported.name().to_string(), - loc_s.to_string(), - )); + named_specs + .push((named.imported.name().to_string(), loc_s.to_string())); } } } diff --git a/crates/slice-cas/tests/bundler_and_resolver_tests.rs b/crates/slice-cas/tests/bundler_and_resolver_tests.rs index ddbf68e..f8366ad 100644 --- a/crates/slice-cas/tests/bundler_and_resolver_tests.rs +++ b/crates/slice-cas/tests/bundler_and_resolver_tests.rs @@ -452,10 +452,8 @@ console.log(APP_TITLE, INTERNAL_TAG, metaUrl, metaDir, cjsDir, dynMod.lazyBanner fn test_issues_33_and_34_class_method_source_mapping_and_cas_lockfile_provenance() { use crate::{map_bundled_stack_trace, CasBuilder, MappedCasArchive}; - let test_dir = std::env::temp_dir().join(format!( - "slice_issues_33_34_test_{}", - std::process::id() - )); + let test_dir = + std::env::temp_dir().join(format!("slice_issues_33_34_test_{}", std::process::id())); let _ = std::fs::remove_dir_all(&test_dir); std::fs::create_dir_all(test_dir.join("src")).unwrap(); std::fs::create_dir_all(test_dir.join("node_modules/ms")).unwrap(); @@ -511,10 +509,8 @@ engine.processCheckout({ requestId: "ORD-99", items: [] }); .unwrap(); let bundled = bundle_entrypoint_with_deps(&entry_path).expect("bundle issue 33 app"); - let mapped_err = map_bundled_error_location( - &bundled, - "Uncaught Error: Invalid checkout request: ORD-99", - ); + let mapped_err = + map_bundled_error_location(&bundled, "Uncaught Error: Invalid checkout request: ORD-99"); assert!( mapped_err.contains("(src/pricing_engine.ts:9)"), "Dynamic error message in class method must map to src/pricing_engine.ts:9, got: {}", @@ -552,7 +548,8 @@ engine.processCheckout({ requestId: "ORD-99", items: [] }); "Compacted CAS //@sm: source map must also resolve src/pricing_engine.ts:9, got: {}", mapped_compacted_err ); - let fake_raw_stack = "Error: Invalid checkout request: ORD-99\n at processCheckout (:10:13)"; + let fake_raw_stack = + "Error: Invalid checkout request: ORD-99\n at processCheckout (:10:13)"; let mapped_stack = map_bundled_stack_trace(&annotated, fake_raw_stack); assert!( mapped_stack.contains("src/pricing_engine.ts:"), diff --git a/crates/slice-cli/src/commands/init.rs b/crates/slice-cli/src/commands/init.rs index 5bcccd6..de0a53c 100644 --- a/crates/slice-cli/src/commands/init.rs +++ b/crates/slice-cli/src/commands/init.rs @@ -314,7 +314,13 @@ export default { "#; fn init_sveltekit_template(target_dir: &std::path::Path, project_name: &str) -> Result<()> { - std::fs::create_dir_all(target_dir.join("src").join("routes").join("api").join("items"))?; + std::fs::create_dir_all( + target_dir + .join("src") + .join("routes") + .join("api") + .join("items"), + )?; let manifest_path = target_dir.join("slice.toml"); if !manifest_path.exists() { @@ -427,7 +433,10 @@ export function handleError({ status, error }) { std::fs::write(&hooks_path, hooks_js)?; } - let page_srv_path = target_dir.join("src").join("routes").join("+page.server.js"); + let page_srv_path = target_dir + .join("src") + .join("routes") + .join("+page.server.js"); if !page_srv_path.exists() { let page_srv = r#"/** @type {import('./$types').PageServerLoad} */ export async function load({ url, cookies, locals }) { diff --git a/crates/slice-cli/src/commands/mod.rs b/crates/slice-cli/src/commands/mod.rs index 351aa68..e64c258 100644 --- a/crates/slice-cli/src/commands/mod.rs +++ b/crates/slice-cli/src/commands/mod.rs @@ -7,5 +7,5 @@ pub(crate) use init::cmd_init; pub(crate) use pack_and_compile::{ cmd_compile, cmd_info, cmd_inspect, cmd_pack, print_usage, unique_temp_cas_path, }; -pub(crate) use run::{cmd_run, run_embedded_standalone}; +pub(crate) use run::{cmd_check, cmd_run, run_embedded_standalone, LAST_CLI_EXIT_CODE}; pub(crate) use test_runner::cmd_test; diff --git a/crates/slice-cli/src/commands/run.rs b/crates/slice-cli/src/commands/run.rs index 7ecda27..b94c2fa 100644 --- a/crates/slice-cli/src/commands/run.rs +++ b/crates/slice-cli/src/commands/run.rs @@ -4,10 +4,125 @@ use slice_cas::MappedCasArchive; use slice_engine::{compile_source_to_bytecode, SliceIsolate}; use slice_sandbox::SliceManifest; use std::env; +use std::io::Read; use std::path::Path; +use std::sync::atomic::{AtomicI32, Ordering}; use std::time::Instant; +pub(crate) static LAST_CLI_EXIT_CODE: AtomicI32 = AtomicI32::new(0); + +#[inline] +fn is_known_slice_runtime_flag(arg: &str) -> bool { + matches!( + arg, + "--quiet" + | "-q" + | "--metrics" + | "--watch" + | "--watch-once" + | "--warm" + | "--no-warm" + | "--boacas" + | "--no-boacas" + | "--exper-boacas" + | "--no-exper-boacas" + | "--deterministic" + | "--no-deterministic" + | "--checkpoints" + | "--no-checkpoints" + | "--trust-directives" + | "--no-trust-directives" + ) || arg.starts_with("--allow-") + || arg.starts_with("--fuel=") + || arg.starts_with("--memory=") + || arg.starts_with("--memory-kb=") + || arg.starts_with("--tier=") + || arg.starts_with("--boacas=") + || arg.starts_with("--exper-boacas=") + || arg.starts_with("--deterministic=") + || arg.starts_with("--env-file=") + || arg.starts_with("--audit-egress=") + || arg.starts_with("--audit-egress-file=") + || arg.starts_with("--checkpoints=") + || arg.starts_with("--max-checkpoints=") + || arg.starts_with("--trace=") + || arg.starts_with("--port=") + || arg.starts_with("--workers=") + || arg.starts_with("--static=") + || arg.starts_with("--tls-cert=") + || arg.starts_with("--tls-key=") +} + +pub(crate) fn cmd_check(args: &[String]) -> Result<()> { + LAST_CLI_EXIT_CODE.store(0, Ordering::Relaxed); + let mut targets: Vec = Vec::new(); + let mut eval_snippets: Vec = Vec::new(); + let mut i = 0; + while i < args.len() { + let arg = &args[i]; + if arg == "--check" || arg == "-c" { + i += 1; + continue; + } else if arg == "-e" || arg == "--eval" || arg == "-p" || arg == "--print" { + if i + 1 >= args.len() { + LAST_CLI_EXIT_CODE.store(1, Ordering::Relaxed); + bail!("Flag {} requires a code argument", arg); + } + eval_snippets.push(args[i + 1].clone()); + i += 2; + continue; + } else if let Some(code) = arg + .strip_prefix("--eval=") + .or_else(|| arg.strip_prefix("-e=")) + .or_else(|| { + arg.strip_prefix("--print=") + .or_else(|| arg.strip_prefix("-p=")) + }) + { + eval_snippets.push(code.to_string()); + } else if arg == "-" || !arg.starts_with('-') { + targets.push(arg.clone()); + } + i += 1; + } + + if targets.is_empty() && eval_snippets.is_empty() { + LAST_CLI_EXIT_CODE.store(1, Ordering::Relaxed); + bail!("Usage: slice --check "); + } + + for snippet in &eval_snippets { + if let Err(err) = slice_cas::check_syntax_source(snippet, None) { + LAST_CLI_EXIT_CODE.store(1, Ordering::Relaxed); + bail!("{}", err); + } + } + for target in &targets { + if target == "-" { + let mut stdin_src = String::new(); + std::io::stdin().read_to_string(&mut stdin_src)?; + if let Err(err) = slice_cas::check_syntax_source(&stdin_src, None) { + LAST_CLI_EXIT_CODE.store(1, Ordering::Relaxed); + bail!("{}", err); + } + } else if target.ends_with(".cas") { + let _ = MappedCasArchive::open(Path::new(target))?; + } else { + let path = Path::new(target); + let entry_file = slice_cas::resolve_project_entrypoint(path, None)?; + let raw = std::fs::read_to_string(&entry_file) + .with_context(|| format!("Failed to read {}", entry_file.display()))?; + if let Err(err) = slice_cas::check_syntax_source(&raw, Some(&entry_file)) { + LAST_CLI_EXIT_CODE.store(1, Ordering::Relaxed); + bail!("{}", err); + } + } + } + Ok(()) +} + pub(crate) fn run_embedded_standalone(archive: MappedCasArchive) -> Result<()> { + LAST_CLI_EXIT_CODE.store(0, Ordering::Relaxed); let args: Vec = env::args().collect(); let quiet = args.iter().any(|a| a == "--quiet" || a == "-q"); let print_metrics = args.iter().any(|a| a == "--metrics"); @@ -36,16 +151,19 @@ pub(crate) fn run_embedded_standalone(archive: MappedCasArchive) -> Result<()> { let bc = archive.get_bytecode_slice(entry); let metrics = isolate.execute_bytecode(bc, Some(&archive))?; + if !quiet { + for line in &metrics.stdout { + println!("{}", line); + } + } + if metrics.return_value.starts_with("Uncaught ") { let annotated = archive.reconstruct_annotated_source(entry); let mapped = slice_cas::map_bundled_error_location(&annotated, &metrics.return_value); bail!("{}", mapped); } - - if !quiet { - for line in &metrics.stdout { - println!("{}", line); - } + if let Some(code) = metrics.exit_code { + LAST_CLI_EXIT_CODE.store(code, Ordering::Relaxed); } if print_metrics { println!( @@ -61,14 +179,68 @@ pub(crate) fn run_embedded_standalone(archive: MappedCasArchive) -> Result<()> { } pub(crate) fn cmd_run(args: &[String]) -> Result<()> { + LAST_CLI_EXIT_CODE.store(0, Ordering::Relaxed); let mut exec_cfg = ResolvedExecutionConfig::from_cli_args(args)?; let mut quiet = false; let mut print_metrics = false; let mut watch_mode = false; let mut watch_once = false; let mut target_opt: Option = None; + let mut eval_opt: Option<(String, bool)> = None; + let mut script_args: Vec = Vec::new(); - for arg in args { + let mut i = 0; + while i < args.len() { + let arg = &args[i]; + if arg == "--" { + for rem in &args[i + 1..] { + if target_opt.is_none() && eval_opt.is_none() { + target_opt = Some(rem.clone()); + } else { + script_args.push(rem.clone()); + } + } + break; + } + if target_opt.is_none() && eval_opt.is_none() { + if arg == "--check" || arg == "-c" { + return cmd_check(args); + } else if arg == "--version" || arg == "-v" || arg == "-V" { + println!("v{}", env!("CARGO_PKG_VERSION")); + return Ok(()); + } else if arg == "--help" || arg == "-h" { + crate::commands::print_usage(); + return Ok(()); + } else if arg == "-e" || arg == "--eval" { + if i + 1 >= args.len() { + bail!("Flag {} requires a code argument", arg); + } + eval_opt = Some((args[i + 1].clone(), false)); + i += 2; + continue; + } else if let Some(code) = arg + .strip_prefix("--eval=") + .or_else(|| arg.strip_prefix("-e=")) + { + eval_opt = Some((code.to_string(), false)); + i += 1; + continue; + } else if arg == "-p" || arg == "--print" { + if i + 1 >= args.len() { + bail!("Flag {} requires a code argument", arg); + } + eval_opt = Some((args[i + 1].clone(), true)); + i += 2; + continue; + } else if let Some(code) = arg + .strip_prefix("--print=") + .or_else(|| arg.strip_prefix("-p=")) + { + eval_opt = Some((code.to_string(), true)); + i += 1; + continue; + } + } if arg == "--quiet" || arg == "-q" { quiet = true; } else if arg == "--metrics" { @@ -78,9 +250,79 @@ pub(crate) fn cmd_run(args: &[String]) -> Result<()> { } else if arg == "--watch-once" { watch_mode = true; watch_once = true; - } else if !arg.starts_with('-') && target_opt.is_none() { - target_opt = Some(arg.clone()); + } else if target_opt.is_none() && eval_opt.is_none() { + if arg == "-" || !arg.starts_with('-') { + target_opt = Some(arg.clone()); + } else if !is_known_slice_runtime_flag(arg) { + bail!("Unknown option: {}", arg); + } + } else if !is_known_slice_runtime_flag(arg) { + script_args.push(arg.clone()); + } + i += 1; + } + + if let Some((raw_code, print_result)) = eval_opt { + exec_cfg.apply_inline_directives(&raw_code, Some(Path::new("."))); + if let Err(syn_err) = slice_cas::check_syntax_source(&raw_code, None) { + bail!("{}", syn_err); + } + let transpiled = slice_cas::strip_typescript_and_normalize(&raw_code, true); + if !exec_cfg.warm_pool && exec_cfg.cli_boacas_override.is_none() { + slice_engine::set_thread_exper_boacas_override(Some(false)); + } + let bytecode = compile_source_to_bytecode(&transpiled); + if !exec_cfg.warm_pool && exec_cfg.cli_boacas_override.is_none() { + slice_engine::set_thread_exper_boacas_override(None); + } + let mut isolate = SliceIsolate::spawn( + "tenant-cli-eval", + 1, + exec_cfg.caps.clone(), + exec_cfg.fuel_limit, + exec_cfg.mem_limit, + ); + isolate.set_deterministic_seed(exec_cfg.deterministic_seed); + if print_result { + isolate.set_capture_return_json(true); + } + if !script_args.is_empty() { + let mut argv = Vec::with_capacity(1 + script_args.len()); + argv.push("slice".to_string()); + argv.extend(script_args); + isolate.set_process_argv(argv); + } + for sec in &exec_cfg.audit_secrets { + isolate.add_egress_audit_secret(sec.clone()); + } + let m = isolate.execute_bytecode(&bytecode, None)?; + if !quiet { + for line in &m.stdout { + println!("{}", line); + } + } + if let Some(code) = m.exit_code { + LAST_CLI_EXIT_CODE.store(code, Ordering::Relaxed); + } + if m.return_value.starts_with("Uncaught ") { + if m.exit_code.is_none() { + LAST_CLI_EXIT_CODE.store(1, Ordering::Relaxed); + } + let mapped = slice_cas::map_bundled_error_location(&transpiled, &m.return_value); + bail!("{}", mapped); } + if print_result && m.exit_code.is_none() && !quiet { + if m.return_value == "[object Object]" { + if let Some(json_str) = isolate.last_return_json() { + println!("{}", json_str); + } else { + println!("{}", m.return_value); + } + } else { + println!("{}", m.return_value); + } + } + return Ok(()); } let target = match target_opt { @@ -91,12 +333,66 @@ pub(crate) fn cmd_run(args: &[String]) -> Result<()> { let raw = std::fs::read_to_string(manifest_path)?; let m = SliceManifest::parse_toml(&raw); m.entrypoint + } else if !std::io::IsTerminal::is_terminal(&std::io::stdin()) { + "-".to_string() } else { bail!("Missing target script/archive path (and no slice.toml found in current directory)"); } } }; + if target == "-" { + let mut stdin_src = String::new(); + std::io::stdin().read_to_string(&mut stdin_src)?; + exec_cfg.apply_inline_directives(&stdin_src, Some(Path::new("."))); + if let Err(syn_err) = slice_cas::check_syntax_source(&stdin_src, None) { + bail!("{}", syn_err); + } + let transpiled = slice_cas::strip_typescript_and_normalize(&stdin_src, true); + if !exec_cfg.warm_pool && exec_cfg.cli_boacas_override.is_none() { + slice_engine::set_thread_exper_boacas_override(Some(false)); + } + let bytecode = compile_source_to_bytecode(&transpiled); + if !exec_cfg.warm_pool && exec_cfg.cli_boacas_override.is_none() { + slice_engine::set_thread_exper_boacas_override(None); + } + let mut isolate = SliceIsolate::spawn( + "tenant-cli-stdin", + 1, + exec_cfg.caps.clone(), + exec_cfg.fuel_limit, + exec_cfg.mem_limit, + ); + isolate.set_deterministic_seed(exec_cfg.deterministic_seed); + if !script_args.is_empty() { + let mut argv = Vec::with_capacity(2 + script_args.len()); + argv.push("slice".to_string()); + argv.push("-".to_string()); + argv.extend(script_args); + isolate.set_process_argv(argv); + } + for sec in &exec_cfg.audit_secrets { + isolate.add_egress_audit_secret(sec.clone()); + } + let m = isolate.execute_bytecode(&bytecode, None)?; + if !quiet { + for line in &m.stdout { + println!("{}", line); + } + } + if let Some(code) = m.exit_code { + LAST_CLI_EXIT_CODE.store(code, Ordering::Relaxed); + } + if m.return_value.starts_with("Uncaught ") { + if m.exit_code.is_none() { + LAST_CLI_EXIT_CODE.store(1, Ordering::Relaxed); + } + let mapped = slice_cas::map_bundled_error_location(&transpiled, &m.return_value); + bail!("{}", mapped); + } + return Ok(()); + } + let path = Path::new(&target); let mut bundled_for_sourcemap = String::new(); let metrics = if target.ends_with(".cas") { @@ -118,6 +414,13 @@ pub(crate) fn cmd_run(args: &[String]) -> Result<()> { exec_cfg.mem_limit, ); isolate.set_deterministic_seed(deterministic_seed); + { + let mut argv = Vec::with_capacity(2 + script_args.len()); + argv.push("slice".to_string()); + argv.push(target.clone()); + argv.extend(script_args.iter().cloned()); + isolate.set_process_argv(argv); + } for sec in &exec_cfg.audit_secrets { isolate.add_egress_audit_secret(sec.clone()); } @@ -166,6 +469,13 @@ pub(crate) fn cmd_run(args: &[String]) -> Result<()> { exec_cfg.mem_limit, ); isolate.set_deterministic_seed(deterministic_seed); + { + let mut argv = Vec::with_capacity(2 + script_args.len()); + argv.push("slice".to_string()); + argv.push(target.clone()); + argv.extend(script_args.iter().cloned()); + isolate.set_process_argv(argv); + } for sec in &exec_cfg.audit_secrets { isolate.add_egress_audit_secret(sec.clone()); } @@ -188,18 +498,25 @@ pub(crate) fn cmd_run(args: &[String]) -> Result<()> { m }; - if metrics.return_value.starts_with("Uncaught ") { - let mapped = - slice_cas::map_bundled_error_location(&bundled_for_sourcemap, &metrics.return_value); - bail!("{}", mapped); - } - if !quiet { for line in &metrics.stdout { println!("{}", line); } } + if let Some(code) = metrics.exit_code { + LAST_CLI_EXIT_CODE.store(code, Ordering::Relaxed); + } + + if metrics.return_value.starts_with("Uncaught ") { + if metrics.exit_code.is_none() { + LAST_CLI_EXIT_CODE.store(1, Ordering::Relaxed); + } + let mapped = + slice_cas::map_bundled_error_location(&bundled_for_sourcemap, &metrics.return_value); + bail!("{}", mapped); + } + if print_metrics { println!( "[Slice Telemetry] Tier: {:?} | Instantiate: {:.2} us | Exec: {:.2} us | Total: {:.2} us | Memory: {} B | Stat Syscalls: {}", diff --git a/crates/slice-cli/src/main.rs b/crates/slice-cli/src/main.rs index 5f1c929..88a5bbf 100644 --- a/crates/slice-cli/src/main.rs +++ b/crates/slice-cli/src/main.rs @@ -34,8 +34,8 @@ pub(crate) mod test_runner { mod cli_tests; pub(crate) use commands::{ - cmd_compile, cmd_info, cmd_init, cmd_inspect, cmd_pack, cmd_run, cmd_test, print_usage, - run_embedded_standalone, + cmd_check, cmd_compile, cmd_info, cmd_init, cmd_inspect, cmd_pack, cmd_run, cmd_test, + print_usage, run_embedded_standalone, LAST_CLI_EXIT_CODE, }; #[cfg(test)] pub(crate) use config::OsNativeFileWatcher; @@ -54,7 +54,16 @@ fn main() -> Result<()> { .stack_size(16 * 1024 * 1024); let handle = builder.spawn(cli_entrypoint)?; match handle.join() { - Ok(res) => res, + Ok(Ok(())) => { + let exit_code = LAST_CLI_EXIT_CODE.load(std::sync::atomic::Ordering::Relaxed); + if exit_code != 0 { + let _ = std::io::Write::flush(&mut std::io::stdout()); + let _ = std::io::Write::flush(&mut std::io::stderr()); + std::process::exit(exit_code); + } + Ok(()) + } + Ok(Err(err)) => Err(err), Err(_) => bail!("Slice main thread panicked"), } } @@ -67,6 +76,9 @@ fn cli_entrypoint() -> Result<()> { let args: Vec = env::args().collect(); if args.len() < 2 { + if !std::io::IsTerminal::is_terminal(&std::io::stdin()) { + return cmd_run(&["-".to_string()]); + } print_usage(); return Ok(()); } @@ -77,20 +89,25 @@ fn cli_entrypoint() -> Result<()> { "test" => cmd_test(&args[2..]), "pack" => cmd_pack(&args[2..]), "compile" => cmd_compile(&args[2..]), - "inspect" | "check" => cmd_inspect(&args[2..]), + "inspect" => cmd_inspect(&args[2..]), + "check" | "--check" | "-c" => cmd_check(&args[2..]), "serve" => cmd_serve(&args[2..]), "mcp" => cmd_mcp(&args[2..]), "repl" => cmd_repl(), "doctor" => cmd_doctor(), "microvm-bench" => cmd_microvm_bench(&args[2..]), "verify-security" => cmd_verify_security(), - "info" | "version" | "--version" | "-v" | "-V" => cmd_info(), + "info" => cmd_info(), + "version" | "--version" | "-v" | "-V" => { + println!("v{}", env!("CARGO_PKG_VERSION")); + Ok(()) + } "help" | "--help" | "-h" => { print_usage(); Ok(()) } _ => { - // Allow `slice ` shorthand + // Allow `slice `, `slice -e `, `slice -p `, `slice -` shorthand cmd_run(&args[1..]) } } diff --git a/crates/slice-cli/tests/cli_security_and_perf_tests.rs b/crates/slice-cli/tests/cli_security_and_perf_tests.rs index 30cb747..801c20b 100644 --- a/crates/slice-cli/tests/cli_security_and_perf_tests.rs +++ b/crates/slice-cli/tests/cli_security_and_perf_tests.rs @@ -199,3 +199,109 @@ fn test_cli_perf_01_02_tenant_hotswap_and_itoa_header_microbench() { let _ = std::fs::remove_dir_all(&tmp_dir); } + +#[test] +fn test_wave1_cli_eval_print_check_argv_and_exit_codes() { + use crate::{cmd_check, cmd_run, LAST_CLI_EXIT_CODE}; + use std::sync::atomic::Ordering; + + let tmp_dir = std::env::temp_dir().join("slice_wave1_cli_ergonomics"); + let _ = std::fs::remove_dir_all(&tmp_dir); + std::fs::create_dir_all(&tmp_dir).unwrap(); + + // 1. `-c` / `--check` / `cmd_check` on valid TS/JS vs invalid syntax (must NOT execute side effects) + let valid_script = tmp_dir.join("valid.ts"); + let side_effect_marker = tmp_dir.join("side_effect_ran.txt"); + std::fs::write( + &valid_script, + format!( + "import fs from 'node:fs';\nfs.writeFileSync({:?}, 'ran');\nconst x: number = 40 + 2;\nexport {{ x }};\n", + side_effect_marker.to_string_lossy() + ), + ) + .unwrap(); + + cmd_check(&[valid_script.to_string_lossy().to_string()]).unwrap(); + assert_eq!(LAST_CLI_EXIT_CODE.load(Ordering::Relaxed), 0); + assert!( + !side_effect_marker.exists(), + "syntax check must not execute script side effects" + ); + + cmd_run(&[ + "--check".to_string(), + valid_script.to_string_lossy().to_string(), + ]) + .unwrap(); + assert_eq!(LAST_CLI_EXIT_CODE.load(Ordering::Relaxed), 0); + + let invalid_script = tmp_dir.join("invalid.js"); + std::fs::write(&invalid_script, "const x = (\n").unwrap(); + let err1 = cmd_check(&[invalid_script.to_string_lossy().to_string()]).unwrap_err(); + assert!(err1.to_string().contains("SyntaxError")); + assert_eq!(LAST_CLI_EXIT_CODE.load(Ordering::Relaxed), 1); + + let err2 = cmd_run(&[ + "-c".to_string(), + invalid_script.to_string_lossy().to_string(), + ]) + .unwrap_err(); + assert!(err2.to_string().contains("SyntaxError")); + assert_eq!(LAST_CLI_EXIT_CODE.load(Ordering::Relaxed), 1); + + // 2. `-e` / `--eval` with `process.argv` and exit code propagation + let eval_code = r#" + if (JSON.stringify(process.argv) !== JSON.stringify(["slice", "arg1", "--flag2"])) { + process.exit(99); + } + process.exit(37); + "#; + cmd_run(&[ + "-e".to_string(), + eval_code.to_string(), + "arg1".to_string(), + "--flag2".to_string(), + ]) + .unwrap(); + assert_eq!(LAST_CLI_EXIT_CODE.load(Ordering::Relaxed), 37); + + // 3. Script file execution with trailing script arguments in `process.argv` and `process.exitCode` + let argv_script = tmp_dir.join("script_argv.js"); + std::fs::write( + &argv_script, + r#" + if (JSON.stringify(process.argv.slice(2)) !== JSON.stringify(["--alpha", "beta", "gamma"])) { + process.exit(98); + } + process.exitCode = 11; + "#, + ) + .unwrap(); + cmd_run(&[ + argv_script.to_string_lossy().to_string(), + "--alpha".to_string(), + "beta".to_string(), + "gamma".to_string(), + ]) + .unwrap(); + assert_eq!(LAST_CLI_EXIT_CODE.load(Ordering::Relaxed), 11); + + // 4. `-p` / `--print` evaluates expressions and sets exit code 0 + cmd_run(&["-p".to_string(), "({ sum: 20 + 22 })".to_string()]).unwrap(); + assert_eq!(LAST_CLI_EXIT_CODE.load(Ordering::Relaxed), 0); + + // 5. Unhandled Promise rejection in `-e` sets LAST_CLI_EXIT_CODE = 1 and returns Uncaught (in promise) + let rej_err = cmd_run(&[ + "-e".to_string(), + "Promise.reject(new Error('cli-unhandled'))".to_string(), + ]) + .unwrap_err(); + assert!(rej_err + .to_string() + .contains("Uncaught (in promise) Error: cli-unhandled")); + assert_eq!(LAST_CLI_EXIT_CODE.load(Ordering::Relaxed), 1); + + // Reset LAST_CLI_EXIT_CODE so test harness exits 0 + LAST_CLI_EXIT_CODE.store(0, Ordering::Relaxed); + let _ = std::fs::remove_dir_all(&tmp_dir); +} diff --git a/crates/slice-engine/src/bindings/core.rs b/crates/slice-engine/src/bindings/core.rs index 8e9bb17..f1fded7 100644 --- a/crates/slice-engine/src/bindings/core.rs +++ b/crates/slice-engine/src/bindings/core.rs @@ -534,4 +534,67 @@ pub(crate) fn register_slice_native_host_functions(ctx: &mut boa_engine::Context bytes_to_js_uint8_array(s.as_bytes(), context) }), ); + + let _ = ctx.register_global_builtin_callable( + js_string!("__slice_process_exit"), + 1, + NativeFunction::from_fn_ptr(|_this, args, context| { + let proc_obj = context + .global_object() + .get(js_string!("process"), context) + .ok() + .and_then(|v| v.as_object()); + let mut code: i32 = match args.first() { + Some(v) if !v.is_undefined() && !v.is_null() => v.to_i32(context).unwrap_or(0), + _ => { + if let Some(ref p) = proc_obj { + p.get(js_string!("exitCode"), context) + .ok() + .filter(|v| !v.is_undefined() && !v.is_null()) + .and_then(|v| v.to_i32(context).ok()) + .unwrap_or(0) + } else { + 0 + } + } + }; + if let Some(ref p) = proc_obj { + let _ = p.set(js_string!("exitCode"), BoaVal::from(code), false, context); + let already_exited = p + .get(js_string!("_exited"), context) + .ok() + .and_then(|v| v.as_boolean()) + .unwrap_or(false); + if !already_exited { + let _ = p.set(js_string!("_exited"), BoaVal::from(true), false, context); + if let Ok(emit_val) = p.get(js_string!("emit"), context) { + if let Some(emit_fn) = emit_val.as_object() { + if emit_fn.is_callable() { + let _ = emit_fn.call( + &BoaVal::from(p.clone()), + &[BoaVal::from(js_string!("exit")), BoaVal::from(code)], + context, + ); + } + } + } + if let Some(inner_code) = crate::boa_realm::BOA_PROCESS_EXIT.with(|e| e.get()) { + code = inner_code; + } else if let Ok(ec_val) = p.get(js_string!("exitCode"), context) { + if !ec_val.is_undefined() && !ec_val.is_null() { + if let Ok(updated) = ec_val.to_i32(context) { + code = updated; + } + } + } + } + } + crate::boa_realm::BOA_PROCESS_EXIT.with(|e| e.set(Some(code))); + crate::boa_realm::BOA_LAST_EXIT_CODE.with(|e| e.set(Some(code))); + if crate::workers::is_worker_thread() { + crate::workers::worker_self_set_listening(false, Some(code)); + } + Err(boa_engine::error::PanicError::new(format!("__SLICE_PROCESS_EXIT__:{code}")).into()) + }), + ); } diff --git a/crates/slice-engine/src/isolate/exec_boa.rs b/crates/slice-engine/src/isolate/exec_boa.rs index 7abb0c3..aa7421f 100644 --- a/crates/slice-engine/src/isolate/exec_boa.rs +++ b/crates/slice-engine/src/isolate/exec_boa.rs @@ -195,6 +195,10 @@ impl SliceIsolate { BOA_STDOUT.with(|out| out.borrow_mut().clear()); BOA_VIOLATION.with(|v| *v.borrow_mut() = None); BOA_DIRTY_SUBSYSTEMS.with(|d| d.set(0)); + crate::boa_realm::BOA_PROCESS_EXIT.with(|e| e.set(None)); + crate::boa_realm::BOA_LAST_EXIT_CODE.with(|e| e.set(None)); + crate::boa_realm::BOA_UNHANDLED_REJECTIONS.with(|u| u.borrow_mut().clear()); + crate::boa_realm::BOA_UNCAUGHT_ERROR.with(|e| *e.borrow_mut() = None); BOA_ACTIVE_TENANT.with(|t| { let mut b = t.borrow_mut(); b.clear(); @@ -326,6 +330,11 @@ impl SliceIsolate { )); BOA_DET_STATE.with(|s| s.set(None)); } + if !self.process_argv.is_empty() { + let _ = realm.ctx.eval(boa_engine::Source::from_bytes( + b"globalThis.__slice_argv=undefined;if(globalThis.process)globalThis.process.argv=[\"slice\"];", + )); + } if flags.has_fs_restrictions { BOA_ACTIVE_CAPS.with(|c| *c.borrow_mut() = None); } @@ -393,16 +402,55 @@ impl SliceIsolate { is_http_req: bool, has_next_tick: bool, check_ses: bool, - eval_res: boa_engine::JsResult, + mut eval_res: boa_engine::JsResult, ) -> Result { - if (!flags.is_worker || eval_res.is_ok()) - && (!BOA_HTTP_REQ.with(|r| r.borrow().active) - || BOA_HTTP_RES.with(|res| res.borrow().is_none()) - || has_pending_event_loop_work()) + let last_cp_violation = BOA_LAST_CHILD_PROC_VIOLATION.with(|v| v.borrow_mut().take()); + if eval_res.is_err() { + if let Some(cp_v) = last_cp_violation { + BOA_VIOLATION.with(|v| { + if v.borrow().is_none() { + *v.borrow_mut() = Some(cp_v); + } + }); + } + } + + if crate::boa_realm::BOA_PROCESS_EXIT.with(|e| e.get().is_some()) { + let _ = realm.ctx.run_jobs(); + crate::boa_realm::BOA_UNHANDLED_REJECTIONS.with(|u| u.borrow_mut().clear()); + eval_res = Ok(boa_engine::JsValue::undefined()); + } else if let Err(err) = eval_res { + let _ = realm + .ctx + .eval(boa_engine::Source::from_bytes(b"__slice_reset_mt();")); + if BOA_VIOLATION.with(|v| v.borrow().is_none()) + && crate::event_loop::handle_uncaught_js_error(err, &mut realm.ctx) + { + eval_res = Ok(boa_engine::JsValue::undefined()); + if crate::boa_realm::BOA_PROCESS_EXIT.with(|e| e.get().is_none()) + && (!BOA_HTTP_REQ.with(|r| r.borrow().active) + || BOA_HTTP_RES.with(|res| res.borrow().is_none()) + || has_pending_event_loop_work()) + { + drive_event_loop_to_completion(&mut realm.ctx); + } + } else { + let _ = realm.ctx.run_jobs(); + crate::boa_realm::BOA_UNHANDLED_REJECTIONS.with(|u| u.borrow_mut().clear()); + eval_res = Ok(boa_engine::JsValue::undefined()); + } + } else if !BOA_HTTP_REQ.with(|r| r.borrow().active) + || BOA_HTTP_RES.with(|res| res.borrow().is_none()) + || has_pending_event_loop_work() { drive_event_loop_to_completion(&mut realm.ctx); } - if is_http_req && BOA_HTTP_RES.with(|res| res.borrow().is_none()) { + + if is_http_req + && crate::boa_realm::BOA_PROCESS_EXIT.with(|e| e.get().is_none()) + && crate::boa_realm::BOA_UNCAUGHT_ERROR.with(|e| e.borrow().is_none()) + && BOA_HTTP_RES.with(|res| res.borrow().is_none()) + { let _ = realm.ctx.eval(boa_engine::Source::from_bytes( b"if(globalThis.__slice_active_http_server&&typeof globalThis.__slice_active_http_server._dispatchActiveHttpBridge==='function'){globalThis.__slice_active_http_server._dispatchActiveHttpBridge();}", )); @@ -431,6 +479,61 @@ impl SliceIsolate { } } + let mut final_val = match eval_res { + Ok(mut val) => { + if val.as_promise().is_some() { + if let Ok(st_val) = realm + .ctx + .global_object() + .get(boa_engine::js_string!("__slice_tla_st"), &mut realm.ctx) + { + let st = st_val.as_number().unwrap_or(0.0) as i32; + if st == 1 || st == 2 { + let _ = realm.ctx.global_object().set( + boa_engine::js_string!("__slice_tla_st"), + boa_engine::JsValue::from(0), + false, + &mut realm.ctx, + ); + let tla_v = realm + .ctx + .global_object() + .get(boa_engine::js_string!("__slice_tla_v"), &mut realm.ctx) + .unwrap_or_default(); + if st == 1 { + val = tla_v; + } else if crate::boa_realm::BOA_PROCESS_EXIT.with(|e| e.get().is_none()) + { + let tla_err = boa_engine::JsError::from_opaque(tla_v); + let _ = crate::event_loop::handle_uncaught_js_error( + tla_err, + &mut realm.ctx, + ); + val = boa_engine::JsValue::undefined(); + } else { + val = boa_engine::JsValue::undefined(); + } + } + } + } + val + } + Err(_) => boa_engine::JsValue::undefined(), + }; + + if (realm.loaded_shards_mask & (1 << 18)) != 0 + || realm + .ctx + .global_object() + .get(boa_engine::js_string!("__shMask"), &mut realm.ctx) + .ok() + .and_then(|v| v.as_number()) + .is_some_and(|n| ((n as u32) & (1 << 18)) != 0) + { + realm.loaded_shards_mask |= 1 << 18; + crate::event_loop::finalize_and_reset_process_state(&mut realm.ctx); + } + self.teardown_boa_compartment_thread_locals(&mut realm, flags, has_next_tick); self.harvest_boa_stdout(); let (cp_taken, cp_restored, cp_restore_ns) = @@ -439,18 +542,20 @@ impl SliceIsolate { self.restores_performed = cp_restored; self.restore_time_ns = cp_restore_ns; - let last_cp_violation = BOA_LAST_CHILD_PROC_VIOLATION.with(|v| v.borrow_mut().take()); - if eval_res.is_err() { - if let Some(cp_v) = last_cp_violation { - BOA_VIOLATION.with(|v| { - if v.borrow().is_none() { - *v.borrow_mut() = Some(cp_v); - } - }); + let exit_code_opt = crate::boa_realm::BOA_PROCESS_EXIT + .with(|e| e.replace(None)) + .or_else(|| crate::boa_realm::BOA_LAST_EXIT_CODE.with(|e| e.replace(None))); + self.last_exit_code = exit_code_opt; + let uncaught_err_opt = crate::boa_realm::BOA_UNCAUGHT_ERROR.with(|e| e.borrow_mut().take()); + crate::boa_realm::BOA_UNHANDLED_REJECTIONS.with(|u| u.borrow_mut().clear()); + + if let Some(mut violation) = BOA_VIOLATION.with(|v| v.borrow_mut().take()) { + if let SandboxViolation::CpuFuelExhausted { ref mut limit, .. } = violation { + if *limit == 0 { + self.compartment.fuel_consumed = self.compartment.fuel_limit; + *limit = self.compartment.fuel_limit; + } } - } - - if let Some(violation) = BOA_VIOLATION.with(|v| v.borrow_mut().take()) { release_warm_boa_realm(realm); return Err(violation); } @@ -476,22 +581,6 @@ impl SliceIsolate { intrinsic: ses_mutated_target, }); } - if let Err(ref err) = eval_res { - let msg = err.to_string(); - if msg.contains("RuntimeLimitError") - || msg.contains("iteration loops") - || msg.contains("loop iteration limit") - || msg.contains("Maximum call stack") - || msg.contains("recursion limit") - { - self.compartment.fuel_consumed = self.compartment.fuel_limit; - release_warm_boa_realm(realm); - return Err(SandboxViolation::CpuFuelExhausted { - tenant_id: self.compartment.tenant_id.clone(), - limit: self.compartment.fuel_limit, - }); - } - } let kv_used = BOA_KV_MEM_BUDGET.with(|m| m.get().0); if kv_used > 0 { if let Err(v) = self.compartment.alloc_memory(kv_used) { @@ -503,129 +592,50 @@ impl SliceIsolate { } self.last_return_json = None; - let result = match eval_res { - Ok(mut val) => { - if val.as_promise().is_some() { - if let Ok(st_val) = realm - .ctx - .global_object() - .get(boa_engine::js_string!("__slice_tla_st"), &mut realm.ctx) - { - let st = st_val.as_number().unwrap_or(0.0) as i32; - if st == 1 || st == 2 { - let _ = realm.ctx.global_object().set( - boa_engine::js_string!("__slice_tla_st"), - boa_engine::JsValue::from(0), - false, - &mut realm.ctx, - ); - let tla_v = realm - .ctx - .global_object() - .get(boa_engine::js_string!("__slice_tla_v"), &mut realm.ctx) - .unwrap_or_default(); - if st == 1 { - val = tla_v; - } else { - let err_msg = tla_v - .to_string(&mut realm.ctx) - .map(|s| s.to_std_string_escaped()) - .unwrap_or_else(|_| "Promise rejected".to_string()); - release_warm_boa_realm(realm); - return Ok(JsValue::str(format!("Uncaught {}", err_msg))); - } - } - } - } - if val.is_undefined() { - Ok(JsValue::Undefined) - } else if val.is_null() { - if self.capture_return_json { - self.last_return_json = Some("null".to_string()); - } - Ok(JsValue::Null) - } else if let Some(b) = val.as_boolean() { - if self.capture_return_json { - self.last_return_json = Some(if b { "true" } else { "false" }.to_string()); - } - Ok(JsValue::Bool(b)) - } else if let Some(n) = val.as_number() { - if self.capture_return_json && n.is_finite() { - self.last_return_json = Some(JsValue::Number(n).to_string()); - } - Ok(JsValue::Number(n)) - } else if let Some(s) = val.as_string() { - let st = s.to_std_string_escaped(); - if self.capture_return_json { - self.last_return_json = serde_json::to_string(&st).ok(); - } - Ok(JsValue::str(st)) - } else { - if self.capture_return_json { - if let Some(obj) = val.as_object() { - if !obj.is_callable() { - if let Ok(Some(json_val)) = val.to_json(&mut realm.ctx) { - self.last_return_json = serde_json::to_string(&json_val).ok(); - } - } - } - } - if let Ok(s) = val.to_string(&mut realm.ctx) { - Ok(JsValue::str(s.to_std_string_escaped())) - } else { - Ok(JsValue::Undefined) - } - } + if let Some(err_str) = uncaught_err_opt { + release_warm_boa_realm(realm); + return Ok(JsValue::str(err_str)); + } + if exit_code_opt.is_some() && final_val.as_promise().is_some() { + final_val = boa_engine::JsValue::undefined(); + } + let result = if final_val.is_undefined() { + Ok(JsValue::Undefined) + } else if final_val.is_null() { + if self.capture_return_json { + self.last_return_json = Some("null".to_string()); } - Err(err) => { - let raw_err = format!("Uncaught {}", err); - let mut mapped = crate::boa_realm::map_error_with_active_source(&raw_err); - if mapped == raw_err { - if let Some((first_line, rest)) = mapped.split_once('\n') { - if let Some(open) = first_line.rfind(" (unknown at ") { - let loc_paren = &first_line[open + 1..]; - if rest.contains(loc_paren) { - mapped = format!("{}\n{}", &first_line[..open], rest); - } - } - } - if let Some(opaque) = err.as_opaque() { - if let Some(obj) = opaque.as_object() { - if let Ok(stack_val) = - obj.get(boa_engine::js_string!("stack"), &mut realm.ctx) - { - if let Some(s) = stack_val.as_string() { - let st = s.to_std_string_escaped(); - for line in st.lines() { - let trimmed = line.trim(); - if trimmed.starts_with("at ") - && !trimmed.contains("") - { - if let Some(open) = trimmed.rfind('(') { - if let Some(close) = trimmed[open + 1..].find(')') { - let loc = &trimmed[open + 1..open + 1 + close]; - if loc.contains(':') && !raw_err.contains(loc) { - mapped = format!("{} ({})", raw_err, loc); - break; - } - } - } else if let Some(loc) = trimmed.strip_prefix("at ") { - if loc.contains(':') - && !loc.contains(' ') - && !raw_err.contains(loc) - { - mapped = format!("{} ({})", raw_err, loc); - break; - } - } - } - } - } - } + Ok(JsValue::Null) + } else if let Some(b) = final_val.as_boolean() { + if self.capture_return_json { + self.last_return_json = Some(if b { "true" } else { "false" }.to_string()); + } + Ok(JsValue::Bool(b)) + } else if let Some(n) = final_val.as_number() { + if self.capture_return_json && n.is_finite() { + self.last_return_json = Some(JsValue::Number(n).to_string()); + } + Ok(JsValue::Number(n)) + } else if let Some(s) = final_val.as_string() { + let st = s.to_std_string_escaped(); + if self.capture_return_json { + self.last_return_json = serde_json::to_string(&st).ok(); + } + Ok(JsValue::str(st)) + } else { + if self.capture_return_json { + if let Some(obj) = final_val.as_object() { + if !obj.is_callable() { + if let Ok(Some(json_val)) = final_val.to_json(&mut realm.ctx) { + self.last_return_json = serde_json::to_string(&json_val).ok(); } } } - Ok(JsValue::str(mapped)) + } + if let Ok(s) = final_val.to_string(&mut realm.ctx) { + Ok(JsValue::str(s.to_std_string_escaped())) + } else { + Ok(JsValue::Undefined) } }; @@ -633,6 +643,39 @@ impl SliceIsolate { result } + #[inline] + fn apply_process_argv_to_realm(&self, realm: &mut WarmBoaRealm) { + if self.process_argv.is_empty() { + return; + } + let argv_arr = boa_engine::object::builtins::JsArray::from_iter( + self.process_argv + .iter() + .map(|s| boa_engine::JsValue::from(boa_engine::js_string!(s.as_str()))), + &mut realm.ctx, + ); + let _ = realm.ctx.global_object().set( + boa_engine::js_string!("__slice_argv"), + boa_engine::JsValue::from(argv_arr.clone()), + false, + &mut realm.ctx, + ); + if let Some(proc_obj) = realm + .ctx + .global_object() + .get(boa_engine::js_string!("process"), &mut realm.ctx) + .ok() + .and_then(|v| v.as_object()) + { + let _ = proc_obj.set( + boa_engine::js_string!("argv"), + boa_engine::JsValue::from(argv_arr), + false, + &mut realm.ctx, + ); + } + } + /// Executes full ES2024+ JavaScript inside the production **Boa (`boa_engine`)** ECMAScript VM, /// reusing pre-warmed contexts from `BOA_CONTEXT_POOL`, capturing `console.log` / `print` output /// via zero-eval native host hooks, and enforcing SES compartment limits. @@ -648,6 +691,7 @@ impl SliceIsolate { } let mut realm = acquire_warm_boa_realm(); + self.apply_process_argv_to_realm(&mut realm); if flags.has_env_keys { let _ = realm.ctx.eval(boa_engine::Source::from_bytes( @@ -814,6 +858,7 @@ impl SliceIsolate { Some(r) => (r, false), None => (create_bare_boa_realm(), true), }; + self.apply_process_argv_to_realm(&mut realm); let has_next_tick = (header.feature_flags & BOACAS_FLAG_HAS_NEXT_TICK) != 0; if is_cold_bare_realm && header.approach_kind >= 1 && !precompiled_slice.is_empty() { diff --git a/crates/slice-engine/src/isolate/exec_bytecode.rs b/crates/slice-engine/src/isolate/exec_bytecode.rs index cf851e4..3156eca 100644 --- a/crates/slice-engine/src/isolate/exec_bytecode.rs +++ b/crates/slice-engine/src/isolate/exec_bytecode.rs @@ -92,6 +92,7 @@ impl SliceIsolate { checkpoints_taken: self.checkpoints_taken, restores_performed: self.restores_performed, restore_time_ns: self.restore_time_ns, + exit_code: self.last_exit_code, }); } @@ -318,6 +319,7 @@ impl SliceIsolate { checkpoints_taken: self.checkpoints_taken, restores_performed: self.restores_performed, restore_time_ns: self.restore_time_ns, + exit_code: self.last_exit_code, }) } diff --git a/crates/slice-engine/src/isolate/mod.rs b/crates/slice-engine/src/isolate/mod.rs index 98a493f..0e76092 100644 --- a/crates/slice-engine/src/isolate/mod.rs +++ b/crates/slice-engine/src/isolate/mod.rs @@ -49,6 +49,8 @@ pub struct ExecutionMetrics { pub restores_performed: u32, #[serde(default)] pub restore_time_ns: u64, + #[serde(default)] + pub exit_code: Option, } #[derive(Debug, Clone)] @@ -78,6 +80,8 @@ pub struct SliceIsolate { pub(crate) capture_return_json: bool, pub(crate) last_return_json: Option, pub(crate) last_exec_check_ses: bool, + pub(crate) last_exit_code: Option, + pub(crate) process_argv: Vec, } impl SliceIsolate { @@ -116,9 +120,36 @@ impl SliceIsolate { capture_return_json: false, last_return_json: None, last_exec_check_ses: false, + last_exit_code: None, + process_argv: Vec::new(), } } + /// Returns the explicit or implicit process exit code (`process.exit(code)`, `process.exitCode`, + /// or `1` on uncaught exception / unhandled rejection) from the most recent execution, if set. + #[inline] + pub fn last_exit_code(&self) -> Option { + self.last_exit_code + } + + /// Enables or disables capturing the JSON representation of the last evaluated expression (`-p` / `--print`). + #[inline] + pub fn set_capture_return_json(&mut self, enable: bool) { + self.capture_return_json = enable; + } + + /// Returns the captured JSON representation of the last evaluated expression, if available. + #[inline] + pub fn last_return_json(&self) -> Option<&str> { + self.last_return_json.as_deref() + } + + /// Configures `process.argv` for CLI script execution (`["slice", script_path, ...args]`). + #[inline] + pub fn set_process_argv(&mut self, argv: Vec) { + self.process_argv = argv; + } + /// Registers a sensitive secret token to be guarded against egress exfiltration (`--audit-egress`). /// When no secrets are registered (default), the egress guard costs `0.00 ns`. /// Pre-computes both standard Base64 and URL-safe Base64 encodings at registration time @@ -273,6 +304,7 @@ impl SliceIsolate { self.restore_time_ns = 0; self.last_return_json = None; self.last_exec_check_ses = false; + self.last_exit_code = None; if let Some(s) = self.deterministic_seed { self.det_prng_state = s ^ 0x9E37_79B9_7F4A_7C15; self.det_clock_ms = 1_700_000_000_000; diff --git a/crates/slice-engine/src/preludes/00_ultra_core.js b/crates/slice-engine/src/preludes/00_ultra_core.js index 7cc27dd..a6ca3d4 100644 --- a/crates/slice-engine/src/preludes/00_ultra_core.js +++ b/crates/slice-engine/src/preludes/00_ultra_core.js @@ -99,6 +99,13 @@ return __slice_env_get(String(k)); }; + function __pLazy(m) { + return function() { + __loadShard(18); + return globalThis.process[m].apply(globalThis.process, arguments); + }; + } + globalThis.process = { pid: 1, env: { NODE_ENV: "production" }, @@ -107,7 +114,22 @@ get arch() { return __slice_os_arch(); }, version: "v24.0.0", versions: { node: "24.0.0" }, - argv: ["slice"] + argv: globalThis.__slice_argv || ["slice"], + on: __pLazy("on"), + once: __pLazy("once"), + off: __pLazy("off"), + addListener: __pLazy("addListener"), + removeListener: __pLazy("removeListener"), + removeAllListeners: __pLazy("removeAllListeners"), + listeners: __pLazy("listeners"), + listenerCount: __pLazy("listenerCount"), + emitWarning: __pLazy("emitWarning"), + exit: function(code) { + __ntQ.length = 0; + __qmQ.length = 0; + __mtS = false; + __slice_process_exit(code); + } }; globalThis.__slice_check_ses = function() { @@ -208,6 +230,12 @@ } }; + globalThis.__slice_reset_mt = function() { + __ntQ.length = 0; + __qmQ.length = 0; + __mtS = false; + }; + globalThis.__slice_set_als_wrap = function(wrapFn) { __alsWrap = wrapFn || null; if (__alsWrap && !__ntHooked) { diff --git a/crates/slice-engine/src/preludes/08_workers_sockets.js b/crates/slice-engine/src/preludes/08_workers_sockets.js index f9e938e..aafc2c3 100644 --- a/crates/slice-engine/src/preludes/08_workers_sockets.js +++ b/crates/slice-engine/src/preludes/08_workers_sockets.js @@ -5,6 +5,13 @@ if (!globalThis.TextEncoder) __slice_ensure_shard(2); function __serReplacer(_k, v) { + var raw = (this && typeof this === "object") ? this[_k] : v; + if (raw instanceof Date) { + return { __slice_t: "date", t: raw.getTime() }; + } + if (typeof raw === "number" && !Number.isFinite(raw)) { + return { __slice_t: "num", v: raw !== raw ? "NaN" : (raw > 0 ? "Inf" : "-Inf") }; + } if (v instanceof Uint8Array) { return { __slice_t: "u8", a: Array.from(v) }; } @@ -32,6 +39,8 @@ function __serReviver(_k, v) { if (v && typeof v === "object" && typeof v.__slice_t === "string") { var t = v.__slice_t; + if (t === "date") return new Date(v.t === null ? NaN : v.t); + if (t === "num") return v.v === "NaN" ? NaN : (v.v === "Inf" ? Infinity : -Infinity); if (t === "u8") return new Uint8Array(v.a || []); if (t === "ab") return new Uint8Array(v.a || []).buffer; if (t === "bi") return BigInt(v.s || "0"); @@ -473,9 +482,6 @@ globalThis.removeEventListener = function(type, fn, opts) { port.removeEventListener(type, fn, opts); }; - globalThis.process.exit = function(code) { - __slice_worker_self_set_listening(false, Number(code) || 0); - }; } else { globalThis.self = globalThis; } diff --git a/crates/slice-engine/src/preludes/18_intl.js b/crates/slice-engine/src/preludes/18_intl.js index 3eaa0ec..ff3397c 100644 --- a/crates/slice-engine/src/preludes/18_intl.js +++ b/crates/slice-engine/src/preludes/18_intl.js @@ -100,10 +100,13 @@ }; var __pEv = Object.create(null); + var __pEvCount = 0; + p._exited = false; p.on = p.addListener = function(ev, fn) { if (typeof fn === "function") { var k = String(ev); (__pEv[k] || (__pEv[k] = [])).push(fn); + __pEvCount++; } return p; }; @@ -121,19 +124,48 @@ p.off = p.removeListener = function(ev, fn) { var k = String(ev), list = __pEv[k]; if (list) { + var prevLen = list.length; __pEv[k] = list.filter(function(f) { return f !== fn && f.listener !== fn; }); + __pEvCount = Math.max(0, __pEvCount - (prevLen - __pEv[k].length)); } return p; }; p.removeAllListeners = function(ev) { - if (arguments.length === 0) __pEv = Object.create(null); - else delete __pEv[String(ev)]; + if (arguments.length === 0) { + __pEv = Object.create(null); + __pEvCount = 0; + } else { + var k = String(ev); + if (__pEv[k]) { + __pEvCount = Math.max(0, __pEvCount - __pEv[k].length); + delete __pEv[k]; + } + } return p; }; p.listeners = function(ev) { return (__pEv[String(ev)] || []).slice(); }; + p.listenerCount = function(ev) { + var l = __pEv[String(ev)]; + return l ? l.length : 0; + }; + p._hasListeners = function(ev) { + if (__pEvCount === 0) return false; + if (!ev) return true; + var l = __pEv[String(ev)]; + return Boolean(l && l.length > 0); + }; + p._resetProcessState = function() { + if (__pEvCount > 0) { + __pEv = Object.create(null); + __pEvCount = 0; + } + p.exitCode = undefined; + p._exited = false; + }; p.emit = function(ev) { + if (__pEvCount === 0) return false; var list = __pEv[String(ev)]; if (!list || list.length === 0) return false; var args = []; @@ -157,15 +189,6 @@ if (c) w.code = c; p.emit("warning", w); }; - p.exit = function(code) { - var c = code !== undefined ? Number(code) : (p.exitCode !== undefined ? Number(p.exitCode) : 0); - p.exitCode = c; - p.emit("exit", c); - var err = new Error("process.exit(" + c + ")"); - err.code = "ERR_PROCESS_EXIT"; - err.exitCode = c; - throw err; - }; } if (!globalThis.navigator) { diff --git a/crates/slice-engine/src/preludes/js_preludes.rs b/crates/slice-engine/src/preludes/js_preludes.rs index d51c888..e8f1f30 100644 --- a/crates/slice-engine/src/preludes/js_preludes.rs +++ b/crates/slice-engine/src/preludes/js_preludes.rs @@ -614,7 +614,14 @@ const SHARD_PATTERNS: &[(&str, ShardTriggerAction)] = &[ "process.removeAllListeners", ShardTriggerAction::Mask(1 << 18), ), + ("process.addListener", ShardTriggerAction::Mask(1 << 18)), + ("process.listenerCount", ShardTriggerAction::Mask(1 << 18)), + ("unhandledRejection", ShardTriggerAction::Mask(1 << 18)), + ("uncaughtException", ShardTriggerAction::Mask(1 << 18)), + ("beforeExit", ShardTriggerAction::Mask(1 << 18)), ("node:process", ShardTriggerAction::Mask(1 << 18)), + ("\"process\"", ShardTriggerAction::BuiltinMask(1 << 18)), + ("'process'", ShardTriggerAction::BuiltinMask(1 << 18)), // Pre-fused `node:*` builtin module triggers (`19..=28`) ("node:crypto", ShardTriggerAction::BuiltinMask(1 << 19)), ("\"crypto\"", ShardTriggerAction::BuiltinMask(1 << 19)), diff --git a/crates/slice-engine/src/runtime/boa_realm.rs b/crates/slice-engine/src/runtime/boa_realm.rs index 5851e30..7f633ad 100644 --- a/crates/slice-engine/src/runtime/boa_realm.rs +++ b/crates/slice-engine/src/runtime/boa_realm.rs @@ -100,6 +100,42 @@ thread_local! { const { std::cell::RefCell::new(Vec::new()) }; pub(crate) static BOA_DIRTY_SUBSYSTEMS: std::cell::Cell = const { std::cell::Cell::new(0) }; + pub(crate) static BOA_PROCESS_EXIT: std::cell::Cell> = + const { std::cell::Cell::new(None) }; + pub(crate) static BOA_LAST_EXIT_CODE: std::cell::Cell> = + const { std::cell::Cell::new(None) }; + pub(crate) static BOA_UNHANDLED_REJECTIONS: std::cell::RefCell> = + const { std::cell::RefCell::new(Vec::new()) }; + pub(crate) static BOA_UNCAUGHT_ERROR: std::cell::RefCell> = + const { std::cell::RefCell::new(None) }; +} + +pub(crate) struct SliceHostHooks; + +impl boa_engine::context::HostHooks for SliceHostHooks { + fn promise_rejection_tracker( + &self, + promise: &boa_engine::object::JsObject, + operation: boa_engine::builtins::promise::OperationType, + _context: &mut boa_engine::Context, + ) { + let upcast = promise.clone().upcast(); + match operation { + boa_engine::builtins::promise::OperationType::Reject => { + BOA_UNHANDLED_REJECTIONS.with(|list| { + let mut b = list.borrow_mut(); + if !b.contains(&upcast) { + b.push(upcast); + } + }); + } + boa_engine::builtins::promise::OperationType::Handle => { + BOA_UNHANDLED_REJECTIONS.with(|list| { + list.borrow_mut().retain(|p| p != &upcast); + }); + } + } + } } #[inline] @@ -245,19 +281,32 @@ impl boa_engine::job::JobExecutor for SliceJobExecutor { self: std::rc::Rc, context: &mut boa_engine::Context, ) -> boa_engine::JsResult<()> { + if BOA_PROCESS_EXIT.with(|e| e.get().is_some()) { + self.promise_jobs.borrow_mut().clear(); + return Ok(()); + } loop { let next_job = self.promise_jobs.borrow_mut().pop_front(); if let Some((job, snap_opt)) = next_job { - if let Some(snap) = snap_opt { + let res = if let Some(snap) = snap_opt { let prev = BOA_ALS_SNAP.with(|s| s.replace(snap)); - let res = job.call(context); + let r = job.call(context); BOA_ALS_SNAP.with(|s| *s.borrow_mut() = prev); - res?; + r } else { - job.call(context)?; + job.call(context) + }; + if BOA_PROCESS_EXIT.with(|e| e.get().is_some()) { + self.promise_jobs.borrow_mut().clear(); + return Ok(()); } + res?; } else { self.fallback.clone().run_jobs(context)?; + if BOA_PROCESS_EXIT.with(|e| e.get().is_some()) { + self.promise_jobs.borrow_mut().clear(); + return Ok(()); + } if self.promise_jobs.borrow().is_empty() { break; } @@ -364,6 +413,7 @@ pub(crate) fn release_warm_boa_realm(realm: WarmBoaRealm) { pub(crate) fn create_bare_boa_realm() -> WarmBoaRealm { let mut ctx = boa_engine::context::ContextBuilder::new() .job_executor(std::rc::Rc::new(SliceJobExecutor::new())) + .host_hooks(std::rc::Rc::new(SliceHostHooks)) .build() .unwrap_or_else(|_| boa_engine::Context::default()); register_slice_native_host_functions(&mut ctx); @@ -493,6 +543,10 @@ pub(crate) struct SavedIsolateContextGuard { pub(crate) saved_http_res: Option, pub(crate) saved_wait_until_count: usize, pub(crate) saved_dirty_subsystems: u8, + pub(crate) saved_process_exit: Option, + pub(crate) saved_last_exit_code: Option, + pub(crate) saved_unhandled_rejections: Vec, + pub(crate) saved_uncaught_error: Option, pub(crate) saved_ev_loop: Option, } @@ -554,6 +608,11 @@ impl SavedIsolateContextGuard { let saved_http_res = BOA_HTTP_RES.with(|res| res.borrow_mut().take()); let saved_wait_until_count = BOA_WAIT_UNTIL_COUNT.with(|c| c.replace(0)); let saved_dirty_subsystems = BOA_DIRTY_SUBSYSTEMS.with(|d| d.replace(0)); + let saved_process_exit = BOA_PROCESS_EXIT.with(|e| e.replace(None)); + let saved_last_exit_code = BOA_LAST_EXIT_CODE.with(|e| e.replace(None)); + let saved_unhandled_rejections = + BOA_UNHANDLED_REJECTIONS.with(|u| std::mem::take(&mut *u.borrow_mut())); + let saved_uncaught_error = BOA_UNCAUGHT_ERROR.with(|e| e.borrow_mut().take()); let saved_ev_loop = crate::event_loop::save_and_reset_event_loop_state(); Self { @@ -580,6 +639,10 @@ impl SavedIsolateContextGuard { saved_http_res, saved_wait_until_count, saved_dirty_subsystems, + saved_process_exit, + saved_last_exit_code, + saved_unhandled_rejections, + saved_uncaught_error, saved_ev_loop, } } @@ -592,6 +655,11 @@ impl Drop for SavedIsolateContextGuard { } let child_dirty = BOA_DIRTY_SUBSYSTEMS.with(|d| d.get()); BOA_DIRTY_SUBSYSTEMS.with(|d| d.set(self.saved_dirty_subsystems)); + BOA_PROCESS_EXIT.with(|e| e.set(self.saved_process_exit)); + BOA_LAST_EXIT_CODE.with(|e| e.set(self.saved_last_exit_code)); + BOA_UNHANDLED_REJECTIONS + .with(|u| *u.borrow_mut() = std::mem::take(&mut self.saved_unhandled_rejections)); + BOA_UNCAUGHT_ERROR.with(|e| *e.borrow_mut() = self.saved_uncaught_error.take()); BOA_STDOUT.with(|out| *out.borrow_mut() = std::mem::take(&mut self.saved_stdout)); BOA_VIOLATION.with(|v| *v.borrow_mut() = self.saved_violation.take()); BOA_ACTIVE_TENANT.with(|t| *t.borrow_mut() = std::mem::take(&mut self.saved_tenant)); diff --git a/crates/slice-engine/src/runtime/event_loop.rs b/crates/slice-engine/src/runtime/event_loop.rs index 4e0cc07..8795803 100644 --- a/crates/slice-engine/src/runtime/event_loop.rs +++ b/crates/slice-engine/src/runtime/event_loop.rs @@ -4,8 +4,9 @@ use crate::boa_realm::{ mark_subsystem_dirty, BOA_ACTIVE_TENANT, BOA_ALS_ACTIVE, BOA_ALS_SNAP, BOA_DET_STATE, - BOA_DIRTY_SUBSYSTEMS, BOA_EGRESS_SECRETS, BOA_KV_MEM_BUDGET, BOA_NET_HOSTS, BOA_VIOLATION, - DIRTY_ALS, DIRTY_EVENT_LOOP, DIRTY_SOCKETS, DIRTY_WORKERS, + BOA_DIRTY_SUBSYSTEMS, BOA_EGRESS_SECRETS, BOA_KV_MEM_BUDGET, BOA_LAST_EXIT_CODE, BOA_NET_HOSTS, + BOA_PROCESS_EXIT, BOA_UNCAUGHT_ERROR, BOA_UNHANDLED_REJECTIONS, BOA_VIOLATION, DIRTY_ALS, + DIRTY_EVENT_LOOP, DIRTY_SOCKETS, DIRTY_WORKERS, }; use crate::outbound_http::{ execute_outbound_fetch_with_redirects, outbound_pool_reuse_count, record_outbound_pool_reuses, @@ -503,6 +504,395 @@ fn settle_completed_fetch( } } +pub(crate) fn format_uncaught_js_error(err: &boa_engine::JsError, ctx: &mut Context) -> String { + let raw_err = format!("Uncaught {}", err); + let mut mapped = crate::boa_realm::map_error_with_active_source(&raw_err); + if mapped == raw_err { + if let Some((first_line, rest)) = mapped.split_once('\n') { + if let Some(open) = first_line.rfind(" (unknown at ") { + let loc_paren = &first_line[open + 1..]; + if rest.contains(loc_paren) { + mapped = format!("{}\n{}", &first_line[..open], rest); + } + } + } + if let Some(opaque) = err.as_opaque() { + if let Some(obj) = opaque.as_object() { + if let Ok(stack_val) = obj.get(js_string!("stack"), ctx) { + if let Some(s) = stack_val.as_string() { + let st = s.to_std_string_escaped(); + for line in st.lines() { + let trimmed = line.trim(); + if trimmed.starts_with("at ") && !trimmed.contains("") { + if let Some(open) = trimmed.rfind('(') { + if let Some(close) = trimmed[open + 1..].find(')') { + let loc = &trimmed[open + 1..open + 1 + close]; + if loc.contains(':') && !raw_err.contains(loc) { + mapped = format!("{} ({})", raw_err, loc); + break; + } + } + } else if let Some(loc) = trimmed.strip_prefix("at ") { + if loc.contains(':') + && !loc.contains(' ') + && !raw_err.contains(loc) + { + mapped = format!("{} ({})", raw_err, loc); + break; + } + } + } + } + } + } + } + } + } + mapped +} + +pub(crate) fn handle_uncaught_js_error(err: boa_engine::JsError, ctx: &mut Context) -> bool { + if BOA_PROCESS_EXIT.with(|e| e.get().is_some()) { + return true; + } + let msg = err.to_string(); + if msg.starts_with("__SLICE_PROCESS_EXIT__:") { + return true; + } + if msg.contains("RuntimeLimitError") + || msg.contains("iteration loops") + || msg.contains("loop iteration limit") + || msg.contains("Maximum call stack") + || msg.contains("recursion limit") + { + let tenant_id = BOA_ACTIVE_TENANT.with(|t| t.borrow().clone()); + BOA_VIOLATION.with(|v| { + if v.borrow().is_none() { + *v.borrow_mut() = Some(SandboxViolation::CpuFuelExhausted { + tenant_id, + limit: 0, + }); + } + }); + return false; + } + + let formatted = format_uncaught_js_error(&err, ctx); + let err_val = err + .into_opaque(ctx) + .unwrap_or_else(|e| JsValue::from(js_string!(e.to_string()))); + + if let Some(proc_obj) = ctx + .global_object() + .get(js_string!("process"), ctx) + .ok() + .and_then(|v| v.as_object()) + { + let has_listeners = proc_obj + .get(js_string!("_hasListeners"), ctx) + .ok() + .and_then(|v| v.as_object()) + .filter(|f| f.is_callable()) + .and_then(|f| { + f.call(&JsValue::from(proc_obj.clone()), &[], ctx) + .ok() + .map(|v| v.to_boolean()) + }) + .unwrap_or(false); + + if has_listeners { + if let Some(emit_fn) = proc_obj + .get(js_string!("emit"), ctx) + .ok() + .and_then(|v| v.as_object()) + .filter(|f| f.is_callable()) + { + let _ = emit_fn.call( + &JsValue::from(proc_obj.clone()), + &[ + JsValue::from(js_string!("uncaughtExceptionMonitor")), + err_val.clone(), + JsValue::from(js_string!("uncaughtException")), + ], + ctx, + ); + if BOA_PROCESS_EXIT.with(|e| e.get().is_some()) { + return true; + } + match emit_fn.call( + &JsValue::from(proc_obj), + &[ + JsValue::from(js_string!("uncaughtException")), + err_val, + JsValue::from(js_string!("uncaughtException")), + ], + ctx, + ) { + Ok(v) if v.to_boolean() => { + let _ = run_jobs_and_check_rejections(ctx); + return BOA_UNCAUGHT_ERROR.with(|e| e.borrow().is_none()) + && BOA_VIOLATION.with(|v| v.borrow().is_none()); + } + Err(handler_err) => { + if BOA_PROCESS_EXIT.with(|e| e.get().is_some()) { + return true; + } + let h_formatted = format_uncaught_js_error(&handler_err, ctx); + BOA_UNCAUGHT_ERROR.with(|e| *e.borrow_mut() = Some(h_formatted)); + BOA_LAST_EXIT_CODE.with(|c| c.set(Some(1))); + return false; + } + Ok(_) => {} + } + } + } + } + + BOA_UNCAUGHT_ERROR.with(|e| *e.borrow_mut() = Some(formatted)); + BOA_LAST_EXIT_CODE.with(|c| c.set(Some(1))); + false +} + +pub(crate) fn drain_unhandled_rejections(ctx: &mut Context) -> bool { + while BOA_UNHANDLED_REJECTIONS.with(|u| !u.borrow().is_empty()) { + if BOA_PROCESS_EXIT.with(|e| e.get().is_some()) + || BOA_UNCAUGHT_ERROR.with(|e| e.borrow().is_some()) + || BOA_VIOLATION.with(|v| v.borrow().is_some()) + { + BOA_UNHANDLED_REJECTIONS.with(|u| u.borrow_mut().clear()); + return false; + } + let pending = BOA_UNHANDLED_REJECTIONS.with(|u| std::mem::take(&mut *u.borrow_mut())); + let proc_obj = ctx + .global_object() + .get(js_string!("process"), ctx) + .ok() + .and_then(|v| v.as_object()); + let has_ur_listener = proc_obj + .as_ref() + .and_then(|p| { + p.get(js_string!("_hasListeners"), ctx) + .ok() + .and_then(|v| v.as_object()) + .filter(|f| f.is_callable()) + .and_then(|f| { + f.call( + &JsValue::from(p.clone()), + &[JsValue::from(js_string!("unhandledRejection"))], + ctx, + ) + .ok() + .map(|v| v.to_boolean()) + }) + }) + .unwrap_or(false); + + for promise_obj in pending { + let Some(reason) = + boa_engine::object::builtins::JsPromise::from_object(promise_obj.clone()) + .ok() + .and_then(|jp| match jp.state() { + boa_engine::builtins::promise::PromiseState::Rejected(r) => Some(r), + _ => None, + }) + else { + continue; + }; + + if has_ur_listener { + if let Some(ref p) = proc_obj { + if let Some(emit_fn) = p + .get(js_string!("emit"), ctx) + .ok() + .and_then(|v| v.as_object()) + .filter(|f| f.is_callable()) + { + match emit_fn.call( + &JsValue::from(p.clone()), + &[ + JsValue::from(js_string!("unhandledRejection")), + reason.clone(), + JsValue::from(promise_obj.clone()), + ], + ctx, + ) { + Ok(v) if v.to_boolean() => { + if let Err(e) = ctx.run_jobs() { + if !handle_uncaught_js_error(e, ctx) { + return false; + } + } + if BOA_PROCESS_EXIT.with(|e| e.get().is_some()) { + return false; + } + continue; + } + Err(handler_err) => { + if !handle_uncaught_js_error(handler_err, ctx) { + return false; + } + if BOA_PROCESS_EXIT.with(|e| e.get().is_some()) { + return false; + } + continue; + } + Ok(_) => {} + } + } + } + } + + let reason_str = reason + .to_string(ctx) + .map(|s| s.to_std_string_escaped()) + .unwrap_or_else(|_| reason.display().to_string()); + BOA_UNCAUGHT_ERROR + .with(|e| *e.borrow_mut() = Some(format!("Uncaught (in promise) {}", reason_str))); + BOA_LAST_EXIT_CODE.with(|c| c.set(Some(1))); + BOA_UNHANDLED_REJECTIONS.with(|u| u.borrow_mut().clear()); + return false; + } + } + true +} + +#[inline] +pub(crate) fn run_jobs_and_check_rejections(ctx: &mut Context) -> bool { + if let Err(err) = ctx.run_jobs() { + if !handle_uncaught_js_error(err, ctx) { + return false; + } + } + if BOA_PROCESS_EXIT.with(|e| e.get().is_some()) { + return false; + } + drain_unhandled_rejections(ctx) +} + +fn try_emit_before_exit(ctx: &mut Context, before_exit_count: &mut usize) -> bool { + if *before_exit_count >= 64 + || BOA_PROCESS_EXIT.with(|e| e.get().is_some()) + || BOA_UNCAUGHT_ERROR.with(|e| e.borrow().is_some()) + || BOA_VIOLATION.with(|v| v.borrow().is_some()) + { + return false; + } + let Some(proc_obj) = ctx + .global_object() + .get(js_string!("process"), ctx) + .ok() + .and_then(|v| v.as_object()) + else { + return false; + }; + let has_be = proc_obj + .get(js_string!("_hasListeners"), ctx) + .ok() + .and_then(|v| v.as_object()) + .filter(|f| f.is_callable()) + .and_then(|f| { + f.call( + &JsValue::from(proc_obj.clone()), + &[JsValue::from(js_string!("beforeExit"))], + ctx, + ) + .ok() + .map(|v| v.to_boolean()) + }) + .unwrap_or(false); + if !has_be { + return false; + } + *before_exit_count += 1; + let code = proc_obj + .get(js_string!("exitCode"), ctx) + .ok() + .filter(|v| !v.is_undefined() && !v.is_null()) + .and_then(|v| v.to_i32(ctx).ok()) + .unwrap_or(0); + if let Some(emit_fn) = proc_obj + .get(js_string!("emit"), ctx) + .ok() + .and_then(|v| v.as_object()) + .filter(|f| f.is_callable()) + { + if let Err(err) = emit_fn.call( + &JsValue::from(proc_obj), + &[JsValue::from(js_string!("beforeExit")), JsValue::from(code)], + ctx, + ) { + if !handle_uncaught_js_error(err, ctx) { + return false; + } + } + } + if !run_jobs_and_check_rejections(ctx) { + return false; + } + has_pending_event_loop_work() +} + +pub(crate) fn finalize_and_reset_process_state(ctx: &mut Context) { + let Some(proc_obj) = ctx + .global_object() + .get(js_string!("process"), ctx) + .ok() + .and_then(|v| v.as_object()) + else { + return; + }; + let already_exited = proc_obj + .get(js_string!("_exited"), ctx) + .ok() + .and_then(|v| v.as_boolean()) + .unwrap_or(false); + if !already_exited { + let mut code = proc_obj + .get(js_string!("exitCode"), ctx) + .ok() + .filter(|v| !v.is_undefined() && !v.is_null()) + .and_then(|v| v.to_i32(ctx).ok()) + .or_else(|| BOA_LAST_EXIT_CODE.with(|e| e.get())) + .unwrap_or(0); + let _ = proc_obj.set(js_string!("_exited"), JsValue::from(true), false, ctx); + if let Some(emit_fn) = proc_obj + .get(js_string!("emit"), ctx) + .ok() + .and_then(|v| v.as_object()) + .filter(|f| f.is_callable()) + { + let _ = emit_fn.call( + &JsValue::from(proc_obj.clone()), + &[JsValue::from(js_string!("exit")), JsValue::from(code)], + ctx, + ); + } + if let Some(inner_code) = BOA_PROCESS_EXIT.with(|e| e.get()) { + code = inner_code; + } else if let Ok(ec_val) = proc_obj.get(js_string!("exitCode"), ctx) { + if !ec_val.is_undefined() && !ec_val.is_null() { + if let Ok(updated) = ec_val.to_i32(ctx) { + code = updated; + } + } + } + if code != 0 || BOA_LAST_EXIT_CODE.with(|e| e.get().is_some()) { + BOA_LAST_EXIT_CODE.with(|e| e.set(Some(code))); + } + if crate::workers::is_worker_thread() && code != 0 { + crate::workers::worker_self_set_listening(false, Some(code)); + } + } + if let Some(reset_fn) = proc_obj + .get(js_string!("_resetProcessState"), ctx) + .ok() + .and_then(|v| v.as_object()) + .filter(|f| f.is_callable()) + { + let _ = reset_fn.call(&JsValue::from(proc_obj), &[], ctx); + } +} + /// Drives the Phase N-2 Event Loop to completion: /// 1. Drains all ECMAScript Promise microtasks & `queueMicrotask` jobs (`ctx.run_jobs()`). /// 2. Dispatches queued outbound `fetch()` calls (executing single un-raced fetches inline for zero @@ -512,8 +902,12 @@ fn settle_completed_fetch( /// 4. In deterministic mode (`--deterministic` or `slice test`), warps `BOA_DET_STATE` virtual time /// forward to the next timer deadline in `0 ns` without sleeping. pub(crate) fn drive_event_loop_to_completion(ctx: &mut Context) { - let _ = ctx.run_jobs(); - if !has_pending_event_loop_work() { + let mut before_exit_count: usize = 0; + if !run_jobs_and_check_rejections(ctx) { + clear_event_loop_state(); + return; + } + if !has_pending_event_loop_work() && !try_emit_before_exit(ctx, &mut before_exit_count) { return; } @@ -522,6 +916,8 @@ pub(crate) fn drive_event_loop_to_completion(ctx: &mut Context) { loop { if BOA_VIOLATION.with(|v| v.borrow().is_some()) + || BOA_PROCESS_EXIT.with(|e| e.get().is_some()) + || BOA_UNCAUGHT_ERROR.with(|e| e.borrow().is_some()) || crate::workers::is_current_worker_terminated() { clear_event_loop_state(); @@ -529,12 +925,17 @@ pub(crate) fn drive_event_loop_to_completion(ctx: &mut Context) { } // 1. Drain microtasks and any non-blocking worker messages first - let _ = ctx.run_jobs(); + if !run_jobs_and_check_rejections(ctx) { + clear_event_loop_state(); + return; + } let did_parent_w = crate::workers::poll_parent_worker_events_and_dispatch(ctx, None); let did_self_w = crate::workers::poll_worker_self_inbox_and_dispatch(ctx, None); if did_parent_w || did_self_w { idle_worker_polls = 0; if BOA_VIOLATION.with(|v| v.borrow().is_some()) + || BOA_PROCESS_EXIT.with(|e| e.get().is_some()) + || BOA_UNCAUGHT_ERROR.with(|e| e.borrow().is_some()) || crate::workers::is_current_worker_terminated() { clear_event_loop_state(); @@ -748,12 +1149,24 @@ pub(crate) fn drive_event_loop_to_completion(ctx: &mut Context) { let prev_als = als_snap .as_ref() .map(|snap| BOA_ALS_SNAP.with(|s| s.replace(snap.clone()))); - let _ = cb.call(&JsValue::undefined(), &args, ctx); + let call_res = cb.call(&JsValue::undefined(), &args, ctx); if let Some(prev) = prev_als { BOA_ALS_SNAP.with(|s| *s.borrow_mut() = prev); } - let _ = ctx.run_jobs(); - if BOA_VIOLATION.with(|v| v.borrow().is_some()) { + if let Err(err) = call_res { + if !handle_uncaught_js_error(err, ctx) { + clear_event_loop_state(); + return; + } + } + if !run_jobs_and_check_rejections(ctx) { + clear_event_loop_state(); + return; + } + if BOA_VIOLATION.with(|v| v.borrow().is_some()) + || BOA_PROCESS_EXIT.with(|e| e.get().is_some()) + || BOA_UNCAUGHT_ERROR.with(|e| e.borrow().is_some()) + { clear_event_loop_state(); return; } @@ -782,6 +1195,9 @@ pub(crate) fn drive_event_loop_to_completion(ctx: &mut Context) { } let has_worker_work = crate::workers::has_pending_worker_work(); if in_flight_count == 0 && next_timer_deadline.is_none() && !has_worker_work { + if try_emit_before_exit(ctx, &mut before_exit_count) { + continue; + } ACTIVE_ASYNC_ITEMS.with(|c| c.set(0)); break; } diff --git a/crates/slice-engine/src/runtime/workers.rs b/crates/slice-engine/src/runtime/workers.rs index e71982e..cb98476 100644 --- a/crates/slice-engine/src/runtime/workers.rs +++ b/crates/slice-engine/src/runtime/workers.rs @@ -807,7 +807,9 @@ fn run_pooled_worker_job(job: WorkerJob, self_job_tx: &Sender) { let exec_res = child_isolate.execute_bytecode(&job.bytecode, None); let was_terminated = terminated_clone.load(Ordering::Acquire); - let explicit_code = WORKER_SELF_ENV.with(|env| env.borrow().explicit_exit_code); + let explicit_code = WORKER_SELF_ENV + .with(|env| env.borrow().explicit_exit_code) + .or_else(|| child_isolate.last_exit_code()); // Scrub thread-local worker state and return this thread to `IDLE_WORKER_POOL` // BEFORE sending `Exit` to the parent isolate so sequential `await worker` loops @@ -1029,8 +1031,14 @@ pub(crate) fn poll_worker_self_inbox_and_dispatch( let arg = JsValue::from(boa_engine::js_string!(msg_json)); let call_res = dispatch_fn.call(&JsValue::undefined(), &[arg], ctx); let _ = ctx.run_jobs(); + if crate::boa_realm::BOA_PROCESS_EXIT.with(|e| e.get().is_some()) { + return true; + } if let Err(err) = call_res { let msg = err.to_string(); + if msg.starts_with("__SLICE_PROCESS_EXIT__:") { + return true; + } if msg.contains("RuntimeLimitError") || msg.contains("iteration loops") || msg.contains("loop iteration limit") diff --git a/crates/slice-engine/tests/async_webcrypto_and_node_tests.rs b/crates/slice-engine/tests/async_webcrypto_and_node_tests.rs index da9c9fa..037284a 100644 --- a/crates/slice-engine/tests/async_webcrypto_and_node_tests.rs +++ b/crates/slice-engine/tests/async_webcrypto_and_node_tests.rs @@ -888,31 +888,32 @@ proc.emitWarning("experimental CLI flag", "CustomWarning", "WARN_01"); let exitEmitted = -1; proc.once("exit", (code: number) => { exitEmitted = code; + console.log(JSON.stringify({ + argv0: proc.argv0, + execPath: proc.execPath, + ppid: proc.ppid, + isTTYOut: proc.stdout.isTTY, + isTTYIn: proc.stdin.isTTY, + hrValid: Array.isArray(diff) && diff.length === 2 && typeof b0 === "bigint" && b1 >= b0, + memValid: mem.rss > 0 && mem.heapUsed > 0 && mem.heapTotal >= mem.heapUsed && rssVal === mem.rss, + uptimeValid: typeof proc.uptime() === "number" && proc.uptime() >= 0, + warningName, + warningMsg, + exitEmitted, + exitCaught, + exitCode: proc.exitCode + })); }); let exitCaught = false; +proc.stdout.write("stdout-line-1\n"); +proc.stderr.write("stderr-line-2\n"); try { proc.exit(7); } catch (e: any) { - exitCaught = (e && e.code === "ERR_PROCESS_EXIT" && e.exitCode === 7); + exitCaught = true; + console.log("UNREACHABLE_AFTER_EXIT"); } - -proc.stdout.write("stdout-line-1\n"); -proc.stderr.write("stderr-line-2\n"); -console.log(JSON.stringify({ - argv0: proc.argv0, - execPath: proc.execPath, - ppid: proc.ppid, - isTTYOut: proc.stdout.isTTY, - isTTYIn: proc.stdin.isTTY, - hrValid: Array.isArray(diff) && diff.length === 2 && typeof b0 === "bigint" && b1 >= b0, - memValid: mem.rss > 0 && mem.heapUsed > 0 && mem.heapTotal >= mem.heapUsed && rssVal === mem.rss, - uptimeValid: typeof proc.uptime() === "number" && proc.uptime() >= 0, - warningName, - warningMsg, - exitEmitted, - exitCaught, - exitCode: proc.exitCode -})); +console.log("UNREACHABLE_AFTER_TRY"); "#; let bundled = slice_cas::strip_typescript_and_normalize(script, true); @@ -926,6 +927,8 @@ console.log(JSON.stringify({ ); iso.set_deterministic_seed(Some(20261106)); let m = iso.execute_bytecode(&bc, None).unwrap(); + assert_eq!(m.exit_code, Some(7)); + assert_eq!(m.stdout.len(), 3); assert_eq!(m.stdout[0], "stdout-line-1"); assert_eq!(m.stdout[1], "stderr-line-2"); let out: serde_json::Value = serde_json::from_str(&m.stdout[2]).unwrap(); @@ -940,7 +943,7 @@ console.log(JSON.stringify({ assert_eq!(out["warningName"], "CustomWarning"); assert_eq!(out["warningMsg"], "experimental CLI flag"); assert_eq!(out["exitEmitted"], 7); - assert_eq!(out["exitCaught"], true); + assert_eq!(out["exitCaught"], false); assert_eq!(out["exitCode"], 7); // Also verify DLP egress firewall catches secrets written to `process.stdout.write` diff --git a/crates/slice-engine/tests/workers_sockets_and_pillars_tests.rs b/crates/slice-engine/tests/workers_sockets_and_pillars_tests.rs index 9b7ebdb..3745774 100644 --- a/crates/slice-engine/tests/workers_sockets_and_pillars_tests.rs +++ b/crates/slice-engine/tests/workers_sockets_and_pillars_tests.rs @@ -906,3 +906,492 @@ fn test_pillar5_sliceboa_payload_compaction_and_multiline_template_preservation( r#"{"rxOk":true,"strOk":true,"lines":["line1"," indented_line2"," indented_line3"]}"# ); } + +#[test] +fn test_wave1_json_parse_ecma404_float_precision_surrogates_nesting_reviver_and_perf() { + let js = r#" +const exact993 = JSON.parse("9007199254740993.0"); +const exact994 = JSON.parse("9007199254740994.0"); +const exact1e23 = JSON.parse("1e23"); +const subnormal = JSON.parse("5e-324"); +const maxVal = JSON.parse("1.7976931348623157e308"); +const posInf = JSON.parse("1e400"); +const negInf = JSON.parse("-1e400"); +const posInf309 = JSON.parse("1e309"); +const negZeroInt = JSON.parse("-0"); +const negZeroFloat = JSON.parse("-0.0"); +const negZeroExp = JSON.parse("-1e-400"); +const posZeroExp = JSON.parse("1e-400"); + +// Lone surrogates, reversed pairs, and valid surrogate pairs +const loneHigh = JSON.parse('"\\ud800"'); +const loneLow = JSON.parse('"\\udfff"'); +const reversedPair = JSON.parse('"\\udc00\\ud800"'); +const highThenAscii = JSON.parse('"\\ud800A"'); +const validPair = JSON.parse('"\\ud83d\\ude00"'); +const surrogateObj = JSON.parse('{"\\ud800":"\\udfff"}'); + +// Deep nesting (300 levels of arrays, 250 levels of objects) +const deepArrJson = "[".repeat(300) + "42" + "]".repeat(300); +let curArr = JSON.parse(deepArrJson); +let arrDepth = 0; +while (Array.isArray(curArr)) { + arrDepth++; + curArr = curArr[0]; +} + +const deepObjJson = '{"k":'.repeat(250) + '"leaf"' + '}'.repeat(250); +let curObj = JSON.parse(deepObjJson); +let objDepth = 0; +while (curObj && typeof curObj === "object") { + objDepth++; + curObj = curObj.k; +} + +// Excessive nesting (> 512) must throw SyntaxError cleanly without stack overflow +let excessiveThrew = false; +try { + JSON.parse("[".repeat(600) + "1" + "]".repeat(600)); +} catch (e) { + excessiveThrew = e instanceof SyntaxError; +} + +// Reviver with context.source and JSON.rawJSON +const reviverSources = {}; +const revived = JSON.parse('{"big":9007199254740993,"inf":1e400,"nested":{"s":"\\ud800"}}', (k, v, ctx) => { + if (ctx && typeof ctx.source === "string") { + reviverSources[k] = ctx.source; + } else if (k === "nested") { + reviverSources[k] = ctx ? typeof ctx.source : "no-ctx"; + } + return v; +}); + +// Throughput benchmark on 2,000-record mixed payload +const records = []; +for (let i = 0; i < 200; i++) { + records.push({ + id: i, + score: i * 1.25, + big: 9007199254740992, + name: "record_" + i, + escaped: "line1\\nline2\\t" + i, + active: (i & 1) === 0, + nil: null, + tags: ["alpha", "beta", "gamma"] + }); +} +const benchPayload = JSON.stringify(records); +const t0 = Date.now(); +let benchSum = 0; +for (let it = 0; it < 20; it++) { + const parsed = JSON.parse(benchPayload); + benchSum += parsed.length + parsed[199].id; +} +const elapsedMs = Date.now() - t0; + +console.log(JSON.stringify({ + exact993Ok: exact993 === 9007199254740992, + exact994Ok: exact994 === 9007199254740994, + exact1e23Ok: exact1e23 === 1e23, + subnormalOk: subnormal === Number.MIN_VALUE, + maxValOk: maxVal === Number.MAX_VALUE, + posInfOk: posInf === Infinity && posInf309 === Infinity, + negInfOk: negInf === -Infinity, + negZeroOk: Object.is(negZeroInt, -0) && Object.is(negZeroFloat, -0) && Object.is(negZeroExp, -0) && Object.is(posZeroExp, 0), + loneHighOk: loneHigh.length === 1 && loneHigh.charCodeAt(0) === 0xD800, + loneLowOk: loneLow.length === 1 && loneLow.charCodeAt(0) === 0xDFFF, + reversedPairOk: reversedPair.length === 2 && reversedPair.charCodeAt(0) === 0xDC00 && reversedPair.charCodeAt(1) === 0xD800, + highThenAsciiOk: highThenAscii.length === 2 && highThenAscii.charCodeAt(0) === 0xD800 && highThenAscii.charCodeAt(1) === 65, + validPairOk: validPair.length === 2 && validPair.codePointAt(0) === 0x1F600, + surrogateObjOk: surrogateObj["\ud800"].charCodeAt(0) === 0xDFFF, + arrDepth, + arrLeaf: curArr, + objDepth, + objLeaf: curObj, + excessiveThrew, + reviverBigSource: reviverSources.big, + reviverInfSource: reviverSources.inf, + reviverStrSource: reviverSources.s, + reviverNestedSource: reviverSources.nested, + revivedInfIsInf: revived.inf === Infinity, + benchSum, + elapsedMs +})); +"#; + + let bc = crate::compile_source_to_bytecode(js); + let mut isolate = SliceIsolate::spawn( + "wave1-json-test", + 1, + CapabilityToken::pure_compute(), + 50_000_000, + 32 * 1024 * 1024, + ); + let m = isolate.execute_bytecode(&bc, None).unwrap(); + let out: serde_json::Value = serde_json::from_str(&m.stdout[0]).unwrap(); + assert_eq!(out["exact993Ok"], true); + assert_eq!(out["exact994Ok"], true); + assert_eq!(out["exact1e23Ok"], true); + assert_eq!(out["subnormalOk"], true); + assert_eq!(out["maxValOk"], true); + assert_eq!(out["posInfOk"], true); + assert_eq!(out["negInfOk"], true); + assert_eq!(out["negZeroOk"], true); + assert_eq!(out["loneHighOk"], true); + assert_eq!(out["loneLowOk"], true); + assert_eq!(out["reversedPairOk"], true); + assert_eq!(out["highThenAsciiOk"], true); + assert_eq!(out["validPairOk"], true); + assert_eq!(out["surrogateObjOk"], true); + assert_eq!(out["arrDepth"], 300); + assert_eq!(out["arrLeaf"], 42); + assert_eq!(out["objDepth"], 250); + assert_eq!(out["objLeaf"], "leaf"); + assert_eq!(out["excessiveThrew"], true); + assert_eq!(out["reviverBigSource"], "9007199254740993"); + assert_eq!(out["reviverInfSource"], "1e400"); + assert_eq!(out["reviverStrSource"], "\"\\ud800\""); + assert_eq!(out["reviverNestedSource"], "undefined"); + assert_eq!(out["revivedInfIsInf"], true); + assert_eq!(out["benchSum"], 20 * (200 + 199)); +} + +#[test] +fn test_wave1_process_exit_lifecycle_unhandled_rejection_and_uncaught_exception() { + // 1. process.exit(42) inside try/catch/finally must NOT be caught by catch and must set exit_code = 42 + { + let js = r#" +let events = []; +process.on("exit", (code) => { + events.push("exit:" + code); + console.log(JSON.stringify(events)); +}); +process.on("beforeExit", (code) => { + events.push("beforeExit:" + code); +}); +try { + events.push("before-exit-call"); + process.exit(42); + events.push("after-exit-call"); +} catch (e) { + events.push("caught:" + e); +} +events.push("after-try"); +"#; + let bc = crate::compile_source_to_bytecode(js); + let mut iso = SliceIsolate::spawn( + "wave1-exit-try", + 1, + CapabilityToken::pure_compute(), + 5_000_000, + 16 * 1024 * 1024, + ); + let m = iso.execute_bytecode(&bc, None).unwrap(); + assert_eq!(m.exit_code, Some(42)); + assert_eq!(m.stdout, vec![r#"["before-exit-call","exit:42"]"#]); + } + + // 2. process.exit(19) inside a Promise.then microtask stops subsequent microtasks & timers + { + let js = r#" +let log = []; +process.on("exit", (code) => { + log.push("exit:" + code); + console.log(JSON.stringify(log)); +}); +setTimeout(() => { + log.push("timer-should-not-run"); +}, 0); +Promise.resolve().then(() => { + log.push("promise-1"); + process.exit(19); + log.push("promise-1-after"); +}).catch((e) => { + log.push("promise-catch-should-not-run"); +}); +Promise.resolve().then(() => { + log.push("promise-2-should-not-run"); +}); +"#; + let bc = crate::compile_source_to_bytecode(js); + let mut iso = SliceIsolate::spawn( + "wave1-exit-promise", + 1, + CapabilityToken::pure_compute(), + 5_000_000, + 16 * 1024 * 1024, + ); + let m = iso.execute_bytecode(&bc, None).unwrap(); + assert_eq!(m.exit_code, Some(19)); + assert_eq!(m.stdout, vec![r#"["promise-1","exit:19"]"#]); + } + + // 3. Natural exit: beforeExit can schedule additional work once, then exit fires and process.exitCode is honored + { + let js = r#" +let seq = []; +let scheduled = false; +process.exitCode = 5; +process.on("beforeExit", (code) => { + seq.push("beforeExit:" + code); + if (!scheduled) { + scheduled = true; + setTimeout(() => { + seq.push("timer-from-beforeExit"); + process.exitCode = 9; + }, 1); + } +}); +process.on("exit", (code) => { + seq.push("exit:" + code); + console.log(JSON.stringify(seq)); +}); +seq.push("sync-done"); +"#; + let bc = crate::compile_source_to_bytecode(js); + let mut iso = SliceIsolate::spawn( + "wave1-before-exit", + 1, + CapabilityToken::pure_compute(), + 5_000_000, + 16 * 1024 * 1024, + ); + iso.set_deterministic_seed(Some(42)); + let m = iso.execute_bytecode(&bc, None).unwrap(); + assert_eq!(m.exit_code, Some(9)); + assert_eq!( + m.stdout, + vec![r#"["sync-done","beforeExit:5","timer-from-beforeExit","beforeExit:9","exit:9"]"#] + ); + } + + // 4. Unhandled Promise rejection WITHOUT listener produces `Uncaught (in promise)` and exit_code = 1 + { + let js = r#" +Promise.reject(new Error("unhandled-async-boom")); +"#; + let bc = crate::compile_source_to_bytecode(js); + let mut iso = SliceIsolate::spawn( + "wave1-unhandled-rej", + 1, + CapabilityToken::pure_compute(), + 5_000_000, + 16 * 1024 * 1024, + ); + let m = iso.execute_bytecode(&bc, None).unwrap(); + assert_eq!(m.exit_code, Some(1)); + assert!( + m.return_value + .contains("Uncaught (in promise) Error: unhandled-async-boom"), + "unexpected return_value: {}", + m.return_value + ); + } + + // 5. Unhandled Promise rejection WITH `process.on('unhandledRejection')` listener is caught cleanly + // AND a rejection handled in the same microtask turn does NOT fire `unhandledRejection` + { + let js = r#" +const caughtReasons = []; +process.on("unhandledRejection", (reason, p) => { + caughtReasons.push(reason instanceof Error ? reason.message : String(reason)); +}); +const handled = Promise.reject(new Error("handled-in-turn")); +handled.catch(() => {}); +Promise.reject(new Error("intercepted-by-process")); +process.on("exit", (code) => { + console.log(JSON.stringify({ code, caughtReasons })); +}); +"#; + let bc = crate::compile_source_to_bytecode(js); + let mut iso = SliceIsolate::spawn( + "wave1-handled-rej", + 1, + CapabilityToken::pure_compute(), + 5_000_000, + 16 * 1024 * 1024, + ); + let m = iso.execute_bytecode(&bc, None).unwrap(); + assert_eq!(m.exit_code.unwrap_or(0), 0); + assert!( + !m.return_value.starts_with("Uncaught"), + "unexpected uncaught error: {}", + m.return_value + ); + assert_eq!( + m.stdout, + vec![r#"{"code":0,"caughtReasons":["intercepted-by-process"]}"#] + ); + } + + // 6. process.on('uncaughtException') and 'uncaughtExceptionMonitor' intercept synchronous & timer errors + { + let js = r#" +const log = []; +process.on("uncaughtExceptionMonitor", (err, origin) => { + log.push("monitor:" + err.message + ":" + origin); +}); +process.on("uncaughtException", (err, origin) => { + log.push("caught:" + err.message + ":" + origin); +}); +setTimeout(() => { + throw new Error("timer-boom"); +}, 1); +process.on("exit", (code) => { + console.log(JSON.stringify({ code, log })); +}); +throw new Error("sync-boom"); +"#; + let bc = crate::compile_source_to_bytecode(js); + let mut iso = SliceIsolate::spawn( + "wave1-uncaught-exc", + 1, + CapabilityToken::pure_compute(), + 5_000_000, + 16 * 1024 * 1024, + ); + iso.set_deterministic_seed(Some(99)); + let m = iso.execute_bytecode(&bc, None).unwrap(); + assert_eq!(m.exit_code.unwrap_or(0), 0); + assert!( + !m.return_value.starts_with("Uncaught"), + "unexpected uncaught error: {}", + m.return_value + ); + assert_eq!( + m.stdout, + vec![ + r#"{"code":0,"log":["monitor:sync-boom:uncaughtException","caught:sync-boom:uncaughtException","monitor:timer-boom:uncaughtException","caught:timer-boom:uncaughtException"]}"# + ] + ); + } +} + +#[test] +fn test_wave1_worker_threads_date_special_numbers_and_process_exit() { + let js = r#" +const { Worker, MessageChannel } = require("node:worker_threads"); + +async function runTests() { + // 1. Date, Invalid Date, NaN, Infinity, -Infinity across MessageChannel + const mc = new MessageChannel(); + const mcResult = await new Promise((resolve) => { + mc.port2.on("message", (msg) => { + mc.port1.close(); + mc.port2.close(); + resolve({ + dValid: msg.d instanceof Date && msg.d.getTime() === 1750000000123, + dEpoch: msg.epoch instanceof Date && msg.epoch.getTime() === 0, + dInvalid: msg.inv instanceof Date && Number.isNaN(msg.inv.getTime()), + nanOk: Number.isNaN(msg.nan), + posInfOk: msg.posInf === Infinity, + negInfOk: msg.negInf === -Infinity, + }); + }); + mc.port1.postMessage({ + d: new Date(1750000000123), + epoch: new Date(0), + inv: new Date(NaN), + nan: NaN, + posInf: Infinity, + negInf: -Infinity, + }); + }); + + // 2. Date & special numbers across workerData, worker.postMessage, parentPort.postMessage, + // AND process.exit(42) inside worker when `process` is referenced + const workerCode = ` + const { parentPort, workerData } = require("node:worker_threads"); + const procRef = process; + parentPort.on("message", (incoming) => { + parentPort.postMessage({ + wdDateOk: workerData.created instanceof Date && workerData.created.getTime() === 1700000000000, + wdInfOk: workerData.limit === Infinity, + inDateOk: incoming.when instanceof Date && incoming.when.getTime() === 1712345678901, + inInvalidDateOk: incoming.badDate instanceof Date && Number.isNaN(incoming.badDate.getTime()), + inNanOk: Number.isNaN(incoming.score), + replyDate: new Date(1720000000555), + replyNegInf: -Infinity, + }); + procRef.exit(42); + }); + `; + + const wResult = await new Promise((resolve, reject) => { + const w = new Worker(workerCode, { + eval: true, + workerData: { + created: new Date(1700000000000), + limit: Infinity, + }, + }); + let msgReply = null; + w.on("message", (m) => { + msgReply = { + wdDateOk: m.wdDateOk, + wdInfOk: m.wdInfOk, + inDateOk: m.inDateOk, + inInvalidDateOk: m.inInvalidDateOk, + inNanOk: m.inNanOk, + replyDateOk: m.replyDate instanceof Date && m.replyDate.getTime() === 1720000000555, + replyNegInfOk: m.replyNegInf === -Infinity, + }; + }); + w.on("error", reject); + w.on("exit", (code) => { + resolve({ exitCode: code, msgReply }); + }); + w.postMessage({ + when: new Date(1712345678901), + badDate: new Date(NaN), + score: NaN, + }); + }); + + // 3. Immediate top-level process.exit(17) inside Worker with `process` referenced + const immediateExitCode = await new Promise((resolve, reject) => { + const w2 = new Worker(`const p = process; p.exit(17);`, { eval: true }); + w2.on("error", reject); + w2.on("exit", (code) => resolve(code)); + }); + + console.log(JSON.stringify({ mcResult, wResult, immediateExitCode })); +} +runTests(); +"#; + + let bundled = slice_cas::strip_typescript_and_normalize(js, true); + let bc = crate::compile_source_to_bytecode(&bundled); + let mut isolate = SliceIsolate::spawn( + "wave1-worker-date-exit", + 1, + CapabilityToken::pure_compute(), + 20_000_000, + 32 * 1024 * 1024, + ); + let m = isolate.execute_bytecode(&bc, None).unwrap(); + assert_eq!( + m.stdout.len(), + 1, + "stdout={:?}, ret={}", + m.stdout, + m.return_value + ); + let out: serde_json::Value = serde_json::from_str(&m.stdout[0]).unwrap(); + assert_eq!(out["mcResult"]["dValid"], true); + assert_eq!(out["mcResult"]["dEpoch"], true); + assert_eq!(out["mcResult"]["dInvalid"], true); + assert_eq!(out["mcResult"]["nanOk"], true); + assert_eq!(out["mcResult"]["posInfOk"], true); + assert_eq!(out["mcResult"]["negInfOk"], true); + assert_eq!(out["wResult"]["exitCode"], 42); + assert_eq!(out["wResult"]["msgReply"]["wdDateOk"], true); + assert_eq!(out["wResult"]["msgReply"]["wdInfOk"], true); + assert_eq!(out["wResult"]["msgReply"]["inDateOk"], true); + assert_eq!(out["wResult"]["msgReply"]["inInvalidDateOk"], true); + assert_eq!(out["wResult"]["msgReply"]["inNanOk"], true); + assert_eq!(out["wResult"]["msgReply"]["replyDateOk"], true); + assert_eq!(out["wResult"]["msgReply"]["replyNegInfOk"], true); + assert_eq!(out["immediateExitCode"], 17); +} diff --git a/vendor/boa_engine/src/builtins/json/mod.rs b/vendor/boa_engine/src/builtins/json/mod.rs index a2684ce..af98f58 100644 --- a/vendor/boa_engine/src/builtins/json/mod.rs +++ b/vendor/boa_engine/src/builtins/json/mod.rs @@ -17,16 +17,14 @@ use std::{borrow::Cow, iter::once}; use rustc_hash::FxHashSet; -use boa_ast::scope::Scope; -use boa_gc::{Finalize, Gc, Trace}; +use boa_gc::{Finalize, Trace}; use boa_macros::utf16; use itertools::Itertools; use crate::JsExpect; use crate::{ - Context, JsArgs, JsBigInt, JsData, JsResult, JsString, JsValue, SpannedSourceText, + Context, JsArgs, JsBigInt, JsData, JsResult, JsString, JsValue, builtins::BuiltInObject, - bytecompiler::ByteCompiler, context::intrinsics::Intrinsics, error::JsNativeError, js_string, @@ -36,9 +34,7 @@ use crate::{ string::{CodePoint, StaticJsStrings}, symbol::JsSymbol, value::IntegerOrInfinity, - vm::{CallFrame, CallFrameFlags, source_info::SourcePath}, }; -use boa_parser::{Parser, Source}; use super::{BuiltInBuilder, IntrinsicObject}; @@ -57,144 +53,67 @@ mod tests; #[derive(Debug, Trace, Finalize, JsData)] pub(crate) struct RawJson; -/// A tree node representing the structure of parsed JSON, preserving source -/// text spans for primitive values. Used to provide `context.source` to -/// the reviver function in `JSON.parse`. -#[derive(Debug)] -enum JsonNode { - /// A primitive value (string, number, boolean, null) with its original source text. - Primitive(String), - /// An array of child nodes. - Array(Vec), - /// An object with key-value pairs. - Object(Vec<(String, JsonNode)>), -} - -/// Visitor that builds a `JsonNode` source tree by walking the parsed AST, -/// extracting source text from `LinearSpan` information tracked by the parser. -/// Uses a stack to build the tree bottom-up during depth-first traversal. -struct JsonSourceVisitor<'a> { - source_text: &'a boa_ast::SourceText, - interner: &'a boa_interner::Interner, - /// Stack used to build the tree bottom-up. Leaf nodes (literals) are pushed - /// first, then parent nodes (arrays/objects) pop their children and push themselves. - stack: Vec, +trait JsonInput<'a>: Copy { + fn len(self) -> usize; + fn code_unit(self, idx: usize) -> u16; + fn slice_to_js_string(self, start: usize, end: usize) -> JsString; + fn ascii_bytes(self, start: usize, end: usize) -> Cow<'a, [u8]>; } -impl<'a> JsonSourceVisitor<'a> { - /// Creates a new `JsonSourceVisitor`. - fn new(source_text: &'a boa_ast::SourceText, interner: &'a boa_interner::Interner) -> Self { - Self { - source_text, - interner, - stack: Vec::new(), - } +impl<'a> JsonInput<'a> for &'a [u8] { + #[inline] + fn len(self) -> usize { + <[u8]>::len(self) } - /// Consumes the visitor and returns the final `JsonNode` tree. - fn finish(mut self) -> Option { - self.stack.pop() + #[inline] + fn code_unit(self, idx: usize) -> u16 { + u16::from(self[idx]) } -} -impl<'ast> boa_ast::visitor::Visitor<'ast> for JsonSourceVisitor<'_> { - type BreakTy = std::convert::Infallible; - - fn visit_literal( - &mut self, - node: &'ast boa_ast::expression::literal::Literal, - ) -> std::ops::ControlFlow { - let span = node.linear_span(); - let code_points = self.source_text.get_code_points_from_span(span); - let text = String::from_utf16_lossy(code_points); - self.stack.push(JsonNode::Primitive(text)); - std::ops::ControlFlow::Continue(()) + #[inline] + fn slice_to_js_string(self, start: usize, end: usize) -> JsString { + JsString::from(boa_string::JsStr::latin1(&self[start..end])) } - fn visit_array_literal( - &mut self, - node: &'ast boa_ast::expression::literal::ArrayLiteral, - ) -> std::ops::ControlFlow { - let count = node.as_ref().len(); - let base = self.stack.len(); - - // Visit each element, which pushes child nodes onto the stack - for elem in node.as_ref() { - if let Some(expr) = elem { - self.visit_expression(expr)?; - } else { - // Sparse element (should not happen in JSON) - self.stack.push(JsonNode::Primitive("null".to_string())); - } - } - - // Pop the children that were just pushed and collect into array - let children: Vec = self.stack.drain(base..).collect(); - debug_assert_eq!(children.len(), count); - self.stack.push(JsonNode::Array(children)); - std::ops::ControlFlow::Continue(()) + #[inline] + fn ascii_bytes(self, start: usize, end: usize) -> Cow<'a, [u8]> { + Cow::Borrowed(&self[start..end]) } +} - fn visit_object_literal( - &mut self, - node: &'ast boa_ast::expression::literal::ObjectLiteral, - ) -> std::ops::ControlFlow { - use boa_ast::expression::literal::PropertyDefinition; - - let base = self.stack.len(); - let mut keys: Vec = Vec::new(); - - for prop in node.properties() { - if let PropertyDefinition::Property(name, value) = prop { - let key = if let Some(ident) = name.literal() { - self.interner.resolve_expect(ident.sym()).to_string() - } else { - String::new() - }; - keys.push(key); - // Visit the value expression, which pushes the child node - self.visit_expression(value)?; - } - } - - // Pop the value nodes and zip with keys - let values: Vec = self.stack.drain(base..).collect(); - debug_assert_eq!(keys.len(), values.len()); - let entries: Vec<(String, JsonNode)> = keys.into_iter().zip(values).collect(); - self.stack.push(JsonNode::Object(entries)); - std::ops::ControlFlow::Continue(()) +impl<'a> JsonInput<'a> for &'a [u16] { + #[inline] + fn len(self) -> usize { + <[u16]>::len(self) } - fn visit_unary( - &mut self, - node: &'ast boa_ast::expression::operator::Unary, - ) -> std::ops::ControlFlow { - use boa_ast::expression::operator::unary::UnaryOp; - use boa_ast::visitor::VisitWith; - - if node.op() == UnaryOp::Minus - && let boa_ast::Expression::Literal(lit) = node.target() - { - let span = lit.linear_span(); - let code_points = self.source_text.get_code_points_from_span(span); - let num_text = String::from_utf16_lossy(code_points); - self.stack.push(JsonNode::Primitive(format!("-{num_text}"))); - return std::ops::ControlFlow::Continue(()); - } + #[inline] + fn code_unit(self, idx: usize) -> u16 { + self[idx] + } - // Default: recurse into children - node.visit_with(self) + #[inline] + fn slice_to_js_string(self, start: usize, end: usize) -> JsString { + js_string!(&self[start..end]) } - fn visit_parenthesized( - &mut self, - node: &'ast boa_ast::expression::Parenthesized, - ) -> std::ops::ControlFlow { - // Unwrap parentheses and visit inner expression - self.visit_expression(node.expression()) + #[inline] + fn ascii_bytes(self, start: usize, end: usize) -> Cow<'a, [u8]> { + Cow::Owned(self[start..end].iter().map(|&u| u as u8).collect()) } } +#[derive(Debug)] +enum JsonNode { + /// A primitive value (string, number, boolean, null) with its original source text and parsed value. + Primitive(JsString, JsValue), + /// An array of child nodes. + Array(Vec), + /// An object with key-value pairs. + Object(Vec<(JsString, JsonNode)>), +} + /// JavaScript `JSON` global object. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub(crate) struct Json; @@ -244,116 +163,32 @@ impl Json { .to_string(context)?; // Check if a reviver is provided, to determine if we need source text tracking - let has_reviver = args.get_or_undefined(1).is_callable(); - - // SLICE PATCH (P01): Direct streaming `serde_json::Deserializer -> JsValue` materialization - // when no `reviver` callback is supplied, preserving exact ECMAScript property insertion - // order and bypassing both intermediate `serde_json::Value` allocations and JS compilation. - if !has_reviver { - if let boa_string::JsStrVariant::Latin1(bytes) = js_str.as_str().variant() { - if let Ok(ascii_str) = std::str::from_utf8(bytes) { - if let Some(js_val) = Self::try_fast_parse_json_str(ascii_str, context)? { - return Ok(js_val); - } + let reviver_obj = args.get_or_undefined(1).as_callable(); + + // 2-10. Parse JSON text directly per ECMA-404 & ECMAScript 25.5.1. + let (unfiltered, source_tree) = match js_str.as_str().variant() { + boa_string::JsStrVariant::Latin1(bytes) => { + if reviver_obj.is_some() { + let (val, node) = Self::parse_ecma404::<_, true>(bytes, context)?; + (val, Some(node)) + } else { + let (val, _) = Self::parse_ecma404::<_, false>(bytes, context)?; + (val, None) } } - let json_string = js_str - .to_std_string() - .map_err(|e| JsNativeError::syntax().with_message(e.to_string()))?; - if let Some(js_val) = Self::try_fast_parse_json_str(&json_string, context)? { - return Ok(js_val); - } - } - - let json_string = js_str - .to_std_string() - .map_err(|e| JsNativeError::syntax().with_message(e.to_string()))?; - - // 2. Parse ! StringToCodePoints(jsonString) as a JSON text as specified in ECMA-404. - // Throw a SyntaxError exception if it is not a valid JSON text as defined in that specification. - serde_json::from_str::(&json_string) - .map_err(|e| JsNativeError::syntax().with_message(e.to_string()))?; - - // 3. Let scriptString be the string-concatenation of "(", jsonString, and ");". - let script_string = format!("({json_string});"); - - // 4-10. Parse and evaluate the script - let source = Source::from_bytes(&script_string); - let mut parser = Parser::new(source); - parser.set_json_parse(); - - // Use parse_script_with_source to get SourceText for span-based source extraction - let (script, source_text) = - parser.parse_script_with_source(&Scope::new_global(), context.interner_mut())?; - - // Build the source tree from the AST if a reviver is present. - // This walks the parsed AST and extracts source text from LinearSpan information, - // avoiding the need to re-parse the JSON string separately. - let source_tree = if has_reviver { - let expr = script.statements().statements().first().and_then(|item| { - if let boa_ast::StatementListItem::Statement(stmt) = item - && let boa_ast::Statement::Expression(expr) = stmt.as_ref() - { - return Some(expr); + boa_string::JsStrVariant::Utf16(units) => { + if reviver_obj.is_some() { + let (val, node) = Self::parse_ecma404::<_, true>(units, context)?; + (val, Some(node)) + } else { + let (val, _) = Self::parse_ecma404::<_, false>(units, context)?; + (val, None) } - None - }); - expr.and_then(|e| { - use boa_ast::visitor::Visitor; - let mut visitor = JsonSourceVisitor::new(&source_text, context.interner()); - let _ = visitor.visit_expression(e); - visitor.finish() - }) - } else { - None - }; - - let code_block = { - let in_with = context.vm.frame().environments.has_object_environment(); - let spanned_source_text = SpannedSourceText::new_source_only( - crate::spanned_source_text::SourceText::new(source_text), - ); - let mut compiler = ByteCompiler::new( - js_string!(""), - script.strict(), - true, - context.realm().scope().clone(), - context.realm().scope().clone(), - false, - false, - context.interner_mut(), - in_with, - spanned_source_text, - SourcePath::Json, - ); - compiler.compile_statement_list(script.statements(), true, false); - Gc::new(compiler.finish()) + } }; - let realm = context.realm().clone(); - - let env_fp = context.vm.frame().environments.len() as u32; - context.vm.push_frame_with_stack( - CallFrame::new( - code_block, - None, - context.vm.frame().environments.clone(), - realm, - ) - .with_env_fp(env_fp) - .with_flags(CallFrameFlags::EXIT_EARLY), - JsValue::undefined(), - JsValue::null(), - ); - - context.realm().resize_global_env(); - let record = context.run(); - context.vm.pop_frame(); - - let unfiltered = record.consume()?; - // 11. If IsCallable(reviver) is true, then - if let Some(obj) = args.get_or_undefined(1).as_callable() { + if let Some(obj) = reviver_obj { // a. Let root be ! OrdinaryObjectCreate(%Object.prototype%). let root = JsObject::with_object_proto(context.intrinsics()); @@ -377,111 +212,467 @@ impl Json { } } - /// SLICE PATCH (P01): Direct streaming `serde_json::Deserializer -> JsValue` materialization. + /// Direct ECMA-404 recursive-descent JSON parser over `Latin1(&[u8])` or `Utf16(&[u16])`. /// - /// Preserves exact ECMAScript property insertion order without allocating an intermediate - /// `serde_json::Value` (`BTreeMap`) tree or invoking `boa_parser` + `ByteCompiler`. - fn try_fast_parse_json_str( - json_str: &str, + /// - Uses `fast-float2` for exact IEEE-754 round-to-nearest-even `f64` conversion and `±Infinity` on overflow. + /// - Preserves lone UTF-16 surrogates (`\uD800`–`\uDFFF` and literal unpaired surrogates). + /// - Zero-copy `JsStr::latin1` / `utf16` slicing for unescaped strings. + /// - Supports nesting up to 512 levels without stack overflow. + fn parse_ecma404<'a, I: JsonInput<'a>, const WITH_SOURCE: bool>( + input: I, context: &mut Context, - ) -> JsResult> { - struct JsonSeed<'a>(&'a mut Context); - - impl<'de> serde::de::DeserializeSeed<'de> for JsonSeed<'_> { - type Value = JsValue; + ) -> JsResult<(JsValue, JsonNode)> { + const MAX_DEPTH: usize = 512; - fn deserialize(self, deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - deserializer.deserialize_any(self) - } + struct ParserState<'a, I> { + input: I, + pos: usize, + _marker: std::marker::PhantomData<&'a ()>, } - impl<'de> serde::de::Visitor<'de> for JsonSeed<'_> { - type Value = JsValue; + impl<'a, I: JsonInput<'a>> ParserState<'a, I> { + #[inline] + fn syntax_err(&self, msg: &str) -> crate::JsError { + if self.pos == 0 { + JsNativeError::syntax() + .with_message(format!("expected value at line 1 column 1: {msg}")) + .into() + } else { + JsNativeError::syntax() + .with_message(format!("{msg} at position {}", self.pos)) + .into() + } + } - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("any valid JSON value") + #[inline] + fn skip_ws(&mut self) { + let len = self.input.len(); + while self.pos < len { + match self.input.code_unit(self.pos) { + 0x20 | 0x09 | 0x0A | 0x0D => self.pos += 1, + _ => break, + } + } } - fn visit_bool(self, v: bool) -> Result { - Ok(JsValue::new(v)) + #[inline] + fn dummy_node() -> JsonNode { + JsonNode::Primitive(js_string!(), JsValue::undefined()) } - fn visit_i64(self, v: i64) -> Result { - if let Ok(i) = i32::try_from(v) { - Ok(JsValue::new(i)) - } else { - Ok(JsValue::new(v as f64)) + fn parse_value( + &mut self, + depth: usize, + context: &mut Context, + ) -> JsResult<(JsValue, JsonNode)> { + if depth > MAX_DEPTH { + return Err(self.syntax_err("JSON nesting depth exceeded")); + } + self.skip_ws(); + if self.pos >= self.input.len() { + return Err(self.syntax_err("unexpected end of JSON input")); + } + match self.input.code_unit(self.pos) { + 0x22 => { + let start = self.pos; + let s = self.parse_string()?; + let val = JsValue::new(s); + let node = if SRC { + JsonNode::Primitive( + self.input.slice_to_js_string(start, self.pos), + val.clone(), + ) + } else { + Self::dummy_node() + }; + Ok((val, node)) + } + 0x7B => self.parse_object::(depth + 1, context), + 0x5B => self.parse_array::(depth + 1, context), + 0x74 => { + if self.pos + 4 <= self.input.len() + && self.input.code_unit(self.pos + 1) == 0x72 + && self.input.code_unit(self.pos + 2) == 0x75 + && self.input.code_unit(self.pos + 3) == 0x65 + { + self.pos += 4; + let val = JsValue::new(true); + let node = if SRC { + JsonNode::Primitive(js_string!("true"), val.clone()) + } else { + Self::dummy_node() + }; + Ok((val, node)) + } else { + Err(self.syntax_err("expected 'true'")) + } + } + 0x66 => { + if self.pos + 5 <= self.input.len() + && self.input.code_unit(self.pos + 1) == 0x61 + && self.input.code_unit(self.pos + 2) == 0x6C + && self.input.code_unit(self.pos + 3) == 0x73 + && self.input.code_unit(self.pos + 4) == 0x65 + { + self.pos += 5; + let val = JsValue::new(false); + let node = if SRC { + JsonNode::Primitive(js_string!("false"), val.clone()) + } else { + Self::dummy_node() + }; + Ok((val, node)) + } else { + Err(self.syntax_err("expected 'false'")) + } + } + 0x6E => { + if self.pos + 4 <= self.input.len() + && self.input.code_unit(self.pos + 1) == 0x75 + && self.input.code_unit(self.pos + 2) == 0x6C + && self.input.code_unit(self.pos + 3) == 0x6C + { + self.pos += 4; + let val = JsValue::null(); + let node = if SRC { + JsonNode::Primitive(js_string!("null"), val.clone()) + } else { + Self::dummy_node() + }; + Ok((val, node)) + } else { + Err(self.syntax_err("expected 'null'")) + } + } + 0x2D | 0x30..=0x39 => { + let start = self.pos; + let val = self.parse_number()?; + let node = if SRC { + JsonNode::Primitive( + self.input.slice_to_js_string(start, self.pos), + val.clone(), + ) + } else { + Self::dummy_node() + }; + Ok((val, node)) + } + _ => Err(self.syntax_err("unexpected token in JSON")), } } - fn visit_u64(self, v: u64) -> Result { - if let Ok(i) = i32::try_from(v) { - Ok(JsValue::new(i)) - } else { - Ok(JsValue::new(v as f64)) + fn parse_string(&mut self) -> JsResult { + debug_assert_eq!(self.input.code_unit(self.pos), 0x22); + self.pos += 1; + let content_start = self.pos; + let len = self.input.len(); + + while self.pos < len { + let c = self.input.code_unit(self.pos); + if c == 0x22 { + let s = self.input.slice_to_js_string(content_start, self.pos); + self.pos += 1; + return Ok(s); + } + if c == 0x5C { + return self.parse_string_escaped(content_start); + } + if c < 0x20 { + return Err(self.syntax_err("unescaped control character in JSON string")); + } + self.pos += 1; } + Err(self.syntax_err("unterminated string in JSON")) } - fn visit_f64(self, v: f64) -> Result { - Ok(JsValue::new(v)) - } + fn parse_string_escaped(&mut self, content_start: usize) -> JsResult { + let len = self.input.len(); + let mut units = Vec::with_capacity((self.pos - content_start) + 16); + for idx in content_start..self.pos { + units.push(self.input.code_unit(idx)); + } - fn visit_str(self, v: &str) -> Result { - Ok(JsValue::new(js_string!(v))) + while self.pos < len { + let c = self.input.code_unit(self.pos); + if c == 0x22 { + self.pos += 1; + return Ok(js_string!(&units[..])); + } + if c < 0x20 { + return Err(self.syntax_err("unescaped control character in JSON string")); + } + if c == 0x5C { + self.pos += 1; + if self.pos >= len { + return Err(self.syntax_err("unterminated escape sequence in JSON")); + } + let esc = self.input.code_unit(self.pos); + self.pos += 1; + match esc { + 0x22 => units.push(0x22), + 0x5C => units.push(0x5C), + 0x2F => units.push(0x2F), + 0x62 => units.push(0x08), + 0x66 => units.push(0x0C), + 0x6E => units.push(0x0A), + 0x72 => units.push(0x0D), + 0x74 => units.push(0x09), + 0x75 => { + if self.pos + 4 > len { + return Err( + self.syntax_err("incomplete unicode escape in JSON") + ); + } + let mut code: u16 = 0; + for _ in 0..4 { + let hex = self.input.code_unit(self.pos); + self.pos += 1; + let nibble = match hex { + 0x30..=0x39 => hex - 0x30, + 0x61..=0x66 => hex - 0x61 + 10, + 0x41..=0x46 => hex - 0x41 + 10, + _ => { + return Err(self + .syntax_err("invalid unicode hex digit in JSON")); + } + }; + code = (code << 4) | nibble; + } + units.push(code); + } + _ => return Err(self.syntax_err("invalid escape character in JSON")), + } + } else { + units.push(c); + self.pos += 1; + } + } + Err(self.syntax_err("unterminated string in JSON")) } - fn visit_string(self, v: String) -> Result { - Ok(JsValue::new(js_string!(v.as_str()))) - } + fn parse_number(&mut self) -> JsResult { + let start = self.pos; + let len = self.input.len(); + let mut is_neg = false; + if self.input.code_unit(self.pos) == 0x2D { + is_neg = true; + self.pos += 1; + } + if self.pos >= len { + return Err(self.syntax_err("unexpected end of number in JSON")); + } + match self.input.code_unit(self.pos) { + 0x30 => { + self.pos += 1; + if self.pos < len && matches!(self.input.code_unit(self.pos), 0x30..=0x39) { + return Err(self.syntax_err("leading zero in JSON number")); + } + } + 0x31..=0x39 => { + self.pos += 1; + while self.pos < len + && matches!(self.input.code_unit(self.pos), 0x30..=0x39) + { + self.pos += 1; + } + } + _ => return Err(self.syntax_err("expected digit in JSON number")), + } - fn visit_unit(self) -> Result { - Ok(JsValue::null()) - } + let mut is_float = false; + if self.pos < len && self.input.code_unit(self.pos) == 0x2E { + is_float = true; + self.pos += 1; + if self.pos >= len || !matches!(self.input.code_unit(self.pos), 0x30..=0x39) { + return Err(self.syntax_err("expected digit after decimal point in JSON")); + } + while self.pos < len && matches!(self.input.code_unit(self.pos), 0x30..=0x39) { + self.pos += 1; + } + } + + if self.pos < len && matches!(self.input.code_unit(self.pos), 0x65 | 0x45) { + is_float = true; + self.pos += 1; + if self.pos < len && matches!(self.input.code_unit(self.pos), 0x2B | 0x2D) { + self.pos += 1; + } + if self.pos >= len || !matches!(self.input.code_unit(self.pos), 0x30..=0x39) { + return Err(self.syntax_err("expected digit in JSON exponent")); + } + while self.pos < len && matches!(self.input.code_unit(self.pos), 0x30..=0x39) { + self.pos += 1; + } + } + + let num_bytes = self.input.ascii_bytes(start, self.pos); + let bytes = num_bytes.as_ref(); + + if !is_float { + if is_neg && bytes.len() == 2 && bytes[1] == b'0' { + return Ok(JsValue::new(-0.0f64)); + } + if bytes.len() <= 10 { + let digits = if is_neg { &bytes[1..] } else { bytes }; + let mut acc: i64 = 0; + for &b in digits { + acc = acc * 10 + i64::from(b - b'0'); + } + if is_neg { + acc = -acc; + } + if let Ok(i) = i32::try_from(acc) { + return Ok(JsValue::new(i)); + } + return Ok(JsValue::new(acc as f64)); + } + } - fn visit_none(self) -> Result { - Ok(JsValue::null()) + let f: f64 = fast_float2::parse(bytes) + .map_err(|_| self.syntax_err("invalid number in JSON"))?; + Ok(JsValue::new(f)) } - fn visit_seq(self, mut seq: A) -> Result - where - A: serde::de::SeqAccess<'de>, - { - let mut elements = thin_vec::ThinVec::with_capacity(seq.size_hint().unwrap_or(0)); - while let Some(elem) = seq.next_element_seed(JsonSeed(self.0))? { + fn parse_array( + &mut self, + depth: usize, + context: &mut Context, + ) -> JsResult<(JsValue, JsonNode)> { + debug_assert_eq!(self.input.code_unit(self.pos), 0x5B); + self.pos += 1; + self.skip_ws(); + if self.pos < self.input.len() && self.input.code_unit(self.pos) == 0x5D { + self.pos += 1; + let arr = crate::builtins::Array::array_create(0, None, context)?; + let node = if SRC { + JsonNode::Array(Vec::new()) + } else { + Self::dummy_node() + }; + return Ok((arr.into(), node)); + } + + let mut elements = thin_vec::ThinVec::new(); + let mut child_nodes = if SRC { Vec::new() } else { Vec::new() }; + + loop { + let (elem, child_node) = self.parse_value::(depth, context)?; elements.push(elem); + if SRC { + child_nodes.push(child_node); + } + self.skip_ws(); + if self.pos >= self.input.len() { + return Err(self.syntax_err("unterminated array in JSON")); + } + match self.input.code_unit(self.pos) { + 0x2C => { + self.pos += 1; + self.skip_ws(); + if self.pos < self.input.len() && self.input.code_unit(self.pos) == 0x5D + { + return Err(self.syntax_err("trailing comma in JSON array")); + } + } + 0x5D => { + self.pos += 1; + break; + } + _ => return Err(self.syntax_err("expected ',' or ']' in JSON array")), + } } + let arr = - crate::builtins::Array::array_create(elements.len() as u64, None, self.0) - .map_err(serde::de::Error::custom)?; + crate::builtins::Array::array_create(elements.len() as u64, None, context)?; arr.borrow_mut() .properties_mut() .override_indexed_properties(elements); - Ok(arr.into()) + let node = if SRC { + JsonNode::Array(child_nodes) + } else { + Self::dummy_node() + }; + Ok((arr.into(), node)) } - fn visit_map(self, mut map: A) -> Result - where - A: serde::de::MapAccess<'de>, - { - let obj = JsObject::with_object_proto(self.0.intrinsics()); - while let Some(key) = map.next_key::>()? { - let val = map.next_value_seed(JsonSeed(self.0))?; - obj.create_data_property_or_throw(js_string!(key.as_ref()), val, self.0) - .map_err(serde::de::Error::custom)?; + fn parse_object( + &mut self, + depth: usize, + context: &mut Context, + ) -> JsResult<(JsValue, JsonNode)> { + debug_assert_eq!(self.input.code_unit(self.pos), 0x7B); + self.pos += 1; + self.skip_ws(); + let obj = JsObject::with_object_proto(context.intrinsics()); + if self.pos < self.input.len() && self.input.code_unit(self.pos) == 0x7D { + self.pos += 1; + let node = if SRC { + JsonNode::Object(Vec::new()) + } else { + Self::dummy_node() + }; + return Ok((obj.into(), node)); + } + + let mut entries = if SRC { Vec::new() } else { Vec::new() }; + + loop { + self.skip_ws(); + if self.pos >= self.input.len() || self.input.code_unit(self.pos) != 0x22 { + return Err(self.syntax_err("expected string key in JSON object")); + } + let key = self.parse_string()?; + self.skip_ws(); + if self.pos >= self.input.len() || self.input.code_unit(self.pos) != 0x3A { + return Err(self.syntax_err("expected ':' after key in JSON object")); + } + self.pos += 1; + let (val, child_node) = self.parse_value::(depth, context)?; + if SRC { + entries.push((key.clone(), child_node)); + } + obj.create_data_property_or_throw(key, val, context)?; + self.skip_ws(); + if self.pos >= self.input.len() { + return Err(self.syntax_err("unterminated object in JSON")); + } + match self.input.code_unit(self.pos) { + 0x2C => { + self.pos += 1; + self.skip_ws(); + if self.pos < self.input.len() && self.input.code_unit(self.pos) == 0x7D + { + return Err(self.syntax_err("trailing comma in JSON object")); + } + } + 0x7D => { + self.pos += 1; + break; + } + _ => return Err(self.syntax_err("expected ',' or '}' in JSON object")), + } } - Ok(obj.into()) + + let node = if SRC { + JsonNode::Object(entries) + } else { + Self::dummy_node() + }; + Ok((obj.into(), node)) } } - let mut de = serde_json::Deserializer::from_str(json_str); - let val = serde::de::DeserializeSeed::deserialize(JsonSeed(context), &mut de) - .map_err(|e| JsNativeError::syntax().with_message(e.to_string()))?; - de.end() - .map_err(|e| JsNativeError::syntax().with_message(e.to_string()))?; - Ok(Some(val)) + let mut state = ParserState { + input, + pos: 0, + _marker: std::marker::PhantomData, + }; + let res = state.parse_value::(0, context)?; + state.skip_ws(); + if state.pos != state.input.len() { + return Err(state.syntax_err("unexpected trailing characters in JSON")); + } + Ok(res) } /// `25.5.1.1 InternalizeJSONProperty ( holder, name, reviver )` @@ -556,9 +747,8 @@ impl Json { .as_string() .js_expect("EnumerableOwnPropertyNames only returns strings")?; - let p_std = p.to_std_string_escaped(); let child_node = - entries.and_then(|e| e.iter().rfind(|(k, _)| k == &p_std).map(|(_, v)| v)); + entries.and_then(|e| e.iter().rfind(|(k, _)| k == &p).map(|(_, v)| v)); // 1. Let newElement be ? InternalizeJSONProperty(val, P, reviver). let new_element = Self::internalize_json_property( @@ -587,46 +777,15 @@ impl Json { } // Build the context object for the reviver call. - // For primitive JSON values: context = { source: "" } - // For objects/arrays or modified values: context = {} (no source property) - // Per spec, source is only provided when the value is still the same - // primitive that was produced by parsing the original JSON text. let ctx_obj = JsObject::with_object_proto(context.intrinsics()); - if let Some(JsonNode::Primitive(source_text)) = source_node { - // Check if the current value matches what the source text produces. - // If the reviver modified the value, it won't match and we skip source. - let value_matches = match source_text.as_str() { - "null" => val.is_null(), - "true" => val.as_boolean() == Some(true), - "false" => val.as_boolean() == Some(false), - s if s.starts_with('"') => { - // String: compare parsed string value - if let Some(js_str) = val.as_string() { - serde_json::from_str::(s) - .is_ok_and(|parsed| js_str.to_std_string_escaped() == parsed) - } else { - false - } - } - s => { - // Number: compare parsed number value - // Exact comparison is intentional: we want to detect if the - // value was replaced by the reviver, not approximate equality. - #[allow(clippy::float_cmp)] - if let Some(n) = val.as_number() { - s.parse::().is_ok_and(|parsed| n == parsed) - } else { - false - } - } - }; - if value_matches { - ctx_obj.create_data_property_or_throw( - js_string!("source"), - JsValue::from(js_string!(source_text.as_str())), - context, - )?; - } + if let Some(JsonNode::Primitive(source_text, original_val)) = source_node + && JsValue::same_value(&val, original_val) + { + ctx_obj.create_data_property_or_throw( + js_string!("source"), + JsValue::from(source_text.clone()), + context, + )?; } // 3. Return ? Call(reviver, holder, « name, val, context »). @@ -657,40 +816,33 @@ impl Json { // either the first or last code unit of jsonString is any of 0x0009 // (CHARACTER TABULATION), 0x000A (LINE FEED), 0x000D (CARRIAGE RETURN), or // 0x0020 (SPACE). - let std_string = json_string - .to_std_string() - .map_err(|e| JsNativeError::syntax().with_message(e.to_string()))?; - - if std_string.is_empty() { + if json_string.is_empty() { return Err(JsNativeError::syntax() .with_message("JSON.rawJSON text must not be the empty string") .into()); } - let first = std_string.as_bytes()[0]; - let last = std_string.as_bytes()[std_string.len() - 1]; - if matches!(first, b'\t' | b'\n' | b'\r' | b' ') - || matches!(last, b'\t' | b'\n' | b'\r' | b' ') - { + let first = json_string.code_unit_at(0).unwrap_or(0); + let last = json_string.code_unit_at(json_string.len() - 1).unwrap_or(0); + if matches!(first, 0x09 | 0x0A | 0x0D | 0x20) || matches!(last, 0x09 | 0x0A | 0x0D | 0x20) { return Err(JsNativeError::syntax() .with_message("JSON.rawJSON text must not start or end with whitespace") .into()); } + if matches!(first, 0x7B | 0x5B) { + return Err(JsNativeError::syntax() + .with_message("JSON.rawJSON text must not be an object or array") + .into()); + } // 3. Parse StringToCodePoints(jsonString) as a JSON text as specified in ECMA-404. - // Throw a SyntaxError exception if it is not a valid JSON text as defined in that - // specification, or if its outermost value is an object or array. - let parsed: serde_json::Value = serde_json::from_str(&std_string) - .map_err(|e| JsNativeError::syntax().with_message(e.to_string()))?; - - // Must not be an object or array - match parsed { - serde_json::Value::Object(_) | serde_json::Value::Array(_) => { - return Err(JsNativeError::syntax() - .with_message("JSON.rawJSON text must not be an object or array") - .into()); + match json_string.as_str().variant() { + boa_string::JsStrVariant::Latin1(bytes) => { + Self::parse_ecma404::<_, false>(bytes, context)?; + } + boa_string::JsStrVariant::Utf16(units) => { + Self::parse_ecma404::<_, false>(units, context)?; } - _ => {} } // 3. Let internalSlotsList be « [[IsRawJSON]] ». @@ -1185,64 +1337,66 @@ impl Json { } stack.push(addr); - let res = props.shape.with_property_keys(|keys| -> JsResult { - buf.push(b'{'); - let mut first = true; - for (key, slot) in keys { - if slot.attributes.is_accessor_descriptor() { - return Ok(FastStatus::Fallback); - } - if !slot.attributes.contains(SlotAttributes::ENUMERABLE) { - continue; - } - let PropertyKey::String(key_str) = key else { - continue; - }; - let prop_val = &props.storage[slot.index as usize]; - match prop_val.variant() { - JsVariant::Undefined | JsVariant::Symbol(_) => continue, - JsVariant::Object(ref child_obj) if child_obj.is_callable() => { - if child_obj - .borrow() - .properties() - .shape - .lookup(to_json_key) - .is_some() - { - return Ok(FastStatus::Fallback); - } + let res = props + .shape + .with_property_keys(|keys| -> JsResult { + buf.push(b'{'); + let mut first = true; + for (key, slot) in keys { + if slot.attributes.is_accessor_descriptor() { + return Ok(FastStatus::Fallback); + } + if !slot.attributes.contains(SlotAttributes::ENUMERABLE) { continue; } - _ => {} - } - if !first { - buf.push(b','); - } - if !write_js_str(key_str, buf, all_ascii) { - return Ok(FastStatus::Fallback); - } - buf.push(b':'); - match write_val( - prop_val, - buf, - all_ascii, - stack, - obj_proto, - arr_proto, - arr_template_shape_addr, - to_json_key, - )? { - FastStatus::Written => { - first = false; + let PropertyKey::String(key_str) = key else { + continue; + }; + let prop_val = &props.storage[slot.index as usize]; + match prop_val.variant() { + JsVariant::Undefined | JsVariant::Symbol(_) => continue, + JsVariant::Object(ref child_obj) if child_obj.is_callable() => { + if child_obj + .borrow() + .properties() + .shape + .lookup(to_json_key) + .is_some() + { + return Ok(FastStatus::Fallback); + } + continue; + } + _ => {} + } + if !first { + buf.push(b','); } - FastStatus::UndefinedOrSkipped | FastStatus::Fallback => { + if !write_js_str(key_str, buf, all_ascii) { return Ok(FastStatus::Fallback); } + buf.push(b':'); + match write_val( + prop_val, + buf, + all_ascii, + stack, + obj_proto, + arr_proto, + arr_template_shape_addr, + to_json_key, + )? { + FastStatus::Written => { + first = false; + } + FastStatus::UndefinedOrSkipped | FastStatus::Fallback => { + return Ok(FastStatus::Fallback); + } + } } - } - buf.push(b'}'); - Ok(FastStatus::Written) - }); + buf.push(b'}'); + Ok(FastStatus::Written) + }); stack.pop(); res } @@ -1252,8 +1406,18 @@ impl Json { let to_json_key = PropertyKey::from(js_string!("toJSON")); let obj_proto = context.intrinsics().constructors().object().prototype(); let arr_proto = context.intrinsics().constructors().array().prototype(); - if obj_proto.borrow().properties().shape.lookup(&to_json_key).is_some() - || arr_proto.borrow().properties().shape.lookup(&to_json_key).is_some() + if obj_proto + .borrow() + .properties() + .shape + .lookup(&to_json_key) + .is_some() + || arr_proto + .borrow() + .properties() + .shape + .lookup(&to_json_key) + .is_some() { return Ok(None); } From 013fae47cbec95c8e8c0c304244092c75db49eff Mon Sep 17 00:00:00 2001 From: SIKTEC Date: Tue, 10 Nov 2026 22:00:33 -0500 Subject: [PATCH 2/2] perf(json): iterative ECMA-404 parser fast paths, benchmarks, and v0.10.9 sync --- crates/slice-engine/microbench/main.rs | 75 ++- crates/slice-engine/src/bindings/core.rs | 10 + crates/slice-engine/src/isolate/exec_boa.rs | 11 +- .../src/preludes/00_ultra_core.js | 8 + crates/slice-engine/src/preludes/18_intl.js | 5 +- crates/slice-engine/src/runtime/boa_realm.rs | 1 + crates/slice-engine/src/runtime/event_loop.rs | 133 ++-- .../workers_sockets_and_pillars_tests.rs | 14 +- docs/general/00_index.md | 4 +- docs/general/01_getting_started_and_cli.md | 4 +- docs/general/COMPATIBILITY.md | 2 +- docs/guides/README.md | 2 +- docs/system/00_index.md | 6 +- docs/system/07_production_tooling_guide.md | 2 +- scripts/bump_version.ps1 | 22 +- scripts/bump_version.sh | 9 +- scripts/install.ps1 | 2 +- scripts/install.sh | 4 +- scripts/package_release.ps1 | 2 +- scripts/run_benchmarks.ps1 | 2 +- tests/README.md | 2 +- tests/run_real_stack_benchmarks.sh | 4 +- tests/scripts/generate_bench_report.mjs | 6 +- vendor/boa_engine/SLICE_PATCH.md | 6 +- vendor/boa_engine/src/builtins/json/mod.rs | 629 ++++++++++++------ vendor/boa_engine/src/object/property_map.rs | 35 + 26 files changed, 675 insertions(+), 325 deletions(-) diff --git a/crates/slice-engine/microbench/main.rs b/crates/slice-engine/microbench/main.rs index 7e3b49e..0028219 100644 --- a/crates/slice-engine/microbench/main.rs +++ b/crates/slice-engine/microbench/main.rs @@ -221,9 +221,82 @@ fib(30); group.finish(); } +fn bench_json_operations(c: &mut Criterion) { + let mut group = c.benchmark_group("slice_engine::json"); + + let json_batch_src = r#" +const payload = JSON.stringify( + Array.from({ length: 100 }, (_, i) => ({ + id: i, + name: "item-" + i, + escaped: "line1\nline2\t\"quoted\"\\slash", + score: i * 1.25, + ts: 1731274800000 + i, + active: (i & 1) === 0, + tags: ["alpha", "beta", "gamma"], + meta: { nested: i + 100, ok: true } + })) +); +const parsed = JSON.parse(payload); +const out = JSON.stringify(parsed); +if (parsed.length !== 100 || out.length < 1000) throw new Error("json batch mismatch"); +"#; + let json_batch_bc = compile_source_to_bytecode(json_batch_src); + group.bench_function("json_parse_and_stringify_batch_100_records", |b| { + SliceIsolate::prewarm_boa_pool(1); + b.iter(|| { + let mut iso = SliceIsolate::spawn( + "bench-json-batch", + 1, + CapabilityToken::pure_compute(), + 50_000_000, + 16 * 1024 * 1024, + ); + let res = iso + .execute_bytecode(black_box(&json_batch_bc), None) + .expect("json batch execute"); + black_box(res.highest_tier_reached) + }); + }); + + let json_edge_src = r#" +const deepArr = "[".repeat(256) + "42" + "]".repeat(256); +const parsedDeep = JSON.parse(deepArr); +const edgeObj = JSON.parse('{"f1":9007199254740993.0,"f2":1e23,"inf":1e400,"surr":"\\ud800"}'); +const revived = JSON.parse('{"id":9007199254740993,"tag":"ok"}', (k, v, ctx) => { + if (k === "id" && ctx && ctx.source) return ctx.source; + return v; +}); +const out = JSON.stringify({ edgeObj, revived }); +if (!Array.isArray(parsedDeep) || revived.id !== "9007199254740993" || out.length < 20) { + throw new Error("json edge mismatch"); +} +"#; + let json_edge_bc = compile_source_to_bytecode(json_edge_src); + group.bench_function("json_deep_nesting_reviver_and_edge_cases", |b| { + SliceIsolate::prewarm_boa_pool(1); + b.iter(|| { + let mut iso = SliceIsolate::spawn( + "bench-json-edge", + 1, + CapabilityToken::pure_compute(), + 50_000_000, + 16 * 1024 * 1024, + ); + let res = iso + .execute_bytecode(black_box(&json_edge_bc), None) + .expect("json edge execute"); + black_box(res.highest_tier_reached) + }); + }); + + group.finish(); +} + criterion_group!( benches, bench_boacas_and_automaton, - bench_intrinsics_and_numeric_vm + bench_intrinsics_and_numeric_vm, + bench_json_operations ); criterion_main!(benches); diff --git a/crates/slice-engine/src/bindings/core.rs b/crates/slice-engine/src/bindings/core.rs index f1fded7..cb444c9 100644 --- a/crates/slice-engine/src/bindings/core.rs +++ b/crates/slice-engine/src/bindings/core.rs @@ -535,10 +535,20 @@ pub(crate) fn register_slice_native_host_functions(ctx: &mut boa_engine::Context }), ); + let _ = ctx.register_global_builtin_callable( + js_string!("__slice_mark_proc_dirty"), + 0, + NativeFunction::from_fn_ptr(|_this, _args, _context| { + crate::boa_realm::mark_subsystem_dirty(crate::boa_realm::DIRTY_PROCESS); + Ok(BoaVal::undefined()) + }), + ); + let _ = ctx.register_global_builtin_callable( js_string!("__slice_process_exit"), 1, NativeFunction::from_fn_ptr(|_this, args, context| { + crate::boa_realm::mark_subsystem_dirty(crate::boa_realm::DIRTY_PROCESS); let proc_obj = context .global_object() .get(js_string!("process"), context) diff --git a/crates/slice-engine/src/isolate/exec_boa.rs b/crates/slice-engine/src/isolate/exec_boa.rs index aa7421f..592aea1 100644 --- a/crates/slice-engine/src/isolate/exec_boa.rs +++ b/crates/slice-engine/src/isolate/exec_boa.rs @@ -521,16 +521,7 @@ impl SliceIsolate { Err(_) => boa_engine::JsValue::undefined(), }; - if (realm.loaded_shards_mask & (1 << 18)) != 0 - || realm - .ctx - .global_object() - .get(boa_engine::js_string!("__shMask"), &mut realm.ctx) - .ok() - .and_then(|v| v.as_number()) - .is_some_and(|n| ((n as u32) & (1 << 18)) != 0) - { - realm.loaded_shards_mask |= 1 << 18; + if (BOA_DIRTY_SUBSYSTEMS.with(|d| d.get()) & crate::boa_realm::DIRTY_PROCESS) != 0 { crate::event_loop::finalize_and_reset_process_state(&mut realm.ctx); } diff --git a/crates/slice-engine/src/preludes/00_ultra_core.js b/crates/slice-engine/src/preludes/00_ultra_core.js index a6ca3d4..8a26361 100644 --- a/crates/slice-engine/src/preludes/00_ultra_core.js +++ b/crates/slice-engine/src/preludes/00_ultra_core.js @@ -99,8 +99,10 @@ return __slice_env_get(String(k)); }; + var __pExitCode = undefined; function __pLazy(m) { return function() { + __slice_mark_proc_dirty(); __loadShard(18); return globalThis.process[m].apply(globalThis.process, arguments); }; @@ -112,6 +114,12 @@ cwd: function() { return "."; }, get platform() { return __slice_os_platform(); }, get arch() { return __slice_os_arch(); }, + get exitCode() { return __pExitCode; }, + set exitCode(v) { + __pExitCode = v; + if (v !== undefined) __slice_mark_proc_dirty(); + }, + _resetExitCode: function() { __pExitCode = undefined; }, version: "v24.0.0", versions: { node: "24.0.0" }, argv: globalThis.__slice_argv || ["slice"], diff --git a/crates/slice-engine/src/preludes/18_intl.js b/crates/slice-engine/src/preludes/18_intl.js index ff3397c..9b1a774 100644 --- a/crates/slice-engine/src/preludes/18_intl.js +++ b/crates/slice-engine/src/preludes/18_intl.js @@ -6,7 +6,6 @@ p.argv0 = "slice"; p.execArgv = []; p.execPath = "/usr/bin/slice"; - p.exitCode = undefined; var __t0 = Date.now(); p.uptime = function() { return Math.max(0, (Date.now() - __t0) / 1000); @@ -104,6 +103,7 @@ p._exited = false; p.on = p.addListener = function(ev, fn) { if (typeof fn === "function") { + __slice_mark_proc_dirty(); var k = String(ev); (__pEv[k] || (__pEv[k] = [])).push(fn); __pEvCount++; @@ -161,7 +161,8 @@ __pEv = Object.create(null); __pEvCount = 0; } - p.exitCode = undefined; + if (p._resetExitCode) p._resetExitCode(); + else p.exitCode = undefined; p._exited = false; }; p.emit = function(ev) { diff --git a/crates/slice-engine/src/runtime/boa_realm.rs b/crates/slice-engine/src/runtime/boa_realm.rs index 7f633ad..39b1a6c 100644 --- a/crates/slice-engine/src/runtime/boa_realm.rs +++ b/crates/slice-engine/src/runtime/boa_realm.rs @@ -13,6 +13,7 @@ pub(crate) const DIRTY_WEBSOCKET: u8 = 1 << 3; pub(crate) const DIRTY_SOCKETS: u8 = 1 << 4; pub(crate) const DIRTY_WORKERS: u8 = 1 << 5; pub(crate) const DIRTY_WASM: u8 = 1 << 6; +pub(crate) const DIRTY_PROCESS: u8 = 1 << 7; #[derive(Default, Clone)] pub(crate) struct ActiveBoaHttpReq { diff --git a/crates/slice-engine/src/runtime/event_loop.rs b/crates/slice-engine/src/runtime/event_loop.rs index 8795803..1b66c09 100644 --- a/crates/slice-engine/src/runtime/event_loop.rs +++ b/crates/slice-engine/src/runtime/event_loop.rs @@ -582,67 +582,69 @@ pub(crate) fn handle_uncaught_js_error(err: boa_engine::JsError, ctx: &mut Conte .into_opaque(ctx) .unwrap_or_else(|e| JsValue::from(js_string!(e.to_string()))); - if let Some(proc_obj) = ctx - .global_object() - .get(js_string!("process"), ctx) - .ok() - .and_then(|v| v.as_object()) - { - let has_listeners = proc_obj - .get(js_string!("_hasListeners"), ctx) + if (BOA_DIRTY_SUBSYSTEMS.with(|d| d.get()) & crate::boa_realm::DIRTY_PROCESS) != 0 { + if let Some(proc_obj) = ctx + .global_object() + .get(js_string!("process"), ctx) .ok() .and_then(|v| v.as_object()) - .filter(|f| f.is_callable()) - .and_then(|f| { - f.call(&JsValue::from(proc_obj.clone()), &[], ctx) - .ok() - .map(|v| v.to_boolean()) - }) - .unwrap_or(false); - - if has_listeners { - if let Some(emit_fn) = proc_obj - .get(js_string!("emit"), ctx) + { + let has_listeners = proc_obj + .get(js_string!("_hasListeners"), ctx) .ok() .and_then(|v| v.as_object()) .filter(|f| f.is_callable()) - { - let _ = emit_fn.call( - &JsValue::from(proc_obj.clone()), - &[ - JsValue::from(js_string!("uncaughtExceptionMonitor")), - err_val.clone(), - JsValue::from(js_string!("uncaughtException")), - ], - ctx, - ); - if BOA_PROCESS_EXIT.with(|e| e.get().is_some()) { - return true; - } - match emit_fn.call( - &JsValue::from(proc_obj), - &[ - JsValue::from(js_string!("uncaughtException")), - err_val, - JsValue::from(js_string!("uncaughtException")), - ], - ctx, - ) { - Ok(v) if v.to_boolean() => { - let _ = run_jobs_and_check_rejections(ctx); - return BOA_UNCAUGHT_ERROR.with(|e| e.borrow().is_none()) - && BOA_VIOLATION.with(|v| v.borrow().is_none()); + .and_then(|f| { + f.call(&JsValue::from(proc_obj.clone()), &[], ctx) + .ok() + .map(|v| v.to_boolean()) + }) + .unwrap_or(false); + + if has_listeners { + if let Some(emit_fn) = proc_obj + .get(js_string!("emit"), ctx) + .ok() + .and_then(|v| v.as_object()) + .filter(|f| f.is_callable()) + { + let _ = emit_fn.call( + &JsValue::from(proc_obj.clone()), + &[ + JsValue::from(js_string!("uncaughtExceptionMonitor")), + err_val.clone(), + JsValue::from(js_string!("uncaughtException")), + ], + ctx, + ); + if BOA_PROCESS_EXIT.with(|e| e.get().is_some()) { + return true; } - Err(handler_err) => { - if BOA_PROCESS_EXIT.with(|e| e.get().is_some()) { - return true; + match emit_fn.call( + &JsValue::from(proc_obj), + &[ + JsValue::from(js_string!("uncaughtException")), + err_val, + JsValue::from(js_string!("uncaughtException")), + ], + ctx, + ) { + Ok(v) if v.to_boolean() => { + let _ = run_jobs_and_check_rejections(ctx); + return BOA_UNCAUGHT_ERROR.with(|e| e.borrow().is_none()) + && BOA_VIOLATION.with(|v| v.borrow().is_none()); } - let h_formatted = format_uncaught_js_error(&handler_err, ctx); - BOA_UNCAUGHT_ERROR.with(|e| *e.borrow_mut() = Some(h_formatted)); - BOA_LAST_EXIT_CODE.with(|c| c.set(Some(1))); - return false; + Err(handler_err) => { + if BOA_PROCESS_EXIT.with(|e| e.get().is_some()) { + return true; + } + let h_formatted = format_uncaught_js_error(&handler_err, ctx); + BOA_UNCAUGHT_ERROR.with(|e| *e.borrow_mut() = Some(h_formatted)); + BOA_LAST_EXIT_CODE.with(|c| c.set(Some(1))); + return false; + } + Ok(_) => {} } - Ok(_) => {} } } } @@ -663,11 +665,16 @@ pub(crate) fn drain_unhandled_rejections(ctx: &mut Context) -> bool { return false; } let pending = BOA_UNHANDLED_REJECTIONS.with(|u| std::mem::take(&mut *u.borrow_mut())); - let proc_obj = ctx - .global_object() - .get(js_string!("process"), ctx) - .ok() - .and_then(|v| v.as_object()); + let proc_dirty = + (BOA_DIRTY_SUBSYSTEMS.with(|d| d.get()) & crate::boa_realm::DIRTY_PROCESS) != 0; + let proc_obj = if proc_dirty { + ctx.global_object() + .get(js_string!("process"), ctx) + .ok() + .and_then(|v| v.as_object()) + } else { + None + }; let has_ur_listener = proc_obj .as_ref() .and_then(|p| { @@ -770,7 +777,8 @@ pub(crate) fn run_jobs_and_check_rejections(ctx: &mut Context) -> bool { } fn try_emit_before_exit(ctx: &mut Context, before_exit_count: &mut usize) -> bool { - if *before_exit_count >= 64 + if (BOA_DIRTY_SUBSYSTEMS.with(|d| d.get()) & crate::boa_realm::DIRTY_PROCESS) == 0 + || *before_exit_count >= 64 || BOA_PROCESS_EXIT.with(|e| e.get().is_some()) || BOA_UNCAUGHT_ERROR.with(|e| e.borrow().is_some()) || BOA_VIOLATION.with(|v| v.borrow().is_some()) @@ -890,6 +898,13 @@ pub(crate) fn finalize_and_reset_process_state(ctx: &mut Context) { .filter(|f| f.is_callable()) { let _ = reset_fn.call(&JsValue::from(proc_obj), &[], ctx); + } else if let Some(reset_ec_fn) = proc_obj + .get(js_string!("_resetExitCode"), ctx) + .ok() + .and_then(|v| v.as_object()) + .filter(|f| f.is_callable()) + { + let _ = reset_ec_fn.call(&JsValue::from(proc_obj), &[], ctx); } } diff --git a/crates/slice-engine/tests/workers_sockets_and_pillars_tests.rs b/crates/slice-engine/tests/workers_sockets_and_pillars_tests.rs index 3745774..1e1158b 100644 --- a/crates/slice-engine/tests/workers_sockets_and_pillars_tests.rs +++ b/crates/slice-engine/tests/workers_sockets_and_pillars_tests.rs @@ -931,8 +931,8 @@ const highThenAscii = JSON.parse('"\\ud800A"'); const validPair = JSON.parse('"\\ud83d\\ude00"'); const surrogateObj = JSON.parse('{"\\ud800":"\\udfff"}'); -// Deep nesting (300 levels of arrays, 250 levels of objects) -const deepArrJson = "[".repeat(300) + "42" + "]".repeat(300); +// Deep nesting (1000 levels of arrays, 500 levels of objects) +const deepArrJson = "[".repeat(1000) + "42" + "]".repeat(1000); let curArr = JSON.parse(deepArrJson); let arrDepth = 0; while (Array.isArray(curArr)) { @@ -940,7 +940,7 @@ while (Array.isArray(curArr)) { curArr = curArr[0]; } -const deepObjJson = '{"k":'.repeat(250) + '"leaf"' + '}'.repeat(250); +const deepObjJson = '{"k":'.repeat(500) + '"leaf"' + '}'.repeat(500); let curObj = JSON.parse(deepObjJson); let objDepth = 0; while (curObj && typeof curObj === "object") { @@ -948,10 +948,10 @@ while (curObj && typeof curObj === "object") { curObj = curObj.k; } -// Excessive nesting (> 512) must throw SyntaxError cleanly without stack overflow +// Excessive nesting (> 2048) must throw SyntaxError cleanly without stack overflow let excessiveThrew = false; try { - JSON.parse("[".repeat(600) + "1" + "]".repeat(600)); + JSON.parse("[".repeat(2500) + "1" + "]".repeat(2500)); } catch (e) { excessiveThrew = e instanceof SyntaxError; } @@ -1044,9 +1044,9 @@ console.log(JSON.stringify({ assert_eq!(out["highThenAsciiOk"], true); assert_eq!(out["validPairOk"], true); assert_eq!(out["surrogateObjOk"], true); - assert_eq!(out["arrDepth"], 300); + assert_eq!(out["arrDepth"], 1000); assert_eq!(out["arrLeaf"], 42); - assert_eq!(out["objDepth"], 250); + assert_eq!(out["objDepth"], 500); assert_eq!(out["objLeaf"], "leaf"); assert_eq!(out["excessiveThrew"], true); assert_eq!(out["reviverBigSource"], "9007199254740993"); diff --git a/docs/general/00_index.md b/docs/general/00_index.md index a8c33e8..afdf74c 100644 --- a/docs/general/00_index.md +++ b/docs/general/00_index.md @@ -1,6 +1,6 @@ -# The Slice (Nexus-JS) Developer Handbook (`v0.10.8`) +# The Slice (Nexus-JS) Developer Handbook (`v0.10.9`) -Welcome to the complete developer handbook for **Slice (Nexus-JS)** (`v0.10.8`) — a microVM-grade JavaScript and TypeScript runtime, compiler, HTTP server, and AI sandbox engineered in Rust. +Welcome to the complete developer handbook for **Slice (Nexus-JS)** (`v0.10.9`) — a microVM-grade JavaScript and TypeScript runtime, compiler, HTTP server, and AI sandbox engineered in Rust. Whether you are building a high-throughput full-stack web service, packaging a CLI tool into a single zero-dependency executable, running 1,000 untrusted customer plugins on a single machine, or executing LLM-generated TypeScript inside an AI coding agent, this handbook walks you through every feature, CLI command, API, security capability, and migration pattern from the perspective of an application developer. diff --git a/docs/general/01_getting_started_and_cli.md b/docs/general/01_getting_started_and_cli.md index 81a1989..2faa162 100644 --- a/docs/general/01_getting_started_and_cli.md +++ b/docs/general/01_getting_started_and_cli.md @@ -1,6 +1,6 @@ # Chapter 01: Installation, Upgrading, Quickstart, `slice.toml` & Complete CLI Reference -This chapter is your complete day-one guide to installing, upgrading, configuring, and operating **Slice (Nexus-JS)** (`v0.10.8`). By the end of this chapter, you will know how to install and update Slice on any operating system, scaffold a new TypeScript project, configure your `slice.toml` manifest and `.env` vault, run live development with `< 3 ms` `--watch` reloads, use the interactive `slice repl` shell, and master all 13 CLI commands. +This chapter is your complete day-one guide to installing, upgrading, configuring, and operating **Slice (Nexus-JS)** (`v0.10.9`). By the end of this chapter, you will know how to install and update Slice on any operating system, scaffold a new TypeScript project, configure your `slice.toml` manifest and `.env` vault, run live development with `< 3 ms` `--watch` reloads, use the interactive `slice repl` shell, and master all 13 CLI commands. --- @@ -545,7 +545,7 @@ Launches an interactive **SES-Locked TypeScript/JavaScript Compartment Shell** w **Example REPL Session:** ```text $ slice repl -Slice (Nexus-JS) v0.10.8 Interactive SES-Locked Compartment Shell +Slice (Nexus-JS) v0.10.9 Interactive SES-Locked Compartment Shell Commands: :help, :metrics, :caps, :vars, :reset, :quit slice(ses)> const price: number = 250 * 4; => 1000 diff --git a/docs/general/COMPATIBILITY.md b/docs/general/COMPATIBILITY.md index 272f06c..61c835a 100644 --- a/docs/general/COMPATIBILITY.md +++ b/docs/general/COMPATIBILITY.md @@ -26,7 +26,7 @@ All WinterTC globals are available in every isolate out of the box. Under the ho | **Events & Signals** | `Event`, `CustomEvent`, `EventTarget`, `AbortController`, `AbortSignal` | **100% Supported** | Listener registration (`once`, `passive`, `signal`), `AbortSignal.abort()`, `AbortSignal.timeout(ms)`, `AbortSignal.any(signals)`, and `DOMException`. | | **Timers & Microtasks** | `setTimeout`, `clearTimeout`, `setInterval`, `clearInterval`, `setImmediate`, `clearImmediate`, `queueMicrotask`, `process.nextTick` | **100% Supported** | $O(1)$ Slab-Allocated Hierarchical Timer Wheel with strict priority ordering (`sync -> process.nextTick -> Promise.then / queueMicrotask -> setImmediate -> setTimeout(0)`), deterministic virtual time-warp (`0 ns` wall-clock wait during cassette tests), and real non-blocking concurrency on live sockets. | | **Cloning & Diagnostics** | `structuredClone`, `performance.now()`, `console.*` (`log`, `info`, `warn`, `error`, `debug`, `table`, `time`, `timeLog`, `timeEnd`, `group`, `groupCollapsed`, `groupEnd`, `dir`, `dirxml`, `count`, `countReset`, `assert`, `trace`, `clear`) | **100% Supported** | Graph-preserving cyclic `structuredClone` (`Map`, `Set`, `Date`, `RegExp`, `ArrayBuffer`, `Uint8Array`, `Error`, `transfer` list) and full WHATWG `console.*` specification routed through the Outbound Egress Secret DLP Firewall (with deterministic elapsed timing under `--deterministic`). | -| **Internationalization & WinterTC Globals** | `Intl` (`DateTimeFormat`, `NumberFormat`, `Collator`, `PluralRules`, `Segmenter`, `getCanonicalLocales`, `supportedValuesOf`), `navigator`, `location` | **100% Supported** | Zero-ICU-bloat deterministic `Intl` fast-path shard (`18_intl.js`) supporting `Date.prototype.toLocale*`, `Number.prototype.toLocaleString` (`decimal`, `currency`, `percent`, `unit`, `formatToParts`), `String.prototype.localeCompare`, `PluralRules` (`cardinal`, `ordinal`), `Segmenter` (`grapheme`, `word`, `sentence`), plus WinterTC `navigator` (`userAgent: "Slice-NexusJS/0.10.8"`, `hardwareConcurrency`, `language`, `languages`) and `location`. | +| **Internationalization & WinterTC Globals** | `Intl` (`DateTimeFormat`, `NumberFormat`, `Collator`, `PluralRules`, `Segmenter`, `getCanonicalLocales`, `supportedValuesOf`), `navigator`, `location` | **100% Supported** | Zero-ICU-bloat deterministic `Intl` fast-path shard (`18_intl.js`) supporting `Date.prototype.toLocale*`, `Number.prototype.toLocaleString` (`decimal`, `currency`, `percent`, `unit`, `formatToParts`), `String.prototype.localeCompare`, `PluralRules` (`cardinal`, `ordinal`), `Segmenter` (`grapheme`, `word`, `sentence`), plus WinterTC `navigator` (`userAgent: "Slice-NexusJS/0.10.9"`, `hardwareConcurrency`, `language`, `languages`) and `location`. | | **JSX / TSX & Edge SSR** | Native `.tsx` / `.jsx` compilation + `slice:jsx` (`react/jsx-runtime`, `preact/jsx-runtime`, `hono/jsx`) | **100% Supported** | Native Rust Oxc JSX/TSX compiler (`runtime: automatic`) paired with a built-in zero-dependency SSR engine (`renderToString`, `jsx`, `jsxs`, `jsxDEV`, `Fragment`, `createElement`/`h`, `raw`) featuring automatic HTML & attribute XSS escaping, void tag handling, and camelCase `style` object serialization. | --- diff --git a/docs/guides/README.md b/docs/guides/README.md index ea0b46d..5596cab 100644 --- a/docs/guides/README.md +++ b/docs/guides/README.md @@ -1,6 +1,6 @@ # Slice (Nexus-JS) — Developer How-To Guides (`docs/guides/`) -This directory contains practical step-by-step developer guides, production deployment how-tos, and ecosystem integration recipes for Slice (`v0.10.8`): +This directory contains practical step-by-step developer guides, production deployment how-tos, and ecosystem integration recipes for Slice (`v0.10.9`): ## Available Guides diff --git a/docs/system/00_index.md b/docs/system/00_index.md index 5e7f78a..2468eff 100644 --- a/docs/system/00_index.md +++ b/docs/system/00_index.md @@ -1,4 +1,4 @@ -# Slice (Nexus-JS): System Architecture & Internal Specification Index (`v0.10.8`) +# Slice (Nexus-JS): System Architecture & Internal Specification Index (`v0.10.9`) Welcome to the **Slice (Nexus-JS)** System Architecture and Internal Specification (`docs/system/`). Engineered in pure Rust (`edition = 2021`, `opt-level = 3`, `lto = "fat"`, `codegen-units = 1`, `panic = "abort"`), Slice replaces the legacy POSIX + monolithic V8/JSC assumptions of Node.js, Deno, and Bun with four orthogonal, tightly integrated Rust crates: @@ -21,7 +21,7 @@ Welcome to the **Slice (Nexus-JS)** System Architecture and Internal Specificati | **[`05_wasm_workers_and_kernel_io.md`](./05_wasm_workers_and_kernel_io.md)** | Pure-Rust WebAssembly (`wasmi` `v2.0.0`, `GLOBAL_WASM_MODULE_CACHE`, 5-Ring WASM Cage), Capability-Attenuated MicroVM Web Workers & `node:worker_threads` (`IDLE_WORKER_POOL`), Direct Kernel Completion Rings (`io_uring` & `IOCP`), & OS-Native `notify` File Watcher | `crates/slice-engine/src/{wasm,workers}.rs`, `crates/slice-io/src/lib.rs`, `crates/slice-cli/src/config_and_watch.rs` | | **[`06_edge_kv_ai_sandbox_and_mcp.md`](./06_edge_kv_ai_sandbox_and_mcp.md)** | 16-Way Sharded `Slice.kv` (`EDGE_KV_SLAB`), Multi-Tenant `--tenant-dir` Atomic Hot-Swap, SplitMix64 `--deterministic` Replay, AI Agent "Code Mode" (`Slice.evalSandbox`, `< 5 us` `Slice.checkpoint`/`restore`), Time-Travel `slice test`, & Built-In MCP Server (`slice mcp`) | `crates/slice-engine/src/state_and_kv.rs`, `crates/slice-cli/src/{test_runner,mcp_and_repl}.rs` | | **[`07_production_tooling_guide.md`](./07_production_tooling_guide.md)** | Production CLI Reference, Unified 4-Layer Configuration System (`slice.toml`, Inline `// @slice-...`, `SLICE_*` Env Vars, CLI Flags), Docker & Kubernetes Deployment Patterns, and Multi-Tenant Operations | `crates/slice-cli/src/{main,config_and_watch}.rs`, `crates/slice-sandbox/src/lib.rs` | -| **[`08_benchmarks_and_comparisons.md`](./08_benchmarks_and_comparisons.md)** | Complete 4-Runtime Empirical Benchmark Suite (Slice `v0.10.8` vs Node.js `v25.8.1`, Deno `v2.7.5`, and Bun `v1.3.10`) Across All 31 Applications, Framework Suites & Case Studies | `benches/`, `tests/apps/`, `tests/frameworks/`, `tests/case_studies/` | +| **[`08_benchmarks_and_comparisons.md`](./08_benchmarks_and_comparisons.md)** | Complete 4-Runtime Empirical Benchmark Suite (Slice `v0.10.9` vs Node.js `v25.8.1`, Deno `v2.7.5`, and Bun `v1.3.10`) Across All 31 Applications, Framework Suites & Case Studies | `benches/`, `tests/apps/`, `tests/frameworks/`, `tests/case_studies/` | | **[`09_research_deep_dive.md`](./09_research_deep_dive.md)** | Foundational Systems Research Paper: Breaking the JS/TS Cold-Start, Memory Density, and Multi-Tenant Security Wall | Architectural Research Blueprint | | **[`vendor/boa_engine/SLICE_PATCH.md`](../../vendor/boa_engine/SLICE_PATCH.md)** | `SLICEBC1` Precompiled `CodeBlock` Binary Serializer/Deserializer & Deferred Function Span Architecture | `vendor/boa_engine/src/{slice_precompile.rs,lib.rs,vm/opcode/call/mod.rs}` | @@ -31,7 +31,7 @@ Welcome to the **Slice (Nexus-JS)** System Architecture and Internal Specificati ``` +---------------------------------------------------------------------------------------------------+ -| Slice (Nexus-JS) v0.10.8 Host | +| Slice (Nexus-JS) v0.10.9 Host | | | | +---------------------------------------------------------------------------------------------+ | | | slice-sandbox: Zero-Trust OCap, SES & Hardware Boundary | | diff --git a/docs/system/07_production_tooling_guide.md b/docs/system/07_production_tooling_guide.md index ef31452..ae71604 100644 --- a/docs/system/07_production_tooling_guide.md +++ b/docs/system/07_production_tooling_guide.md @@ -2,7 +2,7 @@ ## 1. Overview of the Production Toolchain -Slice `v0.10.8` provides a complete developer and production deployment workflow packaged inside a single self-contained native Rust executable (`slice`, embedding the **Oxc** TypeScript compiler, transitive `node_modules` bundler, **Boa** ES2024+ VM, and **Slice** Zero-Stat CAS + Tier-0/Tier-2 engine): +Slice `v0.10.9` provides a complete developer and production deployment workflow packaged inside a single self-contained native Rust executable (`slice`, embedding the **Oxc** TypeScript compiler, transitive `node_modules` bundler, **Boa** ES2024+ VM, and **Slice** Zero-Stat CAS + Tier-0/Tier-2 engine): 1. **Project Initialization & Policy Configuration (`slice init` & `slice.toml`)** 2. **Zero-Stat Content-Addressed Packaging (`slice pack` for single files or full `package.json` + `node_modules` folders)** diff --git a/scripts/bump_version.ps1 b/scripts/bump_version.ps1 index a9f2c37..209c215 100644 --- a/scripts/bump_version.ps1 +++ b/scripts/bump_version.ps1 @@ -68,8 +68,6 @@ $TargetFiles = @( "scripts/run_benchmarks.ps1", "tests/run_real_stack_benchmarks.sh", "tests/scripts/generate_bench_report.mjs", - "tests/reports/latest.md", - "tests/reports/latest.json", "README.md", "tests/README.md", "tests/frameworks/README.md" @@ -81,6 +79,8 @@ Get-ChildItem -Path (Join-Path $RootDir "docs") -Filter "*.md" -Recurse | ForEac } $EscOld = [regex]::Escape($OldVersion) +$StartMarker = "" +$EndMarker = "" foreach ($RelPath in $TargetFiles) { $FullPath = Join-Path $RootDir $RelPath @@ -88,8 +88,18 @@ foreach ($RelPath in $TargetFiles) { continue } $Content = [System.IO.File]::ReadAllText($FullPath) - $Updated = $Content ` - -replace "v$EscOld\b", "v$NewVersion" ` + $SavedBlock = $null + $WorkContent = $Content + $StartIdx = $Content.IndexOf($StartMarker) + $EndIdx = $Content.IndexOf($EndMarker) + if ($StartIdx -ge 0 -and $EndIdx -gt $StartIdx) { + $BlockLen = ($EndIdx + $EndMarker.Length) - $StartIdx + $SavedBlock = $Content.Substring($StartIdx, $BlockLen) + $WorkContent = $Content.Substring(0, $StartIdx) + "__SLICE_BENCH_SUMMARY_PLACEHOLDER__" + $Content.Substring($StartIdx + $BlockLen) + } + + $Updated = $WorkContent ` + -replace "\bv$EscOld\b", "v$NewVersion" ` -replace "`"version`":\s*`"$EscOld`"", "`"version`": `"$NewVersion`"" ` -replace "`"version`":`"$EscOld`"", "`"version`":`"$NewVersion`"" ` -replace "SLICE_VERSION:-$EscOld\b", "SLICE_VERSION:-$NewVersion" ` @@ -97,6 +107,10 @@ foreach ($RelPath in $TargetFiles) { -replace "\|\| `"$EscOld`"", "|| `"$NewVersion`"" ` -replace "Slice-NexusJS/$EscOld\b", "Slice-NexusJS/$NewVersion" + if ($null -ne $SavedBlock) { + $Updated = $Updated.Replace("__SLICE_BENCH_SUMMARY_PLACEHOLDER__", $SavedBlock) + } + if ($Updated -ne $Content) { [System.IO.File]::WriteAllText($FullPath, $Updated) Write-Host " [updated] $RelPath" diff --git a/scripts/bump_version.sh b/scripts/bump_version.sh index 9333118..fcc7df1 100644 --- a/scripts/bump_version.sh +++ b/scripts/bump_version.sh @@ -57,7 +57,9 @@ FILES=( "scripts/install.ps1" "scripts/install.sh" "scripts/package_release.ps1" + "scripts/run_benchmarks.ps1" "tests/run_real_stack_benchmarks.sh" + "tests/scripts/generate_bench_report.mjs" "README.md" "tests/README.md" "tests/frameworks/README.md" @@ -68,11 +70,14 @@ FILES=( for f in "${FILES[@]}"; do if [[ -f "$f" ]]; then - if grep -qE "v${OLD_VERSION}|\"version\": ?\"${OLD_VERSION}\"|Slice-NexusJS/${OLD_VERSION}" "$f"; then + if grep -qE "v${OLD_VERSION}|\"version\": ?\"${OLD_VERSION}\"|SLICE_VERSION:-${OLD_VERSION}|\$Version = \"${OLD_VERSION}\"|\|\| \"${OLD_VERSION}\"|Slice-NexusJS/${OLD_VERSION}" "$f"; then sed -i.bak -E \ - -e "s/v${OLD_VERSION}\b/v${NEW_VERSION}/g" \ + -e "//,//! s/\bv${OLD_VERSION}\b/v${NEW_VERSION}/g" \ -e "s/\"version\": \"${OLD_VERSION}\"/\"version\": \"${NEW_VERSION}\"/g" \ -e "s/\"version\":\"${OLD_VERSION}\"/\"version\":\"${NEW_VERSION}\"/g" \ + -e "s/SLICE_VERSION:-${OLD_VERSION}\b/SLICE_VERSION:-${NEW_VERSION}/g" \ + -e "s/\\\$Version = \"${OLD_VERSION}\"/\$Version = \"${NEW_VERSION}\"/g" \ + -e "s/\|\| \"${OLD_VERSION}\"/|| \"${NEW_VERSION}\"/g" \ -e "s/Slice-NexusJS\/${OLD_VERSION}\b/Slice-NexusJS\/${NEW_VERSION}/g" \ "$f" rm -f "${f}.bak" diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 300d1be..915e5c1 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -16,6 +16,6 @@ if (-not (Test-Path $ReleaseExe)) { $DestExe = Join-Path $InstallDir "slice.exe" Copy-Item -Force $ReleaseExe $DestExe -Write-Host "Installed Slice v0.10.8 to $DestExe" +Write-Host "Installed Slice v0.10.9 to $DestExe" & $DestExe info diff --git a/scripts/install.sh b/scripts/install.sh index f11048e..1c4d6a3 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -9,13 +9,13 @@ INSTALL_DIR="${HOME}/.slice/bin" mkdir -p "$INSTALL_DIR" if [ ! -f "$ROOT_DIR/target/release/slice" ]; then - echo "Compiling Slice v0.10.8 release binary..." + echo "Compiling Slice v0.10.9 release binary..." cargo build --release fi cp -f "$ROOT_DIR/target/release/slice" "$INSTALL_DIR/slice" chmod +x "$INSTALL_DIR/slice" -echo "Installed Slice v0.10.8 to $INSTALL_DIR/slice" +echo "Installed Slice v0.10.9 to $INSTALL_DIR/slice" "$INSTALL_DIR/slice" info diff --git a/scripts/package_release.ps1 b/scripts/package_release.ps1 index dcb51b3..39b7221 100644 --- a/scripts/package_release.ps1 +++ b/scripts/package_release.ps1 @@ -6,7 +6,7 @@ $ErrorActionPreference = "Stop" $RootDir = Split-Path -Parent $PSScriptRoot Set-Location $RootDir -Write-Host "[1/4] Building Slice v0.10.8 Release Binary (LTO=fat, opt-level=3)..." +Write-Host "[1/4] Building Slice v0.10.9 Release Binary (LTO=fat, opt-level=3)..." cargo build --release $DistDir = Join-Path $RootDir "dist" diff --git a/scripts/run_benchmarks.ps1 b/scripts/run_benchmarks.ps1 index 1c2ea74..0b5e9f7 100644 --- a/scripts/run_benchmarks.ps1 +++ b/scripts/run_benchmarks.ps1 @@ -15,7 +15,7 @@ $CargoToml = Get-Content (Join-Path $RootDir "Cargo.toml") -Raw if ($CargoToml -match '(?m)^\[workspace\.package\]\s*\r?\nversion\s*=\s*"([^"]+)"') { $Version = $Matches[1] } else { - $Version = "0.10.8" + $Version = "0.10.9" } try { diff --git a/tests/README.md b/tests/README.md index ddd0ad2..0d0512b 100644 --- a/tests/README.md +++ b/tests/README.md @@ -6,7 +6,7 @@ This directory (`tests/`) contains a reproducible Linux `x86_64` Docker benchmar ## Latest Runtimes Installed in Docker ([`tests/Dockerfile`](./Dockerfile)) -- **Slice (`v0.10.6` — `SLICEBC\x01` Precompiled `CodeBlock` CAS Default):** Hybrid architecture combining **Oxc** (`v0.154.0` production Rust TypeScript compiler + AST ESM-to-CJS module bundler with deferred function module spans (`0x11` tag) + `.wasm` zero-stat module bundler + full `package.json` `"exports"` resolver + static barrel tree-shaker), **Boa (`boa_engine` v0.22 + [`vendor/boa_engine/SLICE_PATCH.md`](../vendor/boa_engine/SLICE_PATCH.md))** full ECMAScript VM with default `SLICEBC\x01` precompiled `Gc` + global `Scope` binary payloads (zero runtime `boa_parser` / `ByteCompiler` overhead) and pre-fused `node:*` built-in shards, thread-local pre-warmed context pool (`BOA_CONTEXT_POOL`), pure-Rust **WebAssembly (`wasmi` v2.0.0)** engine with `GLOBAL_WASM_MODULE_CACHE` and 5-Ring CPU fuel & linear memory cages, native Rust fast-path intrinsics (`SemVer`, `SHA-256`/`SHA-384`/`SHA-512`, `HMAC-SHA256`/`HMAC-SHA512`, `Base64URL`, in-memory `VFS` slab cache, WinterTC `URL`/`URLPattern`/`URLSearchParams`/`Headers`/`Request`/`Response`/`ReadableStream`/`TransformStream`/`CompressionStream`/`DecompressionStream`/`TextEncoder`/`TextDecoder`/`crypto.subtle`/`Blob`/`File`/`FormData`/`structuredClone`/`EventTarget`/`AbortSignal`/`WebAssembly`, real `Uint8Array`-backed `Buffer`, `node:*` Core Tier-1 modules including `node:async_hooks` and `node:zlib`, `Slice.streamSSE` / `Response.sse`, OCap-guarded outbound HTTP/1.1 & HTTPS TLS 1.3 `fetch()` with Keep-Alive pooling, hop-by-hop redirect guard, Cloud Metadata SSRF Shield, and Egress DLP, built-in Time-Travel `slice test` runner, OS-native `notify` (`v8.2`) `< 5 ms` `--watch`, and Auto-DLP `.env`), and **Slice Zero-Stat CAS + `--static` BLAKE3 `ETag` Static Server + Tier-0/Tier-2 fast paths**. +- **Slice (`v0.10.9` — `SLICEBC\x01` Precompiled `CodeBlock` CAS Default):** Hybrid architecture combining **Oxc** (`v0.154.0` production Rust TypeScript compiler + AST ESM-to-CJS module bundler with deferred function module spans (`0x11` tag) + `.wasm` zero-stat module bundler + full `package.json` `"exports"` resolver + static barrel tree-shaker), **Boa (`boa_engine` v0.22 + [`vendor/boa_engine/SLICE_PATCH.md`](../vendor/boa_engine/SLICE_PATCH.md))** full ECMAScript VM with default `SLICEBC\x01` precompiled `Gc` + global `Scope` binary payloads (zero runtime `boa_parser` / `ByteCompiler` overhead) and pre-fused `node:*` built-in shards, thread-local pre-warmed context pool (`BOA_CONTEXT_POOL`), pure-Rust **WebAssembly (`wasmi` v2.0.0)** engine with `GLOBAL_WASM_MODULE_CACHE` and 5-Ring CPU fuel & linear memory cages, native Rust fast-path intrinsics (`SemVer`, `SHA-256`/`SHA-384`/`SHA-512`, `HMAC-SHA256`/`HMAC-SHA512`, `Base64URL`, in-memory `VFS` slab cache, WinterTC `URL`/`URLPattern`/`URLSearchParams`/`Headers`/`Request`/`Response`/`ReadableStream`/`TransformStream`/`CompressionStream`/`DecompressionStream`/`TextEncoder`/`TextDecoder`/`crypto.subtle`/`Blob`/`File`/`FormData`/`structuredClone`/`EventTarget`/`AbortSignal`/`WebAssembly`, real `Uint8Array`-backed `Buffer`, `node:*` Core Tier-1 modules including `node:async_hooks` and `node:zlib`, `Slice.streamSSE` / `Response.sse`, OCap-guarded outbound HTTP/1.1 & HTTPS TLS 1.3 `fetch()` with Keep-Alive pooling, hop-by-hop redirect guard, Cloud Metadata SSRF Shield, and Egress DLP, built-in Time-Travel `slice test` runner, OS-native `notify` (`v8.2`) `< 5 ms` `--watch`, and Auto-DLP `.env`), and **Slice Zero-Stat CAS + `--static` BLAKE3 `ETag` Static Server + Tier-0/Tier-2 fast paths**. - **Node.js (`v27.0.0-alpha` — Absolute Latest Release):** Dynamically resolved at container build time from `https://nodejs.org/dist/index.json` (`.[0].version`). - **Bun (`v1.4.4` — Absolute Latest Release):** Installed via official `https://bun.sh/install`. - **Deno (`v2.9.8` — Absolute Latest Release):** Installed via official `https://deno.land/install.sh`. diff --git a/tests/run_real_stack_benchmarks.sh b/tests/run_real_stack_benchmarks.sh index dc4a283..be44b4d 100644 --- a/tests/run_real_stack_benchmarks.sh +++ b/tests/run_real_stack_benchmarks.sh @@ -44,7 +44,7 @@ BENCH_TAGS="${1:-${SLICE_BENCH_TAGS:-all}}" METRICS_JSONL="${SLICE_BENCH_JSONL:-/tmp/slice_bench_metrics.jsonl}" rm -f "$METRICS_JSONL" -SLICE_VERSION="${SLICE_VERSION:-0.10.6}" +SLICE_VERSION="${SLICE_VERSION:-0.10.9}" SLICE_GIT_COMMIT="${SLICE_GIT_COMMIT:-$(git -C "$ROOT_DIR" rev-parse --short HEAD 2>/dev/null || echo "abdbf89")}" SLICE_BENCH_TS_UTC="${SLICE_BENCH_TS_UTC:-$(date -u +"%Y-%m-%dT%H:%M:%SZ")}" CURRENT_APP_IDX="" @@ -207,7 +207,7 @@ echo "=========================================================================" echo " RUNTIME VERSIONS (LINUX DOCKER x86_64 - LATEST RELEASES)" echo " Active Benchmark Tags: $BENCH_TAGS" echo "=========================================================================" -echo " Slice : v0.10.6 (Oxc TS Compiler + Boa ES2024 VM + Zero-Stat CAS)" +echo " Slice : v${SLICE_VERSION} (Oxc TS Compiler + Boa ES2024 VM + Zero-Stat CAS)" echo " Node.js : $(node -v)" echo " Bun : v$(bun -v)" echo " Deno : $(deno -V)" diff --git a/tests/scripts/generate_bench_report.mjs b/tests/scripts/generate_bench_report.mjs index 8a63df1..a74780a 100644 --- a/tests/scripts/generate_bench_report.mjs +++ b/tests/scripts/generate_bench_report.mjs @@ -140,7 +140,7 @@ export function renderFullReportMarkdown(report, archiveFileName) { * **Canonical Latest Report:** [\`tests/reports/latest.md\`](./latest.md) * **Environment:** \`${meta.environment}\` * **Compared Runtimes:** - * **Slice:** \`v${meta.version}\` (Default \`SLICEBC\\x01\` Precompiled Boa \`CodeBlock\` CAS + Monomorphic Inline Caching \`get_fast\` + Streaming \`JsonSeed\` \`JSON.parse\`) + * **Slice:** \`v${meta.version}\` (Default \`SLICEBC\\x01\` Precompiled Boa \`CodeBlock\` CAS + Monomorphic Inline Caching \`get_fast\` + Iterative ECMA-404 \`JSON.parse\`) * **Bun:** \`${meta.runtimes.bun}\` (JavaScriptCore) * **Deno:** \`${meta.runtimes.deno}\` (V8) * **Node.js:** \`${meta.runtimes.node}\` (V8 native TypeScript execution) @@ -330,12 +330,12 @@ function parseJsonlMetrics(jsonlPath, previousReport) { return { metadata: { - version: metaRec.version || previousReport?.metadata?.version || "0.10.6", + version: metaRec.version || previousReport?.metadata?.version || "0.10.9", commit: metaRec.commit || previousReport?.metadata?.commit || "abdbf89", timestampUtc: metaRec.timestampUtc || new Date().toISOString(), environment: metaRec.environment || previousReport?.metadata?.environment || "Linux x86_64 Docker (io_uring SQPOLL + WRPKRU)", runtimes: { - slice: `v${metaRec.version || "0.10.6"}`, + slice: `v${metaRec.version || "0.10.9"}`, bun: metaRec.bunVersion || previousReport?.metadata?.runtimes?.bun || "v1.4.4", deno: metaRec.denoVersion || previousReport?.metadata?.runtimes?.deno || "v2.9.8", node: metaRec.nodeVersion || previousReport?.metadata?.runtimes?.node || "v27.0.0-alpha" diff --git a/vendor/boa_engine/SLICE_PATCH.md b/vendor/boa_engine/SLICE_PATCH.md index 2d338ca..1daecf3 100644 --- a/vendor/boa_engine/SLICE_PATCH.md +++ b/vendor/boa_engine/SLICE_PATCH.md @@ -26,9 +26,9 @@ Thirteen files in `vendor/boa_engine` differ from upstream `boa_engine v0.22.0`: - `Script::compile_to_slicebc(&self, context: &mut Context) -> JsResult>` - `Script::from_slicebc_bytes(bytes: &[u8], context: &mut Context) -> Option` - Internal recursive helpers `encode_codeblock` / `decode_codeblock` (including bytecode-to-line `SourceMap` entries and deferred `0x11` function source spans) and `write_js_string` / `read_js_string`. -- [`vendor/boa_engine/src/builtins/json/mod.rs`](./src/builtins/json/mod.rs): - - `Json::try_fast_parse_json_str` (`JsonSeed` streaming `serde::de::DeserializeSeed`): Direct zero-intermediate-tree `serde_json::Deserializer::from_str -> JsValue` materialization fast-path in `JSON.parse` when no `reviver` callback is supplied (with zero-copy ASCII `JsStrVariant::Latin1` input borrowing and Latin1 `JsString` output), preserving exact ECMAScript source key insertion order while avoiding `boa_parser` + `ByteCompiler` + VM execution on every JSON payload. - - `Json::try_fast_stringify`: Direct single-buffer `Vec` serializer fast-path in `JSON.stringify` when no `replacer` or `space` argument is supplied, traversing ordinary `JsObject`s (via `Shape::with_property_keys` + direct storage slots) and dense `Array`s (`DenseI32`, `DenseF64`, `DenseElement`) with `ryu_js` number formatting and automatic fallback for `toJSON`, getters, exotic objects, or unpaired surrogates. +- [`vendor/boa_engine/src/builtins/json/mod.rs`](./src/builtins/json/mod.rs) & [`vendor/boa_engine/src/object/property_map.rs`](./src/object/property_map.rs): + - `Json::parse_ecma404` & `PropertyMap::insert_data_property`: Direct iterative ECMA-404 state-machine parser (`MAX_DEPTH = 2048` on heap-allocated `Vec`, `O(1)` C/Rust call stack) over `JsStrVariant::Latin1` and `JsStrVariant::Utf16` with `fast-float2` exact IEEE-754 float parsing (`±Infinity` on overflow), lone UTF-16 surrogate preservation, zero-copy Latin1/UTF-16 string slicing, Latin1 escaped-string fast path, direct `PropertyMap::insert_data_property` slot insertion, and optional `WITH_SOURCE` source-span tracking for `reviver` (`context.source`). + - `Json::try_fast_stringify`: Direct single-buffer `Vec` serializer fast-path in `JSON.stringify` when no `replacer` or `space` argument is supplied, traversing ordinary `JsObject`s (via `Shape::with_property_keys` + direct storage slots) and dense `Array`s (`DenseI32`, `DenseF64`, `DenseElement`) with `ryu_js` number formatting, inline `\uXXXX` escaping for lone UTF-16 surrogates, and automatic fallback for `toJSON`, getters, or exotic objects. - [`vendor/boa_engine/src/object/shape/mod.rs`](./src/object/shape/mod.rs), [`vendor/boa_engine/src/object/shape/shared_shape/mod.rs`](./src/object/shape/shared_shape/mod.rs), & [`vendor/boa_engine/src/object/shape/unique_shape.rs`](./src/object/shape/unique_shape.rs): - `Shape::with_property_keys`: Zero-allocation slice/iterator callback over ordinary object property keys for `Json::try_fast_stringify`. - [`vendor/boa_engine/src/vm/inline_cache/mod.rs`](./src/vm/inline_cache/mod.rs): diff --git a/vendor/boa_engine/src/builtins/json/mod.rs b/vendor/boa_engine/src/builtins/json/mod.rs index af98f58..0e7ae9d 100644 --- a/vendor/boa_engine/src/builtins/json/mod.rs +++ b/vendor/boa_engine/src/builtins/json/mod.rs @@ -58,6 +58,7 @@ trait JsonInput<'a>: Copy { fn code_unit(self, idx: usize) -> u16; fn slice_to_js_string(self, start: usize, end: usize) -> JsString; fn ascii_bytes(self, start: usize, end: usize) -> Cow<'a, [u8]>; + fn latin1_slice(self, start: usize, end: usize) -> Option<&'a [u8]>; } impl<'a> JsonInput<'a> for &'a [u8] { @@ -80,6 +81,11 @@ impl<'a> JsonInput<'a> for &'a [u8] { fn ascii_bytes(self, start: usize, end: usize) -> Cow<'a, [u8]> { Cow::Borrowed(&self[start..end]) } + + #[inline] + fn latin1_slice(self, start: usize, end: usize) -> Option<&'a [u8]> { + Some(&self[start..end]) + } } impl<'a> JsonInput<'a> for &'a [u16] { @@ -102,6 +108,11 @@ impl<'a> JsonInput<'a> for &'a [u16] { fn ascii_bytes(self, start: usize, end: usize) -> Cow<'a, [u8]> { Cow::Owned(self[start..end].iter().map(|&u| u as u8).collect()) } + + #[inline] + fn latin1_slice(self, _start: usize, _end: usize) -> Option<&'a [u8]> { + None + } } #[derive(Debug)] @@ -212,17 +223,29 @@ impl Json { } } - /// Direct ECMA-404 recursive-descent JSON parser over `Latin1(&[u8])` or `Utf16(&[u16])`. + /// Direct ECMA-404 iterative JSON parser over `Latin1(&[u8])` or `Utf16(&[u16])`. /// /// - Uses `fast-float2` for exact IEEE-754 round-to-nearest-even `f64` conversion and `±Infinity` on overflow. /// - Preserves lone UTF-16 surrogates (`\uD800`–`\uDFFF` and literal unpaired surrogates). /// - Zero-copy `JsStr::latin1` / `utf16` slicing for unescaped strings. - /// - Supports nesting up to 512 levels without stack overflow. + /// - Uses an explicit heap-allocated container stack (`O(1)` C/Rust call stack) supporting up to 2048 nesting levels. fn parse_ecma404<'a, I: JsonInput<'a>, const WITH_SOURCE: bool>( input: I, context: &mut Context, ) -> JsResult<(JsValue, JsonNode)> { - const MAX_DEPTH: usize = 512; + const MAX_DEPTH: usize = 2048; + + enum ContainerFrame { + Array { + elements: thin_vec::ThinVec, + child_nodes: Vec, + }, + Object { + obj: JsObject, + current_key: JsString, + entries: Vec<(JsString, JsonNode)>, + }, + } struct ParserState<'a, I> { input: I, @@ -260,104 +283,302 @@ impl Json { JsonNode::Primitive(js_string!(), JsValue::undefined()) } - fn parse_value( + fn parse_root( &mut self, - depth: usize, context: &mut Context, ) -> JsResult<(JsValue, JsonNode)> { - if depth > MAX_DEPTH { - return Err(self.syntax_err("JSON nesting depth exceeded")); - } - self.skip_ws(); - if self.pos >= self.input.len() { - return Err(self.syntax_err("unexpected end of JSON input")); - } - match self.input.code_unit(self.pos) { - 0x22 => { - let start = self.pos; - let s = self.parse_string()?; - let val = JsValue::new(s); - let node = if SRC { - JsonNode::Primitive( - self.input.slice_to_js_string(start, self.pos), - val.clone(), - ) - } else { - Self::dummy_node() - }; - Ok((val, node)) + let mut stack: Vec = Vec::new(); + + 'parse_val: loop { + self.skip_ws(); + if self.pos >= self.input.len() { + return Err(self.syntax_err("unexpected end of JSON input")); } - 0x7B => self.parse_object::(depth + 1, context), - 0x5B => self.parse_array::(depth + 1, context), - 0x74 => { - if self.pos + 4 <= self.input.len() - && self.input.code_unit(self.pos + 1) == 0x72 - && self.input.code_unit(self.pos + 2) == 0x75 - && self.input.code_unit(self.pos + 3) == 0x65 - { - self.pos += 4; - let val = JsValue::new(true); + let (mut cur_val, mut cur_node) = match self.input.code_unit(self.pos) { + 0x22 => { + let start = self.pos; + let s = self.parse_string()?; + let val = JsValue::new(s); let node = if SRC { - JsonNode::Primitive(js_string!("true"), val.clone()) + JsonNode::Primitive( + self.input.slice_to_js_string(start, self.pos), + val.clone(), + ) } else { Self::dummy_node() }; - Ok((val, node)) - } else { - Err(self.syntax_err("expected 'true'")) + (val, node) } - } - 0x66 => { - if self.pos + 5 <= self.input.len() - && self.input.code_unit(self.pos + 1) == 0x61 - && self.input.code_unit(self.pos + 2) == 0x6C - && self.input.code_unit(self.pos + 3) == 0x73 - && self.input.code_unit(self.pos + 4) == 0x65 - { - self.pos += 5; - let val = JsValue::new(false); - let node = if SRC { - JsonNode::Primitive(js_string!("false"), val.clone()) + 0x5B => { + if stack.len() >= MAX_DEPTH { + return Err(self.syntax_err("JSON nesting depth exceeded")); + } + self.pos += 1; + self.skip_ws(); + if self.pos < self.input.len() && self.input.code_unit(self.pos) == 0x5D + { + self.pos += 1; + let arr = crate::builtins::Array::array_create(0, None, context)?; + let node = if SRC { + JsonNode::Array(Vec::new()) + } else { + Self::dummy_node() + }; + (arr.into(), node) } else { - Self::dummy_node() - }; - Ok((val, node)) - } else { - Err(self.syntax_err("expected 'false'")) + stack.push(ContainerFrame::Array { + elements: thin_vec::ThinVec::new(), + child_nodes: Vec::new(), + }); + continue 'parse_val; + } } - } - 0x6E => { - if self.pos + 4 <= self.input.len() - && self.input.code_unit(self.pos + 1) == 0x75 - && self.input.code_unit(self.pos + 2) == 0x6C - && self.input.code_unit(self.pos + 3) == 0x6C - { - self.pos += 4; - let val = JsValue::null(); + 0x7B => { + if stack.len() >= MAX_DEPTH { + return Err(self.syntax_err("JSON nesting depth exceeded")); + } + self.pos += 1; + self.skip_ws(); + let obj = JsObject::with_object_proto(context.intrinsics()); + if self.pos < self.input.len() && self.input.code_unit(self.pos) == 0x7D + { + self.pos += 1; + let node = if SRC { + JsonNode::Object(Vec::new()) + } else { + Self::dummy_node() + }; + (obj.into(), node) + } else { + if self.pos >= self.input.len() + || self.input.code_unit(self.pos) != 0x22 + { + return Err( + self.syntax_err("expected string key in JSON object") + ); + } + let key = self.parse_string()?; + self.skip_ws(); + if self.pos >= self.input.len() + || self.input.code_unit(self.pos) != 0x3A + { + return Err( + self.syntax_err("expected ':' after key in JSON object") + ); + } + self.pos += 1; + stack.push(ContainerFrame::Object { + obj, + current_key: key, + entries: Vec::new(), + }); + continue 'parse_val; + } + } + 0x74 => { + if self.pos + 4 <= self.input.len() + && self.input.code_unit(self.pos + 1) == 0x72 + && self.input.code_unit(self.pos + 2) == 0x75 + && self.input.code_unit(self.pos + 3) == 0x65 + { + self.pos += 4; + let val = JsValue::new(true); + let node = if SRC { + JsonNode::Primitive(js_string!("true"), val.clone()) + } else { + Self::dummy_node() + }; + (val, node) + } else { + return Err(self.syntax_err("expected 'true'")); + } + } + 0x66 => { + if self.pos + 5 <= self.input.len() + && self.input.code_unit(self.pos + 1) == 0x61 + && self.input.code_unit(self.pos + 2) == 0x6C + && self.input.code_unit(self.pos + 3) == 0x73 + && self.input.code_unit(self.pos + 4) == 0x65 + { + self.pos += 5; + let val = JsValue::new(false); + let node = if SRC { + JsonNode::Primitive(js_string!("false"), val.clone()) + } else { + Self::dummy_node() + }; + (val, node) + } else { + return Err(self.syntax_err("expected 'false'")); + } + } + 0x6E => { + if self.pos + 4 <= self.input.len() + && self.input.code_unit(self.pos + 1) == 0x75 + && self.input.code_unit(self.pos + 2) == 0x6C + && self.input.code_unit(self.pos + 3) == 0x6C + { + self.pos += 4; + let val = JsValue::null(); + let node = if SRC { + JsonNode::Primitive(js_string!("null"), val.clone()) + } else { + Self::dummy_node() + }; + (val, node) + } else { + return Err(self.syntax_err("expected 'null'")); + } + } + 0x2D | 0x30..=0x39 => { + let start = self.pos; + let val = self.parse_number()?; let node = if SRC { - JsonNode::Primitive(js_string!("null"), val.clone()) + JsonNode::Primitive( + self.input.slice_to_js_string(start, self.pos), + val.clone(), + ) } else { Self::dummy_node() }; - Ok((val, node)) - } else { - Err(self.syntax_err("expected 'null'")) + (val, node) } - } - 0x2D | 0x30..=0x39 => { - let start = self.pos; - let val = self.parse_number()?; - let node = if SRC { - JsonNode::Primitive( - self.input.slice_to_js_string(start, self.pos), - val.clone(), - ) - } else { - Self::dummy_node() + _ => return Err(self.syntax_err("unexpected token in JSON")), + }; + + 'attach: loop { + let Some(top) = stack.last_mut() else { + return Ok((cur_val, cur_node)); }; - Ok((val, node)) + match top { + ContainerFrame::Array { + elements, + child_nodes, + } => { + elements.push(cur_val); + if SRC { + child_nodes.push(cur_node); + } + self.skip_ws(); + if self.pos >= self.input.len() { + return Err(self.syntax_err("unterminated array in JSON")); + } + match self.input.code_unit(self.pos) { + 0x2C => { + self.pos += 1; + self.skip_ws(); + if self.pos < self.input.len() + && self.input.code_unit(self.pos) == 0x5D + { + return Err( + self.syntax_err("trailing comma in JSON array") + ); + } + continue 'parse_val; + } + 0x5D => { + self.pos += 1; + let Some(ContainerFrame::Array { + elements, + child_nodes, + }) = stack.pop() + else { + unreachable!() + }; + let arr = crate::builtins::Array::array_create( + elements.len() as u64, + None, + context, + )?; + arr.borrow_mut() + .properties_mut() + .override_indexed_properties(elements); + cur_val = arr.into(); + cur_node = if SRC { + JsonNode::Array(child_nodes) + } else { + Self::dummy_node() + }; + continue 'attach; + } + _ => { + return Err( + self.syntax_err("expected ',' or ']' in JSON array") + ); + } + } + } + ContainerFrame::Object { + obj, + current_key, + entries, + } => { + let key = std::mem::take(current_key); + if SRC { + entries.push((key.clone(), cur_node)); + } + obj.borrow_mut().properties_mut().insert_data_property( + crate::property::PropertyKey::from(key), + cur_val, + ); + self.skip_ws(); + if self.pos >= self.input.len() { + return Err(self.syntax_err("unterminated object in JSON")); + } + match self.input.code_unit(self.pos) { + 0x2C => { + self.pos += 1; + self.skip_ws(); + if self.pos < self.input.len() + && self.input.code_unit(self.pos) == 0x7D + { + return Err( + self.syntax_err("trailing comma in JSON object") + ); + } + if self.pos >= self.input.len() + || self.input.code_unit(self.pos) != 0x22 + { + return Err(self + .syntax_err("expected string key in JSON object")); + } + let next_key = self.parse_string()?; + self.skip_ws(); + if self.pos >= self.input.len() + || self.input.code_unit(self.pos) != 0x3A + { + return Err(self.syntax_err( + "expected ':' after key in JSON object", + )); + } + self.pos += 1; + *current_key = next_key; + continue 'parse_val; + } + 0x7D => { + self.pos += 1; + let Some(ContainerFrame::Object { obj, entries, .. }) = + stack.pop() + else { + unreachable!() + }; + cur_val = obj.into(); + cur_node = if SRC { + JsonNode::Object(entries) + } else { + Self::dummy_node() + }; + continue 'attach; + } + _ => { + return Err( + self.syntax_err("expected ',' or '}' in JSON object") + ); + } + } + } + } } - _ => Err(self.syntax_err("unexpected token in JSON")), } } @@ -387,11 +608,103 @@ impl Json { fn parse_string_escaped(&mut self, content_start: usize) -> JsResult { let len = self.input.len(); + if let Some(prefix_bytes) = self.input.latin1_slice(content_start, self.pos) { + let mut latin1 = Vec::with_capacity(prefix_bytes.len() + 16); + latin1.extend_from_slice(prefix_bytes); + + while self.pos < len { + let c = self.input.code_unit(self.pos); + if c == 0x22 { + self.pos += 1; + return Ok(JsString::from(boa_string::JsStr::latin1(&latin1))); + } + if c < 0x20 { + return Err( + self.syntax_err("unescaped control character in JSON string") + ); + } + if c == 0x5C { + self.pos += 1; + if self.pos >= len { + return Err( + self.syntax_err("unterminated escape sequence in JSON") + ); + } + let esc = self.input.code_unit(self.pos); + self.pos += 1; + match esc { + 0x22 => latin1.push(0x22), + 0x5C => latin1.push(0x5C), + 0x2F => latin1.push(0x2F), + 0x62 => latin1.push(0x08), + 0x66 => latin1.push(0x0C), + 0x6E => latin1.push(0x0A), + 0x72 => latin1.push(0x0D), + 0x74 => latin1.push(0x09), + 0x75 => { + if self.pos + 4 > len { + return Err( + self.syntax_err("incomplete unicode escape in JSON") + ); + } + let mut code: u16 = 0; + for _ in 0..4 { + let hex = self.input.code_unit(self.pos); + self.pos += 1; + let nibble = match hex { + 0x30..=0x39 => hex - 0x30, + 0x61..=0x66 => hex - 0x61 + 10, + 0x41..=0x46 => hex - 0x41 + 10, + _ => { + return Err(self.syntax_err( + "invalid unicode hex digit in JSON", + )); + } + }; + code = (code << 4) | nibble; + } + if code <= 0xFF { + latin1.push(code as u8); + } else { + let mut units: Vec = + latin1.iter().map(|&b| u16::from(b)).collect(); + units.push(code); + return self.parse_string_escaped_utf16_tail(units); + } + } + _ => { + return Err( + self.syntax_err("invalid escape character in JSON") + ); + } + } + } else { + let chunk_start = self.pos; + self.pos += 1; + while self.pos < len { + let ch = self.input.code_unit(self.pos); + if ch == 0x22 || ch == 0x5C || ch < 0x20 { + break; + } + self.pos += 1; + } + if let Some(chunk) = self.input.latin1_slice(chunk_start, self.pos) { + latin1.extend_from_slice(chunk); + } + } + } + return Err(self.syntax_err("unterminated string in JSON")); + } + let mut units = Vec::with_capacity((self.pos - content_start) + 16); for idx in content_start..self.pos { units.push(self.input.code_unit(idx)); } + self.parse_string_escaped_utf16_tail(units) + } + fn parse_string_escaped_utf16_tail(&mut self, mut units: Vec) -> JsResult { + let len = self.input.len(); while self.pos < len { let c = self.input.code_unit(self.pos); if c == 0x22 { @@ -512,8 +825,8 @@ impl Json { if is_neg && bytes.len() == 2 && bytes[1] == b'0' { return Ok(JsValue::new(-0.0f64)); } - if bytes.len() <= 10 { - let digits = if is_neg { &bytes[1..] } else { bytes }; + let digits = if is_neg { &bytes[1..] } else { bytes }; + if digits.len() <= 15 { let mut acc: i64 = 0; for &b in digits { acc = acc * 10 + i64::from(b - b'0'); @@ -532,134 +845,6 @@ impl Json { .map_err(|_| self.syntax_err("invalid number in JSON"))?; Ok(JsValue::new(f)) } - - fn parse_array( - &mut self, - depth: usize, - context: &mut Context, - ) -> JsResult<(JsValue, JsonNode)> { - debug_assert_eq!(self.input.code_unit(self.pos), 0x5B); - self.pos += 1; - self.skip_ws(); - if self.pos < self.input.len() && self.input.code_unit(self.pos) == 0x5D { - self.pos += 1; - let arr = crate::builtins::Array::array_create(0, None, context)?; - let node = if SRC { - JsonNode::Array(Vec::new()) - } else { - Self::dummy_node() - }; - return Ok((arr.into(), node)); - } - - let mut elements = thin_vec::ThinVec::new(); - let mut child_nodes = if SRC { Vec::new() } else { Vec::new() }; - - loop { - let (elem, child_node) = self.parse_value::(depth, context)?; - elements.push(elem); - if SRC { - child_nodes.push(child_node); - } - self.skip_ws(); - if self.pos >= self.input.len() { - return Err(self.syntax_err("unterminated array in JSON")); - } - match self.input.code_unit(self.pos) { - 0x2C => { - self.pos += 1; - self.skip_ws(); - if self.pos < self.input.len() && self.input.code_unit(self.pos) == 0x5D - { - return Err(self.syntax_err("trailing comma in JSON array")); - } - } - 0x5D => { - self.pos += 1; - break; - } - _ => return Err(self.syntax_err("expected ',' or ']' in JSON array")), - } - } - - let arr = - crate::builtins::Array::array_create(elements.len() as u64, None, context)?; - arr.borrow_mut() - .properties_mut() - .override_indexed_properties(elements); - let node = if SRC { - JsonNode::Array(child_nodes) - } else { - Self::dummy_node() - }; - Ok((arr.into(), node)) - } - - fn parse_object( - &mut self, - depth: usize, - context: &mut Context, - ) -> JsResult<(JsValue, JsonNode)> { - debug_assert_eq!(self.input.code_unit(self.pos), 0x7B); - self.pos += 1; - self.skip_ws(); - let obj = JsObject::with_object_proto(context.intrinsics()); - if self.pos < self.input.len() && self.input.code_unit(self.pos) == 0x7D { - self.pos += 1; - let node = if SRC { - JsonNode::Object(Vec::new()) - } else { - Self::dummy_node() - }; - return Ok((obj.into(), node)); - } - - let mut entries = if SRC { Vec::new() } else { Vec::new() }; - - loop { - self.skip_ws(); - if self.pos >= self.input.len() || self.input.code_unit(self.pos) != 0x22 { - return Err(self.syntax_err("expected string key in JSON object")); - } - let key = self.parse_string()?; - self.skip_ws(); - if self.pos >= self.input.len() || self.input.code_unit(self.pos) != 0x3A { - return Err(self.syntax_err("expected ':' after key in JSON object")); - } - self.pos += 1; - let (val, child_node) = self.parse_value::(depth, context)?; - if SRC { - entries.push((key.clone(), child_node)); - } - obj.create_data_property_or_throw(key, val, context)?; - self.skip_ws(); - if self.pos >= self.input.len() { - return Err(self.syntax_err("unterminated object in JSON")); - } - match self.input.code_unit(self.pos) { - 0x2C => { - self.pos += 1; - self.skip_ws(); - if self.pos < self.input.len() && self.input.code_unit(self.pos) == 0x7D - { - return Err(self.syntax_err("trailing comma in JSON object")); - } - } - 0x7D => { - self.pos += 1; - break; - } - _ => return Err(self.syntax_err("expected ',' or '}' in JSON object")), - } - } - - let node = if SRC { - JsonNode::Object(entries) - } else { - Self::dummy_node() - }; - Ok((obj.into(), node)) - } } let mut state = ParserState { @@ -667,7 +852,7 @@ impl Json { pos: 0, _marker: std::marker::PhantomData, }; - let res = state.parse_value::(0, context)?; + let res = state.parse_root::(context)?; state.skip_ws(); if state.pos != state.input.len() { return Err(state.syntax_err("unexpected trailing characters in JSON")); @@ -1143,8 +1328,20 @@ impl Json { buf.push(b'"'); let mut utf8_tmp = [0u8; 4]; for decoded in char::decode_utf16(units.iter().copied()) { - let Ok(c) = decoded else { - return false; + let c = match decoded { + Ok(c) => c, + Err(unpaired) => { + let u = unpaired.unpaired_surrogate(); + buf.extend_from_slice(&[ + b'\\', + b'u', + HEX[((u >> 12) & 0xF) as usize], + HEX[((u >> 8) & 0xF) as usize], + HEX[((u >> 4) & 0xF) as usize], + HEX[(u & 0xF) as usize], + ]); + continue; + } }; match c { '"' => buf.extend_from_slice(b"\\\""), diff --git a/vendor/boa_engine/src/object/property_map.rs b/vendor/boa_engine/src/object/property_map.rs index 80cf440..e16254d 100644 --- a/vendor/boa_engine/src/object/property_map.rs +++ b/vendor/boa_engine/src/object/property_map.rs @@ -592,6 +592,41 @@ impl PropertyMap { self.insert_with_slot(key, property, &mut dummy_slot) } + /// Fast path for inserting a standard data property (`writable: true`, `enumerable: true`, + /// `configurable: true`) on a freshly created ordinary object (such as in `JSON.parse`), + /// avoiding `PropertyKey`/`JsValue` clones and `PropertyDescriptor` construction. + #[inline] + pub(crate) fn insert_data_property(&mut self, key: PropertyKey, value: JsValue) { + if let PropertyKey::Index(index) = &key { + let desc = PropertyDescriptorBuilder::new() + .value(value) + .writable(true) + .enumerable(true) + .configurable(true) + .build(); + self.indexed_properties.insert(index.get(), desc); + return; + } + + let attributes = + SlotAttributes::WRITABLE | SlotAttributes::ENUMERABLE | SlotAttributes::CONFIGURABLE; + + if let Some(slot) = self.shape.lookup(&key) { + let index = slot.index as usize; + if slot.attributes == attributes { + self.storage[index] = value; + return; + } + } + + let transition_key = TransitionKey { + property_key: key, + attributes, + }; + self.shape = self.shape.insert_property_transition(transition_key); + self.storage.push(value); + } + /// Insert the given property descriptor with the given key [`PropertyMap`]. pub(crate) fn insert_with_slot( &mut self,