From cdd709ef0367a8ffee07a430ed842da464d20ed3 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Wed, 24 Jun 2026 10:46:59 +0800 Subject: [PATCH 01/32] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=9F=BA?= =?UTF-8?q?=E5=87=86=E6=B5=8B=E8=AF=95=E5=9F=BA=E7=A1=80=E8=AE=BE=E6=96=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BENCHMARK_BASELINE.md | 7 + Cargo.lock | 175 ++++++++++++++++++ benchmark_baseline.json | 92 ++++++++++ crates/oxide_cli/Cargo.toml | 2 + crates/oxide_cli/src/bench/baseline.rs | 94 ++++++++++ crates/oxide_cli/src/bench/js_stress.rs | 208 ++++++++++++++++++++++ crates/oxide_cli/src/bench/leak_detect.rs | 141 +++++++++++++++ crates/oxide_cli/src/bench/metrics.rs | 43 +++++ crates/oxide_cli/src/bench/mod.rs | 34 ++++ crates/oxide_cli/src/bench/output.rs | 40 +++++ crates/oxide_cli/src/bench/rust_bench.rs | 20 +++ crates/oxide_cli/src/main.rs | 41 ++++- crates/oxide_cli/tests/cli_smoke.rs | 10 +- crates/oxide_code_cache/src/lib.rs | 8 + crates/oxide_test262/src/main.rs | 24 ++- crates/oxide_vm/Cargo.toml | 21 +++ crates/oxide_vm/benches/call.rs | 27 +++ crates/oxide_vm/benches/coercion.rs | 27 +++ crates/oxide_vm/benches/dispatch.rs | 27 +++ crates/oxide_vm/benches/gc_mark_sweep.rs | 29 +++ crates/oxide_vm/benches/property.rs | 27 +++ crates/oxide_vm/src/dispatch/property.rs | 5 + crates/oxide_vm/src/vm.rs | 47 ++++- crates/oxide_vm/src/vm_support.rs | 6 + tests/stress/array.js | 11 ++ tests/stress/call.js | 7 + tests/stress/code_forge.js | 6 + tests/stress/coercion.js | 7 + tests/stress/gc.js | 8 + tests/stress/ic_cache.js | 13 ++ tests/stress/property.js | 11 ++ tests/stress/string.js | 7 + 32 files changed, 1215 insertions(+), 10 deletions(-) create mode 100644 BENCHMARK_BASELINE.md create mode 100644 benchmark_baseline.json create mode 100644 crates/oxide_cli/src/bench/baseline.rs create mode 100644 crates/oxide_cli/src/bench/js_stress.rs create mode 100644 crates/oxide_cli/src/bench/leak_detect.rs create mode 100644 crates/oxide_cli/src/bench/metrics.rs create mode 100644 crates/oxide_cli/src/bench/mod.rs create mode 100644 crates/oxide_cli/src/bench/output.rs create mode 100644 crates/oxide_cli/src/bench/rust_bench.rs create mode 100644 crates/oxide_vm/benches/call.rs create mode 100644 crates/oxide_vm/benches/coercion.rs create mode 100644 crates/oxide_vm/benches/dispatch.rs create mode 100644 crates/oxide_vm/benches/gc_mark_sweep.rs create mode 100644 crates/oxide_vm/benches/property.rs create mode 100644 tests/stress/array.js create mode 100644 tests/stress/call.js create mode 100644 tests/stress/code_forge.js create mode 100644 tests/stress/coercion.js create mode 100644 tests/stress/gc.js create mode 100644 tests/stress/ic_cache.js create mode 100644 tests/stress/property.js create mode 100644 tests/stress/string.js diff --git a/BENCHMARK_BASELINE.md b/BENCHMARK_BASELINE.md new file mode 100644 index 00000000..4fe890d1 --- /dev/null +++ b/BENCHMARK_BASELINE.md @@ -0,0 +1,7 @@ +Test Name Wall(ms) Comp(ms) Exec(ms) Instrs GC Ct IC Rate Sess Objs Sess B +------------------------------------------------------------------------------------------------------------------------ +array 0.19 0.000 0.193 291 0 0.0% 0 0 +call 0.06 0.000 0.058 157 0 0.0% 0 0 +coercion 0.03 0.000 0.027 163 0 0.0% 0 0 +gc 0.26 0.000 0.254 343 0 100.0% 0 0 +string 0.05 0.000 0.049 115 0 100.0% 0 0 diff --git a/Cargo.lock b/Cargo.lock index 96e123d1..79804039 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "ansi_term" version = "0.12.1" @@ -136,6 +142,12 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "castaway" version = "0.2.4" @@ -178,6 +190,33 @@ dependencies = [ "windows-link", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "clap" version = "4.6.1" @@ -259,6 +298,42 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79" +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -284,6 +359,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "dashmap" version = "6.2.1" @@ -428,6 +509,17 @@ dependencies = [ "wasip3", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -462,6 +554,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "home" version = "0.5.12" @@ -513,12 +611,32 @@ dependencies = [ "serde_core", ] +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -667,6 +785,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "owo-colors" version = "4.3.0" @@ -908,6 +1032,8 @@ dependencies = [ "oxide_types", "oxide_vm", "rustyline", + "serde", + "serde_json", ] [[package]] @@ -988,6 +1114,7 @@ version = "0.1.0" dependencies = [ "bumpalo", "chrono", + "criterion", "fast-float", "indexmap", "memchr", @@ -1073,6 +1200,34 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1516,6 +1671,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.11.0" @@ -1789,6 +1954,16 @@ dependencies = [ "semver", ] +[[package]] +name = "web-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/benchmark_baseline.json b/benchmark_baseline.json new file mode 100644 index 00000000..1c37c9dd --- /dev/null +++ b/benchmark_baseline.json @@ -0,0 +1,92 @@ +[ + { + "test_name": "array", + "wall_time_us": 193, + "session_objects": 0, + "session_bytes": 0, + "epoch_objects": 1, + "epoch_bytes": 0, + "gc_trigger_count": 0, + "gc_bytes_freed": 0, + "gc_objects_scanned": 0, + "gc_collection_us": 0, + "instruction_count": 291, + "compile_time_us": 0, + "exec_time_us": 193, + "ic_hit_rate": 0.0, + "ic_hits": 0, + "ic_misses": 0 + }, + { + "test_name": "call", + "wall_time_us": 59, + "session_objects": 0, + "session_bytes": 0, + "epoch_objects": 2, + "epoch_bytes": 0, + "gc_trigger_count": 0, + "gc_bytes_freed": 0, + "gc_objects_scanned": 0, + "gc_collection_us": 0, + "instruction_count": 157, + "compile_time_us": 0, + "exec_time_us": 58, + "ic_hit_rate": 0.0, + "ic_hits": 0, + "ic_misses": 0 + }, + { + "test_name": "coercion", + "wall_time_us": 28, + "session_objects": 0, + "session_bytes": 0, + "epoch_objects": 0, + "epoch_bytes": 0, + "gc_trigger_count": 0, + "gc_bytes_freed": 0, + "gc_objects_scanned": 0, + "gc_collection_us": 0, + "instruction_count": 163, + "compile_time_us": 0, + "exec_time_us": 27, + "ic_hit_rate": 0.0, + "ic_hits": 0, + "ic_misses": 0 + }, + { + "test_name": "gc", + "wall_time_us": 255, + "session_objects": 0, + "session_bytes": 0, + "epoch_objects": 0, + "epoch_bytes": 0, + "gc_trigger_count": 0, + "gc_bytes_freed": 0, + "gc_objects_scanned": 0, + "gc_collection_us": 0, + "instruction_count": 343, + "compile_time_us": 0, + "exec_time_us": 254, + "ic_hit_rate": 1.0, + "ic_hits": 135, + "ic_misses": 0 + }, + { + "test_name": "string", + "wall_time_us": 50, + "session_objects": 0, + "session_bytes": 0, + "epoch_objects": 0, + "epoch_bytes": 0, + "gc_trigger_count": 0, + "gc_bytes_freed": 0, + "gc_objects_scanned": 0, + "gc_collection_us": 0, + "instruction_count": 115, + "compile_time_us": 0, + "exec_time_us": 49, + "ic_hit_rate": 1.0, + "ic_hits": 216, + "ic_misses": 0 + } +] \ No newline at end of file diff --git a/crates/oxide_cli/Cargo.toml b/crates/oxide_cli/Cargo.toml index b87ca2fc..de2be463 100644 --- a/crates/oxide_cli/Cargo.toml +++ b/crates/oxide_cli/Cargo.toml @@ -22,3 +22,5 @@ oxide_api = { path = "../oxide_api" } clap.workspace = true rustyline.workspace = true ansi_term.workspace = true +serde_json.workspace = true +serde = { workspace = true, features = ["derive"] } diff --git a/crates/oxide_cli/src/bench/baseline.rs b/crates/oxide_cli/src/bench/baseline.rs new file mode 100644 index 00000000..110e469b --- /dev/null +++ b/crates/oxide_cli/src/bench/baseline.rs @@ -0,0 +1,94 @@ +use crate::bench::metrics::MetricCollection; + +pub struct Baseline { + pub entries: Vec, +} + +impl Baseline { + pub fn empty() -> Self { + Self { entries: Vec::new() } + } +} + +pub struct Regression { + pub metric: String, + pub test_name: String, + pub baseline: f64, + pub current: f64, + pub ratio: f64, + pub tolerance: f64, +} + +pub fn load_baseline() -> Result { + let json_path = "benchmark_baseline.json"; + let data = match std::fs::read_to_string(json_path) { + Ok(d) => d, + Err(_) => { + eprintln!("No baseline found at {} — run --update-baseline first", json_path); + return Ok(Baseline::empty()); + } + }; + let entries: Vec = + serde_json::from_str(&data).map_err(|e| format!("Failed to parse baseline: {}", e))?; + Ok(Baseline { entries }) +} + +pub fn save_baseline(results: &[MetricCollection]) -> Result<(), String> { + let json = serde_json::to_string_pretty(results).map_err(|e| format!("Failed to serialize baseline: {}", e))?; + std::fs::write("benchmark_baseline.json", &json) + .map_err(|e| format!("Failed to write benchmark_baseline.json: {}", e))?; + let md = crate::bench::output::format_text_table(results); + std::fs::write("BENCHMARK_BASELINE.md", md).map_err(|e| format!("Failed to write BENCHMARK_BASELINE.md: {}", e))?; + Ok(()) +} + +pub fn compare_baseline(current: &[MetricCollection]) -> Vec { + let baseline = match load_baseline() { + Ok(b) => b, + Err(_) => return Vec::new(), + }; + if baseline.entries.is_empty() { + eprintln!("No baseline data — skipping regression check"); + return Vec::new(); + } + let mut regressions = Vec::new(); + for cur in current { + for base in &baseline.entries { + if base.test_name != cur.test_name { + continue; + } + for (name, cur_val) in cur.iter_metrics() { + if cur_val == 0.0 { + continue; + } + let base_val = base + .iter_metrics() + .iter() + .find(|(n, _)| *n == name) + .map(|(_, v)| *v) + .unwrap_or(cur_val); + if base_val == 0.0 { + continue; + } + let tolerance = match name { + "wall_time_us" => 0.10, + "gc_trigger_count" | "gc_bytes_freed" | "gc_objects_scanned" | "gc_collection_us" => 0.50, + "session_bytes" => 0.20, + _ => 0.20, + }; + let ratio = cur_val / base_val; + if ratio > 1.0 + tolerance { + regressions.push(Regression { + metric: name.to_string(), + test_name: cur.test_name.clone(), + baseline: base_val, + current: cur_val, + ratio, + tolerance, + }); + } + } + } + } + regressions +} diff --git a/crates/oxide_cli/src/bench/js_stress.rs b/crates/oxide_cli/src/bench/js_stress.rs new file mode 100644 index 00000000..0616be8f --- /dev/null +++ b/crates/oxide_cli/src/bench/js_stress.rs @@ -0,0 +1,208 @@ +use std::fs; +use std::process::ExitCode; +use std::sync::Arc; +use std::time::Instant; + +use oxide_compiler::compiler::{compiled_module_hash, Compiler}; +use oxide_kernel::kernel::KernelCore; +use oxide_parser::Allocator; +use oxide_vm::vm_pool::VmPool; + +use crate::bench::baseline::{compare_baseline, save_baseline}; +use crate::bench::metrics::MetricCollection; +use crate::bench::output::{format_json, format_text_table}; +use crate::bench::BenchConfig; + +pub fn run_js_stress_bench(config: &BenchConfig, kernel: &Arc, pool: &Arc) -> ExitCode { + let mut results: Vec = Vec::new(); + let compiler = Compiler::new(); + + let test_dir = "tests/stress"; + let mut files: Vec<_> = match fs::read_dir(test_dir) { + Ok(entries) => entries + .filter_map(|e| e.ok()) + .filter(|e| { + e.path().extension().is_some_and(|ext| ext == "js") + && config + .filter + .as_ref() + .map_or(true, |f| e.file_name().to_string_lossy().contains(f.as_str())) + }) + .map(|e| e.path()) + .collect(), + Err(e) => { + eprintln!("Could not read {}: {}", test_dir, e); + return ExitCode::FAILURE; + } + }; + files.sort(); + + if files.is_empty() { + eprintln!("No matching stress tests found in {}", test_dir); + return ExitCode::FAILURE; + } + + for path in &files { + let test_name = path.file_stem().unwrap_or_default().to_string_lossy().to_string(); + let mut js = match fs::read_to_string(path) { + Ok(s) => s, + Err(e) => { + eprintln!("Failed to read {:?}: {}", path, e); + continue; + } + }; + + if config.iterations > 0 { + js = override_iterations(&js, config.iterations); + } + + let allocator = Allocator::default(); + let program = match oxide_parser::parse(&allocator, &js) { + Ok(p) => p, + Err(errors) => { + eprintln!("Parse error in {}: {:?}", test_name, errors.first()); + continue; + } + }; + + let hash = compiled_module_hash(&program); + let module = match kernel.code_forge().get_or_insert_with(hash, || compiler.compile(&program)) { + Ok(m) => m, + Err(e) => { + eprintln!("Compile error in {}: {}", test_name, e); + continue; + } + }; + + for _ in 0..config.warmup { + let mut guard = pool.spawn(); + let _ = guard.vm_mut().run(&module); + } + + let mut metrics = Vec::new(); + for _ in 0..config.iterations { + let compile_start = Instant::now(); + let mut guard = pool.spawn(); + let vm = guard.vm_mut(); + let compile_time = compile_start.elapsed(); + + let pre_session = vm.session_object_count(); + let pre_epoch = vm.epoch_object_count(); + + let exec_start = Instant::now(); + let _result = vm.run(&module); + let exec_time = exec_start.elapsed(); + + let gc_stats = vm.session_gc_stats(); + + metrics.push(MetricCollection { + test_name: test_name.clone(), + wall_time_us: (compile_time + exec_time).as_micros() as u64, + session_objects: vm.session_object_count().saturating_sub(pre_session), + session_bytes: vm.session_bytes_allocated(), + epoch_objects: vm.epoch_object_count().saturating_sub(pre_epoch), + epoch_bytes: 0, + gc_trigger_count: gc_stats.total_collections, + gc_bytes_freed: gc_stats.last_collection_bytes_freed, + gc_objects_scanned: gc_stats.last_collection_objects_scanned, + gc_collection_us: gc_stats.last_collection_duration_us, + instruction_count: vm.instruction_count(), + compile_time_us: compile_time.as_micros() as u64, + exec_time_us: exec_time.as_micros() as u64, + ic_hit_rate: vm.ic_hit_rate(), + ic_hits: vm.ic_hit_count(), + ic_misses: vm.ic_miss_count(), + }); + } + + let avg = average_metrics(&metrics); + results.push(avg); + } + + let json = format_json(&results); + let table = format_text_table(&results); + eprintln!("{}", table); + println!("{}", json); + + if config.update_baseline { + if let Err(e) = save_baseline(&results) { + eprintln!("Failed to save baseline: {}", e); + return ExitCode::FAILURE; + } + eprintln!("Baseline saved to BENCHMARK_BASELINE.md + benchmark_baseline.json"); + } else { + let regressions = compare_baseline(&results); + if !regressions.is_empty() { + eprintln!("\nRegression detected:"); + for r in ®ressions { + eprintln!( + " {}: {} baseline={} current={} ratio={:.2}% tolerance={:.0}%", + r.test_name, + r.metric, + r.baseline, + r.current, + (r.ratio - 1.0) * 100.0, + r.tolerance * 100.0, + ); + } + return ExitCode::FAILURE; + } + } + + ExitCode::SUCCESS +} + +fn average_metrics(metrics: &[MetricCollection]) -> MetricCollection { + let n = metrics.len() as f64; + if n == 0.0 || metrics.is_empty() { + return metrics.first().cloned().unwrap_or(MetricCollection { + test_name: String::new(), + wall_time_us: 0, + session_objects: 0, + session_bytes: 0, + epoch_objects: 0, + epoch_bytes: 0, + gc_trigger_count: 0, + gc_bytes_freed: 0, + gc_objects_scanned: 0, + gc_collection_us: 0, + instruction_count: 0, + compile_time_us: 0, + exec_time_us: 0, + ic_hit_rate: 0.0, + ic_hits: 0, + ic_misses: 0, + }); + } + MetricCollection { + test_name: metrics[0].test_name.clone(), + wall_time_us: (metrics.iter().map(|m| m.wall_time_us as f64).sum::() / n) as u64, + session_objects: (metrics.iter().map(|m| m.session_objects as f64).sum::() / n) as usize, + session_bytes: (metrics.iter().map(|m| m.session_bytes as f64).sum::() / n) as usize, + epoch_objects: (metrics.iter().map(|m| m.epoch_objects as f64).sum::() / n) as usize, + epoch_bytes: (metrics.iter().map(|m| m.epoch_bytes as f64).sum::() / n) as u64, + gc_trigger_count: (metrics.iter().map(|m| m.gc_trigger_count as f64).sum::() / n) as u64, + gc_bytes_freed: (metrics.iter().map(|m| m.gc_bytes_freed as f64).sum::() / n) as u64, + gc_objects_scanned: (metrics.iter().map(|m| m.gc_objects_scanned as f64).sum::() / n) as u64, + gc_collection_us: (metrics.iter().map(|m| m.gc_collection_us as f64).sum::() / n) as u64, + instruction_count: (metrics.iter().map(|m| m.instruction_count as f64).sum::() / n) as u64, + compile_time_us: (metrics.iter().map(|m| m.compile_time_us as f64).sum::() / n) as u64, + exec_time_us: (metrics.iter().map(|m| m.exec_time_us as f64).sum::() / n) as u64, + ic_hit_rate: metrics.iter().map(|m| m.ic_hit_rate).sum::() / n, + ic_hits: (metrics.iter().map(|m| m.ic_hits as f64).sum::() / n) as u64, + ic_misses: (metrics.iter().map(|m| m.ic_misses as f64).sum::() / n) as u64, + } +} + +fn override_iterations(js: &str, iters: u32) -> String { + js.lines() + .map(|line| { + if line.trim_start().starts_with("var ITERATIONS ") { + format!("var ITERATIONS = {};", iters) + } else { + line.to_string() + } + }) + .collect::>() + .join("\n") +} diff --git a/crates/oxide_cli/src/bench/leak_detect.rs b/crates/oxide_cli/src/bench/leak_detect.rs new file mode 100644 index 00000000..d829ef99 --- /dev/null +++ b/crates/oxide_cli/src/bench/leak_detect.rs @@ -0,0 +1,141 @@ +use std::collections::VecDeque; +use std::process::ExitCode; +use std::sync::Arc; +use std::time::Instant; + +use oxide_compiler::compiler::{compiled_module_hash, Compiler}; +use oxide_kernel::kernel::KernelCore; +use oxide_parser::Allocator; +use oxide_vm::vm_pool::VmPool; + +use crate::bench::metrics::MetricCollection; +use crate::bench::output::format_json; +use crate::bench::BenchConfig; + +pub struct LeakSampler { + window: VecDeque<(usize, f64)>, + window_size: usize, +} + +impl LeakSampler { + pub fn new(window_size: usize) -> Self { + Self { + window: VecDeque::new(), + window_size, + } + } + + pub fn add_sample(&mut self, iteration: usize, value: f64) -> Option { + self.window.push_back((iteration, value)); + if self.window.len() >= self.window_size { + self.window.pop_front(); + } + if self.window.len() < 10 { + return None; + } + let n = self.window.len() as f64; + let sum_x: f64 = self.window.iter().map(|(i, _)| *i as f64).sum(); + let sum_y: f64 = self.window.iter().map(|(_, v)| v).sum(); + let sum_xy: f64 = self.window.iter().map(|(i, v)| *i as f64 * v).sum(); + let sum_x2: f64 = self.window.iter().map(|(i, _)| (*i as f64).powi(2)).sum(); + let slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x * sum_x); + let intercept = (sum_y - slope * sum_x) / n; + let y_mean = sum_y / n; + let ss_res: f64 = self + .window + .iter() + .map(|(i, v)| (v - (slope * *i as f64 + intercept)).powi(2)) + .sum(); + let ss_tot: f64 = self.window.iter().map(|(_, v)| (v - y_mean).powi(2)).sum(); + let r2 = if ss_tot == 0.0 { 1.0 } else { 1.0 - ss_res / ss_tot }; + if r2 > 0.9 && slope > 0.0 { + Some(LeakVerdict { slope, r2 }) + } else { + None + } + } +} + +pub struct LeakVerdict { + pub slope: f64, + pub r2: f64, +} + +pub fn run_leak_detect(config: &BenchConfig, kernel: &Arc, pool: &Arc) -> ExitCode { + let js = + "var ITERATIONS = 1000; var obj = {}; for (var i = 0; i < ITERATIONS; i++) { obj['key' + i] = i; } obj['key0']"; + let allocator = Allocator::default(); + let program = match oxide_parser::parse(&allocator, js) { + Ok(p) => p, + Err(_) => return ExitCode::FAILURE, + }; + let compiler = Compiler::new(); + let hash = compiled_module_hash(&program); + let module = match kernel.code_forge().get_or_insert_with(hash, || compiler.compile(&program)) { + Ok(m) => m, + Err(_) => return ExitCode::FAILURE, + }; + + let mut samplers: Vec<(&str, LeakSampler)> = vec![ + ("session_objects", LeakSampler::new(20)), + ("session_bytes", LeakSampler::new(20)), + ("code_forge_entries", LeakSampler::new(20)), + ("symbol_registry", LeakSampler::new(20)), + ]; + + let mut results: Vec = Vec::new(); + + for i in 0..config.iterations as usize { + let start = Instant::now(); + let mut guard = pool.spawn(); + let vm = guard.vm_mut(); + let _ = vm.run(&module); + let elapsed = start.elapsed(); + + for (name, sampler) in &mut samplers { + let value: f64 = match *name { + "session_objects" => vm.session_object_count() as f64, + "session_bytes" => vm.session_bytes_allocated() as f64, + "code_forge_entries" => kernel.code_forge().len() as f64, + "symbol_registry" => vm.symbol_registry_len() as f64, + _ => continue, + }; + if let Some(verdict) = sampler.add_sample(i, value) { + eprintln!( + "[LEAK] {}: slope={:.6} R²={:.4} over {} samples", + name, + verdict.slope, + verdict.r2, + sampler.window.len(), + ); + } + } + + if i == config.iterations as usize - 1 { + let guard = pool.spawn(); + let vm = guard.vm(); + results.push(MetricCollection { + test_name: "leak_detect".to_string(), + wall_time_us: elapsed.as_micros() as u64, + session_objects: vm.session_object_count(), + session_bytes: vm.session_bytes_allocated(), + epoch_objects: vm.epoch_object_count(), + epoch_bytes: 0, + gc_trigger_count: vm.session_gc_stats().total_collections, + gc_bytes_freed: vm.session_gc_stats().last_collection_bytes_freed, + gc_objects_scanned: vm.session_gc_stats().last_collection_objects_scanned, + gc_collection_us: vm.session_gc_stats().last_collection_duration_us, + instruction_count: vm.instruction_count(), + compile_time_us: 0, + exec_time_us: 0, + ic_hit_rate: vm.ic_hit_rate(), + ic_hits: vm.ic_hit_count(), + ic_misses: vm.ic_miss_count(), + }); + } + } + + let json = format_json(&results); + println!("{}", json); + ExitCode::SUCCESS +} diff --git a/crates/oxide_cli/src/bench/metrics.rs b/crates/oxide_cli/src/bench/metrics.rs new file mode 100644 index 00000000..65e6be44 --- /dev/null +++ b/crates/oxide_cli/src/bench/metrics.rs @@ -0,0 +1,43 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MetricCollection { + pub test_name: String, + pub wall_time_us: u64, + pub session_objects: usize, + pub session_bytes: usize, + pub epoch_objects: usize, + pub epoch_bytes: u64, + pub gc_trigger_count: u64, + pub gc_bytes_freed: u64, + pub gc_objects_scanned: u64, + pub gc_collection_us: u64, + pub instruction_count: u64, + pub compile_time_us: u64, + pub exec_time_us: u64, + pub ic_hit_rate: f64, + pub ic_hits: u64, + pub ic_misses: u64, +} + +impl MetricCollection { + pub fn iter_metrics(&self) -> Vec<(&'static str, f64)> { + vec![ + ("wall_time_us", self.wall_time_us as f64), + ("session_objects", self.session_objects as f64), + ("session_bytes", self.session_bytes as f64), + ("epoch_objects", self.epoch_objects as f64), + ("epoch_bytes", self.epoch_bytes as f64), + ("gc_trigger_count", self.gc_trigger_count as f64), + ("gc_bytes_freed", self.gc_bytes_freed as f64), + ("gc_objects_scanned", self.gc_objects_scanned as f64), + ("gc_collection_us", self.gc_collection_us as f64), + ("instruction_count", self.instruction_count as f64), + ("compile_time_us", self.compile_time_us as f64), + ("exec_time_us", self.exec_time_us as f64), + ("ic_hit_rate", self.ic_hit_rate), + ("ic_hits", self.ic_hits as f64), + ("ic_misses", self.ic_misses as f64), + ] + } +} diff --git a/crates/oxide_cli/src/bench/mod.rs b/crates/oxide_cli/src/bench/mod.rs new file mode 100644 index 00000000..44b0f221 --- /dev/null +++ b/crates/oxide_cli/src/bench/mod.rs @@ -0,0 +1,34 @@ +pub mod baseline; +pub mod js_stress; +pub mod leak_detect; +pub mod metrics; +pub mod output; +pub mod rust_bench; + +use std::process::ExitCode; +use std::sync::Arc; + +use oxide_kernel::kernel::KernelCore; +use oxide_vm::vm_pool::VmPool; + +pub struct BenchConfig { + pub mode: String, + pub filter: Option, + pub warmup: u32, + pub iterations: u32, + pub process: bool, + pub update_baseline: bool, + pub leak_check_interval: usize, +} + +pub fn run_benchmarks(config: BenchConfig, kernel: Arc, pool: Arc) -> ExitCode { + match config.mode.as_str() { + "js" => js_stress::run_js_stress_bench(&config, &kernel, &pool), + "rust" => rust_bench::run_rust_bench(config.filter.as_deref()), + "leak" => leak_detect::run_leak_detect(&config, &kernel, &pool), + _ => { + eprintln!("Unknown bench mode: {}. Use: js, rust, or leak", config.mode); + ExitCode::FAILURE + } + } +} diff --git a/crates/oxide_cli/src/bench/output.rs b/crates/oxide_cli/src/bench/output.rs new file mode 100644 index 00000000..8a881fa5 --- /dev/null +++ b/crates/oxide_cli/src/bench/output.rs @@ -0,0 +1,40 @@ +use crate::bench::metrics::MetricCollection; + +pub fn format_json(results: &[MetricCollection]) -> String { + serde_json::to_string_pretty(results).unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e)) +} + +pub fn format_text_table(results: &[MetricCollection]) -> String { + let mut out = String::new(); + out.push_str(&format!( + "{:<35} {:>10} {:>10} {:>10} {:>12} {:>8} {:>8} {:>10} {:>10}\n", + "Test Name", "Wall(ms)", "Comp(ms)", "Exec(ms)", "Instrs", "GC Ct", "IC Rate", "Sess Objs", "Sess B", + )); + out.push_str(&"-".repeat(120)); + out.push('\n'); + + for m in results { + out.push_str(&format!( + "{:<35} {:>10.2} {:>10.3} {:>10.3} {:>12} {:>8} {:>7.1}% {:>10} {:>10}", + truncate(&m.test_name, 35), + m.wall_time_us as f64 / 1000.0, + m.compile_time_us as f64 / 1000.0, + m.exec_time_us as f64 / 1000.0, + m.instruction_count, + m.gc_trigger_count, + m.ic_hit_rate * 100.0, + m.session_objects, + m.session_bytes, + )); + out.push('\n'); + } + out +} + +fn truncate(s: &str, max: usize) -> &str { + if s.len() <= max { + s + } else { + &s[..max] + } +} diff --git a/crates/oxide_cli/src/bench/rust_bench.rs b/crates/oxide_cli/src/bench/rust_bench.rs new file mode 100644 index 00000000..5de7a7a3 --- /dev/null +++ b/crates/oxide_cli/src/bench/rust_bench.rs @@ -0,0 +1,20 @@ +pub fn run_rust_bench(filter: Option<&str>) -> std::process::ExitCode { + let mut cmd = std::process::Command::new("cargo"); + cmd.args(["bench", "-p", "oxide_vm"]); + if let Some(name) = filter { + cmd.args(["--bench", name]); + } + match cmd.status() { + Ok(status) => { + if status.success() { + std::process::ExitCode::SUCCESS + } else { + std::process::ExitCode::from(1) + } + } + Err(e) => { + eprintln!("Failed to run cargo bench: {}", e); + std::process::ExitCode::from(1) + } + } +} diff --git a/crates/oxide_cli/src/main.rs b/crates/oxide_cli/src/main.rs index ea818ce6..384ed882 100644 --- a/crates/oxide_cli/src/main.rs +++ b/crates/oxide_cli/src/main.rs @@ -1,4 +1,5 @@ #![allow(clippy::arc_with_non_send_sync)] +#![allow(dead_code)] use std::fs; use std::process::ExitCode; @@ -15,6 +16,8 @@ use oxide_types::object::JsObject; use oxide_vm::vm_pool::VmPool; use oxide_vm::JsValue; +mod bench; + #[derive(Parser)] #[command(name = "oxide", version, about = "OxideJS - Rust JavaScript engine")] struct Cli { @@ -42,7 +45,20 @@ enum Commands { file: Option, }, Bench { - suite: Option, + #[arg(default_value = "js")] + mode: Option, + #[arg(short, long)] + filter: Option, + #[arg(long, default_value = "2")] + warmup: u32, + #[arg(long, default_value = "10")] + iterations: u32, + #[arg(long)] + process: bool, + #[arg(long)] + update_baseline: bool, + #[arg(long, default_value = "1000")] + leak_check_interval: usize, }, Test { suite: Option, @@ -64,7 +80,28 @@ fn main() -> ExitCode { run(&file, &kernel, &pool) } Some(Commands::Compile { expr, file }) => compile(expr, file), - Some(Commands::Bench { .. }) => not_implemented("bench"), + Some(Commands::Bench { + mode, + filter, + warmup, + iterations, + process, + update_baseline, + leak_check_interval, + }) => { + let kernel = make_kernel(); + let pool = make_pool(&kernel); + let config = bench::BenchConfig { + mode: mode.unwrap_or_else(|| "js".to_string()), + filter, + warmup, + iterations, + process, + update_baseline, + leak_check_interval, + }; + bench::run_benchmarks(config, kernel, pool) + } Some(Commands::Test { .. }) => not_implemented("test"), None => repl(), } diff --git a/crates/oxide_cli/tests/cli_smoke.rs b/crates/oxide_cli/tests/cli_smoke.rs index d7451f83..4b088d99 100644 --- a/crates/oxide_cli/tests/cli_smoke.rs +++ b/crates/oxide_cli/tests/cli_smoke.rs @@ -38,12 +38,12 @@ fn run_file() { } #[test] -fn bench_not_implemented() { +fn bench_help() { let output = Command::new("cargo") - .args(["run", "--", "bench"]) + .args(["run", "--", "bench", "--help"]) .output() - .expect("failed to run oxide bench"); + .expect("failed to run oxide bench --help"); - let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("not yet implemented"), "bench should print not-implemented message"); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("--iterations"), "bench --help should list --iterations flag"); } diff --git a/crates/oxide_code_cache/src/lib.rs b/crates/oxide_code_cache/src/lib.rs index 06a4aabb..17e7b3a2 100644 --- a/crates/oxide_code_cache/src/lib.rs +++ b/crates/oxide_code_cache/src/lib.rs @@ -39,6 +39,14 @@ impl CodeForge { Ok(self.insert(hash, compile()?)) } + + pub fn len(&self) -> usize { + self.map.len() + } + + pub fn is_empty(&self) -> bool { + self.map.is_empty() + } } impl Default for CodeForge { diff --git a/crates/oxide_test262/src/main.rs b/crates/oxide_test262/src/main.rs index 5b61848e..e9f636d0 100644 --- a/crates/oxide_test262/src/main.rs +++ b/crates/oxide_test262/src/main.rs @@ -149,6 +149,8 @@ struct RunConfig { filter: Option, no_skip: bool, supervise: bool, + leak_check: bool, + leak_check_interval: usize, } struct HarnessSources { @@ -284,15 +286,31 @@ fn get_harness_prefix( } impl RunConfig { + fn new() -> Self { + Self { + test262_root: None, + filter: None, + no_skip: false, + supervise: false, + leak_check: false, + leak_check_interval: 1000, + } + } + fn parse(args: &[String]) -> Result { - let mut config = Self::default(); + let mut config = Self::new(); let mut positional = Vec::new(); for arg in args.iter().skip(1) { match arg.as_str() { "--no-skip" => config.no_skip = true, "--supervise" => config.supervise = true, + "--leak-check" => config.leak_check = true, "--help" | "-h" => return Err(Self::usage()), + _ if arg.starts_with("--leak-check-interval=") => { + config.leak_check_interval = + arg.strip_prefix("--leak-check-interval=").unwrap().parse().unwrap_or(1000); + } _ if arg.starts_with("--") => return Err(format!("unknown option: {arg}\n\n{}", Self::usage())), _ => positional.push(arg.clone()), } @@ -312,11 +330,13 @@ impl RunConfig { } fn usage() -> String { - "usage: test262-runner [--no-skip] [--supervise] [test262-root] [path-filter]\n\ + "usage: test262-runner [--no-skip] [--supervise] [--leak-check] [--leak-check-interval=N] [test262-root] [path-filter]\n\ \n\ --no-skip Run capability-excluded tests and count unsupported compile/runtime results as failures.\n\ --supervise Run the suite as single-worker child-process windows with a hard per-test timeout and\n\ \x20 automatic resume past any hanging/crashing test. A hang or crash is reported by path.\n\ + --leak-check Monitor session_object_ptrs, session_bytes, code_forge.len(), symbol_registry.len() every\n\ + \x20 --leak-check-interval tests (default 1000). Flags sustained linear growth (R^2>0.9).\n\ \n\ supervised-mode env tunables:\n\ \x20 OXIDE_TEST262_TIMEOUT_SECS per-test wall-clock timeout (default 10)\n\ diff --git a/crates/oxide_vm/Cargo.toml b/crates/oxide_vm/Cargo.toml index 3d501f1e..365a1cb2 100644 --- a/crates/oxide_vm/Cargo.toml +++ b/crates/oxide_vm/Cargo.toml @@ -26,3 +26,24 @@ tracing.workspace = true oxide_compiler = { path = "../oxide_compiler" } proptest.workspace = true oxide_parser = { path = "../oxide_parser" } +criterion = { version = "0.5", features = ["html_reports"] } + +[[bench]] +name = "dispatch" +harness = false + +[[bench]] +name = "property" +harness = false + +[[bench]] +name = "coercion" +harness = false + +[[bench]] +name = "gc_mark_sweep" +harness = false + +[[bench]] +name = "call" +harness = false diff --git a/crates/oxide_vm/benches/call.rs b/crates/oxide_vm/benches/call.rs new file mode 100644 index 00000000..64af270b --- /dev/null +++ b/crates/oxide_vm/benches/call.rs @@ -0,0 +1,27 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use oxide_compiler::compiler::Compiler; +use oxide_kernel::kernel::{KernelConfig, KernelCore}; +use oxide_vm::vm::Vm; +use std::sync::Arc; + +fn bench_call(c: &mut Criterion) { + let kernel = KernelCore::new(KernelConfig::minimal()); + let js = "function fib(n) { if (n < 2) return n; return fib(n-1) + fib(n-2); } fib(15);"; + let alloc = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&alloc, js).expect("parse"); + let module = Compiler.compile(&program).expect("compile"); + + c.bench_function("call", |b| { + b.iter_batched( + || Vm::with_kernel_core(Arc::clone(&kernel)), + |mut vm| { + vm.run(&module).ok(); + black_box(vm) + }, + criterion::BatchSize::SmallInput, + ); + }); +} + +criterion_group!(benches, bench_call); +criterion_main!(benches); diff --git a/crates/oxide_vm/benches/coercion.rs b/crates/oxide_vm/benches/coercion.rs new file mode 100644 index 00000000..c7bfcc30 --- /dev/null +++ b/crates/oxide_vm/benches/coercion.rs @@ -0,0 +1,27 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use oxide_compiler::compiler::Compiler; +use oxide_kernel::kernel::{KernelConfig, KernelCore}; +use oxide_vm::vm::Vm; +use std::sync::Arc; + +fn bench_coercion(c: &mut Criterion) { + let kernel = KernelCore::new(KernelConfig::minimal()); + let js = "var x = \"42\"; var y = \"3.14\"; var r = 0; for (var i = 0; i < 5000; i++) { r += (+x) + (+y); } r"; + let alloc = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&alloc, js).expect("parse"); + let module = Compiler.compile(&program).expect("compile"); + + c.bench_function("coercion", |b| { + b.iter_batched( + || Vm::with_kernel_core(Arc::clone(&kernel)), + |mut vm| { + vm.run(&module).ok(); + black_box(vm) + }, + criterion::BatchSize::SmallInput, + ); + }); +} + +criterion_group!(benches, bench_coercion); +criterion_main!(benches); diff --git a/crates/oxide_vm/benches/dispatch.rs b/crates/oxide_vm/benches/dispatch.rs new file mode 100644 index 00000000..e68c108b --- /dev/null +++ b/crates/oxide_vm/benches/dispatch.rs @@ -0,0 +1,27 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use oxide_compiler::compiler::Compiler; +use oxide_kernel::kernel::{KernelConfig, KernelCore}; +use oxide_vm::vm::Vm; +use std::sync::Arc; + +fn bench_dispatch(c: &mut Criterion) { + let kernel = KernelCore::new(KernelConfig::minimal()); + let js = "var x = 0; for (var i = 0; i < 10000; i++) { x = x + i - i * 2 + (i % 3); } x"; + let alloc = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&alloc, js).expect("parse"); + let module = Compiler.compile(&program).expect("compile"); + + c.bench_function("dispatch", |b| { + b.iter_batched( + || Vm::with_kernel_core(Arc::clone(&kernel)), + |mut vm| { + vm.run(&module).ok(); + black_box(vm) + }, + criterion::BatchSize::SmallInput, + ); + }); +} + +criterion_group!(benches, bench_dispatch); +criterion_main!(benches); diff --git a/crates/oxide_vm/benches/gc_mark_sweep.rs b/crates/oxide_vm/benches/gc_mark_sweep.rs new file mode 100644 index 00000000..6d85c661 --- /dev/null +++ b/crates/oxide_vm/benches/gc_mark_sweep.rs @@ -0,0 +1,29 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use oxide_compiler::compiler::Compiler; +use oxide_kernel::kernel::{KernelConfig, KernelCore}; +use oxide_vm::vm::Vm; +use std::sync::Arc; + +fn bench_gc(c: &mut Criterion) { + let mut config = KernelConfig::minimal(); + config.session_gc_threshold = 1024 * 1024; + let kernel = KernelCore::new(config); + let js = "var g = {}; for (var i = 0; i < 500; i++) { var obj = { x: i, y: i * 2 }; g[i] = obj; } g[\"x\"]"; + let alloc = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&alloc, js).expect("parse"); + let module = Compiler.compile(&program).expect("compile"); + + c.bench_function("gc_mark_sweep", |b| { + b.iter_batched( + || Vm::with_kernel_core(Arc::clone(&kernel)), + |mut vm| { + vm.run(&module).ok(); + black_box(vm) + }, + criterion::BatchSize::SmallInput, + ); + }); +} + +criterion_group!(benches, bench_gc); +criterion_main!(benches); diff --git a/crates/oxide_vm/benches/property.rs b/crates/oxide_vm/benches/property.rs new file mode 100644 index 00000000..c7361c07 --- /dev/null +++ b/crates/oxide_vm/benches/property.rs @@ -0,0 +1,27 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use oxide_compiler::compiler::Compiler; +use oxide_kernel::kernel::{KernelConfig, KernelCore}; +use oxide_vm::vm::Vm; +use std::sync::Arc; + +fn bench_property(c: &mut Criterion) { + let kernel = KernelCore::new(KernelConfig::minimal()); + let js = "var obj = { a: 1, b: 2, c: 3, d: 4, e: 5 }; var sum = 0; for (var i = 0; i < 10000; i++) { sum += obj.a + obj.b + obj.c + obj.d + obj.e; } sum"; + let alloc = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&alloc, js).expect("parse"); + let module = Compiler.compile(&program).expect("compile"); + + c.bench_function("property", |b| { + b.iter_batched( + || Vm::with_kernel_core(Arc::clone(&kernel)), + |mut vm| { + vm.run(&module).ok(); + black_box(vm) + }, + criterion::BatchSize::SmallInput, + ); + }); +} + +criterion_group!(benches, bench_property); +criterion_main!(benches); diff --git a/crates/oxide_vm/src/dispatch/property.rs b/crates/oxide_vm/src/dispatch/property.rs index f68f418f..b9fbf9cd 100644 --- a/crates/oxide_vm/src/dispatch/property.rs +++ b/crates/oxide_vm/src/dispatch/property.rs @@ -203,8 +203,10 @@ impl Vm { if let Some(value) = ic_get_hit(obj, cached_shape_id, cached_slot) { self.regs[a] = value; + self.ic_hits.set(self.ic_hits.get() + 1); ic_trace!("IC_GET hit shape={} slot={}", cached_shape_id, cached_slot); } else if let Some(template) = self.kernel_core.prop_forge().get_template(obj.shape_id()) { + self.ic_misses.set(self.ic_misses.get() + 1); prop_cache_miss(); if template.prop_name == prop_name_si { if template.position < obj.prop_vec_len() as u32 { @@ -271,13 +273,16 @@ impl Vm { let cached_slot = ext1; if ic_set_hit(obj, cached_shape_id, cached_slot, value) { + self.ic_hits.set(self.ic_hits.get() + 1); ic_trace!("IC_SET hit shape={} slot={}", cached_shape_id, cached_slot); } else if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { + self.ic_misses.set(self.ic_misses.get() + 1); prop_cache_miss(); obj.set_prop_at(pos, value); self.write_ic_back(obj.shape_id(), pos); ic_debug!("IC_SET write-back shape={} slot={}", obj.shape_id(), pos); } else { + self.ic_misses.set(self.ic_misses.get() + 1); prop_cache_miss(); let old_shape = obj.shape_id(); self.ordinary_set_dispatch(obj, prop_name_si, value, receiver)?; diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index cccd1350..3f73b71b 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -180,6 +180,9 @@ pub struct Vm { /// The dispatch loop checks this flag and skips writing `regs[target_reg]` from the /// call result — the value will be delivered by the RETURN handler instead. pub(crate) accessor_frame_target_reg: Option, + pub(crate) ic_hits: std::cell::Cell, + pub(crate) ic_misses: std::cell::Cell, + pub(crate) instruction_count: u64, } impl Vm { @@ -328,6 +331,43 @@ impl Vm { &self.session_gc } + pub fn session_object_count(&self) -> usize { + self.session_object_ptrs.len() + } + + pub fn session_bytes_allocated(&self) -> usize { + self.session_bytes_allocated + } + + pub fn epoch_object_count(&self) -> usize { + self.epoch_object_ptrs.len() + } + + pub fn ic_hit_rate(&self) -> f64 { + let total = self.ic_hits.get() + self.ic_misses.get(); + if total == 0 { + 0.0 + } else { + self.ic_hits.get() as f64 / total as f64 + } + } + + pub fn instruction_count(&self) -> u64 { + self.instruction_count + } + + pub fn symbol_registry_len(&self) -> usize { + self.symbol_registry.len() + } + + pub fn ic_hit_count(&self) -> u64 { + self.ic_hits.get() + } + + pub fn ic_miss_count(&self) -> u64 { + self.ic_misses.get() + } + pub(crate) fn checked_object_ptr( &mut self, val: JsValue, error_msg: &str, ) -> Result, String> { @@ -649,10 +689,12 @@ impl Vm { steps += 1; if let Some(max_steps) = self.kernel_core.config.max_steps { if steps > max_steps { + self.instruction_count = steps; return Err(format!("VM step limit exceeded at pc={}", self.pc)); } } if self.pc >= self.bytecode.len() { + self.instruction_count = steps; return Err("program counter out of bounds".into()); } @@ -666,7 +708,10 @@ impl Vm { match op { OpCode::NOP => {} - OpCode::HALT => return Ok(self.regs[0]), + OpCode::HALT => { + self.instruction_count = steps; + return Ok(self.regs[0]); + } OpCode::LOAD_CONST => { self.dispatch_load_const(rd, instr)?; diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index 44c9163b..7eb5ed88 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -54,6 +54,9 @@ impl Vm { active_reg_limit: 0, native_call_depth: 0, accessor_frame_target_reg: None, + ic_hits: std::cell::Cell::new(0), + ic_misses: std::cell::Cell::new(0), + instruction_count: 0, } } @@ -96,6 +99,9 @@ impl Vm { active_reg_limit: 0, native_call_depth: 0, accessor_frame_target_reg: None, + ic_hits: std::cell::Cell::new(0), + ic_misses: std::cell::Cell::new(0), + instruction_count: 0, } } diff --git a/tests/stress/array.js b/tests/stress/array.js new file mode 100644 index 00000000..d6266174 --- /dev/null +++ b/tests/stress/array.js @@ -0,0 +1,11 @@ +var ITERATIONS = 50000; + +var arr = []; +for (var i = 0; i < ITERATIONS; i++) { + arr.push(i); +} +var sum = 0; +while (arr.length > 0) { + sum += arr.pop(); +} +sum diff --git a/tests/stress/call.js b/tests/stress/call.js new file mode 100644 index 00000000..8002001b --- /dev/null +++ b/tests/stress/call.js @@ -0,0 +1,7 @@ +var ITERATIONS = 200; + +function sumTo(n) { + if (n <= 0) return 0; + return n + sumTo(n - 1); +} +sumTo(ITERATIONS) diff --git a/tests/stress/code_forge.js b/tests/stress/code_forge.js new file mode 100644 index 00000000..e1cc8a6b --- /dev/null +++ b/tests/stress/code_forge.js @@ -0,0 +1,6 @@ +var ITERATIONS = 5000; + +for (var i = 0; i < ITERATIONS; i++) { + eval("var x" + i + " = " + i + "; x" + i + " + 1"); +} +1 diff --git a/tests/stress/coercion.js b/tests/stress/coercion.js new file mode 100644 index 00000000..494e082a --- /dev/null +++ b/tests/stress/coercion.js @@ -0,0 +1,7 @@ +var ITERATIONS = 50000; + +var sum = 0; +for (var i = 0; i < ITERATIONS; i++) { + sum += (+"42") + (+"3.14") + (+"0"); +} +sum diff --git a/tests/stress/gc.js b/tests/stress/gc.js new file mode 100644 index 00000000..230271d3 --- /dev/null +++ b/tests/stress/gc.js @@ -0,0 +1,8 @@ +var ITERATIONS = 100000; + +var sum = 0; +for (var i = 0; i < ITERATIONS; i++) { + var obj = { x: i, y: i * 2, nested: { a: 1, b: 2 } }; + sum += obj.x + obj.y; +} +sum diff --git a/tests/stress/ic_cache.js b/tests/stress/ic_cache.js new file mode 100644 index 00000000..ac37baf9 --- /dev/null +++ b/tests/stress/ic_cache.js @@ -0,0 +1,13 @@ +var ITERATIONS = 10000; + +var objs = []; +for (var i = 0; i < ITERATIONS; i++) { + objs.push({ x: i }); + if (i % 3 === 0) objs[i].y = i; + if (i % 5 === 0) objs[i].z = i; +} +var sum = 0; +for (var i = 0; i < ITERATIONS; i++) { + sum += objs[i].x; +} +sum diff --git a/tests/stress/property.js b/tests/stress/property.js new file mode 100644 index 00000000..8542d0d8 --- /dev/null +++ b/tests/stress/property.js @@ -0,0 +1,11 @@ +var ITERATIONS = 10000; + +var obj = {}; +for (var i = 0; i < ITERATIONS; i++) { + obj["key" + i] = i; +} +var sum = 0; +for (var i = 0; i < ITERATIONS; i++) { + sum += obj["key" + i]; +} +sum diff --git a/tests/stress/string.js b/tests/stress/string.js new file mode 100644 index 00000000..46f0cd78 --- /dev/null +++ b/tests/stress/string.js @@ -0,0 +1,7 @@ +var ITERATIONS = 10000; + +var s = ""; +for (var i = 0; i < ITERATIONS; i++) { + s += "a" + i; +} +s.length From 1815958b11f3177acb8d6d356e2f5612012b102d Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Wed, 24 Jun 2026 12:12:44 +0800 Subject: [PATCH 02/32] =?UTF-8?q?refactor:=20=E6=B8=85=E7=90=86=E6=9C=AA?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E7=AC=A6=E5=8F=B7=EF=BC=8C=E5=90=88=E5=B9=B6?= =?UTF-8?q?bind=5Fconstructor=E5=AE=8F=EF=BC=8C=E7=BB=9F=E4=B8=80init/rebi?= =?UTF-8?q?nd=E7=BB=91=E5=AE=9A=E5=88=97=E8=A1=A8=EF=BC=8Cbind=5Fnumber?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E8=A1=A8=E9=A9=B1=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_bytecode/src/opcode.rs | 12 -- crates/oxide_compiler/src/compiler.rs | 2 +- crates/oxide_kernel/src/builtin.rs | 44 ------- crates/oxide_kernel/src/kernel.rs | 5 - crates/oxide_kernel/src/string_forge.rs | 4 +- crates/oxide_test262/src/main.rs | 2 +- crates/oxide_types/src/mem.rs | 12 +- crates/oxide_types/src/object.rs | 11 -- crates/oxide_types/src/shape.rs | 4 - crates/oxide_vm/src/bindings/bind_array.rs | 4 +- .../src/bindings/bind_array_buffer.rs | 7 +- crates/oxide_vm/src/bindings/bind_boolean.rs | 4 +- .../oxide_vm/src/bindings/bind_data_view.rs | 4 +- crates/oxide_vm/src/bindings/bind_map.rs | 4 +- crates/oxide_vm/src/bindings/bind_number.rs | 111 +++++++++--------- crates/oxide_vm/src/bindings/bind_regexp.rs | 4 +- crates/oxide_vm/src/bindings/bind_set.rs | 4 +- crates/oxide_vm/src/bindings/bind_symbol.rs | 4 +- .../oxide_vm/src/bindings/bind_typed_array.rs | 4 +- crates/oxide_vm/src/bindings/mod.rs | 84 +++++-------- crates/oxide_vm/src/coercion.rs | 4 - crates/oxide_vm/src/native.rs | 8 -- crates/oxide_vm/src/session_gc.rs | 44 +++---- crates/oxide_vm/src/vm.rs | 16 +-- crates/oxide_vm/src/vm_pool.rs | 28 +---- crates/oxide_vm/src/vm_runtime.rs | 5 - crates/oxide_vm/src/vm_support.rs | 2 +- 27 files changed, 140 insertions(+), 297 deletions(-) diff --git a/crates/oxide_bytecode/src/opcode.rs b/crates/oxide_bytecode/src/opcode.rs index 8fb1fb25..8fa97ada 100644 --- a/crates/oxide_bytecode/src/opcode.rs +++ b/crates/oxide_bytecode/src/opcode.rs @@ -153,18 +153,6 @@ pub enum OpCode { } impl OpCode { - pub fn is_implemented(&self) -> bool { - !matches!( - self, - OpCode::PROFILE_TYPE - | OpCode::PROFILE_SHAPE - | OpCode::PROFILE_BRANCH - | OpCode::PROFILE_CALL - | OpCode::FORK - | OpCode::JOIN - ) - } - pub fn has_ic_ext_words(&self) -> bool { matches!( self, diff --git a/crates/oxide_compiler/src/compiler.rs b/crates/oxide_compiler/src/compiler.rs index 936e2e17..0ba1167a 100644 --- a/crates/oxide_compiler/src/compiler.rs +++ b/crates/oxide_compiler/src/compiler.rs @@ -646,7 +646,7 @@ impl Compiler { ) } - #[allow(clippy::too_many_arguments)] + #[expect(clippy::too_many_arguments)] pub(crate) fn compile_function_body_with_field_hooks<'a, C, E>( &self, param_specs: &[ParamSpec<'a>], body_stmts: &[Statement<'a>], parent_ctx: &CompileCtx, is_expression_body: bool, extra_bindings: &[(&str, u8)], body_context: FunctionBodyContext, diff --git a/crates/oxide_kernel/src/builtin.rs b/crates/oxide_kernel/src/builtin.rs index f1761a2f..6233c014 100644 --- a/crates/oxide_kernel/src/builtin.rs +++ b/crates/oxide_kernel/src/builtin.rs @@ -144,20 +144,6 @@ pub struct RegExpMethods { pub to_string: *const (), } -pub struct NumberMethods { - pub is_nan: *const (), - pub is_finite: *const (), - pub parse_int: *const (), - pub parse_float: *const (), - pub to_string: *const (), - pub to_fixed: *const (), - pub is_integer: *const (), - pub is_safe_integer: *const (), - pub to_exponential: *const (), - pub to_precision: *const (), - pub value_of: *const (), -} - pub struct FunctionMethods { pub call: *const (), pub apply: *const (), @@ -979,36 +965,6 @@ impl BuiltinWorld { ); } - pub fn bind_number_methods(&self, methods: &NumberMethods, string_forge: &StringForge, shape_forge: &ShapeForge) { - let ctor_ptr = P::as_ptr(&self.number_constructor) as *mut JsObject; - let ctor = unsafe { &mut *ctor_ptr }; - let proto_ptr = P::as_ptr(&self.number_proto) as *mut JsObject; - let proto = unsafe { &mut *proto_ptr }; - bind_methods!( - self, - ctor, - string_forge, - shape_forge, - ("isNaN", methods.is_nan, 1), - ("isFinite", methods.is_finite, 1), - ("parseInt", methods.parse_int, 1), - ("parseFloat", methods.parse_float, 1), - ("isInteger", methods.is_integer, 1), - ("isSafeInteger", methods.is_safe_integer, 1), - ); - bind_methods!( - self, - proto, - string_forge, - shape_forge, - ("toString", methods.to_string, 0), - ("toFixed", methods.to_fixed, 0), - ("toExponential", methods.to_exponential, 0), - ("toPrecision", methods.to_precision, 0), - ("valueOf", methods.value_of, 0), - ); - } - pub fn bind_function_methods( &self, methods: &FunctionMethods, string_forge: &StringForge, shape_forge: &ShapeForge, ) { diff --git a/crates/oxide_kernel/src/kernel.rs b/crates/oxide_kernel/src/kernel.rs index e87d1ea5..60ed7784 100644 --- a/crates/oxide_kernel/src/kernel.rs +++ b/crates/oxide_kernel/src/kernel.rs @@ -80,11 +80,6 @@ impl KernelConfig { pub fn set_session_gc_threshold(&mut self, bytes: usize) { self.session_gc_threshold = bytes; } - - pub fn with_session_gc_threshold(mut self, bytes: usize) -> Self { - self.session_gc_threshold = bytes; - self - } } impl Default for KernelConfig { diff --git a/crates/oxide_kernel/src/string_forge.rs b/crates/oxide_kernel/src/string_forge.rs index 6c64c0d8..8ab5bef9 100644 --- a/crates/oxide_kernel/src/string_forge.rs +++ b/crates/oxide_kernel/src/string_forge.rs @@ -14,8 +14,8 @@ pub fn hash16(s: &str) -> u16 { } pub struct StringEntry { - pub data: String, - pub ref_count: AtomicU32, + pub(crate) data: String, + pub(crate) ref_count: AtomicU32, pub hash: u16, } diff --git a/crates/oxide_test262/src/main.rs b/crates/oxide_test262/src/main.rs index e9f636d0..075759f8 100644 --- a/crates/oxide_test262/src/main.rs +++ b/crates/oxide_test262/src/main.rs @@ -670,7 +670,7 @@ fn read_heartbeat(path: &Path) -> Option { /// and a fresh child resumes from `culprit + 1`. A child that crashes mid-test /// is recovered through the same path. Timeouts/crashes count as skip by default /// and fail under `--no-skip`. -#[allow(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] fn supervise_window( exe: &Path, args: &[String], no_skip: bool, wstart: usize, wend: usize, timeout: Duration, startup_grace: Duration, paths: &[PathBuf], window_id: usize, diff --git a/crates/oxide_types/src/mem.rs b/crates/oxide_types/src/mem.rs index 468cbc63..2ad779d1 100644 --- a/crates/oxide_types/src/mem.rs +++ b/crates/oxide_types/src/mem.rs @@ -15,16 +15,14 @@ impl P { Self(Arc::new(value)) } - pub fn from_arena(value: &T) -> Self - where - T: Clone, - { - Self(Arc::new(value.clone())) - } - pub fn as_ptr(&self) -> *const T { Arc::as_ptr(&self.0) } + + #[inline(always)] + pub fn as_mut_ptr(&self) -> *mut T { + self.as_ptr() as *mut T + } } impl Clone for P { diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index 9596ca41..15572316 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -648,17 +648,6 @@ impl JsObject { pos as u32 } - /// Get a pointer to the JsValue at position. - /// Returns None if hash_props not allocated or position out of bounds. - pub fn prop_ptr_at(&self, position: impl PropIndex) -> Option<*const JsValue> { - if self.hash_props.is_null() { - return None; - } - // SAFETY: hash_props was set from Box> in ensure_hash_props/new_array. - let vec = unsafe { &*(self.hash_props as *const Vec) }; - vec.get(position.to_u32() as usize).map(|v| v as *const JsValue) - } - /// Get the length of hash_props vec (returns 0 if not allocated). pub fn prop_vec_len(&self) -> usize { if self.hash_props.is_null() { diff --git a/crates/oxide_types/src/shape.rs b/crates/oxide_types/src/shape.rs index 99a8cd60..4f0697f7 100644 --- a/crates/oxide_types/src/shape.rs +++ b/crates/oxide_types/src/shape.rs @@ -163,10 +163,6 @@ pub fn shape_prop_count(shape_id: ShapeId) -> u32 { store().lock().unwrap().shape_prop_count(shape_id) } -pub fn init_shape_store() { - let _ = store(); -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/oxide_vm/src/bindings/bind_array.rs b/crates/oxide_vm/src/bindings/bind_array.rs index 72f6343f..76b15465 100644 --- a/crates/oxide_vm/src/bindings/bind_array.rs +++ b/crates/oxide_vm/src/bindings/bind_array.rs @@ -4,7 +4,7 @@ use oxide_kernel::builtin::ArrayMethods; use oxide_kernel::kernel::{KernelCore, KernelSession}; use oxide_types::object::JsObject; -use crate::bind_constructor_hash; +use crate::bind_constructor; pub fn bind_array(core: &Arc, session: &KernelSession, global: &mut JsObject) { let _array_methods = ArrayMethods { @@ -47,5 +47,5 @@ pub fn bind_array(core: &Arc, session: &KernelSession, global: &mut ); let ctor_ptr = session.builtin_world().array_constructor.as_ptr() as *mut JsObject; - bind_constructor_hash!(core, global, "Array", ctor_ptr, crate::builtins::array::array_constructor, 1); + bind_constructor!(core, global, "Array", ctor_ptr, crate::builtins::array::array_constructor, 1, hash: true); } diff --git a/crates/oxide_vm/src/bindings/bind_array_buffer.rs b/crates/oxide_vm/src/bindings/bind_array_buffer.rs index 57c6e6f9..b3cd284e 100644 --- a/crates/oxide_vm/src/bindings/bind_array_buffer.rs +++ b/crates/oxide_vm/src/bindings/bind_array_buffer.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use crate::bind_constructor_hash; +use crate::bind_constructor; use crate::bindings::{apply_binding_table, configure_native_constructor}; use oxide_kernel::kernel::{KernelCore, KernelSession}; use oxide_types::object::JsObject; @@ -31,12 +31,13 @@ pub fn bind_array_buffer(core: &Arc, session: &KernelSession, global ], ); - bind_constructor_hash!( + bind_constructor!( core, global, "ArrayBuffer", ctor_ptr, crate::builtins::array_buffer::array_buffer_constructor, - 1 + 1, + hash: true ); } diff --git a/crates/oxide_vm/src/bindings/bind_boolean.rs b/crates/oxide_vm/src/bindings/bind_boolean.rs index 71e21fb8..5106feb1 100644 --- a/crates/oxide_vm/src/bindings/bind_boolean.rs +++ b/crates/oxide_vm/src/bindings/bind_boolean.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use crate::bind_constructor_hash; +use crate::bind_constructor; use crate::bindings::{apply_binding_table, configure_native_constructor}; use oxide_kernel::kernel::{KernelCore, KernelSession}; use oxide_types::object::JsObject; @@ -23,5 +23,5 @@ pub fn bind_boolean(core: &Arc, session: &KernelSession, global: &mu ], ); - bind_constructor_hash!(core, global, "Boolean", ctor_ptr, crate::builtins::boolean::boolean_constructor, 1); + bind_constructor!(core, global, "Boolean", ctor_ptr, crate::builtins::boolean::boolean_constructor, 1, hash: true); } diff --git a/crates/oxide_vm/src/bindings/bind_data_view.rs b/crates/oxide_vm/src/bindings/bind_data_view.rs index 4c590ebb..76613947 100644 --- a/crates/oxide_vm/src/bindings/bind_data_view.rs +++ b/crates/oxide_vm/src/bindings/bind_data_view.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use crate::bind_constructor_hash; +use crate::bind_constructor; use crate::bindings::{apply_binding_table, configure_native_constructor}; use oxide_kernel::kernel::{KernelCore, KernelSession}; use oxide_types::object::JsObject; @@ -42,5 +42,5 @@ pub fn bind_data_view(core: &Arc, session: &KernelSession, global: & ], ); - bind_constructor_hash!(core, global, "DataView", ctor_ptr, crate::builtins::data_view::data_view_constructor, 3); + bind_constructor!(core, global, "DataView", ctor_ptr, crate::builtins::data_view::data_view_constructor, 3, hash: true); } diff --git a/crates/oxide_vm/src/bindings/bind_map.rs b/crates/oxide_vm/src/bindings/bind_map.rs index e8ea2c21..a567fe2e 100644 --- a/crates/oxide_vm/src/bindings/bind_map.rs +++ b/crates/oxide_vm/src/bindings/bind_map.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use crate::bind_constructor_hash; +use crate::bind_constructor; use crate::bindings::{apply_binding_table, configure_native_constructor}; use oxide_kernel::kernel::{KernelCore, KernelSession}; use oxide_types::object::JsObject; @@ -27,5 +27,5 @@ pub fn bind_map(core: &Arc, session: &KernelSession, global: &mut Js ], ); - bind_constructor_hash!(core, global, "Map", ctor_ptr, crate::builtins::map::map_constructor, 1); + bind_constructor!(core, global, "Map", ctor_ptr, crate::builtins::map::map_constructor, 1, hash: true); } diff --git a/crates/oxide_vm/src/bindings/bind_number.rs b/crates/oxide_vm/src/bindings/bind_number.rs index c0238fdd..988a4c83 100644 --- a/crates/oxide_vm/src/bindings/bind_number.rs +++ b/crates/oxide_vm/src/bindings/bind_number.rs @@ -1,72 +1,69 @@ use std::sync::Arc; -use oxide_kernel::bind_methods; -use oxide_kernel::builtin::NumberMethods; +use crate::bind_constructor; +use crate::bindings::{apply_binding_table, configure_native_constructor}; use oxide_kernel::kernel::{KernelCore, KernelSession}; -use oxide_types::object::{JsObject, NativeFnPtr}; +use oxide_types::object::JsObject; use oxide_types::value::JsValue; pub fn bind_number(core: &Arc, session: &KernelSession, global: &mut JsObject) { - let number_methods = NumberMethods { - is_nan: crate::builtins::number::number_is_nan as *const (), - is_finite: crate::builtins::number::number_is_finite as *const (), - parse_int: crate::builtins::number::number_parse_int as *const (), - parse_float: crate::builtins::number::number_parse_float as *const (), - to_string: crate::builtins::number::number_to_string as *const (), - to_fixed: crate::builtins::number::number_to_fixed as *const (), - is_integer: crate::builtins::number::number_is_integer as *const (), - is_safe_integer: crate::builtins::number::number_is_safe_integer as *const (), - to_exponential: crate::builtins::number::number_to_exponential as *const (), - to_precision: crate::builtins::number::number_to_precision as *const (), - value_of: crate::builtins::number::number_value_of as *const (), - }; - session.builtin_world().bind_number_methods( - &number_methods, - core.string_forge().as_ref(), - core.shape_forge().as_ref(), + let ctor_ptr = session.builtin_world().number_constructor.as_ptr() as *mut JsObject; + let ctor = unsafe { &mut *ctor_ptr }; + let proto_ptr = session.builtin_world().number_proto.as_ptr() as *mut JsObject; + let proto = unsafe { &mut *proto_ptr }; + + configure_native_constructor(ctor, crate::builtins::number::number_constructor as *const (), 1); + + apply_binding_table( + session.builtin_world(), + ctor, + core, + &[ + ("isNaN", crate::builtins::number::number_is_nan as *const (), 1), + ("isFinite", crate::builtins::number::number_is_finite as *const (), 1), + ("isInteger", crate::builtins::number::number_is_integer as *const (), 1), + ("isSafeInteger", crate::builtins::number::number_is_safe_integer as *const (), 1), + ], ); - let si_num = core.string_forge().intern("Number").0; - let num_shape = core.shape_forge().make_shape(global.shape_id(), si_num); - let num_val = JsValue::from_js_object(session.builtin_world().number_constructor.as_ptr() as *mut JsObject); - global.set_shape_id(num_shape); - global.ensure_hash_props().push(num_val); - global.bump_generation(); + apply_binding_table( + session.builtin_world(), + proto, + core, + &[ + ("toString", crate::builtins::number::number_to_string as *const (), 0), + ("toFixed", crate::builtins::number::number_to_fixed as *const (), 0), + ("toExponential", crate::builtins::number::number_to_exponential as *const (), 0), + ("toPrecision", crate::builtins::number::number_to_precision as *const (), 0), + ("valueOf", crate::builtins::number::number_value_of as *const (), 0), + ], + ); - { - let num_ctor_ptr = session.builtin_world().number_constructor.as_ptr() as *mut JsObject; - let num_ctor = unsafe { &mut *num_ctor_ptr }; - // SAFETY: number_constructor is a NativeFn fn-item. - num_ctor.set_native_fn(Some(unsafe { - NativeFnPtr::from_raw(crate::builtins::number::number_constructor as *const ()) - })); - num_ctor.set_native_arg_count(1); - - for (name, value) in [ - ("EPSILON", JsValue::float(2.220446049250313e-16)), - ("MAX_SAFE_INTEGER", JsValue::float(9007199254740991f64)), - ("MIN_SAFE_INTEGER", JsValue::float(-9007199254740991f64)), - ("MAX_VALUE", JsValue::float(1.7976931348623157e308)), - ("MIN_VALUE", JsValue::float(5e-324)), - ("NaN", JsValue::float(f64::NAN)), - ("NEGATIVE_INFINITY", JsValue::float(f64::NEG_INFINITY)), - ("POSITIVE_INFINITY", JsValue::float(f64::INFINITY)), - ] { - num_ctor.ensure_hash_props().push(value); - let prop_si = core.string_forge().intern(name).0; - let next_shape = core.shape_forge().make_shape(num_ctor.shape_id(), prop_si); - num_ctor.set_shape_id(next_shape); - } + for (name, value) in [ + ("EPSILON", JsValue::float(2.220446049250313e-16)), + ("MAX_SAFE_INTEGER", JsValue::float(9007199254740991f64)), + ("MIN_SAFE_INTEGER", JsValue::float(-9007199254740991f64)), + ("MAX_VALUE", JsValue::float(1.7976931348623157e308)), + ("MIN_VALUE", JsValue::float(5e-324)), + ("NaN", JsValue::float(f64::NAN)), + ("NEGATIVE_INFINITY", JsValue::float(f64::NEG_INFINITY)), + ("POSITIVE_INFINITY", JsValue::float(f64::INFINITY)), + ] { + ctor.ensure_hash_props().push(value); + let prop_si = core.string_forge().intern(name).0; + let next_shape = core.shape_forge().make_shape(ctor.shape_id(), prop_si); + ctor.set_shape_id(next_shape); } - let pi_fn = crate::builtins::number::number_parse_int as *const (); - let pf_fn = crate::builtins::number::number_parse_float as *const (); - bind_methods!( + bind_constructor!(core, global, "Number", ctor_ptr, crate::builtins::number::number_constructor, 1, hash: true); + + apply_binding_table( session.builtin_world(), global, - core.string_forge().as_ref(), - core.shape_forge().as_ref(), - ("parseInt", pi_fn, 1), - ("parseFloat", pf_fn, 1), + core, + &[ + ("parseInt", crate::builtins::number::number_parse_int as *const (), 1), + ("parseFloat", crate::builtins::number::number_parse_float as *const (), 1), + ], ); } diff --git a/crates/oxide_vm/src/bindings/bind_regexp.rs b/crates/oxide_vm/src/bindings/bind_regexp.rs index 26cca18c..a3c80a88 100644 --- a/crates/oxide_vm/src/bindings/bind_regexp.rs +++ b/crates/oxide_vm/src/bindings/bind_regexp.rs @@ -4,7 +4,7 @@ use crate::bindings::{apply_binding_table, configure_native_constructor}; use oxide_kernel::kernel::{KernelCore, KernelSession}; use oxide_types::object::JsObject; -use crate::bind_constructor_hash; +use crate::bind_constructor; pub fn bind_regexp(core: &Arc, session: &KernelSession, global: &mut JsObject) { let ctor_ptr = session.builtin_world().regexp_constructor.as_ptr() as *mut JsObject; @@ -25,5 +25,5 @@ pub fn bind_regexp(core: &Arc, session: &KernelSession, global: &mut ], ); - bind_constructor_hash!(core, global, "RegExp", ctor_ptr, crate::builtins::regexp::regexp_constructor, 2); + bind_constructor!(core, global, "RegExp", ctor_ptr, crate::builtins::regexp::regexp_constructor, 2, hash: true); } diff --git a/crates/oxide_vm/src/bindings/bind_set.rs b/crates/oxide_vm/src/bindings/bind_set.rs index 9629dd3c..0e51ffc1 100644 --- a/crates/oxide_vm/src/bindings/bind_set.rs +++ b/crates/oxide_vm/src/bindings/bind_set.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use crate::bind_constructor_hash; +use crate::bind_constructor; use crate::bindings::{apply_binding_table, configure_native_constructor}; use oxide_kernel::kernel::{KernelCore, KernelSession}; use oxide_types::object::JsObject; @@ -26,5 +26,5 @@ pub fn bind_set(core: &Arc, session: &KernelSession, global: &mut Js ], ); - bind_constructor_hash!(core, global, "Set", ctor_ptr, crate::builtins::set::set_constructor, 1); + bind_constructor!(core, global, "Set", ctor_ptr, crate::builtins::set::set_constructor, 1, hash: true); } diff --git a/crates/oxide_vm/src/bindings/bind_symbol.rs b/crates/oxide_vm/src/bindings/bind_symbol.rs index abad8524..3625666a 100644 --- a/crates/oxide_vm/src/bindings/bind_symbol.rs +++ b/crates/oxide_vm/src/bindings/bind_symbol.rs @@ -5,7 +5,7 @@ use oxide_kernel::kernel::{KernelCore, KernelSession}; use oxide_types::object::JsObject; use oxide_types::value::JsValue; -use crate::bind_constructor_hash; +use crate::bind_constructor; pub fn bind_symbol(core: &Arc, session: &KernelSession, global: &mut JsObject) { let ctor_ptr = session.builtin_world().symbol_constructor.as_ptr() as *mut JsObject; @@ -57,7 +57,7 @@ pub fn bind_symbol(core: &Arc, session: &KernelSession, global: &mut bind_well_known_symbol(core, ctor, name, val); } - bind_constructor_hash!(core, global, "Symbol", ctor_ptr, crate::builtins::symbol::symbol_constructor, 1); + bind_constructor!(core, global, "Symbol", ctor_ptr, crate::builtins::symbol::symbol_constructor, 1, hash: true); } fn bind_well_known_symbol(core: &Arc, ctor: &mut JsObject, name: &str, val: JsValue) { diff --git a/crates/oxide_vm/src/bindings/bind_typed_array.rs b/crates/oxide_vm/src/bindings/bind_typed_array.rs index f5c09cd2..dcd8a883 100644 --- a/crates/oxide_vm/src/bindings/bind_typed_array.rs +++ b/crates/oxide_vm/src/bindings/bind_typed_array.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use crate::bind_constructor_hash; +use crate::bind_constructor; use crate::bindings::{apply_binding_table, configure_native_constructor}; use oxide_kernel::kernel::{KernelCore, KernelSession}; use oxide_types::object::JsObject; @@ -9,7 +9,7 @@ macro_rules! bind_typed_array_constructor { ($core:expr, $global:expr, $name:literal, $ctor_ptr:expr, $ctor_fn:path) => {{ let ctor = unsafe { &mut *$ctor_ptr }; configure_native_constructor(ctor, $ctor_fn as *const (), 1); - bind_constructor_hash!($core, $global, $name, $ctor_ptr, $ctor_fn, 1); + bind_constructor!($core, $global, $name, $ctor_ptr, $ctor_fn, 1, hash: true); }}; } diff --git a/crates/oxide_vm/src/bindings/mod.rs b/crates/oxide_vm/src/bindings/mod.rs index 3117967d..596f37d0 100644 --- a/crates/oxide_vm/src/bindings/mod.rs +++ b/crates/oxide_vm/src/bindings/mod.rs @@ -32,29 +32,20 @@ use oxide_types::value::JsValue; #[macro_export] macro_rules! bind_constructor { ($core:expr, $global:expr, $name:literal, $ctor_ptr:expr, $ctor_fn:path, $nargs:literal) => {{ - let si = $core.string_forge().intern($name).0; - let shape = $core.shape_forge().make_shape($global.shape_id(), si); - let val = $crate::JsValue::from_js_object($ctor_ptr); - $global.set_shape_id(shape); - $global.push_prop(val); - let ctor = unsafe { &mut *$ctor_ptr }; - // SAFETY: $ctor_fn is a fn-item expression; casting to *const () preserves validity. - ctor.set_native_fn(Some(unsafe { oxide_types::object::NativeFnPtr::from_raw($ctor_fn as *const ()) })); - ctor.set_native_arg_count($nargs); + bind_constructor!($core, $global, $name, $ctor_ptr, $ctor_fn, $nargs, hash: false) }}; -} - -#[macro_export] -macro_rules! bind_constructor_hash { - ($core:expr, $global:expr, $name:literal, $ctor_ptr:expr, $ctor_fn:path, $nargs:literal) => {{ + ($core:expr, $global:expr, $name:literal, $ctor_ptr:expr, $ctor_fn:path, $nargs:literal, hash: $hash:literal) => {{ let si = $core.string_forge().intern($name).0; let shape = $core.shape_forge().make_shape($global.shape_id(), si); let val = $crate::JsValue::from_js_object($ctor_ptr); $global.set_shape_id(shape); - $global.ensure_hash_props().push(val); - $global.bump_generation(); + if $hash { + $global.ensure_hash_props().push(val); + $global.bump_generation(); + } else { + $global.push_prop(val); + } let ctor = unsafe { &mut *$ctor_ptr }; - // SAFETY: $ctor_fn is a fn-item expression; casting to *const () preserves validity. ctor.set_native_fn(Some(unsafe { oxide_types::object::NativeFnPtr::from_raw($ctor_fn as *const ()) })); ctor.set_native_arg_count($nargs); }}; @@ -383,92 +374,73 @@ pub fn bind_global_builtin_slots(core: &Arc, session: &KernelSession /// Maintenance: when adding a `BuiltinDirtySet` group, update this rebind map, /// `BuiltinSnapshot`, and `BuiltinWorld::rebuild_with_dirty()` together. -pub fn rebind_dirty_builtins(core: &Arc, session: &mut KernelSession, dirty: &BuiltinDirtySet) { +pub fn rebind_dirty_builtins(core: &Arc, session: &mut KernelSession, dirty: Option<&BuiltinDirtySet>) { let global_ptr = session.global_object().as_ptr() as *mut JsObject; let global = unsafe { &mut *global_ptr }; - if dirty.object { + if dirty.map_or(true, |d| d.object) { bind_object::bind_object(core, session, global); } - if dirty.array { + if dirty.map_or(true, |d| d.array) { bind_array::bind_array(core, session, global); } - if dirty.array_buffer { + if dirty.map_or(true, |d| d.array_buffer) { bind_array_buffer::bind_array_buffer(core, session, global); } - if dirty.data_view { + if dirty.map_or(true, |d| d.data_view) { bind_data_view::bind_data_view(core, session, global); } - if dirty.typed_array_family { + if dirty.map_or(true, |d| d.typed_array_family) { bind_typed_array::bind_typed_array(core, session, global); } - if dirty.error_family { + if dirty.map_or(true, |d| d.error_family) { bind_error::bind_error(core, session, global); } - if dirty.string { + if dirty.map_or(true, |d| d.string) { bind_string::bind_string(core, session, global); } - if dirty.number { + if dirty.map_or(true, |d| d.number) { bind_number::bind_number(core, session, global); } - if dirty.math { + if dirty.map_or(true, |d| d.math) { bind_math::bind_math(core, session, global); } - if dirty.json { + if dirty.map_or(true, |d| d.json) { bind_json::bind_json(core, session, global); } - if dirty.date { + if dirty.map_or(true, |d| d.date) { bind_date::bind_date(core, session, global); } - if dirty.set { + if dirty.map_or(true, |d| d.set) { bind_set::bind_set(core, session, global); } - if dirty.map { + if dirty.map_or(true, |d| d.map) { bind_map::bind_map(core, session, global); } - if dirty.boolean { + if dirty.map_or(true, |d| d.boolean) { bind_boolean::bind_boolean(core, session, global); } - if dirty.function { + if dirty.map_or(true, |d| d.function) { bind_function::bind_function(core, session, global); } - if dirty.regexp { + if dirty.map_or(true, |d| d.regexp) { bind_regexp::bind_regexp(core, session, global); } - if dirty.symbol_family { + if dirty.map_or(true, |d| d.symbol_family) { bind_symbol::bind_symbol(core, session, global); } - if dirty.stubs { + if dirty.map_or(true, |d| d.stubs) { bind_stubs::bind_stubs(core, session, global); } } pub fn init_kernel_builtins(core: &Arc, session: &mut KernelSession) { + rebind_dirty_builtins(core, session, None); let global_ptr = session.global_object().as_ptr() as *mut oxide_types::object::JsObject; let global = unsafe { &mut *global_ptr }; - - bind_object::bind_object(core, session, global); - bind_array::bind_array(core, session, global); - bind_array_buffer::bind_array_buffer(core, session, global); - bind_data_view::bind_data_view(core, session, global); - bind_typed_array::bind_typed_array(core, session, global); - bind_error::bind_error(core, session, global); - bind_string::bind_string(core, session, global); - bind_number::bind_number(core, session, global); - bind_math::bind_math(core, session, global); - bind_json::bind_json(core, session, global); - bind_date::bind_date(core, session, global); - bind_set::bind_set(core, session, global); - bind_map::bind_map(core, session, global); - bind_boolean::bind_boolean(core, session, global); - bind_function::bind_function(core, session, global); - bind_regexp::bind_regexp(core, session, global); - bind_symbol::bind_symbol(core, session, global); bind_iterator::bind_iterator(core, session, global); bind_reflect::bind_reflect(core, session, global); bind_global::bind_global(core, session, global); - bind_stubs::bind_stubs(core, session, global); - bind_global_value(core, global, "globalThis", JsValue::from_js_object(global_ptr)); session.record_snapshot(); } diff --git a/crates/oxide_vm/src/coercion.rs b/crates/oxide_vm/src/coercion.rs index 2cc428a0..d4c27c85 100644 --- a/crates/oxide_vm/src/coercion.rs +++ b/crates/oxide_vm/src/coercion.rs @@ -4,10 +4,6 @@ use oxide_types::object::JsObject; use oxide_types::shape::EMPTY_SHAPE_ID; use oxide_types::value::JsValue; -pub fn to_primitive(val: JsValue) -> JsValue { - val -} - pub fn to_number(val: JsValue, string_forge: &StringForge) -> f64 { if val.is_int() { return val.as_int() as f64; diff --git a/crates/oxide_vm/src/native.rs b/crates/oxide_vm/src/native.rs index beb1752e..c693b389 100644 --- a/crates/oxide_vm/src/native.rs +++ b/crates/oxide_vm/src/native.rs @@ -15,14 +15,6 @@ impl NativeResult { Self::Err(val) } - pub fn into_result(self) -> Result { - match self { - Self::Ok(val) => Ok(val), - Self::Err(err) => Err(err), - Self::TailCall { .. } => panic!("TailCall cannot be converted to Result"), - } - } - pub fn unwrap(self) -> JsValue { match self { Self::Ok(val) => val, diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index d76a79bf..41d468d9 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -25,22 +25,11 @@ pub struct SessionGc { impl SessionGc { pub fn new() -> Self { - Self { - total_collections: 0, - total_bytes_freed: 0, - total_objects_scanned: 0, - total_objects_live: 0, - total_objects_dead: 0, - last_collection_objects_scanned: 0, - last_collection_objects_live: 0, - last_collection_objects_dead: 0, - last_collection_bytes_freed: 0, - last_collection_duration_us: 0, - max_collection_duration_us: 0, - min_collection_duration_us: u64::MAX, - } + Self::default() } +} +impl SessionGc { #[inline] pub(crate) fn is_session_ptr(&self, vm: &Vm, obj_ptr: *mut JsObject) -> bool { vm.is_session_ptr(obj_ptr) @@ -329,7 +318,20 @@ impl SessionGc { impl Default for SessionGc { fn default() -> Self { - Self::new() + Self { + total_collections: 0, + total_bytes_freed: 0, + total_objects_scanned: 0, + total_objects_live: 0, + total_objects_dead: 0, + last_collection_objects_scanned: 0, + last_collection_objects_live: 0, + last_collection_objects_dead: 0, + last_collection_bytes_freed: 0, + last_collection_duration_us: 0, + max_collection_duration_us: 0, + min_collection_duration_us: u64::MAX, + } } } @@ -487,7 +489,7 @@ mod tests { vm.regs[0] = JsValue::from_js_object(root_session); let roots = vm.gc_roots(); - let mut gc = std::mem::replace(&mut vm.session_gc, SessionGc::new()); + let mut gc = std::mem::take(&mut vm.session_gc); gc.mark(&vm, &roots); vm.session_gc = gc; @@ -512,7 +514,7 @@ mod tests { vm.regs[0] = JsValue::from_js_object(root_session); let dead_session = vm.promote_object(dead); let roots = vm.gc_roots(); - let mut gc = std::mem::replace(&mut vm.session_gc, SessionGc::new()); + let mut gc = std::mem::take(&mut vm.session_gc); gc.mark(&vm, &roots); let _ = gc.sweep(&mut vm); vm.session_gc = gc; @@ -624,7 +626,7 @@ mod tests { let map_session = vm.promote_object(map_value.as_js_object_ptr()); vm.regs.fill(JsValue::undefined()); vm.regs[0] = JsValue::from_js_object(map_session); - let mut gc = std::mem::replace(&mut vm.session_gc, SessionGc::new()); + let mut gc = std::mem::take(&mut vm.session_gc); gc.collect(&mut vm); vm.session_gc = gc; @@ -647,7 +649,7 @@ mod tests { let set_session = vm.promote_object(set_value.as_js_object_ptr()); vm.regs.fill(JsValue::undefined()); vm.regs[0] = JsValue::from_js_object(set_session); - let mut gc = std::mem::replace(&mut vm.session_gc, SessionGc::new()); + let mut gc = std::mem::take(&mut vm.session_gc); gc.collect(&mut vm); vm.session_gc = gc; @@ -686,7 +688,7 @@ mod tests { let root_session = vm.promote_object(root); vm.regs.fill(JsValue::undefined()); vm.regs[0] = JsValue::from_js_object(root_session); - let mut gc = std::mem::replace(&mut vm.session_gc, SessionGc::new()); + let mut gc = std::mem::take(&mut vm.session_gc); gc.collect(&mut vm); vm.session_gc = gc; @@ -735,7 +737,7 @@ mod tests { let view_session = vm.promote_object(view.as_js_object_ptr()); vm.regs.fill(JsValue::undefined()); vm.regs[0] = JsValue::from_js_object(view_session); - let mut gc = std::mem::replace(&mut vm.session_gc, SessionGc::new()); + let mut gc = std::mem::take(&mut vm.session_gc); gc.collect(&mut vm); vm.session_gc = gc; diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 3f73b71b..f74a2cf5 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -315,12 +315,6 @@ impl Vm { roots } - pub fn gc_clear_marks(&mut self) { - let mut session_gc = std::mem::take(&mut self.session_gc); - session_gc.clear_all_marks(self); - self.session_gc = session_gc; - } - pub(crate) fn maybe_collect_session_gc(&mut self) { let mut session_gc = std::mem::take(&mut self.session_gc); session_gc.maybe_collect(self); @@ -442,14 +436,6 @@ impl Vm { self.regs[idx as usize] = val; } - pub fn regs_mut(&mut self) -> &mut [JsValue; 256] { - &mut self.regs - } - - pub fn epoch_mut(&mut self) -> &mut Epoch { - &mut self.epoch - } - pub fn epoch(&self) -> &Epoch { &self.epoch } @@ -603,7 +589,7 @@ impl Vm { self.call_bytecode_function_inline(callee, callee_obj, receiver, args) } - #[allow(clippy::too_many_arguments)] + #[expect(clippy::too_many_arguments)] pub(crate) fn push_bytecode_frame( &mut self, callee: JsValue, this_value: JsValue, args: &[JsValue], construct_result_reg: Option, constructed_this: Option, new_target: JsValue, continuation: FrameContinuation, diff --git a/crates/oxide_vm/src/vm_pool.rs b/crates/oxide_vm/src/vm_pool.rs index 7e5b48bb..daf04366 100644 --- a/crates/oxide_vm/src/vm_pool.rs +++ b/crates/oxide_vm/src/vm_pool.rs @@ -15,8 +15,6 @@ pub struct VmPool { kernel_core: Arc, inner: Mutex, condvar: Condvar, - #[allow(dead_code)] - min_size: usize, max_size: Option, } @@ -28,30 +26,16 @@ pub struct VmGuard { } impl VmPool { - pub fn new( - kernel_core: Arc, #[allow(dead_code)] min_size: usize, max_size: Option, - ) -> Arc { - let pool = Arc::new(Self { + pub fn new(kernel_core: Arc, _min_size: usize, max_size: Option) -> Arc { + Arc::new(Self { kernel_core, inner: Mutex::new(VmPoolInner { - available: Vec::with_capacity(min_size), + available: Vec::new(), total_count: 0, }), condvar: Condvar::new(), - min_size, max_size, - }); - - { - let mut inner = pool.inner.lock().unwrap(); - for _ in 0..min_size { - let vm = Self::new_vm(&pool.kernel_core); - inner.available.push(vm); - inner.total_count += 1; - } - } - - pool + }) } fn new_vm(core: &Arc) -> Vm { @@ -117,10 +101,6 @@ impl VmGuard { pub fn vm_mut(&mut self) -> &mut Vm { self.vm.as_mut().expect("VmGuard has no VM") } - - pub fn mark_dirty(&mut self) { - self.dirty = true; - } } impl Drop for VmGuard { diff --git a/crates/oxide_vm/src/vm_runtime.rs b/crates/oxide_vm/src/vm_runtime.rs index 8272fb23..e0d8a592 100644 --- a/crates/oxide_vm/src/vm_runtime.rs +++ b/crates/oxide_vm/src/vm_runtime.rs @@ -157,11 +157,6 @@ impl Vm { self.dispatch() } - #[allow(dead_code)] - pub fn call_bytecode_func(&mut self, _callback_obj: &JsObject, _args_regs: &[u8]) -> Result { - Err("bytecode function calls not yet supported".into()) - } - pub(crate) fn unwind(&mut self) -> Result<(), String> { while let Some(handler) = self.try_stack.pop() { while self.frames.len() > handler.frame_depth { diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index 7eb5ed88..2084b395 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -108,7 +108,7 @@ impl Vm { pub fn full_reset(&mut self) { let dirty = self.session.selective_reset(&self.kernel_core); if dirty.any_builtin_dirty() { - bindings::rebind_dirty_builtins(&self.kernel_core, &mut self.session, &dirty); + bindings::rebind_dirty_builtins(&self.kernel_core, &mut self.session, Some(&dirty)); } if dirty.global { let global_ptr = self.session.global_object().as_ptr() as *mut JsObject; From f0da49a54c6e9c47136c27e86cf4ee8d88e362b3 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Wed, 24 Jun 2026 15:47:35 +0800 Subject: [PATCH 03/32] =?UTF-8?q?fix:=20GC=20eprintln!=20=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=20vm=5Fdebug!=EF=BC=8Crelease=20=E4=B8=8B=E9=9B=B6=20?= =?UTF-8?q?stderr?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_vm/src/session_gc.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index 41d468d9..64b1b1ba 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -2,6 +2,7 @@ use std::collections::{HashMap, VecDeque}; use std::mem::size_of; use std::time::Instant; +use oxide_kernel::vm_debug; use oxide_types::object::{JsObject, PropMetaEntry}; use oxide_types::value::JsValue; @@ -258,9 +259,15 @@ impl SessionGc { let total_ptrs = survivors + dead; if total_ptrs > 0 { if dead == 0 { - eprintln!("[GC] sweep phase -> no objects collected ({} live, {} dead)", survivors, dead); + vm_debug!("[GC] sweep phase -> no objects collected ({} live, {} dead)", survivors, dead); } else { - eprintln!("[GC] sweep phase: {total_ptrs} scanned, {survivors} live, {dead} dead, {freed_bytes} bytes"); + vm_debug!( + "[GC] sweep phase: {} scanned, {} live, {} dead, {} bytes", + total_ptrs, + survivors, + dead, + freed_bytes + ); } } @@ -293,7 +300,7 @@ impl SessionGc { self.min_collection_duration_us = self.min_collection_duration_us.min(self.last_collection_duration_us); if freed_bytes > 0 || self.total_collections % 100 == 0 { - eprintln!("{}", self.stats_summary()); + vm_debug!("{}", self.stats_summary()); } } From 4535f70f80b003f9b0f1e98fc9ff9505bb7e7bd6 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Wed, 24 Jun 2026 15:59:55 +0800 Subject: [PATCH 04/32] =?UTF-8?q?fix:=20GC=20sweep=20=E5=A4=8D=E7=94=A8=20?= =?UTF-8?q?Vm.forwarding=EF=BC=8C=E6=B6=88=E9=99=A4=E6=AF=8F=E8=BD=AE=20Ha?= =?UTF-8?q?shMap=20=E5=88=86=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 1 + crates/oxide_vm/Cargo.toml | 1 + crates/oxide_vm/src/session_gc.rs | 33 ++++++++++++++++++++++++++++--- crates/oxide_vm/src/vm.rs | 5 +++++ crates/oxide_vm/src/vm_support.rs | 2 ++ 5 files changed, 39 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 79804039..e56ce4b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1125,6 +1125,7 @@ dependencies = [ "oxide_types", "proptest", "regex", + "rustc-hash", "ryu", "serde_json", "smallvec", diff --git a/crates/oxide_vm/Cargo.toml b/crates/oxide_vm/Cargo.toml index 365a1cb2..9807b34d 100644 --- a/crates/oxide_vm/Cargo.toml +++ b/crates/oxide_vm/Cargo.toml @@ -21,6 +21,7 @@ indexmap.workspace = true smallvec.workspace = true unicode-normalization.workspace = true tracing.workspace = true +rustc-hash.workspace = true [dev-dependencies] oxide_compiler = { path = "../oxide_compiler" } diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index 64b1b1ba..7e945fbd 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -5,6 +5,7 @@ use std::time::Instant; use oxide_kernel::vm_debug; use oxide_types::object::{JsObject, PropMetaEntry}; use oxide_types::value::JsValue; +use rustc_hash::FxBuildHasher; use crate::builtins::{array_buffer, data_view, map, regexp, set, typed_array}; use crate::vm::Vm; @@ -187,7 +188,7 @@ impl SessionGc { pub(crate) fn sweep(&mut self, vm: &mut Vm) -> u64 { let old_ptrs = std::mem::take(&mut vm.session_object_ptrs); - let mut forwarding: HashMap<*mut JsObject, *mut JsObject> = HashMap::new(); + let mut forwarding = std::mem::take(&mut vm.forwarding); let new_arena = bumpalo::Bump::new(); let mut survivors = 0u64; let mut dead = 0u64; @@ -251,6 +252,8 @@ impl SessionGc { rewrite_vm_roots(vm, &forwarding); vm.session_object_ptrs = forwarding.values().copied().collect(); + forwarding.clear(); + vm.forwarding = forwarding; vm.session_epoch = new_arena; vm.session_bytes_allocated = vm.session_object_ptrs.len() * size_of::(); @@ -342,7 +345,9 @@ impl Default for SessionGc { } } -fn rewrite_forwarded_value(value: JsValue, forwarding: &HashMap<*mut JsObject, *mut JsObject>) -> JsValue { +fn rewrite_forwarded_value( + value: JsValue, forwarding: &HashMap<*mut JsObject, *mut JsObject, FxBuildHasher>, +) -> JsValue { if !value.is_object() { return value; } @@ -352,7 +357,7 @@ fn rewrite_forwarded_value(value: JsValue, forwarding: &HashMap<*mut JsObject, * .unwrap_or(value) } -fn rewrite_vm_roots(vm: &mut Vm, forwarding: &HashMap<*mut JsObject, *mut JsObject>) { +fn rewrite_vm_roots(vm: &mut Vm, forwarding: &HashMap<*mut JsObject, *mut JsObject, FxBuildHasher>) { for value in &mut vm.regs { *value = rewrite_forwarded_value(*value, forwarding); } @@ -532,6 +537,28 @@ mod tests { assert!(!vm.session_object_ptrs.iter().any(|ptr| unsafe { (*(*ptr)).is_gc_marked() })); } + #[test] + fn forwarding_is_cleared_after_sweep() { + let mut vm = Vm::new(); + let root = plain_object(&mut vm); + let child = plain_object(&mut vm); + unsafe { + (*root).set_prop_at(0, JsValue::from_js_object(child)); + } + let root_session = vm.promote_object(root); + vm.regs[0] = JsValue::from_js_object(root_session); + let _ = vm.promote_object(child); + let roots = vm.gc_roots(); + let mut gc = std::mem::take(&mut vm.session_gc); + gc.mark(&vm, &roots); + let _ = gc.sweep(&mut vm); + vm.session_gc = gc; + + // The reused forwarding map MUST be cleared after sweep, otherwise + // promote would observe stale old->new entries pointing into the freed arena. + assert!(vm.forwarding.is_empty()); + } + fn vm_with_low_threshold() -> Vm { let mut cfg = KernelConfig::minimal(); cfg.set_session_gc_threshold(1); diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index f74a2cf5..49130033 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -155,6 +155,11 @@ pub struct Vm { pub epoch: Epoch, pub(crate) session_epoch: bumpalo::Bump, pub(crate) session_gc: SessionGc, + /// Long-lived forwarding map reused by GC sweep and promote. + /// `mem::take`'d out for use, `clear()`'d and put back — capacity retained + /// across collections, so no per-event `HashMap` allocation. `FxBuildHasher` + /// because the keys are raw pointers (collision resistance is irrelevant). + pub(crate) forwarding: HashMap<*mut JsObject, *mut JsObject, rustc_hash::FxBuildHasher>, pub(crate) epoch_object_ptrs: Vec<*mut JsObject>, pub(crate) session_object_ptrs: Vec<*mut JsObject>, pub(crate) session_bytes_allocated: usize, diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index 2084b395..a74c66c9 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -32,6 +32,7 @@ impl Vm { epoch: Epoch::new(), session_epoch: bumpalo::Bump::new(), session_gc: crate::session_gc::SessionGc::new(), + forwarding: std::collections::HashMap::with_hasher(rustc_hash::FxBuildHasher), epoch_object_ptrs: Vec::new(), session_object_ptrs: Vec::new(), session_bytes_allocated: 0, @@ -77,6 +78,7 @@ impl Vm { epoch: Epoch::new(), session_epoch: bumpalo::Bump::new(), session_gc: crate::session_gc::SessionGc::new(), + forwarding: std::collections::HashMap::with_hasher(rustc_hash::FxBuildHasher), epoch_object_ptrs: Vec::new(), session_object_ptrs: Vec::new(), session_bytes_allocated: 0, From 94405f50d73e2bca3463d07bc28cf4a217d4f6d0 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Wed, 24 Jun 2026 16:03:53 +0800 Subject: [PATCH 05/32] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E6=B3=A8=E9=87=8A=E4=B8=AD=E7=9A=84=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E5=86=85=E9=83=A8=E7=BC=96=E5=8F=B7=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_bytecode/src/module.rs | 4 ++-- crates/oxide_compiler/src/compiler.rs | 2 +- crates/oxide_compiler/src/emitter/expr/function.rs | 2 +- crates/oxide_compiler/src/emitter/expr/object.rs | 2 +- crates/oxide_compiler/tests/arrow_template_tests.rs | 2 +- crates/oxide_types/src/mem.rs | 6 +++--- crates/oxide_vm/src/builtins/stubs.rs | 2 +- crates/oxide_vm/src/vm_support.rs | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/oxide_bytecode/src/module.rs b/crates/oxide_bytecode/src/module.rs index ac4cd0ea..827d7eb9 100644 --- a/crates/oxide_bytecode/src/module.rs +++ b/crates/oxide_bytecode/src/module.rs @@ -22,13 +22,13 @@ pub struct CompiledModule { pub param_base: u8, pub builtin_reg_map: Vec<(String, u8)>, pub sub_modules: Vec, - /// True when this module is an arrow function body (D-01). + /// True when this module is an arrow function body. /// Arrow functions capture lexical `this` from the enclosing scope. pub is_arrow: bool, /// Index into `constants` holding the captured `this` JsValue. /// 0 means "not captured - use standard this binding". pub captured_this_const_idx: u16, - /// Function name inferred from assignment context (D-04). + /// Function name inferred from assignment context. /// Set at the VariableDeclaration / ObjectProperty assignment site. pub function_name: Option, /// True when this bytecode function is a class constructor. diff --git a/crates/oxide_compiler/src/compiler.rs b/crates/oxide_compiler/src/compiler.rs index 0ba1167a..2563251b 100644 --- a/crates/oxide_compiler/src/compiler.rs +++ b/crates/oxide_compiler/src/compiler.rs @@ -159,7 +159,7 @@ pub(crate) struct CompileCtx { pub(crate) builtin_reg_map: Vec<(String, u8)>, pub(crate) sub_modules: Vec, /// Register holding `this` in the enclosing function context. - /// Used by arrow functions to capture lexical `this` (D-01). + /// Used by arrow functions to capture lexical `this`. /// Initialized to 254 (conventional this register) at the top level. pub(crate) enclosing_this_reg: u8, pub(crate) in_derived_constructor: bool, diff --git a/crates/oxide_compiler/src/emitter/expr/function.rs b/crates/oxide_compiler/src/emitter/expr/function.rs index 681c0a4a..d8773ecd 100644 --- a/crates/oxide_compiler/src/emitter/expr/function.rs +++ b/crates/oxide_compiler/src/emitter/expr/function.rs @@ -4,7 +4,7 @@ impl Compiler { pub(in crate::emitter) fn emit_arrow_function_expression( &self, arrow: &oxide_parser::ArrowFunctionExpression, ctx: &mut CompileCtx, ) -> Result { - // Rest params not yet supported (D-06 placeholder) + // Rest params not yet supported if let Some(_rest) = &arrow.params.rest { return Err("rest params in arrow functions not yet supported".into()); } diff --git a/crates/oxide_compiler/src/emitter/expr/object.rs b/crates/oxide_compiler/src/emitter/expr/object.rs index 96d6ee83..e8792f07 100644 --- a/crates/oxide_compiler/src/emitter/expr/object.rs +++ b/crates/oxide_compiler/src/emitter/expr/object.rs @@ -38,7 +38,7 @@ impl Compiler { let key_reg = ctx.alloc_reg(); ctx.emit_load_const(key_reg, idx); let val_reg = self.emit_expression(&p.value, ctx)?; - // Name inference (D-04): if property value is an arrow function, + // Name inference: if property value is an arrow function, // set the compiled sub_module's function_name to the property key. if matches!(&p.value, Expression::ArrowFunctionExpression(_)) { if let Some(sub_mod) = ctx.sub_modules.last_mut() { diff --git a/crates/oxide_compiler/tests/arrow_template_tests.rs b/crates/oxide_compiler/tests/arrow_template_tests.rs index 5480a331..532af265 100644 --- a/crates/oxide_compiler/tests/arrow_template_tests.rs +++ b/crates/oxide_compiler/tests/arrow_template_tests.rs @@ -76,7 +76,7 @@ fn compile_arrow_name_inference() { .sub_modules .iter() .any(|m| m.is_arrow && m.function_name.as_deref() == Some("myArrow")); - assert!(named, "arrow function should have function_name='myArrow' from D-04 inference"); + assert!(named, "arrow function should have function_name='myArrow' from name inference"); } #[test] diff --git a/crates/oxide_types/src/mem.rs b/crates/oxide_types/src/mem.rs index 2ad779d1..0f46a5f0 100644 --- a/crates/oxide_types/src/mem.rs +++ b/crates/oxide_types/src/mem.rs @@ -53,9 +53,9 @@ impl fmt::Display for P { /// Persistent heap for cross-epoch object storage. /// -/// Phase 3 provides a minimal API: `promote(value)` moves `value` to the -/// global heap under `Arc` and returns `P`. Phase 7 (OxideKernel) expands -/// with typed storage for shapes, code, IC templates, and strings. +/// Provides a minimal API: `promote(value)` moves `value` to the +/// global heap under `Arc` and returns `P`. Typed storage for shapes, +/// code, IC templates, and strings lives in the OxideKernel. pub struct PersistentHeap; impl PersistentHeap { diff --git a/crates/oxide_vm/src/builtins/stubs.rs b/crates/oxide_vm/src/builtins/stubs.rs index 1f985c5d..3769ae84 100644 --- a/crates/oxide_vm/src/builtins/stubs.rs +++ b/crates/oxide_vm/src/builtins/stubs.rs @@ -5,7 +5,7 @@ fn stub_error(vm: &mut Vm, name: &str) -> NativeResult { NativeResult::Err(crate::builtins::error::create_type_error(vm, &format!("{name} is not implemented"))) } -// Architecturally deferred — see 13.5-CONTEXT.md for rationale +// Architecturally deferred — these features are excluded from the supported language subset by design. pub fn proxy_stub(vm: &mut Vm, _args: &[u8]) -> NativeResult { stub_error(vm, "Proxy") } diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index a74c66c9..ad575273 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -202,7 +202,7 @@ impl Vm { /// Create a function JsObject for a BytecodeFunc constant. /// When `is_arrow` is true, captures the current `this` (regs[254]) - /// for lexical this binding at call time (D-01). + /// for lexical this binding at call time. fn create_function_object( &mut self, sub_idx: u32, is_arrow: bool, is_class_constructor: bool, is_derived_constructor: bool, needs_home_object: bool, From c0f16dc76443065bccf5989d69637902819358be Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Wed, 24 Jun 2026 16:08:17 +0800 Subject: [PATCH 06/32] =?UTF-8?q?fix:=20promote=20=E5=A4=8D=E7=94=A8=20Vm.?= =?UTF-8?q?forwarding=EF=BC=8C=E6=B6=88=E9=99=A4=20promote=20=E7=9A=84=20H?= =?UTF-8?q?ashMap=20=E5=88=86=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_vm/src/session_arena.rs | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/crates/oxide_vm/src/session_arena.rs b/crates/oxide_vm/src/session_arena.rs index 2c5e9313..dbe6bf44 100644 --- a/crates/oxide_vm/src/session_arena.rs +++ b/crates/oxide_vm/src/session_arena.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use oxide_types::object::JsObject; use oxide_types::value::JsValue; +use rustc_hash::FxBuildHasher; use crate::builtins::{data_view, map, set, typed_array}; use crate::vm::Vm; @@ -16,12 +17,15 @@ impl Vm { } pub(crate) fn promote_object(&mut self, src: *mut JsObject) -> *mut JsObject { - let mut forwarding = HashMap::new(); - self.promote_object_inner(src, &mut forwarding) + let mut forwarding = std::mem::take(&mut self.forwarding); + let result = self.promote_object_inner(src, &mut forwarding); + forwarding.clear(); + self.forwarding = forwarding; + result } pub(crate) fn promote_object_inner( - &mut self, src: *mut JsObject, forwarding: &mut HashMap<*mut JsObject, *mut JsObject>, + &mut self, src: *mut JsObject, forwarding: &mut HashMap<*mut JsObject, *mut JsObject, FxBuildHasher>, ) -> *mut JsObject { if src.is_null() { return src; @@ -63,7 +67,7 @@ impl Vm { } pub(crate) fn promote_value_if_epoch_object( - &mut self, value: JsValue, forwarding: &mut HashMap<*mut JsObject, *mut JsObject>, + &mut self, value: JsValue, forwarding: &mut HashMap<*mut JsObject, *mut JsObject, FxBuildHasher>, ) -> JsValue { if !value.is_object() { return value; @@ -230,4 +234,20 @@ mod tests { assert!(!is_epoch_object(&vm, meta.get)); assert!(!is_epoch_object(&vm, meta.set)); } + + #[test] + fn promote_clears_forwarding_map() { + let mut vm = Vm::new(); + let first = plain_object(&mut vm); + let promoted = vm.promote_object(first); + assert!(!promoted.is_null()); + // The shared forwarding map must be cleared after each promote so a + // later promote (or GC sweep) never observes stale old->new mappings. + assert!(vm.forwarding.is_empty()); + + let second = plain_object(&mut vm); + let promoted2 = vm.promote_object(second); + assert!(!promoted2.is_null()); + assert!(vm.forwarding.is_empty()); + } } From bb4ce39e50cc06742c8c19cdb2f1ba896ae01b67 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Wed, 24 Jun 2026 16:17:40 +0800 Subject: [PATCH 07/32] =?UTF-8?q?feat:=20CodeForge=20=E6=94=B9=E7=94=A8?= =?UTF-8?q?=E6=9C=89=E7=95=8C=20Mutex=EF=BC=8CKernelConfig=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20max=5Fcached=5Fmodules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 21 ++++- Cargo.toml | 1 + crates/oxide_code_cache/Cargo.toml | 2 +- crates/oxide_code_cache/src/lib.rs | 93 ++++++++++++++----- .../tests/structural_hash_tests.rs | 4 +- crates/oxide_kernel/src/kernel.rs | 25 ++++- 6 files changed, 120 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e56ce4b5..67ada84c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -460,6 +460,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "futures-core" version = "0.3.32" @@ -536,7 +542,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -546,6 +552,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ "allocator-api2", + "equivalent", + "foldhash 0.2.0", ] [[package]] @@ -694,6 +702,15 @@ version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" +[[package]] +name = "lru" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9" +dependencies = [ + "hashbrown 0.17.1", +] + [[package]] name = "matchers" version = "0.2.0" @@ -1040,7 +1057,7 @@ dependencies = [ name = "oxide_code_cache" version = "0.1.0" dependencies = [ - "dashmap", + "lru", "oxide_bytecode", ] diff --git a/Cargo.toml b/Cargo.toml index 0001a246..6e83ad8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ ryu = "1" memchr = "2" regex = "1" indexmap = "2" +lru = "0.18" smallvec = { version = "1", features = ["union"] } chrono = { version = "0.4", default-features = false, features = ["clock"] } unicode-normalization = "0.1" diff --git a/crates/oxide_code_cache/Cargo.toml b/crates/oxide_code_cache/Cargo.toml index 2e7cadf3..eeb4d051 100644 --- a/crates/oxide_code_cache/Cargo.toml +++ b/crates/oxide_code_cache/Cargo.toml @@ -8,4 +8,4 @@ description = "OxideJS - Compiler-independent compiled module cache" [dependencies] oxide_bytecode = { path = "../oxide_bytecode" } -dashmap.workspace = true +lru.workspace = true diff --git a/crates/oxide_code_cache/src/lib.rs b/crates/oxide_code_cache/src/lib.rs index 17e7b3a2..fd70cf7e 100644 --- a/crates/oxide_code_cache/src/lib.rs +++ b/crates/oxide_code_cache/src/lib.rs @@ -1,57 +1,65 @@ #![doc = "OxideJS - Compiler-independent compiled module cache"] -use std::sync::Arc; +use std::num::NonZeroUsize; +use std::sync::{Arc, Mutex}; -use dashmap::DashMap; +use lru::LruCache; use oxide_bytecode::CompiledModule; /// Shared compiled-module cache keyed by a caller-provided safe module hash. /// /// The cache intentionally does not know how to parse or compile JavaScript. /// Compiler-aware callers compute the key and provide the compile callback. +/// +/// Bounded by an LRU eviction policy: at most `capacity` modules are retained, +/// so an eval loop that compiles unbounded distinct sources cannot grow the +/// cache without limit. pub struct CodeForge { - map: DashMap>, + map: Mutex>>, } impl CodeForge { - pub fn new() -> Self { + pub fn new(capacity: NonZeroUsize) -> Self { Self { - map: DashMap::with_shard_amount(16), + map: Mutex::new(LruCache::new(capacity)), } } pub fn get(&self, hash: u64) -> Option> { - self.map.get(&hash).map(|entry| Arc::clone(&entry)) + self.map.lock().unwrap().get(&hash).map(Arc::clone) } pub fn insert(&self, hash: u64, module: CompiledModule) -> Arc { let module = Arc::new(module); - Arc::clone(self.map.entry(hash).or_insert(module).value()) + self.map.lock().unwrap().put(hash, Arc::clone(&module)); + module } pub fn get_or_insert_with(&self, hash: u64, compile: F) -> Result, String> where F: FnOnce() -> Result, { - if let Some(module) = self.get(hash) { - return Ok(module); + // Two-phase: look up under the lock, then release the lock BEFORE + // running the compile closure so a slow compilation never blocks other + // cache users. Re-acquire the lock only to insert the freshly built + // module. + { + let mut cache = self.map.lock().unwrap(); + if let Some(module) = cache.get(&hash) { + return Ok(Arc::clone(module)); + } } - - Ok(self.insert(hash, compile()?)) + let module = Arc::new(compile()?); + self.map.lock().unwrap().put(hash, Arc::clone(&module)); + Ok(module) } pub fn len(&self) -> usize { - self.map.len() + self.map.lock().unwrap().len() } pub fn is_empty(&self) -> bool { - self.map.is_empty() - } -} - -impl Default for CodeForge { - fn default() -> Self { - Self::new() + self.map.lock().unwrap().is_empty() } } @@ -59,9 +67,13 @@ impl Default for CodeForge { mod tests { use super::*; + fn forge(capacity: usize) -> CodeForge { + CodeForge::new(NonZeroUsize::new(capacity).unwrap()) + } + #[test] fn cache_returns_same_arc_for_same_hash() { - let forge = CodeForge::new(); + let forge = forge(16); let first = forge.insert(1, CompiledModule::new()); let second = forge.get_or_insert_with(1, || Ok(CompiledModule::new())).expect("cache hit"); @@ -70,10 +82,49 @@ mod tests { #[test] fn cache_misses_for_different_hashes() { - let forge = CodeForge::new(); + let forge = forge(16); let first = forge.insert(1, CompiledModule::new()); let second = forge.insert(2, CompiledModule::new()); assert!(!Arc::ptr_eq(&first, &second)); } + + #[test] + fn lru_eviction_enforces_cap() { + let forge = forge(2); + forge.insert(1, CompiledModule::new()); + forge.insert(2, CompiledModule::new()); + forge.insert(3, CompiledModule::new()); + + assert_eq!(forge.len(), 2); + assert!(forge.get(1).is_none()); + assert!(forge.get(2).is_some()); + assert!(forge.get(3).is_some()); + } + + #[test] + fn lru_eviction_evicts_least_recently_used() { + let forge = forge(3); + forge.insert(1, CompiledModule::new()); + forge.insert(2, CompiledModule::new()); + forge.insert(3, CompiledModule::new()); + + // Touch key 1 so key 2 becomes the least-recently-used entry. + assert!(forge.get(1).is_some()); + forge.insert(4, CompiledModule::new()); + + assert!(forge.get(1).is_some()); + assert!(forge.get(2).is_none()); + assert!(forge.get(3).is_some()); + assert!(forge.get(4).is_some()); + } + + #[test] + fn len_never_exceeds_cap_under_many_distinct_inserts() { + let forge = forge(10); + for hash in 0..100 { + forge.insert(hash, CompiledModule::new()); + } + assert_eq!(forge.len(), 10); + } } diff --git a/crates/oxide_compiler/tests/structural_hash_tests.rs b/crates/oxide_compiler/tests/structural_hash_tests.rs index ed059349..ec1602e0 100644 --- a/crates/oxide_compiler/tests/structural_hash_tests.rs +++ b/crates/oxide_compiler/tests/structural_hash_tests.rs @@ -1,3 +1,5 @@ +use std::num::NonZeroUsize; + use oxide_code_cache::CodeForge; use oxide_compiler::compiler::Compiler; use oxide_parser::Allocator; @@ -24,7 +26,7 @@ fn parse_to_compiled_hash(source: &str) -> u64 { #[test] fn structural_hash_hit() { - let forge = CodeForge::new(); + let forge = CodeForge::new(NonZeroUsize::new(512).unwrap()); let compiler = Compiler::new(); let allocator = Allocator::default(); let program = oxide_parser::parse(&allocator, "1 + 2").expect("parse failed"); diff --git a/crates/oxide_kernel/src/kernel.rs b/crates/oxide_kernel/src/kernel.rs index 60ed7784..feb30351 100644 --- a/crates/oxide_kernel/src/kernel.rs +++ b/crates/oxide_kernel/src/kernel.rs @@ -1,5 +1,6 @@ #![allow(clippy::arc_with_non_send_sync)] +use std::num::NonZeroUsize; use std::sync::Arc; use oxide_types::mem::P; @@ -21,6 +22,7 @@ pub struct KernelConfig { pub max_steps: Option, pub max_call_depth: usize, pub session_gc_threshold: usize, + pub max_cached_modules: usize, pub log_levels: [LogLevel; SUBSYSTEM_COUNT], pub warmup_builtin_shapes: bool, pub warmup_builtin_code: bool, @@ -36,6 +38,7 @@ impl KernelConfig { max_steps: None, max_call_depth: 1024, session_gc_threshold: 8_388_608, + max_cached_modules: 512, log_levels: [LogLevel::Off; SUBSYSTEM_COUNT], warmup_builtin_shapes: true, warmup_builtin_code: false, @@ -51,6 +54,7 @@ impl KernelConfig { max_steps: None, max_call_depth: 1024, session_gc_threshold: 8_388_608, + max_cached_modules: 512, log_levels: [LogLevel::Off; SUBSYSTEM_COUNT], warmup_builtin_shapes: true, warmup_builtin_code: true, @@ -66,6 +70,7 @@ impl KernelConfig { max_steps: None, max_call_depth: 1024, session_gc_threshold: 8_388_608, + max_cached_modules: 512, log_levels: [LogLevel::Off; SUBSYSTEM_COUNT], warmup_builtin_shapes: true, warmup_builtin_code: true, @@ -80,6 +85,14 @@ impl KernelConfig { pub fn set_session_gc_threshold(&mut self, bytes: usize) { self.session_gc_threshold = bytes; } + + pub fn max_cached_modules(&self) -> usize { + self.max_cached_modules + } + + pub fn set_max_cached_modules(&mut self, cap: usize) { + self.max_cached_modules = cap; + } } impl Default for KernelConfig { @@ -103,7 +116,9 @@ impl KernelCore { init_logging(&config.log_levels); let string_forge = Arc::new(StringForge::new()); let shape_forge = Arc::new(ShapeForge::new()); - let code_forge = Arc::new(CodeForge::new()); + let code_forge = Arc::new(CodeForge::new( + NonZeroUsize::new(config.max_cached_modules).expect("max_cached_modules must be greater than zero"), + )); let prop_forge = Arc::new(PropForge::new()); Arc::new(Self { config, @@ -142,6 +157,14 @@ impl KernelCore { self.config.session_gc_threshold = bytes; } + pub fn max_cached_modules(&self) -> usize { + self.config.max_cached_modules + } + + pub fn set_max_cached_modules(&mut self, cap: usize) { + self.config.max_cached_modules = cap; + } + pub fn sweep_runner_forges(&self) { self.string_forge.maybe_sweep(self.config.max_dead_strings.or(Some(1))); // test262 creates a fresh VM/session per test. At this boundary no JS object From 9a62cf1610dc2072c26b519b946401fc5887936b Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Wed, 24 Jun 2026 19:01:51 +0800 Subject: [PATCH 08/32] =?UTF-8?q?perf:=20for=5Feach=5Froot=20=E8=BF=AD?= =?UTF-8?q?=E4=BB=A3=E5=99=A8=E6=9B=BF=E4=BB=A3=20gc=5Froots=EF=BC=8C?= =?UTF-8?q?=E5=A4=8D=E7=94=A8=20Vec=20DFS=20mark=20=E6=A0=88=E6=B6=88?= =?UTF-8?q?=E9=99=A4=E6=AF=8F=E8=BD=AE=20GC=20=E5=A0=86=E5=88=86=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_vm/src/session_gc.rs | 70 +++++++++++++++---------------- crates/oxide_vm/src/vm.rs | 60 +++++++++++++++++--------- 2 files changed, 75 insertions(+), 55 deletions(-) diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index 7e945fbd..12c6be19 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -1,4 +1,4 @@ -use std::collections::{HashMap, VecDeque}; +use std::collections::HashMap; use std::mem::size_of; use std::time::Instant; @@ -23,6 +23,7 @@ pub struct SessionGc { pub last_collection_duration_us: u64, pub max_collection_duration_us: u64, pub min_collection_duration_us: u64, + pub(crate) mark_stack: Vec<*mut JsObject>, } impl SessionGc { @@ -32,11 +33,6 @@ impl SessionGc { } impl SessionGc { - #[inline] - pub(crate) fn is_session_ptr(&self, vm: &Vm, obj_ptr: *mut JsObject) -> bool { - vm.is_session_ptr(obj_ptr) - } - pub(crate) fn clear_all_marks(&mut self, vm: &mut Vm) { for &ptr in &vm.session_object_ptrs { if ptr.is_null() { @@ -48,7 +44,7 @@ impl SessionGc { } } - fn object_edges(&self, obj: &JsObject) -> Vec { + fn object_edges(obj: &JsObject) -> Vec { let mut edges = Vec::new(); if let Some(props) = obj.hash_props_vec() { edges.extend(props.iter().copied().filter(|val| val.is_object())); @@ -87,32 +83,36 @@ impl SessionGc { edges } - pub(crate) fn mark(&mut self, vm: &Vm, roots: &[JsValue]) { - let mut queue = VecDeque::<*mut JsObject>::new(); + pub(crate) fn mark(&mut self, vm: &Vm) { + let mut seeds = Vec::new(); + vm.for_each_root(|root| { + if root.is_object() { + seeds.push(root.as_js_object_ptr()); + } + }); - for root in roots { - if !root.is_object() { + let stack = &mut self.mark_stack; + stack.clear(); + for ptr in seeds { + if ptr.is_null() { continue; } - let ptr = root.as_js_object_ptr(); - if self.is_session_ptr(vm, ptr) { - queue.push_back(ptr); + if vm.is_session_ptr(ptr) { + stack.push(ptr); continue; } - if !ptr.is_null() { - // SAFETY: object roots are produced by VM-owned fields and builtin objects. - for edge in self.object_edges(unsafe { &*ptr }) { - if edge.is_object() { - let edge_ptr = edge.as_js_object_ptr(); - if self.is_session_ptr(vm, edge_ptr) { - queue.push_back(edge_ptr); - } + // SAFETY: object roots are produced by VM-owned fields and builtin objects. + for edge in Self::object_edges(unsafe { &*ptr }) { + if edge.is_object() { + let edge_ptr = edge.as_js_object_ptr(); + if vm.is_session_ptr(edge_ptr) { + stack.push(edge_ptr); } } } } - while let Some(ptr) = queue.pop_front() { + while let Some(ptr) = stack.pop() { if ptr.is_null() { continue; } @@ -124,14 +124,14 @@ impl SessionGc { continue; } obj.set_gc_mark(true); - let edges = self.object_edges(obj); + let edges = Self::object_edges(obj); for edge in edges { if !edge.is_object() { continue; } let child_ptr = edge.as_js_object_ptr(); - if self.is_session_ptr(vm, child_ptr) { - queue.push_back(child_ptr); + if vm.is_session_ptr(child_ptr) { + stack.push(child_ptr); } } } @@ -293,7 +293,7 @@ impl SessionGc { pub(crate) fn collect(&mut self, vm: &mut Vm) { let start = Instant::now(); - self.mark(vm, &vm.gc_roots()); + self.mark(vm); let freed_bytes = self.sweep(vm); let elapsed = start.elapsed(); @@ -341,6 +341,7 @@ impl Default for SessionGc { last_collection_duration_us: 0, max_collection_duration_us: 0, min_collection_duration_us: u64::MAX, + mark_stack: Vec::new(), } } } @@ -473,7 +474,8 @@ mod tests { vm.constants.push(JsValue::from_js_object(frame_session)); vm.sub_module_constants = vec![vec![JsValue::from_js_object(child_session)]]; - let roots = vm.gc_roots(); + let mut roots = Vec::new(); + vm.for_each_root(|v| roots.push(v)); assert!(has_ptr(&roots, root_session)); assert!(has_ptr(&roots, frame_session)); assert!(has_ptr(&roots, this_session)); @@ -500,9 +502,8 @@ mod tests { let unreachable_session = vm.promote_object(unreachable); vm.regs[0] = JsValue::from_js_object(root_session); - let roots = vm.gc_roots(); let mut gc = std::mem::take(&mut vm.session_gc); - gc.mark(&vm, &roots); + gc.mark(&vm); vm.session_gc = gc; assert!(unsafe { (*root_session).is_gc_marked() }); @@ -525,9 +526,8 @@ mod tests { let root_session = vm.promote_object(root); vm.regs[0] = JsValue::from_js_object(root_session); let dead_session = vm.promote_object(dead); - let roots = vm.gc_roots(); let mut gc = std::mem::take(&mut vm.session_gc); - gc.mark(&vm, &roots); + gc.mark(&vm); let _ = gc.sweep(&mut vm); vm.session_gc = gc; @@ -548,9 +548,8 @@ mod tests { let root_session = vm.promote_object(root); vm.regs[0] = JsValue::from_js_object(root_session); let _ = vm.promote_object(child); - let roots = vm.gc_roots(); let mut gc = std::mem::take(&mut vm.session_gc); - gc.mark(&vm, &roots); + gc.mark(&vm); let _ = gc.sweep(&mut vm); vm.session_gc = gc; @@ -640,8 +639,7 @@ mod tests { assert!(map_obj.hash_props_vec().is_none()); assert!(!map_obj.native_data().is_null()); - assert!(!SessionGc::new() - .object_edges(map_obj) + assert!(!SessionGc::object_edges(map_obj) .iter() .any(|edge| std::ptr::eq(edge.as_js_object_ptr(), native_ptr))); } diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 49130033..aa79907b 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -295,29 +295,51 @@ impl Vm { ptr } - pub(crate) fn gc_roots(&self) -> Vec { - let mut roots = Vec::new(); - roots.extend_from_slice(&self.regs); - + pub(crate) fn for_each_root(&self, mut f: impl FnMut(JsValue)) { + for value in &self.regs { + if value.is_object() || value.is_string() { + f(*value); + } + } for frame in &self.frames { - roots.extend(frame.saved_regs.iter().copied()); - roots.push(frame.saved_this); - roots.push(frame.saved_new_target); - roots.push(frame.callee); - roots.push(frame.constructed_this.unwrap_or(JsValue::undefined())); + for &v in &frame.saved_regs { + f(v); + } + f(frame.saved_this); + f(frame.saved_new_target); + f(frame.callee); + f(frame.constructed_this.unwrap_or(JsValue::undefined())); + } + f(JsValue::from_js_object(self.session.global_object().as_ptr() as *mut JsObject)); + f(self.exception_value.unwrap_or(JsValue::undefined())); + f(self.pending_exception.unwrap_or(JsValue::undefined())); + for &v in &self.for_of_iters { + f(v); + } + f(self.last_for_of_result); + for &v in &self.constants { + f(v); } - - roots.push(JsValue::from_js_object(self.session.global_object().as_ptr() as *mut JsObject)); - roots.push(self.exception_value.unwrap_or(JsValue::undefined())); - roots.push(self.pending_exception.unwrap_or(JsValue::undefined())); - roots.extend(self.for_of_iters.iter().copied()); - roots.push(self.last_for_of_result); - roots.extend(self.constants.iter().copied()); for sub_module in &self.sub_module_constants { - roots.extend(sub_module.iter().copied()); + for &v in sub_module { + f(v); + } + } + for saved_constants in &self.saved_constants_stack { + for &v in saved_constants { + f(v); + } + } + for iter in &self.for_in_iters { + if iter.is_null() { + continue; + } + unsafe { + for &v in (*(*iter)).keys.iter() { + f(v); + } + } } - - roots } pub(crate) fn maybe_collect_session_gc(&mut self) { From ddffa8504eca29695a8bef99b727979fc70d3c3c Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Wed, 24 Jun 2026 20:15:15 +0800 Subject: [PATCH 09/32] =?UTF-8?q?refactor:=20=E5=AD=97=E7=AC=A6=E4=B8=B2?= =?UTF-8?q?=E5=80=BC=E6=94=B9=E7=94=A8=20JsString=20=E6=8C=87=E9=92=88?= =?UTF-8?q?=E8=A1=A8=E7=A4=BA=EF=BC=8C=E9=94=AE=E4=B8=8E=E5=AD=97=E9=9D=A2?= =?UTF-8?q?=E9=87=8F=E6=94=B9=E7=94=A8=20PermInterner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_cli/src/main.rs | 20 +- crates/oxide_kernel/src/builtin.rs | 42 +-- crates/oxide_kernel/src/kernel.rs | 37 +-- crates/oxide_kernel/src/string_forge.rs | 287 +++++++++--------- crates/oxide_types/src/object.rs | 37 +++ crates/oxide_types/src/value.rs | 66 ++-- crates/oxide_vm/src/bindings/bind_array.rs | 2 +- crates/oxide_vm/src/bindings/bind_error.rs | 8 +- crates/oxide_vm/src/bindings/bind_function.rs | 2 +- crates/oxide_vm/src/bindings/bind_math.rs | 6 +- crates/oxide_vm/src/bindings/bind_number.rs | 2 +- crates/oxide_vm/src/bindings/bind_object.rs | 2 +- crates/oxide_vm/src/bindings/bind_string.rs | 4 +- crates/oxide_vm/src/bindings/bind_symbol.rs | 2 +- crates/oxide_vm/src/bindings/mod.rs | 12 +- crates/oxide_vm/src/builtins/array.rs | 57 ++-- crates/oxide_vm/src/builtins/array_buffer.rs | 10 +- crates/oxide_vm/src/builtins/boolean.rs | 16 +- crates/oxide_vm/src/builtins/data_view.rs | 14 +- crates/oxide_vm/src/builtins/date.rs | 78 ++--- crates/oxide_vm/src/builtins/error.rs | 26 +- crates/oxide_vm/src/builtins/function.rs | 13 +- crates/oxide_vm/src/builtins/global.rs | 2 +- .../oxide_vm/src/builtins/global/annex_b.rs | 4 +- crates/oxide_vm/src/builtins/global/uri.rs | 8 +- crates/oxide_vm/src/builtins/iterator.rs | 26 +- crates/oxide_vm/src/builtins/json.rs | 18 +- crates/oxide_vm/src/builtins/math.rs | 2 +- crates/oxide_vm/src/builtins/number.rs | 64 ++-- crates/oxide_vm/src/builtins/object.rs | 48 ++- crates/oxide_vm/src/builtins/reflect.rs | 28 +- crates/oxide_vm/src/builtins/regexp.rs | 39 +-- crates/oxide_vm/src/builtins/string.rs | 109 +++---- crates/oxide_vm/src/builtins/symbol.rs | 22 +- crates/oxide_vm/src/builtins/typed_array.rs | 14 +- crates/oxide_vm/src/coercion.rs | 75 +++-- crates/oxide_vm/src/dispatch/arithmetic.rs | 10 +- crates/oxide_vm/src/dispatch/call.rs | 6 +- crates/oxide_vm/src/dispatch/comparison.rs | 12 +- crates/oxide_vm/src/dispatch/control_flow.rs | 6 +- crates/oxide_vm/src/dispatch/member.rs | 10 +- crates/oxide_vm/src/dispatch/misc.rs | 2 +- crates/oxide_vm/src/dispatch/property.rs | 17 +- crates/oxide_vm/src/session_gc.rs | 2 +- crates/oxide_vm/src/vm.rs | 75 ++--- crates/oxide_vm/src/vm_dispatch_misc.rs | 46 ++- crates/oxide_vm/src/vm_pool.rs | 8 - crates/oxide_vm/src/vm_props.rs | 2 +- crates/oxide_vm/src/vm_runtime.rs | 4 +- crates/oxide_vm/src/vm_support.rs | 67 ++-- crates/oxide_vm/tests/arrow_tests.rs | 5 +- .../tests/builtins_array_buffer_tests.rs | 5 +- crates/oxide_vm/tests/builtins_array_tests.rs | 5 +- .../oxide_vm/tests/builtins_boolean_tests.rs | 7 +- .../tests/builtins_data_view_tests.rs | 5 +- crates/oxide_vm/tests/builtins_date_tests.rs | 5 +- crates/oxide_vm/tests/builtins_error_tests.rs | 2 +- .../oxide_vm/tests/builtins_function_tests.rs | 12 +- .../oxide_vm/tests/builtins_globals_tests.rs | 5 +- .../oxide_vm/tests/builtins_iterator_tests.rs | 5 +- crates/oxide_vm/tests/builtins_map_tests.rs | 5 +- .../oxide_vm/tests/builtins_number_tests.rs | 20 +- .../oxide_vm/tests/builtins_object_tests.rs | 6 +- .../oxide_vm/tests/builtins_reflect_tests.rs | 5 +- .../oxide_vm/tests/builtins_regexp_tests.rs | 5 +- .../oxide_vm/tests/builtins_string_tests.rs | 5 +- .../oxide_vm/tests/builtins_symbol_tests.rs | 5 +- .../tests/builtins_typed_array_tests.rs | 5 +- crates/oxide_vm/tests/coercion_tests.rs | 29 +- crates/oxide_vm/tests/function_tests.rs | 6 +- crates/oxide_vm/tests/json_tests.rs | 5 +- crates/oxide_vm/tests/session_arena_tests.rs | 2 +- crates/oxide_vm/tests/template_tests.rs | 5 +- 73 files changed, 768 insertions(+), 860 deletions(-) diff --git a/crates/oxide_cli/src/main.rs b/crates/oxide_cli/src/main.rs index 384ed882..af1a2dc6 100644 --- a/crates/oxide_cli/src/main.rs +++ b/crates/oxide_cli/src/main.rs @@ -10,7 +10,7 @@ use clap::{Parser, Subcommand}; use oxide_compiler::compiler::{compiled_module_hash, Compiler}; use oxide_kernel::kernel::{KernelConfig, KernelCore}; use oxide_kernel::shape_forge::{ShapeForge, EMPTY_SHAPE_ID}; -use oxide_kernel::string_forge::StringForge; +use oxide_kernel::string_forge::PermInterner; use oxide_parser::Allocator; use oxide_types::object::JsObject; use oxide_vm::vm_pool::VmPool; @@ -140,7 +140,7 @@ fn eval(code: &str, kernel: &Arc, pool: &Arc) -> ExitCode { let mut guard = pool.spawn(); match guard.vm_mut().run(&module) { Ok(result) => { - format_result(kernel.string_forge().as_ref(), kernel.shape_forge().as_ref(), result); + format_result(kernel.perm_interner().as_ref(), kernel.shape_forge().as_ref(), result); ExitCode::SUCCESS } Err(err) => { @@ -150,17 +150,15 @@ fn eval(code: &str, kernel: &Arc, pool: &Arc) -> ExitCode { } } -fn format_result(string_forge: &StringForge, shape_forge: &ShapeForge, val: JsValue) { +fn format_result(string_forge: &PermInterner, shape_forge: &ShapeForge, val: JsValue) { println!("{}", format_js_value(string_forge, shape_forge, val)); } -fn format_js_value(string_forge: &StringForge, shape_forge: &ShapeForge, val: JsValue) -> String { +fn format_js_value(string_forge: &PermInterner, shape_forge: &ShapeForge, val: JsValue) -> String { if val.is_string() { - if let Some(s) = string_forge.lookup(val.as_string_index()) { - format!("\"{s}\"") - } else { - format!("{val}") - } + // SAFETY: val is a string value. + let s = unsafe { (*val.as_string_ptr()).data.clone() }; + format!("\"{s}\"") } else if val.is_object() { let obj = unsafe { &*val.as_js_object_ptr() }; if obj.is_function() { @@ -177,7 +175,7 @@ fn format_js_value(string_forge: &StringForge, shape_forge: &ShapeForge, val: Js } } -fn format_object(string_forge: &StringForge, shape_forge: &ShapeForge, obj: &JsObject) -> String { +fn format_object(string_forge: &PermInterner, shape_forge: &ShapeForge, obj: &JsObject) -> String { let mut entries = Vec::new(); let shape_id = obj.shape_id(); let mut shape_ids = Vec::new(); @@ -214,7 +212,7 @@ fn format_object(string_forge: &StringForge, shape_forge: &ShapeForge, obj: &JsO format!("{{{}}}", entries.join(", ")) } -fn format_array(string_forge: &StringForge, shape_forge: &ShapeForge, obj: &JsObject) -> String { +fn format_array(string_forge: &PermInterner, shape_forge: &ShapeForge, obj: &JsObject) -> String { let len = obj.prop_vec_len(); let mut items = Vec::new(); for i in 0..len { diff --git a/crates/oxide_kernel/src/builtin.rs b/crates/oxide_kernel/src/builtin.rs index 6233c014..712f5b3d 100644 --- a/crates/oxide_kernel/src/builtin.rs +++ b/crates/oxide_kernel/src/builtin.rs @@ -4,7 +4,7 @@ use oxide_types::value::JsValue; use crate::kernel::BuiltinDirtySet; use crate::shape_forge::{ShapeForge, EMPTY_SHAPE_ID}; -use crate::string_forge::StringForge; +use crate::string_forge::PermInterner; #[macro_export] macro_rules! bind_method { @@ -219,12 +219,12 @@ pub struct BuiltinWorld { pub stub_objects: Vec>, } -fn intern_label(string_forge: &StringForge, label: &str) -> u32 { +fn intern_label(string_forge: &PermInterner, label: &str) -> u32 { string_forge.intern(label).0 } fn make_pair( - string_forge: &StringForge, shape_forge: &ShapeForge, name: &str, si_prototype: u32, si_constructor: u32, + string_forge: &PermInterner, shape_forge: &ShapeForge, name: &str, si_prototype: u32, si_constructor: u32, si_name: u32, ) -> (P, P) { intern_label(string_forge, name); @@ -258,7 +258,7 @@ struct BuiltinLabels { name: u32, } -fn builtin_labels(string_forge: &StringForge) -> BuiltinLabels { +fn builtin_labels(string_forge: &PermInterner) -> BuiltinLabels { let labels = BuiltinLabels { prototype: intern_label(string_forge, "prototype"), constructor: intern_label(string_forge, "constructor"), @@ -271,7 +271,7 @@ fn builtin_labels(string_forge: &StringForge) -> BuiltinLabels { } fn make_named_pair( - string_forge: &StringForge, shape_forge: &ShapeForge, labels: BuiltinLabels, name: &str, + string_forge: &PermInterner, shape_forge: &ShapeForge, labels: BuiltinLabels, name: &str, ) -> (P, P) { make_pair(string_forge, shape_forge, name, labels.prototype, labels.constructor, labels.name) } @@ -324,7 +324,7 @@ struct TypedArrayFamily { } fn make_typed_array_family( - string_forge: &StringForge, shape_forge: &ShapeForge, labels: BuiltinLabels, object_proto: &P, + string_forge: &PermInterner, shape_forge: &ShapeForge, labels: BuiltinLabels, object_proto: &P, ) -> TypedArrayFamily { let obj_proto_val = JsValue::from_js_object(object_proto.as_ptr() as *mut JsObject); let typed_array_proto = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, obj_proto_val)); @@ -472,7 +472,7 @@ impl BuiltinWorld { JsValue::from_js_object(self.function_proto.as_ptr() as *mut JsObject) } - pub fn new(string_forge: &StringForge, shape_forge: &ShapeForge) -> Self { + pub fn new(string_forge: &PermInterner, shape_forge: &ShapeForge) -> Self { let labels = builtin_labels(string_forge); let (object_proto, object_constructor) = make_named_pair(string_forge, shape_forge, labels, "Object"); @@ -578,7 +578,7 @@ impl BuiltinWorld { } pub fn rebuild_with_dirty( - current: &BuiltinWorld, string_forge: &StringForge, shape_forge: &ShapeForge, dirty: &BuiltinDirtySet, + current: &BuiltinWorld, string_forge: &PermInterner, shape_forge: &ShapeForge, dirty: &BuiltinDirtySet, ) -> BuiltinWorld { let labels = builtin_labels(string_forge); @@ -796,7 +796,7 @@ impl BuiltinWorld { world } - pub fn bind_object_methods(&self, methods: &ObjectMethods, string_forge: &StringForge, shape_forge: &ShapeForge) { + pub fn bind_object_methods(&self, methods: &ObjectMethods, string_forge: &PermInterner, shape_forge: &ShapeForge) { let ctor_ptr = P::as_ptr(&self.object_constructor) as *mut JsObject; let ctor = unsafe { &mut *ctor_ptr }; bind_methods!( @@ -843,7 +843,7 @@ impl BuiltinWorld { } } - pub fn bind_array_methods(&self, methods: &ArrayMethods, string_forge: &StringForge, shape_forge: &ShapeForge) { + pub fn bind_array_methods(&self, methods: &ArrayMethods, string_forge: &PermInterner, shape_forge: &ShapeForge) { let ctor_ptr = P::as_ptr(&self.array_constructor) as *mut JsObject; let ctor = unsafe { &mut *ctor_ptr }; bind_methods!(self, ctor, string_forge, shape_forge, ("isArray", methods.is_array, 1),); @@ -895,7 +895,7 @@ impl BuiltinWorld { debug_assert!(shape_forge.lookup_position(proto.shape_id(), si).is_some()); } - pub fn bind_error_methods(&self, methods: &ErrorMethods, string_forge: &StringForge, shape_forge: &ShapeForge) { + pub fn bind_error_methods(&self, methods: &ErrorMethods, string_forge: &PermInterner, shape_forge: &ShapeForge) { let ctor_ptr = P::as_ptr(&self.error_constructor) as *mut JsObject; let ctor = unsafe { &mut *ctor_ptr }; bind_methods!( @@ -924,7 +924,7 @@ impl BuiltinWorld { ); } - pub fn bind_string_methods(&self, methods: &StringMethods, string_forge: &StringForge, shape_forge: &ShapeForge) { + pub fn bind_string_methods(&self, methods: &StringMethods, string_forge: &PermInterner, shape_forge: &ShapeForge) { let ctor_ptr = P::as_ptr(&self.string_constructor) as *mut JsObject; let ctor = unsafe { &mut *ctor_ptr }; bind_methods!(self, ctor, string_forge, shape_forge, ("fromCharCode", methods.from_char_code, 1),); @@ -966,7 +966,7 @@ impl BuiltinWorld { } pub fn bind_function_methods( - &self, methods: &FunctionMethods, string_forge: &StringForge, shape_forge: &ShapeForge, + &self, methods: &FunctionMethods, string_forge: &PermInterner, shape_forge: &ShapeForge, ) { let proto_ptr = P::as_ptr(&self.function_proto) as *mut JsObject; let proto = unsafe { &mut *proto_ptr }; @@ -984,7 +984,7 @@ impl BuiltinWorld { } pub fn bind_method( - &self, proto: &mut JsObject, shape_forge: &ShapeForge, string_forge: &StringForge, method_name: &str, + &self, proto: &mut JsObject, shape_forge: &ShapeForge, string_forge: &PermInterner, method_name: &str, native_fn_ptr: NativeFnPtr, arg_count: u8, ) -> Result<(), String> { Self::bind_method_static( @@ -999,7 +999,7 @@ impl BuiltinWorld { } pub fn bind_method_static( - proto: &mut JsObject, shape_forge: &ShapeForge, string_forge: &StringForge, method_name: &str, + proto: &mut JsObject, shape_forge: &ShapeForge, string_forge: &PermInterner, method_name: &str, native_fn_ptr: NativeFnPtr, arg_count: u8, wrapper_proto: JsValue, ) -> Result<(), String> { let si = string_forge.intern(method_name).0; @@ -1022,7 +1022,7 @@ impl BuiltinWorld { wrapper.set_shape_id(name_shape); wrapper .ensure_hash_props() - .push(JsValue::string(string_forge.intern(method_name).0, 0)); + .push(JsValue::perm_string(string_forge.string_ptr(si))); let wrapper_val = JsValue::from_js_object(Box::into_raw(wrapper)); let new_shape = shape_forge.make_shape(proto.shape_id(), si); proto.set_shape_id(new_shape); @@ -1036,10 +1036,10 @@ impl BuiltinWorld { mod tests { use super::*; use crate::shape_forge::{ShapeForge, EMPTY_SHAPE_ID}; - use crate::string_forge::StringForge; + use crate::string_forge::PermInterner; fn make_world() -> BuiltinWorld { - let sf = StringForge::new(); + let sf = PermInterner::new(); let sh = ShapeForge::new(); BuiltinWorld::new(&sf, &sh) } @@ -1115,7 +1115,7 @@ mod tests { #[test] fn builtin_rebuild_with_dirty_reuses_clean_fields() { - let sf = StringForge::new(); + let sf = PermInterner::new(); let sh = ShapeForge::new(); let w = BuiltinWorld::new(&sf, &sh); let rebuilt = BuiltinWorld::rebuild_with_dirty(&w, &sf, &sh, &crate::kernel::BuiltinDirtySet::default()); @@ -1127,7 +1127,7 @@ mod tests { #[test] fn builtin_rebuild_with_dirty_replaces_only_dirty_group() { - let sf = StringForge::new(); + let sf = PermInterner::new(); let sh = ShapeForge::new(); let w = BuiltinWorld::new(&sf, &sh); let dirty = crate::kernel::BuiltinDirtySet { @@ -1144,7 +1144,7 @@ mod tests { #[test] fn builtin_rebuild_with_dirty_repairs_ctor_proto_links() { - let sf = StringForge::new(); + let sf = PermInterner::new(); let sh = ShapeForge::new(); let w = BuiltinWorld::new(&sf, &sh); let dirty = crate::kernel::BuiltinDirtySet { diff --git a/crates/oxide_kernel/src/kernel.rs b/crates/oxide_kernel/src/kernel.rs index feb30351..c324361b 100644 --- a/crates/oxide_kernel/src/kernel.rs +++ b/crates/oxide_kernel/src/kernel.rs @@ -12,7 +12,7 @@ use crate::code_forge::CodeForge; use crate::logging::{init_logging, LogLevel, SUBSYSTEM_COUNT}; use crate::prop_forge::PropForge; use crate::shape_forge::{ShapeForge, EMPTY_SHAPE_ID}; -use crate::string_forge::StringForge; +use crate::string_forge::PermInterner; #[derive(Clone)] pub struct KernelConfig { @@ -105,7 +105,7 @@ impl Default for KernelConfig { /// Never rebuilt after construction — forge tables are append-only. pub struct KernelCore { pub config: KernelConfig, - pub string_forge: Arc, + pub perm_interner: Arc, pub shape_forge: Arc, pub code_forge: Arc, pub prop_forge: Arc, @@ -114,7 +114,7 @@ pub struct KernelCore { impl KernelCore { pub fn new(config: KernelConfig) -> Arc { init_logging(&config.log_levels); - let string_forge = Arc::new(StringForge::new()); + let perm_interner = Arc::new(PermInterner::new()); let shape_forge = Arc::new(ShapeForge::new()); let code_forge = Arc::new(CodeForge::new( NonZeroUsize::new(config.max_cached_modules).expect("max_cached_modules must be greater than zero"), @@ -122,15 +122,15 @@ impl KernelCore { let prop_forge = Arc::new(PropForge::new()); Arc::new(Self { config, - string_forge, + perm_interner, shape_forge, code_forge, prop_forge, }) } - pub fn string_forge(&self) -> &Arc { - &self.string_forge + pub fn perm_interner(&self) -> &Arc { + &self.perm_interner } pub fn shape_forge(&self) -> &Arc { @@ -166,7 +166,8 @@ impl KernelCore { } pub fn sweep_runner_forges(&self) { - self.string_forge.maybe_sweep(self.config.max_dead_strings.or(Some(1))); + // The key interner is append-only (no per-run sweep); only the transient + // shape/prop tables need bounding at the test262 per-test boundary. // test262 creates a fresh VM/session per test. At this boundary no JS object // from prior tests may retain transient shapes/templates. if self.shape_forge.len() > 50_000 { @@ -394,9 +395,9 @@ impl KernelSession { fn new_global_object(core: &KernelCore) -> P { let mut global_obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); - let si_nan = core.string_forge.intern("NaN").0; - let si_undef = core.string_forge.intern("undefined").0; - let si_infinity = core.string_forge.intern("Infinity").0; + let si_nan = core.perm_interner.intern("NaN").0; + let si_undef = core.perm_interner.intern("undefined").0; + let si_infinity = core.perm_interner.intern("Infinity").0; let nan_shape = core.shape_forge.make_shape(EMPTY_SHAPE_ID, si_nan); global_obj.set_shape_id(nan_shape); @@ -416,7 +417,7 @@ impl KernelSession { /// Build a fresh session from a KernelCore. All string/shape intern calls hit /// cache on second and subsequent calls — net cost is < 0.5 ms. pub fn new(core: &KernelCore) -> Self { - let builtin_world = Arc::new(BuiltinWorld::new(&core.string_forge, &core.shape_forge)); + let builtin_world = Arc::new(BuiltinWorld::new(&core.perm_interner, &core.shape_forge)); let global_object = Self::new_global_object(core); let builtin_snapshot = BuiltinSnapshot::new(&builtin_world, &global_object); @@ -536,7 +537,7 @@ impl KernelSession { if dirty.any_builtin_dirty() { self.builtin_world = Arc::new(BuiltinWorld::rebuild_with_dirty( &self.builtin_world, - core.string_forge.as_ref(), + core.perm_interner.as_ref(), core.shape_forge.as_ref(), &dirty, )); @@ -553,8 +554,8 @@ mod tests { #[test] fn test_kernel_new() { let core = KernelCore::new(KernelConfig::minimal()); - let (i1, _) = core.string_forge().intern("test"); - let (i2, _) = core.string_forge().intern("test"); + let (i1, _) = core.perm_interner().intern("test"); + let (i2, _) = core.perm_interner().intern("test"); assert_eq!(i1, i2); } @@ -575,8 +576,8 @@ mod tests { #[test] fn test_kernel_string_forge() { let core = KernelCore::new(KernelConfig::minimal()); - let (i1, _) = core.string_forge().intern("hello"); - let (i2, _) = core.string_forge().intern("hello"); + let (i1, _) = core.perm_interner().intern("hello"); + let (i2, _) = core.perm_interner().intern("hello"); assert_eq!(i1, i2); } @@ -596,9 +597,9 @@ mod tests { #[test] fn test_session_rebuild_shares_forges() { let core = KernelCore::new(KernelConfig::minimal()); - let (i1, _) = core.string_forge().intern("hello"); + let (i1, _) = core.perm_interner().intern("hello"); let _s2 = KernelSession::new(&core); - let (i2, _) = core.string_forge().intern("hello"); + let (i2, _) = core.perm_interner().intern("hello"); assert_eq!(i1, i2); } diff --git a/crates/oxide_kernel/src/string_forge.rs b/crates/oxide_kernel/src/string_forge.rs index 8ab5bef9..dd45c4a0 100644 --- a/crates/oxide_kernel/src/string_forge.rs +++ b/crates/oxide_kernel/src/string_forge.rs @@ -1,142 +1,137 @@ use std::hash::{Hash, Hasher}; -use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::RwLock; -use hashbrown::hash_map::DefaultHashBuilder as FxBuildHasher; -use hashbrown::HashMap; +use dashmap::DashMap; +use oxide_types::object::JsString; +use rustc_hash::FxHasher; use crate::{kernel_debug, kernel_trace}; -pub fn hash16(s: &str) -> u16 { - let mut h = rustc_hash::FxHasher::default(); +/// Full 64-bit content hash. Replaces the old 16-bit `hash16` so the interner's +/// hash→candidate map has negligible collision risk. +fn hash64(s: &str) -> u64 { + let mut h = FxHasher::default(); s.hash(&mut h); - (h.finish() >> 48) as u16 + h.finish() } -pub struct StringEntry { - pub(crate) data: String, - pub(crate) ref_count: AtomicU32, - pub hash: u16, +/// One interned key. `data` is a leaked `&'static str` — permanent keys are never +/// freed (append-only by design), so the leak is the storage model, not a bug. +#[derive(Clone, Copy)] +struct PermEntry { + data: &'static str, + hash: u64, } -struct StringForgeInner { - map: HashMap, - entries: Vec, -} - -/// Global string interning pool shared by all VMs. +/// Append-only, never-move, lock-free-read key interner shared by all VMs. /// -/// Concurrency model: -/// `StringForge` keeps a single `RwLock` around the interner state. -/// - `lookup()` takes a shared read lock and scales across readers. -/// - `intern()` uses a read-check then write-check pattern to keep hot-path hits cheap. -/// - New string insertion is serialized, which matches the workload here: most strings -/// are interned early and later accesses are dominated by reads. -pub struct StringForge { - inner: RwLock, +/// Replaces the old ref-counted `StringForge` (and its buggy `maybe_sweep` +/// renumber path). Keys (property names, method names) are interned once and +/// addressed by a stable `u32` id that the shape/IC system keys on. Runtime +/// string *values* are no longer interned here — they are heap `JsString` +/// pointers (see `oxide_vm::Vm::new_string`). +/// +/// Concurrency: +/// - `hash_map` (`DashMap`) gives sharded, lock-free reads of the hash→candidates +/// mapping on the hot intern path. +/// - `entries` sits behind a short `RwLock`; reads copy out a `&'static str` +/// (Copy) so the borrow outlives the guard. +pub struct PermInterner { + hash_map: DashMap>, + entries: RwLock>, + /// Lazily materialized permanent `JsString` values, indexed by entry id. + /// Used when a permanent key must also exist as a JS string *value* + /// (e.g. method names exposed as property values at builtin init). + permanent_strings: RwLock>>>, } -impl StringForge { +impl PermInterner { pub fn new() -> Self { Self { - inner: RwLock::new(StringForgeInner { - map: HashMap::with_hasher(FxBuildHasher::default()), - entries: Vec::new(), - }), + hash_map: DashMap::new(), + entries: RwLock::new(Vec::new()), + permanent_strings: RwLock::new(Vec::new()), } } - pub fn intern(&self, s: &str) -> (u32, u16) { - { - let inner = self.inner.read().unwrap(); - if let Some(&idx) = inner.map.get(s) { - let entry = &inner.entries[idx as usize]; - entry.ref_count.fetch_add(1, Ordering::Release); - kernel_trace!("StringForge intern hit idx={}", idx); - return (idx, entry.hash); + /// Intern a key. Returns its stable id and full 64-bit hash. Each unique + /// string is stored exactly once (single allocation, no double `to_string`). + pub fn intern(&self, s: &str) -> (u32, u64) { + let hash = hash64(s); + + // Fast path: lock-free candidate read, short entries read-lock. + if let Some(candidates) = self.hash_map.get(&hash) { + let entries = self.entries.read().unwrap(); + for &id in candidates.iter() { + if entries[id as usize].data == s { + kernel_trace!("PermInterner intern hit id={}", id); + return (id, hash); + } } } - let mut inner = self.inner.write().unwrap(); - if let Some(&idx) = inner.map.get(s) { - let entry = &inner.entries[idx as usize]; - entry.ref_count.fetch_add(1, Ordering::Release); - kernel_trace!("StringForge intern hit idx={}", idx); - return (idx, entry.hash); - } - - let h = hash16(s); - let idx = inner.entries.len() as u32; - inner.map.insert(s.to_string(), idx); - inner.entries.push(StringEntry { - data: s.to_string(), - ref_count: AtomicU32::new(1), - hash: h, - }); - kernel_debug!("StringForge intern new idx={} len={}", idx, s.len()); - (idx, h) - } - - pub fn lookup(&self, idx: u32) -> Option { - let inner = self.inner.read().unwrap(); - inner.entries.get(idx as usize).map(|e| e.data.clone()) - } - - pub fn get_hash(&self, idx: u32) -> Option { - let inner = self.inner.read().unwrap(); - inner.entries.get(idx as usize).map(|e| e.hash) - } - - pub fn decref(&self, idx: u32) { - let inner = self.inner.read().unwrap(); - if let Some(entry) = inner.entries.get(idx as usize) { - entry.ref_count.fetch_sub(1, Ordering::Release); - } - } - - pub fn maybe_sweep(&self, max_dead: Option) { - let threshold = match max_dead { - Some(t) => t, - None => return, - }; - - let mut inner = self.inner.write().unwrap(); - let dead_count = inner - .entries - .iter() - .filter(|e| e.ref_count.load(Ordering::Acquire) == 0) - .count(); - - if dead_count < threshold { - return; - } - - let mut new_map: HashMap = HashMap::with_hasher(FxBuildHasher::default()); - let mut new_entries = Vec::with_capacity(inner.entries.len() - dead_count); - - for entry in inner.entries.drain(..) { - let rc = entry.ref_count.load(Ordering::Acquire); - if rc > 0 { - let new_idx = new_entries.len() as u32; - new_map.insert(entry.data.clone(), new_idx); - new_entries.push(entry); + // Slow path: append under write lock, re-checking for a racing insert. + let mut entries = self.entries.write().unwrap(); + if let Some(candidates) = self.hash_map.get(&hash) { + for &id in candidates.iter() { + if entries[id as usize].data == s { + return (id, hash); + } } } - - inner.map = new_map; - inner.entries = new_entries; + let id = entries.len() as u32; + let leaked: &'static str = Box::leak(s.to_string().into_boxed_str()); + entries.push(PermEntry { data: leaked, hash }); + drop(entries); + self.hash_map.entry(hash).or_default().push(id); + kernel_debug!("PermInterner intern new id={} len={}", id, s.len()); + (id, hash) } - pub fn len(&self) -> usize { - self.inner.read().unwrap().entries.len() + /// Resolve a key id to its text with zero clone. The returned `&'static str` + /// is valid for the program lifetime (keys are never freed). + pub fn lookup(&self, id: u32) -> Option<&'static str> { + let entries = self.entries.read().unwrap(); + entries.get(id as usize).map(|e| e.data) + } + + /// Full 64-bit hash for a key id. + pub fn get_hash(&self, id: u32) -> Option { + let entries = self.entries.read().unwrap(); + entries.get(id as usize).map(|e| e.hash) + } + + /// Total number of unique keys interned. + pub fn entry_count(&self) -> u32 { + self.entries.read().unwrap().len() as u32 } pub fn is_empty(&self) -> bool { - self.len() == 0 + self.entry_count() == 0 + } + + /// Materialize (once) and return a stable pointer to a permanent `JsString` + /// for the given key id. The `JsString` lives for the program lifetime. + pub fn string_ptr(&self, id: u32) -> *const JsString { + { + let perm = self.permanent_strings.read().unwrap(); + if let Some(Some(boxed)) = perm.get(id as usize) { + return boxed.as_ref() as *const JsString; + } + } + let text = self.lookup(id).unwrap_or(""); + let mut perm = self.permanent_strings.write().unwrap(); + if perm.len() <= id as usize { + perm.resize_with(id as usize + 1, || None); + } + if perm[id as usize].is_none() { + perm[id as usize] = Some(Box::new(JsString::new(text.to_string()))); + } + perm[id as usize].as_ref().unwrap().as_ref() as *const JsString } } -impl Default for StringForge { +impl Default for PermInterner { fn default() -> Self { Self::new() } @@ -147,67 +142,63 @@ mod tests { use super::*; #[test] - fn test_hash16_same() { - assert_eq!(hash16("hello"), hash16("hello")); - } - - #[test] - fn test_hash16_different() { - assert_ne!(hash16("hello"), hash16("world")); - } - - #[test] - fn test_intern_dedup() { - let string_forge = StringForge::new(); - let (i1, h1) = string_forge.intern("abc"); - let (i2, h2) = string_forge.intern("abc"); + fn intern_dedup() { + let interner = PermInterner::new(); + let (i1, h1) = interner.intern("abc"); + let (i2, h2) = interner.intern("abc"); assert_eq!(i1, i2); assert_eq!(h1, h2); } #[test] - fn test_intern_different() { - let string_forge = StringForge::new(); - let (i1, _) = string_forge.intern("x"); - let (i2, _) = string_forge.intern("y"); + fn intern_different() { + let interner = PermInterner::new(); + let (i1, _) = interner.intern("x"); + let (i2, _) = interner.intern("y"); assert_ne!(i1, i2); } #[test] - fn test_lookup_found() { - let string_forge = StringForge::new(); - let (idx, _) = string_forge.intern("hello"); - assert_eq!(string_forge.lookup(idx), Some("hello".to_string())); + fn lookup_zero_clone() { + let interner = PermInterner::new(); + let (id, _) = interner.intern("hello"); + assert_eq!(interner.lookup(id), Some("hello")); } #[test] - fn test_lookup_not_found() { - let string_forge = StringForge::new(); - assert_eq!(string_forge.lookup(99999), None); + fn lookup_not_found() { + let interner = PermInterner::new(); + assert_eq!(interner.lookup(99999), None); } #[test] - fn test_ref_count_and_decref() { - let string_forge = StringForge::new(); - let (idx, _) = string_forge.intern("s"); - assert_eq!(string_forge.lookup(idx), Some("s".to_string())); - string_forge.decref(idx); - assert_eq!(string_forge.lookup(idx), Some("s".to_string())); + fn entry_count_monotonic() { + let interner = PermInterner::new(); + assert_eq!(interner.entry_count(), 0); + interner.intern("a"); + interner.intern("b"); + interner.intern("a"); + assert_eq!(interner.entry_count(), 2); } #[test] - fn test_maybe_sweep_noop_when_none() { - let string_forge = StringForge::new(); - string_forge.intern("live"); - string_forge.maybe_sweep(None); - assert!(string_forge.lookup(0).is_some()); + fn many_unique_no_collision() { + let interner = PermInterner::new(); + for i in 0..10_000 { + let s = format!("key{i}"); + let (id, _) = interner.intern(&s); + assert_eq!(interner.lookup(id), Some(&*Box::leak(s.into_boxed_str()))); + } + assert_eq!(interner.entry_count(), 10_000); } #[test] - fn test_maybe_sweep_skip_below_threshold() { - let string_forge = StringForge::new(); - string_forge.intern("live"); - string_forge.maybe_sweep(Some(100)); - assert_eq!(string_forge.lookup(0), Some("live".to_string())); + fn string_ptr_roundtrip() { + let interner = PermInterner::new(); + let (id, _) = interner.intern("perm"); + let ptr = interner.string_ptr(id); + assert_eq!(unsafe { (*ptr).as_str() }, "perm"); + // Second call returns the same stable pointer (materialized once). + assert_eq!(interner.string_ptr(id), ptr); } } diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index 15572316..72208698 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -1,5 +1,42 @@ +use std::hash::{Hash, Hasher}; + +use rustc_hash::FxHasher; + use crate::value::JsValue; +/// Heap-allocated JS string value. +/// +/// String *values* are NaN-boxed as 48-bit pointers to a `JsString` (see +/// `JsValue::string`), replacing the old interner `(index, hash16)` pair. `hash` +/// is a full 64-bit FxHash computed once at construction and used as a fast +/// inequality reject before content comparison. +#[derive(Debug)] +pub struct JsString { + pub data: String, + pub hash: u64, +} + +impl JsString { + pub fn new(data: String) -> Self { + let mut h = FxHasher::default(); + data.hash(&mut h); + let hash = h.finish(); + Self { data, hash } + } + + pub fn len(&self) -> usize { + self.data.len() + } + + pub fn is_empty(&self) -> bool { + self.data.is_empty() + } + + pub fn as_str(&self) -> &str { + self.data.as_str() + } +} + /// Type-safe opaque wrapper around a native function pointer. /// /// Stored as `*const ()` rather than a concrete `fn` type so that `oxide_types` does not diff --git a/crates/oxide_types/src/value.rs b/crates/oxide_types/src/value.rs index 7135835a..0410de73 100644 --- a/crates/oxide_types/src/value.rs +++ b/crates/oxide_types/src/value.rs @@ -1,6 +1,6 @@ use std::fmt; -use crate::object::JsObject; +use crate::object::{JsObject, JsString}; /// Quiet NaN prefix — bits 63-51 = sign(1) + exponent(0x7FF) + quiet_bit(1) const QNAN_PREFIX: u64 = 0xFFF8_0000_0000_0000; @@ -28,11 +28,6 @@ const PTR_MASK: u64 = 0x0000_FFFF_FFFF_FFFF; /// 32-bit integer payload mask const INT_MASK: u64 = 0x0000_0000_FFFF_FFFF; -/// String payload: [hash_prefix:16bit][table_index:32bit] -const STRING_HASH_SHIFT: u64 = 32; -const STRING_HASH_MASK: u64 = 0xFFFF_0000_0000; -const STRING_INDEX_MASK: u64 = 0x0000_FFFF_FFFF; - fn make_tag(tag: u64) -> u64 { QNAN_PREFIX | (tag << TAG_SHIFT) } @@ -71,12 +66,7 @@ impl PartialEq for JsValue { return self.as_ptr() == other.as_ptr(); } if self.is_string() && other.is_string() { - let ha = self.as_string_hash(); - let hb = other.as_string_hash(); - if ha != hb { - return false; - } - return self.as_string_index() == other.as_string_index(); + return self.as_string_ptr() == other.as_string_ptr(); } if self.is_symbol() && other.is_symbol() { return self.as_symbol_index() == other.as_symbol_index(); @@ -122,10 +112,17 @@ impl JsValue { Self(make_tag(TAG_OBJECT) | addr) } - pub fn string(index: u32, hash: u16) -> Self { - let payload = ((hash as u64) << STRING_HASH_SHIFT) | (index as u64); - debug_assert!(payload & !(STRING_HASH_MASK | STRING_INDEX_MASK) == 0, "string payload overflow"); - Self(make_tag(TAG_STRING) | payload) + pub fn string(ptr: *const JsString) -> Self { + let addr = ptr as u64; + debug_assert!(addr <= PTR_MASK, "string pointer must fit in 48 bits"); + Self(make_tag(TAG_STRING) | addr) + } + + /// Semantic alias for permanent (kernel-owned, never-collected) strings. + /// Encoding is identical to `string`; the perm-vs-session distinction is by + /// ownership (membership in the GC root set), not by the NaN-box bits. + pub fn perm_string(ptr: *const JsString) -> Self { + Self::string(ptr) } pub fn is_double(&self) -> bool { @@ -214,14 +211,14 @@ impl JsValue { Self(make_tag(TAG_OBJECT) | addr) } - pub fn as_string_index(&self) -> u32 { + pub fn as_string_ptr(&self) -> *const JsString { debug_assert!(self.is_string(), "JsValue is not a string"); - (self.0 & STRING_INDEX_MASK) as u32 + (self.0 & PTR_MASK) as *const JsString } - pub fn as_string_hash(&self) -> u16 { + pub fn as_string_ptr_mut(&self) -> *mut JsString { debug_assert!(self.is_string(), "JsValue is not a string"); - ((self.0 & STRING_HASH_MASK) >> STRING_HASH_SHIFT) as u16 + (self.0 & PTR_MASK) as *mut JsString } pub fn symbol(index: u32) -> Self { @@ -234,7 +231,7 @@ impl JsValue { pub fn as_symbol_index(&self) -> u32 { debug_assert!(self.is_symbol()); - (self.0 & STRING_INDEX_MASK) as u32 + (self.0 & INT_MASK) as u32 } } @@ -301,7 +298,7 @@ impl fmt::Debug for JsValue { } else if self.is_object() { write!(f, "JsValue(Object({:p}))", self.as_ptr()) } else if self.is_string() { - write!(f, "JsValue(String(idx={}, hash={:#06x}))", self.as_string_index(), self.as_string_hash()) + write!(f, "JsValue(String({:p}))", self.as_string_ptr()) } else if self.is_symbol() { write!(f, "JsValue(Symbol(idx={}))", self.as_symbol_index()) } else { @@ -411,4 +408,29 @@ mod tests { }) .unwrap(); } + + #[test] + fn string_ptr_roundtrip() { + use crate::object::JsString; + let s = Box::new(JsString::new("test".to_string())); + let ptr: *const JsString = &*s; + let val = JsValue::string(ptr); + assert!(val.is_string()); + assert_eq!(val.as_string_ptr(), ptr); + assert_eq!(unsafe { (*val.as_string_ptr()).as_str() }, "test"); + } + + #[test] + fn string_pointer_equality() { + use crate::object::JsString; + let a = Box::new(JsString::new("x".to_string())); + let b = Box::new(JsString::new("x".to_string())); + let va = JsValue::string(&*a); + let vb = JsValue::string(&*b); + // Distinct allocations with identical content are NOT == (pointer identity). + // Semantic content equality is handled in coercion, not PartialEq. + assert_ne!(va, vb); + assert_eq!(va, JsValue::string(&*a)); + assert_eq!(unsafe { (*va.as_string_ptr()).as_str() }, unsafe { (*vb.as_string_ptr()).as_str() }); + } } diff --git a/crates/oxide_vm/src/bindings/bind_array.rs b/crates/oxide_vm/src/bindings/bind_array.rs index 76b15465..403a6cbc 100644 --- a/crates/oxide_vm/src/bindings/bind_array.rs +++ b/crates/oxide_vm/src/bindings/bind_array.rs @@ -42,7 +42,7 @@ pub fn bind_array(core: &Arc, session: &KernelSession, global: &mut session.builtin_world().bind_array_methods( &_array_methods, - core.string_forge().as_ref(), + core.perm_interner().as_ref(), core.shape_forge().as_ref(), ); diff --git a/crates/oxide_vm/src/bindings/bind_error.rs b/crates/oxide_vm/src/bindings/bind_error.rs index 3d7e071c..e6d1e512 100644 --- a/crates/oxide_vm/src/bindings/bind_error.rs +++ b/crates/oxide_vm/src/bindings/bind_error.rs @@ -10,7 +10,7 @@ fn bind_error_subtype_constructor( core: &Arc, session: &KernelSession, global: &mut JsObject, name: &str, proto_ptr: *mut JsObject, ctor_fn: *const (), ) { - let sf = core.string_forge().as_ref(); + let sf = core.perm_interner().as_ref(); let sh = core.shape_forge().as_ref(); let function_proto_ptr = session.builtin_world().function_proto.as_ptr() as *mut JsObject; @@ -28,7 +28,7 @@ fn bind_error_subtype_constructor( let ctor_shape2 = sh.make_shape(ctor_shape1, si_name); ctor.set_shape_id(ctor_shape2); ctor.ensure_hash_props().push(JsValue::from_js_object(proto_ptr)); - ctor.ensure_hash_props().push(JsValue::string(name_si, 0)); + ctor.ensure_hash_props().push(JsValue::perm_string(sf.string_ptr(name_si))); let ctor_ptr = Box::into_raw(ctor); @@ -57,11 +57,11 @@ pub fn bind_error(core: &Arc, session: &KernelSession, global: &mut }; session.builtin_world().bind_error_methods( &error_methods, - core.string_forge().as_ref(), + core.perm_interner().as_ref(), core.shape_forge().as_ref(), ); - let si_err = core.string_forge().intern("Error").0; + let si_err = core.perm_interner().intern("Error").0; let err_shape = core.shape_forge().make_shape(global.shape_id(), si_err); let err_val = JsValue::from_js_object(session.builtin_world().error_constructor.as_ptr() as *mut JsObject); global.set_shape_id(err_shape); diff --git a/crates/oxide_vm/src/bindings/bind_function.rs b/crates/oxide_vm/src/bindings/bind_function.rs index f5ec29fc..30867e3b 100644 --- a/crates/oxide_vm/src/bindings/bind_function.rs +++ b/crates/oxide_vm/src/bindings/bind_function.rs @@ -16,7 +16,7 @@ pub fn bind_function(core: &Arc, session: &KernelSession, global: &m }; session.builtin_world().bind_function_methods( &function_methods, - core.string_forge().as_ref(), + core.perm_interner().as_ref(), core.shape_forge().as_ref(), ); diff --git a/crates/oxide_vm/src/bindings/bind_math.rs b/crates/oxide_vm/src/bindings/bind_math.rs index 7a6c1a57..6e00bcd6 100644 --- a/crates/oxide_vm/src/bindings/bind_math.rs +++ b/crates/oxide_vm/src/bindings/bind_math.rs @@ -10,7 +10,7 @@ pub fn bind_math(core: &Arc, session: &KernelSession, global: &mut J let math = unsafe { &mut *math_ptr }; let bw = session.builtin_world(); - let sf = core.string_forge().as_ref(); + let sf = core.perm_interner().as_ref(); let sh = core.shape_forge().as_ref(); bind_methods!( @@ -65,13 +65,13 @@ pub fn bind_math(core: &Arc, session: &KernelSession, global: &mut J ("SQRT1_2", std::f64::consts::FRAC_1_SQRT_2), ("SQRT2", std::f64::consts::SQRT_2), ] { - let si = core.string_forge().as_ref().intern(name).0; + let si = core.perm_interner().as_ref().intern(name).0; let sh_c = core.shape_forge().as_ref().make_shape(math.shape_id(), si); math.set_shape_id(sh_c); math.ensure_hash_props().push(JsValue::float(val)); } - let si_m = core.string_forge().intern("Math").0; + let si_m = core.perm_interner().intern("Math").0; let m_shape = core.shape_forge().make_shape(global.shape_id(), si_m); let m_val = JsValue::from_js_object(session.builtin_world().math_object.as_ptr() as *mut JsObject); global.set_shape_id(m_shape); diff --git a/crates/oxide_vm/src/bindings/bind_number.rs b/crates/oxide_vm/src/bindings/bind_number.rs index 988a4c83..df3b3909 100644 --- a/crates/oxide_vm/src/bindings/bind_number.rs +++ b/crates/oxide_vm/src/bindings/bind_number.rs @@ -50,7 +50,7 @@ pub fn bind_number(core: &Arc, session: &KernelSession, global: &mut ("POSITIVE_INFINITY", JsValue::float(f64::INFINITY)), ] { ctor.ensure_hash_props().push(value); - let prop_si = core.string_forge().intern(name).0; + let prop_si = core.perm_interner().intern(name).0; let next_shape = core.shape_forge().make_shape(ctor.shape_id(), prop_si); ctor.set_shape_id(next_shape); } diff --git a/crates/oxide_vm/src/bindings/bind_object.rs b/crates/oxide_vm/src/bindings/bind_object.rs index 76183695..c976e176 100644 --- a/crates/oxide_vm/src/bindings/bind_object.rs +++ b/crates/oxide_vm/src/bindings/bind_object.rs @@ -32,7 +32,7 @@ pub fn bind_object(core: &Arc, session: &KernelSession, global: &mut }; session .builtin_world() - .bind_object_methods(&methods, core.string_forge().as_ref(), core.shape_forge().as_ref()); + .bind_object_methods(&methods, core.perm_interner().as_ref(), core.shape_forge().as_ref()); let ctor_ptr = session.builtin_world().object_constructor.as_ptr() as *mut JsObject; bind_constructor!(core, global, "Object", ctor_ptr, crate::builtins::object::object_constructor, 1); diff --git a/crates/oxide_vm/src/bindings/bind_string.rs b/crates/oxide_vm/src/bindings/bind_string.rs index cbd70b1c..01dd6a25 100644 --- a/crates/oxide_vm/src/bindings/bind_string.rs +++ b/crates/oxide_vm/src/bindings/bind_string.rs @@ -38,7 +38,7 @@ pub fn bind_string(core: &Arc, session: &KernelSession, global: &mut }; session.builtin_world().bind_string_methods( &string_methods, - core.string_forge().as_ref(), + core.perm_interner().as_ref(), core.shape_forge().as_ref(), ); @@ -55,7 +55,7 @@ pub fn bind_string(core: &Arc, session: &KernelSession, global: &mut &[("toString", crate::builtins::string::string_to_string as *const (), 0)], ); - let si_str = core.string_forge().intern("String").0; + let si_str = core.perm_interner().intern("String").0; let str_shape = core.shape_forge().make_shape(global.shape_id(), si_str); let str_val = JsValue::from_js_object(session.builtin_world().string_constructor.as_ptr() as *mut JsObject); global.set_shape_id(str_shape); diff --git a/crates/oxide_vm/src/bindings/bind_symbol.rs b/crates/oxide_vm/src/bindings/bind_symbol.rs index 3625666a..fd6180da 100644 --- a/crates/oxide_vm/src/bindings/bind_symbol.rs +++ b/crates/oxide_vm/src/bindings/bind_symbol.rs @@ -61,7 +61,7 @@ pub fn bind_symbol(core: &Arc, session: &KernelSession, global: &mut } fn bind_well_known_symbol(core: &Arc, ctor: &mut JsObject, name: &str, val: JsValue) { - let si = core.string_forge().intern(name).0; + let si = core.perm_interner().intern(name).0; let shape_id = core.shape_forge().make_shape(ctor.shape_id(), si); ctor.set_shape_id(shape_id); ctor.ensure_hash_props().push(val); diff --git a/crates/oxide_vm/src/bindings/mod.rs b/crates/oxide_vm/src/bindings/mod.rs index 596f37d0..d399da90 100644 --- a/crates/oxide_vm/src/bindings/mod.rs +++ b/crates/oxide_vm/src/bindings/mod.rs @@ -35,7 +35,7 @@ macro_rules! bind_constructor { bind_constructor!($core, $global, $name, $ctor_ptr, $ctor_fn, $nargs, hash: false) }}; ($core:expr, $global:expr, $name:literal, $ctor_ptr:expr, $ctor_fn:path, $nargs:literal, hash: $hash:literal) => {{ - let si = $core.string_forge().intern($name).0; + let si = $core.perm_interner().intern($name).0; let shape = $core.shape_forge().make_shape($global.shape_id(), si); let val = $crate::JsValue::from_js_object($ctor_ptr); $global.set_shape_id(shape); @@ -67,7 +67,7 @@ pub(crate) fn apply_binding_table( world: &BuiltinWorld, target: &mut JsObject, core: &Arc, bindings: &[(&'static str, *const (), u8)], ) { let shape_forge = core.shape_forge().as_ref(); - let string_forge = core.string_forge().as_ref(); + let string_forge = core.perm_interner().as_ref(); for (name, func, nargs) in bindings { // SAFETY: all entries in the binding table are NativeFn fn-item pointers cast to *const (). let fn_ptr = unsafe { oxide_types::object::NativeFnPtr::from_raw(*func) }; @@ -76,7 +76,7 @@ pub(crate) fn apply_binding_table( } pub(crate) fn bind_global_value(core: &Arc, global: &mut JsObject, name: &str, value: JsValue) { - let si = core.string_forge().intern(name).0; + let si = core.perm_interner().intern(name).0; let shape = core.shape_forge().make_shape(global.shape_id(), si); global.set_shape_id(shape); global.ensure_hash_props().push(value); @@ -91,7 +91,7 @@ fn bind_error_subtype_global( core: &Arc, session: &KernelSession, global: &mut JsObject, name: &str, proto: &P, ctor_fn: *const (), ) { - let constructor_si = core.string_forge().intern("constructor").0; + let constructor_si = core.perm_interner().intern("constructor").0; if let Some(pos) = core.shape_forge().lookup_position(proto.shape_id(), constructor_si) { let existing_ctor = proto.get_prop_at(pos); if existing_ctor.is_object() { @@ -105,7 +105,7 @@ fn bind_error_subtype_global( ctor.set_function(true); configure_native_constructor(&mut ctor, ctor_fn, 1); - let sf = core.string_forge().as_ref(); + let sf = core.perm_interner().as_ref(); let sh = core.shape_forge().as_ref(); let si_prototype = sf.intern("prototype").0; let si_name = sf.intern("name").0; @@ -115,7 +115,7 @@ fn bind_error_subtype_global( ctor.set_shape_id(ctor_shape2); ctor.ensure_hash_props() .push(JsValue::from_js_object(proto.as_ptr() as *mut JsObject)); - ctor.ensure_hash_props().push(JsValue::string(name_si, 0)); + ctor.ensure_hash_props().push(JsValue::perm_string(sf.string_ptr(name_si))); let ctor_ptr = Box::into_raw(ctor); bind_existing_global(core, global, name, JsValue::from_js_object(ctor_ptr)); diff --git a/crates/oxide_vm/src/builtins/array.rs b/crates/oxide_vm/src/builtins/array.rs index 96717476..b10051cb 100644 --- a/crates/oxide_vm/src/builtins/array.rs +++ b/crates/oxide_vm/src/builtins/array.rs @@ -64,7 +64,7 @@ fn create_new_array(vm: &mut Vm, n: usize) -> *mut JsObject { } fn array_length_arg(vm: &mut Vm, value: JsValue) -> Result { - let n = coercion::to_number(value, vm.kernel_core().string_forge().as_ref()); + let n = coercion::to_number(value); if !n.is_finite() || n < 0.0 || n.fract() != 0.0 || n > MAX_DENSE_PROPS as f64 { return Err(crate::builtins::error::create_range_error(vm, "Invalid array length")); } @@ -186,12 +186,12 @@ pub fn array_slice(vm: &mut Vm, args: &[u8]) -> NativeResult { let arr = unsafe { &*arr_ptr }; let n = arr.prop_count() as usize; let start = if args.len() > 1 { - (coercion::to_number(vm.reg(args[1]), vm.kernel_core().string_forge().as_ref()) as i32) as usize + (coercion::to_number(vm.reg(args[1])) as i32) as usize } else { 0 }; let end = if args.len() > 2 { - (coercion::to_number(vm.reg(args[2]), vm.kernel_core().string_forge().as_ref()) as i32) as usize + (coercion::to_number(vm.reg(args[2])) as i32) as usize } else { n }; @@ -215,7 +215,7 @@ pub fn array_splice(vm: &mut Vm, args: &[u8]) -> NativeResult { let n = arr.prop_count() as usize; let start = if args.len() > 1 { - let v = coercion::to_number(vm.reg(args[1]), vm.kernel_core().string_forge().as_ref()); + let v = coercion::to_number(vm.reg(args[1])); let s = v as i32; if s < 0 { (n as i32 + s).max(0) as usize @@ -227,7 +227,7 @@ pub fn array_splice(vm: &mut Vm, args: &[u8]) -> NativeResult { }; let delete_count = if args.len() > 2 { - let v = coercion::to_number(vm.reg(args[2]), vm.kernel_core().string_forge().as_ref()); + let v = coercion::to_number(vm.reg(args[2])); (v as usize).min(n - start) } else { n - start @@ -311,23 +311,22 @@ pub fn array_join(vm: &mut Vm, args: &[u8]) -> NativeResult { let arr = unsafe { &*arr_ptr }; let n = arr.prop_count() as usize; let sep = if args.len() > 1 { - coercion::to_string(vm.kernel_core().string_forge().as_ref(), vm.reg(args[1])) + coercion::to_string(vm.reg(args[1])) } else { ",".to_string() }; - let sf = vm.kernel_core().string_forge().as_ref(); let parts: Vec = (0..n) .map(|i| { let v = arr.get_prop_at(i); if v.is_undefined() || v.is_null() { String::new() } else { - coercion::to_string(sf, v) + coercion::to_string(v) } }) .collect(); let joined = parts.join(&sep); - let result_val = vm.intern(&joined); + let result_val = vm.new_string(&joined); NativeResult::Ok(result_val) } @@ -392,7 +391,7 @@ pub fn array_flat(vm: &mut Vm, args: &[u8]) -> NativeResult { let arr = unsafe { &*arr_ptr }; let n = arr.prop_count() as usize; let depth = if args.len() > 1 { - let n = coercion::to_number(vm.reg(args[1]), vm.kernel_core().string_forge().as_ref()); + let n = coercion::to_number(vm.reg(args[1])); if !n.is_finite() { vm.kernel_core.config.max_call_depth } else { @@ -506,8 +505,7 @@ pub fn array_filter(vm: &mut Vm, args: &[u8]) -> NativeResult { let elem = unsafe { (*arr_ptr).get_prop_at(i) }; match invoke_native_callback(vm, callback_val, this_val, &[elem, JsValue::int(i as i32), vm.reg(args[0])]) { NativeResult::Ok(result_val) => { - let sf = vm.kernel_core().string_forge().as_ref(); - if coercion::to_boolean(result_val, sf) { + if coercion::to_boolean(result_val) { kept.push(elem); } } @@ -578,8 +576,7 @@ pub fn array_find(vm: &mut Vm, args: &[u8]) -> NativeResult { let elem = unsafe { (*arr_ptr).get_prop_at(i) }; match invoke_native_callback(vm, callback_val, this_val, &[elem, JsValue::int(i as i32), vm.reg(args[0])]) { NativeResult::Ok(result_val) => { - let sf = vm.kernel_core().string_forge().as_ref(); - if coercion::to_boolean(result_val, sf) { + if coercion::to_boolean(result_val) { return NativeResult::Ok(elem); } } @@ -604,8 +601,7 @@ pub fn array_some(vm: &mut Vm, args: &[u8]) -> NativeResult { let elem = unsafe { (*arr_ptr).get_prop_at(i) }; match invoke_native_callback(vm, callback_val, this_val, &[elem, JsValue::int(i as i32), vm.reg(args[0])]) { NativeResult::Ok(result_val) => { - let sf = vm.kernel_core().string_forge().as_ref(); - if coercion::to_boolean(result_val, sf) { + if coercion::to_boolean(result_val) { return NativeResult::Ok(JsValue::bool(true)); } } @@ -630,8 +626,7 @@ pub fn array_every(vm: &mut Vm, args: &[u8]) -> NativeResult { let elem = unsafe { (*arr_ptr).get_prop_at(i) }; match invoke_native_callback(vm, callback_val, this_val, &[elem, JsValue::int(i as i32), vm.reg(args[0])]) { NativeResult::Ok(result_val) => { - let sf = vm.kernel_core().string_forge().as_ref(); - if !coercion::to_boolean(result_val, sf) { + if !coercion::to_boolean(result_val) { return NativeResult::Ok(JsValue::bool(false)); } } @@ -725,12 +720,12 @@ pub fn array_fill(vm: &mut Vm, args: &[u8]) -> NativeResult { let len = arr.prop_count() as usize; let value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; let start = if args.len() > 2 { - (coercion::to_number(vm.reg(args[2]), vm.kernel_core().string_forge().as_ref()) as i32).max(0) as usize + (coercion::to_number(vm.reg(args[2])) as i32).max(0) as usize } else { 0 }; let end = if args.len() > 3 { - let e = coercion::to_number(vm.reg(args[3]), vm.kernel_core().string_forge().as_ref()) as i32; + let e = coercion::to_number(vm.reg(args[3])) as i32; (e as usize).min(len) } else { len @@ -749,17 +744,17 @@ pub fn array_copy_within(vm: &mut Vm, args: &[u8]) -> NativeResult { return NativeResult::Ok(vm.reg(args[0])); } let target = if args.len() > 1 { - (coercion::to_number(vm.reg(args[1]), vm.kernel_core().string_forge().as_ref()) as i32) as usize + (coercion::to_number(vm.reg(args[1])) as i32) as usize } else { 0 }; let start = if args.len() > 2 { - (coercion::to_number(vm.reg(args[2]), vm.kernel_core().string_forge().as_ref()) as i32) as usize + (coercion::to_number(vm.reg(args[2])) as i32) as usize } else { 0 }; let end = if args.len() > 3 { - (coercion::to_number(vm.reg(args[3]), vm.kernel_core().string_forge().as_ref()) as i32 as usize).min(len) + (coercion::to_number(vm.reg(args[3])) as i32 as usize).min(len) } else { len }; @@ -779,11 +774,7 @@ pub fn array_at(vm: &mut Vm, args: &[u8]) -> NativeResult { let arr_ptr = array_ptr!(vm, args); let arr = unsafe { &*arr_ptr }; let len = arr.prop_count() as i32; - let mut index = if args.len() > 1 { - coercion::to_number(vm.reg(args[1]), vm.kernel_core().string_forge().as_ref()) as i32 - } else { - 0 - }; + let mut index = if args.len() > 1 { coercion::to_number(vm.reg(args[1])) as i32 } else { 0 }; if index < 0 { index += len; } @@ -820,7 +811,7 @@ pub fn array_find_index(vm: &mut Vm, args: &[u8]) -> NativeResult { let elem = unsafe { (*arr_ptr).get_prop_at(i) }; match invoke_native_callback(vm, callback_val, this_val, &[elem, JsValue::int(i as i32), vm.reg(args[0])]) { NativeResult::Ok(r) => { - if coercion::to_boolean(r, vm.kernel_core().string_forge().as_ref()) { + if coercion::to_boolean(r) { return NativeResult::Ok(JsValue::int(i as i32)); } } @@ -848,7 +839,7 @@ pub fn array_find_last(vm: &mut Vm, args: &[u8]) -> NativeResult { let elem = unsafe { (*arr_ptr).get_prop_at(i) }; match invoke_native_callback(vm, callback_val, this_val, &[elem, JsValue::int(i), vm.reg(args[0])]) { NativeResult::Ok(r) => { - if coercion::to_boolean(r, vm.kernel_core().string_forge().as_ref()) { + if coercion::to_boolean(r) { return NativeResult::Ok(elem); } } @@ -917,7 +908,7 @@ pub fn array_sort(vm: &mut Vm, args: &[u8]) -> NativeResult { if let Some(callback) = comparator { match invoke_native_callback(vm, callback, JsValue::undefined(), &[*a, *b]) { NativeResult::Ok(result) => { - let n = coercion::to_number(result, vm.kernel_core().string_forge().as_ref()); + let n = coercion::to_number(result); if n.is_nan() || n == 0.0 { std::cmp::Ordering::Equal } else if n < 0.0 { @@ -937,8 +928,8 @@ pub fn array_sort(vm: &mut Vm, args: &[u8]) -> NativeResult { } } } else { - let sa = coercion::to_string(vm.kernel_core().string_forge().as_ref(), *a); - let sb = coercion::to_string(vm.kernel_core().string_forge().as_ref(), *b); + let sa = coercion::to_string(*a); + let sb = coercion::to_string(*b); sa.cmp(&sb) } }); diff --git a/crates/oxide_vm/src/builtins/array_buffer.rs b/crates/oxide_vm/src/builtins/array_buffer.rs index 93e71027..001dde2f 100644 --- a/crates/oxide_vm/src/builtins/array_buffer.rs +++ b/crates/oxide_vm/src/builtins/array_buffer.rs @@ -22,7 +22,7 @@ fn type_error(vm: &mut Vm, msg: &str) -> NativeResult { } fn to_index(vm: &mut Vm, value: JsValue) -> Result { - let n = coercion::to_number(value, vm.kernel_core().string_forge().as_ref()); + let n = coercion::to_number(value); if n.is_nan() { return Ok(0); } @@ -32,8 +32,8 @@ fn to_index(vm: &mut Vm, value: JsValue) -> Result { Ok(n.trunc() as usize) } -fn normalize_index(vm: &mut Vm, value: JsValue, len: usize) -> usize { - let n = coercion::to_number(value, vm.kernel_core().string_forge().as_ref()); +fn normalize_index(_vm: &mut Vm, value: JsValue, len: usize) -> usize { + let n = coercion::to_number(value); if n.is_nan() { return 0; } @@ -46,7 +46,7 @@ fn normalize_index(vm: &mut Vm, value: JsValue, len: usize) -> usize { } fn set_named_prop(vm: &mut Vm, obj: &mut JsObject, name: &str, value: JsValue, attributes: PropAttributes) { - let si = vm.kernel_core().string_forge().intern(name).0; + let si = vm.kernel_core().perm_interner().intern(name).0; let _ = vm.define_data_property(obj, si, value, attributes); } @@ -177,5 +177,5 @@ pub fn array_buffer_to_string(vm: &mut Vm, args: &[u8]) -> NativeResult { if array_buffer_data_ptr(vm, this_val).is_err() { return type_error(vm, "ArrayBuffer.prototype.toString called on incompatible receiver"); } - NativeResult::Ok(vm.intern("[object ArrayBuffer]")) + NativeResult::Ok(vm.new_string("[object ArrayBuffer]")) } diff --git a/crates/oxide_vm/src/builtins/boolean.rs b/crates/oxide_vm/src/builtins/boolean.rs index 56f41645..07adbf3f 100644 --- a/crates/oxide_vm/src/builtins/boolean.rs +++ b/crates/oxide_vm/src/builtins/boolean.rs @@ -13,12 +13,8 @@ pub fn boolean_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { } else if arg.is_bool() { arg.as_bool() } else if arg.is_string() { - let s = vm - .kernel_core() - .string_forge() - .lookup(arg.as_string_index()) - .unwrap_or_default(); - !s.is_empty() + // SAFETY: arg is a string value. + !unsafe { (*arg.as_string_ptr()).is_empty() } } else if arg.is_object() { true } else if arg.is_int() { @@ -81,9 +77,9 @@ pub fn boolean_prototype_to_string(vm: &mut Vm, args: &[u8]) -> NativeResult { let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); if this_val.is_bool() { return if this_val.as_bool() { - NativeResult::Ok(vm.intern("true")) + NativeResult::Ok(vm.new_string("true")) } else { - NativeResult::Ok(vm.intern("false")) + NativeResult::Ok(vm.new_string("false")) }; } if !this_val.is_object() { @@ -106,8 +102,8 @@ pub fn boolean_prototype_to_string(vm: &mut Vm, args: &[u8]) -> NativeResult { !val.is_null() && !val.is_undefined() }; if is_true { - NativeResult::Ok(vm.intern("true")) + NativeResult::Ok(vm.new_string("true")) } else { - NativeResult::Ok(vm.intern("false")) + NativeResult::Ok(vm.new_string("false")) } } diff --git a/crates/oxide_vm/src/builtins/data_view.rs b/crates/oxide_vm/src/builtins/data_view.rs index c642a502..b431c9f1 100644 --- a/crates/oxide_vm/src/builtins/data_view.rs +++ b/crates/oxide_vm/src/builtins/data_view.rs @@ -24,7 +24,7 @@ macro_rules! native_try { } fn to_index(vm: &mut Vm, value: JsValue, msg: &str) -> Result { - let n = coercion::to_number(value, vm.kernel_core().string_forge().as_ref()); + let n = coercion::to_number(value); if n.is_nan() { return Ok(0); } @@ -35,19 +35,15 @@ fn to_index(vm: &mut Vm, value: JsValue, msg: &str) -> Result { } fn is_little_endian(vm: &mut Vm, args: &[u8], idx: usize) -> bool { - args.get(idx) - .map(|reg| coercion::to_boolean(vm.reg(*reg), vm.kernel_core().string_forge().as_ref())) - .unwrap_or(false) + args.get(idx).map(|reg| coercion::to_boolean(vm.reg(*reg))).unwrap_or(false) } fn numeric_arg(vm: &mut Vm, args: &[u8], idx: usize) -> f64 { - args.get(idx) - .map(|reg| coercion::to_number(vm.reg(*reg), vm.kernel_core().string_forge().as_ref())) - .unwrap_or(0.0) + args.get(idx).map(|reg| coercion::to_number(vm.reg(*reg))).unwrap_or(0.0) } fn set_named_prop(vm: &mut Vm, obj: &mut JsObject, name: &str, value: JsValue, attributes: PropAttributes) { - let si = vm.kernel_core().string_forge().intern(name).0; + let si = vm.kernel_core().perm_interner().intern(name).0; let _ = vm.define_data_property(obj, si, value, attributes); } @@ -438,5 +434,5 @@ pub fn data_view_set_big_uint64(vm: &mut Vm, args: &[u8]) -> NativeResult { pub fn data_view_to_string(vm: &mut Vm, args: &[u8]) -> NativeResult { let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); native_try!(get_data_view_data(vm, this_val)); - NativeResult::Ok(vm.intern("[object DataView]")) + NativeResult::Ok(vm.new_string("[object DataView]")) } diff --git a/crates/oxide_vm/src/builtins/date.rs b/crates/oxide_vm/src/builtins/date.rs index 55f5ced2..1436ff2d 100644 --- a/crates/oxide_vm/src/builtins/date.rs +++ b/crates/oxide_vm/src/builtins/date.rs @@ -104,33 +104,13 @@ pub fn date_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { let timestamp = if args.len() < 2 { Utc::now().timestamp_millis() as f64 } else if args.len() > 2 { - let y = coercion::to_number(vm.reg(args[1]), vm.kernel_core().string_forge().as_ref()) as i32; - let m = coercion::to_number(vm.reg(args[2]), vm.kernel_core().string_forge().as_ref()) as u32; - let d = if args.len() > 3 { - coercion::to_number(vm.reg(args[3]), vm.kernel_core().string_forge().as_ref()) as u32 - } else { - 1 - }; - let h = if args.len() > 4 { - coercion::to_number(vm.reg(args[4]), vm.kernel_core().string_forge().as_ref()) as u32 - } else { - 0 - }; - let min = if args.len() > 5 { - coercion::to_number(vm.reg(args[5]), vm.kernel_core().string_forge().as_ref()) as u32 - } else { - 0 - }; - let sec = if args.len() > 6 { - coercion::to_number(vm.reg(args[6]), vm.kernel_core().string_forge().as_ref()) as u32 - } else { - 0 - }; - let ms = if args.len() > 7 { - coercion::to_number(vm.reg(args[7]), vm.kernel_core().string_forge().as_ref()) as u32 - } else { - 0 - }; + let y = coercion::to_number(vm.reg(args[1])) as i32; + let m = coercion::to_number(vm.reg(args[2])) as u32; + let d = if args.len() > 3 { coercion::to_number(vm.reg(args[3])) as u32 } else { 1 }; + let h = if args.len() > 4 { coercion::to_number(vm.reg(args[4])) as u32 } else { 0 }; + let min = if args.len() > 5 { coercion::to_number(vm.reg(args[5])) as u32 } else { 0 }; + let sec = if args.len() > 6 { coercion::to_number(vm.reg(args[6])) as u32 } else { 0 }; + let ms = if args.len() > 7 { coercion::to_number(vm.reg(args[7])) as u32 } else { 0 }; NaiveDate::from_ymd_opt(y, m + 1, d) .and_then(|nd| { nd.and_hms_milli_opt(h, min, sec, ms) @@ -141,11 +121,8 @@ pub fn date_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { } else { let val = vm.reg(args[1]); if val.is_string() { - let s = vm - .kernel_core() - .string_forge() - .lookup(val.as_string_index()) - .unwrap_or_default(); + // SAFETY: val is a string value. + let s = unsafe { (*val.as_string_ptr()).data.clone() }; let formats = ["%Y-%m-%dT%H:%M:%S%.fZ", "%Y-%m-%dT%H:%M:%S%.f"]; let mut ts = f64::NAN; for fmt in &formats { @@ -191,7 +168,7 @@ pub fn date_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { } else { "Invalid Date".to_string() }; - return NativeResult::Ok(vm.intern(&s)); + return NativeResult::Ok(vm.new_string(&s)); } let mut obj = JsObject::new_empty( @@ -217,11 +194,8 @@ pub fn date_parse(vm: &mut Vm, args: &[u8]) -> NativeResult { if !val.is_string() { return NativeResult::Ok(JsValue::float(f64::NAN)); } - let s = vm - .kernel_core() - .string_forge() - .lookup(val.as_string_index()) - .unwrap_or_default(); + // SAFETY: val is a string value. + let s = unsafe { (*val.as_string_ptr()).data.clone() }; let formats = ["%Y-%m-%dT%H:%M:%S%.fZ", "%Y-%m-%dT%H:%M:%S%.f"]; let mut ts = f64::NAN; for fmt in &formats { @@ -315,7 +289,7 @@ fn local_offset_minutes() -> i32 { fn get_opt_arg(vm: &Vm, args: &[u8], idx: usize, default: u32) -> u32 { if args.len() > idx { - coercion::to_number(vm.reg(args[idx]), vm.kernel_core().string_forge().as_ref()) as u32 + coercion::to_number(vm.reg(args[idx])) as u32 } else { default } @@ -323,18 +297,14 @@ fn get_opt_arg(vm: &Vm, args: &[u8], idx: usize, default: u32) -> u32 { pub fn date_set_time(vm: &mut Vm, args: &[u8]) -> NativeResult { let obj = unsafe { &mut *native_try!(date_this_mut(vm, args)) }; - let val = if args.len() > 1 { - coercion::to_number(vm.reg(args[1]), vm.kernel_core().string_forge().as_ref()) - } else { - f64::NAN - }; + let val = if args.len() > 1 { coercion::to_number(vm.reg(args[1])) } else { f64::NAN }; set_timestamp(obj, val); NativeResult::Ok(JsValue::float(val)) } pub fn date_set_full_year(vm: &mut Vm, args: &[u8]) -> NativeResult { let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; - let v = coercion::to_number(val, vm.kernel_core().string_forge().as_ref()); + let v = coercion::to_number(val); let obj = unsafe { &mut *native_try!(date_this_mut(vm, args)) }; let ms = get_timestamp(obj); if !ms.is_finite() { @@ -357,7 +327,7 @@ pub fn date_set_full_year(vm: &mut Vm, args: &[u8]) -> NativeResult { } pub fn date_set_month(vm: &mut Vm, args: &[u8]) -> NativeResult { let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; - let v = coercion::to_number(val, vm.kernel_core().string_forge().as_ref()); + let v = coercion::to_number(val); let obj = unsafe { &mut *native_try!(date_this_mut(vm, args)) }; let ms = get_timestamp(obj); if !ms.is_finite() { @@ -375,7 +345,7 @@ pub fn date_set_month(vm: &mut Vm, args: &[u8]) -> NativeResult { } pub fn date_set_date(vm: &mut Vm, args: &[u8]) -> NativeResult { let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; - let v = coercion::to_number(val, vm.kernel_core().string_forge().as_ref()); + let v = coercion::to_number(val); let obj = unsafe { &mut *native_try!(date_this_mut(vm, args)) }; let ms = get_timestamp(obj); if !ms.is_finite() { @@ -392,7 +362,7 @@ pub fn date_set_date(vm: &mut Vm, args: &[u8]) -> NativeResult { } pub fn date_set_hours(vm: &mut Vm, args: &[u8]) -> NativeResult { let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; - let v = coercion::to_number(val, vm.kernel_core().string_forge().as_ref()); + let v = coercion::to_number(val); let obj = unsafe { &mut *native_try!(date_this_mut(vm, args)) }; let ms = get_timestamp(obj); if !ms.is_finite() { @@ -417,7 +387,7 @@ pub fn date_set_hours(vm: &mut Vm, args: &[u8]) -> NativeResult { } pub fn date_set_minutes(vm: &mut Vm, args: &[u8]) -> NativeResult { let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; - let v = coercion::to_number(val, vm.kernel_core().string_forge().as_ref()); + let v = coercion::to_number(val); let obj = unsafe { &mut *native_try!(date_this_mut(vm, args)) }; let ms = get_timestamp(obj); if !ms.is_finite() { @@ -440,7 +410,7 @@ pub fn date_set_minutes(vm: &mut Vm, args: &[u8]) -> NativeResult { } pub fn date_set_seconds(vm: &mut Vm, args: &[u8]) -> NativeResult { let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; - let v = coercion::to_number(val, vm.kernel_core().string_forge().as_ref()); + let v = coercion::to_number(val); let obj = unsafe { &mut *native_try!(date_this_mut(vm, args)) }; let ms = get_timestamp(obj); if !ms.is_finite() { @@ -461,7 +431,7 @@ pub fn date_set_seconds(vm: &mut Vm, args: &[u8]) -> NativeResult { } pub fn date_set_milliseconds(vm: &mut Vm, args: &[u8]) -> NativeResult { let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; - let v = coercion::to_number(val, vm.kernel_core().string_forge().as_ref()); + let v = coercion::to_number(val); let obj = unsafe { &mut *native_try!(date_this_mut(vm, args)) }; let ms = get_timestamp(obj); if !ms.is_finite() { @@ -481,13 +451,13 @@ fn date_to_string_inner(vm: &mut Vm, args: &[u8], format_str: &str, invalid: &st let obj = unsafe { &*native_try!(date_this(vm, args)) }; let ms = get_timestamp(obj); if !ms.is_finite() { - return NativeResult::Ok(vm.intern(invalid)); + return NativeResult::Ok(vm.new_string(invalid)); } let s = match dt_from_ms(ms) { Some(dt) => dt.format(format_str).to_string(), None => invalid.to_string(), }; - NativeResult::Ok(vm.intern(&s)) + NativeResult::Ok(vm.new_string(&s)) } pub fn date_to_iso_string(vm: &mut Vm, args: &[u8]) -> NativeResult { @@ -504,7 +474,7 @@ pub fn date_to_json(vm: &mut Vm, args: &[u8]) -> NativeResult { Some(dt) => dt.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(), None => return NativeResult::Ok(JsValue::null()), }; - NativeResult::Ok(vm.intern(&s)) + NativeResult::Ok(vm.new_string(&s)) } pub fn date_to_string(vm: &mut Vm, args: &[u8]) -> NativeResult { diff --git a/crates/oxide_vm/src/builtins/error.rs b/crates/oxide_vm/src/builtins/error.rs index b704b9c6..77f5fac2 100644 --- a/crates/oxide_vm/src/builtins/error.rs +++ b/crates/oxide_vm/src/builtins/error.rs @@ -14,12 +14,12 @@ fn create_error_object(vm: &mut Vm, error_proto_ptr: *mut JsObject, name: &str, .epoch() .alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(error_proto_ptr))); - let sf = Arc::clone(vm.kernel_core().string_forge()); + let sf = Arc::clone(vm.kernel_core().perm_interner()); let sh = Arc::clone(vm.kernel_core().shape_forge()); let si_message = sf.intern("message").0; let sh_message = sh.make_shape(EMPTY_SHAPE_ID, si_message); - let msg_val = vm.intern(message); + let msg_val = vm.new_string(message); unsafe { (*obj).set_shape_id(sh_message); (*obj).push_prop(msg_val); @@ -27,7 +27,7 @@ fn create_error_object(vm: &mut Vm, error_proto_ptr: *mut JsObject, name: &str, let si_name = sf.intern("name").0; let sh_name = sh.make_shape(sh_message, si_name); - let name_val = vm.intern(name); + let name_val = vm.new_string(name); unsafe { (*obj).set_shape_id(sh_name); (*obj).push_prop(name_val); @@ -75,7 +75,7 @@ pub fn create_uri_error(vm: &mut Vm, msg: &str) -> JsValue { fn get_msg(vm: &mut Vm, args: &[u8]) -> String { if args.len() > 1 { - coercion::to_string(vm.kernel_core().string_forge().as_ref(), vm.reg(args[1])) + coercion::to_string(vm.reg(args[1])) } else { String::new() } @@ -145,8 +145,7 @@ pub fn error_to_string(vm: &mut Vm, args: &[u8]) -> NativeResult { ("Error".to_string(), String::new()) }; let result = crate::vm::format_error_message(&name, &msg); - let si = vm.kernel_core().string_forge().intern(&result).0; - NativeResult::Ok(JsValue::string(si, 0)) + NativeResult::Ok(vm.new_string(&result)) } pub fn error_stack_getter(vm: &mut Vm, _args: &[u8]) -> NativeResult { @@ -154,15 +153,14 @@ pub fn error_stack_getter(vm: &mut Vm, _args: &[u8]) -> NativeResult { for frame in vm.frames.iter().rev() { let func_name = vm .kernel_core() - .string_forge() + .perm_interner() .lookup(frame.function_name) - .unwrap_or_else(|| "".to_string()); + .unwrap_or(""); lines.push(format!(" at {} (native)", func_name)); } lines.push(" at (native)".to_string()); let result = lines.join("\n"); - let si = vm.kernel_core().string_forge().intern(&result).0; - NativeResult::Ok(JsValue::string(si, 0)) + NativeResult::Ok(vm.new_string(&result)) } #[cfg(test)] @@ -174,7 +172,7 @@ mod tests { #[test] fn error_stack_uses_call_frame_function_name() { let mut vm = Vm::new(); - let name_si = vm.kernel_core().string_forge().intern("foo").0; + let name_si = vm.kernel_core().perm_interner().intern("foo").0; vm.frames.push(CallFrame { return_addr: 0, function_name: name_si, @@ -190,11 +188,7 @@ mod tests { }); let stack = error_stack_getter(&mut vm, &[0]).unwrap(); - let stack_str = vm - .kernel_core() - .string_forge() - .lookup(stack.as_string_index()) - .unwrap_or_default(); + let stack_str = vm.lookup_str(stack).unwrap_or_default(); assert!(stack_str.contains("foo"), "expected function name in stack: {stack_str}"); } } diff --git a/crates/oxide_vm/src/builtins/function.rs b/crates/oxide_vm/src/builtins/function.rs index dd1470bb..a69579d5 100644 --- a/crates/oxide_vm/src/builtins/function.rs +++ b/crates/oxide_vm/src/builtins/function.rs @@ -121,16 +121,10 @@ pub fn function_to_string(vm: &mut Vm, args: &[u8]) -> NativeResult { )); } - let name_si = vm.kernel_core().string_forge().intern("name").0; + let name_si = vm.kernel_core().perm_interner().intern("name").0; let name = vm .resolve_property(func, name_si) - .and_then(|v| { - if v.is_string() { - vm.kernel_core().string_forge().lookup(v.as_string_index()) - } else { - None - } - }) + .and_then(|v| vm.lookup_str(v)) .unwrap_or_else(|| { let sub_idx = func.sub_module_index(); if sub_idx > 0 && (sub_idx as usize) <= vm.sub_modules.len() { @@ -146,6 +140,5 @@ pub fn function_to_string(vm: &mut Vm, args: &[u8]) -> NativeResult { } else { format!("function {name}() {{ {body} }}") }; - let si = vm.kernel_core().string_forge().intern(&result).0; - NativeResult::Ok(JsValue::string(si, 0)) + NativeResult::Ok(vm.new_string(&result)) } diff --git a/crates/oxide_vm/src/builtins/global.rs b/crates/oxide_vm/src/builtins/global.rs index c7c0acee..16051375 100644 --- a/crates/oxide_vm/src/builtins/global.rs +++ b/crates/oxide_vm/src/builtins/global.rs @@ -9,7 +9,7 @@ pub use uri::{decode_uri, decode_uri_component, encode_uri, encode_uri_component fn string_arg(vm: &mut Vm, args: &[u8]) -> String { if args.len() > 1 { - coercion::to_string(vm.kernel_core().string_forge().as_ref(), vm.reg(args[1])) + coercion::to_string(vm.reg(args[1])) } else { "undefined".to_string() } diff --git a/crates/oxide_vm/src/builtins/global/annex_b.rs b/crates/oxide_vm/src/builtins/global/annex_b.rs index 4aea2a7b..7e4950ab 100644 --- a/crates/oxide_vm/src/builtins/global/annex_b.rs +++ b/crates/oxide_vm/src/builtins/global/annex_b.rs @@ -87,12 +87,12 @@ fn unescape_string(input: &str) -> String { pub fn js_escape(vm: &mut Vm, args: &[u8]) -> NativeResult { let input = super::string_arg(vm, args); - NativeResult::Ok(vm.intern(&escape_string(&input))) + NativeResult::Ok(vm.new_string(&escape_string(&input))) } pub fn js_unescape(vm: &mut Vm, args: &[u8]) -> NativeResult { let input = super::string_arg(vm, args); - NativeResult::Ok(vm.intern(&unescape_string(&input))) + NativeResult::Ok(vm.new_string(&unescape_string(&input))) } #[cfg(test)] diff --git a/crates/oxide_vm/src/builtins/global/uri.rs b/crates/oxide_vm/src/builtins/global/uri.rs index d2291da8..a37b7838 100644 --- a/crates/oxide_vm/src/builtins/global/uri.rs +++ b/crates/oxide_vm/src/builtins/global/uri.rs @@ -88,18 +88,18 @@ fn uri_error(vm: &mut Vm) -> NativeResult { pub fn encode_uri(vm: &mut Vm, args: &[u8]) -> NativeResult { let input = super::string_arg(vm, args); - NativeResult::Ok(vm.intern(&encode_uri_string(&input, URI_SAFE))) + NativeResult::Ok(vm.new_string(&encode_uri_string(&input, URI_SAFE))) } pub fn encode_uri_component(vm: &mut Vm, args: &[u8]) -> NativeResult { let input = super::string_arg(vm, args); - NativeResult::Ok(vm.intern(&encode_uri_string(&input, URI_UNESCAPED))) + NativeResult::Ok(vm.new_string(&encode_uri_string(&input, URI_UNESCAPED))) } pub fn decode_uri(vm: &mut Vm, args: &[u8]) -> NativeResult { let input = super::string_arg(vm, args); match decode_uri_string(&input, true) { - Ok(decoded) => NativeResult::Ok(vm.intern(&decoded)), + Ok(decoded) => NativeResult::Ok(vm.new_string(&decoded)), Err(()) => uri_error(vm), } } @@ -107,7 +107,7 @@ pub fn decode_uri(vm: &mut Vm, args: &[u8]) -> NativeResult { pub fn decode_uri_component(vm: &mut Vm, args: &[u8]) -> NativeResult { let input = super::string_arg(vm, args); match decode_uri_string(&input, false) { - Ok(decoded) => NativeResult::Ok(vm.intern(&decoded)), + Ok(decoded) => NativeResult::Ok(vm.new_string(&decoded)), Err(()) => uri_error(vm), } } diff --git a/crates/oxide_vm/src/builtins/iterator.rs b/crates/oxide_vm/src/builtins/iterator.rs index 81b50396..4e561ef7 100644 --- a/crates/oxide_vm/src/builtins/iterator.rs +++ b/crates/oxide_vm/src/builtins/iterator.rs @@ -28,9 +28,9 @@ pub(crate) fn make_iterator_for_value(vm: &mut Vm, value: JsValue) -> Result NativeResult { } let wrapper = unsafe { &mut *this_val.as_js_object_ptr() }; - let inner_si = vm.kernel_core().string_forge().intern(INNER_PROP).0; - let index_si = vm.kernel_core().string_forge().intern(INDEX_PROP).0; + let inner_si = vm.kernel_core().perm_interner().intern(INNER_PROP).0; + let index_si = vm.kernel_core().perm_interner().intern(INDEX_PROP).0; let inner = match vm.ordinary_get(wrapper, inner_si, this_val) { Ok(inner) if !inner.is_undefined() => inner, _ => { @@ -69,7 +69,7 @@ pub fn iterator_wrapper_next(vm: &mut Vm, args: &[u8]) -> NativeResult { if inner.is_object() { let inner_obj = unsafe { &*inner.as_js_object_ptr() }; - let next_si = vm.kernel_core().string_forge().intern("next").0; + let next_si = vm.kernel_core().perm_interner().intern("next").0; let next = match vm.ordinary_get(inner_obj, next_si, inner) { Ok(next) => next, Err(err) => return NativeResult::Err(crate::builtins::error::create_type_error(vm, &err)), @@ -90,7 +90,7 @@ fn get_iterator(vm: &mut Vm, value: JsValue) -> Result { if value.is_object() { let obj = unsafe { &*value.as_js_object_ptr() }; - let next_si = vm.kernel_core().string_forge().intern("next").0; + let next_si = vm.kernel_core().perm_interner().intern("next").0; if let Ok(next) = vm.ordinary_get(obj, next_si, value) { if is_callable(next) { return Ok(value); @@ -115,11 +115,11 @@ fn next_array_like(vm: &mut Vm, wrapper: &mut JsObject, inner: JsValue, index_si if inner.is_string() { let index = current_index(vm, wrapper, index_si); - let source = coercion::to_string(vm.kernel_core().string_forge().as_ref(), inner); + let source = coercion::to_string(inner); let mut chars = source.chars(); if let Some(ch) = chars.nth(index) { vm.set_or_create_prop_value(wrapper, index_si, JsValue::int((index + 1) as i32)); - let value = vm.intern(&ch.to_string()); + let value = vm.new_string(&ch.to_string()); return Some(make_iter_result(vm, value, false)); } return Some(make_iter_result(vm, JsValue::undefined(), true)); @@ -141,8 +141,8 @@ fn make_iter_result(vm: &mut Vm, value: JsValue, done: bool) -> JsValue { let obj = vm .epoch() .alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(object_proto))); - let value_si = vm.kernel_core().string_forge().intern("value").0; - let done_si = vm.kernel_core().string_forge().intern("done").0; + let value_si = vm.kernel_core().perm_interner().intern("value").0; + let done_si = vm.kernel_core().perm_interner().intern("done").0; let obj_ref = unsafe { &mut *obj }; vm.set_or_create_prop_value(obj_ref, value_si, value); vm.set_or_create_prop_value(obj_ref, done_si, JsValue::bool(done)); @@ -157,8 +157,8 @@ fn make_native_function(vm: &mut Vm, name: &str, native_fn: *const (), arg_count func.set_native_fn(Some(unsafe { oxide_types::object::NativeFnPtr::from_raw(native_fn) })); func.set_native_arg_count(arg_count); let func = vm.alloc_object(func); - let name_si = vm.kernel_core().string_forge().intern("name").0; - let value = vm.intern(name); + let name_si = vm.kernel_core().perm_interner().intern("name").0; + let value = vm.new_string(name); let func_ref = unsafe { &mut *func }; vm.set_or_create_prop_value(func_ref, name_si, value); JsValue::from_js_object(func) diff --git a/crates/oxide_vm/src/builtins/json.rs b/crates/oxide_vm/src/builtins/json.rs index 515d2dcb..a0485c6e 100644 --- a/crates/oxide_vm/src/builtins/json.rs +++ b/crates/oxide_vm/src/builtins/json.rs @@ -21,8 +21,8 @@ pub fn json_parse(vm: &mut Vm, args: &[u8]) -> NativeResult { )); } let text = { - let si = val.as_string_index(); - vm.kernel_core().string_forge().lookup(si).unwrap_or_default() + // SAFETY: val is a string value. + unsafe { (*val.as_string_ptr()).data.clone() } }; let parsed: serde_json::Value = match serde_json::from_str(&text) { @@ -44,7 +44,7 @@ fn value_to_jsvalue(vm: &mut Vm, val: &serde_json::Value) -> JsValue { JsValue::float(0.0) } } - serde_json::Value::String(s) => vm.intern(s), + serde_json::Value::String(s) => vm.new_string(s), serde_json::Value::Array(arr) => { let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject; let n = arr.len(); @@ -66,7 +66,7 @@ fn value_to_jsvalue(vm: &mut Vm, val: &serde_json::Value) -> JsValue { let object_proto = vm.session().builtin_world().object_proto.as_ptr() as *mut JsObject; let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(object_proto)); for (key, val) in map { - let si = vm.kernel_core().string_forge().intern(key).0; + let si = vm.kernel_core().perm_interner().intern(key).0; let jsv = value_to_jsvalue(vm, val); let new_shape = vm.kernel_core().shape_forge().make_shape(obj.shape_id(), si); obj.set_shape_id(new_shape); @@ -91,7 +91,7 @@ pub fn json_stringify(vm: &mut Vm, args: &[u8]) -> NativeResult { if jsvalue_to_json(vm, value, &mut visited, &mut output).is_err() { return NativeResult::Err(crate::builtins::error::create_type_error(vm, "cyclic object value")); }; - NativeResult::Ok(vm.intern(&output)) + NativeResult::Ok(vm.new_string(&output)) } fn jsvalue_to_json(vm: &Vm, val: JsValue, visited: &mut HashSet<*const u8>, out: &mut String) -> Result<(), ()> { @@ -113,8 +113,8 @@ fn jsvalue_to_json(vm: &Vm, val: JsValue, visited: &mut HashSet<*const u8>, out: out.push_str(buf.format(n)); } } else if val.is_string() { - let si = val.as_string_index(); - let s = vm.kernel_core().string_forge().lookup(si).unwrap_or_default(); + // SAFETY: val is a string value. + let s = unsafe { (*val.as_string_ptr()).data.clone() }; stringify_string(&s, out); } else if val.is_object() { let obj_ptr = val.as_js_object_ptr(); @@ -184,8 +184,8 @@ fn stringify_object(vm: &Vm, obj: &JsObject, visited: &mut HashSet<*const u8>, o out.push(','); } first = false; - let name = vm.kernel_core().string_forge().lookup(si).unwrap_or_default(); - stringify_string(&name, out); + let name = vm.kernel_core().perm_interner().lookup(si).unwrap_or_default(); + stringify_string(name, out); out.push(':'); jsvalue_to_json(vm, val, visited, out)?; } diff --git a/crates/oxide_vm/src/builtins/math.rs b/crates/oxide_vm/src/builtins/math.rs index acd3840a..d1e3949b 100644 --- a/crates/oxide_vm/src/builtins/math.rs +++ b/crates/oxide_vm/src/builtins/math.rs @@ -5,7 +5,7 @@ use crate::native::NativeResult; use crate::vm::Vm; fn num(vm: &Vm, reg: u8) -> f64 { - coercion::to_number(vm.reg(reg), vm.kernel_core().string_forge().as_ref()) + coercion::to_number(vm.reg(reg)) } fn arg1(vm: &Vm, args: &[u8]) -> f64 { diff --git a/crates/oxide_vm/src/builtins/number.rs b/crates/oxide_vm/src/builtins/number.rs index e96fe66d..c6aa6760 100644 --- a/crates/oxide_vm/src/builtins/number.rs +++ b/crates/oxide_vm/src/builtins/number.rs @@ -5,11 +5,7 @@ use crate::native::NativeResult; use crate::vm::Vm; pub fn number_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { - let n = if args.len() > 1 { - coercion::to_number(vm.reg(args[1]), vm.kernel_core().string_forge().as_ref()) - } else { - 0.0 - }; + let n = if args.len() > 1 { coercion::to_number(vm.reg(args[1])) } else { 0.0 }; let number_proto = vm.session().builtin_world().number_proto.as_ptr() as *mut oxide_types::object::JsObject; let is_ctor = if let Some(this_reg) = args.first().copied() { let this_val = vm.reg(this_reg); @@ -71,7 +67,7 @@ pub fn number_is_finite(vm: &mut Vm, args: &[u8]) -> NativeResult { if val.is_double() { return NativeResult::Ok(JsValue::bool(val.as_double().is_finite())); } - let n = coercion::to_number(val, vm.kernel_core().string_forge().as_ref()); + let n = coercion::to_number(val); NativeResult::Ok(JsValue::bool(n.is_finite())) } @@ -79,8 +75,7 @@ pub fn number_parse_int(vm: &mut Vm, args: &[u8]) -> NativeResult { if args.len() < 2 { return NativeResult::Ok(JsValue::float(f64::NAN)); } - let sf = vm.kernel_core().string_forge().as_ref(); - let s = coercion::to_string(sf, vm.reg(args[1])); + let s = coercion::to_string(vm.reg(args[1])); let s = s.trim(); if s.is_empty() { @@ -88,7 +83,7 @@ pub fn number_parse_int(vm: &mut Vm, args: &[u8]) -> NativeResult { } let radix = if args.len() > 2 { - let r = coercion::to_number(vm.reg(args[2]), sf) as i32; + let r = coercion::to_number(vm.reg(args[2])) as i32; if r == 0 { 10 } else { @@ -121,8 +116,7 @@ pub fn number_parse_float(vm: &mut Vm, args: &[u8]) -> NativeResult { if args.len() < 2 { return NativeResult::Ok(JsValue::float(f64::NAN)); } - let sf = vm.kernel_core().string_forge().as_ref(); - let s = coercion::to_string(sf, vm.reg(args[1])); + let s = coercion::to_string(vm.reg(args[1])); let s = s.trim(); if s.is_empty() { @@ -136,9 +130,9 @@ pub fn number_parse_float(vm: &mut Vm, args: &[u8]) -> NativeResult { } pub fn number_to_string(vm: &mut Vm, args: &[u8]) -> NativeResult { - let n = coercion::to_number(vm.reg(args[0]), vm.kernel_core().string_forge().as_ref()); + let n = coercion::to_number(vm.reg(args[0])); let radix = if args.len() > 1 { - let r = coercion::to_number(vm.reg(args[1]), vm.kernel_core().string_forge().as_ref()) as u32; + let r = coercion::to_number(vm.reg(args[1])) as u32; r.clamp(2, 36) } else { 10u32 @@ -146,22 +140,22 @@ pub fn number_to_string(vm: &mut Vm, args: &[u8]) -> NativeResult { if radix == 10 { if n.is_nan() { - return NativeResult::Ok(vm.intern("NaN")); + return NativeResult::Ok(vm.new_string("NaN")); } if n.is_infinite() { if n.is_sign_positive() { - return NativeResult::Ok(vm.intern("Infinity")); + return NativeResult::Ok(vm.new_string("Infinity")); } - return NativeResult::Ok(vm.intern("-Infinity")); + return NativeResult::Ok(vm.new_string("-Infinity")); } if n.fract() == 0.0 && n >= i32::MIN as f64 && n <= i32::MAX as f64 { - return NativeResult::Ok(vm.intern(&(n as i64).to_string())); + return NativeResult::Ok(vm.new_string(&(n as i64).to_string())); } let mut buf = ryu::Buffer::new(); - NativeResult::Ok(vm.intern(buf.format(n))) + NativeResult::Ok(vm.new_string(buf.format(n))) } else { if n.is_nan() { - return NativeResult::Ok(vm.intern("NaN")); + return NativeResult::Ok(vm.new_string("NaN")); } let nn = n as i64; let mut result = String::new(); @@ -182,70 +176,70 @@ pub fn number_to_string(vm: &mut Vm, args: &[u8]) -> NativeResult { if nn < 0 { result.insert(0, '-'); } - NativeResult::Ok(vm.intern(&result)) + NativeResult::Ok(vm.new_string(&result)) } } pub fn number_to_fixed(vm: &mut Vm, args: &[u8]) -> NativeResult { - let n = coercion::to_number(vm.reg(args[0]), vm.kernel_core().string_forge().as_ref()); + let n = coercion::to_number(vm.reg(args[0])); let digits = if args.len() > 1 { - coercion::to_number(vm.reg(args[1]), vm.kernel_core().string_forge().as_ref()) as usize + coercion::to_number(vm.reg(args[1])) as usize } else { 0usize } .min(20); let formatted = format!("{:.digits$}", n); - NativeResult::Ok(vm.intern(&formatted)) + NativeResult::Ok(vm.new_string(&formatted)) } pub fn number_is_integer(vm: &mut Vm, args: &[u8]) -> NativeResult { let val = vm.reg(if args.len() > 1 { args[1] } else { args[0] }); - let n = coercion::to_number(val, vm.kernel_core().string_forge().as_ref()); + let n = coercion::to_number(val); NativeResult::Ok(JsValue::bool(n.trunc() == n && n.is_finite())) } pub fn number_is_safe_integer(vm: &mut Vm, args: &[u8]) -> NativeResult { let val = vm.reg(if args.len() > 1 { args[1] } else { args[0] }); - let n = coercion::to_number(val, vm.kernel_core().string_forge().as_ref()); + let n = coercion::to_number(val); let safe = n.trunc() == n && n.is_finite() && n >= -9007199254740991i64 as f64 && n <= 9007199254740991i64 as f64; NativeResult::Ok(JsValue::bool(safe)) } pub fn number_to_exponential(vm: &mut Vm, args: &[u8]) -> NativeResult { - let n = coercion::to_number(vm.reg(args[0]), vm.kernel_core().string_forge().as_ref()); + let n = coercion::to_number(vm.reg(args[0])); let digits = if args.len() > 1 { - coercion::to_number(vm.reg(args[1]), vm.kernel_core().string_forge().as_ref()) as usize + coercion::to_number(vm.reg(args[1])) as usize } else { 0usize } .min(100); if n.is_nan() { - return NativeResult::Ok(vm.intern("NaN")); + return NativeResult::Ok(vm.new_string("NaN")); } if n.is_infinite() { - return NativeResult::Ok(vm.intern(if n.is_sign_positive() { "Infinity" } else { "-Infinity" })); + return NativeResult::Ok(vm.new_string(if n.is_sign_positive() { "Infinity" } else { "-Infinity" })); } let formatted = format!("{:.digits$e}", n); - NativeResult::Ok(vm.intern(&formatted)) + NativeResult::Ok(vm.new_string(&formatted)) } pub fn number_to_precision(vm: &mut Vm, args: &[u8]) -> NativeResult { - let n = coercion::to_number(vm.reg(args[0]), vm.kernel_core().string_forge().as_ref()); + let n = coercion::to_number(vm.reg(args[0])); let precision = if args.len() > 1 { - coercion::to_number(vm.reg(args[1]), vm.kernel_core().string_forge().as_ref()) as usize + coercion::to_number(vm.reg(args[1])) as usize } else { 0usize } .min(21); if n.is_nan() { - return NativeResult::Ok(vm.intern("NaN")); + return NativeResult::Ok(vm.new_string("NaN")); } if n.is_infinite() { - return NativeResult::Ok(vm.intern(if n.is_sign_positive() { "Infinity" } else { "-Infinity" })); + return NativeResult::Ok(vm.new_string(if n.is_sign_positive() { "Infinity" } else { "-Infinity" })); } let formatted = format!("{:.precision$}", n); - NativeResult::Ok(vm.intern(&formatted)) + NativeResult::Ok(vm.new_string(&formatted)) } pub fn number_value_of(vm: &mut Vm, args: &[u8]) -> NativeResult { diff --git a/crates/oxide_vm/src/builtins/object.rs b/crates/oxide_vm/src/builtins/object.rs index a2445d8e..a2c5f2d0 100644 --- a/crates/oxide_vm/src/builtins/object.rs +++ b/crates/oxide_vm/src/builtins/object.rs @@ -1,5 +1,5 @@ use oxide_kernel::shape_forge::{ShapeForge, EMPTY_SHAPE_ID}; -use oxide_kernel::string_forge::StringForge; +use oxide_kernel::string_forge::PermInterner; use oxide_types::object::{JsObject, PropAttributes}; use oxide_types::private_key::is_private_name_key; use oxide_types::value::JsValue; @@ -62,7 +62,7 @@ pub fn object_keys(vm: &mut Vm, args: &[u8]) -> NativeResult { let keys = walk_own_keys(vm, obj); key_names = keys .iter() - .map(|(si, _offset)| vm.kernel_core().string_forge().lookup(*si).unwrap_or_default()) + .map(|(si, _offset)| vm.kernel_core().perm_interner().lookup(*si).unwrap_or("").to_string()) .collect(); } @@ -75,7 +75,7 @@ pub fn object_keys(vm: &mut Vm, args: &[u8]) -> NativeResult { vm.epoch().bump(), )); for (i, k) in key_names.iter().enumerate() { - let str_val = vm.intern(k); + let str_val = vm.new_string(k); unsafe { (*arr).set_prop_at(i, str_val); } @@ -159,27 +159,23 @@ pub fn object_define_property(vm: &mut Vm, args: &[u8]) -> NativeResult { if desc_ptr.is_null() { return NativeResult::Err(JsValue::undefined()); } - let prop_name_str = coercion::to_string(vm.kernel_core().string_forge().as_ref(), vm.reg(args[2])); - let si = vm.kernel_core().string_forge().intern(&prop_name_str).0; + let prop_name_str = coercion::to_string(vm.reg(args[2])); + let si = vm.kernel_core().perm_interner().intern(&prop_name_str).0; - let value_si = vm.kernel_core().string_forge().intern("value").0; - let get_si = vm.kernel_core().string_forge().intern("get").0; - let set_si = vm.kernel_core().string_forge().intern("set").0; - let writable_si = vm.kernel_core().string_forge().intern("writable").0; - let enumerable_si = vm.kernel_core().string_forge().intern("enumerable").0; - let configurable_si = vm.kernel_core().string_forge().intern("configurable").0; + let value_si = vm.kernel_core().perm_interner().intern("value").0; + let get_si = vm.kernel_core().perm_interner().intern("get").0; + let set_si = vm.kernel_core().perm_interner().intern("set").0; + let writable_si = vm.kernel_core().perm_interner().intern("writable").0; + let enumerable_si = vm.kernel_core().perm_interner().intern("enumerable").0; + let configurable_si = vm.kernel_core().perm_interner().intern("configurable").0; let desc = unsafe { &*desc_ptr }; let value_field = own_field(vm, desc, value_si); let get_field = own_field(vm, desc, get_si); let set_field = own_field(vm, desc, set_si); let writable_field = own_field(vm, desc, writable_si); - let enumerable = own_field(vm, desc, enumerable_si) - .map(|v| coercion::to_boolean(v, vm.kernel_core().string_forge().as_ref())) - .unwrap_or(false); - let configurable = own_field(vm, desc, configurable_si) - .map(|v| coercion::to_boolean(v, vm.kernel_core().string_forge().as_ref())) - .unwrap_or(false); + let enumerable = own_field(vm, desc, enumerable_si).map(coercion::to_boolean).unwrap_or(false); + let configurable = own_field(vm, desc, configurable_si).map(coercion::to_boolean).unwrap_or(false); let has_data = value_field.is_some() || writable_field.is_some(); let has_accessor = get_field.is_some() || set_field.is_some(); @@ -208,9 +204,7 @@ pub fn object_define_property(vm: &mut Vm, args: &[u8]) -> NativeResult { } } else { let value = value_field.unwrap_or(JsValue::undefined()); - let writable = writable_field - .map(|v| coercion::to_boolean(v, vm.kernel_core().string_forge().as_ref())) - .unwrap_or(false); + let writable = writable_field.map(coercion::to_boolean).unwrap_or(false); if vm .define_data_property(obj, si, value, PropAttributes::new(writable, enumerable, configurable)) .is_err() @@ -233,8 +227,8 @@ pub fn object_get_own_property_descriptor(vm: &mut Vm, args: &[u8]) -> NativeRes if obj_ptr.is_null() { return NativeResult::Ok(JsValue::undefined()); } - let prop_name_str = coercion::to_string(vm.kernel_core().string_forge().as_ref(), vm.reg(args[2])); - let si = vm.kernel_core().string_forge().intern(&prop_name_str).0; + let prop_name_str = coercion::to_string(vm.reg(args[2])); + let si = vm.kernel_core().perm_interner().intern(&prop_name_str).0; let (found_value, found_meta, found) = { let obj = unsafe { &*obj_ptr }; @@ -257,7 +251,7 @@ pub fn object_get_own_property_descriptor(vm: &mut Vm, args: &[u8]) -> NativeRes return NativeResult::Ok(JsValue::undefined()); } - let sf_ptr = vm.kernel_core().string_forge().as_ref() as *const StringForge; + let sf_ptr = vm.kernel_core().perm_interner().as_ref() as *const PermInterner; let sh_ptr = vm.kernel_core().shape_forge().as_ref() as *const ShapeForge; let desc_proto = vm.session().builtin_world().object_proto.as_ptr() as *mut JsObject; let desc = vm @@ -406,7 +400,7 @@ pub fn object_get_own_property_names(vm: &mut Vm, args: &[u8]) -> NativeResult { let obj = unsafe { &*obj_ptr }; let keys = walk_own_keys(vm, obj); keys.iter() - .map(|(si, _)| vm.kernel_core().string_forge().lookup(*si).unwrap_or_default()) + .map(|(si, _)| vm.kernel_core().perm_interner().lookup(*si).unwrap_or("").to_string()) .collect() }; @@ -419,7 +413,7 @@ pub fn object_get_own_property_names(vm: &mut Vm, args: &[u8]) -> NativeResult { vm.epoch().bump(), )); for (i, k) in key_names.iter().enumerate() { - let str_val = vm.intern(k); + let str_val = vm.new_string(k); unsafe { (*arr).set_prop_at(i, str_val); } @@ -524,8 +518,8 @@ pub fn object_entries(vm: &mut Vm, args: &[u8]) -> NativeResult { vm.epoch().bump(), )); for (i, (si, offset)) in keys.iter().enumerate() { - let key_str = vm.kernel_core().string_forge().lookup(*si).unwrap_or_default(); - let key_val = vm.intern(&key_str); + let key_str = vm.kernel_core().perm_interner().lookup(*si).unwrap_or_default(); + let key_val = vm.new_string(key_str); let val = obj.get_prop_at(*offset); let pair = vm.alloc_object(JsObject::new_array( EMPTY_SHAPE_ID, diff --git a/crates/oxide_vm/src/builtins/reflect.rs b/crates/oxide_vm/src/builtins/reflect.rs index 9d8ada1d..10b123b7 100644 --- a/crates/oxide_vm/src/builtins/reflect.rs +++ b/crates/oxide_vm/src/builtins/reflect.rs @@ -39,23 +39,19 @@ pub fn reflect_define_property(vm: &mut Vm, args: &[u8]) -> NativeResult { let key_si = vm.property_key_si(arg(vm, args, 2)); let desc = unsafe { &*desc_ptr }; - let value_si = vm.kernel_core().string_forge().intern("value").0; - let get_si = vm.kernel_core().string_forge().intern("get").0; - let set_si = vm.kernel_core().string_forge().intern("set").0; - let writable_si = vm.kernel_core().string_forge().intern("writable").0; - let enumerable_si = vm.kernel_core().string_forge().intern("enumerable").0; - let configurable_si = vm.kernel_core().string_forge().intern("configurable").0; + let value_si = vm.kernel_core().perm_interner().intern("value").0; + let get_si = vm.kernel_core().perm_interner().intern("get").0; + let set_si = vm.kernel_core().perm_interner().intern("set").0; + let writable_si = vm.kernel_core().perm_interner().intern("writable").0; + let enumerable_si = vm.kernel_core().perm_interner().intern("enumerable").0; + let configurable_si = vm.kernel_core().perm_interner().intern("configurable").0; let value_field = own_field(vm, desc, value_si); let get_field = own_field(vm, desc, get_si); let set_field = own_field(vm, desc, set_si); let writable_field = own_field(vm, desc, writable_si); - let enumerable = own_field(vm, desc, enumerable_si) - .map(|v| coercion::to_boolean(v, vm.kernel_core().string_forge().as_ref())) - .unwrap_or(false); - let configurable = own_field(vm, desc, configurable_si) - .map(|v| coercion::to_boolean(v, vm.kernel_core().string_forge().as_ref())) - .unwrap_or(false); + let enumerable = own_field(vm, desc, enumerable_si).map(coercion::to_boolean).unwrap_or(false); + let configurable = own_field(vm, desc, configurable_si).map(coercion::to_boolean).unwrap_or(false); let has_data = value_field.is_some() || writable_field.is_some(); let has_accessor = get_field.is_some() || set_field.is_some(); @@ -73,9 +69,7 @@ pub fn reflect_define_property(vm: &mut Vm, args: &[u8]) -> NativeResult { vm.define_accessor_property(target, key_si, get, set, PropAttributes::new(false, enumerable, configurable)) } else { let value = value_field.unwrap_or(JsValue::undefined()); - let writable = writable_field - .map(|v| coercion::to_boolean(v, vm.kernel_core().string_forge().as_ref())) - .unwrap_or(false); + let writable = writable_field.map(coercion::to_boolean).unwrap_or(false); vm.define_data_property(target, key_si, value, PropAttributes::new(writable, enumerable, configurable)) }; NativeResult::Ok(JsValue::bool(result.is_ok())) @@ -145,7 +139,7 @@ pub fn reflect_own_keys(vm: &mut Vm, args: &[u8]) -> NativeResult { let target = unsafe { &*target_ptr }; let key_names: Vec = walk_own_keys(vm, target) .into_iter() - .map(|(si, _)| vm.kernel_core().string_forge().lookup(si).unwrap_or_default()) + .map(|(si, _)| vm.kernel_core().perm_interner().lookup(si).unwrap_or("").to_string()) .collect(); NativeResult::Ok(make_string_array(vm, &key_names)) } @@ -228,7 +222,7 @@ fn make_string_array(vm: &mut Vm, parts: &[String]) -> JsValue { vm.epoch().bump(), )); for (idx, part) in parts.iter().enumerate() { - let value = vm.intern(part); + let value = vm.new_string(part); unsafe { &mut *arr }.set_prop_at(idx, value); } unsafe { &mut *arr }.set_prop_count(parts.len()); diff --git a/crates/oxide_vm/src/builtins/regexp.rs b/crates/oxide_vm/src/builtins/regexp.rs index 1c32d52c..f5293441 100644 --- a/crates/oxide_vm/src/builtins/regexp.rs +++ b/crates/oxide_vm/src/builtins/regexp.rs @@ -43,7 +43,7 @@ fn parse_flags(flags: &str) -> (bool, bool, bool) { } fn set_prop(obj: &mut JsObject, name: &str, val: JsValue, vm: &Vm) { - let si = vm.kernel_core().string_forge().intern(name).0; + let si = vm.kernel_core().perm_interner().intern(name).0; let shape_id = vm.kernel_core().shape_forge().make_shape(obj.shape_id(), si); obj.set_shape_id(shape_id); obj.ensure_hash_props().push(val); @@ -71,11 +71,11 @@ pub fn regexp_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { let (pattern, flags) = if args.len() < 2 { (String::new(), String::new()) } else if args.len() < 3 { - let pat = coercion::to_string(vm.kernel_core().string_forge().as_ref(), vm.reg(args[1])); + let pat = coercion::to_string(vm.reg(args[1])); (pat, String::new()) } else { - let pat = coercion::to_string(vm.kernel_core().string_forge().as_ref(), vm.reg(args[1])); - let fl = coercion::to_string(vm.kernel_core().string_forge().as_ref(), vm.reg(args[2])); + let pat = coercion::to_string(vm.reg(args[1])); + let fl = coercion::to_string(vm.reg(args[2])); (pat, fl) }; @@ -109,8 +109,8 @@ pub fn regexp_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { } set_prop(&mut obj, "lastIndex", JsValue::int(0), vm); - set_prop(&mut obj, "source", vm.intern(&pattern), vm); - set_prop(&mut obj, "flags", vm.intern(&flags), vm); + set_prop(&mut obj, "source", vm.new_string(&pattern), vm); + set_prop(&mut obj, "flags", vm.new_string(&flags), vm); set_prop(&mut obj, "global", JsValue::bool(global), vm); set_prop(&mut obj, "ignoreCase", JsValue::bool(ignore_case), vm); set_prop(&mut obj, "multiline", JsValue::bool(multi_line), vm); @@ -155,11 +155,8 @@ pub fn regexp_test(vm: &mut Vm, args: &[u8]) -> NativeResult { // SAFETY: fn_ptr holds a Box pointer stored by regexp_constructor. let regex = unsafe { &*(fn_ptr.as_ptr() as *const regex::Regex) }; - let haystack = coercion::to_string( - vm.kernel_core().string_forge().as_ref(), - vm.reg(if args.len() > 1 { args[1] } else { args[0] }), - ); - let last_index = coercion::to_number(get_prop(re, 0), vm.kernel_core().string_forge().as_ref()) as usize; + let haystack = coercion::to_string(vm.reg(if args.len() > 1 { args[1] } else { args[0] })); + let last_index = coercion::to_number(get_prop(re, 0)) as usize; let is_global = get_prop(re, 3).as_bool(); if is_global { @@ -188,14 +185,11 @@ pub fn regexp_exec(vm: &mut Vm, args: &[u8]) -> NativeResult { // SAFETY: fn_ptr holds a Box pointer stored by regexp_constructor. let regex = unsafe { &*(fn_ptr.as_ptr() as *const regex::Regex) }; - let haystack = coercion::to_string( - vm.kernel_core().string_forge().as_ref(), - vm.reg(if args.len() > 1 { args[1] } else { args[0] }), - ); + let haystack = coercion::to_string(vm.reg(if args.len() > 1 { args[1] } else { args[0] })); let (last_index, is_global) = { let re = unsafe { &*re_ptr }; - let li = coercion::to_number(get_prop(re, 0), vm.kernel_core().string_forge().as_ref()) as usize; + let li = coercion::to_number(get_prop(re, 0)) as usize; let g = get_prop(re, 3).as_bool(); (li, g) }; @@ -216,20 +210,20 @@ pub fn regexp_exec(vm: &mut Vm, args: &[u8]) -> NativeResult { ); let full_match = &haystack[m.start()..m.end()]; - let full_si = vm.kernel_core().string_forge().intern(full_match).0; - match_obj.ensure_hash_props().push(JsValue::string(full_si, 0)); + let full_val = vm.new_string(full_match); + match_obj.ensure_hash_props().push(full_val); let captures = regex.captures(&haystack[m.start()..m.end()]); if let Some(caps) = &captures { for i in 1..caps.len() { let cap_str = caps.get(i).map(|cm| cm.as_str()).unwrap_or(""); - let si = vm.kernel_core().string_forge().intern(cap_str).0; - match_obj.ensure_hash_props().push(JsValue::string(si, 0)); + let cap_val = vm.new_string(cap_str); + match_obj.ensure_hash_props().push(cap_val); } } set_prop(&mut match_obj, "index", JsValue::int(m.start() as i32), vm); - let haystack_val = vm.intern(&haystack); + let haystack_val = vm.new_string(&haystack); set_prop(&mut match_obj, "input", haystack_val, vm); set_prop(&mut match_obj, "groups", JsValue::undefined(), vm); @@ -262,6 +256,5 @@ pub fn regexp_to_string(vm: &mut Vm, args: &[u8]) -> NativeResult { vm.lookup_str(val).unwrap_or_default() }; let result = format!("/{}/{}", source, flags); - let si = vm.kernel_core().string_forge().intern(&result).0; - NativeResult::Ok(JsValue::string(si, 0)) + NativeResult::Ok(vm.new_string(&result)) } diff --git a/crates/oxide_vm/src/builtins/string.rs b/crates/oxide_vm/src/builtins/string.rs index 862b3840..dbc7fdfc 100644 --- a/crates/oxide_vm/src/builtins/string.rs +++ b/crates/oxide_vm/src/builtins/string.rs @@ -8,20 +8,20 @@ use crate::vm::Vm; use memchr::memchr; fn this_string(vm: &Vm, args: &[u8]) -> String { - coercion::to_string(vm.kernel_core().string_forge().as_ref(), vm.reg(args[0])) + coercion::to_string(vm.reg(args[0])) } pub fn string_from_char_code(vm: &mut Vm, args: &[u8]) -> NativeResult { let mut out = String::new(); for &arg_reg in args.iter().skip(1) { - let code = coercion::to_uint32(vm.reg(arg_reg), vm.kernel_core().string_forge().as_ref()) & 0xFFFF; + let code = coercion::to_uint32(vm.reg(arg_reg)) & 0xFFFF; if let Some(ch) = char::from_u32(code) { out.push(ch); } else { out.push('\u{FFFD}'); } } - NativeResult::Ok(vm.intern(&out)) + NativeResult::Ok(vm.new_string(&out)) } pub fn string_value_of(vm: &mut Vm, args: &[u8]) -> NativeResult { @@ -46,11 +46,11 @@ pub fn string_value_of(vm: &mut Vm, args: &[u8]) -> NativeResult { pub fn string_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { let s = if args.len() > 1 { - coercion::to_string(vm.kernel_core().string_forge().as_ref(), vm.reg(args[1])) + coercion::to_string(vm.reg(args[1])) } else { String::new() }; - let str_val = vm.intern(&s); + let str_val = vm.new_string(&s); let string_proto = vm.session().builtin_world().string_proto.as_ptr() as *mut JsObject; let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); @@ -104,7 +104,7 @@ fn make_string_array(vm: &mut Vm, parts: &[String]) -> JsValue { .alloc(JsObject::new_array(EMPTY_SHAPE_ID, JsValue::from_js_object(proto), n, vm.epoch().bump())); unsafe { for (i, s) in parts.iter().enumerate() { - let sv = vm.intern(s); + let sv = vm.new_string(s); (*arr).set_prop_at(i, sv); } (*arr).set_prop_count(n); @@ -112,8 +112,8 @@ fn make_string_array(vm: &mut Vm, parts: &[String]) -> JsValue { JsValue::from_js_object(arr) } -fn as_string(vm: &Vm, val: JsValue) -> String { - coercion::to_string(vm.kernel_core().string_forge().as_ref(), val) +fn as_string(_vm: &Vm, val: JsValue) -> String { + coercion::to_string(val) } fn char_len(s: &str) -> usize { @@ -166,7 +166,7 @@ pub fn string_index_of(vm: &mut Vm, args: &[u8]) -> NativeResult { } let search = as_string(vm, vm.reg(args[1])); let pos = if args.len() > 2 { - (coercion::to_number(vm.reg(args[2]), vm.kernel_core().string_forge().as_ref()) as usize).min(n) + (coercion::to_number(vm.reg(args[2])) as usize).min(n) } else { 0 }; @@ -197,7 +197,7 @@ pub fn string_includes(vm: &mut Vm, args: &[u8]) -> NativeResult { } let search = as_string(vm, vm.reg(args[1])); let pos = if args.len() > 2 { - (coercion::to_number(vm.reg(args[2]), vm.kernel_core().string_forge().as_ref()) as usize).min(n) + (coercion::to_number(vm.reg(args[2])) as usize).min(n) } else { 0 }; @@ -218,16 +218,16 @@ pub fn string_char_at(vm: &mut Vm, args: &[u8]) -> NativeResult { let s = this_string(vm, args); if args.len() < 2 { if s.is_empty() { - return NativeResult::Ok(vm.intern("")); + return NativeResult::Ok(vm.new_string("")); } - return NativeResult::Ok(vm.intern(&take_chars(&s, 1))); + return NativeResult::Ok(vm.new_string(&take_chars(&s, 1))); } - let idx = coercion::to_number(vm.reg(args[1]), vm.kernel_core().string_forge().as_ref()) as i32; + let idx = coercion::to_number(vm.reg(args[1])) as i32; if idx < 0 || idx as usize >= char_len(&s) { - return NativeResult::Ok(vm.intern("")); + return NativeResult::Ok(vm.new_string("")); } let ch = s.chars().nth(idx as usize).map(|c| c.to_string()).unwrap_or_default(); - NativeResult::Ok(vm.intern(&ch)) + NativeResult::Ok(vm.new_string(&ch)) } pub fn string_char_code_at(vm: &mut Vm, args: &[u8]) -> NativeResult { @@ -238,7 +238,7 @@ pub fn string_char_code_at(vm: &mut Vm, args: &[u8]) -> NativeResult { } return NativeResult::Ok(JsValue::int(s.chars().next().unwrap() as i32)); } - let idx = coercion::to_number(vm.reg(args[1]), vm.kernel_core().string_forge().as_ref()) as i32; + let idx = coercion::to_number(vm.reg(args[1])) as i32; if idx < 0 || idx as usize >= char_len(&s) { return NativeResult::Ok(JsValue::float(f64::NAN)); } @@ -247,18 +247,17 @@ pub fn string_char_code_at(vm: &mut Vm, args: &[u8]) -> NativeResult { pub fn string_concat(vm: &mut Vm, args: &[u8]) -> NativeResult { let mut result = this_string(vm, args); - let sf = vm.kernel_core().string_forge().as_ref(); for &arg_reg in args.iter().skip(1) { - result.push_str(&coercion::to_string(sf, vm.reg(arg_reg))); + result.push_str(&coercion::to_string(vm.reg(arg_reg))); } - NativeResult::Ok(vm.intern(&result)) + NativeResult::Ok(vm.new_string(&result)) } pub fn string_slice(vm: &mut Vm, args: &[u8]) -> NativeResult { let s = this_string(vm, args); let n = char_len(&s) as i32; let start = if args.len() > 1 { - let v = coercion::to_number(vm.reg(args[1]), vm.kernel_core().string_forge().as_ref()) as i32; + let v = coercion::to_number(vm.reg(args[1])) as i32; if v < 0 { (n + v).max(0) } else { @@ -268,7 +267,7 @@ pub fn string_slice(vm: &mut Vm, args: &[u8]) -> NativeResult { 0 }; let end = if args.len() > 2 { - let v = coercion::to_number(vm.reg(args[2]), vm.kernel_core().string_forge().as_ref()) as i32; + let v = coercion::to_number(vm.reg(args[2])) as i32; if v < 0 { (n + v).max(0) } else { @@ -280,23 +279,19 @@ pub fn string_slice(vm: &mut Vm, args: &[u8]) -> NativeResult { let start = start as usize; let end = end as usize; let result = if start < end { char_slice(&s, start, end) } else { "" }; - NativeResult::Ok(vm.intern(result)) + NativeResult::Ok(vm.new_string(result)) } pub fn string_substring(vm: &mut Vm, args: &[u8]) -> NativeResult { let s = this_string(vm, args); let n = char_len(&s) as i32; let mut start = if args.len() > 1 { - (coercion::to_number(vm.reg(args[1]), vm.kernel_core().string_forge().as_ref()) as i32) - .max(0) - .min(n) + (coercion::to_number(vm.reg(args[1])) as i32).max(0).min(n) } else { 0 }; let mut end = if args.len() > 2 { - (coercion::to_number(vm.reg(args[2]), vm.kernel_core().string_forge().as_ref()) as i32) - .max(0) - .min(n) + (coercion::to_number(vm.reg(args[2])) as i32).max(0).min(n) } else { n }; @@ -304,39 +299,39 @@ pub fn string_substring(vm: &mut Vm, args: &[u8]) -> NativeResult { std::mem::swap(&mut start, &mut end); } let result = char_slice(&s, start as usize, end as usize); - NativeResult::Ok(vm.intern(result)) + NativeResult::Ok(vm.new_string(result)) } pub fn string_to_upper_case(vm: &mut Vm, args: &[u8]) -> NativeResult { let s = this_string(vm, args); - NativeResult::Ok(vm.intern(&s.to_uppercase())) + NativeResult::Ok(vm.new_string(&s.to_uppercase())) } pub fn string_to_lower_case(vm: &mut Vm, args: &[u8]) -> NativeResult { let s = this_string(vm, args); - NativeResult::Ok(vm.intern(&s.to_lowercase())) + NativeResult::Ok(vm.new_string(&s.to_lowercase())) } pub fn string_trim(vm: &mut Vm, args: &[u8]) -> NativeResult { let s = this_string(vm, args); - NativeResult::Ok(vm.intern(s.trim())) + NativeResult::Ok(vm.new_string(s.trim())) } pub fn string_repeat(vm: &mut Vm, args: &[u8]) -> NativeResult { let s = this_string(vm, args); let n = if args.len() > 1 { - (coercion::to_number(vm.reg(args[1]), vm.kernel_core().string_forge().as_ref()) as usize).min(10000) + (coercion::to_number(vm.reg(args[1])) as usize).min(10000) } else { 1 }; - NativeResult::Ok(vm.intern(&s.repeat(n))) + NativeResult::Ok(vm.new_string(&s.repeat(n))) } pub fn string_pad_start(vm: &mut Vm, args: &[u8]) -> NativeResult { let s = this_string(vm, args); let s_len = char_len(&s); let target = if args.len() > 1 { - coercion::to_number(vm.reg(args[1]), vm.kernel_core().string_forge().as_ref()) as usize + coercion::to_number(vm.reg(args[1])) as usize } else { s_len }; @@ -345,21 +340,21 @@ pub fn string_pad_start(vm: &mut Vm, args: &[u8]) -> NativeResult { } let pad = if args.len() > 2 { as_string(vm, vm.reg(args[2])) } else { " ".to_string() }; if s_len >= target || pad.is_empty() { - return NativeResult::Ok(vm.intern(&s)); + return NativeResult::Ok(vm.new_string(&s)); } let needed = target - s_len; let pad_len = char_len(&pad).max(1); let reps = needed.div_ceil(pad_len); let mut out = take_chars(&pad.repeat(reps), needed); out.push_str(&s); - NativeResult::Ok(vm.intern(&out)) + NativeResult::Ok(vm.new_string(&out)) } pub fn string_pad_end(vm: &mut Vm, args: &[u8]) -> NativeResult { let s = this_string(vm, args); let s_len = char_len(&s); let target = if args.len() > 1 { - coercion::to_number(vm.reg(args[1]), vm.kernel_core().string_forge().as_ref()) as usize + coercion::to_number(vm.reg(args[1])) as usize } else { s_len }; @@ -368,14 +363,14 @@ pub fn string_pad_end(vm: &mut Vm, args: &[u8]) -> NativeResult { } let pad = if args.len() > 2 { as_string(vm, vm.reg(args[2])) } else { " ".to_string() }; if s_len >= target || pad.is_empty() { - return NativeResult::Ok(vm.intern(&s)); + return NativeResult::Ok(vm.new_string(&s)); } let needed = target - s_len; let pad_len = char_len(&pad).max(1); let reps = needed.div_ceil(pad_len); let mut out = s; out.push_str(&take_chars(&pad.repeat(reps), needed)); - NativeResult::Ok(vm.intern(&out)) + NativeResult::Ok(vm.new_string(&out)) } pub fn string_starts_with(vm: &mut Vm, args: &[u8]) -> NativeResult { @@ -386,7 +381,7 @@ pub fn string_starts_with(vm: &mut Vm, args: &[u8]) -> NativeResult { } let search = as_string(vm, vm.reg(args[1])); let pos = if args.len() > 2 { - (coercion::to_number(vm.reg(args[2]), vm.kernel_core().string_forge().as_ref()) as usize).min(n) + (coercion::to_number(vm.reg(args[2])) as usize).min(n) } else { 0 }; @@ -401,7 +396,7 @@ pub fn string_ends_with(vm: &mut Vm, args: &[u8]) -> NativeResult { } let search = as_string(vm, vm.reg(args[1])); let end_pos = if args.len() > 2 { - (coercion::to_number(vm.reg(args[2]), vm.kernel_core().string_forge().as_ref()) as usize).min(n) + (coercion::to_number(vm.reg(args[2])) as usize).min(n) } else { n }; @@ -416,7 +411,7 @@ pub fn string_split(vm: &mut Vm, args: &[u8]) -> NativeResult { } let sep_val = vm.reg(args[1]); let limit = if args.len() > 2 { - coercion::to_number(vm.reg(args[2]), vm.kernel_core().string_forge().as_ref()) as usize + coercion::to_number(vm.reg(args[2])) as usize } else { usize::MAX }; @@ -450,7 +445,7 @@ pub fn string_split(vm: &mut Vm, args: &[u8]) -> NativeResult { pub fn string_replace(vm: &mut Vm, args: &[u8]) -> NativeResult { let s = this_string(vm, args); if args.len() < 2 { - return NativeResult::Ok(vm.intern(&s)); + return NativeResult::Ok(vm.new_string(&s)); } let pattern_val = vm.reg(args[1]); let replacement = if args.len() > 2 { as_string(vm, vm.reg(args[2])) } else { String::new() }; @@ -460,17 +455,17 @@ pub fn string_replace(vm: &mut Vm, args: &[u8]) -> NativeResult { let re = unsafe { &*re_ptr }; let fn_ptr = match re.native_fn() { Some(p) => p, - None => return NativeResult::Ok(vm.intern(&s)), + None => return NativeResult::Ok(vm.new_string(&s)), }; // SAFETY: fn_ptr holds a Box pointer stored by regexp_constructor. let regex = unsafe { &*(fn_ptr.as_ptr() as *const regex::Regex) }; let result = regex.replace_all(&s, replacement.as_str()).to_string(); - return NativeResult::Ok(vm.intern(&result)); + return NativeResult::Ok(vm.new_string(&result)); } let pattern = as_string(vm, pattern_val); let result = s.replacen(&pattern, &replacement, 1); - NativeResult::Ok(vm.intern(&result)) + NativeResult::Ok(vm.new_string(&result)) } pub fn string_match_fn(vm: &mut Vm, args: &[u8]) -> NativeResult { @@ -531,21 +526,17 @@ pub fn string_search(vm: &mut Vm, args: &[u8]) -> NativeResult { pub fn string_trim_start(vm: &mut Vm, args: &[u8]) -> NativeResult { let s = this_string(vm, args); - NativeResult::Ok(vm.intern(s.trim_start())) + NativeResult::Ok(vm.new_string(s.trim_start())) } pub fn string_trim_end(vm: &mut Vm, args: &[u8]) -> NativeResult { let s = this_string(vm, args); - NativeResult::Ok(vm.intern(s.trim_end())) + NativeResult::Ok(vm.new_string(s.trim_end())) } pub fn string_code_point_at(vm: &mut Vm, args: &[u8]) -> NativeResult { let s = this_string(vm, args); - let pos = if args.len() > 1 { - coercion::to_number(vm.reg(args[1]), vm.kernel_core().string_forge().as_ref()) as usize - } else { - 0 - }; + let pos = if args.len() > 1 { coercion::to_number(vm.reg(args[1])) as usize } else { 0 }; let chars: Vec = s.chars().collect(); if pos >= chars.len() { return NativeResult::Ok(JsValue::undefined()); @@ -571,7 +562,7 @@ pub fn string_normalize(vm: &mut Vm, args: &[u8]) -> NativeResult { "NFKD" => s.nfkd().collect(), _ => s.nfc().collect(), }; - NativeResult::Ok(vm.intern(&result)) + NativeResult::Ok(vm.new_string(&result)) } pub fn string_match_all(vm: &mut Vm, args: &[u8]) -> NativeResult { @@ -598,7 +589,7 @@ pub fn string_match_all(vm: &mut Vm, args: &[u8]) -> NativeResult { pub fn string_replace_all(vm: &mut Vm, args: &[u8]) -> NativeResult { let s = this_string(vm, args); if args.len() < 2 { - return NativeResult::Ok(vm.intern(&s)); + return NativeResult::Ok(vm.new_string(&s)); } let pattern_val = vm.reg(args[1]); let replacement = if args.len() > 2 { as_string(vm, vm.reg(args[2])) } else { String::new() }; @@ -607,14 +598,14 @@ pub fn string_replace_all(vm: &mut Vm, args: &[u8]) -> NativeResult { let re = unsafe { &*re_ptr }; let fn_ptr = match re.native_fn() { Some(p) => p, - None => return NativeResult::Ok(vm.intern(&s)), + None => return NativeResult::Ok(vm.new_string(&s)), }; // SAFETY: fn_ptr holds a Box pointer stored by regexp_constructor. let regex = unsafe { &*(fn_ptr.as_ptr() as *const regex::Regex) }; let result = regex.replace_all(&s, replacement.as_str()).to_string(); - return NativeResult::Ok(vm.intern(&result)); + return NativeResult::Ok(vm.new_string(&result)); } let pattern = as_string(vm, pattern_val); let result = s.replace(&pattern, &replacement); - NativeResult::Ok(vm.intern(&result)) + NativeResult::Ok(vm.new_string(&result)) } diff --git a/crates/oxide_vm/src/builtins/symbol.rs b/crates/oxide_vm/src/builtins/symbol.rs index 501a3570..aedf0e67 100644 --- a/crates/oxide_vm/src/builtins/symbol.rs +++ b/crates/oxide_vm/src/builtins/symbol.rs @@ -28,7 +28,7 @@ pub fn symbol_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { } let description = if args.len() > 1 { - coercion::to_string(vm.kernel_core().string_forge().as_ref(), vm.reg(args[1])) + coercion::to_string(vm.reg(args[1])) } else { String::new() }; @@ -51,13 +51,12 @@ pub fn symbol_to_string(vm: &mut Vm, args: &[u8]) -> NativeResult { let idx = this_val.as_symbol_index(); let desc = vm.symbol_descriptions.get(idx as usize).cloned().unwrap_or_default(); let result = format!("Symbol({})", desc); - let si = vm.kernel_core().string_forge().intern(&result).0; - NativeResult::Ok(JsValue::string(si, 0)) + NativeResult::Ok(vm.new_string(&result)) } pub fn symbol_for(vm: &mut Vm, args: &[u8]) -> NativeResult { let key = if args.len() > 1 { - coercion::to_string(vm.kernel_core().string_forge().as_ref(), vm.reg(args[1])) + coercion::to_string(vm.reg(args[1])) } else { "undefined".to_string() }; @@ -79,12 +78,13 @@ pub fn symbol_key_for(vm: &mut Vm, args: &[u8]) -> NativeResult { } let idx = sym.as_symbol_index(); - for (key, ®istered_idx) in &vm.symbol_registry { - if registered_idx == idx { - let si = vm.kernel_core().string_forge().intern(key).0; - return NativeResult::Ok(JsValue::string(si, 0)); - } + let found = vm + .symbol_registry + .iter() + .find(|(_, ®istered_idx)| registered_idx == idx) + .map(|(key, _)| key.clone()); + match found { + Some(key) => NativeResult::Ok(vm.new_string(&key)), + None => NativeResult::Ok(JsValue::undefined()), } - - NativeResult::Ok(JsValue::undefined()) } diff --git a/crates/oxide_vm/src/builtins/typed_array.rs b/crates/oxide_vm/src/builtins/typed_array.rs index b1b9b0ea..04b6f159 100644 --- a/crates/oxide_vm/src/builtins/typed_array.rs +++ b/crates/oxide_vm/src/builtins/typed_array.rs @@ -33,7 +33,7 @@ fn range_error(vm: &mut Vm, msg: &str) -> JsValue { } fn to_index(vm: &mut Vm, value: JsValue, msg: &str) -> Result { - let n = coercion::to_number(value, vm.kernel_core().string_forge().as_ref()); + let n = coercion::to_number(value); if n.is_nan() { return Ok(0); } @@ -43,8 +43,8 @@ fn to_index(vm: &mut Vm, value: JsValue, msg: &str) -> Result { Ok(n.trunc() as usize) } -fn normalize_index(vm: &mut Vm, value: JsValue, len: usize) -> usize { - let n = coercion::to_number(value, vm.kernel_core().string_forge().as_ref()); +fn normalize_index(_vm: &mut Vm, value: JsValue, len: usize) -> usize { + let n = coercion::to_number(value); if n.is_nan() { return 0; } @@ -57,7 +57,7 @@ fn normalize_index(vm: &mut Vm, value: JsValue, len: usize) -> usize { } fn set_named_prop(vm: &mut Vm, obj: &mut JsObject, name: &str, value: JsValue, attributes: PropAttributes) { - let si = vm.kernel_core().string_forge().intern(name).0; + let si = vm.kernel_core().perm_interner().intern(name).0; let _ = vm.define_data_property(obj, si, value, attributes); } @@ -236,8 +236,8 @@ fn read_element(kind: TypedArrayKind, bytes: &[u8], offset: usize) -> JsValue { } } -fn numeric_value(vm: &mut Vm, value: JsValue) -> f64 { - coercion::to_number(value, vm.kernel_core().string_forge().as_ref()) +fn numeric_value(_vm: &mut Vm, value: JsValue) -> f64 { + coercion::to_number(value) } fn write_element(kind: TypedArrayKind, bytes: &mut [u8], offset: usize, value: f64) { @@ -477,5 +477,5 @@ pub fn typed_array_set(vm: &mut Vm, args: &[u8]) -> NativeResult { pub fn typed_array_to_string(vm: &mut Vm, args: &[u8]) -> NativeResult { let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); native_try!(get_typed_array_data(vm, this_val)); - NativeResult::Ok(vm.intern("[object TypedArray]")) + NativeResult::Ok(vm.new_string("[object TypedArray]")) } diff --git a/crates/oxide_vm/src/coercion.rs b/crates/oxide_vm/src/coercion.rs index d4c27c85..5c267162 100644 --- a/crates/oxide_vm/src/coercion.rs +++ b/crates/oxide_vm/src/coercion.rs @@ -1,10 +1,32 @@ use crate::vm::Vm; -use oxide_kernel::string_forge::StringForge; use oxide_types::object::JsObject; use oxide_types::shape::EMPTY_SHAPE_ID; use oxide_types::value::JsValue; -pub fn to_number(val: JsValue, string_forge: &StringForge) -> f64 { +/// Borrow a string value's content. Caller must hold a valid string `JsValue`. +/// +/// # Safety +/// `val` must be a string `JsValue` whose `JsString` pointer is alive (session or +/// permanent string) — the same contract as object-pointer derefs in the VM. +#[inline] +unsafe fn string_data(val: JsValue) -> &'static str { + (*val.as_string_ptr()).as_str() +} + +/// Content equality of two string values: pointer identity → hash+len reject → +/// byte compare. Replaces the old interner-index compare. +#[inline] +fn string_value_eq(a: JsValue, b: JsValue) -> bool { + if a.as_string_ptr() == b.as_string_ptr() { + return true; + } + // SAFETY: both are string JsValues; their JsString pointers are valid. + let sa = unsafe { &*a.as_string_ptr() }; + let sb = unsafe { &*b.as_string_ptr() }; + sa.hash == sb.hash && sa.data == sb.data +} + +pub fn to_number(val: JsValue) -> f64 { if val.is_int() { return val.as_int() as f64; } @@ -21,7 +43,8 @@ pub fn to_number(val: JsValue, string_forge: &StringForge) -> f64 { return f64::NAN; } if val.is_string() { - let s = string_forge.lookup(val.as_string_index()).unwrap_or_default(); + // SAFETY: val is a string value. + let s = unsafe { string_data(val) }; return s.parse::().unwrap_or(f64::NAN); } if val.is_object() { @@ -30,16 +53,16 @@ pub fn to_number(val: JsValue, string_forge: &StringForge) -> f64 { f64::NAN } -pub fn to_uint32(val: JsValue, string_forge: &StringForge) -> u32 { - let n = to_number(val, string_forge); +pub fn to_uint32(val: JsValue) -> u32 { + let n = to_number(val); if n == 0.0 || !n.is_finite() { return 0; } n.trunc().rem_euclid(4_294_967_296.0) as u32 } -pub fn to_int32(val: JsValue, string_forge: &StringForge) -> i32 { - let int = to_uint32(val, string_forge); +pub fn to_int32(val: JsValue) -> i32 { + let int = to_uint32(val); if int > i32::MAX as u32 { (int as i64 - 4_294_967_296i64) as i32 } else { @@ -47,7 +70,7 @@ pub fn to_int32(val: JsValue, string_forge: &StringForge) -> i32 { } } -pub fn to_string(string_forge: &StringForge, val: JsValue) -> String { +pub fn to_string(val: JsValue) -> String { if val.is_int() { return val.as_int().to_string(); } @@ -79,7 +102,8 @@ pub fn to_string(string_forge: &StringForge, val: JsValue) -> String { return "undefined".to_string(); } if val.is_string() { - return string_forge.lookup(val.as_string_index()).unwrap_or_default(); + // SAFETY: val is a string value. + return unsafe { string_data(val) }.to_string(); } if val.is_object() { return "[object]".to_string(); @@ -87,7 +111,7 @@ pub fn to_string(string_forge: &StringForge, val: JsValue) -> String { String::new() } -pub fn to_boolean(val: JsValue, string_forge: &StringForge) -> bool { +pub fn to_boolean(val: JsValue) -> bool { if val.is_undefined() || val.is_null() { return false; } @@ -102,8 +126,8 @@ pub fn to_boolean(val: JsValue, string_forge: &StringForge) -> bool { return !(d == 0.0 || d == -0.0 || d.is_nan()); } if val.is_string() { - let s = string_forge.lookup(val.as_string_index()).unwrap_or_default(); - return !s.is_empty(); + // SAFETY: val is a string value. + return !unsafe { (*val.as_string_ptr()).is_empty() }; } if val.is_object() { return true; @@ -114,7 +138,7 @@ pub fn to_boolean(val: JsValue, string_forge: &StringForge) -> bool { false } -pub fn abstract_eq(lhs: JsValue, rhs: JsValue, string_forge: &StringForge) -> bool { +pub fn abstract_eq(lhs: JsValue, rhs: JsValue) -> bool { if lhs.is_null() && rhs.is_undefined() { return true; } @@ -122,7 +146,7 @@ pub fn abstract_eq(lhs: JsValue, rhs: JsValue, string_forge: &StringForge) -> bo return true; } if lhs.is_string() && rhs.is_string() { - return lhs == rhs; + return string_value_eq(lhs, rhs); } if lhs.is_int() && rhs.is_int() { return lhs.as_int() == rhs.as_int(); @@ -140,16 +164,16 @@ pub fn abstract_eq(lhs: JsValue, rhs: JsValue, string_forge: &StringForge) -> bo return true; } if (lhs.is_int() || lhs.is_double()) && (rhs.is_int() || rhs.is_double()) { - return strict_double_eq(to_number(lhs, string_forge), to_number(rhs, string_forge)); + return strict_double_eq(to_number(lhs), to_number(rhs)); } if (lhs.is_int() || lhs.is_double()) && rhs.is_bool() { - return to_number(lhs, string_forge) == to_number(rhs, string_forge); + return to_number(lhs) == to_number(rhs); } if lhs.is_bool() && (rhs.is_int() || rhs.is_double()) { - return to_number(lhs, string_forge) == to_number(rhs, string_forge); + return to_number(lhs) == to_number(rhs); } if lhs.is_bool() || rhs.is_bool() { - return to_number(lhs, string_forge) == to_number(rhs, string_forge); + return to_number(lhs) == to_number(rhs); } if lhs.is_null() || rhs.is_null() { return false; @@ -177,7 +201,7 @@ pub fn strict_eq(lhs: JsValue, rhs: JsValue) -> bool { return true; } if lhs.is_string() && rhs.is_string() { - return lhs == rhs; + return string_value_eq(lhs, rhs); } if lhs.is_object() && rhs.is_object() { return lhs.as_ptr() == rhs.as_ptr(); @@ -195,14 +219,15 @@ fn strict_double_eq(a: f64, b: f64) -> bool { a == b } -pub fn relational_compare(string_forge: &StringForge, lhs: JsValue, rhs: JsValue) -> Option { +pub fn relational_compare(lhs: JsValue, rhs: JsValue) -> Option { if lhs.is_string() && rhs.is_string() { - let ls = string_forge.lookup(lhs.as_string_index()).unwrap_or_default(); - let rs = string_forge.lookup(rhs.as_string_index()).unwrap_or_default(); + // SAFETY: both are string values. + let ls = unsafe { string_data(lhs) }; + let rs = unsafe { string_data(rhs) }; return Some(ls < rs); } - let l = to_number(lhs, string_forge); - let r = to_number(rhs, string_forge); + let l = to_number(lhs); + let r = to_number(rhs); if l.is_nan() || r.is_nan() { return None; } @@ -264,7 +289,7 @@ pub fn same_value(lhs: JsValue, rhs: JsValue) -> bool { return true; } if lhs.is_string() && rhs.is_string() { - return lhs == rhs; + return string_value_eq(lhs, rhs); } if lhs.is_object() && rhs.is_object() { return lhs.as_ptr() == rhs.as_ptr(); diff --git a/crates/oxide_vm/src/dispatch/arithmetic.rs b/crates/oxide_vm/src/dispatch/arithmetic.rs index 3f8ec75d..9ae4ed09 100644 --- a/crates/oxide_vm/src/dispatch/arithmetic.rs +++ b/crates/oxide_vm/src/dispatch/arithmetic.rs @@ -8,13 +8,13 @@ impl Vm { let lhs = self.coerce_primitive_bounded(self.regs[a], false)?; let rhs = self.coerce_primitive_bounded(self.regs[b], false)?; if lhs.is_string() || rhs.is_string() { - let ls = coercion::to_string(self.kernel_core.string_forge().as_ref(), lhs); - let rs = coercion::to_string(self.kernel_core.string_forge().as_ref(), rhs); + let ls = coercion::to_string(lhs); + let rs = coercion::to_string(rhs); let concat = format!("{ls}{rs}"); - self.regs[rd] = self.intern(&concat); + self.regs[rd] = self.new_string(&concat); } else { - let ln = coercion::to_number(lhs, self.kernel_core.string_forge().as_ref()); - let rn = coercion::to_number(rhs, self.kernel_core.string_forge().as_ref()); + let ln = coercion::to_number(lhs); + let rn = coercion::to_number(rhs); self.regs[rd] = JsValue::float(ln + rn); } Ok(()) diff --git a/crates/oxide_vm/src/dispatch/call.rs b/crates/oxide_vm/src/dispatch/call.rs index 0a6437b1..b2729c01 100644 --- a/crates/oxide_vm/src/dispatch/call.rs +++ b/crates/oxide_vm/src/dispatch/call.rs @@ -45,10 +45,8 @@ impl Vm { (err_val, self.thrown_error_kind(err_val)) } else { let msg = if err_val.is_string() { - self.kernel_core - .string_forge() - .lookup(err_val.as_string_index()) - .unwrap_or_else(|| format!("{err_val}")) + // SAFETY: err_val is a string value. + unsafe { (*err_val.as_string_ptr()).data.clone() } } else { format!("{err_val}") }; diff --git a/crates/oxide_vm/src/dispatch/comparison.rs b/crates/oxide_vm/src/dispatch/comparison.rs index c9587393..7180e096 100644 --- a/crates/oxide_vm/src/dispatch/comparison.rs +++ b/crates/oxide_vm/src/dispatch/comparison.rs @@ -5,37 +5,37 @@ use oxide_types::value::JsValue; impl Vm { #[inline(always)] pub(crate) fn dispatch_eq(&mut self, rd: usize, a: usize, b: usize) { - let eq = coercion::abstract_eq(self.regs[a], self.regs[b], self.kernel_core.string_forge().as_ref()); + let eq = coercion::abstract_eq(self.regs[a], self.regs[b]); self.regs[rd] = JsValue::bool(eq); } #[inline(always)] pub(crate) fn dispatch_neq(&mut self, rd: usize, a: usize, b: usize) { - let ne = !coercion::abstract_eq(self.regs[a], self.regs[b], self.kernel_core.string_forge().as_ref()); + let ne = !coercion::abstract_eq(self.regs[a], self.regs[b]); self.regs[rd] = JsValue::bool(ne); } #[inline(always)] pub(crate) fn dispatch_lt(&mut self, rd: usize, a: usize, b: usize) { - let rel = coercion::relational_compare(self.kernel_core.string_forge().as_ref(), self.regs[a], self.regs[b]); + let rel = coercion::relational_compare(self.regs[a], self.regs[b]); self.regs[rd] = JsValue::bool(rel.unwrap_or(false)); } #[inline(always)] pub(crate) fn dispatch_gt(&mut self, rd: usize, a: usize, b: usize) { - let rel = coercion::relational_compare(self.kernel_core.string_forge().as_ref(), self.regs[b], self.regs[a]); + let rel = coercion::relational_compare(self.regs[b], self.regs[a]); self.regs[rd] = JsValue::bool(rel.unwrap_or(false)); } #[inline(always)] pub(crate) fn dispatch_lte(&mut self, rd: usize, a: usize, b: usize) { - let rel = coercion::relational_compare(self.kernel_core.string_forge().as_ref(), self.regs[b], self.regs[a]); + let rel = coercion::relational_compare(self.regs[b], self.regs[a]); self.regs[rd] = JsValue::bool(!rel.unwrap_or(true)); } #[inline(always)] pub(crate) fn dispatch_gte(&mut self, rd: usize, a: usize, b: usize) { - let rel = coercion::relational_compare(self.kernel_core.string_forge().as_ref(), self.regs[a], self.regs[b]); + let rel = coercion::relational_compare(self.regs[a], self.regs[b]); self.regs[rd] = JsValue::bool(!rel.unwrap_or(true)); } diff --git a/crates/oxide_vm/src/dispatch/control_flow.rs b/crates/oxide_vm/src/dispatch/control_flow.rs index 69331faa..e42df1c6 100644 --- a/crates/oxide_vm/src/dispatch/control_flow.rs +++ b/crates/oxide_vm/src/dispatch/control_flow.rs @@ -6,7 +6,7 @@ impl Vm { #[inline(always)] pub(crate) fn dispatch_and(&mut self, rd: usize, a: usize, b: usize) { let val = self.regs[a]; - if coercion::to_boolean(val, self.kernel_core.string_forge().as_ref()) { + if coercion::to_boolean(val) { self.regs[rd] = self.regs[b]; } else { self.regs[rd] = val; @@ -16,7 +16,7 @@ impl Vm { #[inline(always)] pub(crate) fn dispatch_or(&mut self, rd: usize, a: usize, b: usize) { let val = self.regs[a]; - if coercion::to_boolean(val, self.kernel_core.string_forge().as_ref()) { + if coercion::to_boolean(val) { self.regs[rd] = val; } else { self.regs[rd] = self.regs[b]; @@ -31,7 +31,7 @@ impl Vm { #[inline(always)] pub(crate) fn dispatch_not(&mut self, rd: usize, a: usize) { - let b = !coercion::to_boolean(self.regs[a], self.kernel_core.string_forge().as_ref()); + let b = !coercion::to_boolean(self.regs[a]); self.regs[rd] = JsValue::bool(b); } } diff --git a/crates/oxide_vm/src/dispatch/member.rs b/crates/oxide_vm/src/dispatch/member.rs index 986add06..06931fbb 100644 --- a/crates/oxide_vm/src/dispatch/member.rs +++ b/crates/oxide_vm/src/dispatch/member.rs @@ -110,12 +110,12 @@ impl Vm { let prop_val = self.coerce_primitive_bounded(prop_raw, false)?; let rhs = self.coerce_primitive_bounded(self.regs[a], false)?; let new_val = if prop_val.is_string() || rhs.is_string() { - let ls = coercion::to_string(self.kernel_core.string_forge().as_ref(), prop_val); - let rs = coercion::to_string(self.kernel_core.string_forge().as_ref(), rhs); - self.intern(&format!("{ls}{rs}")) + let ls = coercion::to_string(prop_val); + let rs = coercion::to_string(rhs); + self.new_string(&format!("{ls}{rs}")) } else { - let ln = coercion::to_number(prop_val, self.kernel_core.string_forge().as_ref()); - let rn = coercion::to_number(rhs, self.kernel_core.string_forge().as_ref()); + let ln = coercion::to_number(prop_val); + let rn = coercion::to_number(rhs); JsValue::float(ln + rn) }; let obj = unsafe { &mut *obj_ptr }; diff --git a/crates/oxide_vm/src/dispatch/misc.rs b/crates/oxide_vm/src/dispatch/misc.rs index d8921393..d790ddac 100644 --- a/crates/oxide_vm/src/dispatch/misc.rs +++ b/crates/oxide_vm/src/dispatch/misc.rs @@ -37,6 +37,6 @@ impl Vm { } else { "undefined" }; - self.regs[rd] = self.intern(result); + self.regs[rd] = self.new_string(result); } } diff --git a/crates/oxide_vm/src/dispatch/property.rs b/crates/oxide_vm/src/dispatch/property.rs index b9fbf9cd..348ba942 100644 --- a/crates/oxide_vm/src/dispatch/property.rs +++ b/crates/oxide_vm/src/dispatch/property.rs @@ -61,15 +61,10 @@ impl Vm { fn primitive_property_get(&mut self, val: JsValue, prop_name_si: u32) -> Result, String> { if val.is_string() { - let length_si = self.kernel_core.string_forge().intern("length").0; + let length_si = self.kernel_core.perm_interner().intern("length").0; if prop_name_si == length_si { - let len = self - .kernel_core - .string_forge() - .lookup(val.as_string_index()) - .unwrap_or_default() - .encode_utf16() - .count(); + // SAFETY: val is a string value. + let len = unsafe { (*val.as_string_ptr()).data.encode_utf16().count() }; return Ok(Some(JsValue::int(len as i32))); } let proto_ptr = self.session.builtin_world().string_proto.as_ptr() as *mut JsObject; @@ -245,7 +240,7 @@ impl Vm { }; let prop_name_si = self.property_key_si(self.regs[b]); - if self.kernel_core.string_forge().lookup(prop_name_si).as_deref() == Some("__proto__") { + if self.kernel_core.perm_interner().lookup(prop_name_si) == Some("__proto__") { let proto_value = self.promote_if_needed_for_write_ptr(obj_ptr, self.regs[a]); if self.is_object_prototype(obj_ptr) && !proto_value.is_null() { self.raise_type_error("Object.prototype.__proto__ is immutable")?; @@ -328,7 +323,7 @@ impl Vm { return Ok(()); }; let prop_name_si = self.property_key_si(self.regs[b]); - if self.kernel_core.string_forge().lookup(prop_name_si).as_deref() == Some("__proto__") { + if self.kernel_core.perm_interner().lookup(prop_name_si) == Some("__proto__") { let proto_value = self.promote_if_needed_for_write_ptr(obj_ptr, self.regs[a]); if self.is_object_prototype(obj_ptr) && !proto_value.is_null() { self.raise_type_error("Object.prototype.__proto__ is immutable")?; @@ -366,7 +361,7 @@ impl Vm { return Ok(()); }; let prop_name_si = self.property_key_si(self.regs[a]); - if self.kernel_core.string_forge().lookup(prop_name_si).as_deref() == Some("__proto__") { + if self.kernel_core.perm_interner().lookup(prop_name_si) == Some("__proto__") { let proto_value = self.promote_if_needed_for_write_ptr(obj_ptr, self.regs[b]); if self.is_object_prototype(obj_ptr) && !proto_value.is_null() { self.raise_type_error("Object.prototype.__proto__ is immutable")?; diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index 12c6be19..160b1ad9 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -609,7 +609,7 @@ mod tests { (*obj).set_prop_at(0, JsValue::int(42)); } let old_ptr = vm.promote_object(obj); - let key = vm.kernel_core.string_forge().intern("gcRoot").0; + let key = vm.kernel_core.perm_interner().intern("gcRoot").0; let global_ptr = vm.session.global_object().as_ptr() as *mut JsObject; unsafe { let global = &mut *global_ptr; diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index aa79907b..6ee8f9f1 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -15,7 +15,7 @@ use oxide_kernel::kernel::{KernelCore, KernelSession}; use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_types::error::{JsError, JsErrorKind}; use oxide_types::mem::{Epoch, P}; -use oxide_types::object::{JsObject, NativeFnPtr}; +use oxide_types::object::{JsObject, JsString, NativeFnPtr}; use oxide_types::value::JsValue; pub(crate) const MAX_PROTO_CHAIN_DEPTH: usize = 1024; @@ -151,7 +151,7 @@ pub struct Vm { pub for_in_iters: Vec<*mut ForInIter<'static>>, pub(crate) kernel_core: Arc, pub(crate) session: KernelSession, - pub(crate) interned_strings: Vec, + pub(crate) session_string_ptrs: Vec<*mut JsString>, pub epoch: Epoch, pub(crate) session_epoch: bumpalo::Bump, pub(crate) session_gc: SessionGc, @@ -207,7 +207,7 @@ impl Vm { let method_names = if prefer_string { ["toString", "valueOf"] } else { ["valueOf", "toString"] }; for method_name in method_names { - let method_si = self.kernel_core.string_forge().intern(method_name).0; + let method_si = self.kernel_core.perm_interner().intern(method_name).0; let method = { let obj = unsafe { &*obj_ptr }; self.ordinary_get(obj, method_si, value)? @@ -234,7 +234,7 @@ impl Vm { pub(crate) fn coerce_number_bounded(&mut self, value: JsValue) -> Result { let primitive = self.coerce_primitive_bounded(value, false)?; - Ok(coercion::to_number(primitive, self.kernel_core.string_forge().as_ref())) + Ok(coercion::to_number(primitive)) } pub(crate) fn coerce_int32_bounded(&mut self, value: JsValue) -> Result { @@ -471,14 +471,15 @@ impl Vm { if !val.is_string() { return None; } - self.kernel_core.string_forge().lookup(val.as_string_index()) + // SAFETY: val is a string value; its JsString pointer is alive for its lifetime. + Some(unsafe { (*val.as_string_ptr()).data.clone() }) } pub(crate) fn thrown_error_kind(&self, val: JsValue) -> &'static str { if !val.is_object() { return "Error"; } - let name_si = self.kernel_core.string_forge().intern("name").0; + let name_si = self.kernel_core.perm_interner().intern("name").0; let obj = unsafe { &*val.as_js_object_ptr() }; let Some(name_val) = self.resolve_property(obj, name_si) else { return "Error"; @@ -500,14 +501,16 @@ impl Vm { pub(crate) fn property_key_si(&mut self, val: JsValue) -> u32 { if val.is_string() { - return val.as_string_index(); + // SAFETY: val is a string value; bridge its content to a permanent key id. + let s = unsafe { &(*val.as_string_ptr()).data }; + return self.kernel_core.perm_interner().intern(s).0; } - let key = coercion::to_string(self.kernel_core.string_forge().as_ref(), val); - self.kernel_core.string_forge().intern(&key).0 + let key = coercion::to_string(val); + self.kernel_core.perm_interner().intern(&key).0 } pub(crate) fn array_index_from_property_key(&self, prop_name_si: u32) -> Option { - let key = self.kernel_core.string_forge().lookup(prop_name_si)?; + let key = self.kernel_core.perm_interner().lookup(prop_name_si)?; if key.is_empty() || (key.len() > 1 && key.starts_with('0')) { return None; } @@ -515,7 +518,7 @@ impl Vm { } pub(crate) fn resolve_property(&self, obj: &JsObject, prop_name_si: u32) -> Option { - let length_si = self.kernel_core.string_forge().intern("length").0; + let length_si = self.kernel_core.perm_interner().intern("length").0; if obj.is_array() && prop_name_si == length_si { return Some(JsValue::int(obj.prop_count() as i32)); } @@ -553,7 +556,7 @@ impl Vm { } pub(crate) fn get_own_property_slot(&self, obj: &JsObject, prop_name_si: u32) -> Option { - let length_si = self.kernel_core.string_forge().intern("length").0; + let length_si = self.kernel_core.perm_interner().intern("length").0; if obj.is_array() && prop_name_si == length_si { return None; } @@ -664,7 +667,7 @@ impl Vm { let function_name = self.sub_modules[sub_idx] .function_name .as_deref() - .map(|name| self.kernel_core.string_forge().intern(name).0) + .map(|name| self.kernel_core.perm_interner().intern(name).0) .unwrap_or(0); self.frames.push(CallFrame { @@ -684,7 +687,7 @@ impl Vm { self.bytecode = sub_bytecode; self.constants = converted_sub_constants; for (name, reg) in &self.sub_modules[sub_idx].builtin_reg_map.clone() { - let si = self.kernel_core.string_forge().intern(name.as_str()).0; + let si = self.kernel_core.perm_interner().intern(name.as_str()).0; let global = self.session.global_object(); if let Some(pos) = self.kernel_core.shape_forge().lookup_position(global.shape_id(), si) { self.regs[*reg as usize] = global.get_prop_at(pos); @@ -826,7 +829,7 @@ impl Vm { } OpCode::JMP_IF_FALSE => { - let cond = coercion::to_boolean(self.regs[rd], self.kernel_core.string_forge().as_ref()); + let cond = coercion::to_boolean(self.regs[rd]); if !cond { let offset = opcode::offset16(instr) as isize; self.pc = ((self.pc as isize) + offset - 1) as usize; @@ -834,7 +837,7 @@ impl Vm { } OpCode::JMP_IF_TRUE => { - let cond = coercion::to_boolean(self.regs[rd], self.kernel_core.string_forge().as_ref()); + let cond = coercion::to_boolean(self.regs[rd]); if cond { let offset = opcode::offset16(instr) as isize; self.pc = ((self.pc as isize) + offset - 1) as usize; @@ -987,7 +990,7 @@ impl Vm { function_name: self.sub_modules[sub_idx] .function_name .as_deref() - .map(|name| self.kernel_core.string_forge().intern(name).0) + .map(|name| self.kernel_core.perm_interner().intern(name).0) .unwrap_or(0), caller_reg_limit, saved_regs, @@ -1003,7 +1006,7 @@ impl Vm { self.bytecode = sub_bytecode; self.constants = converted_sub_constants; for (name, reg) in &self.sub_modules[sub_idx].builtin_reg_map { - let si = self.kernel_core.string_forge().intern(name.as_str()).0; + let si = self.kernel_core.perm_interner().intern(name.as_str()).0; let global = self.session.global_object(); if let Some(pos) = self.kernel_core.shape_forge().lookup_position(global.shape_id(), si) { self.regs[*reg as usize] = global.get_prop_at(pos); @@ -1107,13 +1110,13 @@ impl Vm { let lhs = self.coerce_primitive_bounded(self.regs[rd], false)?; let rhs = self.coerce_primitive_bounded(self.regs[a], false)?; if lhs.is_string() || rhs.is_string() { - let ls = coercion::to_string(self.kernel_core.string_forge().as_ref(), lhs); - let rs = coercion::to_string(self.kernel_core.string_forge().as_ref(), rhs); + let ls = coercion::to_string(lhs); + let rs = coercion::to_string(rhs); let concat = format!("{ls}{rs}"); - self.regs[rd] = self.intern(&concat); + self.regs[rd] = self.new_string(&concat); } else { - let ln = coercion::to_number(lhs, self.kernel_core.string_forge().as_ref()); - let rn = coercion::to_number(rhs, self.kernel_core.string_forge().as_ref()); + let ln = coercion::to_number(lhs); + let rn = coercion::to_number(rhs); self.regs[rd] = JsValue::float(ln + rn); } } @@ -1357,7 +1360,7 @@ mod tests { if !this_val.is_object() { return NativeResult::Ok(JsValue::undefined()); } - let marker_si = vm.kernel_core.string_forge().intern("marker").0; + let marker_si = vm.kernel_core.perm_interner().intern("marker").0; let obj = unsafe { &*this_val.as_js_object_ptr() }; NativeResult::Ok(vm.resolve_property(obj, marker_si).unwrap_or(JsValue::undefined())) } @@ -1368,7 +1371,7 @@ mod tests { if !this_val.is_object() { return NativeResult::Ok(JsValue::undefined()); } - let marker_si = vm.kernel_core.string_forge().intern("marker").0; + let marker_si = vm.kernel_core.perm_interner().intern("marker").0; let obj = unsafe { &mut *this_val.as_js_object_ptr() }; vm.set_or_create_prop_value(obj, marker_si, value); NativeResult::Ok(JsValue::undefined()) @@ -1399,7 +1402,7 @@ mod tests { } fn add_accessor(vm: &mut Vm, obj_val: JsValue, name: &str, get: JsValue, set: JsValue) { - let si = vm.kernel_core.string_forge().intern(name).0; + let si = vm.kernel_core.perm_interner().intern(name).0; let obj = unsafe { &mut *obj_val.as_js_object_ptr() }; let shape_id = vm.kernel_core.shape_forge().make_shape(obj.shape_id(), si); obj.set_shape_id(shape_id); @@ -1409,7 +1412,7 @@ mod tests { } fn set_data(vm: &mut Vm, obj_val: JsValue, name: &str, val: JsValue) { - let si = vm.kernel_core.string_forge().intern(name).0; + let si = vm.kernel_core.perm_interner().intern(name).0; let obj = unsafe { &mut *obj_val.as_js_object_ptr() }; vm.set_or_create_prop_value(obj, si, val); } @@ -1568,7 +1571,7 @@ mod tests { let getter = native_function(&mut vm, native_return_7); add_accessor(&mut vm, obj_val, "x", getter, JsValue::undefined()); - let x_si = vm.kernel_core.string_forge().intern("x").0; + let x_si = vm.kernel_core.perm_interner().intern("x").0; let obj = unsafe { &*obj_val.as_js_object_ptr() }; let value = vm.ordinary_get(obj, x_si, obj_val).expect("getter"); assert_eq!(value, JsValue::int(7)); @@ -1581,11 +1584,11 @@ mod tests { let setter = native_function(&mut vm, native_set_marker); add_accessor(&mut vm, obj_val, "x", JsValue::undefined(), setter); - let x_si = vm.kernel_core.string_forge().intern("x").0; + let x_si = vm.kernel_core.perm_interner().intern("x").0; let obj = unsafe { &mut *obj_val.as_js_object_ptr() }; vm.ordinary_set(obj, x_si, JsValue::int(9), obj_val).expect("setter"); - let marker_si = vm.kernel_core.string_forge().intern("marker").0; + let marker_si = vm.kernel_core.perm_interner().intern("marker").0; let obj = unsafe { &*obj_val.as_js_object_ptr() }; assert_eq!(vm.resolve_property(obj, marker_si), Some(JsValue::int(9))); } @@ -1602,7 +1605,7 @@ mod tests { (*child_val.as_js_object_ptr()).set_proto(proto_val).expect("proto"); } - let x_si = vm.kernel_core.string_forge().intern("x").0; + let x_si = vm.kernel_core.perm_interner().intern("x").0; let child = unsafe { &*child_val.as_js_object_ptr() }; let value = vm.ordinary_get(child, x_si, child_val).expect("getter"); assert_eq!(value, JsValue::int(42)); @@ -1619,11 +1622,11 @@ mod tests { (*child_val.as_js_object_ptr()).set_proto(proto_val).expect("proto"); } - let x_si = vm.kernel_core.string_forge().intern("x").0; + let x_si = vm.kernel_core.perm_interner().intern("x").0; let child = unsafe { &mut *child_val.as_js_object_ptr() }; vm.ordinary_set(child, x_si, JsValue::int(12), child_val).expect("setter"); - let marker_si = vm.kernel_core.string_forge().intern("marker").0; + let marker_si = vm.kernel_core.perm_interner().intern("marker").0; let child = unsafe { &*child_val.as_js_object_ptr() }; let proto = unsafe { &*proto_val.as_js_object_ptr() }; assert_eq!(vm.resolve_property(child, marker_si), Some(JsValue::int(12))); @@ -1643,11 +1646,11 @@ mod tests { (*child_val.as_js_object_ptr()).set_proto(proto_val).expect("proto"); } - let x_si = vm.kernel_core.string_forge().intern("x").0; + let x_si = vm.kernel_core.perm_interner().intern("x").0; let child = unsafe { &mut *child_val.as_js_object_ptr() }; vm.ordinary_set(child, x_si, JsValue::int(15), child_val).expect("setter"); - let marker_si = vm.kernel_core.string_forge().intern("marker").0; + let marker_si = vm.kernel_core.perm_interner().intern("marker").0; let child = unsafe { &*child_val.as_js_object_ptr() }; assert_eq!(vm.resolve_property(child, marker_si), Some(JsValue::int(15))); } @@ -1658,7 +1661,7 @@ mod tests { let obj_val = plain_object(&mut vm); set_data(&mut vm, obj_val, "x", JsValue::int(1)); - let x_si = vm.kernel_core.string_forge().intern("x").0; + let x_si = vm.kernel_core.perm_interner().intern("x").0; let obj = unsafe { &mut *obj_val.as_js_object_ptr() }; assert!(!obj.has_prop_meta()); assert_eq!(vm.ordinary_get(obj, x_si, obj_val).expect("get"), JsValue::int(1)); diff --git a/crates/oxide_vm/src/vm_dispatch_misc.rs b/crates/oxide_vm/src/vm_dispatch_misc.rs index 5c72005e..1e1c9c48 100644 --- a/crates/oxide_vm/src/vm_dispatch_misc.rs +++ b/crates/oxide_vm/src/vm_dispatch_misc.rs @@ -42,7 +42,7 @@ impl Vm { JsValue::from_js_object(proto_ptr), )); - let proto_si = self.kernel_core.string_forge().intern("prototype").0; + let proto_si = self.kernel_core.perm_interner().intern("prototype").0; if let Some(proto_val) = self.resolve_property(ctor_obj, proto_si) { if proto_val.is_object() { let new_obj_mut = unsafe { &mut *new_obj }; @@ -123,7 +123,7 @@ impl Vm { function_name: self.sub_modules[sub_idx] .function_name .as_deref() - .map(|name| self.kernel_core.string_forge().intern(name).0) + .map(|name| self.kernel_core.perm_interner().intern(name).0) .unwrap_or(0), caller_reg_limit, saved_regs, @@ -140,7 +140,7 @@ impl Vm { self.constants = converted_sub_constants; for (name, reg) in &self.sub_modules[sub_idx].builtin_reg_map { - let si = self.kernel_core.string_forge().intern(name.as_str()).0; + let si = self.kernel_core.perm_interner().intern(name.as_str()).0; let global = self.session.global_object(); if let Some(pos) = self.kernel_core.shape_forge().lookup_position(global.shape_id(), si) { self.regs[*reg as usize] = global.get_prop_at(pos); @@ -173,10 +173,8 @@ impl Vm { let reg = (seg & 0x7F) as u8; let val = self.regs[reg as usize]; let s = if val.is_string() { - self.kernel_core - .string_forge() - .lookup(val.as_string_index()) - .unwrap_or_default() + // SAFETY: val is a string value. + unsafe { (*val.as_string_ptr()).data.clone() } } else { format!("{}", val) }; @@ -186,18 +184,14 @@ impl Vm { if const_idx < self.constants.len() { let val = self.constants[const_idx]; if val.is_string() { - let s = self - .kernel_core - .string_forge() - .lookup(val.as_string_index()) - .unwrap_or_default(); + // SAFETY: val is a string value. + let s = unsafe { (*val.as_string_ptr()).data.clone() }; result.push_str(&s); } } } } - let si = self.kernel_core.string_forge().intern(&result).0; - self.regs[rd] = JsValue::string(si, 0); + self.regs[rd] = self.new_string(&result); } pub(crate) fn dispatch_instanceof(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { @@ -213,7 +207,7 @@ impl Vm { } let rhs_obj = unsafe { &*rhs_val.as_js_object_ptr() }; - let proto_si = self.kernel_core.string_forge().intern("prototype").0; + let proto_si = self.kernel_core.perm_interner().intern("prototype").0; let ctor_proto = self.resolve_property(rhs_obj, proto_si); let ctor_proto_ptr = match ctor_proto { @@ -297,8 +291,9 @@ impl Vm { .map(|meta| meta.attributes.enumerable()) .unwrap_or(PropAttributes::DEFAULT_DATA.enumerable()); if enumerable { - let hash = self.kernel_core.string_forge().get_hash(shape.property_name).unwrap_or(0); - keys_vec.push(JsValue::string(shape.property_name, hash)); + keys_vec.push(JsValue::perm_string( + self.kernel_core.perm_interner().string_ptr(shape.property_name), + )); } } cursor = shape.parent; @@ -368,7 +363,7 @@ impl Vm { } let iter_obj = unsafe { &*iterator.as_js_object_ptr() }; - let next_si = self.kernel_core.string_forge().intern("next").0; + let next_si = self.kernel_core.perm_interner().intern("next").0; let next_fn = self.ordinary_get(iter_obj, next_si, iterator)?; let result = self.call_function_sync(next_fn, iterator, &[])?; if !result.is_object() { @@ -376,9 +371,9 @@ impl Vm { } let result_obj = unsafe { &*result.as_js_object_ptr() }; - let done_si = self.kernel_core.string_forge().intern("done").0; + let done_si = self.kernel_core.perm_interner().intern("done").0; let done_val = self.ordinary_get(result_obj, done_si, result)?; - let done = crate::coercion::to_boolean(done_val, self.kernel_core.string_forge().as_ref()); + let done = crate::coercion::to_boolean(done_val); self.last_for_of_result = result; self.regs[rd] = JsValue::bool(!done); Ok(()) @@ -391,7 +386,7 @@ impl Vm { return Ok(()); } let result_obj = unsafe { &*result.as_js_object_ptr() }; - let value_si = self.kernel_core.string_forge().intern("value").0; + let value_si = self.kernel_core.perm_interner().intern("value").0; self.regs[rd] = self.ordinary_get(result_obj, value_si, result)?; Ok(()) } @@ -404,7 +399,7 @@ impl Vm { return Ok(()); } let iter_obj = unsafe { &*iterator.as_js_object_ptr() }; - let return_si = self.kernel_core.string_forge().intern("return").0; + let return_si = self.kernel_core.perm_interner().intern("return").0; let return_fn = self.ordinary_get(iter_obj, return_si, iterator)?; if return_fn.is_object() { let return_obj = unsafe { &*return_fn.as_js_object_ptr() }; @@ -427,7 +422,8 @@ impl Vm { .get(excluded_idx) .and_then(|v| { if v.is_string() { - self.kernel_core.string_forge().lookup(v.as_string_index()) + // SAFETY: v is a string constant value. + Some(unsafe { (*v.as_string_ptr()).data.clone() }) } else { None } @@ -450,8 +446,8 @@ impl Vm { break; }; if shape.property_name != u32::MAX && !is_private_name_key(shape.property_name) { - if let Some(name) = self.kernel_core.string_forge().lookup(shape.property_name) { - if !excluded.contains(name.as_str()) { + if let Some(name) = self.kernel_core.perm_interner().lookup(shape.property_name) { + if !excluded.contains(name) { if let Some(pos) = self .kernel_core .shape_forge() diff --git a/crates/oxide_vm/src/vm_pool.rs b/crates/oxide_vm/src/vm_pool.rs index daf04366..7675e686 100644 --- a/crates/oxide_vm/src/vm_pool.rs +++ b/crates/oxide_vm/src/vm_pool.rs @@ -22,7 +22,6 @@ pub struct VmGuard { vm: Option, pool: Arc, dirty: bool, - interned_strings: Vec, } impl VmPool { @@ -55,7 +54,6 @@ impl VmPool { vm: Some(vm), pool: Arc::clone(self), dirty: false, - interned_strings: Vec::new(), }; } @@ -72,7 +70,6 @@ impl VmPool { vm: Some(vm), pool: Arc::clone(self), dirty: false, - interned_strings: Vec::new(), }; } @@ -86,7 +83,6 @@ impl VmPool { vm: Some(vm), pool: Arc::clone(self), dirty: false, - interned_strings: Vec::new(), }; } } @@ -115,10 +111,6 @@ impl Drop for VmGuard { let new_vm = self.pool.replace_vm(); inner.available.push(new_vm); } else { - for &idx in &self.interned_strings { - self.pool.kernel_core.string_forge().decref(idx); - } - vm.full_reset(); inner.available.push(vm); } diff --git a/crates/oxide_vm/src/vm_props.rs b/crates/oxide_vm/src/vm_props.rs index 93e9caa5..567fc5d0 100644 --- a/crates/oxide_vm/src/vm_props.rs +++ b/crates/oxide_vm/src/vm_props.rs @@ -19,7 +19,7 @@ impl Vm { fn ordinary_get_inner( &mut self, obj: &JsObject, prop_name_si: u32, receiver: JsValue, target_reg: Option, ) -> Result { - let length_si = self.kernel_core.string_forge().intern("length").0; + let length_si = self.kernel_core.perm_interner().intern("length").0; let mut current = Some(obj); let mut depth = 0usize; while let Some(obj) = current { diff --git a/crates/oxide_vm/src/vm_runtime.rs b/crates/oxide_vm/src/vm_runtime.rs index e0d8a592..e8cf273f 100644 --- a/crates/oxide_vm/src/vm_runtime.rs +++ b/crates/oxide_vm/src/vm_runtime.rs @@ -64,7 +64,7 @@ impl Vm { self.regs[254] = if sub.is_arrow { callee_obj.captured_this() } else { receiver }; self.regs[255] = JsValue::undefined(); for (name, reg) in &sub.builtin_reg_map { - let si = self.kernel_core.string_forge().intern(name.as_str()).0; + let si = self.kernel_core.perm_interner().intern(name.as_str()).0; let global = self.session.global_object(); if let Some(pos) = self.kernel_core.shape_forge().lookup_position(global.shape_id(), si) { self.regs[*reg as usize] = global.get_prop_at(pos); @@ -144,7 +144,7 @@ impl Vm { self.active_reg_limit = self.root_reg_limit; for (name, reg) in &module.builtin_reg_map { - let si = self.kernel_core.string_forge().intern(name.as_str()).0; + let si = self.kernel_core.perm_interner().intern(name.as_str()).0; let global = self.session.global_object(); if let Some(pos) = self.kernel_core.shape_forge().lookup_position(global.shape_id(), si) { self.regs[*reg as usize] = global.get_prop_at(pos); diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index ad575273..c79efd31 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -10,7 +10,7 @@ use oxide_kernel::kernel::{KernelConfig, KernelCore, KernelSession}; use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_types::error::JsError; use oxide_types::mem::{Epoch, P}; -use oxide_types::object::{JsObject, PropAttributes}; +use oxide_types::object::{JsObject, JsString, PropAttributes}; use oxide_types::value::JsValue; impl Vm { @@ -28,7 +28,7 @@ impl Vm { for_in_iters: Vec::new(), kernel_core: core, session, - interned_strings: Vec::new(), + session_string_ptrs: Vec::new(), epoch: Epoch::new(), session_epoch: bumpalo::Bump::new(), session_gc: crate::session_gc::SessionGc::new(), @@ -74,7 +74,7 @@ impl Vm { for_in_iters: Vec::new(), kernel_core: core, session, - interned_strings: Vec::new(), + session_string_ptrs: Vec::new(), epoch: Epoch::new(), session_epoch: bumpalo::Bump::new(), session_gc: crate::session_gc::SessionGc::new(), @@ -141,7 +141,7 @@ impl Vm { self.session_object_ptrs.clear(); self.session_bytes_allocated = 0; self.session_gc = crate::session_gc::SessionGc::new(); - self.interned_strings.clear(); + self.free_session_string_heap_data(); self.symbol_counter = 0; self.symbol_descriptions.clear(); self.symbol_registry.clear(); @@ -189,15 +189,44 @@ impl Vm { self.free_epoch_object_heap_data(); self.epoch.reset(); self.epoch_object_ptrs.clear(); - self.interned_strings.clear(); self.root_reg_limit = 0; self.active_reg_limit = 0; } - pub fn intern(&mut self, s: &str) -> JsValue { - let (idx, hash) = self.kernel_core.string_forge().intern(s); - self.interned_strings.push(idx); - JsValue::string(idx, hash) + pub fn new_string(&mut self, s: &str) -> JsValue { + let ptr = Box::into_raw(Box::new(JsString::new(s.to_string()))); + self.session_string_ptrs.push(ptr); + self.session_bytes_allocated += std::mem::size_of::() + s.len(); + JsValue::string(ptr) + } + + pub fn intern_key(&self, s: &str) -> u32 { + self.kernel_core.perm_interner().intern(s).0 + } + + /// Intern a compile-time string literal as a permanent, process-lifetime + /// `JsString` value shared across sessions. Source literals and RegExp + /// source/flags recur (templated/repetitive code) and are immutable, so + /// sharing them via `PermInterner` restores cross-session string reuse — + /// without interning transient computed values, which stay session-heap + /// (`new_string`) so they remain collectable. + pub fn perm_string(&self, s: &str) -> JsValue { + let id = self.kernel_core.perm_interner().intern(s).0; + JsValue::perm_string(self.kernel_core.perm_interner().string_ptr(id)) + } + + /// Drop all session-heap `JsString` values. Called only on full isolation reset + /// (`full_reset` / `clear_full_reset_state`), where no surviving session object + /// can reference them. The lighter `reset()` deliberately keeps them alive, + /// mirroring session-object survival across evals. + fn free_session_string_heap_data(&mut self) { + for ptr in self.session_string_ptrs.drain(..) { + // SAFETY: each ptr came from Box::into_raw(Box::new(JsString)) in new_string + // and is dropped exactly once here. + unsafe { + drop(Box::from_raw(ptr)); + } + } } /// Create a function JsObject for a BytecodeFunc constant. @@ -230,7 +259,7 @@ impl Vm { self.epoch_object_ptrs.push(prototype_obj); let prototype_val = JsValue::from_js_object(prototype_obj); - let constructor_si = self.kernel_core.string_forge().intern("constructor").0; + let constructor_si = self.kernel_core.perm_interner().intern("constructor").0; let constructor_shape = self.kernel_core.shape_forge().make_shape(EMPTY_SHAPE_ID, constructor_si); let prototype = unsafe { &mut *prototype_obj }; prototype.set_shape_id(constructor_shape); @@ -238,7 +267,7 @@ impl Vm { prototype.set_data_meta(constructor_pos, PropAttributes::new(true, false, true)); prototype.bump_generation(); - let prototype_si = self.kernel_core.string_forge().intern("prototype").0; + let prototype_si = self.kernel_core.perm_interner().intern("prototype").0; let func = unsafe { &mut *obj_ptr }; let prototype_shape = self.kernel_core.shape_forge().make_shape(func.shape_id(), prototype_si); func.set_shape_id(prototype_shape); @@ -255,8 +284,8 @@ impl Vm { } if val.is_object() { let obj = unsafe { &*val.as_js_object_ptr() }; - let name_si = self.kernel_core.string_forge().intern("name").0; - let message_si = self.kernel_core.string_forge().intern("message").0; + let name_si = self.kernel_core.perm_interner().intern("name").0; + let message_si = self.kernel_core.perm_interner().intern("message").0; let name = self .resolve_property(obj, name_si) .and_then(|v| self.lookup_str(v)) @@ -274,7 +303,7 @@ impl Vm { match constant { Constant::Number(v) => Ok(JsValue::float(*v)), Constant::Int(v) => Ok(JsValue::int(*v)), - Constant::String(s) => Ok(self.intern(s)), + Constant::String(s) => Ok(self.perm_string(s)), Constant::Boolean(b) => Ok(JsValue::bool(*b)), Constant::Null => Ok(JsValue::null()), Constant::Undefined => Ok(JsValue::undefined()), @@ -301,10 +330,8 @@ impl Vm { )) } Constant::RegExp(pattern, flags) => { - let pat_si = self.kernel_core.string_forge().intern(pattern).0; - let flags_si = self.kernel_core.string_forge().intern(flags).0; - let pat_val = JsValue::string(pat_si, 0); - let flags_val = JsValue::string(flags_si, 0); + let pat_val = self.perm_string(pattern); + let flags_val = self.perm_string(flags); let ctor_ptr = self.session.builtin_world().regexp_constructor.as_ptr() as *mut JsObject; let ctor = unsafe { &*ctor_ptr }; @@ -347,7 +374,7 @@ mod tests { fn global_prop_opt(vm: &Vm, name: &str) -> Option { let global = vm.session.global_object(); - let si = vm.kernel_core.string_forge().intern(name).0; + let si = vm.kernel_core.perm_interner().intern(name).0; vm.kernel_core .shape_forge() .lookup_position(global.shape_id(), si) @@ -418,7 +445,7 @@ mod tests { global_prop(&vm, "Array").as_js_object_ptr(), vm.session.builtin_world().array_constructor.as_ptr() as *mut JsObject )); - let constructor_si = vm.kernel_core.string_forge().intern("constructor").0; + let constructor_si = vm.kernel_core.perm_interner().intern("constructor").0; let array_proto = &*vm.session.builtin_world().array_proto; let constructor = vm .resolve_property(array_proto, constructor_si) diff --git a/crates/oxide_vm/tests/arrow_tests.rs b/crates/oxide_vm/tests/arrow_tests.rs index f7a0c834..c68d9673 100644 --- a/crates/oxide_vm/tests/arrow_tests.rs +++ b/crates/oxide_vm/tests/arrow_tests.rs @@ -37,10 +37,7 @@ fn eval_val(source: &str) -> (Vm, Result) { fn to_str(vm: &Vm, val: JsValue) -> String { if val.is_string() { - vm.kernel_core() - .string_forge() - .lookup(val.as_string_index()) - .unwrap_or_default() + vm.lookup_str(val).unwrap_or_default() } else { format!("{}", val) } diff --git a/crates/oxide_vm/tests/builtins_array_buffer_tests.rs b/crates/oxide_vm/tests/builtins_array_buffer_tests.rs index 70360a4d..97464e2b 100644 --- a/crates/oxide_vm/tests/builtins_array_buffer_tests.rs +++ b/crates/oxide_vm/tests/builtins_array_buffer_tests.rs @@ -10,10 +10,7 @@ fn eval(vm: &mut Vm, source: &str) -> Result { } fn to_str(vm: &Vm, val: JsValue) -> String { - vm.kernel_core() - .string_forge() - .lookup(val.as_string_index()) - .unwrap_or_default() + vm.lookup_str(val).unwrap_or_default() } #[test] diff --git a/crates/oxide_vm/tests/builtins_array_tests.rs b/crates/oxide_vm/tests/builtins_array_tests.rs index df36ee6f..78822112 100644 --- a/crates/oxide_vm/tests/builtins_array_tests.rs +++ b/crates/oxide_vm/tests/builtins_array_tests.rs @@ -13,10 +13,7 @@ fn eval(source: &str) -> Result<(Vm, JsValue), String> { } fn to_str(vm: &Vm, val: JsValue) -> String { - vm.kernel_core() - .string_forge() - .lookup(val.as_string_index()) - .unwrap_or_default() + vm.lookup_str(val).unwrap_or_default() } fn assert_num_eq(val: JsValue, expected: f64) { diff --git a/crates/oxide_vm/tests/builtins_boolean_tests.rs b/crates/oxide_vm/tests/builtins_boolean_tests.rs index c5b6e8b2..b11684f9 100644 --- a/crates/oxide_vm/tests/builtins_boolean_tests.rs +++ b/crates/oxide_vm/tests/builtins_boolean_tests.rs @@ -14,10 +14,7 @@ fn eval(vm: &mut Vm, source: &str) -> Result { } fn str_val(vm: &Vm, val: JsValue) -> String { - vm.kernel_core() - .string_forge() - .lookup(val.as_string_index()) - .unwrap_or_default() + vm.lookup_str(val).unwrap_or_default() } // -- direct native fn tests -- @@ -47,7 +44,7 @@ fn tboolean_call_true_false() { #[test] fn tboolean_call_empty_string() { let mut vm = Vm::new(); - let empty = vm.intern(""); + let empty = vm.new_string(""); assert!(!bool_ctor_call(&mut vm, empty).as_bool()); } diff --git a/crates/oxide_vm/tests/builtins_data_view_tests.rs b/crates/oxide_vm/tests/builtins_data_view_tests.rs index 0dc90999..ebe923f6 100644 --- a/crates/oxide_vm/tests/builtins_data_view_tests.rs +++ b/crates/oxide_vm/tests/builtins_data_view_tests.rs @@ -10,10 +10,7 @@ fn eval(vm: &mut Vm, source: &str) -> Result { } fn to_str(vm: &Vm, val: JsValue) -> String { - vm.kernel_core() - .string_forge() - .lookup(val.as_string_index()) - .unwrap_or_default() + vm.lookup_str(val).unwrap_or_default() } #[test] diff --git a/crates/oxide_vm/tests/builtins_date_tests.rs b/crates/oxide_vm/tests/builtins_date_tests.rs index 55f3d741..6be7816b 100644 --- a/crates/oxide_vm/tests/builtins_date_tests.rs +++ b/crates/oxide_vm/tests/builtins_date_tests.rs @@ -9,10 +9,7 @@ fn eval(vm: &mut Vm, source: &str) -> Result String { - vm.kernel_core() - .string_forge() - .lookup(val.as_string_index()) - .unwrap_or_default() + vm.lookup_str(val).unwrap_or_default() } // -- static methods -- diff --git a/crates/oxide_vm/tests/builtins_error_tests.rs b/crates/oxide_vm/tests/builtins_error_tests.rs index 3ddab65f..47a4eb12 100644 --- a/crates/oxide_vm/tests/builtins_error_tests.rs +++ b/crates/oxide_vm/tests/builtins_error_tests.rs @@ -25,7 +25,7 @@ fn error_constructor_creates_object() { #[test] fn error_has_message() { let mut vm = make_vm(); - let msg = vm.intern("test message"); + let msg = vm.new_string("test message"); vm.set_reg(1, msg); let result = error::error_constructor(&mut vm, &[0u8, 1u8]).unwrap(); assert!(result.is_object()); diff --git a/crates/oxide_vm/tests/builtins_function_tests.rs b/crates/oxide_vm/tests/builtins_function_tests.rs index 0dfcb381..ea1bb17f 100644 --- a/crates/oxide_vm/tests/builtins_function_tests.rs +++ b/crates/oxide_vm/tests/builtins_function_tests.rs @@ -34,11 +34,7 @@ fn function_bind_creates_wrapper() { fn function_constructor_is_global() { let mut vm = Vm::new(); let result = eval(&mut vm, "typeof Function").unwrap(); - let rendered = vm - .kernel_core() - .string_forge() - .lookup(result.as_string_index()) - .unwrap_or_default(); + let rendered = vm.lookup_str(result).unwrap_or_default(); assert_eq!(rendered, "function"); } @@ -86,11 +82,7 @@ fn function_to_string_includes_function() { let mut vm = Vm::new(); let result = eval(&mut vm, "Math.max.toString()").unwrap(); assert!(result.is_string()); - let rendered = vm - .kernel_core() - .string_forge() - .lookup(result.as_string_index()) - .unwrap_or_default(); + let rendered = vm.lookup_str(result).unwrap_or_default(); assert_eq!(rendered, "function max() { [native code] }"); } diff --git a/crates/oxide_vm/tests/builtins_globals_tests.rs b/crates/oxide_vm/tests/builtins_globals_tests.rs index 4f3aadc3..5271d624 100644 --- a/crates/oxide_vm/tests/builtins_globals_tests.rs +++ b/crates/oxide_vm/tests/builtins_globals_tests.rs @@ -21,10 +21,7 @@ fn eval_with_vm(source: &str) -> (Vm, Result) { } fn str_val(vm: &Vm, val: JsValue) -> String { - vm.kernel_core() - .string_forge() - .lookup(val.as_string_index()) - .unwrap_or_default() + vm.lookup_str(val).unwrap_or_default() } #[test] diff --git a/crates/oxide_vm/tests/builtins_iterator_tests.rs b/crates/oxide_vm/tests/builtins_iterator_tests.rs index 597d5d1a..9563c21a 100644 --- a/crates/oxide_vm/tests/builtins_iterator_tests.rs +++ b/crates/oxide_vm/tests/builtins_iterator_tests.rs @@ -10,10 +10,7 @@ fn eval(vm: &mut Vm, source: &str) -> Result { } fn to_str(vm: &Vm, val: JsValue) -> String { - vm.kernel_core() - .string_forge() - .lookup(val.as_string_index()) - .unwrap_or_default() + vm.lookup_str(val).unwrap_or_default() } #[test] diff --git a/crates/oxide_vm/tests/builtins_map_tests.rs b/crates/oxide_vm/tests/builtins_map_tests.rs index 972a2e0d..ad3ee11e 100644 --- a/crates/oxide_vm/tests/builtins_map_tests.rs +++ b/crates/oxide_vm/tests/builtins_map_tests.rs @@ -11,10 +11,7 @@ fn eval(vm: &mut Vm, source: &str) -> Result { } fn str_val(vm: &Vm, val: JsValue) -> String { - vm.kernel_core() - .string_forge() - .lookup(val.as_string_index()) - .unwrap_or_default() + vm.lookup_str(val).unwrap_or_default() } // -- direct native fn tests -- diff --git a/crates/oxide_vm/tests/builtins_number_tests.rs b/crates/oxide_vm/tests/builtins_number_tests.rs index 824bec26..846c5aac 100644 --- a/crates/oxide_vm/tests/builtins_number_tests.rs +++ b/crates/oxide_vm/tests/builtins_number_tests.rs @@ -76,11 +76,7 @@ fn number_constructor() { fn to_string_of_number() { let mut vm = Vm::new(); let result = eval(&mut vm, "var n = 42; n.toString()").unwrap(); - let s = vm - .kernel_core() - .string_forge() - .lookup(result.as_string_index()) - .unwrap_or_default(); + let s = vm.lookup_str(result).unwrap_or_default(); assert_eq!(s, "42"); } @@ -88,11 +84,7 @@ fn to_string_of_number() { fn to_fixed_of_number() { let mut vm = Vm::new(); let result = eval(&mut vm, "var n = 3.14159; n.toFixed(2)").unwrap(); - let s = vm - .kernel_core() - .string_forge() - .lookup(result.as_string_index()) - .unwrap_or_default(); + let s = vm.lookup_str(result).unwrap_or_default(); assert_eq!(s, "3.14"); } @@ -121,11 +113,7 @@ fn number_epsilon_is_positive() { fn number_to_exponential() { let mut vm = Vm::new(); let result = eval(&mut vm, "var n = 123.456; n.toExponential(2)").unwrap(); - let s = vm - .kernel_core() - .string_forge() - .lookup(result.as_string_index()) - .unwrap_or_default(); + let s = vm.lookup_str(result).unwrap_or_default(); assert_eq!(s, "1.23e2"); } @@ -155,7 +143,7 @@ fn boxed_number_valueof_preserves_int_and_float() { fn boxed_number_is_object_and_call_stays_primitive() { let mut vm = Vm::new(); let ty = eval(&mut vm, "typeof new Number(5)").unwrap(); - assert_eq!(vm.kernel_core().string_forge().lookup(ty.as_string_index()).unwrap(), "object"); + assert_eq!(vm.lookup_str(ty).unwrap(), "object"); let n = eval(&mut vm, "Number('7')").unwrap(); assert_eq!(n.as_int(), 7); diff --git a/crates/oxide_vm/tests/builtins_object_tests.rs b/crates/oxide_vm/tests/builtins_object_tests.rs index a38c1041..4d39a7cd 100644 --- a/crates/oxide_vm/tests/builtins_object_tests.rs +++ b/crates/oxide_vm/tests/builtins_object_tests.rs @@ -63,11 +63,7 @@ fn object_assign_copies() { fn object_assign_boxes_primitive_target() { let (_vm, result) = eval("var result = Object.assign('a'); result.valueOf()").unwrap(); assert!(result.is_string()); - let rendered = _vm - .kernel_core() - .string_forge() - .lookup(result.as_string_index()) - .unwrap_or_default(); + let rendered = _vm.lookup_str(result).unwrap_or_default(); assert_eq!(rendered, "a"); } diff --git a/crates/oxide_vm/tests/builtins_reflect_tests.rs b/crates/oxide_vm/tests/builtins_reflect_tests.rs index 3f1fceaa..70b1544d 100644 --- a/crates/oxide_vm/tests/builtins_reflect_tests.rs +++ b/crates/oxide_vm/tests/builtins_reflect_tests.rs @@ -10,10 +10,7 @@ fn eval(vm: &mut Vm, source: &str) -> Result { } fn to_str(vm: &Vm, val: JsValue) -> String { - vm.kernel_core() - .string_forge() - .lookup(val.as_string_index()) - .unwrap_or_default() + vm.lookup_str(val).unwrap_or_default() } #[test] diff --git a/crates/oxide_vm/tests/builtins_regexp_tests.rs b/crates/oxide_vm/tests/builtins_regexp_tests.rs index df67bd62..bd13b857 100644 --- a/crates/oxide_vm/tests/builtins_regexp_tests.rs +++ b/crates/oxide_vm/tests/builtins_regexp_tests.rs @@ -10,10 +10,7 @@ fn eval(vm: &mut Vm, source: &str) -> Result { } fn to_str(vm: &Vm, val: JsValue) -> String { - vm.kernel_core() - .string_forge() - .lookup(val.as_string_index()) - .unwrap_or_default() + vm.lookup_str(val).unwrap_or_default() } // --- RegExp constructor --- diff --git a/crates/oxide_vm/tests/builtins_string_tests.rs b/crates/oxide_vm/tests/builtins_string_tests.rs index bd1be7cd..feb1e90b 100644 --- a/crates/oxide_vm/tests/builtins_string_tests.rs +++ b/crates/oxide_vm/tests/builtins_string_tests.rs @@ -10,10 +10,7 @@ fn eval(vm: &mut Vm, source: &str) -> Result { } fn to_str(vm: &Vm, val: JsValue) -> String { - vm.kernel_core() - .string_forge() - .lookup(val.as_string_index()) - .unwrap_or_default() + vm.lookup_str(val).unwrap_or_default() } fn assert_num_eq(val: JsValue, expected: f64) { diff --git a/crates/oxide_vm/tests/builtins_symbol_tests.rs b/crates/oxide_vm/tests/builtins_symbol_tests.rs index 1e7c7c47..3f70562d 100644 --- a/crates/oxide_vm/tests/builtins_symbol_tests.rs +++ b/crates/oxide_vm/tests/builtins_symbol_tests.rs @@ -10,10 +10,7 @@ fn eval(vm: &mut Vm, source: &str) -> Result { } fn to_str(vm: &Vm, val: JsValue) -> String { - vm.kernel_core() - .string_forge() - .lookup(val.as_string_index()) - .unwrap_or_default() + vm.lookup_str(val).unwrap_or_default() } // --- Symbol constructor --- diff --git a/crates/oxide_vm/tests/builtins_typed_array_tests.rs b/crates/oxide_vm/tests/builtins_typed_array_tests.rs index 36a81d7f..4c6ab571 100644 --- a/crates/oxide_vm/tests/builtins_typed_array_tests.rs +++ b/crates/oxide_vm/tests/builtins_typed_array_tests.rs @@ -10,10 +10,7 @@ fn eval(vm: &mut Vm, source: &str) -> Result { } fn to_str(vm: &Vm, val: JsValue) -> String { - vm.kernel_core() - .string_forge() - .lookup(val.as_string_index()) - .unwrap_or_default() + vm.lookup_str(val).unwrap_or_default() } #[test] diff --git a/crates/oxide_vm/tests/coercion_tests.rs b/crates/oxide_vm/tests/coercion_tests.rs index 757d3a03..28a24bbf 100644 --- a/crates/oxide_vm/tests/coercion_tests.rs +++ b/crates/oxide_vm/tests/coercion_tests.rs @@ -85,25 +85,18 @@ fn test_strict_equality_null_undefined() { #[test] fn test_to_int32_and_to_uint32() { - let vm = Vm::new(); - let sf = vm.kernel_core().string_forge().as_ref(); - let string_three = { - let (idx, hash) = vm.kernel_core().string_forge().intern("3"); - JsValue::string(idx, hash) - }; - let string_bad = { - let (idx, hash) = vm.kernel_core().string_forge().intern("x"); - JsValue::string(idx, hash) - }; + let mut vm = Vm::new(); + let string_three = vm.new_string("3"); + let string_bad = vm.new_string("x"); - assert_eq!(coercion::to_int32(string_three, sf), 3); - assert_eq!(coercion::to_int32(string_bad, sf), 0); - assert_eq!(coercion::to_int32(JsValue::undefined(), sf), 0); - assert_eq!(coercion::to_int32(JsValue::float(1.9), sf), 1); - assert_eq!(coercion::to_int32(JsValue::float(-1.9), sf), -1); - assert_eq!(coercion::to_int32(JsValue::float(f64::NAN), sf), 0); - assert_eq!(coercion::to_int32(JsValue::float(f64::INFINITY), sf), 0); - assert_eq!(coercion::to_uint32(JsValue::int(-1), sf), 4_294_967_295); + assert_eq!(coercion::to_int32(string_three), 3); + assert_eq!(coercion::to_int32(string_bad), 0); + assert_eq!(coercion::to_int32(JsValue::undefined()), 0); + assert_eq!(coercion::to_int32(JsValue::float(1.9)), 1); + assert_eq!(coercion::to_int32(JsValue::float(-1.9)), -1); + assert_eq!(coercion::to_int32(JsValue::float(f64::NAN)), 0); + assert_eq!(coercion::to_int32(JsValue::float(f64::INFINITY)), 0); + assert_eq!(coercion::to_uint32(JsValue::int(-1)), 4_294_967_295); } #[test] diff --git a/crates/oxide_vm/tests/function_tests.rs b/crates/oxide_vm/tests/function_tests.rs index 96d8cb18..5731120c 100644 --- a/crates/oxide_vm/tests/function_tests.rs +++ b/crates/oxide_vm/tests/function_tests.rs @@ -204,10 +204,6 @@ fn function_default_prototype_accepts_member_assignment() { "function Test262Error(message) { this.message = message || ''; } Test262Error.prototype.toString = function () { return this.message; }; new Test262Error('ok').toString()", ) .unwrap(); - let rendered = vm - .kernel_core() - .string_forge() - .lookup(result.as_string_index()) - .unwrap_or_default(); + let rendered = vm.lookup_str(result).unwrap_or_default(); assert_eq!(rendered, "ok"); } diff --git a/crates/oxide_vm/tests/json_tests.rs b/crates/oxide_vm/tests/json_tests.rs index 6a2a7ae6..ddbf6444 100644 --- a/crates/oxide_vm/tests/json_tests.rs +++ b/crates/oxide_vm/tests/json_tests.rs @@ -10,10 +10,7 @@ fn eval(vm: &mut Vm, source: &str) -> Result { } fn string_value(vm: &Vm, val: JsValue) -> String { - vm.kernel_core() - .string_forge() - .lookup(val.as_string_index()) - .unwrap_or_default() + vm.lookup_str(val).unwrap_or_default() } // -- JSON.parse -- diff --git a/crates/oxide_vm/tests/session_arena_tests.rs b/crates/oxide_vm/tests/session_arena_tests.rs index d4b77fd8..0bd440ef 100644 --- a/crates/oxide_vm/tests/session_arena_tests.rs +++ b/crates/oxide_vm/tests/session_arena_tests.rs @@ -11,7 +11,7 @@ fn run_source(vm: &mut Vm, source: &str) -> JsValue { } fn global_prop(vm: &Vm, name: &str) -> JsValue { - let si = vm.kernel_core().string_forge().intern(name).0; + let si = vm.kernel_core().perm_interner().intern(name).0; let global = vm.session().global_object(); let pos = vm .kernel_core() diff --git a/crates/oxide_vm/tests/template_tests.rs b/crates/oxide_vm/tests/template_tests.rs index 6f2d888b..28474be3 100644 --- a/crates/oxide_vm/tests/template_tests.rs +++ b/crates/oxide_vm/tests/template_tests.rs @@ -37,10 +37,7 @@ fn eval_val(source: &str) -> (Vm, Result) { fn to_str(vm: &Vm, val: JsValue) -> String { if val.is_string() { - vm.kernel_core() - .string_forge() - .lookup(val.as_string_index()) - .unwrap_or_default() + vm.lookup_str(val).unwrap_or_default() } else { format!("{}", val) } From 967ac323787b868aa65afde6877e61a0dd217668 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Wed, 24 Jun 2026 21:15:31 +0800 Subject: [PATCH 10/32] =?UTF-8?q?feat:=20JsString=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=A0=87=E8=AE=B0=E6=B8=85=E9=99=A4=20GC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_vm/src/session_gc.rs | 197 +++++++++++++++++++++++++++++- 1 file changed, 191 insertions(+), 6 deletions(-) diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index 160b1ad9..77fe1fed 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -1,9 +1,9 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::mem::size_of; use std::time::Instant; use oxide_kernel::vm_debug; -use oxide_types::object::{JsObject, PropMetaEntry}; +use oxide_types::object::{JsObject, JsString, PropMetaEntry}; use oxide_types::value::JsValue; use rustc_hash::FxBuildHasher; @@ -24,6 +24,7 @@ pub struct SessionGc { pub max_collection_duration_us: u64, pub min_collection_duration_us: u64, pub(crate) mark_stack: Vec<*mut JsObject>, + pub(crate) live_strings: HashSet<*mut JsString, FxBuildHasher>, } impl SessionGc { @@ -83,16 +84,64 @@ impl SessionGc { edges } + /// Record session-string values held directly by `obj` into `live`. JsStrings hold no GC + /// references, so "reaching" a string IS marking it — there is no string DFS stack. Permanent + /// strings are also recorded here harmlessly; the sweep only iterates `session_string_ptrs`, so + /// non-session pointers in `live` are simply never consulted. + fn record_object_string_edges(live: &mut HashSet<*mut JsString, FxBuildHasher>, obj: &JsObject) { + if let Some(props) = obj.hash_props_vec() { + for value in props.iter() { + if value.is_string() { + live.insert(value.as_string_ptr_mut()); + } + } + } + if obj.captured_this().is_string() { + live.insert(obj.captured_this().as_string_ptr_mut()); + } + if obj.home_object().is_string() { + live.insert(obj.home_object().as_string_ptr_mut()); + } + if obj.is_map() { + for value in map::map_native_edges(obj) { + if value.is_string() { + live.insert(value.as_string_ptr_mut()); + } + } + } + if obj.is_set() { + for value in set::set_native_edges(obj) { + if value.is_string() { + live.insert(value.as_string_ptr_mut()); + } + } + } + } + pub(crate) fn mark(&mut self, vm: &Vm) { let mut seeds = Vec::new(); + let mut string_seeds: Vec<*mut JsString> = Vec::new(); vm.for_each_root(|root| { if root.is_object() { seeds.push(root.as_js_object_ptr()); + } else if root.is_string() { + string_seeds.push(root.as_string_ptr_mut()); } }); - let stack = &mut self.mark_stack; + let Self { + mark_stack: stack, + live_strings, + .. + } = self; stack.clear(); + live_strings.clear(); + for ptr in string_seeds { + if !ptr.is_null() { + live_strings.insert(ptr); + } + } + for ptr in seeds { if ptr.is_null() { continue; @@ -102,7 +151,9 @@ impl SessionGc { continue; } // SAFETY: object roots are produced by VM-owned fields and builtin objects. - for edge in Self::object_edges(unsafe { &*ptr }) { + let obj = unsafe { &*ptr }; + Self::record_object_string_edges(live_strings, obj); + for edge in Self::object_edges(obj) { if edge.is_object() { let edge_ptr = edge.as_js_object_ptr(); if vm.is_session_ptr(edge_ptr) { @@ -124,6 +175,7 @@ impl SessionGc { continue; } obj.set_gc_mark(true); + Self::record_object_string_edges(live_strings, obj); let edges = Self::object_edges(obj); for edge in edges { if !edge.is_object() { @@ -186,6 +238,18 @@ impl SessionGc { Self::drop_session_object_heap_data(obj_ptr) + size_of::() as u64 } + /// Drop a dead session `JsString` (allocated by `Vm::new_string` via `Box::into_raw`), + /// returning the bytes freed. Mirrors `Vm::free_session_string_heap_data`'s drop, but applied + /// selectively to a single dead pointer. + /// + /// SAFETY: `ptr` must be a non-null pointer produced by `Box::into_raw(Box::new(JsString))` in + /// `Vm::new_string`, still present in `session_string_ptrs`, and dropped exactly once. + unsafe fn drop_dead_session_string(ptr: *mut JsString) -> u64 { + let bytes = (size_of::() + (*ptr).len()) as u64; + drop(Box::from_raw(ptr)); + bytes + } + pub(crate) fn sweep(&mut self, vm: &mut Vm) -> u64 { let old_ptrs = std::mem::take(&mut vm.session_object_ptrs); let mut forwarding = std::mem::take(&mut vm.forwarding); @@ -285,8 +349,40 @@ impl SessionGc { freed_bytes } + /// Sweep session `JsString`s: keep the pointers recorded live during `mark()`, drop the rest + /// via `Box::from_raw`. Live strings are NOT moved — Box addresses are stable — so no forwarding + /// map and no root-pointer rewriting are needed. Run after the object sweep (which resets + /// `session_bytes_allocated` to object-only), re-adding surviving string bytes. Returns the + /// bytes freed. + pub(crate) fn sweep_session_strings(&mut self, vm: &mut Vm) -> u64 { + let old = std::mem::take(&mut vm.session_string_ptrs); + let mut freed = 0u64; + let mut live_bytes = 0usize; + let mut live = Vec::with_capacity(old.len()); + for ptr in old { + if ptr.is_null() { + continue; + } + if self.live_strings.contains(&ptr) { + // Survivor — address unchanged, no rewrite needed. + // SAFETY: ptr is a live session-string box still owned by the VM. + live_bytes += unsafe { size_of::() + (*ptr).len() }; + live.push(ptr); + } else { + // SAFETY: ptr is in session_string_ptrs but not reachable; drop exactly once. + freed += unsafe { Self::drop_dead_session_string(ptr) }; + } + } + vm.session_string_ptrs = live; + vm.session_bytes_allocated = vm.session_bytes_allocated.saturating_add(live_bytes); + + self.total_bytes_freed = self.total_bytes_freed.saturating_add(freed); + self.last_collection_bytes_freed = self.last_collection_bytes_freed.saturating_add(freed); + freed + } + pub(crate) fn should_collect(&self, vm: &Vm) -> bool { - !vm.session_object_ptrs.is_empty() + (!vm.session_object_ptrs.is_empty() || !vm.session_string_ptrs.is_empty()) && vm.session_bytes_allocated >= vm.kernel_core().config().session_gc_threshold } @@ -294,7 +390,8 @@ impl SessionGc { let start = Instant::now(); self.mark(vm); - let freed_bytes = self.sweep(vm); + let mut freed_bytes = self.sweep(vm); + freed_bytes += self.sweep_session_strings(vm); let elapsed = start.elapsed(); self.total_collections += 1; @@ -342,6 +439,7 @@ impl Default for SessionGc { max_collection_duration_us: 0, min_collection_duration_us: u64::MAX, mark_stack: Vec::new(), + live_strings: HashSet::default(), } } } @@ -784,4 +882,91 @@ mod tests { vm.regs[1] = JsValue::int(0); assert_eq!(native_ok(data_view::data_view_get_int32(&mut vm, &[0, 1])), JsValue::int(9)); } + + fn collect(vm: &mut Vm) { + let mut gc = std::mem::take(&mut vm.session_gc); + gc.collect(vm); + vm.session_gc = gc; + } + + #[test] + fn session_string_collected_when_dead() { + let mut vm = Vm::new(); + let dead = vm.new_string("dead-string"); + let dead_ptr = dead.as_string_ptr_mut(); + assert!(vm.session_string_ptrs.contains(&dead_ptr)); + + // No root references `dead` (it lives only on the Rust stack as a value wrapper). + collect(&mut vm); + + assert!(!vm.session_string_ptrs.contains(&dead_ptr)); + } + + #[test] + fn session_string_survives_when_in_register() { + let mut vm = Vm::new(); + let live = vm.new_string("live-in-reg"); + let live_ptr = live.as_string_ptr_mut(); + vm.regs[0] = live; + + collect(&mut vm); + + assert!(vm.session_string_ptrs.contains(&live_ptr)); + // Live strings are never moved — the register still points at the same box. + assert_eq!(vm.regs[0].as_string_ptr_mut(), live_ptr); + assert_eq!(unsafe { (*live_ptr).as_str() }, "live-in-reg"); + } + + #[test] + fn session_string_survives_via_object_property() { + let mut vm = Vm::new(); + let obj = plain_object(&mut vm); + let s = vm.new_string("prop-string"); + let s_ptr = s.as_string_ptr_mut(); + unsafe { + (*obj).set_prop_at(0, s); + } + let obj_session = vm.promote_object(obj); + + // The string is reachable ONLY through the live object's property, not via any register. + vm.regs.fill(JsValue::undefined()); + vm.regs[0] = JsValue::from_js_object(obj_session); + + collect(&mut vm); + + assert!(vm.session_string_ptrs.contains(&s_ptr)); + assert_eq!(unsafe { (*s_ptr).as_str() }, "prop-string"); + } + + #[test] + fn permanent_string_untouched_by_sweep() { + let mut vm = Vm::new(); + let perm = vm.perm_string("perm"); + let perm_ptr = perm.as_string_ptr_mut(); + // Permanent strings live in PermInterner, never in the session set. + assert!(!vm.session_string_ptrs.contains(&perm_ptr)); + vm.regs[0] = perm; + + collect(&mut vm); + + // Never dropped by the session sweep (still readable, still absent from session set). + assert!(!vm.session_string_ptrs.contains(&perm_ptr)); + assert_eq!(unsafe { (*perm_ptr).as_str() }, "perm"); + } + + #[test] + fn string_sweep_byte_accounting() { + let mut vm = Vm::new(); + let dead = vm.new_string("0123456789"); + let _ = dead.as_string_ptr_mut(); + let expected = (size_of::() + "0123456789".len()) as u64; + + let mut gc = std::mem::take(&mut vm.session_gc); + let before = gc.total_bytes_freed; + gc.collect(&mut vm); + let after = gc.total_bytes_freed; + vm.session_gc = gc; + + assert!(after >= before + expected); + } } From 12bfe551cfd5b32bd7b91143ba11c0c1050679dd Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Wed, 24 Jun 2026 22:20:34 +0800 Subject: [PATCH 11/32] =?UTF-8?q?perf:=20=E5=AF=84=E5=AD=98=E5=99=A8?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E6=94=B9=E7=94=A8=E5=85=B1=E4=BA=AB=20save?= =?UTF-8?q?=5Fstack,=E6=B6=88=E9=99=A4=20Box<[JsValue]>=20=E5=A0=86?= =?UTF-8?q?=E5=88=86=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_vm/src/builtins/error.rs | 2 +- crates/oxide_vm/src/session_gc.rs | 11 +++++----- crates/oxide_vm/src/vm.rs | 27 ++++++++++++++++--------- crates/oxide_vm/src/vm_dispatch_misc.rs | 5 +++-- crates/oxide_vm/src/vm_runtime.rs | 8 ++++++-- crates/oxide_vm/src/vm_support.rs | 3 +++ 6 files changed, 37 insertions(+), 19 deletions(-) diff --git a/crates/oxide_vm/src/builtins/error.rs b/crates/oxide_vm/src/builtins/error.rs index 77f5fac2..27024dd3 100644 --- a/crates/oxide_vm/src/builtins/error.rs +++ b/crates/oxide_vm/src/builtins/error.rs @@ -177,7 +177,7 @@ mod tests { return_addr: 0, function_name: name_si, caller_reg_limit: 1, - saved_regs: vec![JsValue::undefined()].into_boxed_slice(), + saved_reg_offset: 0, saved_this: JsValue::undefined(), saved_new_target: JsValue::undefined(), callee: JsValue::undefined(), diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index 77fe1fed..93a804da 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -460,10 +460,10 @@ fn rewrite_vm_roots(vm: &mut Vm, forwarding: &HashMap<*mut JsObject, *mut JsObje for value in &mut vm.regs { *value = rewrite_forwarded_value(*value, forwarding); } + for value in &mut vm.save_stack { + *value = rewrite_forwarded_value(*value, forwarding); + } for frame in &mut vm.frames { - for value in frame.saved_regs.iter_mut() { - *value = rewrite_forwarded_value(*value, forwarding); - } frame.saved_this = rewrite_forwarded_value(frame.saved_this, forwarding); frame.saved_new_target = rewrite_forwarded_value(frame.saved_new_target, forwarding); frame.callee = rewrite_forwarded_value(frame.callee, forwarding); @@ -553,8 +553,8 @@ mod tests { vm.frames.push(CallFrame { return_addr: 0, function_name: 0, - caller_reg_limit: 0, - saved_regs: vec![JsValue::from_js_object(frame_session)].into_boxed_slice(), + caller_reg_limit: 1, + saved_reg_offset: 0, saved_this: JsValue::from_js_object(this_session), saved_new_target: JsValue::from_js_object(child_session), callee: JsValue::from_js_object(child_session), @@ -563,6 +563,7 @@ mod tests { is_derived_constructor: false, continuation: FrameContinuation::None, }); + vm.save_stack.push(JsValue::from_js_object(frame_session)); vm.regs[1] = JsValue::from_js_object(child_session); vm.exception_value = Some(JsValue::from_js_object(root_session)); diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 6ee8f9f1..6a9a67e7 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -99,7 +99,7 @@ pub struct CallFrame { pub return_addr: usize, pub function_name: u32, pub caller_reg_limit: u8, - pub saved_regs: Box<[JsValue]>, + pub saved_reg_offset: u32, pub saved_this: JsValue, pub saved_new_target: JsValue, pub callee: JsValue, @@ -140,6 +140,7 @@ pub(crate) struct InlineSyncState { pub(crate) last_for_of_result: JsValue, pub(crate) saved_bytecode_stack: Vec>, pub(crate) saved_constants_stack: Vec>, + pub(crate) save_stack: Vec, } pub struct Vm { @@ -169,6 +170,10 @@ pub struct Vm { pub(crate) sub_module_constants: Vec>, pub(crate) saved_bytecode_stack: Vec>, pub(crate) saved_constants_stack: Vec>, + /// Shared register save-stack. Each active `CallFrame` saved its caller's live + /// registers (`regs[..caller_reg_limit]`) here at `saved_reg_offset`; restore copies + /// them back and truncates. Capacity is retained across calls — zero per-call heap alloc. + pub(crate) save_stack: Vec, pub(crate) try_stack: Vec, pub(crate) exception_value: Option, pub(crate) pending_exception: Option, @@ -302,14 +307,14 @@ impl Vm { } } for frame in &self.frames { - for &v in &frame.saved_regs { - f(v); - } f(frame.saved_this); f(frame.saved_new_target); f(frame.callee); f(frame.constructed_this.unwrap_or(JsValue::undefined())); } + for &v in &self.save_stack { + f(v); + } f(JsValue::from_js_object(self.session.global_object().as_ptr() as *mut JsObject)); f(self.exception_value.unwrap_or(JsValue::undefined())); f(self.pending_exception.unwrap_or(JsValue::undefined())); @@ -650,7 +655,8 @@ impl Vm { let sub_param_base = self.sub_modules[sub_idx].param_base as usize; let sub_is_arrow = self.sub_modules[sub_idx].is_arrow; let caller_reg_limit = self.active_reg_limit.max(1); - let saved_regs = self.regs[..caller_reg_limit as usize].to_vec().into_boxed_slice(); + let saved_reg_offset = self.save_stack.len() as u32; + self.save_stack.extend_from_slice(&self.regs[..caller_reg_limit as usize]); let saved_this = self.regs[254]; let saved_new_target = self.regs[255]; @@ -674,7 +680,7 @@ impl Vm { return_addr: self.pc, function_name, caller_reg_limit, - saved_regs, + saved_reg_offset, saved_this, saved_new_target, callee, @@ -971,7 +977,8 @@ impl Vm { let sub_constants = self.sub_modules[sub_idx].constants.clone(); let sub_param_base = self.sub_modules[sub_idx].param_base as usize; let caller_reg_limit = self.active_reg_limit.max(1); - let saved_regs = self.regs[..caller_reg_limit as usize].to_vec().into_boxed_slice(); + let saved_reg_offset = self.save_stack.len() as u32; + self.save_stack.extend_from_slice(&self.regs[..caller_reg_limit as usize]); let saved_this = self.regs[254]; let saved_new_target = self.regs[255]; @@ -993,7 +1000,7 @@ impl Vm { .map(|name| self.kernel_core.perm_interner().intern(name).0) .unwrap_or(0), caller_reg_limit, - saved_regs, + saved_reg_offset, saved_this, saved_new_target, callee: super_ctor, @@ -1426,7 +1433,7 @@ mod tests { return_addr: 1, function_name: 0, caller_reg_limit: 2, - saved_regs: vec![JsValue::undefined()].into_boxed_slice(), + saved_reg_offset: 0, saved_this: JsValue::undefined(), saved_new_target: JsValue::undefined(), callee: JsValue::undefined(), @@ -1435,6 +1442,7 @@ mod tests { is_derived_constructor: false, continuation: super::FrameContinuation::None, }); + vm.save_stack.push(JsValue::undefined()); vm.for_in_iters.push(std::ptr::dangling_mut::>()); vm.for_of_iters.push(JsValue::undefined()); vm.saved_bytecode_stack @@ -1453,6 +1461,7 @@ mod tests { assert_eq!(vm.pc, 0); assert!(vm.frames.is_empty()); + assert!(vm.save_stack.is_empty()); assert!(vm.for_in_iters.is_empty()); assert!(vm.for_of_iters.is_empty()); assert!(vm.saved_bytecode_stack.is_empty()); diff --git a/crates/oxide_vm/src/vm_dispatch_misc.rs b/crates/oxide_vm/src/vm_dispatch_misc.rs index 1e1c9c48..14163038 100644 --- a/crates/oxide_vm/src/vm_dispatch_misc.rs +++ b/crates/oxide_vm/src/vm_dispatch_misc.rs @@ -99,7 +99,8 @@ impl Vm { let sub_constants = self.sub_modules[sub_idx].constants.clone(); let sub_param_base = self.sub_modules[sub_idx].param_base as usize; let caller_reg_limit = self.active_reg_limit.max(1); - let saved_regs = self.regs[..caller_reg_limit as usize].to_vec().into_boxed_slice(); + let saved_reg_offset = self.save_stack.len() as u32; + self.save_stack.extend_from_slice(&self.regs[..caller_reg_limit as usize]); let saved_this = self.regs[254]; let saved_new_target = self.regs[255]; @@ -126,7 +127,7 @@ impl Vm { .map(|name| self.kernel_core.perm_interner().intern(name).0) .unwrap_or(0), caller_reg_limit, - saved_regs, + saved_reg_offset, saved_this, saved_new_target, callee: constructor, diff --git a/crates/oxide_vm/src/vm_runtime.rs b/crates/oxide_vm/src/vm_runtime.rs index e8cf273f..01742e96 100644 --- a/crates/oxide_vm/src/vm_runtime.rs +++ b/crates/oxide_vm/src/vm_runtime.rs @@ -50,6 +50,7 @@ impl Vm { last_for_of_result: self.last_for_of_result, saved_bytecode_stack: std::mem::take(&mut self.saved_bytecode_stack), saved_constants_stack: std::mem::take(&mut self.saved_constants_stack), + save_stack: std::mem::take(&mut self.save_stack), }); self.regs = [JsValue::undefined(); 256]; @@ -91,6 +92,7 @@ impl Vm { self.last_for_of_result = saved.last_for_of_result; self.saved_bytecode_stack = saved.saved_bytecode_stack; self.saved_constants_stack = saved.saved_constants_stack; + self.save_stack = saved.save_stack; result } @@ -102,8 +104,10 @@ impl Vm { if let Some(saved_consts) = self.saved_constants_stack.pop() { self.constants = saved_consts; } - let restore_len = frame.saved_regs.len(); - self.regs[..restore_len].copy_from_slice(&frame.saved_regs); + let offset = frame.saved_reg_offset as usize; + let len = frame.caller_reg_limit as usize; + self.regs[..len].copy_from_slice(&self.save_stack[offset..offset + len]); + self.save_stack.truncate(offset); self.regs[254] = frame.saved_this; self.regs[255] = frame.saved_new_target; self.active_reg_limit = frame.caller_reg_limit; diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index c79efd31..64f8a72a 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -42,6 +42,7 @@ impl Vm { sub_module_constants: Vec::new(), saved_bytecode_stack: Vec::new(), saved_constants_stack: Vec::new(), + save_stack: Vec::new(), try_stack: Vec::new(), exception_value: None, pending_exception: None, @@ -88,6 +89,7 @@ impl Vm { sub_module_constants: Vec::new(), saved_bytecode_stack: Vec::new(), saved_constants_stack: Vec::new(), + save_stack: Vec::new(), try_stack: Vec::new(), exception_value: None, pending_exception: None, @@ -174,6 +176,7 @@ impl Vm { self.last_for_of_result = JsValue::undefined(); self.saved_bytecode_stack.clear(); self.saved_constants_stack.clear(); + self.save_stack.clear(); self.try_stack.clear(); self.exception_value = None; self.pending_exception = None; From d7bc6a9b4ec16beec6756cde9fd1e0a8b8b2cc24 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Wed, 24 Jun 2026 22:52:25 +0800 Subject: [PATCH 12/32] =?UTF-8?q?perf:=20=E6=A8=A1=E5=9D=97=E6=A0=91?= =?UTF-8?q?=E6=94=B9=E7=94=A8=20Arc=20=E6=8C=81=E6=9C=89,=E6=B6=88?= =?UTF-8?q?=E9=99=A4=E5=90=8C=E6=AD=A5=E8=B0=83=E7=94=A8=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E6=B7=B1=E5=B1=82=E6=A8=A1=E5=9D=97=E5=85=8B=E9=9A=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_vm/src/vm.rs | 2 +- crates/oxide_vm/src/vm_runtime.rs | 9 ++++++--- crates/oxide_vm/src/vm_support.rs | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 6a9a67e7..8918c7ff 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -166,7 +166,7 @@ pub struct Vm { pub(crate) session_bytes_allocated: usize, pub object_prototype: P, pub math_rng_state: u64, - pub(crate) sub_modules: Vec, + pub(crate) sub_modules: Arc>, pub(crate) sub_module_constants: Vec>, pub(crate) saved_bytecode_stack: Vec>, pub(crate) saved_constants_stack: Vec>, diff --git a/crates/oxide_vm/src/vm_runtime.rs b/crates/oxide_vm/src/vm_runtime.rs index 01742e96..7af1cfcc 100644 --- a/crates/oxide_vm/src/vm_runtime.rs +++ b/crates/oxide_vm/src/vm_runtime.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use oxide_bytecode::module::CompiledModule; use oxide_bytecode::opcode; @@ -30,7 +32,8 @@ impl Vm { } self.native_call_depth += 1; - let sub = self.sub_modules[sub_idx].clone(); + let subs = Arc::clone(&self.sub_modules); + let sub = &subs[sub_idx]; let converted_constants = self.convert_constants(&sub.constants).map_err(String::from)?; let saved = Box::new(InlineSyncState { @@ -55,7 +58,7 @@ impl Vm { self.regs = [JsValue::undefined(); 256]; self.pc = 0; - self.bytecode = sub.bytecode; + self.bytecode = sub.bytecode.clone(); self.constants = converted_constants; self.active_reg_limit = sub.n_registers.max(1); self.root_reg_limit = self.active_reg_limit; @@ -140,7 +143,7 @@ impl Vm { pub fn run(&mut self, module: &CompiledModule) -> Result { self.clear_execution_state(); - self.sub_modules = module.sub_modules.clone(); + self.sub_modules = Arc::new(module.sub_modules.clone()); self.constants = self.convert_constants(&module.constants).map_err(String::from)?; self.sub_module_constants = vec![Vec::new(); self.sub_modules.len()]; self.bytecode = module.bytecode.clone(); diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index 64f8a72a..dab0d4be 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -38,7 +38,7 @@ impl Vm { session_bytes_allocated: 0, object_prototype: obj_proto, math_rng_state: 0, - sub_modules: Vec::new(), + sub_modules: Arc::new(Vec::new()), sub_module_constants: Vec::new(), saved_bytecode_stack: Vec::new(), saved_constants_stack: Vec::new(), @@ -85,7 +85,7 @@ impl Vm { session_bytes_allocated: 0, object_prototype: obj_proto, math_rng_state: 0, - sub_modules: Vec::new(), + sub_modules: Arc::new(Vec::new()), sub_module_constants: Vec::new(), saved_bytecode_stack: Vec::new(), saved_constants_stack: Vec::new(), From c378b92d1a460cb32e6146e0b16f48a433263026 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Thu, 25 Jun 2026 10:16:32 +0800 Subject: [PATCH 13/32] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=20CreateClosur?= =?UTF-8?q?e=20=E5=AD=97=E8=8A=82=E7=A0=81,=E5=87=BD=E6=95=B0=E5=AD=97?= =?UTF-8?q?=E9=9D=A2=E9=87=8F=E6=AF=8F=E6=AC=A1=E6=89=A7=E8=A1=8C=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E6=96=B0=E9=97=AD=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_bytecode/src/opcode.rs | 3 +++ crates/oxide_compiler/src/compiler.rs | 5 +++++ crates/oxide_compiler/src/emitter/expr/function.rs | 6 ++---- crates/oxide_compiler/src/emitter/mod.rs | 6 ++---- .../oxide_compiler/src/emitter/stmt/declaration.rs | 3 +-- crates/oxide_vm/src/vm.rs | 14 ++++++++++++++ crates/oxide_vm/src/vm_runtime.rs | 3 +-- crates/oxide_vm/src/vm_support.rs | 2 +- 8 files changed, 29 insertions(+), 13 deletions(-) diff --git a/crates/oxide_bytecode/src/opcode.rs b/crates/oxide_bytecode/src/opcode.rs index 8fa97ada..64fa3368 100644 --- a/crates/oxide_bytecode/src/opcode.rs +++ b/crates/oxide_bytecode/src/opcode.rs @@ -90,6 +90,7 @@ pub enum OpCode { SUPER_STATIC_GET_PROP = 0x46, SET_HOME_OBJECT = 0x47, DEFINE_ACCESSOR = 0x48, + CREATE_CLOSURE = 0x4A, // ── Object Property (0x50-0x5F) ── IC_GET_PROP = 0x50, @@ -238,6 +239,7 @@ impl TryFrom for OpCode { 0x46 => Ok(OpCode::SUPER_STATIC_GET_PROP), 0x47 => Ok(OpCode::SET_HOME_OBJECT), 0x48 => Ok(OpCode::DEFINE_ACCESSOR), + 0x4A => Ok(OpCode::CREATE_CLOSURE), 0x50 => Ok(OpCode::IC_GET_PROP), 0x51 => Ok(OpCode::IC_SET_PROP), 0x52 => Ok(OpCode::GET_PROP), @@ -357,6 +359,7 @@ impl fmt::Display for OpCode { OpCode::SUPER_STATIC_GET_PROP => "SUPER_STATIC_GET_PROP", OpCode::SET_HOME_OBJECT => "SET_HOME_OBJECT", OpCode::DEFINE_ACCESSOR => "DEFINE_ACCESSOR", + OpCode::CREATE_CLOSURE => "CREATE_CLOSURE", OpCode::IC_GET_PROP => "IC_GET_PROP", OpCode::IC_SET_PROP => "IC_SET_PROP", OpCode::GET_PROP => "GET_PROP", diff --git a/crates/oxide_compiler/src/compiler.rs b/crates/oxide_compiler/src/compiler.rs index 2563251b..d1ff1fcf 100644 --- a/crates/oxide_compiler/src/compiler.rs +++ b/crates/oxide_compiler/src/compiler.rs @@ -254,6 +254,11 @@ impl CompileCtx { self.emit(opcode::encode(OpCode::LOAD_CONST, reg, (idx & 0xFF) as u8, ((idx >> 8) & 0xFF) as u8)); } + pub(crate) fn emit_create_closure(&mut self, reg: u8, sub_idx: u32) { + let idx = sub_idx as u16; + self.emit(opcode::encode(OpCode::CREATE_CLOSURE, reg, (idx & 0xFF) as u8, ((idx >> 8) & 0xFF) as u8)); + } + pub(crate) fn count_word(&mut self) { self.projected_pc += 1; } diff --git a/crates/oxide_compiler/src/emitter/expr/function.rs b/crates/oxide_compiler/src/emitter/expr/function.rs index d8773ecd..ecefa87a 100644 --- a/crates/oxide_compiler/src/emitter/expr/function.rs +++ b/crates/oxide_compiler/src/emitter/expr/function.rs @@ -37,9 +37,8 @@ impl Compiler { // 1-indexed: 0 = no sub_module (sentinel) let sub_idx = ctx.sub_modules.len() as u32; - let const_idx = ctx.add_constant(Constant::BytecodeFunc(sub_idx)); let r = ctx.alloc_reg(); - ctx.emit_load_const(r, const_idx); + ctx.emit_create_closure(r, sub_idx); Ok(r) } @@ -72,9 +71,8 @@ impl Compiler { // 1-indexed: 0 = no sub_module (sentinel) let sub_idx = ctx.sub_modules.len() as u32; - let const_idx = ctx.add_constant(Constant::BytecodeFunc(sub_idx)); let r = ctx.alloc_reg(); - ctx.emit_load_const(r, const_idx); + ctx.emit_create_closure(r, sub_idx); Ok(r) } diff --git a/crates/oxide_compiler/src/emitter/mod.rs b/crates/oxide_compiler/src/emitter/mod.rs index 02d200d0..a0af0ec9 100644 --- a/crates/oxide_compiler/src/emitter/mod.rs +++ b/crates/oxide_compiler/src/emitter/mod.rs @@ -811,8 +811,7 @@ impl Compiler { ctor_module.function_name = ctor_name.clone(); ctx.sub_modules.push(ctor_module); - let ctor_idx = ctx.add_constant(Constant::BytecodeFunc(ctx.sub_modules.len() as u32)); - ctx.emit_load_const(ctor_reg, ctor_idx); + ctx.emit_create_closure(ctor_reg, ctx.sub_modules.len() as u32); ctx.emit(opcode::encode(OpCode::NEW_OBJECT, proto_reg, 0, 0)); if let Some(super_reg) = super_reg { @@ -947,9 +946,8 @@ impl Compiler { method_module.needs_home_object = true; ctx.sub_modules.push(method_module); - let method_idx = ctx.add_constant(Constant::BytecodeFunc(ctx.sub_modules.len() as u32)); let method_reg = ctx.alloc_reg(); - ctx.emit_load_const(method_reg, method_idx); + ctx.emit_create_closure(method_reg, ctx.sub_modules.len() as u32); ctx.emit(opcode::encode(OpCode::SET_HOME_OBJECT, method_reg, home_reg, 0)); Ok(method_reg) } diff --git a/crates/oxide_compiler/src/emitter/stmt/declaration.rs b/crates/oxide_compiler/src/emitter/stmt/declaration.rs index 67ae72f3..3b6d1649 100644 --- a/crates/oxide_compiler/src/emitter/stmt/declaration.rs +++ b/crates/oxide_compiler/src/emitter/stmt/declaration.rs @@ -77,8 +77,7 @@ impl Compiler { let var_reg = ctx.lookup(&name)?; ctx.reserve_reg(var_reg); - let const_idx = ctx.add_constant(Constant::BytecodeFunc(sub_idx)); - ctx.emit_load_const(var_reg, const_idx); + ctx.emit_create_closure(var_reg, sub_idx); ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, var_reg, 0)); Ok(None) diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 8918c7ff..fe1ec94f 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -739,6 +739,20 @@ impl Vm { self.dispatch_load_const(rd, instr)?; } + OpCode::CREATE_CLOSURE => { + let sub_idx = opcode::imm16(instr) as u32; + debug_assert!(sub_idx > 0 && (sub_idx as usize) <= self.sub_modules.len()); + let sub = &self.sub_modules[sub_idx as usize - 1]; + let result = self.create_function_object( + sub_idx, + sub.is_arrow, + sub.is_class_constructor, + sub.is_derived_constructor, + sub.needs_home_object, + ); + self.regs[rd] = result; + } + OpCode::ADD => { self.dispatch_add(rd, a, b)?; } diff --git a/crates/oxide_vm/src/vm_runtime.rs b/crates/oxide_vm/src/vm_runtime.rs index 7af1cfcc..8faa7b04 100644 --- a/crates/oxide_vm/src/vm_runtime.rs +++ b/crates/oxide_vm/src/vm_runtime.rs @@ -158,8 +158,7 @@ impl Vm { } } - let global_ptr = self.session.global_object().as_ptr() as *mut JsObject; - self.regs[254] = JsValue::from_js_object(global_ptr); + self.regs[254] = JsValue::undefined(); self.dispatch() } diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index dab0d4be..9a8b47da 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -235,7 +235,7 @@ impl Vm { /// Create a function JsObject for a BytecodeFunc constant. /// When `is_arrow` is true, captures the current `this` (regs[254]) /// for lexical this binding at call time. - fn create_function_object( + pub(crate) fn create_function_object( &mut self, sub_idx: u32, is_arrow: bool, is_class_constructor: bool, is_derived_constructor: bool, needs_home_object: bool, ) -> JsValue { From 63737ef74db8418b3424cb471bddb62d45fd3567 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Thu, 25 Jun 2026 10:28:35 +0800 Subject: [PATCH 14/32] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=20CreateRegExp?= =?UTF-8?q?=20=E5=AD=97=E8=8A=82=E7=A0=81,=E6=AD=A3=E5=88=99=E5=AD=97?= =?UTF-8?q?=E9=9D=A2=E9=87=8F=E6=AF=8F=E6=AC=A1=E6=89=A7=E8=A1=8C=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E6=96=B0=E5=AF=B9=E8=B1=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_bytecode/src/opcode.rs | 3 ++ .../src/counter/expr/literal.rs | 6 +++- .../src/emitter/expr/literal.rs | 13 +++++--- crates/oxide_vm/src/vm.rs | 31 +++++++++++++++++++ 4 files changed, 48 insertions(+), 5 deletions(-) diff --git a/crates/oxide_bytecode/src/opcode.rs b/crates/oxide_bytecode/src/opcode.rs index 64fa3368..e6e29643 100644 --- a/crates/oxide_bytecode/src/opcode.rs +++ b/crates/oxide_bytecode/src/opcode.rs @@ -91,6 +91,7 @@ pub enum OpCode { SET_HOME_OBJECT = 0x47, DEFINE_ACCESSOR = 0x48, CREATE_CLOSURE = 0x4A, + CREATE_REGEXP = 0x4B, // ── Object Property (0x50-0x5F) ── IC_GET_PROP = 0x50, @@ -240,6 +241,7 @@ impl TryFrom for OpCode { 0x47 => Ok(OpCode::SET_HOME_OBJECT), 0x48 => Ok(OpCode::DEFINE_ACCESSOR), 0x4A => Ok(OpCode::CREATE_CLOSURE), + 0x4B => Ok(OpCode::CREATE_REGEXP), 0x50 => Ok(OpCode::IC_GET_PROP), 0x51 => Ok(OpCode::IC_SET_PROP), 0x52 => Ok(OpCode::GET_PROP), @@ -360,6 +362,7 @@ impl fmt::Display for OpCode { OpCode::SET_HOME_OBJECT => "SET_HOME_OBJECT", OpCode::DEFINE_ACCESSOR => "DEFINE_ACCESSOR", OpCode::CREATE_CLOSURE => "CREATE_CLOSURE", + OpCode::CREATE_REGEXP => "CREATE_REGEXP", OpCode::IC_GET_PROP => "IC_GET_PROP", OpCode::IC_SET_PROP => "IC_SET_PROP", OpCode::GET_PROP => "GET_PROP", diff --git a/crates/oxide_compiler/src/counter/expr/literal.rs b/crates/oxide_compiler/src/counter/expr/literal.rs index e3234428..2ba0caf5 100644 --- a/crates/oxide_compiler/src/counter/expr/literal.rs +++ b/crates/oxide_compiler/src/counter/expr/literal.rs @@ -18,7 +18,11 @@ impl Compiler { } pub(in crate::counter) fn count_regexp_literal(&self, ctx: &mut CompileCtx) { - ctx.alloc_reg(); + ctx.alloc_reg(); // pattern reg (LOAD_CONST) + ctx.projected_pc += 1; + ctx.alloc_reg(); // flags reg (LOAD_CONST) + ctx.projected_pc += 1; + ctx.alloc_reg(); // result reg (CREATE_REGEXP) ctx.projected_pc += 1; } diff --git a/crates/oxide_compiler/src/emitter/expr/literal.rs b/crates/oxide_compiler/src/emitter/expr/literal.rs index 7d25b98d..4f2e0560 100644 --- a/crates/oxide_compiler/src/emitter/expr/literal.rs +++ b/crates/oxide_compiler/src/emitter/expr/literal.rs @@ -68,11 +68,16 @@ impl Compiler { let raw_str = raw.to_string(); if raw_str.len() >= 2 && raw_str.starts_with('/') { let last_slash = raw_str.rfind('/').unwrap_or(raw_str.len() - 1); - let pattern = &raw_str[1..last_slash]; - let flags = &raw_str[last_slash + 1..]; - let ci = ctx.add_constant(Constant::RegExp(pattern.to_string(), flags.to_string())); + let pattern = raw_str[1..last_slash].to_string(); + let flags = raw_str[last_slash + 1..].to_string(); + let pat_ci = ctx.add_constant(Constant::String(pattern)); + let pat_reg = ctx.alloc_reg(); + ctx.emit_load_const(pat_reg, pat_ci); + let flags_ci = ctx.add_constant(Constant::String(flags)); + let flags_reg = ctx.alloc_reg(); + ctx.emit_load_const(flags_reg, flags_ci); let r = ctx.alloc_reg(); - ctx.emit_load_const(r, ci); + ctx.emit(opcode::encode(OpCode::CREATE_REGEXP, r, pat_reg, flags_reg)); Ok(r) } else { Err(format!("unsupported regexp literal: {:?}", lit)) diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index fe1ec94f..462ba899 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -752,6 +752,37 @@ impl Vm { ); self.regs[rd] = result; } + OpCode::CREATE_REGEXP => { + let pat_val = self.regs[a]; + let flags_val = self.regs[b]; + let ctor_ptr = + self.session.builtin_world().regexp_constructor.as_ptr() as *mut JsObject; + let ctor = unsafe { &*ctor_ptr }; + let Some(native_fn) = ctor.native_fn() else { + self.raise_error_kind("TypeError", "RegExp constructor unavailable")?; + return Ok(JsValue::undefined()); + }; + let saved_0 = self.regs[0]; + let saved_1 = self.regs[1]; + let saved_2 = self.regs[2]; + self.regs[0] = JsValue::undefined(); + self.regs[1] = pat_val; + self.regs[2] = flags_val; + let func = unsafe { crate::vm::native_fn_ptr_to_fn(native_fn) }; + let result = match func(self, &[0, 1, 2]) { + crate::native::NativeResult::Ok(v) => v, + crate::native::NativeResult::Err(e) => { + return Err(self.error_message_text("TypeError", &self.error_text(e))); + } + crate::native::NativeResult::TailCall { .. } => { + return Err(self.error_message_text("TypeError", "unexpected tail call")); + } + }; + self.regs[0] = saved_0; + self.regs[1] = saved_1; + self.regs[2] = saved_2; + self.regs[rd] = result; + } OpCode::ADD => { self.dispatch_add(rd, a, b)?; From 1cab5bde65229f208d002b5c9bc557eab383d936 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Thu, 25 Jun 2026 14:32:49 +0800 Subject: [PATCH 15/32] =?UTF-8?q?perf:=20=E4=B8=8D=E5=8F=AF=E5=8F=98?= =?UTF-8?q?=E5=B8=B8=E9=87=8F=E6=94=B9=20OnceLock=20=E4=B8=80=E6=AC=A1?= =?UTF-8?q?=E6=80=A7=E7=BC=93=E5=AD=98,=E5=88=A0=20Constant=20=E6=AD=BB?= =?UTF-8?q?=E5=8F=98=E4=BD=93=E5=B9=B6=E7=A7=BB=E5=87=BA=20GC=20=E6=A0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_bytecode/src/module.rs | 7 +- crates/oxide_compiler/src/compiler.rs | 3 - crates/oxide_vm/src/dispatch/misc.rs | 5 +- crates/oxide_vm/src/session_gc.rs | 18 +--- crates/oxide_vm/src/vm.rs | 126 ++++++++++-------------- crates/oxide_vm/src/vm_dispatch_ctrl.rs | 5 +- crates/oxide_vm/src/vm_dispatch_misc.rs | 16 +-- crates/oxide_vm/src/vm_runtime.rs | 22 ++--- crates/oxide_vm/src/vm_support.rs | 105 +++++++------------- 9 files changed, 115 insertions(+), 192 deletions(-) diff --git a/crates/oxide_bytecode/src/module.rs b/crates/oxide_bytecode/src/module.rs index 827d7eb9..eb445186 100644 --- a/crates/oxide_bytecode/src/module.rs +++ b/crates/oxide_bytecode/src/module.rs @@ -10,8 +10,6 @@ pub enum Constant { Boolean(bool), Null, Undefined, - BytecodeFunc(u32), - RegExp(String, String), } pub struct CompiledModule { @@ -92,10 +90,7 @@ impl fmt::Display for CompiledModule { writeln!(f, "; n_registers = {}", self.n_registers)?; writeln!(f, "; constants:")?; for (i, c) in self.constants.iter().enumerate() { - match c { - Constant::BytecodeFunc(idx) => writeln!(f, "; [{i}] = BytecodeFunc(sub_module[{idx}])")?, - other => writeln!(f, "; [{i}] = {other:?}")?, - } + writeln!(f, "; [{i}] = {c:?}")?; } writeln!(f)?; for (offset, &instr) in self.bytecode.iter().enumerate() { diff --git a/crates/oxide_compiler/src/compiler.rs b/crates/oxide_compiler/src/compiler.rs index d1ff1fcf..2dd2ec91 100644 --- a/crates/oxide_compiler/src/compiler.rs +++ b/crates/oxide_compiler/src/compiler.rs @@ -209,7 +209,6 @@ enum ConstantKey { Boolean(bool), Null, Undefined, - BytecodeFunc(u32), } impl CompileCtx { @@ -576,8 +575,6 @@ impl ConstantKey { Constant::Boolean(v) => Some(Self::Boolean(*v)), Constant::Null => Some(Self::Null), Constant::Undefined => Some(Self::Undefined), - Constant::BytecodeFunc(v) => Some(Self::BytecodeFunc(*v)), - Constant::RegExp(_, _) => None, } } } diff --git a/crates/oxide_vm/src/dispatch/misc.rs b/crates/oxide_vm/src/dispatch/misc.rs index d790ddac..397996b8 100644 --- a/crates/oxide_vm/src/dispatch/misc.rs +++ b/crates/oxide_vm/src/dispatch/misc.rs @@ -4,8 +4,9 @@ impl Vm { #[inline(always)] pub(crate) fn dispatch_load_const(&mut self, rd: usize, instr: u32) -> Result<(), String> { let idx = (instr >> 16) as usize; - if idx < self.constants.len() { - self.regs[rd] = self.constants[idx]; + let imm = self.immutables(); + if idx < imm.len() { + self.regs[rd] = imm[idx]; Ok(()) } else { Err(format!("constant index {idx} out of bounds")) diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index 93a804da..a831b17c 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -475,19 +475,7 @@ fn rewrite_vm_roots(vm: &mut Vm, forwarding: &HashMap<*mut JsObject, *mut JsObje *value = rewrite_forwarded_value(*value, forwarding); } vm.last_for_of_result = rewrite_forwarded_value(vm.last_for_of_result, forwarding); - for value in &mut vm.constants { - *value = rewrite_forwarded_value(*value, forwarding); - } - for values in &mut vm.sub_module_constants { - for value in values { - *value = rewrite_forwarded_value(*value, forwarding); - } - } - for values in &mut vm.saved_constants_stack { - for value in values { - *value = rewrite_forwarded_value(*value, forwarding); - } - } + // Converted immutables are non-session (scalars + perm-strings) — not rewritten. for iter in &mut vm.for_in_iters { if iter.is_null() { continue; @@ -570,8 +558,6 @@ mod tests { vm.pending_exception = Some(JsValue::from_js_object(child_session)); vm.for_of_iters.push(JsValue::from_js_object(child_session)); vm.last_for_of_result = JsValue::from_js_object(root_session); - vm.constants.push(JsValue::from_js_object(frame_session)); - vm.sub_module_constants = vec![vec![JsValue::from_js_object(child_session)]]; let mut roots = Vec::new(); vm.for_each_root(|v| roots.push(v)); @@ -580,7 +566,7 @@ mod tests { assert!(has_ptr(&roots, this_session)); assert!(has_ptr(&roots, child_session)); assert!(has_ptr(&roots, vm.session.global_object().as_ptr() as *mut JsObject)); - assert!(roots.len() >= 1); + assert!(!roots.is_empty()); assert!(roots.contains(&JsValue::from_js_object(root_session))); assert_eq!(vm.exception_value, Some(JsValue::from_js_object(root_session))); } diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 462ba899..fb77a0cd 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -1,9 +1,9 @@ #![allow(clippy::arc_with_non_send_sync)] use std::collections::HashMap; -use std::sync::Arc; +use std::sync::{Arc, OnceLock}; -use oxide_bytecode::module::CompiledModule; +use oxide_bytecode::module::{CompiledModule, Constant}; use oxide_bytecode::opcode::{self, OpCode}; use smallvec::SmallVec; @@ -127,7 +127,7 @@ pub(crate) struct InlineSyncState { pub(crate) regs: Box<[JsValue; 256]>, pub(crate) pc: usize, pub(crate) bytecode: Vec, - pub(crate) constants: Vec, + pub(crate) active_immutables: *const [JsValue], pub(crate) active_reg_limit: u8, pub(crate) root_reg_limit: u8, pub(crate) try_stack: Vec, @@ -139,7 +139,7 @@ pub(crate) struct InlineSyncState { pub(crate) for_of_iters: Vec, pub(crate) last_for_of_result: JsValue, pub(crate) saved_bytecode_stack: Vec>, - pub(crate) saved_constants_stack: Vec>, + pub(crate) saved_immutables_stack: Vec<*const [JsValue]>, pub(crate) save_stack: Vec, } @@ -147,7 +147,13 @@ pub struct Vm { pub(crate) regs: [JsValue; 256], pub(crate) pc: usize, pub(crate) bytecode: Vec, - pub(crate) constants: Vec, + /// Per-run convert-once immutables cache. Index 0 = top module, sub_idx+1 = sub_modules[sub_idx]. + /// Each `OnceLock` holds that module's constants converted to `JsValue`s exactly once this run. + /// Rebuilt every `run()`. Immutables are scalars + perm-strings — read-only, never GC roots. + pub(crate) immutables_cache: Vec>>, + /// Read-only view into the currently-active module's converted immutables (inside immutables_cache). + /// A fat `*const` because the cache Vec is VM-owned and run-stable (OnceLock filled once). + pub(crate) active_immutables: *const [JsValue], pub(crate) frames: SmallVec<[CallFrame; 16]>, pub for_in_iters: Vec<*mut ForInIter<'static>>, pub(crate) kernel_core: Arc, @@ -167,9 +173,8 @@ pub struct Vm { pub object_prototype: P, pub math_rng_state: u64, pub(crate) sub_modules: Arc>, - pub(crate) sub_module_constants: Vec>, pub(crate) saved_bytecode_stack: Vec>, - pub(crate) saved_constants_stack: Vec>, + pub(crate) saved_immutables_stack: Vec<*const [JsValue]>, /// Shared register save-stack. Each active `CallFrame` saved its caller's live /// registers (`regs[..caller_reg_limit]`) here at `saved_reg_offset`; restore copies /// them back and truncates. Capacity is retained across calls — zero per-call heap alloc. @@ -300,6 +305,30 @@ impl Vm { ptr } + /// Read-only view into the active module's converted immutables. Empty before any `run()`. + #[inline(always)] + pub(crate) fn immutables(&self) -> &[JsValue] { + if self.active_immutables.is_null() { + &[] + } else { + // SAFETY: active_immutables points into a OnceLock> inside immutables_cache, + // which the VM owns; the Vec is filled once and never reallocated for the run's lifetime. + unsafe { &*self.active_immutables } + } + } + + /// Activate module `cache_idx`'s immutables (0 = top, sub_idx+1 = sub_modules[sub_idx]), + /// converting them once into `immutables_cache[cache_idx]` and pointing `active_immutables` at + /// the cached Vec. `constants` is passed by the caller (it already holds `&module.constants`). + pub(crate) fn activate_immutables(&mut self, cache_idx: usize, constants: &[Constant]) { + // Raw-ptr the cache slot so `get_or_init` (which borrows immutables_cache) and the &self + // convert_immutables closure don't conflict with the subsequent self.active_immutables write. + // Sound: immutables_cache is VM-owned, read-only, and run-stable. + let slot: *const OnceLock> = &self.immutables_cache[cache_idx]; + let vec = unsafe { &*slot }.get_or_init(|| self.convert_immutables(constants)); + self.active_immutables = vec.as_slice() as *const [JsValue]; + } + pub(crate) fn for_each_root(&self, mut f: impl FnMut(JsValue)) { for value in &self.regs { if value.is_object() || value.is_string() { @@ -322,19 +351,7 @@ impl Vm { f(v); } f(self.last_for_of_result); - for &v in &self.constants { - f(v); - } - for sub_module in &self.sub_module_constants { - for &v in sub_module { - f(v); - } - } - for saved_constants in &self.saved_constants_stack { - for &v in saved_constants { - f(v); - } - } + // Converted immutables (scalars + perm-strings) are NOT session GC roots — not scanned. for iter in &self.for_in_iters { if iter.is_null() { continue; @@ -651,7 +668,6 @@ impl Vm { let sub_bytecode = self.sub_modules[sub_idx].bytecode.clone(); let sub_n_args = self.sub_modules[sub_idx].n_args as usize; let sub_n_registers = self.sub_modules[sub_idx].n_registers; - let sub_constants = self.sub_modules[sub_idx].constants.clone(); let sub_param_base = self.sub_modules[sub_idx].param_base as usize; let sub_is_arrow = self.sub_modules[sub_idx].is_arrow; let caller_reg_limit = self.active_reg_limit.max(1); @@ -666,9 +682,8 @@ impl Vm { self.regs[254] = if sub_is_arrow { obj.captured_this() } else { this_value }; self.regs[255] = new_target; - let converted_sub_constants = self.convert_constants(&sub_constants).map_err(String::from)?; self.saved_bytecode_stack.push(std::mem::take(&mut self.bytecode)); - self.saved_constants_stack.push(std::mem::take(&mut self.constants)); + self.saved_immutables_stack.push(self.active_immutables); let function_name = self.sub_modules[sub_idx] .function_name @@ -691,7 +706,8 @@ impl Vm { }); self.bytecode = sub_bytecode; - self.constants = converted_sub_constants; + let subs = Arc::clone(&self.sub_modules); + self.activate_immutables(sub_idx + 1, &subs[sub_idx].constants); for (name, reg) in &self.sub_modules[sub_idx].builtin_reg_map.clone() { let si = self.kernel_core.perm_interner().intern(name.as_str()).0; let global = self.session.global_object(); @@ -755,8 +771,7 @@ impl Vm { OpCode::CREATE_REGEXP => { let pat_val = self.regs[a]; let flags_val = self.regs[b]; - let ctor_ptr = - self.session.builtin_world().regexp_constructor.as_ptr() as *mut JsObject; + let ctor_ptr = self.session.builtin_world().regexp_constructor.as_ptr() as *mut JsObject; let ctor = unsafe { &*ctor_ptr }; let Some(native_fn) = ctor.native_fn() else { self.raise_error_kind("TypeError", "RegExp constructor unavailable")?; @@ -1019,7 +1034,6 @@ impl Vm { let sub_bytecode = self.sub_modules[sub_idx].bytecode.clone(); let sub_n_args = self.sub_modules[sub_idx].n_args as usize; let sub_n_registers = self.sub_modules[sub_idx].n_registers; - let sub_constants = self.sub_modules[sub_idx].constants.clone(); let sub_param_base = self.sub_modules[sub_idx].param_base as usize; let caller_reg_limit = self.active_reg_limit.max(1); let saved_reg_offset = self.save_stack.len() as u32; @@ -1034,9 +1048,8 @@ impl Vm { self.regs[254] = derived_this; self.regs[255] = new_target; - let converted_sub_constants = self.convert_constants(&sub_constants)?; self.saved_bytecode_stack.push(std::mem::take(&mut self.bytecode)); - self.saved_constants_stack.push(std::mem::take(&mut self.constants)); + self.saved_immutables_stack.push(self.active_immutables); self.frames.push(CallFrame { return_addr: self.pc, function_name: self.sub_modules[sub_idx] @@ -1056,7 +1069,8 @@ impl Vm { }); self.bytecode = sub_bytecode; - self.constants = converted_sub_constants; + let subs = Arc::clone(&self.sub_modules); + self.activate_immutables(sub_idx + 1, &subs[sub_idx].constants); for (name, reg) in &self.sub_modules[sub_idx].builtin_reg_map { let si = self.kernel_core.perm_interner().intern(name.as_str()).0; let global = self.session.global_object(); @@ -1234,7 +1248,7 @@ impl Vm { OpCode::DELETE_PROP_STATIC => { let prop_idx = self.bytecode[self.pc] as usize; self.pc += 1; - let key_val = self.constants.get(prop_idx).copied().unwrap_or_else(JsValue::undefined); + let key_val = self.immutables().get(prop_idx).copied().unwrap_or_else(JsValue::undefined); let prop_name_si = self.property_key_si(key_val); let Some(obj_ptr) = self.checked_object_ptr(self.regs[rd], "delete on non-object")? else { continue; @@ -1399,7 +1413,7 @@ impl Default for Vm { mod tests { use super::{opcode, JsValue, TryHandler, Vm}; use crate::native::NativeResult; - use oxide_bytecode::module::{CompiledModule, Constant}; + use oxide_bytecode::module::CompiledModule; use oxide_types::object::NativeFnPtr; use oxide_types::object::{JsObject, PropAttributes}; @@ -1492,7 +1506,8 @@ mod tests { vm.for_of_iters.push(JsValue::undefined()); vm.saved_bytecode_stack .push(vec![opcode::encode(opcode::OpCode::HALT, 0, 0, 0)]); - vm.saved_constants_stack.push(vec![JsValue::int(1)]); + vm.saved_immutables_stack + .push(std::ptr::slice_from_raw_parts(std::ptr::null(), 0)); vm.try_stack.push(TryHandler { catch_pc: Some(1), finally_pc: None, @@ -1510,13 +1525,13 @@ mod tests { assert!(vm.for_in_iters.is_empty()); assert!(vm.for_of_iters.is_empty()); assert!(vm.saved_bytecode_stack.is_empty()); - assert!(vm.saved_constants_stack.is_empty()); + assert!(vm.saved_immutables_stack.is_empty()); assert!(vm.try_stack.is_empty()); assert!(vm.exception_value.is_none()); assert!(vm.pending_exception.is_none()); assert!(vm.pending_error_kind.is_none()); assert!(vm.bytecode.is_empty()); - assert!(vm.constants.is_empty()); + assert!(vm.immutables().is_empty()); } #[test] @@ -1551,47 +1566,6 @@ mod tests { assert!(vm.for_of_iters.is_empty()); } - #[test] - fn invalid_regexp_constant_fails_explicitly() { - let module = CompiledModule { - constants: vec![Constant::RegExp("[".into(), "".into())], - bytecode: vec![opcode::encode(opcode::OpCode::HALT, 0, 0, 0)], - n_registers: 1, - ..CompiledModule::new() - }; - let mut vm = Vm::new(); - let err = vm.run(&module).expect_err("invalid RegExp constant should fail explicitly"); - assert!(err.contains("SyntaxError: Invalid regular expression"), "unexpected error: {err}"); - } - - #[test] - fn invalid_regexp_constant_in_submodule_fails_explicitly() { - let submodule = CompiledModule { - constants: vec![Constant::RegExp("[".into(), "".into())], - bytecode: vec![opcode::encode(opcode::OpCode::RETURN, 0, 0, 0)], - n_registers: 1, - ..CompiledModule::new() - }; - let module = CompiledModule { - constants: vec![Constant::BytecodeFunc(1), Constant::Undefined], - bytecode: vec![ - opcode::encode(opcode::OpCode::LOAD_CONST, 0, 0, 0), - opcode::encode(opcode::OpCode::LOAD_CONST, 1, 1, 0), - opcode::encode(opcode::OpCode::CALL, 0, 1, 0), - 0, - opcode::encode(opcode::OpCode::HALT, 0, 0, 0), - ], - n_registers: 2, - sub_modules: vec![submodule], - ..CompiledModule::new() - }; - let mut vm = Vm::new(); - let err = vm - .run(&module) - .expect_err("invalid submodule RegExp constant should fail explicitly"); - assert!(err.contains("SyntaxError: Invalid regular expression"), "unexpected error: {err}"); - } - #[test] fn write_ic_back_updates_three_extension_words() { let mut vm = Vm::new(); diff --git a/crates/oxide_vm/src/vm_dispatch_ctrl.rs b/crates/oxide_vm/src/vm_dispatch_ctrl.rs index 635b342c..c27d8801 100644 --- a/crates/oxide_vm/src/vm_dispatch_ctrl.rs +++ b/crates/oxide_vm/src/vm_dispatch_ctrl.rs @@ -84,14 +84,15 @@ impl Vm { pub(crate) fn dispatch_define_accessor(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { let prop_idx = self.bytecode[self.pc] as usize; self.pc += 1; - if prop_idx >= self.constants.len() { + if prop_idx >= self.immutables().len() { return self.raise_type_error("DEFINE_ACCESSOR constant index out of bounds"); } let obj_val = self.regs[rd]; if !obj_val.is_object() { return self.raise_type_error("DEFINE_ACCESSOR target is not object"); } - let prop_name_si = self.property_key_si(self.constants[prop_idx]); + let key_val = self.immutables()[prop_idx]; + let prop_name_si = self.property_key_si(key_val); let getter = self.regs[a]; let setter = self.regs[b]; let obj = unsafe { &mut *obj_val.as_js_object_ptr() }; diff --git a/crates/oxide_vm/src/vm_dispatch_misc.rs b/crates/oxide_vm/src/vm_dispatch_misc.rs index 14163038..45fdfa08 100644 --- a/crates/oxide_vm/src/vm_dispatch_misc.rs +++ b/crates/oxide_vm/src/vm_dispatch_misc.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use crate::native::{NativeFn, NativeResult}; use crate::vm::{native_fn_ptr_to_fn, CallFrame, ForInIter, FrameContinuation, Vm, MAX_PROTO_CHAIN_DEPTH}; use oxide_types::object::{JsObject, PropAttributes}; @@ -96,7 +98,6 @@ impl Vm { let sub_bytecode = self.sub_modules[sub_idx].bytecode.clone(); let sub_n_args = self.sub_modules[sub_idx].n_args as usize; let sub_n_registers = self.sub_modules[sub_idx].n_registers; - let sub_constants = self.sub_modules[sub_idx].constants.clone(); let sub_param_base = self.sub_modules[sub_idx].param_base as usize; let caller_reg_limit = self.active_reg_limit.max(1); let saved_reg_offset = self.save_stack.len() as u32; @@ -115,9 +116,8 @@ impl Vm { }; self.regs[255] = constructor; - let converted_sub_constants = self.convert_constants(&sub_constants)?; self.saved_bytecode_stack.push(std::mem::take(&mut self.bytecode)); - self.saved_constants_stack.push(std::mem::take(&mut self.constants)); + self.saved_immutables_stack.push(self.active_immutables); self.frames.push(CallFrame { return_addr: self.pc, @@ -138,7 +138,8 @@ impl Vm { }); self.bytecode = sub_bytecode; - self.constants = converted_sub_constants; + let subs = Arc::clone(&self.sub_modules); + self.activate_immutables(sub_idx + 1, &subs[sub_idx].constants); for (name, reg) in &self.sub_modules[sub_idx].builtin_reg_map { let si = self.kernel_core.perm_interner().intern(name.as_str()).0; @@ -182,8 +183,9 @@ impl Vm { result.push_str(&s); } else { let const_idx = (seg & 0x7FFF_FFFF) as usize; - if const_idx < self.constants.len() { - let val = self.constants[const_idx]; + let imm = self.immutables(); + if const_idx < imm.len() { + let val = imm[const_idx]; if val.is_string() { // SAFETY: val is a string value. let s = unsafe { (*val.as_string_ptr()).data.clone() }; @@ -419,7 +421,7 @@ impl Vm { let excluded_idx = self.bytecode[self.pc] as usize; self.pc += 1; let excluded = self - .constants + .immutables() .get(excluded_idx) .and_then(|v| { if v.is_string() { diff --git a/crates/oxide_vm/src/vm_runtime.rs b/crates/oxide_vm/src/vm_runtime.rs index 8faa7b04..912d237e 100644 --- a/crates/oxide_vm/src/vm_runtime.rs +++ b/crates/oxide_vm/src/vm_runtime.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::sync::{Arc, OnceLock}; use oxide_bytecode::module::CompiledModule; use oxide_bytecode::opcode; @@ -34,13 +34,12 @@ impl Vm { let subs = Arc::clone(&self.sub_modules); let sub = &subs[sub_idx]; - let converted_constants = self.convert_constants(&sub.constants).map_err(String::from)?; let saved = Box::new(InlineSyncState { regs: Box::new(self.regs), pc: self.pc, bytecode: std::mem::take(&mut self.bytecode), - constants: std::mem::take(&mut self.constants), + active_immutables: self.active_immutables, active_reg_limit: self.active_reg_limit, root_reg_limit: self.root_reg_limit, try_stack: std::mem::take(&mut self.try_stack), @@ -52,14 +51,14 @@ impl Vm { for_of_iters: std::mem::take(&mut self.for_of_iters), last_for_of_result: self.last_for_of_result, saved_bytecode_stack: std::mem::take(&mut self.saved_bytecode_stack), - saved_constants_stack: std::mem::take(&mut self.saved_constants_stack), + saved_immutables_stack: std::mem::take(&mut self.saved_immutables_stack), save_stack: std::mem::take(&mut self.save_stack), }); self.regs = [JsValue::undefined(); 256]; self.pc = 0; self.bytecode = sub.bytecode.clone(); - self.constants = converted_constants; + self.activate_immutables(sub_idx + 1, &sub.constants); self.active_reg_limit = sub.n_registers.max(1); self.root_reg_limit = self.active_reg_limit; for i in 0..sub.n_args as usize { @@ -82,7 +81,7 @@ impl Vm { self.regs = *saved.regs; self.pc = saved.pc; self.bytecode = saved.bytecode; - self.constants = saved.constants; + self.active_immutables = saved.active_immutables; self.active_reg_limit = saved.active_reg_limit; self.root_reg_limit = saved.root_reg_limit; self.try_stack = saved.try_stack; @@ -94,7 +93,7 @@ impl Vm { self.for_of_iters = saved.for_of_iters; self.last_for_of_result = saved.last_for_of_result; self.saved_bytecode_stack = saved.saved_bytecode_stack; - self.saved_constants_stack = saved.saved_constants_stack; + self.saved_immutables_stack = saved.saved_immutables_stack; self.save_stack = saved.save_stack; result @@ -104,8 +103,8 @@ impl Vm { if let Some(saved_bc) = self.saved_bytecode_stack.pop() { self.bytecode = saved_bc; } - if let Some(saved_consts) = self.saved_constants_stack.pop() { - self.constants = saved_consts; + if let Some(saved_imm) = self.saved_immutables_stack.pop() { + self.active_immutables = saved_imm; } let offset = frame.saved_reg_offset as usize; let len = frame.caller_reg_limit as usize; @@ -144,9 +143,10 @@ impl Vm { pub fn run(&mut self, module: &CompiledModule) -> Result { self.clear_execution_state(); self.sub_modules = Arc::new(module.sub_modules.clone()); - self.constants = self.convert_constants(&module.constants).map_err(String::from)?; - self.sub_module_constants = vec![Vec::new(); self.sub_modules.len()]; + // Per-run convert-once cache: slot 0 = top module, slot sub_idx+1 = sub_modules[sub_idx]. + self.immutables_cache = (0..=self.sub_modules.len()).map(|_| OnceLock::new()).collect(); self.bytecode = module.bytecode.clone(); + self.activate_immutables(0, &module.constants); self.root_reg_limit = module.n_registers.max(1); self.active_reg_limit = self.root_reg_limit; diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index 9a8b47da..907382a1 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -5,10 +5,9 @@ use std::sync::Arc; use oxide_bytecode::module::Constant; use crate::bindings; -use crate::vm::{native_fn_ptr_to_fn, Vm}; +use crate::vm::Vm; use oxide_kernel::kernel::{KernelConfig, KernelCore, KernelSession}; use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; -use oxide_types::error::JsError; use oxide_types::mem::{Epoch, P}; use oxide_types::object::{JsObject, JsString, PropAttributes}; use oxide_types::value::JsValue; @@ -23,7 +22,8 @@ impl Vm { regs: [JsValue::undefined(); 256], pc: 0, bytecode: Vec::new(), - constants: Vec::new(), + immutables_cache: Vec::new(), + active_immutables: std::ptr::slice_from_raw_parts(std::ptr::null(), 0), frames: smallvec::SmallVec::new(), for_in_iters: Vec::new(), kernel_core: core, @@ -39,9 +39,8 @@ impl Vm { object_prototype: obj_proto, math_rng_state: 0, sub_modules: Arc::new(Vec::new()), - sub_module_constants: Vec::new(), saved_bytecode_stack: Vec::new(), - saved_constants_stack: Vec::new(), + saved_immutables_stack: Vec::new(), save_stack: Vec::new(), try_stack: Vec::new(), exception_value: None, @@ -70,7 +69,8 @@ impl Vm { regs: [JsValue::undefined(); 256], pc: 0, bytecode: Vec::new(), - constants: Vec::new(), + immutables_cache: Vec::new(), + active_immutables: std::ptr::slice_from_raw_parts(std::ptr::null(), 0), frames: smallvec::SmallVec::new(), for_in_iters: Vec::new(), kernel_core: core, @@ -86,9 +86,8 @@ impl Vm { object_prototype: obj_proto, math_rng_state: 0, sub_modules: Arc::new(Vec::new()), - sub_module_constants: Vec::new(), saved_bytecode_stack: Vec::new(), - saved_constants_stack: Vec::new(), + saved_immutables_stack: Vec::new(), save_stack: Vec::new(), try_stack: Vec::new(), exception_value: None, @@ -135,7 +134,8 @@ impl Vm { fn clear_full_reset_state(&mut self) { self.clear_execution_state(); self.bytecode.clear(); - self.constants.clear(); + self.immutables_cache.clear(); + self.active_immutables = std::ptr::slice_from_raw_parts(std::ptr::null(), 0); self.free_epoch_object_heap_data(); self.epoch.reset(); self.epoch_object_ptrs.clear(); @@ -175,7 +175,7 @@ impl Vm { self.for_of_iters.clear(); self.last_for_of_result = JsValue::undefined(); self.saved_bytecode_stack.clear(); - self.saved_constants_stack.clear(); + self.saved_immutables_stack.clear(); self.save_stack.clear(); self.try_stack.clear(); self.exception_value = None; @@ -188,7 +188,8 @@ impl Vm { self.clear_execution_state(); self.maybe_collect_session_gc(); self.bytecode.clear(); - self.constants.clear(); + self.immutables_cache.clear(); + self.active_immutables = std::ptr::slice_from_raw_parts(std::ptr::null(), 0); self.free_epoch_object_heap_data(); self.epoch.reset(); self.epoch_object_ptrs.clear(); @@ -302,68 +303,22 @@ impl Vm { format!("{val}") } - fn convert_constant(&mut self, constant: &Constant) -> Result { + fn convert_constant(&self, constant: &Constant) -> JsValue { match constant { - Constant::Number(v) => Ok(JsValue::float(*v)), - Constant::Int(v) => Ok(JsValue::int(*v)), - Constant::String(s) => Ok(self.perm_string(s)), - Constant::Boolean(b) => Ok(JsValue::bool(*b)), - Constant::Null => Ok(JsValue::null()), - Constant::Undefined => Ok(JsValue::undefined()), - Constant::BytecodeFunc(idx) => { - let sub_idx = *idx as usize; - let (is_arrow, is_class_constructor, is_derived_constructor, needs_home_object) = - if sub_idx > 0 && sub_idx <= self.sub_modules.len() { - let sub_module = &self.sub_modules[sub_idx - 1]; - ( - sub_module.is_arrow, - sub_module.is_class_constructor, - sub_module.is_derived_constructor, - sub_module.needs_home_object, - ) - } else { - (false, false, false, false) - }; - Ok(self.create_function_object( - *idx, - is_arrow, - is_class_constructor, - is_derived_constructor, - needs_home_object, - )) - } - Constant::RegExp(pattern, flags) => { - let pat_val = self.perm_string(pattern); - let flags_val = self.perm_string(flags); - - let ctor_ptr = self.session.builtin_world().regexp_constructor.as_ptr() as *mut JsObject; - let ctor = unsafe { &*ctor_ptr }; - let Some(native_fn) = ctor.native_fn() else { - return Err(JsError::syntax_error("RegExp constructor unavailable")); - }; - - let saved_0 = self.regs[0]; - let saved_1 = self.regs[1]; - let saved_2 = self.regs[2]; - self.regs[0] = JsValue::undefined(); - self.regs[1] = pat_val; - self.regs[2] = flags_val; - let func = unsafe { native_fn_ptr_to_fn(native_fn) }; - let result = func(self, &[0, 1, 2]); - self.regs[0] = saved_0; - self.regs[1] = saved_1; - self.regs[2] = saved_2; - result.map_err(|err| JsError::syntax_error(self.error_text(err))) - } + Constant::Number(v) => JsValue::float(*v), + Constant::Int(v) => JsValue::int(*v), + Constant::String(s) => self.perm_string(s), + Constant::Boolean(b) => JsValue::bool(*b), + Constant::Null => JsValue::null(), + Constant::Undefined => JsValue::undefined(), } } - pub(crate) fn convert_constants(&mut self, constants: &[Constant]) -> Result, JsError> { - let mut values = Vec::with_capacity(constants.len()); - for constant in constants { - values.push(self.convert_constant(constant)?); - } - Ok(values) + /// Convert a module's immutable constant pool to `JsValue`s. Infallible — the pool holds only + /// scalars + permanent-interner string values after CreateClosure/CreateRegExp pulled functions + /// and regexes out of the pool. `&self` so it can run inside `OnceLock::get_or_init`. + pub(crate) fn convert_immutables(&self, constants: &[Constant]) -> Vec { + constants.iter().map(|c| self.convert_constant(c)).collect() } } @@ -483,6 +438,18 @@ mod tests { assert!(unsafe { *session_ptr } == 123); } + #[test] + fn immutables_cache_filled_once_per_module() { + let mut vm = Vm::new(); + // `f` recurses (same sub-module entered 4x). Its immutables convert once via OnceLock. + let result = run_source(&mut vm, "function f(n){ if(n<=0){ return 'done'; } return f(n-1); } f(3)"); + assert!(result.is_string()); + assert_eq!(vm.lookup_str(result).as_deref(), Some("done")); + // Cache = top module + 1 sub-module (f); the sub-module slot was initialized by the calls. + assert_eq!(vm.immutables_cache.len(), 2); + assert!(vm.immutables_cache[1].get().is_some(), "f's immutables should be cached after its calls"); + } + #[test] fn session_epoch_reset_is_only_in_full_reset_state_clear() { let src = include_str!("vm_support.rs"); From 14f82d2c4d49b1ab5a59a8c5b410bf88c13f9cc6 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Thu, 25 Jun 2026 18:12:37 +0800 Subject: [PATCH 16/32] =?UTF-8?q?refactor:=20=E7=94=A8=20define=5Fopcodes!?= =?UTF-8?q?=20=E5=AE=8F=E7=BB=9F=E4=B8=80=E7=94=9F=E6=88=90=20OpCode=20?= =?UTF-8?q?=E6=9E=9A=E4=B8=BE/TryFrom/Display?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_bytecode/src/opcode.rs | 553 ++++++++++------------------ 1 file changed, 188 insertions(+), 365 deletions(-) diff --git a/crates/oxide_bytecode/src/opcode.rs b/crates/oxide_bytecode/src/opcode.rs index e6e29643..8fbbbbff 100644 --- a/crates/oxide_bytecode/src/opcode.rs +++ b/crates/oxide_bytecode/src/opcode.rs @@ -1,157 +1,193 @@ use std::fmt; -/// OpCode for register-based bytecode VM. +/// Generate the `OpCode` enum plus its `TryFrom` and `Display` impls from a +/// single table. Each row is `VARIANT = 0xXX => "DISPLAY"` — the byte value is +/// the enum discriminant, the `TryFrom` match arm, and the `Display` name come +/// from the same row, so the three tables can never drift. A new opcode is one +/// row. /// -/// Organized in groups of 16 for readability. Implemented opcodes have -/// emitter support in the compiler; placeholder opcodes are reserved -/// for future phases (IC, profiling, parallelization). -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[allow(non_camel_case_types)] -pub enum OpCode { +/// Grouping comments (`// -- Arithmetic --`) are stripped by the lexer before +/// macro expansion, so they can be interleaved freely between rows. +macro_rules! define_opcodes { + ( $( $name:ident = $val:literal => $disp:literal ),+ $(,)? ) => { + /// OpCode for register-based bytecode VM. + /// + /// Organized in groups of 16 for readability. Implemented opcodes have + /// emitter support in the compiler; placeholder opcodes are reserved + /// for future phases (IC, profiling, parallelization). + #[repr(u8)] + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + #[allow(non_camel_case_types)] + pub enum OpCode { + $( $name = $val, )+ + } + + impl TryFrom for OpCode { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + $( $val => Ok(OpCode::$name), )+ + _ => Err(()), + } + } + } + + impl fmt::Display for OpCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match self { + $( OpCode::$name => $disp, )+ + }; + write!(f, "{name}") + } + } + }; +} + +define_opcodes! { // ── Arithmetic (0x00-0x0F) ── - ADD = 0x00, - SUB = 0x01, - MUL = 0x02, - DIV = 0x03, - MOD = 0x04, - NEG = 0x05, - COMPOUND_ADD = 0x06, - COMPOUND_SUB = 0x07, - COMPOUND_MUL = 0x08, - COMPOUND_DIV = 0x09, - COMPOUND_MOD = 0x0A, - COMPOUND_EXP = 0x0B, + ADD = 0x00 => "ADD", + SUB = 0x01 => "SUB", + MUL = 0x02 => "MUL", + DIV = 0x03 => "DIV", + MOD = 0x04 => "MOD", + NEG = 0x05 => "NEG", + COMPOUND_ADD = 0x06 => "COMPOUND_ADD", + COMPOUND_SUB = 0x07 => "COMPOUND_SUB", + COMPOUND_MUL = 0x08 => "COMPOUND_MUL", + COMPOUND_DIV = 0x09 => "COMPOUND_DIV", + COMPOUND_MOD = 0x0A => "COMPOUND_MOD", + COMPOUND_EXP = 0x0B => "COMPOUND_EXP", // -- Comparison (0x10-0x1F) -- - EQ = 0x10, - NEQ = 0x11, - LT = 0x12, - GT = 0x13, - LTE = 0x14, - GTE = 0x15, - IN = 0x16, - NOT = 0x17, - AND = 0x18, - OR = 0x19, - STRICT_EQ = 0x1A, - STRICT_NEQ = 0x1C, - UNARY_PLUS = 0x1D, + EQ = 0x10 => "EQ", + NEQ = 0x11 => "NEQ", + LT = 0x12 => "LT", + GT = 0x13 => "GT", + LTE = 0x14 => "LTE", + GTE = 0x15 => "GTE", + IN = 0x16 => "IN", + NOT = 0x17 => "NOT", + AND = 0x18 => "AND", + OR = 0x19 => "OR", + STRICT_EQ = 0x1A => "STRICT_EQ", + STRICT_NEQ = 0x1C => "STRICT_NEQ", + UNARY_PLUS = 0x1D => "UNARY_PLUS", // -- Control Flow (0x20-0x2F) -- - JMP = 0x20, - JMP_IF_FALSE = 0x21, - JMP_IF_TRUE = 0x22, - FOR_OF_INIT = 0x23, - FOR_OF_NEXT = 0x24, + JMP = 0x20 => "JMP", + JMP_IF_FALSE = 0x21 => "JMP_IF_FALSE", + JMP_IF_TRUE = 0x22 => "JMP_IF_TRUE", + FOR_OF_INIT = 0x23 => "FOR_OF_INIT", + FOR_OF_NEXT = 0x24 => "FOR_OF_NEXT", // -- Update (0x25-0x28) -- - INC_PRE = 0x25, - INC_POST = 0x26, - DEC_PRE = 0x27, - DEC_POST = 0x28, + INC_PRE = 0x25 => "INC_PRE", + INC_POST = 0x26 => "INC_POST", + DEC_PRE = 0x27 => "DEC_PRE", + DEC_POST = 0x28 => "DEC_POST", - FOR_IN_INIT = 0x29, - FOR_IN_NEXT = 0x2A, - FOR_IN_DONE = 0x2B, - SWITCH_TABLE = 0x2C, - FOR_IN_CLEANUP = 0x2D, + FOR_IN_INIT = 0x29 => "FOR_IN_INIT", + FOR_IN_NEXT = 0x2A => "FOR_IN_NEXT", + FOR_IN_DONE = 0x2B => "FOR_IN_DONE", + SWITCH_TABLE = 0x2C => "SWITCH_TABLE", + FOR_IN_CLEANUP = 0x2D => "FOR_IN_CLEANUP", // -- Exception (0x2E-0x2F, 0x33-0x35) -- - THROW = 0x2E, - TRY_BEGIN = 0x2F, - TRY_END = 0x33, - TRY_FINALLY_BEGIN = 0x34, - TRY_FINALLY_END = 0x35, - FOR_OF_DONE = 0x36, - FOR_OF_CLOSE = 0x37, + THROW = 0x2E => "THROW", + TRY_BEGIN = 0x2F => "TRY_BEGIN", + TRY_END = 0x33 => "TRY_END", + TRY_FINALLY_BEGIN = 0x34 => "TRY_FINALLY_BEGIN", + TRY_FINALLY_END = 0x35 => "TRY_FINALLY_END", + FOR_OF_DONE = 0x36 => "FOR_OF_DONE", + FOR_OF_CLOSE = 0x37 => "FOR_OF_CLOSE", // -- Template Literal (0x38) -- - TEMPLATE_STR = 0x38, + TEMPLATE_STR = 0x38 => "TEMPLATE_STR", // -- Small Language Features (0x39-0x3B) -- - DELETE_PROP_STATIC = 0x39, - DELETE_PROP_DYNAMIC = 0x3A, - INSTANCEOF = 0x3B, - REST_OBJECT = 0x3E, + DELETE_PROP_STATIC = 0x39 => "DELETE_PROP_STATIC", + DELETE_PROP_DYNAMIC = 0x3A => "DELETE_PROP_DYNAMIC", + INSTANCEOF = 0x3B => "INSTANCEOF", + REST_OBJECT = 0x3E => "REST_OBJECT", // -- Variable (0x30-0x32) -- - LOAD_VAR = 0x30, - STORE_VAR = 0x31, - LOAD_CONST = 0x32, + LOAD_VAR = 0x30 => "LOAD_VAR", + STORE_VAR = 0x31 => "STORE_VAR", + LOAD_CONST = 0x32 => "LOAD_CONST", // -- Call (0x40-0x4F) -- - CALL = 0x40, - RETURN = 0x41, - CALL_NATIVE = 0x42, - NEW_EXPRESSION = 0x43, - SUPER_CALL = 0x44, - SUPER_GET_PROP = 0x45, - SUPER_STATIC_GET_PROP = 0x46, - SET_HOME_OBJECT = 0x47, - DEFINE_ACCESSOR = 0x48, - CREATE_CLOSURE = 0x4A, - CREATE_REGEXP = 0x4B, + CALL = 0x40 => "CALL", + RETURN = 0x41 => "RETURN", + CALL_NATIVE = 0x42 => "CALL_NATIVE", + NEW_EXPRESSION = 0x43 => "NEW_EXPRESSION", + SUPER_CALL = 0x44 => "SUPER_CALL", + SUPER_GET_PROP = 0x45 => "SUPER_GET_PROP", + SUPER_STATIC_GET_PROP = 0x46 => "SUPER_STATIC_GET_PROP", + SET_HOME_OBJECT = 0x47 => "SET_HOME_OBJECT", + DEFINE_ACCESSOR = 0x48 => "DEFINE_ACCESSOR", + CREATE_CLOSURE = 0x4A => "CREATE_CLOSURE", + CREATE_REGEXP = 0x4B => "CREATE_REGEXP", // ── Object Property (0x50-0x5F) ── - IC_GET_PROP = 0x50, - IC_SET_PROP = 0x51, - GET_PROP = 0x52, - GET_PROP_DYNAMIC = 0x53, - SET_PROP = 0x54, - SET_PROP_DYNAMIC = 0x55, - NEW_OBJECT = 0x56, - NEW_ARRAY = 0x57, - SET_ELEM = 0x58, + IC_GET_PROP = 0x50 => "IC_GET_PROP", + IC_SET_PROP = 0x51 => "IC_SET_PROP", + GET_PROP = 0x52 => "GET_PROP", + GET_PROP_DYNAMIC = 0x53 => "GET_PROP_DYNAMIC", + SET_PROP = 0x54 => "SET_PROP", + SET_PROP_DYNAMIC = 0x55 => "SET_PROP_DYNAMIC", + NEW_OBJECT = 0x56 => "NEW_OBJECT", + NEW_ARRAY = 0x57 => "NEW_ARRAY", + SET_ELEM = 0x58 => "SET_ELEM", // -- Member Update (0x59-0x62) -- - MEMBER_INC = 0x59, - MEMBER_DEC = 0x5A, - DYN_MEMBER_INC = 0x5B, - DYN_MEMBER_DEC = 0x5C, - COMPOUND_MEMBER_ADD = 0x5D, - COMPOUND_MEMBER_SUB = 0x5E, - COMPOUND_MEMBER_MUL = 0x5F, - COMPOUND_MEMBER_DIV = 0x60, - COMPOUND_MEMBER_MOD = 0x61, - COMPOUND_MEMBER_EXP = 0x62, + MEMBER_INC = 0x59 => "MEMBER_INC", + MEMBER_DEC = 0x5A => "MEMBER_DEC", + DYN_MEMBER_INC = 0x5B => "DYN_MEMBER_INC", + DYN_MEMBER_DEC = 0x5C => "DYN_MEMBER_DEC", + COMPOUND_MEMBER_ADD = 0x5D => "COMPOUND_MEMBER_ADD", + COMPOUND_MEMBER_SUB = 0x5E => "COMPOUND_MEMBER_SUB", + COMPOUND_MEMBER_MUL = 0x5F => "COMPOUND_MEMBER_MUL", + COMPOUND_MEMBER_DIV = 0x60 => "COMPOUND_MEMBER_DIV", + COMPOUND_MEMBER_MOD = 0x61 => "COMPOUND_MEMBER_MOD", + COMPOUND_MEMBER_EXP = 0x62 => "COMPOUND_MEMBER_EXP", // -- Profiling - placeholders (0x63-0x6F) -- - PROFILE_TYPE = 0x63, - PROFILE_SHAPE = 0x64, - PROFILE_BRANCH = 0x65, - PROFILE_CALL = 0x66, + PROFILE_TYPE = 0x63 => "PROFILE_TYPE", + PROFILE_SHAPE = 0x64 => "PROFILE_SHAPE", + PROFILE_BRANCH = 0x65 => "PROFILE_BRANCH", + PROFILE_CALL = 0x66 => "PROFILE_CALL", // ── Parallel — placeholders (0x70-0x7F) ── - FORK = 0x70, - JOIN = 0x71, - GET_PRIVATE = 0x72, - SET_PRIVATE = 0x73, - INIT_PRIVATE = 0x74, - PRIVATE_BRAND_IN = 0x75, + FORK = 0x70 => "FORK", + JOIN = 0x71 => "JOIN", + GET_PRIVATE = 0x72 => "GET_PRIVATE", + SET_PRIVATE = 0x73 => "SET_PRIVATE", + INIT_PRIVATE = 0x74 => "INIT_PRIVATE", + PRIVATE_BRAND_IN = 0x75 => "PRIVATE_BRAND_IN", // -- Bitwise (0x80-0x8F) -- - BIT_AND = 0x80, - BIT_OR = 0x81, - BIT_XOR = 0x82, - SHL = 0x83, - SHR = 0x84, - USHR = 0x85, - BIT_NOT = 0x86, - COMPOUND_AND = 0x88, - COMPOUND_OR = 0x89, - COMPOUND_XOR = 0x8A, - COMPOUND_SHL = 0x8B, - COMPOUND_SHR = 0x8C, - COMPOUND_USHR = 0x8D, - NULLISH = 0x8E, - JMP_IF_NULLISH = 0x8F, + BIT_AND = 0x80 => "BIT_AND", + BIT_OR = 0x81 => "BIT_OR", + BIT_XOR = 0x82 => "BIT_XOR", + SHL = 0x83 => "SHL", + SHR = 0x84 => "SHR", + USHR = 0x85 => "USHR", + BIT_NOT = 0x86 => "BIT_NOT", + COMPOUND_AND = 0x88 => "COMPOUND_AND", + COMPOUND_OR = 0x89 => "COMPOUND_OR", + COMPOUND_XOR = 0x8A => "COMPOUND_XOR", + COMPOUND_SHL = 0x8B => "COMPOUND_SHL", + COMPOUND_SHR = 0x8C => "COMPOUND_SHR", + COMPOUND_USHR = 0x8D => "COMPOUND_USHR", + NULLISH = 0x8E => "NULLISH", + JMP_IF_NULLISH = 0x8F => "JMP_IF_NULLISH", // ── Misc (0xF0-0xFF) ── - NOP = 0xF0, - HALT = 0xF1, - TYPEOF = 0xF2, - VOID = 0xF3, + NOP = 0xF0 => "NOP", + HALT = 0xF1 => "HALT", + TYPEOF = 0xF2 => "TYPEOF", + VOID = 0xF3 => "VOID", } impl OpCode { @@ -172,250 +208,6 @@ impl OpCode { } } -impl TryFrom for OpCode { - type Error = (); - - fn try_from(value: u8) -> Result { - match value { - 0x00 => Ok(OpCode::ADD), - 0x01 => Ok(OpCode::SUB), - 0x02 => Ok(OpCode::MUL), - 0x03 => Ok(OpCode::DIV), - 0x04 => Ok(OpCode::MOD), - 0x05 => Ok(OpCode::NEG), - 0x06 => Ok(OpCode::COMPOUND_ADD), - 0x07 => Ok(OpCode::COMPOUND_SUB), - 0x08 => Ok(OpCode::COMPOUND_MUL), - 0x09 => Ok(OpCode::COMPOUND_DIV), - 0x0A => Ok(OpCode::COMPOUND_MOD), - 0x0B => Ok(OpCode::COMPOUND_EXP), - 0x10 => Ok(OpCode::EQ), - 0x11 => Ok(OpCode::NEQ), - 0x12 => Ok(OpCode::LT), - 0x13 => Ok(OpCode::GT), - 0x14 => Ok(OpCode::LTE), - 0x15 => Ok(OpCode::GTE), - 0x16 => Ok(OpCode::IN), - 0x17 => Ok(OpCode::NOT), - 0x18 => Ok(OpCode::AND), - 0x19 => Ok(OpCode::OR), - 0x1A => Ok(OpCode::STRICT_EQ), - 0x1C => Ok(OpCode::STRICT_NEQ), - 0x1D => Ok(OpCode::UNARY_PLUS), - 0x20 => Ok(OpCode::JMP), - 0x21 => Ok(OpCode::JMP_IF_FALSE), - 0x22 => Ok(OpCode::JMP_IF_TRUE), - 0x23 => Ok(OpCode::FOR_OF_INIT), - 0x24 => Ok(OpCode::FOR_OF_NEXT), - 0x25 => Ok(OpCode::INC_PRE), - 0x26 => Ok(OpCode::INC_POST), - 0x27 => Ok(OpCode::DEC_PRE), - 0x28 => Ok(OpCode::DEC_POST), - 0x29 => Ok(OpCode::FOR_IN_INIT), - 0x2A => Ok(OpCode::FOR_IN_NEXT), - 0x2B => Ok(OpCode::FOR_IN_DONE), - 0x2C => Ok(OpCode::SWITCH_TABLE), - 0x2D => Ok(OpCode::FOR_IN_CLEANUP), - 0x2E => Ok(OpCode::THROW), - 0x2F => Ok(OpCode::TRY_BEGIN), - 0x30 => Ok(OpCode::LOAD_VAR), - 0x31 => Ok(OpCode::STORE_VAR), - 0x32 => Ok(OpCode::LOAD_CONST), - 0x33 => Ok(OpCode::TRY_END), - 0x34 => Ok(OpCode::TRY_FINALLY_BEGIN), - 0x35 => Ok(OpCode::TRY_FINALLY_END), - 0x36 => Ok(OpCode::FOR_OF_DONE), - 0x37 => Ok(OpCode::FOR_OF_CLOSE), - 0x38 => Ok(OpCode::TEMPLATE_STR), - 0x39 => Ok(OpCode::DELETE_PROP_STATIC), - 0x3A => Ok(OpCode::DELETE_PROP_DYNAMIC), - 0x3B => Ok(OpCode::INSTANCEOF), - 0x3E => Ok(OpCode::REST_OBJECT), - 0x40 => Ok(OpCode::CALL), - 0x41 => Ok(OpCode::RETURN), - 0x42 => Ok(OpCode::CALL_NATIVE), - 0x43 => Ok(OpCode::NEW_EXPRESSION), - 0x44 => Ok(OpCode::SUPER_CALL), - 0x45 => Ok(OpCode::SUPER_GET_PROP), - 0x46 => Ok(OpCode::SUPER_STATIC_GET_PROP), - 0x47 => Ok(OpCode::SET_HOME_OBJECT), - 0x48 => Ok(OpCode::DEFINE_ACCESSOR), - 0x4A => Ok(OpCode::CREATE_CLOSURE), - 0x4B => Ok(OpCode::CREATE_REGEXP), - 0x50 => Ok(OpCode::IC_GET_PROP), - 0x51 => Ok(OpCode::IC_SET_PROP), - 0x52 => Ok(OpCode::GET_PROP), - 0x53 => Ok(OpCode::GET_PROP_DYNAMIC), - 0x54 => Ok(OpCode::SET_PROP), - 0x55 => Ok(OpCode::SET_PROP_DYNAMIC), - 0x56 => Ok(OpCode::NEW_OBJECT), - 0x57 => Ok(OpCode::NEW_ARRAY), - 0x58 => Ok(OpCode::SET_ELEM), - 0x59 => Ok(OpCode::MEMBER_INC), - 0x5A => Ok(OpCode::MEMBER_DEC), - 0x5B => Ok(OpCode::DYN_MEMBER_INC), - 0x5C => Ok(OpCode::DYN_MEMBER_DEC), - 0x5D => Ok(OpCode::COMPOUND_MEMBER_ADD), - 0x5E => Ok(OpCode::COMPOUND_MEMBER_SUB), - 0x5F => Ok(OpCode::COMPOUND_MEMBER_MUL), - 0x60 => Ok(OpCode::COMPOUND_MEMBER_DIV), - 0x61 => Ok(OpCode::COMPOUND_MEMBER_MOD), - 0x62 => Ok(OpCode::COMPOUND_MEMBER_EXP), - 0x63 => Ok(OpCode::PROFILE_TYPE), - 0x64 => Ok(OpCode::PROFILE_SHAPE), - 0x65 => Ok(OpCode::PROFILE_BRANCH), - 0x66 => Ok(OpCode::PROFILE_CALL), - 0x70 => Ok(OpCode::FORK), - 0x71 => Ok(OpCode::JOIN), - 0x72 => Ok(OpCode::GET_PRIVATE), - 0x73 => Ok(OpCode::SET_PRIVATE), - 0x74 => Ok(OpCode::INIT_PRIVATE), - 0x75 => Ok(OpCode::PRIVATE_BRAND_IN), - 0x80 => Ok(OpCode::BIT_AND), - 0x81 => Ok(OpCode::BIT_OR), - 0x82 => Ok(OpCode::BIT_XOR), - 0x83 => Ok(OpCode::SHL), - 0x84 => Ok(OpCode::SHR), - 0x85 => Ok(OpCode::USHR), - 0x86 => Ok(OpCode::BIT_NOT), - 0x88 => Ok(OpCode::COMPOUND_AND), - 0x89 => Ok(OpCode::COMPOUND_OR), - 0x8A => Ok(OpCode::COMPOUND_XOR), - 0x8B => Ok(OpCode::COMPOUND_SHL), - 0x8C => Ok(OpCode::COMPOUND_SHR), - 0x8D => Ok(OpCode::COMPOUND_USHR), - 0x8E => Ok(OpCode::NULLISH), - 0x8F => Ok(OpCode::JMP_IF_NULLISH), - 0xF0 => Ok(OpCode::NOP), - 0xF1 => Ok(OpCode::HALT), - 0xF2 => Ok(OpCode::TYPEOF), - 0xF3 => Ok(OpCode::VOID), - _ => Err(()), - } - } -} - -impl fmt::Display for OpCode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let name = match self { - OpCode::ADD => "ADD", - OpCode::SUB => "SUB", - OpCode::MUL => "MUL", - OpCode::DIV => "DIV", - OpCode::MOD => "MOD", - OpCode::NEG => "NEG", - OpCode::COMPOUND_ADD => "COMPOUND_ADD", - OpCode::COMPOUND_SUB => "COMPOUND_SUB", - OpCode::COMPOUND_MUL => "COMPOUND_MUL", - OpCode::COMPOUND_DIV => "COMPOUND_DIV", - OpCode::COMPOUND_MOD => "COMPOUND_MOD", - OpCode::COMPOUND_EXP => "COMPOUND_EXP", - OpCode::EQ => "EQ", - OpCode::NEQ => "NEQ", - OpCode::LT => "LT", - OpCode::GT => "GT", - OpCode::LTE => "LTE", - OpCode::GTE => "GTE", - OpCode::IN => "IN", - OpCode::NOT => "NOT", - OpCode::AND => "AND", - OpCode::OR => "OR", - OpCode::STRICT_EQ => "STRICT_EQ", - OpCode::STRICT_NEQ => "STRICT_NEQ", - OpCode::UNARY_PLUS => "UNARY_PLUS", - OpCode::JMP => "JMP", - OpCode::JMP_IF_FALSE => "JMP_IF_FALSE", - OpCode::JMP_IF_TRUE => "JMP_IF_TRUE", - OpCode::FOR_OF_INIT => "FOR_OF_INIT", - OpCode::FOR_OF_NEXT => "FOR_OF_NEXT", - OpCode::INC_PRE => "INC_PRE", - OpCode::INC_POST => "INC_POST", - OpCode::DEC_PRE => "DEC_PRE", - OpCode::DEC_POST => "DEC_POST", - OpCode::FOR_IN_INIT => "FOR_IN_INIT", - OpCode::FOR_IN_NEXT => "FOR_IN_NEXT", - OpCode::FOR_IN_DONE => "FOR_IN_DONE", - OpCode::SWITCH_TABLE => "SWITCH_TABLE", - OpCode::FOR_IN_CLEANUP => "FOR_IN_CLEANUP", - OpCode::THROW => "THROW", - OpCode::TRY_BEGIN => "TRY_BEGIN", - OpCode::TRY_END => "TRY_END", - OpCode::TRY_FINALLY_BEGIN => "TRY_FINALLY_BEGIN", - OpCode::TRY_FINALLY_END => "TRY_FINALLY_END", - OpCode::FOR_OF_DONE => "FOR_OF_DONE", - OpCode::FOR_OF_CLOSE => "FOR_OF_CLOSE", - OpCode::TEMPLATE_STR => "TEMPLATE_STR", - OpCode::DELETE_PROP_STATIC => "DELETE_PROP_STATIC", - OpCode::DELETE_PROP_DYNAMIC => "DELETE_PROP_DYNAMIC", - OpCode::INSTANCEOF => "INSTANCEOF", - OpCode::REST_OBJECT => "REST_OBJECT", - OpCode::LOAD_VAR => "LOAD_VAR", - OpCode::STORE_VAR => "STORE_VAR", - OpCode::LOAD_CONST => "LOAD_CONST", - OpCode::CALL => "CALL", - OpCode::RETURN => "RETURN", - OpCode::CALL_NATIVE => "CALL_NATIVE", - OpCode::NEW_EXPRESSION => "NEW_EXPRESSION", - OpCode::SUPER_CALL => "SUPER_CALL", - OpCode::SUPER_GET_PROP => "SUPER_GET_PROP", - OpCode::SUPER_STATIC_GET_PROP => "SUPER_STATIC_GET_PROP", - OpCode::SET_HOME_OBJECT => "SET_HOME_OBJECT", - OpCode::DEFINE_ACCESSOR => "DEFINE_ACCESSOR", - OpCode::CREATE_CLOSURE => "CREATE_CLOSURE", - OpCode::CREATE_REGEXP => "CREATE_REGEXP", - OpCode::IC_GET_PROP => "IC_GET_PROP", - OpCode::IC_SET_PROP => "IC_SET_PROP", - OpCode::GET_PROP => "GET_PROP", - OpCode::GET_PROP_DYNAMIC => "GET_PROP_DYNAMIC", - OpCode::SET_PROP => "SET_PROP", - OpCode::SET_PROP_DYNAMIC => "SET_PROP_DYNAMIC", - OpCode::NEW_OBJECT => "NEW_OBJECT", - OpCode::NEW_ARRAY => "NEW_ARRAY", - OpCode::SET_ELEM => "SET_ELEM", - OpCode::MEMBER_INC => "MEMBER_INC", - OpCode::MEMBER_DEC => "MEMBER_DEC", - OpCode::DYN_MEMBER_INC => "DYN_MEMBER_INC", - OpCode::DYN_MEMBER_DEC => "DYN_MEMBER_DEC", - OpCode::COMPOUND_MEMBER_ADD => "COMPOUND_MEMBER_ADD", - OpCode::COMPOUND_MEMBER_SUB => "COMPOUND_MEMBER_SUB", - OpCode::COMPOUND_MEMBER_MUL => "COMPOUND_MEMBER_MUL", - OpCode::COMPOUND_MEMBER_DIV => "COMPOUND_MEMBER_DIV", - OpCode::COMPOUND_MEMBER_MOD => "COMPOUND_MEMBER_MOD", - OpCode::COMPOUND_MEMBER_EXP => "COMPOUND_MEMBER_EXP", - OpCode::PROFILE_TYPE => "PROFILE_TYPE", - OpCode::PROFILE_SHAPE => "PROFILE_SHAPE", - OpCode::PROFILE_BRANCH => "PROFILE_BRANCH", - OpCode::PROFILE_CALL => "PROFILE_CALL", - OpCode::FORK => "FORK", - OpCode::JOIN => "JOIN", - OpCode::GET_PRIVATE => "GET_PRIVATE", - OpCode::SET_PRIVATE => "SET_PRIVATE", - OpCode::INIT_PRIVATE => "INIT_PRIVATE", - OpCode::PRIVATE_BRAND_IN => "PRIVATE_BRAND_IN", - OpCode::BIT_AND => "BIT_AND", - OpCode::BIT_OR => "BIT_OR", - OpCode::BIT_XOR => "BIT_XOR", - OpCode::SHL => "SHL", - OpCode::SHR => "SHR", - OpCode::USHR => "USHR", - OpCode::BIT_NOT => "BIT_NOT", - OpCode::COMPOUND_AND => "COMPOUND_AND", - OpCode::COMPOUND_OR => "COMPOUND_OR", - OpCode::COMPOUND_XOR => "COMPOUND_XOR", - OpCode::COMPOUND_SHL => "COMPOUND_SHL", - OpCode::COMPOUND_SHR => "COMPOUND_SHR", - OpCode::COMPOUND_USHR => "COMPOUND_USHR", - OpCode::NULLISH => "NULLISH", - OpCode::JMP_IF_NULLISH => "JMP_IF_NULLISH", - OpCode::NOP => "NOP", - OpCode::HALT => "HALT", - OpCode::TYPEOF => "TYPEOF", - OpCode::VOID => "VOID", - }; - write!(f, "{name}") - } -} - /// 4-byte instruction. /// /// Layout: `[opcode: u8] [rd: u8] [a: u8] [b: u8]` @@ -487,3 +279,34 @@ pub fn encode_try_finally_begin(offset: i16) -> Instr { let hi = ((offset as u16 >> 8) & 0xFF) as u8; encode(OpCode::TRY_FINALLY_BEGIN, 0, lo, hi) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn opcode_table_is_byte_identical() { + for byte in 0u8..=u8::MAX { + if let Ok(op) = OpCode::try_from(byte) { + assert_eq!(op as u8, byte, "discriminant mismatch for 0x{byte:02X}"); + assert!(!op.to_string().is_empty(), "empty Display for 0x{byte:02X}"); + } + } + + assert_eq!(OpCode::ADD as u8, 0x00); + assert_eq!(OpCode::STRICT_EQ as u8, 0x1A); + assert_eq!(OpCode::JMP_IF_NULLISH as u8, 0x8F); + assert_eq!(OpCode::VOID as u8, 0xF3); + assert_eq!(OpCode::ADD.to_string(), "ADD"); + assert_eq!(OpCode::COMPOUND_MEMBER_EXP.to_string(), "COMPOUND_MEMBER_EXP"); + + assert!(OpCode::try_from(0x1B).is_err()); + assert!(OpCode::try_from(0xFF).is_err()); + + let ic_count = (0u8..=u8::MAX) + .filter_map(|b| OpCode::try_from(b).ok()) + .filter(OpCode::has_ic_ext_words) + .count(); + assert_eq!(ic_count, 10); + } +} From 00c38ba5f9904b1fd18880ac83bc3a7ff216e25a Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Thu, 25 Jun 2026 18:32:09 +0800 Subject: [PATCH 17/32] =?UTF-8?q?feat:=20=E8=BF=81=E7=A7=BB=20NativeResult?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 9 ++ Cargo.toml | 1 + crates/oxide_runtime_api/Cargo.toml | 11 ++ crates/oxide_runtime_api/src/lib.rs | 108 ++++++++++++++++++ crates/oxide_vm/Cargo.toml | 1 + crates/oxide_vm/src/builtins/array.rs | 2 +- crates/oxide_vm/src/builtins/array_buffer.rs | 2 +- crates/oxide_vm/src/builtins/boolean.rs | 2 +- crates/oxide_vm/src/builtins/data_view.rs | 2 +- crates/oxide_vm/src/builtins/date.rs | 2 +- crates/oxide_vm/src/builtins/error.rs | 2 +- crates/oxide_vm/src/builtins/function.rs | 2 +- .../oxide_vm/src/builtins/global/annex_b.rs | 2 +- crates/oxide_vm/src/builtins/global/uri.rs | 2 +- crates/oxide_vm/src/builtins/iterator.rs | 2 +- crates/oxide_vm/src/builtins/json.rs | 2 +- crates/oxide_vm/src/builtins/map.rs | 2 +- crates/oxide_vm/src/builtins/math.rs | 2 +- crates/oxide_vm/src/builtins/number.rs | 2 +- crates/oxide_vm/src/builtins/object.rs | 2 +- crates/oxide_vm/src/builtins/reflect.rs | 2 +- crates/oxide_vm/src/builtins/regexp.rs | 2 +- crates/oxide_vm/src/builtins/set.rs | 2 +- crates/oxide_vm/src/builtins/string.rs | 2 +- crates/oxide_vm/src/builtins/stubs.rs | 2 +- crates/oxide_vm/src/builtins/symbol.rs | 2 +- crates/oxide_vm/src/builtins/typed_array.rs | 2 +- crates/oxide_vm/src/dispatch/call.rs | 3 +- crates/oxide_vm/src/native.rs | 37 +----- crates/oxide_vm/src/session_gc.rs | 2 +- crates/oxide_vm/src/vm.rs | 11 +- crates/oxide_vm/src/vm_dispatch_misc.rs | 3 +- 32 files changed, 164 insertions(+), 66 deletions(-) create mode 100644 crates/oxide_runtime_api/Cargo.toml create mode 100644 crates/oxide_runtime_api/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 67ada84c..5b2773f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1101,6 +1101,14 @@ dependencies = [ "oxc_syntax", ] +[[package]] +name = "oxide_runtime_api" +version = "0.1.0" +dependencies = [ + "oxide_kernel", + "oxide_types", +] + [[package]] name = "oxide_test262" version = "0.1.0" @@ -1139,6 +1147,7 @@ dependencies = [ "oxide_compiler", "oxide_kernel", "oxide_parser", + "oxide_runtime_api", "oxide_types", "proptest", "regex", diff --git a/Cargo.toml b/Cargo.toml index 6e83ad8a..71a4b20b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "crates/oxide_types", "crates/oxide_vm", "crates/oxide_kernel", + "crates/oxide_runtime_api", "crates/oxide_api", "crates/oxide_cli", "crates/oxide_test262", diff --git a/crates/oxide_runtime_api/Cargo.toml b/crates/oxide_runtime_api/Cargo.toml new file mode 100644 index 00000000..8d10ca32 --- /dev/null +++ b/crates/oxide_runtime_api/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "oxide_runtime_api" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +description = "OxideJS - VmHost trait: the abstract Vm interface builtins are written against" + +[dependencies] +oxide_types = { path = "../oxide_types" } +oxide_kernel = { path = "../oxide_kernel" } diff --git a/crates/oxide_runtime_api/src/lib.rs b/crates/oxide_runtime_api/src/lib.rs new file mode 100644 index 00000000..26fe8342 --- /dev/null +++ b/crates/oxide_runtime_api/src/lib.rs @@ -0,0 +1,108 @@ +//! `oxide_runtime_api` — the abstract interface between builtins and the VM. +//! +//! Builtins are written generically against the [`VmHost`] trait +//! (`fn xxx(host: &mut H, args: &[u8]) -> NativeResult`); `Vm` +//! implements `VmHost`. This breaks what would otherwise be a circular +//! dependency between the builtins crate and `oxide_vm`: +//! +//! `oxide_types ← oxide_kernel ← oxide_runtime_api ← oxide_builtins ← oxide_vm` +//! +//! The trait is GENERIC-friendly, not object-safe: monomorphizing `H = Vm` +//! inlines every `host.*()` call, so there is zero runtime overhead versus +//! builtins living inside `oxide_vm`. + +use std::sync::Arc; + +use oxide_kernel::kernel::{KernelCore, KernelSession}; +use oxide_types::mem::Epoch; +use oxide_types::object::{JsObject, PropAttributes}; +use oxide_types::value::JsValue; + +/// Return value of every builtin native function. +pub enum NativeResult { + Ok(JsValue), + Err(JsValue), + TailCall { callee: JsValue, this: JsValue, args: Vec }, +} + +impl NativeResult { + pub fn ok(val: JsValue) -> Self { + Self::Ok(val) + } + + pub fn err(val: JsValue) -> Self { + Self::Err(val) + } + + pub fn unwrap(self) -> JsValue { + match self { + Self::Ok(val) => val, + Self::Err(_) => panic!("called `NativeResult::unwrap()` on an `Err` value"), + Self::TailCall { .. } => panic!("called `NativeResult::unwrap()` on a `TailCall` value"), + } + } + + pub fn map_err(self, op: F) -> Result + where + F: FnOnce(JsValue) -> E, + { + match self { + Self::Ok(val) => Ok(val), + Self::Err(err) => Err(op(err)), + Self::TailCall { .. } => panic!("TailCall cannot be converted to Result"), + } + } +} + +/// The set of `Vm` capabilities that builtins depend on. +/// +/// Signatures are byte-for-byte copies of the corresponding inherent methods on +/// `Vm`; `impl VmHost for Vm` delegates to them. The trait is intentionally +/// flat and not object-safe — builtins always take `&mut impl VmHost`. +pub trait VmHost { + // Register access + fn reg(&self, idx: u8) -> JsValue; + fn set_reg(&mut self, idx: u8, val: JsValue); + + // Object allocation / string creation + fn alloc_object(&mut self, obj: JsObject) -> *mut JsObject; + fn new_string(&mut self, s: &str) -> JsValue; + + // Kernel accessors + fn kernel_core(&self) -> &Arc; + fn session(&self) -> &KernelSession; + fn epoch(&self) -> &Epoch; + + // Property resolution + fn property_key_si(&mut self, val: JsValue) -> u32; + fn resolve_property(&self, obj: &JsObject, prop_name_si: u32) -> Option; + fn get_own_property_slot(&self, obj: &JsObject, prop_name_si: u32) -> Option; + + // Property access + fn ordinary_get(&mut self, obj: &JsObject, prop_name_si: u32, receiver: JsValue) -> Result; + fn ordinary_set( + &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, + ) -> Result<(), String>; + + // Property definition + fn define_data_property( + &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, attributes: PropAttributes, + ) -> Result<(), String>; + fn define_accessor_property( + &mut self, obj: &mut JsObject, prop_name_si: u32, get: JsValue, set: JsValue, attributes: PropAttributes, + ) -> Result<(), String>; + fn set_or_create_prop_value(&mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue); + + // Lookup / coercion + fn lookup_str(&self, val: JsValue) -> Option; + fn coerce_primitive_bounded(&mut self, value: JsValue, prefer_string: bool) -> Result; + fn coerce_number_bounded(&mut self, value: JsValue) -> Result; + + // Call infrastructure + fn call_function_sync(&mut self, callee: JsValue, receiver: JsValue, args: &[JsValue]) -> Result; + + // Error handling + fn checked_object_ptr(&mut self, val: JsValue, error_msg: &str) -> Result, String>; + fn raise_type_error(&mut self, msg: &str) -> Result<(), String>; + fn error_message_text(&self, kind: &str, msg: &str) -> String; +} diff --git a/crates/oxide_vm/Cargo.toml b/crates/oxide_vm/Cargo.toml index 9807b34d..700b1e43 100644 --- a/crates/oxide_vm/Cargo.toml +++ b/crates/oxide_vm/Cargo.toml @@ -9,6 +9,7 @@ description = "OxideJS - Register-based VM with epoch arena memory" [dependencies] oxide_bytecode = { path = "../oxide_bytecode" } oxide_kernel = { path = "../oxide_kernel" } +oxide_runtime_api = { path = "../oxide_runtime_api" } oxide_types = { path = "../oxide_types" } bumpalo.workspace = true fast-float.workspace = true diff --git a/crates/oxide_vm/src/builtins/array.rs b/crates/oxide_vm/src/builtins/array.rs index b10051cb..44632516 100644 --- a/crates/oxide_vm/src/builtins/array.rs +++ b/crates/oxide_vm/src/builtins/array.rs @@ -3,8 +3,8 @@ use oxide_types::object::{JsObject, MAX_DENSE_PROPS}; use oxide_types::value::JsValue; use crate::coercion; -use crate::native::NativeResult; use crate::vm::Vm; +use oxide_runtime_api::NativeResult; macro_rules! array_ptr { ($vm:expr, $args:expr) => {{ diff --git a/crates/oxide_vm/src/builtins/array_buffer.rs b/crates/oxide_vm/src/builtins/array_buffer.rs index 001dde2f..13059c43 100644 --- a/crates/oxide_vm/src/builtins/array_buffer.rs +++ b/crates/oxide_vm/src/builtins/array_buffer.rs @@ -3,8 +3,8 @@ use oxide_types::object::{JsObject, NativeFnPtr, PropAttributes}; use oxide_types::value::JsValue; use crate::coercion; -use crate::native::NativeResult; use crate::vm::Vm; +use oxide_runtime_api::NativeResult; pub(crate) const MAX_ARRAY_BUFFER_LENGTH: usize = 1 << 30; diff --git a/crates/oxide_vm/src/builtins/boolean.rs b/crates/oxide_vm/src/builtins/boolean.rs index 07adbf3f..41db5b94 100644 --- a/crates/oxide_vm/src/builtins/boolean.rs +++ b/crates/oxide_vm/src/builtins/boolean.rs @@ -1,8 +1,8 @@ use oxide_types::object::JsObject; use oxide_types::value::JsValue; -use crate::native::NativeResult; use crate::vm::Vm; +use oxide_runtime_api::NativeResult; pub fn boolean_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); diff --git a/crates/oxide_vm/src/builtins/data_view.rs b/crates/oxide_vm/src/builtins/data_view.rs index b431c9f1..23481e63 100644 --- a/crates/oxide_vm/src/builtins/data_view.rs +++ b/crates/oxide_vm/src/builtins/data_view.rs @@ -4,8 +4,8 @@ use oxide_types::value::JsValue; use crate::builtins::array_buffer::array_buffer_data_ptr; use crate::coercion; -use crate::native::NativeResult; use crate::vm::Vm; +use oxide_runtime_api::NativeResult; #[derive(Clone, Copy)] pub(crate) struct DataViewData { diff --git a/crates/oxide_vm/src/builtins/date.rs b/crates/oxide_vm/src/builtins/date.rs index 1436ff2d..7be0eacc 100644 --- a/crates/oxide_vm/src/builtins/date.rs +++ b/crates/oxide_vm/src/builtins/date.rs @@ -5,8 +5,8 @@ use oxide_types::object::JsObject; use oxide_types::value::JsValue; use crate::coercion; -use crate::native::NativeResult; use crate::vm::Vm; +use oxide_runtime_api::NativeResult; fn get_timestamp(obj: &JsObject) -> f64 { let v = obj.get_prop_at(0); diff --git a/crates/oxide_vm/src/builtins/error.rs b/crates/oxide_vm/src/builtins/error.rs index 27024dd3..2cb12967 100644 --- a/crates/oxide_vm/src/builtins/error.rs +++ b/crates/oxide_vm/src/builtins/error.rs @@ -6,8 +6,8 @@ use oxide_types::object::JsObject; use oxide_types::value::JsValue; use crate::coercion; -use crate::native::NativeResult; use crate::vm::Vm; +use oxide_runtime_api::NativeResult; fn create_error_object(vm: &mut Vm, error_proto_ptr: *mut JsObject, name: &str, message: &str) -> JsValue { let obj = vm diff --git a/crates/oxide_vm/src/builtins/function.rs b/crates/oxide_vm/src/builtins/function.rs index a69579d5..aa8d6ae9 100644 --- a/crates/oxide_vm/src/builtins/function.rs +++ b/crates/oxide_vm/src/builtins/function.rs @@ -2,8 +2,8 @@ use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_types::object::{JsObject, NativeFnPtr}; use oxide_types::value::JsValue; -use crate::native::NativeResult; use crate::vm::Vm; +use oxide_runtime_api::NativeResult; fn invoke_target(vm: &mut Vm, target_val: JsValue, this_val: JsValue, arg_regs: &[u8]) -> NativeResult { let args: Vec = arg_regs.iter().map(|&r| vm.reg(r)).collect(); diff --git a/crates/oxide_vm/src/builtins/global/annex_b.rs b/crates/oxide_vm/src/builtins/global/annex_b.rs index 7e4950ab..dc7553c4 100644 --- a/crates/oxide_vm/src/builtins/global/annex_b.rs +++ b/crates/oxide_vm/src/builtins/global/annex_b.rs @@ -1,5 +1,5 @@ -use crate::native::NativeResult; use crate::vm::Vm; +use oxide_runtime_api::NativeResult; const ESCAPE_SAFE: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_+-./"; diff --git a/crates/oxide_vm/src/builtins/global/uri.rs b/crates/oxide_vm/src/builtins/global/uri.rs index a37b7838..1c5344f8 100644 --- a/crates/oxide_vm/src/builtins/global/uri.rs +++ b/crates/oxide_vm/src/builtins/global/uri.rs @@ -1,5 +1,5 @@ -use crate::native::NativeResult; use crate::vm::Vm; +use oxide_runtime_api::NativeResult; const URI_UNESCAPED: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.!~*'()"; const URI_RESERVED: &str = ";/?:@&=+$,#"; diff --git a/crates/oxide_vm/src/builtins/iterator.rs b/crates/oxide_vm/src/builtins/iterator.rs index 4e561ef7..3f71ddc9 100644 --- a/crates/oxide_vm/src/builtins/iterator.rs +++ b/crates/oxide_vm/src/builtins/iterator.rs @@ -3,8 +3,8 @@ use oxide_types::object::JsObject; use oxide_types::value::JsValue; use crate::coercion; -use crate::native::NativeResult; use crate::vm::Vm; +use oxide_runtime_api::NativeResult; const INNER_PROP: &str = "__inner__"; const INDEX_PROP: &str = "__index__"; diff --git a/crates/oxide_vm/src/builtins/json.rs b/crates/oxide_vm/src/builtins/json.rs index a0485c6e..3fda8733 100644 --- a/crates/oxide_vm/src/builtins/json.rs +++ b/crates/oxide_vm/src/builtins/json.rs @@ -6,8 +6,8 @@ use oxide_types::object::JsObject; use oxide_types::value::JsValue; use crate::builtins::object::walk_own_keys; -use crate::native::NativeResult; use crate::vm::Vm; +use oxide_runtime_api::NativeResult; pub fn json_parse(vm: &mut Vm, args: &[u8]) -> NativeResult { if args.len() < 2 { diff --git a/crates/oxide_vm/src/builtins/map.rs b/crates/oxide_vm/src/builtins/map.rs index b679350b..0f54d263 100644 --- a/crates/oxide_vm/src/builtins/map.rs +++ b/crates/oxide_vm/src/builtins/map.rs @@ -3,8 +3,8 @@ use oxide_types::object::JsObject; use oxide_types::value::JsValue; use crate::builtins::set::SetKey; -use crate::native::NativeResult; use crate::vm::Vm; +use oxide_runtime_api::NativeResult; macro_rules! native_try { ($expr:expr) => { diff --git a/crates/oxide_vm/src/builtins/math.rs b/crates/oxide_vm/src/builtins/math.rs index d1e3949b..c4b709f2 100644 --- a/crates/oxide_vm/src/builtins/math.rs +++ b/crates/oxide_vm/src/builtins/math.rs @@ -1,8 +1,8 @@ use oxide_types::value::JsValue; use crate::coercion; -use crate::native::NativeResult; use crate::vm::Vm; +use oxide_runtime_api::NativeResult; fn num(vm: &Vm, reg: u8) -> f64 { coercion::to_number(vm.reg(reg)) diff --git a/crates/oxide_vm/src/builtins/number.rs b/crates/oxide_vm/src/builtins/number.rs index c6aa6760..0912c074 100644 --- a/crates/oxide_vm/src/builtins/number.rs +++ b/crates/oxide_vm/src/builtins/number.rs @@ -1,8 +1,8 @@ use oxide_types::value::JsValue; use crate::coercion; -use crate::native::NativeResult; use crate::vm::Vm; +use oxide_runtime_api::NativeResult; pub fn number_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { let n = if args.len() > 1 { coercion::to_number(vm.reg(args[1])) } else { 0.0 }; diff --git a/crates/oxide_vm/src/builtins/object.rs b/crates/oxide_vm/src/builtins/object.rs index a2c5f2d0..aeaf671c 100644 --- a/crates/oxide_vm/src/builtins/object.rs +++ b/crates/oxide_vm/src/builtins/object.rs @@ -5,8 +5,8 @@ use oxide_types::private_key::is_private_name_key; use oxide_types::value::JsValue; use crate::coercion; -use crate::native::NativeResult; use crate::vm::Vm; +use oxide_runtime_api::NativeResult; pub(crate) fn walk_own_keys(vm: &Vm, obj: &JsObject) -> Vec<(u32, u32)> { let mut keys: Vec<(u32, u32)> = Vec::new(); diff --git a/crates/oxide_vm/src/builtins/reflect.rs b/crates/oxide_vm/src/builtins/reflect.rs index 10b123b7..c8fc1f9f 100644 --- a/crates/oxide_vm/src/builtins/reflect.rs +++ b/crates/oxide_vm/src/builtins/reflect.rs @@ -4,8 +4,8 @@ use oxide_types::value::JsValue; use crate::builtins::object::walk_own_keys; use crate::coercion; -use crate::native::NativeResult; use crate::vm::Vm; +use oxide_runtime_api::NativeResult; pub fn reflect_apply(vm: &mut Vm, args: &[u8]) -> NativeResult { let target = arg(vm, args, 1); diff --git a/crates/oxide_vm/src/builtins/regexp.rs b/crates/oxide_vm/src/builtins/regexp.rs index f5293441..453b6389 100644 --- a/crates/oxide_vm/src/builtins/regexp.rs +++ b/crates/oxide_vm/src/builtins/regexp.rs @@ -3,8 +3,8 @@ use oxide_types::object::{JsObject, NativeFnPtr}; use oxide_types::value::JsValue; use crate::coercion; -use crate::native::NativeResult; use crate::vm::Vm; +use oxide_runtime_api::NativeResult; fn get_regexp_ptr(vm: &mut Vm, args: &[u8]) -> Result<*mut JsObject, JsValue> { let this_val = vm.reg(args[0]); diff --git a/crates/oxide_vm/src/builtins/set.rs b/crates/oxide_vm/src/builtins/set.rs index 05f8e3bd..c1862a88 100644 --- a/crates/oxide_vm/src/builtins/set.rs +++ b/crates/oxide_vm/src/builtins/set.rs @@ -4,8 +4,8 @@ use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_types::object::JsObject; use oxide_types::value::JsValue; -use crate::native::NativeResult; use crate::vm::Vm; +use oxide_runtime_api::NativeResult; macro_rules! native_try { ($expr:expr) => { diff --git a/crates/oxide_vm/src/builtins/string.rs b/crates/oxide_vm/src/builtins/string.rs index dbc7fdfc..3b7782a9 100644 --- a/crates/oxide_vm/src/builtins/string.rs +++ b/crates/oxide_vm/src/builtins/string.rs @@ -3,9 +3,9 @@ use oxide_types::object::JsObject; use oxide_types::value::JsValue; use crate::coercion; -use crate::native::NativeResult; use crate::vm::Vm; use memchr::memchr; +use oxide_runtime_api::NativeResult; fn this_string(vm: &Vm, args: &[u8]) -> String { coercion::to_string(vm.reg(args[0])) diff --git a/crates/oxide_vm/src/builtins/stubs.rs b/crates/oxide_vm/src/builtins/stubs.rs index 3769ae84..02307a8b 100644 --- a/crates/oxide_vm/src/builtins/stubs.rs +++ b/crates/oxide_vm/src/builtins/stubs.rs @@ -1,5 +1,5 @@ -use crate::native::NativeResult; use crate::vm::Vm; +use oxide_runtime_api::NativeResult; fn stub_error(vm: &mut Vm, name: &str) -> NativeResult { NativeResult::Err(crate::builtins::error::create_type_error(vm, &format!("{name} is not implemented"))) diff --git a/crates/oxide_vm/src/builtins/symbol.rs b/crates/oxide_vm/src/builtins/symbol.rs index aedf0e67..266ba839 100644 --- a/crates/oxide_vm/src/builtins/symbol.rs +++ b/crates/oxide_vm/src/builtins/symbol.rs @@ -2,8 +2,8 @@ use oxide_types::object::JsObject; use oxide_types::value::JsValue; use crate::coercion; -use crate::native::NativeResult; use crate::vm::Vm; +use oxide_runtime_api::NativeResult; pub fn symbol_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); diff --git a/crates/oxide_vm/src/builtins/typed_array.rs b/crates/oxide_vm/src/builtins/typed_array.rs index 04b6f159..26d87792 100644 --- a/crates/oxide_vm/src/builtins/typed_array.rs +++ b/crates/oxide_vm/src/builtins/typed_array.rs @@ -4,8 +4,8 @@ use oxide_types::value::JsValue; use crate::builtins::array_buffer::{array_buffer_data_ptr, new_array_buffer, MAX_ARRAY_BUFFER_LENGTH}; use crate::coercion; -use crate::native::NativeResult; use crate::vm::Vm; +use oxide_runtime_api::NativeResult; #[derive(Clone, Copy)] pub(crate) struct TypedArrayData { diff --git a/crates/oxide_vm/src/dispatch/call.rs b/crates/oxide_vm/src/dispatch/call.rs index b2729c01..76c918ad 100644 --- a/crates/oxide_vm/src/dispatch/call.rs +++ b/crates/oxide_vm/src/dispatch/call.rs @@ -1,6 +1,7 @@ -use crate::native::{NativeFn, NativeResult}; +use crate::native::NativeFn; use crate::vm::{native_fn_ptr_to_fn, FrameContinuation, Vm}; use oxide_kernel::{builtins_debug, builtins_trace}; +use oxide_runtime_api::NativeResult; use oxide_types::object::JsObject; use oxide_types::value::JsValue; diff --git a/crates/oxide_vm/src/native.rs b/crates/oxide_vm/src/native.rs index c693b389..0a8ad198 100644 --- a/crates/oxide_vm/src/native.rs +++ b/crates/oxide_vm/src/native.rs @@ -1,38 +1,3 @@ -use oxide_types::value::JsValue; - -pub enum NativeResult { - Ok(JsValue), - Err(JsValue), - TailCall { callee: JsValue, this: JsValue, args: Vec }, -} - -impl NativeResult { - pub fn ok(val: JsValue) -> Self { - Self::Ok(val) - } - - pub fn err(val: JsValue) -> Self { - Self::Err(val) - } - - pub fn unwrap(self) -> JsValue { - match self { - Self::Ok(val) => val, - Self::Err(_) => panic!("called `NativeResult::unwrap()` on an `Err` value"), - Self::TailCall { .. } => panic!("called `NativeResult::unwrap()` on a `TailCall` value"), - } - } - - pub fn map_err(self, op: F) -> Result - where - F: FnOnce(JsValue) -> E, - { - match self { - Self::Ok(val) => Ok(val), - Self::Err(err) => Err(op(err)), - Self::TailCall { .. } => panic!("TailCall cannot be converted to Result"), - } - } -} +use oxide_runtime_api::NativeResult; pub type NativeFn = fn(&mut crate::vm::Vm, args: &[u8]) -> NativeResult; diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index a831b17c..e1169c9b 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -503,8 +503,8 @@ mod tests { use super::*; use crate::builtins::{array_buffer, data_view, map, set, typed_array}; - use crate::native::NativeResult; use crate::vm::{CallFrame, FrameContinuation}; + use oxide_runtime_api::NativeResult; fn plain_object(vm: &mut Vm) -> *mut JsObject { let proto_ptr = vm.session.builtin_world().object_proto.as_ptr() as *mut JsObject; diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index fb77a0cd..0b5f1039 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -9,10 +9,11 @@ use smallvec::SmallVec; pub use crate::bindings::init_kernel_builtins; use crate::coercion; -use crate::native::{NativeFn, NativeResult}; +use crate::native::NativeFn; use crate::session_gc::SessionGc; use oxide_kernel::kernel::{KernelCore, KernelSession}; use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; +use oxide_runtime_api::NativeResult; use oxide_types::error::{JsError, JsErrorKind}; use oxide_types::mem::{Epoch, P}; use oxide_types::object::{JsObject, JsString, NativeFnPtr}; @@ -785,11 +786,11 @@ impl Vm { self.regs[2] = flags_val; let func = unsafe { crate::vm::native_fn_ptr_to_fn(native_fn) }; let result = match func(self, &[0, 1, 2]) { - crate::native::NativeResult::Ok(v) => v, - crate::native::NativeResult::Err(e) => { + oxide_runtime_api::NativeResult::Ok(v) => v, + oxide_runtime_api::NativeResult::Err(e) => { return Err(self.error_message_text("TypeError", &self.error_text(e))); } - crate::native::NativeResult::TailCall { .. } => { + oxide_runtime_api::NativeResult::TailCall { .. } => { return Err(self.error_message_text("TypeError", "unexpected tail call")); } }; @@ -1412,8 +1413,8 @@ impl Default for Vm { #[cfg(test)] mod tests { use super::{opcode, JsValue, TryHandler, Vm}; - use crate::native::NativeResult; use oxide_bytecode::module::CompiledModule; + use oxide_runtime_api::NativeResult; use oxide_types::object::NativeFnPtr; use oxide_types::object::{JsObject, PropAttributes}; diff --git a/crates/oxide_vm/src/vm_dispatch_misc.rs b/crates/oxide_vm/src/vm_dispatch_misc.rs index 45fdfa08..ceb1749d 100644 --- a/crates/oxide_vm/src/vm_dispatch_misc.rs +++ b/crates/oxide_vm/src/vm_dispatch_misc.rs @@ -1,7 +1,8 @@ use std::sync::Arc; -use crate::native::{NativeFn, NativeResult}; +use crate::native::NativeFn; use crate::vm::{native_fn_ptr_to_fn, CallFrame, ForInIter, FrameContinuation, Vm, MAX_PROTO_CHAIN_DEPTH}; +use oxide_runtime_api::NativeResult; use oxide_types::object::{JsObject, PropAttributes}; use oxide_types::private_key::is_private_name_key; use oxide_types::value::JsValue; From d195d7615c6aebf8c1468a1af2e73bae5e63d682 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Thu, 25 Jun 2026 19:22:16 +0800 Subject: [PATCH 18/32] =?UTF-8?q?refactor:=20Vm=20=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E5=88=86=E7=BB=84=E3=80=81dispatch=20=E6=8B=86=E5=88=86?= =?UTF-8?q?=E4=B8=BA=E5=88=86=E5=8F=91=E8=A1=A8=E3=80=81IC=20=E8=BE=85?= =?UTF-8?q?=E5=8A=A9=E5=87=BD=E6=95=B0=E7=8B=AC=E7=AB=8B=E6=88=90=E6=A8=A1?= =?UTF-8?q?=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_vm/src/builtins/symbol.rs | 17 +- crates/oxide_vm/src/dispatch/arithmetic.rs | 104 ++++ crates/oxide_vm/src/dispatch/call.rs | 242 ++++++++- crates/oxide_vm/src/dispatch/control_flow.rs | 33 ++ crates/oxide_vm/src/dispatch/misc.rs | 51 ++ crates/oxide_vm/src/dispatch/property.rs | 49 +- crates/oxide_vm/src/ic_helper.rs | 37 ++ crates/oxide_vm/src/lib.rs | 2 + crates/oxide_vm/src/session_arena.rs | 14 +- crates/oxide_vm/src/session_gc.rs | 114 ++--- crates/oxide_vm/src/vm.rs | 487 ++++--------------- crates/oxide_vm/src/vm_dispatch_misc.rs | 20 +- crates/oxide_vm/src/vm_props.rs | 13 +- crates/oxide_vm/src/vm_runtime.rs | 32 +- crates/oxide_vm/src/vm_state.rs | 57 +++ crates/oxide_vm/src/vm_support.rs | 126 +++-- 16 files changed, 835 insertions(+), 563 deletions(-) create mode 100644 crates/oxide_vm/src/ic_helper.rs create mode 100644 crates/oxide_vm/src/vm_state.rs diff --git a/crates/oxide_vm/src/builtins/symbol.rs b/crates/oxide_vm/src/builtins/symbol.rs index 266ba839..08e8cd97 100644 --- a/crates/oxide_vm/src/builtins/symbol.rs +++ b/crates/oxide_vm/src/builtins/symbol.rs @@ -33,9 +33,9 @@ pub fn symbol_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { String::new() }; - vm.symbol_counter = vm.symbol_counter.wrapping_add(1); - let idx = vm.symbol_descriptions.len() as u32; - vm.symbol_descriptions.push(description); + vm.symbols.symbol_counter = vm.symbols.symbol_counter.wrapping_add(1); + let idx = vm.symbols.symbol_descriptions.len() as u32; + vm.symbols.symbol_descriptions.push(description); NativeResult::Ok(JsValue::symbol(idx)) } @@ -49,7 +49,7 @@ pub fn symbol_to_string(vm: &mut Vm, args: &[u8]) -> NativeResult { } let idx = this_val.as_symbol_index(); - let desc = vm.symbol_descriptions.get(idx as usize).cloned().unwrap_or_default(); + let desc = vm.symbols.symbol_descriptions.get(idx as usize).cloned().unwrap_or_default(); let result = format!("Symbol({})", desc); NativeResult::Ok(vm.new_string(&result)) } @@ -61,13 +61,13 @@ pub fn symbol_for(vm: &mut Vm, args: &[u8]) -> NativeResult { "undefined".to_string() }; - if let Some(&idx) = vm.symbol_registry.get(&key) { + if let Some(&idx) = vm.symbols.symbol_registry.get(&key) { return NativeResult::Ok(JsValue::symbol(idx)); } - let idx = vm.symbol_descriptions.len() as u32; - vm.symbol_descriptions.push(key.clone()); - vm.symbol_registry.insert(key, idx); + let idx = vm.symbols.symbol_descriptions.len() as u32; + vm.symbols.symbol_descriptions.push(key.clone()); + vm.symbols.symbol_registry.insert(key, idx); NativeResult::Ok(JsValue::symbol(idx)) } @@ -79,6 +79,7 @@ pub fn symbol_key_for(vm: &mut Vm, args: &[u8]) -> NativeResult { let idx = sym.as_symbol_index(); let found = vm + .symbols .symbol_registry .iter() .find(|(_, ®istered_idx)| registered_idx == idx) diff --git a/crates/oxide_vm/src/dispatch/arithmetic.rs b/crates/oxide_vm/src/dispatch/arithmetic.rs index 9ae4ed09..2c5c0e5b 100644 --- a/crates/oxide_vm/src/dispatch/arithmetic.rs +++ b/crates/oxide_vm/src/dispatch/arithmetic.rs @@ -19,4 +19,108 @@ impl Vm { } Ok(()) } + + #[inline(always)] + pub(crate) fn dispatch_neg(&mut self, rd: usize, a: usize) -> Result<(), String> { + let v = self.coerce_number_bounded(self.regs[a])?; + self.regs[rd] = JsValue::float(-v); + Ok(()) + } + + #[inline(always)] + pub(crate) fn dispatch_unary_plus(&mut self, rd: usize, a: usize) -> Result<(), String> { + let v = self.coerce_number_bounded(self.regs[a])?; + self.regs[rd] = JsValue::float(v); + Ok(()) + } + + pub(crate) fn dispatch_compound_add(&mut self, rd: usize, a: usize) -> Result<(), String> { + let lhs = self.coerce_primitive_bounded(self.regs[rd], false)?; + let rhs = self.coerce_primitive_bounded(self.regs[a], false)?; + if lhs.is_string() || rhs.is_string() { + let ls = coercion::to_string(lhs); + let rs = coercion::to_string(rhs); + let concat = format!("{ls}{rs}"); + self.regs[rd] = self.new_string(&concat); + } else { + let ln = coercion::to_number(lhs); + let rn = coercion::to_number(rhs); + self.regs[rd] = JsValue::float(ln + rn); + } + Ok(()) + } + + #[inline(always)] + pub(crate) fn dispatch_compound_sub(&mut self, rd: usize, a: usize) -> Result<(), String> { + let l = self.coerce_number_bounded(self.regs[rd])?; + let r = self.coerce_number_bounded(self.regs[a])?; + self.regs[rd] = JsValue::float(l - r); + Ok(()) + } + + #[inline(always)] + pub(crate) fn dispatch_compound_mul(&mut self, rd: usize, a: usize) -> Result<(), String> { + let l = self.coerce_number_bounded(self.regs[rd])?; + let r = self.coerce_number_bounded(self.regs[a])?; + self.regs[rd] = JsValue::float(l * r); + Ok(()) + } + + #[inline(always)] + pub(crate) fn dispatch_compound_div(&mut self, rd: usize, a: usize) -> Result<(), String> { + let l = self.coerce_number_bounded(self.regs[rd])?; + let r = self.coerce_number_bounded(self.regs[a])?; + self.regs[rd] = JsValue::float(l / r); + Ok(()) + } + + #[inline(always)] + pub(crate) fn dispatch_compound_mod(&mut self, rd: usize, a: usize) -> Result<(), String> { + let l = self.coerce_number_bounded(self.regs[rd])?; + let r = self.coerce_number_bounded(self.regs[a])?; + self.regs[rd] = JsValue::float(l % r); + Ok(()) + } + + #[inline(always)] + pub(crate) fn dispatch_compound_exp(&mut self, rd: usize, a: usize) -> Result<(), String> { + let l = self.coerce_number_bounded(self.regs[rd])?; + let r = self.coerce_number_bounded(self.regs[a])?; + self.regs[rd] = JsValue::float(l.powf(r)); + Ok(()) + } + + #[inline(always)] + pub(crate) fn dispatch_inc_pre(&mut self, rd: usize, a: usize) -> Result<(), String> { + let n = self.coerce_number_bounded(self.regs[rd])?; + let result = JsValue::float(n + 1.0); + self.regs[rd] = result; + self.regs[a] = result; + Ok(()) + } + + #[inline(always)] + pub(crate) fn dispatch_inc_post(&mut self, rd: usize, a: usize) -> Result<(), String> { + let n = self.coerce_number_bounded(self.regs[rd])?; + self.regs[a] = JsValue::float(n); + self.regs[rd] = JsValue::float(n + 1.0); + Ok(()) + } + + #[inline(always)] + pub(crate) fn dispatch_dec_pre(&mut self, rd: usize, a: usize) -> Result<(), String> { + let n = self.coerce_number_bounded(self.regs[rd])?; + let result = JsValue::float(n - 1.0); + self.regs[rd] = result; + self.regs[a] = result; + Ok(()) + } + + #[inline(always)] + pub(crate) fn dispatch_dec_post(&mut self, rd: usize, a: usize) -> Result<(), String> { + let n = self.coerce_number_bounded(self.regs[rd])?; + self.regs[a] = JsValue::float(n); + self.regs[rd] = JsValue::float(n - 1.0); + Ok(()) + } } diff --git a/crates/oxide_vm/src/dispatch/call.rs b/crates/oxide_vm/src/dispatch/call.rs index 76c918ad..3d974693 100644 --- a/crates/oxide_vm/src/dispatch/call.rs +++ b/crates/oxide_vm/src/dispatch/call.rs @@ -1,9 +1,11 @@ use crate::native::NativeFn; -use crate::vm::{native_fn_ptr_to_fn, FrameContinuation, Vm}; +use crate::vm::{native_fn_ptr_to_fn, CallFrame, FrameContinuation, Vm}; +use oxide_bytecode::opcode; use oxide_kernel::{builtins_debug, builtins_trace}; use oxide_runtime_api::NativeResult; use oxide_types::object::JsObject; use oxide_types::value::JsValue; +use std::sync::Arc; impl Vm { #[inline(always)] @@ -73,3 +75,241 @@ impl Vm { } } } + +impl Vm { + pub(crate) fn dispatch_create_closure(&mut self, rd: usize, instr: u32) { + let sub_idx = opcode::imm16(instr) as u32; + debug_assert!(sub_idx > 0 && (sub_idx as usize) <= self.sub_modules.len()); + let sub = &self.sub_modules[sub_idx as usize - 1]; + let result = self.create_function_object( + sub_idx, + sub.is_arrow, + sub.is_class_constructor, + sub.is_derived_constructor, + sub.needs_home_object, + ); + self.regs[rd] = result; + } + + pub(crate) fn dispatch_create_regexp(&mut self, rd: usize, a: usize, b: usize) -> Result, String> { + let pat_val = self.regs[a]; + let flags_val = self.regs[b]; + let ctor_ptr = self.session.builtin_world().regexp_constructor.as_ptr() as *mut JsObject; + let ctor = unsafe { &*ctor_ptr }; + let Some(native_fn) = ctor.native_fn() else { + self.raise_error_kind("TypeError", "RegExp constructor unavailable")?; + return Ok(Some(JsValue::undefined())); + }; + let saved_0 = self.regs[0]; + let saved_1 = self.regs[1]; + let saved_2 = self.regs[2]; + self.regs[0] = JsValue::undefined(); + self.regs[1] = pat_val; + self.regs[2] = flags_val; + let func = unsafe { native_fn_ptr_to_fn(native_fn) }; + let result = match func(self, &[0, 1, 2]) { + NativeResult::Ok(v) => v, + NativeResult::Err(e) => { + return Err(self.error_message_text("TypeError", &self.error_text(e))); + } + NativeResult::TailCall { .. } => { + return Err(self.error_message_text("TypeError", "unexpected tail call")); + } + }; + self.regs[0] = saved_0; + self.regs[1] = saved_1; + self.regs[2] = saved_2; + self.regs[rd] = result; + Ok(None) + } + + pub(crate) fn dispatch_super_call(&mut self, rd: usize, a: usize) -> Result { + let first_arg_reg = a as u8; + let ext = self.bytecode[self.pc]; + self.pc += 1; + let arg_count = (ext & 0xFF) as usize; + + let Some(frame) = self.frames.last() else { + self.raise_error_kind("ReferenceError", "super() used outside class constructor")?; + return Ok(true); + }; + if !frame.is_derived_constructor { + self.raise_error_kind("ReferenceError", "super() used outside derived constructor")?; + return Ok(true); + } + if !self.regs[254].is_undefined() { + self.raise_error_kind("ReferenceError", "super() called more than once")?; + return Ok(true); + } + let Some(derived_this) = frame.constructed_this else { + self.raise_error_kind("ReferenceError", "super() without derived this")?; + return Ok(true); + }; + + let new_target = self.regs[255]; + if !new_target.is_object() { + self.raise_error_kind("TypeError", "super() new.target is not an object")?; + return Ok(true); + } + let new_target_obj = unsafe { &*new_target.as_js_object_ptr() }; + let super_ctor = new_target_obj.proto(); + if !super_ctor.is_object() { + self.raise_error_kind("TypeError", "super constructor is not an object")?; + return Ok(true); + } + let super_obj = unsafe { &*super_ctor.as_js_object_ptr() }; + if !super_obj.is_function() { + self.raise_error_kind("TypeError", "super constructor is not a function")?; + return Ok(true); + } + + if super_obj.native_fn().is_some() { + self.regs[253] = derived_this; + self.regs[254] = super_ctor; // bind_dispatcher reads regs[254] as the wrapper callee + let (args_buf, len) = Self::build_native_args(first_arg_reg, arg_count, 253); + // SAFETY: native_fn was set via set_native_fn with a valid NativeFn pointer; + // native_fn_ptr_to_fn is the single coercion point for NativeFnPtr → NativeFn. + let func: NativeFn = unsafe { native_fn_ptr_to_fn(super_obj.native_fn().unwrap()) }; + match func(self, &args_buf[..len]) { + NativeResult::Ok(val) => { + self.regs[254] = if val.is_object() { val } else { derived_this }; + self.regs[rd] = self.regs[254]; + } + NativeResult::Err(err_val) => { + self.exception_value = Some(err_val); + self.pending_error_kind = Some(self.thrown_error_kind(err_val)); + match self.unwind() { + Ok(()) => return Ok(true), + Err(e) => return Err(e), + } + } + NativeResult::TailCall { callee, this, args } => { + // e.g. bound function: resolve the tail call, use its return + // value as the constructed instance (or fall back to derived_this). + match self.call_function_sync(callee, this, &args) { + Ok(val) => { + self.regs[254] = if val.is_object() { val } else { derived_this }; + self.regs[rd] = self.regs[254]; + } + Err(e) => return Err(e), + } + } + } + } else if super_obj.sub_module_index() > 0 { + let sub_idx = super_obj.sub_module_index() as usize - 1; + if sub_idx >= self.sub_modules.len() { + return Err(format!( + "SUPER_CALL: sub_module_index {} out of bounds (max {})", + sub_idx, + self.sub_modules.len() + )); + } + if self.frames.len() >= self.kernel_core.config.max_call_depth { + return Err(self.error_message_text("RangeError", "Maximum call stack size exceeded")); + } + + let sub_bytecode = self.sub_modules[sub_idx].bytecode.clone(); + let sub_n_args = self.sub_modules[sub_idx].n_args as usize; + let sub_n_registers = self.sub_modules[sub_idx].n_registers; + let sub_param_base = self.sub_modules[sub_idx].param_base as usize; + let caller_reg_limit = self.active_reg_limit.max(1); + let saved_reg_offset = self.save_stack.len() as u32; + self.save_stack.extend_from_slice(&self.regs[..caller_reg_limit as usize]); + let saved_this = self.regs[254]; + let saved_new_target = self.regs[255]; + + for i in 0..sub_n_args { + let src_reg = first_arg_reg.wrapping_add(i as u8) as usize; + self.regs[sub_param_base + i] = self.regs[src_reg]; + } + self.regs[254] = derived_this; + self.regs[255] = new_target; + + self.saved_bytecode_stack.push(std::mem::take(&mut self.bytecode)); + self.saved_immutables_stack.push(self.active_immutables); + self.frames.push(CallFrame { + return_addr: self.pc, + function_name: self.sub_modules[sub_idx] + .function_name + .as_deref() + .map(|name| self.kernel_core.perm_interner().intern(name).0) + .unwrap_or(0), + caller_reg_limit, + saved_reg_offset, + saved_this, + saved_new_target, + callee: super_ctor, + construct_result_reg: Some(254), + constructed_this: Some(derived_this), + is_derived_constructor: super_obj.is_derived_constructor(), + continuation: FrameContinuation::None, + }); + + self.bytecode = sub_bytecode; + let subs = Arc::clone(&self.sub_modules); + self.activate_immutables(sub_idx + 1, &subs[sub_idx].constants); + for (name, reg) in &self.sub_modules[sub_idx].builtin_reg_map { + let si = self.kernel_core.perm_interner().intern(name.as_str()).0; + let global = self.session.global_object(); + if let Some(pos) = self.kernel_core.shape_forge().lookup_position(global.shape_id(), si) { + self.regs[*reg as usize] = global.get_prop_at(pos); + } + } + self.active_reg_limit = sub_n_registers.max(1); + self.pc = 0; + return Ok(true); + } else { + self.raise_error_kind("TypeError", "super constructor is not callable")?; + return Ok(true); + } + Ok(false) + } + + pub(crate) fn dispatch_super_get_prop(&mut self, rd: usize, a: usize, b: usize) -> Result { + let key_val = self.regs[b]; + let prop_name_si = self.property_key_si(key_val); + let Some(frame) = self.frames.last() else { + self.raise_error_kind("ReferenceError", "super property used outside function")?; + return Ok(true); + }; + if !frame.callee.is_object() { + self.raise_error_kind("ReferenceError", "super property has no home object")?; + return Ok(true); + } + let callee_obj = unsafe { &*frame.callee.as_js_object_ptr() }; + let home_object = callee_obj.home_object(); + if !home_object.is_object() { + self.raise_error_kind("ReferenceError", "super property has no home object")?; + return Ok(true); + } + let home_obj = unsafe { &*home_object.as_js_object_ptr() }; + let super_base = home_obj.proto(); + if !super_base.is_object() { + self.regs[rd] = JsValue::undefined(); + } else { + let super_obj = unsafe { &*super_base.as_js_object_ptr() }; + let val = self.ordinary_get_with_target(super_obj, prop_name_si, self.regs[a], rd as u8)?; + if self.accessor_frame_target_reg.take().is_none() { + self.regs[rd] = val; + } + } + Ok(false) + } + + pub(crate) fn dispatch_set_home_object(&mut self, rd: usize, a: usize) -> Result { + let func_val = self.regs[rd]; + let home_val = self.regs[a]; + if !func_val.is_object() || !home_val.is_object() { + self.raise_error_kind("TypeError", "SET_HOME_OBJECT expects function and object")?; + return Ok(true); + } + let home_val = self.promote_if_needed_for_write_ptr(func_val.as_js_object_ptr(), home_val); + let func_obj = unsafe { &mut *func_val.as_js_object_ptr() }; + if !func_obj.is_function() { + self.raise_error_kind("TypeError", "SET_HOME_OBJECT target is not a function")?; + return Ok(true); + } + func_obj.set_home_object(home_val); + Ok(false) + } +} diff --git a/crates/oxide_vm/src/dispatch/control_flow.rs b/crates/oxide_vm/src/dispatch/control_flow.rs index e42df1c6..00030466 100644 --- a/crates/oxide_vm/src/dispatch/control_flow.rs +++ b/crates/oxide_vm/src/dispatch/control_flow.rs @@ -1,8 +1,41 @@ use crate::coercion; use crate::vm::Vm; +use oxide_bytecode::opcode::{self, Instr}; use oxide_types::value::JsValue; impl Vm { + #[inline(always)] + pub(crate) fn dispatch_jmp(&mut self, instr: Instr) { + let offset = opcode::offset16(instr) as isize; + self.pc = ((self.pc as isize) + offset - 1) as usize; + } + + #[inline(always)] + pub(crate) fn dispatch_jmp_if_false(&mut self, rd: usize, instr: Instr) { + let cond = coercion::to_boolean(self.regs[rd]); + if !cond { + let offset = opcode::offset16(instr) as isize; + self.pc = ((self.pc as isize) + offset - 1) as usize; + } + } + + #[inline(always)] + pub(crate) fn dispatch_jmp_if_true(&mut self, rd: usize, instr: Instr) { + let cond = coercion::to_boolean(self.regs[rd]); + if cond { + let offset = opcode::offset16(instr) as isize; + self.pc = ((self.pc as isize) + offset - 1) as usize; + } + } + + #[inline(always)] + pub(crate) fn dispatch_jmp_if_nullish(&mut self, rd: usize, instr: Instr) { + if self.regs[rd].is_nullish() { + let offset = opcode::offset16(instr) as isize; + self.pc = ((self.pc as isize) + offset - 1) as usize; + } + } + #[inline(always)] pub(crate) fn dispatch_and(&mut self, rd: usize, a: usize, b: usize) { let val = self.regs[a]; diff --git a/crates/oxide_vm/src/dispatch/misc.rs b/crates/oxide_vm/src/dispatch/misc.rs index 397996b8..65f39b97 100644 --- a/crates/oxide_vm/src/dispatch/misc.rs +++ b/crates/oxide_vm/src/dispatch/misc.rs @@ -1,4 +1,8 @@ use crate::vm::Vm; +use oxide_bytecode::opcode; +use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; +use oxide_types::object::JsObject; +use oxide_types::value::JsValue; impl Vm { #[inline(always)] @@ -40,4 +44,51 @@ impl Vm { }; self.regs[rd] = self.new_string(result); } + + pub(crate) fn dispatch_load_var(&mut self, rd: usize, a: usize) -> Result { + if a == 254 + && self.frames.last().map(|frame| frame.is_derived_constructor).unwrap_or(false) + && self.regs[a].is_undefined() + { + self.raise_error_kind("ReferenceError", "must call super constructor before using 'this'")?; + return Ok(true); + } + self.regs[rd] = self.regs[a]; + Ok(false) + } + + pub(crate) fn dispatch_store_var(&mut self, rd: usize, a: usize, b: usize) -> Result { + if b != 0 { + // const guard: check if already initialized + if !self.regs[rd].is_undefined() { + self.raise_error_kind("TypeError", "Assignment to constant variable")?; + return Ok(true); + } + } + self.regs[rd] = self.regs[a]; + Ok(false) + } + + #[inline(always)] + pub(crate) fn dispatch_new_object(&mut self, rd: usize) { + let proto_ptr = &*self.object_prototype as *const JsObject as *mut JsObject; + let obj = self + .epoch + .alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(proto_ptr))); + self.regs[rd] = JsValue::object(obj as *mut u8); + } + + #[inline(always)] + pub(crate) fn dispatch_new_array(&mut self, rd: usize, instr: u32) { + let proto_ptr = self.session.builtin_world().array_proto.as_ptr() as *mut JsObject; + let n = opcode::imm16(instr) as usize; + let bump = self.epoch.bump(); + let obj = self.alloc_object(JsObject::new_array(EMPTY_SHAPE_ID, JsValue::from_js_object(proto_ptr), n, bump)); + self.regs[rd] = JsValue::object(obj as *mut u8); + } + + #[inline(always)] + pub(crate) fn dispatch_void(&mut self, rd: usize) { + self.regs[rd] = JsValue::undefined(); + } } diff --git a/crates/oxide_vm/src/dispatch/property.rs b/crates/oxide_vm/src/dispatch/property.rs index 348ba942..def10c9b 100644 --- a/crates/oxide_vm/src/dispatch/property.rs +++ b/crates/oxide_vm/src/dispatch/property.rs @@ -198,14 +198,14 @@ impl Vm { if let Some(value) = ic_get_hit(obj, cached_shape_id, cached_slot) { self.regs[a] = value; - self.ic_hits.set(self.ic_hits.get() + 1); + self.profiling.ic_hits.set(self.profiling.ic_hits.get() + 1); ic_trace!("IC_GET hit shape={} slot={}", cached_shape_id, cached_slot); } else if let Some(template) = self.kernel_core.prop_forge().get_template(obj.shape_id()) { - self.ic_misses.set(self.ic_misses.get() + 1); + self.profiling.ic_misses.set(self.profiling.ic_misses.get() + 1); prop_cache_miss(); if template.prop_name == prop_name_si { if template.position < obj.prop_vec_len() as u32 { - self.write_ic_back(obj.shape_id(), template.position); + crate::ic_helper::write_ic_back(&mut self.bytecode, self.pc, obj.shape_id(), template.position); ic_debug!( "IC_GET propforge hit shape={} prop={} slot={}", obj.shape_id(), @@ -226,7 +226,7 @@ impl Vm { ic_debug!("IC_GET miss shape={} prop={}", obj.shape_id(), prop_name_si); let resolved = self.ordinary_get(obj, prop_name_si, val)?; if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { - self.write_ic_back(obj.shape_id(), pos); + crate::ic_helper::write_ic_back(&mut self.bytecode, self.pc, obj.shape_id(), pos); } self.regs[a] = resolved; } @@ -268,22 +268,22 @@ impl Vm { let cached_slot = ext1; if ic_set_hit(obj, cached_shape_id, cached_slot, value) { - self.ic_hits.set(self.ic_hits.get() + 1); + self.profiling.ic_hits.set(self.profiling.ic_hits.get() + 1); ic_trace!("IC_SET hit shape={} slot={}", cached_shape_id, cached_slot); } else if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { - self.ic_misses.set(self.ic_misses.get() + 1); + self.profiling.ic_misses.set(self.profiling.ic_misses.get() + 1); prop_cache_miss(); obj.set_prop_at(pos, value); - self.write_ic_back(obj.shape_id(), pos); + crate::ic_helper::write_ic_back(&mut self.bytecode, self.pc, obj.shape_id(), pos); ic_debug!("IC_SET write-back shape={} slot={}", obj.shape_id(), pos); } else { - self.ic_misses.set(self.ic_misses.get() + 1); + self.profiling.ic_misses.set(self.profiling.ic_misses.get() + 1); prop_cache_miss(); let old_shape = obj.shape_id(); self.ordinary_set_dispatch(obj, prop_name_si, value, receiver)?; if old_shape != obj.shape_id() { if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { - self.write_ic_back(obj.shape_id(), pos); + crate::ic_helper::write_ic_back(&mut self.bytecode, self.pc, obj.shape_id(), pos); ic_debug!("IC_SET write-back shape={} slot={}", obj.shape_id(), pos); self.kernel_core.prop_forge().upsert( obj.shape_id(), @@ -394,3 +394,34 @@ impl Vm { Ok(()) } } + +impl Vm { + pub(crate) fn dispatch_delete_prop_static(&mut self, rd: usize) -> Result { + let prop_idx = self.bytecode[self.pc] as usize; + self.pc += 1; + let key_val = self.immutables().get(prop_idx).copied().unwrap_or_else(JsValue::undefined); + let prop_name_si = self.property_key_si(key_val); + let Some(obj_ptr) = self.checked_object_ptr(self.regs[rd], "delete on non-object")? else { + return Ok(true); + }; + let obj = unsafe { &mut *obj_ptr }; + if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { + obj.set_prop_at(pos, JsValue::undefined()); + } + self.regs[rd] = JsValue::bool(true); + Ok(false) + } + + pub(crate) fn dispatch_delete_prop_dynamic(&mut self, rd: usize, b: usize) -> Result { + let prop_name_si = self.property_key_si(self.regs[b]); + let Some(obj_ptr) = self.checked_object_ptr(self.regs[rd], "delete on non-object")? else { + return Ok(true); + }; + let obj = unsafe { &mut *obj_ptr }; + if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { + obj.set_prop_at(pos, JsValue::undefined()); + } + self.regs[rd] = JsValue::bool(true); + Ok(false) + } +} diff --git a/crates/oxide_vm/src/ic_helper.rs b/crates/oxide_vm/src/ic_helper.rs new file mode 100644 index 00000000..7f3b3cef --- /dev/null +++ b/crates/oxide_vm/src/ic_helper.rs @@ -0,0 +1,37 @@ +//! Inline-cache (IC) bytecode-stream helpers. +//! +//! These operate purely on the bytecode `Instr` slice — they read/clear/write +//! the IC extension words embedded after IC-bearing opcodes. They take no `Vm` +//! state, so they live outside `Vm` for clearer ownership and to ease the future +//! IC side-table migration. + +use oxide_bytecode::opcode::{self, Instr}; + +/// Write a resolved `(shape_id, slot)` back into the three IC extension words +/// that precede the current `pc`. Called on an IC miss after resolving a +/// property, so the next execution of this site hits the cache. +pub(crate) fn write_ic_back(bytecode: &mut [Instr], pc: usize, shape_id: u32, slot_index: u32) { + debug_assert!(pc >= 3, "IC write-back requires 3 extension words before pc"); + bytecode[pc - 3] = shape_id & 0x00FF_FFFF; + bytecode[pc - 2] = slot_index; + bytecode[pc - 1] = 0; +} + +/// Zero every IC extension word in the stream, invalidating all cached shapes. +/// Used on `rerun()` so re-execution never derefs a stale cached pointer. +pub(crate) fn clear_ic_caches(bytecode: &mut [Instr]) { + let mut i = 0; + while i < bytecode.len() { + let op = opcode::opcode(bytecode[i]); + if op.has_ic_ext_words() { + if i + 3 < bytecode.len() { + bytecode[i + 1] = 0; + bytecode[i + 2] = 0; + bytecode[i + 3] = 0; + } + i += 4; + } else { + i += 1; + } + } +} diff --git a/crates/oxide_vm/src/lib.rs b/crates/oxide_vm/src/lib.rs index aaa593df..9ba93811 100644 --- a/crates/oxide_vm/src/lib.rs +++ b/crates/oxide_vm/src/lib.rs @@ -4,6 +4,7 @@ pub mod bindings; pub mod builtins; pub mod coercion; mod dispatch; +mod ic_helper; pub mod native; mod session_arena; pub mod session_gc; @@ -13,5 +14,6 @@ mod vm_dispatch_misc; pub mod vm_pool; mod vm_props; mod vm_runtime; +mod vm_state; mod vm_support; pub use oxide_types::value::JsValue; diff --git a/crates/oxide_vm/src/session_arena.rs b/crates/oxide_vm/src/session_arena.rs index dbe6bf44..15aaa604 100644 --- a/crates/oxide_vm/src/session_arena.rs +++ b/crates/oxide_vm/src/session_arena.rs @@ -17,10 +17,10 @@ impl Vm { } pub(crate) fn promote_object(&mut self, src: *mut JsObject) -> *mut JsObject { - let mut forwarding = std::mem::take(&mut self.forwarding); + let mut forwarding = std::mem::take(&mut self.gc_state.forwarding); let result = self.promote_object_inner(src, &mut forwarding); forwarding.clear(); - self.forwarding = forwarding; + self.gc_state.forwarding = forwarding; result } @@ -39,10 +39,10 @@ impl Vm { } let clone = src_ref.clone_for_session_epoch(); - let dst = self.session_epoch.alloc(clone) as *mut JsObject; + let dst = self.gc_state.session_epoch.alloc(clone) as *mut JsObject; forwarding.insert(src, dst); - self.session_object_ptrs.push(dst); - self.session_bytes_allocated += std::mem::size_of::(); + self.gc_state.session_object_ptrs.push(dst); + self.gc_state.session_bytes_allocated += std::mem::size_of::(); let dst_ref = unsafe { &mut *dst }; dst_ref.rewrite_object_values(|value| self.promote_value_if_epoch_object(value, forwarding)); @@ -243,11 +243,11 @@ mod tests { assert!(!promoted.is_null()); // The shared forwarding map must be cleared after each promote so a // later promote (or GC sweep) never observes stale old->new mappings. - assert!(vm.forwarding.is_empty()); + assert!(vm.gc_state.forwarding.is_empty()); let second = plain_object(&mut vm); let promoted2 = vm.promote_object(second); assert!(!promoted2.is_null()); - assert!(vm.forwarding.is_empty()); + assert!(vm.gc_state.forwarding.is_empty()); } } diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index e1169c9b..79e10018 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -35,7 +35,7 @@ impl SessionGc { impl SessionGc { pub(crate) fn clear_all_marks(&mut self, vm: &mut Vm) { - for &ptr in &vm.session_object_ptrs { + for &ptr in &vm.gc_state.session_object_ptrs { if ptr.is_null() { continue; } @@ -251,8 +251,8 @@ impl SessionGc { } pub(crate) fn sweep(&mut self, vm: &mut Vm) -> u64 { - let old_ptrs = std::mem::take(&mut vm.session_object_ptrs); - let mut forwarding = std::mem::take(&mut vm.forwarding); + let old_ptrs = std::mem::take(&mut vm.gc_state.session_object_ptrs); + let mut forwarding = std::mem::take(&mut vm.gc_state.forwarding); let new_arena = bumpalo::Bump::new(); let mut survivors = 0u64; let mut dead = 0u64; @@ -315,11 +315,11 @@ impl SessionGc { rewrite_vm_roots(vm, &forwarding); - vm.session_object_ptrs = forwarding.values().copied().collect(); + vm.gc_state.session_object_ptrs = forwarding.values().copied().collect(); forwarding.clear(); - vm.forwarding = forwarding; - vm.session_epoch = new_arena; - vm.session_bytes_allocated = vm.session_object_ptrs.len() * size_of::(); + vm.gc_state.forwarding = forwarding; + vm.gc_state.session_epoch = new_arena; + vm.gc_state.session_bytes_allocated = vm.gc_state.session_object_ptrs.len() * size_of::(); self.clear_all_marks(vm); @@ -355,7 +355,7 @@ impl SessionGc { /// `session_bytes_allocated` to object-only), re-adding surviving string bytes. Returns the /// bytes freed. pub(crate) fn sweep_session_strings(&mut self, vm: &mut Vm) -> u64 { - let old = std::mem::take(&mut vm.session_string_ptrs); + let old = std::mem::take(&mut vm.gc_state.session_string_ptrs); let mut freed = 0u64; let mut live_bytes = 0usize; let mut live = Vec::with_capacity(old.len()); @@ -373,8 +373,8 @@ impl SessionGc { freed += unsafe { Self::drop_dead_session_string(ptr) }; } } - vm.session_string_ptrs = live; - vm.session_bytes_allocated = vm.session_bytes_allocated.saturating_add(live_bytes); + vm.gc_state.session_string_ptrs = live; + vm.gc_state.session_bytes_allocated = vm.gc_state.session_bytes_allocated.saturating_add(live_bytes); self.total_bytes_freed = self.total_bytes_freed.saturating_add(freed); self.last_collection_bytes_freed = self.last_collection_bytes_freed.saturating_add(freed); @@ -382,8 +382,8 @@ impl SessionGc { } pub(crate) fn should_collect(&self, vm: &Vm) -> bool { - (!vm.session_object_ptrs.is_empty() || !vm.session_string_ptrs.is_empty()) - && vm.session_bytes_allocated >= vm.kernel_core().config().session_gc_threshold + (!vm.gc_state.session_object_ptrs.is_empty() || !vm.gc_state.session_string_ptrs.is_empty()) + && vm.gc_state.session_bytes_allocated >= vm.kernel_core().config().session_gc_threshold } pub(crate) fn collect(&mut self, vm: &mut Vm) { @@ -471,12 +471,12 @@ fn rewrite_vm_roots(vm: &mut Vm, forwarding: &HashMap<*mut JsObject, *mut JsObje } vm.exception_value = vm.exception_value.map(|value| rewrite_forwarded_value(value, forwarding)); vm.pending_exception = vm.pending_exception.map(|value| rewrite_forwarded_value(value, forwarding)); - for value in &mut vm.for_of_iters { + for value in &mut vm.iters.for_of_iters { *value = rewrite_forwarded_value(*value, forwarding); } - vm.last_for_of_result = rewrite_forwarded_value(vm.last_for_of_result, forwarding); + vm.iters.last_for_of_result = rewrite_forwarded_value(vm.iters.last_for_of_result, forwarding); // Converted immutables are non-session (scalars + perm-strings) — not rewritten. - for iter in &mut vm.for_in_iters { + for iter in &mut vm.iters.for_in_iters { if iter.is_null() { continue; } @@ -556,8 +556,8 @@ mod tests { vm.regs[1] = JsValue::from_js_object(child_session); vm.exception_value = Some(JsValue::from_js_object(root_session)); vm.pending_exception = Some(JsValue::from_js_object(child_session)); - vm.for_of_iters.push(JsValue::from_js_object(child_session)); - vm.last_for_of_result = JsValue::from_js_object(root_session); + vm.iters.for_of_iters.push(JsValue::from_js_object(child_session)); + vm.iters.last_for_of_result = JsValue::from_js_object(root_session); let mut roots = Vec::new(); vm.for_each_root(|v| roots.push(v)); @@ -587,9 +587,9 @@ mod tests { let unreachable_session = vm.promote_object(unreachable); vm.regs[0] = JsValue::from_js_object(root_session); - let mut gc = std::mem::take(&mut vm.session_gc); + let mut gc = std::mem::take(&mut vm.gc_state.session_gc); gc.mark(&vm); - vm.session_gc = gc; + vm.gc_state.session_gc = gc; assert!(unsafe { (*root_session).is_gc_marked() }); assert!(unsafe { (*reachable_session).is_gc_marked() }); @@ -611,15 +611,19 @@ mod tests { let root_session = vm.promote_object(root); vm.regs[0] = JsValue::from_js_object(root_session); let dead_session = vm.promote_object(dead); - let mut gc = std::mem::take(&mut vm.session_gc); + let mut gc = std::mem::take(&mut vm.gc_state.session_gc); gc.mark(&vm); let _ = gc.sweep(&mut vm); - vm.session_gc = gc; + vm.gc_state.session_gc = gc; - assert_eq!(vm.session_object_ptrs.len(), 2); - assert!(!vm.session_object_ptrs.contains(&root_session)); - assert!(!vm.session_object_ptrs.contains(&dead_session)); - assert!(!vm.session_object_ptrs.iter().any(|ptr| unsafe { (*(*ptr)).is_gc_marked() })); + assert_eq!(vm.gc_state.session_object_ptrs.len(), 2); + assert!(!vm.gc_state.session_object_ptrs.contains(&root_session)); + assert!(!vm.gc_state.session_object_ptrs.contains(&dead_session)); + assert!(!vm + .gc_state + .session_object_ptrs + .iter() + .any(|ptr| unsafe { (*(*ptr)).is_gc_marked() })); } #[test] @@ -633,14 +637,14 @@ mod tests { let root_session = vm.promote_object(root); vm.regs[0] = JsValue::from_js_object(root_session); let _ = vm.promote_object(child); - let mut gc = std::mem::take(&mut vm.session_gc); + let mut gc = std::mem::take(&mut vm.gc_state.session_gc); gc.mark(&vm); let _ = gc.sweep(&mut vm); - vm.session_gc = gc; + vm.gc_state.session_gc = gc; // The reused forwarding map MUST be cleared after sweep, otherwise // promote would observe stale old->new entries pointing into the freed arena. - assert!(vm.forwarding.is_empty()); + assert!(vm.gc_state.forwarding.is_empty()); } fn vm_with_low_threshold() -> Vm { @@ -663,16 +667,16 @@ mod tests { let mut vm = vm_with_low_threshold(); let obj = plain_object(&mut vm); vm.promote_object(obj); - assert!(!vm.session_object_ptrs.is_empty()); - let tracked_before = vm.session_object_ptrs.len(); + assert!(!vm.gc_state.session_object_ptrs.is_empty()); + let tracked_before = vm.gc_state.session_object_ptrs.len(); vm.regs[0] = JsValue::undefined(); vm.regs[1] = JsValue::undefined(); vm.reset(); - assert!(vm.session_object_ptrs.len() <= tracked_before); - assert_eq!(vm.session_object_ptrs.len(), 0); - assert_eq!(vm.session_bytes_allocated, 0); + assert!(vm.gc_state.session_object_ptrs.len() <= tracked_before); + assert_eq!(vm.gc_state.session_object_ptrs.len(), 0); + assert_eq!(vm.gc_state.session_bytes_allocated, 0); } #[test] @@ -682,7 +686,7 @@ mod tests { vm.promote_object(obj); vm.regs[0] = JsValue::undefined(); vm.maybe_collect_session_gc(); - let summary = vm.session_gc.stats_summary(); + let summary = vm.gc_state.session_gc.stats_summary(); assert!(summary.contains("[GC] collection")); } @@ -743,15 +747,15 @@ mod tests { let map_session = vm.promote_object(map_value.as_js_object_ptr()); vm.regs.fill(JsValue::undefined()); vm.regs[0] = JsValue::from_js_object(map_session); - let mut gc = std::mem::take(&mut vm.session_gc); + let mut gc = std::mem::take(&mut vm.gc_state.session_gc); gc.collect(&mut vm); - vm.session_gc = gc; + vm.gc_state.session_gc = gc; let live_map = unsafe { &*vm.regs[0].as_js_object_ptr() }; let edges = map::map_native_edges(live_map); assert_eq!(edges.len(), 2); assert!(edges.iter().all(|value| vm.is_session_ptr(value.as_js_object_ptr()))); - assert_eq!(vm.session_object_ptrs.len(), 3); + assert_eq!(vm.gc_state.session_object_ptrs.len(), 3); } #[test] @@ -766,15 +770,15 @@ mod tests { let set_session = vm.promote_object(set_value.as_js_object_ptr()); vm.regs.fill(JsValue::undefined()); vm.regs[0] = JsValue::from_js_object(set_session); - let mut gc = std::mem::take(&mut vm.session_gc); + let mut gc = std::mem::take(&mut vm.gc_state.session_gc); gc.collect(&mut vm); - vm.session_gc = gc; + vm.gc_state.session_gc = gc; let live_set = unsafe { &*vm.regs[0].as_js_object_ptr() }; let edges = set::set_native_edges(live_set); assert_eq!(edges.len(), 1); assert!(vm.is_session_ptr(edges[0].as_js_object_ptr())); - assert_eq!(vm.session_object_ptrs.len(), 2); + assert_eq!(vm.gc_state.session_object_ptrs.len(), 2); } #[test] @@ -805,9 +809,9 @@ mod tests { let root_session = vm.promote_object(root); vm.regs.fill(JsValue::undefined()); vm.regs[0] = JsValue::from_js_object(root_session); - let mut gc = std::mem::take(&mut vm.session_gc); + let mut gc = std::mem::take(&mut vm.gc_state.session_gc); gc.collect(&mut vm); - vm.session_gc = gc; + vm.gc_state.session_gc = gc; let live_root = unsafe { &*vm.regs[0].as_js_object_ptr() }; let live_typed = live_root.get_prop_at(0); @@ -854,16 +858,16 @@ mod tests { let view_session = vm.promote_object(view.as_js_object_ptr()); vm.regs.fill(JsValue::undefined()); vm.regs[0] = JsValue::from_js_object(view_session); - let mut gc = std::mem::take(&mut vm.session_gc); + let mut gc = std::mem::take(&mut vm.gc_state.session_gc); gc.collect(&mut vm); - vm.session_gc = gc; + vm.gc_state.session_gc = gc; let live_view = vm.regs[0]; let live_view_obj = unsafe { &*live_view.as_js_object_ptr() }; let edges = data_view::data_view_native_edges(live_view_obj); assert_eq!(edges.len(), 1); assert!(vm.is_session_ptr(edges[0].as_js_object_ptr())); - assert_eq!(vm.session_object_ptrs.len(), 2); + assert_eq!(vm.gc_state.session_object_ptrs.len(), 2); vm.regs[0] = live_view; vm.regs[1] = JsValue::int(0); @@ -871,9 +875,9 @@ mod tests { } fn collect(vm: &mut Vm) { - let mut gc = std::mem::take(&mut vm.session_gc); + let mut gc = std::mem::take(&mut vm.gc_state.session_gc); gc.collect(vm); - vm.session_gc = gc; + vm.gc_state.session_gc = gc; } #[test] @@ -881,12 +885,12 @@ mod tests { let mut vm = Vm::new(); let dead = vm.new_string("dead-string"); let dead_ptr = dead.as_string_ptr_mut(); - assert!(vm.session_string_ptrs.contains(&dead_ptr)); + assert!(vm.gc_state.session_string_ptrs.contains(&dead_ptr)); // No root references `dead` (it lives only on the Rust stack as a value wrapper). collect(&mut vm); - assert!(!vm.session_string_ptrs.contains(&dead_ptr)); + assert!(!vm.gc_state.session_string_ptrs.contains(&dead_ptr)); } #[test] @@ -898,7 +902,7 @@ mod tests { collect(&mut vm); - assert!(vm.session_string_ptrs.contains(&live_ptr)); + assert!(vm.gc_state.session_string_ptrs.contains(&live_ptr)); // Live strings are never moved — the register still points at the same box. assert_eq!(vm.regs[0].as_string_ptr_mut(), live_ptr); assert_eq!(unsafe { (*live_ptr).as_str() }, "live-in-reg"); @@ -921,7 +925,7 @@ mod tests { collect(&mut vm); - assert!(vm.session_string_ptrs.contains(&s_ptr)); + assert!(vm.gc_state.session_string_ptrs.contains(&s_ptr)); assert_eq!(unsafe { (*s_ptr).as_str() }, "prop-string"); } @@ -931,13 +935,13 @@ mod tests { let perm = vm.perm_string("perm"); let perm_ptr = perm.as_string_ptr_mut(); // Permanent strings live in PermInterner, never in the session set. - assert!(!vm.session_string_ptrs.contains(&perm_ptr)); + assert!(!vm.gc_state.session_string_ptrs.contains(&perm_ptr)); vm.regs[0] = perm; collect(&mut vm); // Never dropped by the session sweep (still readable, still absent from session set). - assert!(!vm.session_string_ptrs.contains(&perm_ptr)); + assert!(!vm.gc_state.session_string_ptrs.contains(&perm_ptr)); assert_eq!(unsafe { (*perm_ptr).as_str() }, "perm"); } @@ -948,11 +952,11 @@ mod tests { let _ = dead.as_string_ptr_mut(); let expected = (size_of::() + "0123456789".len()) as u64; - let mut gc = std::mem::take(&mut vm.session_gc); + let mut gc = std::mem::take(&mut vm.gc_state.session_gc); let before = gc.total_bytes_freed; gc.collect(&mut vm); let after = gc.total_bytes_freed; - vm.session_gc = gc; + vm.gc_state.session_gc = gc; assert!(after >= before + expected); } diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 0b5f1039..336027ff 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -1,6 +1,5 @@ #![allow(clippy::arc_with_non_send_sync)] -use std::collections::HashMap; use std::sync::{Arc, OnceLock}; use oxide_bytecode::module::{CompiledModule, Constant}; @@ -11,12 +10,12 @@ pub use crate::bindings::init_kernel_builtins; use crate::coercion; use crate::native::NativeFn; use crate::session_gc::SessionGc; +use crate::vm_state::{GcState, IterState, ProfilingState, SymbolState}; use oxide_kernel::kernel::{KernelCore, KernelSession}; -use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_runtime_api::NativeResult; use oxide_types::error::{JsError, JsErrorKind}; use oxide_types::mem::{Epoch, P}; -use oxide_types::object::{JsObject, JsString, NativeFnPtr}; +use oxide_types::object::{JsObject, NativeFnPtr}; use oxide_types::value::JsValue; pub(crate) const MAX_PROTO_CHAIN_DEPTH: usize = 1024; @@ -81,14 +80,6 @@ macro_rules! binary_arith { }} } -macro_rules! compound_arith { - ($self:ident, $rd:expr, $a:expr, $op:tt) => {{ - let l = $self.coerce_number_bounded($self.regs[$rd])?; - let r = $self.coerce_number_bounded($self.regs[$a])?; - $self.regs[$rd] = JsValue::float(l $op r); - }} -} - #[derive(Debug, Clone, Copy)] pub enum FrameContinuation { None, @@ -156,21 +147,9 @@ pub struct Vm { /// A fat `*const` because the cache Vec is VM-owned and run-stable (OnceLock filled once). pub(crate) active_immutables: *const [JsValue], pub(crate) frames: SmallVec<[CallFrame; 16]>, - pub for_in_iters: Vec<*mut ForInIter<'static>>, pub(crate) kernel_core: Arc, pub(crate) session: KernelSession, - pub(crate) session_string_ptrs: Vec<*mut JsString>, pub epoch: Epoch, - pub(crate) session_epoch: bumpalo::Bump, - pub(crate) session_gc: SessionGc, - /// Long-lived forwarding map reused by GC sweep and promote. - /// `mem::take`'d out for use, `clear()`'d and put back — capacity retained - /// across collections, so no per-event `HashMap` allocation. `FxBuildHasher` - /// because the keys are raw pointers (collision resistance is irrelevant). - pub(crate) forwarding: HashMap<*mut JsObject, *mut JsObject, rustc_hash::FxBuildHasher>, - pub(crate) epoch_object_ptrs: Vec<*mut JsObject>, - pub(crate) session_object_ptrs: Vec<*mut JsObject>, - pub(crate) session_bytes_allocated: usize, pub object_prototype: P, pub math_rng_state: u64, pub(crate) sub_modules: Arc>, @@ -184,11 +163,6 @@ pub struct Vm { pub(crate) exception_value: Option, pub(crate) pending_exception: Option, pub(crate) pending_error_kind: Option<&'static str>, - pub(crate) symbol_counter: u32, - pub(crate) symbol_descriptions: Vec, - pub(crate) symbol_registry: HashMap, - pub(crate) for_of_iters: Vec, - pub(crate) last_for_of_result: JsValue, pub(crate) root_reg_limit: u8, pub(crate) active_reg_limit: u8, pub(crate) native_call_depth: usize, @@ -196,9 +170,14 @@ pub struct Vm { /// The dispatch loop checks this flag and skips writing `regs[target_reg]` from the /// call result — the value will be delivered by the RETURN handler instead. pub(crate) accessor_frame_target_reg: Option, - pub(crate) ic_hits: std::cell::Cell, - pub(crate) ic_misses: std::cell::Cell, - pub(crate) instruction_count: u64, + /// Grouped session-arena / GC bookkeeping. + pub(crate) gc_state: GcState, + /// Grouped `Symbol` interning state. + pub(crate) symbols: SymbolState, + /// Grouped live `for-in` / `for-of` iterator state. + pub(crate) iters: IterState, + /// Grouped inline-cache and instruction counters. + pub(crate) profiling: ProfilingState, } impl Vm { @@ -302,7 +281,7 @@ impl Vm { pub(crate) fn alloc_object(&mut self, obj: JsObject) -> *mut JsObject { let ptr = self.epoch.alloc(obj); - self.epoch_object_ptrs.push(ptr); + self.gc_state.epoch_object_ptrs.push(ptr); ptr } @@ -348,12 +327,12 @@ impl Vm { f(JsValue::from_js_object(self.session.global_object().as_ptr() as *mut JsObject)); f(self.exception_value.unwrap_or(JsValue::undefined())); f(self.pending_exception.unwrap_or(JsValue::undefined())); - for &v in &self.for_of_iters { + for &v in &self.iters.for_of_iters { f(v); } - f(self.last_for_of_result); + f(self.iters.last_for_of_result); // Converted immutables (scalars + perm-strings) are NOT session GC roots — not scanned. - for iter in &self.for_in_iters { + for iter in &self.iters.for_in_iters { if iter.is_null() { continue; } @@ -366,50 +345,50 @@ impl Vm { } pub(crate) fn maybe_collect_session_gc(&mut self) { - let mut session_gc = std::mem::take(&mut self.session_gc); + let mut session_gc = std::mem::take(&mut self.gc_state.session_gc); session_gc.maybe_collect(self); - self.session_gc = session_gc; + self.gc_state.session_gc = session_gc; } pub fn session_gc_stats(&self) -> &SessionGc { - &self.session_gc + &self.gc_state.session_gc } pub fn session_object_count(&self) -> usize { - self.session_object_ptrs.len() + self.gc_state.session_object_ptrs.len() } pub fn session_bytes_allocated(&self) -> usize { - self.session_bytes_allocated + self.gc_state.session_bytes_allocated } pub fn epoch_object_count(&self) -> usize { - self.epoch_object_ptrs.len() + self.gc_state.epoch_object_ptrs.len() } pub fn ic_hit_rate(&self) -> f64 { - let total = self.ic_hits.get() + self.ic_misses.get(); + let total = self.profiling.ic_hits.get() + self.profiling.ic_misses.get(); if total == 0 { 0.0 } else { - self.ic_hits.get() as f64 / total as f64 + self.profiling.ic_hits.get() as f64 / total as f64 } } pub fn instruction_count(&self) -> u64 { - self.instruction_count + self.profiling.instruction_count } pub fn symbol_registry_len(&self) -> usize { - self.symbol_registry.len() + self.symbols.symbol_registry.len() } pub fn ic_hit_count(&self) -> u64 { - self.ic_hits.get() + self.profiling.ic_hits.get() } pub fn ic_miss_count(&self) -> u64 { - self.ic_misses.get() + self.profiling.ic_misses.get() } pub(crate) fn checked_object_ptr( @@ -728,12 +707,12 @@ impl Vm { steps += 1; if let Some(max_steps) = self.kernel_core.config.max_steps { if steps > max_steps { - self.instruction_count = steps; + self.profiling.instruction_count = steps; return Err(format!("VM step limit exceeded at pc={}", self.pc)); } } if self.pc >= self.bytecode.len() { - self.instruction_count = steps; + self.profiling.instruction_count = steps; return Err("program counter out of bounds".into()); } @@ -748,7 +727,7 @@ impl Vm { OpCode::NOP => {} OpCode::HALT => { - self.instruction_count = steps; + self.profiling.instruction_count = steps; return Ok(self.regs[0]); } @@ -757,48 +736,13 @@ impl Vm { } OpCode::CREATE_CLOSURE => { - let sub_idx = opcode::imm16(instr) as u32; - debug_assert!(sub_idx > 0 && (sub_idx as usize) <= self.sub_modules.len()); - let sub = &self.sub_modules[sub_idx as usize - 1]; - let result = self.create_function_object( - sub_idx, - sub.is_arrow, - sub.is_class_constructor, - sub.is_derived_constructor, - sub.needs_home_object, - ); - self.regs[rd] = result; - } - OpCode::CREATE_REGEXP => { - let pat_val = self.regs[a]; - let flags_val = self.regs[b]; - let ctor_ptr = self.session.builtin_world().regexp_constructor.as_ptr() as *mut JsObject; - let ctor = unsafe { &*ctor_ptr }; - let Some(native_fn) = ctor.native_fn() else { - self.raise_error_kind("TypeError", "RegExp constructor unavailable")?; - return Ok(JsValue::undefined()); - }; - let saved_0 = self.regs[0]; - let saved_1 = self.regs[1]; - let saved_2 = self.regs[2]; - self.regs[0] = JsValue::undefined(); - self.regs[1] = pat_val; - self.regs[2] = flags_val; - let func = unsafe { crate::vm::native_fn_ptr_to_fn(native_fn) }; - let result = match func(self, &[0, 1, 2]) { - oxide_runtime_api::NativeResult::Ok(v) => v, - oxide_runtime_api::NativeResult::Err(e) => { - return Err(self.error_message_text("TypeError", &self.error_text(e))); - } - oxide_runtime_api::NativeResult::TailCall { .. } => { - return Err(self.error_message_text("TypeError", "unexpected tail call")); - } - }; - self.regs[0] = saved_0; - self.regs[1] = saved_1; - self.regs[2] = saved_2; - self.regs[rd] = result; + self.dispatch_create_closure(rd, instr); } + OpCode::CREATE_REGEXP => match self.dispatch_create_regexp(rd, a, b) { + Ok(Some(result)) => return Ok(result), + Ok(None) => {} + Err(e) => return Err(e), + }, OpCode::ADD => { self.dispatch_add(rd, a, b)?; @@ -821,8 +765,7 @@ impl Vm { } OpCode::NEG => { - let v = self.coerce_number_bounded(self.regs[a])?; - self.regs[rd] = JsValue::float(-v); + self.dispatch_neg(rd, a)?; } OpCode::BIT_AND => { @@ -886,57 +829,36 @@ impl Vm { } OpCode::UNARY_PLUS => { - let v = self.coerce_number_bounded(self.regs[a])?; - self.regs[rd] = JsValue::float(v); + self.dispatch_unary_plus(rd, a)?; } OpCode::JMP => { - let offset = opcode::offset16(instr) as isize; - self.pc = ((self.pc as isize) + offset - 1) as usize; + self.dispatch_jmp(instr); } OpCode::JMP_IF_FALSE => { - let cond = coercion::to_boolean(self.regs[rd]); - if !cond { - let offset = opcode::offset16(instr) as isize; - self.pc = ((self.pc as isize) + offset - 1) as usize; - } + self.dispatch_jmp_if_false(rd, instr); } OpCode::JMP_IF_TRUE => { - let cond = coercion::to_boolean(self.regs[rd]); - if cond { - let offset = opcode::offset16(instr) as isize; - self.pc = ((self.pc as isize) + offset - 1) as usize; - } + self.dispatch_jmp_if_true(rd, instr); } OpCode::JMP_IF_NULLISH => { - if self.regs[rd].is_nullish() { - let offset = opcode::offset16(instr) as isize; - self.pc = ((self.pc as isize) + offset - 1) as usize; - } + self.dispatch_jmp_if_nullish(rd, instr); } - OpCode::LOAD_VAR => { - if a == 254 - && self.frames.last().map(|frame| frame.is_derived_constructor).unwrap_or(false) - && self.regs[a].is_undefined() - { - throw_err!(self, ReferenceError, "must call super constructor before using 'this'"); - } - self.regs[rd] = self.regs[a]; - } + OpCode::LOAD_VAR => match self.dispatch_load_var(rd, a) { + Ok(true) => continue, + Ok(false) => {} + Err(e) => return Err(e), + }, - OpCode::STORE_VAR => { - if b != 0 { - // const guard: check if already initialized - if !self.regs[rd].is_undefined() { - throw_err!(self, TypeError, "Assignment to constant variable"); - } - } - self.regs[rd] = self.regs[a]; - } + OpCode::STORE_VAR => match self.dispatch_store_var(rd, a, b) { + Ok(true) => continue, + Ok(false) => {} + Err(e) => return Err(e), + }, OpCode::CALL => match self.dispatch_call(rd, a, b) { Ok(true) => continue, @@ -954,179 +876,25 @@ impl Vm { Err(e) => return Err(e), }, - OpCode::SUPER_CALL => { - let first_arg_reg = a as u8; - let ext = self.bytecode[self.pc]; - self.pc += 1; - let arg_count = (ext & 0xFF) as usize; - - let Some(frame) = self.frames.last() else { - throw_err!(self, ReferenceError, "super() used outside class constructor"); - }; - if !frame.is_derived_constructor { - throw_err!(self, ReferenceError, "super() used outside derived constructor"); - } - if !self.regs[254].is_undefined() { - throw_err!(self, ReferenceError, "super() called more than once"); - } - let Some(derived_this) = frame.constructed_this else { - throw_err!(self, ReferenceError, "super() without derived this"); - }; - - let new_target = self.regs[255]; - if !new_target.is_object() { - throw_err!(self, TypeError, "super() new.target is not an object"); - } - let new_target_obj = unsafe { &*new_target.as_js_object_ptr() }; - let super_ctor = new_target_obj.proto(); - if !super_ctor.is_object() { - throw_err!(self, TypeError, "super constructor is not an object"); - } - let super_obj = unsafe { &*super_ctor.as_js_object_ptr() }; - if !super_obj.is_function() { - throw_err!(self, TypeError, "super constructor is not a function"); - } - - if super_obj.native_fn().is_some() { - self.regs[253] = derived_this; - self.regs[254] = super_ctor; // bind_dispatcher reads regs[254] as the wrapper callee - let (args_buf, len) = Self::build_native_args(first_arg_reg, arg_count, 253); - // SAFETY: native_fn was set via set_native_fn with a valid NativeFn pointer; - // native_fn_ptr_to_fn is the single coercion point for NativeFnPtr → NativeFn. - let func: NativeFn = unsafe { native_fn_ptr_to_fn(super_obj.native_fn().unwrap()) }; - match func(self, &args_buf[..len]) { - NativeResult::Ok(val) => { - self.regs[254] = if val.is_object() { val } else { derived_this }; - self.regs[rd] = self.regs[254]; - } - NativeResult::Err(err_val) => { - self.exception_value = Some(err_val); - self.pending_error_kind = Some(self.thrown_error_kind(err_val)); - match self.unwind() { - Ok(()) => continue, - Err(e) => return Err(e), - } - } - NativeResult::TailCall { callee, this, args } => { - // e.g. bound function: resolve the tail call, use its return - // value as the constructed instance (or fall back to derived_this). - match self.call_function_sync(callee, this, &args) { - Ok(val) => { - self.regs[254] = if val.is_object() { val } else { derived_this }; - self.regs[rd] = self.regs[254]; - } - Err(e) => return Err(e), - } - } - } - } else if super_obj.sub_module_index() > 0 { - let sub_idx = super_obj.sub_module_index() as usize - 1; - if sub_idx >= self.sub_modules.len() { - return Err(format!( - "SUPER_CALL: sub_module_index {} out of bounds (max {})", - sub_idx, - self.sub_modules.len() - )); - } - if self.frames.len() >= self.kernel_core.config.max_call_depth { - return Err(self.error_message_text("RangeError", "Maximum call stack size exceeded")); - } - - let sub_bytecode = self.sub_modules[sub_idx].bytecode.clone(); - let sub_n_args = self.sub_modules[sub_idx].n_args as usize; - let sub_n_registers = self.sub_modules[sub_idx].n_registers; - let sub_param_base = self.sub_modules[sub_idx].param_base as usize; - let caller_reg_limit = self.active_reg_limit.max(1); - let saved_reg_offset = self.save_stack.len() as u32; - self.save_stack.extend_from_slice(&self.regs[..caller_reg_limit as usize]); - let saved_this = self.regs[254]; - let saved_new_target = self.regs[255]; - - for i in 0..sub_n_args { - let src_reg = first_arg_reg.wrapping_add(i as u8) as usize; - self.regs[sub_param_base + i] = self.regs[src_reg]; - } - self.regs[254] = derived_this; - self.regs[255] = new_target; - - self.saved_bytecode_stack.push(std::mem::take(&mut self.bytecode)); - self.saved_immutables_stack.push(self.active_immutables); - self.frames.push(CallFrame { - return_addr: self.pc, - function_name: self.sub_modules[sub_idx] - .function_name - .as_deref() - .map(|name| self.kernel_core.perm_interner().intern(name).0) - .unwrap_or(0), - caller_reg_limit, - saved_reg_offset, - saved_this, - saved_new_target, - callee: super_ctor, - construct_result_reg: Some(254), - constructed_this: Some(derived_this), - is_derived_constructor: super_obj.is_derived_constructor(), - continuation: FrameContinuation::None, - }); - - self.bytecode = sub_bytecode; - let subs = Arc::clone(&self.sub_modules); - self.activate_immutables(sub_idx + 1, &subs[sub_idx].constants); - for (name, reg) in &self.sub_modules[sub_idx].builtin_reg_map { - let si = self.kernel_core.perm_interner().intern(name.as_str()).0; - let global = self.session.global_object(); - if let Some(pos) = self.kernel_core.shape_forge().lookup_position(global.shape_id(), si) { - self.regs[*reg as usize] = global.get_prop_at(pos); - } - } - self.active_reg_limit = sub_n_registers.max(1); - self.pc = 0; - continue; - } else { - throw_err!(self, TypeError, "super constructor is not callable"); - } - } + OpCode::SUPER_CALL => match self.dispatch_super_call(rd, a) { + Ok(true) => continue, + Ok(false) => {} + Err(e) => return Err(e), + }, OpCode::SUPER_GET_PROP | OpCode::SUPER_STATIC_GET_PROP => { - let key_val = self.regs[b]; - let prop_name_si = self.property_key_si(key_val); - let Some(frame) = self.frames.last() else { - throw_err!(self, ReferenceError, "super property used outside function"); - }; - if !frame.callee.is_object() { - throw_err!(self, ReferenceError, "super property has no home object"); - } - let callee_obj = unsafe { &*frame.callee.as_js_object_ptr() }; - let home_object = callee_obj.home_object(); - if !home_object.is_object() { - throw_err!(self, ReferenceError, "super property has no home object"); - } - let home_obj = unsafe { &*home_object.as_js_object_ptr() }; - let super_base = home_obj.proto(); - if !super_base.is_object() { - self.regs[rd] = JsValue::undefined(); - } else { - let super_obj = unsafe { &*super_base.as_js_object_ptr() }; - let val = self.ordinary_get_with_target(super_obj, prop_name_si, self.regs[a], rd as u8)?; - if self.accessor_frame_target_reg.take().is_none() { - self.regs[rd] = val; - } + match self.dispatch_super_get_prop(rd, a, b) { + Ok(true) => continue, + Ok(false) => {} + Err(e) => return Err(e), } } - OpCode::SET_HOME_OBJECT => { - let func_val = self.regs[rd]; - let home_val = self.regs[a]; - if !func_val.is_object() || !home_val.is_object() { - throw_err!(self, TypeError, "SET_HOME_OBJECT expects function and object"); - } - let home_val = self.promote_if_needed_for_write_ptr(func_val.as_js_object_ptr(), home_val); - let func_obj = unsafe { &mut *func_val.as_js_object_ptr() }; - if !func_obj.is_function() { - throw_err!(self, TypeError, "SET_HOME_OBJECT target is not a function"); - } - func_obj.set_home_object(home_val); - } + OpCode::SET_HOME_OBJECT => match self.dispatch_set_home_object(rd, a) { + Ok(true) => continue, + Ok(false) => {} + Err(e) => return Err(e), + }, OpCode::DEFINE_ACCESSOR => { self.dispatch_define_accessor(rd, a, b)?; @@ -1153,61 +921,35 @@ impl Vm { } OpCode::NEW_OBJECT => { - let proto_ptr = &*self.object_prototype as *const JsObject as *mut JsObject; - let obj = self - .epoch - .alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(proto_ptr))); - self.regs[rd] = JsValue::object(obj as *mut u8); + self.dispatch_new_object(rd); } OpCode::NEW_ARRAY => { - let proto_ptr = self.session.builtin_world().array_proto.as_ptr() as *mut JsObject; - let n = opcode::imm16(instr) as usize; - let bump = self.epoch.bump(); - let obj = self.alloc_object(JsObject::new_array( - EMPTY_SHAPE_ID, - JsValue::from_js_object(proto_ptr), - n, - bump, - )); - self.regs[rd] = JsValue::object(obj as *mut u8); + self.dispatch_new_array(rd, instr); } OpCode::COMPOUND_ADD => { - let lhs = self.coerce_primitive_bounded(self.regs[rd], false)?; - let rhs = self.coerce_primitive_bounded(self.regs[a], false)?; - if lhs.is_string() || rhs.is_string() { - let ls = coercion::to_string(lhs); - let rs = coercion::to_string(rhs); - let concat = format!("{ls}{rs}"); - self.regs[rd] = self.new_string(&concat); - } else { - let ln = coercion::to_number(lhs); - let rn = coercion::to_number(rhs); - self.regs[rd] = JsValue::float(ln + rn); - } + self.dispatch_compound_add(rd, a)?; } OpCode::COMPOUND_SUB => { - compound_arith!(self, rd, a, -); + self.dispatch_compound_sub(rd, a)?; } OpCode::COMPOUND_MUL => { - compound_arith!(self, rd, a, *); + self.dispatch_compound_mul(rd, a)?; } OpCode::COMPOUND_DIV => { - compound_arith!(self, rd, a, /); + self.dispatch_compound_div(rd, a)?; } OpCode::COMPOUND_MOD => { - compound_arith!(self, rd, a, %); + self.dispatch_compound_mod(rd, a)?; } OpCode::COMPOUND_EXP => { - let l = self.coerce_number_bounded(self.regs[rd])?; - let r = self.coerce_number_bounded(self.regs[a])?; - self.regs[rd] = JsValue::float(l.powf(r)); + self.dispatch_compound_exp(rd, a)?; } OpCode::COMPOUND_AND => { @@ -1239,39 +981,24 @@ impl Vm { } OpCode::VOID => { - self.regs[rd] = JsValue::undefined(); + self.dispatch_void(rd); } OpCode::TEMPLATE_STR => { self.dispatch_template_str(rd); } - OpCode::DELETE_PROP_STATIC => { - let prop_idx = self.bytecode[self.pc] as usize; - self.pc += 1; - let key_val = self.immutables().get(prop_idx).copied().unwrap_or_else(JsValue::undefined); - let prop_name_si = self.property_key_si(key_val); - let Some(obj_ptr) = self.checked_object_ptr(self.regs[rd], "delete on non-object")? else { - continue; - }; - let obj = unsafe { &mut *obj_ptr }; - if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { - obj.set_prop_at(pos, JsValue::undefined()); - } - self.regs[rd] = JsValue::bool(true); - } + OpCode::DELETE_PROP_STATIC => match self.dispatch_delete_prop_static(rd) { + Ok(true) => continue, + Ok(false) => {} + Err(e) => return Err(e), + }, - OpCode::DELETE_PROP_DYNAMIC => { - let prop_name_si = self.property_key_si(self.regs[b]); - let Some(obj_ptr) = self.checked_object_ptr(self.regs[rd], "delete on non-object")? else { - continue; - }; - let obj = unsafe { &mut *obj_ptr }; - if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { - obj.set_prop_at(pos, JsValue::undefined()); - } - self.regs[rd] = JsValue::bool(true); - } + OpCode::DELETE_PROP_DYNAMIC => match self.dispatch_delete_prop_dynamic(rd, b) { + Ok(true) => continue, + Ok(false) => {} + Err(e) => return Err(e), + }, OpCode::INSTANCEOF => { self.dispatch_instanceof(rd, a, b)?; @@ -1298,29 +1025,19 @@ impl Vm { } OpCode::INC_PRE => { - let n = self.coerce_number_bounded(self.regs[rd])?; - let result = JsValue::float(n + 1.0); - self.regs[rd] = result; - self.regs[a] = result; + self.dispatch_inc_pre(rd, a)?; } OpCode::INC_POST => { - let n = self.coerce_number_bounded(self.regs[rd])?; - self.regs[a] = JsValue::float(n); - self.regs[rd] = JsValue::float(n + 1.0); + self.dispatch_inc_post(rd, a)?; } OpCode::DEC_PRE => { - let n = self.coerce_number_bounded(self.regs[rd])?; - let result = JsValue::float(n - 1.0); - self.regs[rd] = result; - self.regs[a] = result; + self.dispatch_dec_pre(rd, a)?; } OpCode::DEC_POST => { - let n = self.coerce_number_bounded(self.regs[rd])?; - self.regs[a] = JsValue::float(n); - self.regs[rd] = JsValue::float(n - 1.0); + self.dispatch_dec_post(rd, a)?; } OpCode::MEMBER_INC @@ -1503,8 +1220,10 @@ mod tests { continuation: super::FrameContinuation::None, }); vm.save_stack.push(JsValue::undefined()); - vm.for_in_iters.push(std::ptr::dangling_mut::>()); - vm.for_of_iters.push(JsValue::undefined()); + vm.iters + .for_in_iters + .push(std::ptr::dangling_mut::>()); + vm.iters.for_of_iters.push(JsValue::undefined()); vm.saved_bytecode_stack .push(vec![opcode::encode(opcode::OpCode::HALT, 0, 0, 0)]); vm.saved_immutables_stack @@ -1523,8 +1242,8 @@ mod tests { assert_eq!(vm.pc, 0); assert!(vm.frames.is_empty()); assert!(vm.save_stack.is_empty()); - assert!(vm.for_in_iters.is_empty()); - assert!(vm.for_of_iters.is_empty()); + assert!(vm.iters.for_in_iters.is_empty()); + assert!(vm.iters.for_of_iters.is_empty()); assert!(vm.saved_bytecode_stack.is_empty()); assert!(vm.saved_immutables_stack.is_empty()); assert!(vm.try_stack.is_empty()); @@ -1538,15 +1257,15 @@ mod tests { #[test] fn full_reset_clears_symbol_state() { let mut vm = Vm::new(); - vm.symbol_counter = 1; - vm.symbol_descriptions.push("shared".to_string()); - vm.symbol_registry.insert("shared".to_string(), 0); + vm.symbols.symbol_counter = 1; + vm.symbols.symbol_descriptions.push("shared".to_string()); + vm.symbols.symbol_registry.insert("shared".to_string(), 0); vm.full_reset(); - assert_eq!(vm.symbol_counter, 0); - assert!(vm.symbol_descriptions.is_empty()); - assert!(vm.symbol_registry.is_empty()); + assert_eq!(vm.symbols.symbol_counter, 0); + assert!(vm.symbols.symbol_descriptions.is_empty()); + assert!(vm.symbols.symbol_registry.is_empty()); } #[test] @@ -1560,11 +1279,11 @@ mod tests { ..CompiledModule::new() }; let mut vm = Vm::new(); - vm.for_of_iters.push(JsValue::undefined()); + vm.iters.for_of_iters.push(JsValue::undefined()); vm.run(&module).expect("FOR_OF_CLOSE should tolerate non-object sentinel"); - assert!(vm.for_of_iters.is_empty()); + assert!(vm.iters.for_of_iters.is_empty()); } #[test] @@ -1572,7 +1291,7 @@ mod tests { let mut vm = Vm::new(); vm.bytecode = vec![0, 0, 0]; vm.pc = 3; - vm.write_ic_back(0x1234_5678, 7); + crate::ic_helper::write_ic_back(&mut vm.bytecode, vm.pc, 0x1234_5678, 7); assert_eq!(vm.bytecode[0], 0x0034_5678); assert_eq!(vm.bytecode[1], 7); assert_eq!(vm.bytecode[2], 0); diff --git a/crates/oxide_vm/src/vm_dispatch_misc.rs b/crates/oxide_vm/src/vm_dispatch_misc.rs index ceb1749d..4ae8c46a 100644 --- a/crates/oxide_vm/src/vm_dispatch_misc.rs +++ b/crates/oxide_vm/src/vm_dispatch_misc.rs @@ -309,12 +309,12 @@ impl Vm { } let iter = self.epoch.alloc(ForInIter { keys: keys_vec, index: 0 }); - self.for_in_iters.push(iter.cast::>()); + self.iters.for_in_iters.push(iter.cast::>()); Ok(()) } pub(crate) fn dispatch_for_in_next(&mut self, rd: usize) -> Result<(), String> { - let iter_ptr = self.for_in_iters.last().copied().unwrap_or(std::ptr::null_mut()); + let iter_ptr = self.iters.for_in_iters.last().copied().unwrap_or(std::ptr::null_mut()); if iter_ptr.is_null() { return Err("FOR_IN_NEXT without active iterator".into()); } @@ -329,7 +329,7 @@ impl Vm { } pub(crate) fn dispatch_for_in_done(&mut self, rd: usize) { - let iter_ptr = self.for_in_iters.last().copied().unwrap_or(std::ptr::null_mut()); + let iter_ptr = self.iters.for_in_iters.last().copied().unwrap_or(std::ptr::null_mut()); if iter_ptr.is_null() { self.regs[rd] = JsValue::bool(true); } else { @@ -339,15 +339,15 @@ impl Vm { } pub(crate) fn dispatch_for_in_cleanup(&mut self) { - self.for_in_iters.pop(); + self.iters.for_in_iters.pop(); } pub(crate) fn dispatch_for_of_init(&mut self, a: usize) -> Result<(), String> { let iterable = self.regs[a]; match crate::builtins::iterator::make_iterator_for_value(self, iterable) { Ok(iterator) => { - self.for_of_iters.push(iterator); - self.last_for_of_result = JsValue::undefined(); + self.iters.for_of_iters.push(iterator); + self.iters.last_for_of_result = JsValue::undefined(); Ok(()) } Err(err) => { @@ -359,7 +359,7 @@ impl Vm { } pub(crate) fn dispatch_for_of_done(&mut self, rd: usize) -> Result<(), String> { - let Some(iterator) = self.for_of_iters.last().copied() else { + let Some(iterator) = self.iters.for_of_iters.last().copied() else { return Err("FOR_OF_DONE without active iterator".into()); }; if !iterator.is_object() { @@ -378,13 +378,13 @@ impl Vm { let done_si = self.kernel_core.perm_interner().intern("done").0; let done_val = self.ordinary_get(result_obj, done_si, result)?; let done = crate::coercion::to_boolean(done_val); - self.last_for_of_result = result; + self.iters.last_for_of_result = result; self.regs[rd] = JsValue::bool(!done); Ok(()) } pub(crate) fn dispatch_for_of_next(&mut self, rd: usize) -> Result<(), String> { - let result = self.last_for_of_result; + let result = self.iters.last_for_of_result; if !result.is_object() { self.regs[rd] = JsValue::undefined(); return Ok(()); @@ -396,7 +396,7 @@ impl Vm { } pub(crate) fn dispatch_for_of_close(&mut self) -> Result<(), String> { - let Some(iterator) = self.for_of_iters.pop() else { + let Some(iterator) = self.iters.for_of_iters.pop() else { return Ok(()); }; if !iterator.is_object() { diff --git a/crates/oxide_vm/src/vm_props.rs b/crates/oxide_vm/src/vm_props.rs index 567fc5d0..13f3e017 100644 --- a/crates/oxide_vm/src/vm_props.rs +++ b/crates/oxide_vm/src/vm_props.rs @@ -204,7 +204,7 @@ impl Vm { if template.prop_name != prop_name_si { self.ordinary_get(obj, prop_name_si, receiver)? } else if template.position < obj.prop_vec_len() as u32 { - self.write_ic_back(obj.shape_id(), template.position); + crate::ic_helper::write_ic_back(&mut self.bytecode, self.pc, obj.shape_id(), template.position); obj.get_prop_at(template.position) } else { self.ordinary_get(obj, prop_name_si, receiver)? @@ -213,7 +213,7 @@ impl Vm { let resolved = self.ordinary_get(obj, prop_name_si, receiver)?; if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { if !obj.is_accessor_meta(pos) { - self.write_ic_back(obj.shape_id(), pos); + crate::ic_helper::write_ic_back(&mut self.bytecode, self.pc, obj.shape_id(), pos); } } resolved @@ -231,7 +231,7 @@ impl Vm { return Ok(()); } obj.set_prop_at(pos, val); - self.write_ic_back(obj.shape_id(), pos); + crate::ic_helper::write_ic_back(&mut self.bytecode, self.pc, obj.shape_id(), pos); } else { self.ordinary_set(obj, prop_name_si, val, receiver)?; } @@ -314,11 +314,4 @@ impl Vm { obj.bump_generation(); Ok(()) } - - pub(crate) fn write_ic_back(&mut self, shape_id: u32, slot_index: u32) { - debug_assert!(self.pc >= 3, "IC write-back requires 3 extension words before pc"); - self.bytecode[self.pc - 3] = shape_id & 0x00FF_FFFF; - self.bytecode[self.pc - 2] = slot_index; - self.bytecode[self.pc - 1] = 0; - } } diff --git a/crates/oxide_vm/src/vm_runtime.rs b/crates/oxide_vm/src/vm_runtime.rs index 912d237e..3d8a0f5b 100644 --- a/crates/oxide_vm/src/vm_runtime.rs +++ b/crates/oxide_vm/src/vm_runtime.rs @@ -1,7 +1,6 @@ use std::sync::{Arc, OnceLock}; use oxide_bytecode::module::CompiledModule; -use oxide_bytecode::opcode; use crate::vm::{CallFrame, InlineSyncState, Vm}; use oxide_types::object::JsObject; @@ -47,9 +46,9 @@ impl Vm { exception_value: self.exception_value.take(), pending_exception: self.pending_exception.take(), pending_error_kind: self.pending_error_kind.take(), - for_in_iters: std::mem::take(&mut self.for_in_iters), - for_of_iters: std::mem::take(&mut self.for_of_iters), - last_for_of_result: self.last_for_of_result, + for_in_iters: std::mem::take(&mut self.iters.for_in_iters), + for_of_iters: std::mem::take(&mut self.iters.for_of_iters), + last_for_of_result: self.iters.last_for_of_result, saved_bytecode_stack: std::mem::take(&mut self.saved_bytecode_stack), saved_immutables_stack: std::mem::take(&mut self.saved_immutables_stack), save_stack: std::mem::take(&mut self.save_stack), @@ -89,9 +88,9 @@ impl Vm { self.exception_value = saved.exception_value; self.pending_exception = saved.pending_exception; self.pending_error_kind = saved.pending_error_kind; - self.for_in_iters = saved.for_in_iters; - self.for_of_iters = saved.for_of_iters; - self.last_for_of_result = saved.last_for_of_result; + self.iters.for_in_iters = saved.for_in_iters; + self.iters.for_of_iters = saved.for_of_iters; + self.iters.last_for_of_result = saved.last_for_of_result; self.saved_bytecode_stack = saved.saved_bytecode_stack; self.saved_immutables_stack = saved.saved_immutables_stack; self.save_stack = saved.save_stack; @@ -119,27 +118,10 @@ impl Vm { pub fn rerun(&mut self) -> Result { self.clear_execution_state(); self.active_reg_limit = self.root_reg_limit; - self.clear_ic_caches(); + crate::ic_helper::clear_ic_caches(&mut self.bytecode); self.dispatch() } - fn clear_ic_caches(&mut self) { - let mut i = 0; - while i < self.bytecode.len() { - let op = opcode::opcode(self.bytecode[i]); - if op.has_ic_ext_words() { - if i + 3 < self.bytecode.len() { - self.bytecode[i + 1] = 0; - self.bytecode[i + 2] = 0; - self.bytecode[i + 3] = 0; - } - i += 4; - } else { - i += 1; - } - } - } - pub fn run(&mut self, module: &CompiledModule) -> Result { self.clear_execution_state(); self.sub_modules = Arc::new(module.sub_modules.clone()); diff --git a/crates/oxide_vm/src/vm_state.rs b/crates/oxide_vm/src/vm_state.rs new file mode 100644 index 00000000..5c0a87f1 --- /dev/null +++ b/crates/oxide_vm/src/vm_state.rs @@ -0,0 +1,57 @@ +//! Grouped `Vm` state sub-structs. +//! +//! `Vm` owns these as fields (`gc_state`, `symbols`, `iters`, `profiling`) so +//! that workers refactoring one subsystem touch one struct instead of the +//! central `Vm` god-struct. Per the project's parallel-dev decisions: +//! Symbol/Iter/Profiling are full sinks (their logic is self-contained), while +//! `GcState` is field-classification only — GC `mark`/`sweep`/`alloc_object` +//! scan roots across *all* sub-structs and stay as `&mut Vm` methods. +//! +//! This file defines fields only; methods live with `Vm`. + +use std::cell::Cell; +use std::collections::HashMap; + +use rustc_hash::FxBuildHasher; + +use oxide_types::object::{JsObject, JsString}; +use oxide_types::value::JsValue; + +use crate::session_gc::SessionGc; +use crate::vm::ForInIter; + +/// Session-arena and garbage-collection bookkeeping. +pub(crate) struct GcState { + pub(crate) session_epoch: bumpalo::Bump, + pub(crate) session_gc: SessionGc, + pub(crate) epoch_object_ptrs: Vec<*mut JsObject>, + pub(crate) session_object_ptrs: Vec<*mut JsObject>, + pub(crate) session_string_ptrs: Vec<*mut JsString>, + pub(crate) session_bytes_allocated: usize, + /// Long-lived forwarding map reused by GC sweep and promote. + /// `mem::take`'d out for use, `clear()`'d and put back — capacity retained + /// across collections, so no per-event `HashMap` allocation. `FxBuildHasher` + /// because the keys are raw pointers (collision resistance is irrelevant). + pub(crate) forwarding: HashMap<*mut JsObject, *mut JsObject, FxBuildHasher>, +} + +/// `Symbol` interning state. +pub(crate) struct SymbolState { + pub(crate) symbol_counter: u32, + pub(crate) symbol_descriptions: Vec, + pub(crate) symbol_registry: HashMap, +} + +/// Live iterator state for `for-in` / `for-of`. +pub(crate) struct IterState { + pub(crate) for_in_iters: Vec<*mut ForInIter<'static>>, + pub(crate) for_of_iters: Vec, + pub(crate) last_for_of_result: JsValue, +} + +/// Inline-cache hit/miss and instruction counters. +pub(crate) struct ProfilingState { + pub(crate) ic_hits: Cell, + pub(crate) ic_misses: Cell, + pub(crate) instruction_count: u64, +} diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index 907382a1..46ea9361 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -6,6 +6,7 @@ use oxide_bytecode::module::Constant; use crate::bindings; use crate::vm::Vm; +use crate::vm_state::{GcState, IterState, ProfilingState, SymbolState}; use oxide_kernel::kernel::{KernelConfig, KernelCore, KernelSession}; use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_types::mem::{Epoch, P}; @@ -25,17 +26,9 @@ impl Vm { immutables_cache: Vec::new(), active_immutables: std::ptr::slice_from_raw_parts(std::ptr::null(), 0), frames: smallvec::SmallVec::new(), - for_in_iters: Vec::new(), kernel_core: core, session, - session_string_ptrs: Vec::new(), epoch: Epoch::new(), - session_epoch: bumpalo::Bump::new(), - session_gc: crate::session_gc::SessionGc::new(), - forwarding: std::collections::HashMap::with_hasher(rustc_hash::FxBuildHasher), - epoch_object_ptrs: Vec::new(), - session_object_ptrs: Vec::new(), - session_bytes_allocated: 0, object_prototype: obj_proto, math_rng_state: 0, sub_modules: Arc::new(Vec::new()), @@ -46,18 +39,34 @@ impl Vm { exception_value: None, pending_exception: None, pending_error_kind: None, - symbol_counter: 0, - symbol_descriptions: Vec::new(), - symbol_registry: std::collections::HashMap::new(), - for_of_iters: Vec::new(), - last_for_of_result: JsValue::undefined(), root_reg_limit: 0, active_reg_limit: 0, native_call_depth: 0, accessor_frame_target_reg: None, - ic_hits: std::cell::Cell::new(0), - ic_misses: std::cell::Cell::new(0), - instruction_count: 0, + gc_state: GcState { + session_epoch: bumpalo::Bump::new(), + session_gc: crate::session_gc::SessionGc::new(), + epoch_object_ptrs: Vec::new(), + session_object_ptrs: Vec::new(), + session_string_ptrs: Vec::new(), + session_bytes_allocated: 0, + forwarding: std::collections::HashMap::with_hasher(rustc_hash::FxBuildHasher), + }, + symbols: SymbolState { + symbol_counter: 0, + symbol_descriptions: Vec::new(), + symbol_registry: std::collections::HashMap::new(), + }, + iters: IterState { + for_in_iters: Vec::new(), + for_of_iters: Vec::new(), + last_for_of_result: JsValue::undefined(), + }, + profiling: ProfilingState { + ic_hits: std::cell::Cell::new(0), + ic_misses: std::cell::Cell::new(0), + instruction_count: 0, + }, } } @@ -72,17 +81,9 @@ impl Vm { immutables_cache: Vec::new(), active_immutables: std::ptr::slice_from_raw_parts(std::ptr::null(), 0), frames: smallvec::SmallVec::new(), - for_in_iters: Vec::new(), kernel_core: core, session, - session_string_ptrs: Vec::new(), epoch: Epoch::new(), - session_epoch: bumpalo::Bump::new(), - session_gc: crate::session_gc::SessionGc::new(), - forwarding: std::collections::HashMap::with_hasher(rustc_hash::FxBuildHasher), - epoch_object_ptrs: Vec::new(), - session_object_ptrs: Vec::new(), - session_bytes_allocated: 0, object_prototype: obj_proto, math_rng_state: 0, sub_modules: Arc::new(Vec::new()), @@ -93,18 +94,34 @@ impl Vm { exception_value: None, pending_exception: None, pending_error_kind: None, - symbol_counter: 0, - symbol_descriptions: Vec::new(), - symbol_registry: std::collections::HashMap::new(), - for_of_iters: Vec::new(), - last_for_of_result: JsValue::undefined(), root_reg_limit: 0, active_reg_limit: 0, native_call_depth: 0, accessor_frame_target_reg: None, - ic_hits: std::cell::Cell::new(0), - ic_misses: std::cell::Cell::new(0), - instruction_count: 0, + gc_state: GcState { + session_epoch: bumpalo::Bump::new(), + session_gc: crate::session_gc::SessionGc::new(), + epoch_object_ptrs: Vec::new(), + session_object_ptrs: Vec::new(), + session_string_ptrs: Vec::new(), + session_bytes_allocated: 0, + forwarding: std::collections::HashMap::with_hasher(rustc_hash::FxBuildHasher), + }, + symbols: SymbolState { + symbol_counter: 0, + symbol_descriptions: Vec::new(), + symbol_registry: std::collections::HashMap::new(), + }, + iters: IterState { + for_in_iters: Vec::new(), + for_of_iters: Vec::new(), + last_for_of_result: JsValue::undefined(), + }, + profiling: ProfilingState { + ic_hits: std::cell::Cell::new(0), + ic_misses: std::cell::Cell::new(0), + instruction_count: 0, + }, } } @@ -138,27 +155,28 @@ impl Vm { self.active_immutables = std::ptr::slice_from_raw_parts(std::ptr::null(), 0); self.free_epoch_object_heap_data(); self.epoch.reset(); - self.epoch_object_ptrs.clear(); - self.session_epoch.reset(); - self.session_object_ptrs.clear(); - self.session_bytes_allocated = 0; - self.session_gc = crate::session_gc::SessionGc::new(); + self.gc_state.epoch_object_ptrs.clear(); + self.gc_state.session_epoch.reset(); + self.gc_state.session_object_ptrs.clear(); + self.gc_state.session_bytes_allocated = 0; + self.gc_state.session_gc = crate::session_gc::SessionGc::new(); self.free_session_string_heap_data(); - self.symbol_counter = 0; - self.symbol_descriptions.clear(); - self.symbol_registry.clear(); + self.symbols.symbol_counter = 0; + self.symbols.symbol_descriptions.clear(); + self.symbols.symbol_registry.clear(); self.root_reg_limit = 0; self.active_reg_limit = 0; } fn free_epoch_object_heap_data(&mut self) { let mut freed = 0u64; - for ptr in self.epoch_object_ptrs.drain(..) { + for ptr in self.gc_state.epoch_object_ptrs.drain(..) { freed += crate::session_gc::SessionGc::drop_object_heap_data(ptr, false); } if freed > 0 { - self.session_gc.total_bytes_freed = self.session_gc.total_bytes_freed.saturating_add(freed); - self.session_gc.last_collection_bytes_freed = freed; + self.gc_state.session_gc.total_bytes_freed = + self.gc_state.session_gc.total_bytes_freed.saturating_add(freed); + self.gc_state.session_gc.last_collection_bytes_freed = freed; } } @@ -171,9 +189,9 @@ impl Vm { self.regs = [JsValue::undefined(); 256]; self.pc = 0; self.frames.clear(); - self.for_in_iters.clear(); - self.for_of_iters.clear(); - self.last_for_of_result = JsValue::undefined(); + self.iters.for_in_iters.clear(); + self.iters.for_of_iters.clear(); + self.iters.last_for_of_result = JsValue::undefined(); self.saved_bytecode_stack.clear(); self.saved_immutables_stack.clear(); self.save_stack.clear(); @@ -192,15 +210,15 @@ impl Vm { self.active_immutables = std::ptr::slice_from_raw_parts(std::ptr::null(), 0); self.free_epoch_object_heap_data(); self.epoch.reset(); - self.epoch_object_ptrs.clear(); + self.gc_state.epoch_object_ptrs.clear(); self.root_reg_limit = 0; self.active_reg_limit = 0; } pub fn new_string(&mut self, s: &str) -> JsValue { let ptr = Box::into_raw(Box::new(JsString::new(s.to_string()))); - self.session_string_ptrs.push(ptr); - self.session_bytes_allocated += std::mem::size_of::() + s.len(); + self.gc_state.session_string_ptrs.push(ptr); + self.gc_state.session_bytes_allocated += std::mem::size_of::() + s.len(); JsValue::string(ptr) } @@ -224,7 +242,7 @@ impl Vm { /// can reference them. The lighter `reset()` deliberately keeps them alive, /// mirroring session-object survival across evals. fn free_session_string_heap_data(&mut self) { - for ptr in self.session_string_ptrs.drain(..) { + for ptr in self.gc_state.session_string_ptrs.drain(..) { // SAFETY: each ptr came from Box::into_raw(Box::new(JsString)) in new_string // and is dropped exactly once here. unsafe { @@ -260,7 +278,7 @@ impl Vm { let prototype_obj = self .epoch .alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(object_proto_ptr))); - self.epoch_object_ptrs.push(prototype_obj); + self.gc_state.epoch_object_ptrs.push(prototype_obj); let prototype_val = JsValue::from_js_object(prototype_obj); let constructor_si = self.kernel_core.perm_interner().intern("constructor").0; @@ -431,7 +449,7 @@ mod tests { #[test] fn session_epoch_survives_reset() { let mut vm = Vm::new(); - let session_ptr = vm.session_epoch.alloc(123i32) as *mut i32; + let session_ptr = vm.gc_state.session_epoch.alloc(123i32) as *mut i32; vm.reset(); @@ -454,9 +472,9 @@ mod tests { fn session_epoch_reset_is_only_in_full_reset_state_clear() { let src = include_str!("vm_support.rs"); let production = src.split("#[cfg(test)]").next().expect("production source"); - assert_eq!(production.matches("self.session_epoch.reset()").count(), 1); + assert_eq!(production.matches("self.gc_state.session_epoch.reset()").count(), 1); assert!(production.contains("fn clear_full_reset_state(&mut self)")); - assert!(production.contains("self.session_epoch.reset();")); + assert!(production.contains("self.gc_state.session_epoch.reset();")); } #[test] From f7bcfd384627c7b10175a6022af560beed2f684f Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Thu, 25 Jun 2026 19:46:35 +0800 Subject: [PATCH 19/32] =?UTF-8?q?refactor:=20CompileCtx=20=E6=8B=86?= =?UTF-8?q?=E5=88=86=E4=B8=BA=20LabelCtx/ScopeCtx=EF=BC=8C=E5=AD=97?= =?UTF-8?q?=E9=9D=A2=E9=87=8F=E5=9F=9F=E6=94=B9=E7=94=A8=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=E8=A1=A8=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_compiler/src/compiler.rs | 130 +++++++++--------- .../src/counter/expr/assignment.rs | 6 +- .../oxide_compiler/src/counter/expr/chain.rs | 4 +- .../src/counter/expr/control.rs | 8 +- .../src/counter/expr/literal.rs | 11 ++ crates/oxide_compiler/src/counter/expr/mod.rs | 6 +- .../src/counter/helper/member.rs | 2 +- .../oxide_compiler/src/counter/stmt/basic.rs | 2 +- .../src/counter/stmt/control.rs | 4 +- .../src/counter/stmt/exception.rs | 6 +- .../src/counter/stmt/iteration.rs | 22 +-- .../oxide_compiler/src/counter/stmt/switch.rs | 4 +- crates/oxide_compiler/src/emit_ctx.rs | 38 +++++ .../src/emitter/expr/literal.rs | 14 ++ crates/oxide_compiler/src/emitter/mod.rs | 29 ++-- .../src/emitter/stmt/exception.rs | 6 +- crates/oxide_compiler/src/lib.rs | 1 + 17 files changed, 180 insertions(+), 113 deletions(-) create mode 100644 crates/oxide_compiler/src/emit_ctx.rs diff --git a/crates/oxide_compiler/src/compiler.rs b/crates/oxide_compiler/src/compiler.rs index 2dd2ec91..bdafe390 100644 --- a/crates/oxide_compiler/src/compiler.rs +++ b/crates/oxide_compiler/src/compiler.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use crate::emit_ctx::{LabelCtx, ScopeCtx}; use crate::symbol_table::{Binding, SymbolTable}; use oxide_bytecode::module::CompiledModule; use oxide_bytecode::opcode::{self, OpCode}; @@ -145,18 +146,9 @@ pub(crate) struct CompileCtx { next_reg: u8, pub(crate) max_regs: u8, reserved_reg_start: u8, - symbols: SymbolTable, - pub(crate) label_map: HashMap, - pub(crate) loop_stack: Vec<(Label, Label)>, - #[allow(dead_code)] - pub(crate) switch_stack: Vec