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] =?UTF-8?q?perf:=20=E4=B8=8D=E5=8F=AF=E5=8F=98=E5=B8=B8?= =?UTF-8?q?=E9=87=8F=E6=94=B9=20OnceLock=20=E4=B8=80=E6=AC=A1=E6=80=A7?= =?UTF-8?q?=E7=BC=93=E5=AD=98,=E5=88=A0=20Constant=20=E6=AD=BB=E5=8F=98?= =?UTF-8?q?=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");