perf: 不可变常量改 OnceLock 一次性缓存,删 Constant 死变体并移出 GC 根

This commit is contained in:
爱特每羊队
2026-06-25 14:32:49 +08:00
parent 1ef8349208
commit 06f769c4e0
9 changed files with 115 additions and 192 deletions
+1 -6
View File
@@ -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() {
-3
View File
@@ -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,
}
}
}
+3 -2
View File
@@ -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"))
+2 -16
View File
@@ -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)));
}
+50 -76
View File
@@ -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<opcode::Instr>,
pub(crate) constants: Vec<JsValue>,
pub(crate) active_immutables: *const [JsValue],
pub(crate) active_reg_limit: u8,
pub(crate) root_reg_limit: u8,
pub(crate) try_stack: Vec<TryHandler>,
@@ -139,7 +139,7 @@ pub(crate) struct InlineSyncState {
pub(crate) for_of_iters: Vec<JsValue>,
pub(crate) last_for_of_result: JsValue,
pub(crate) saved_bytecode_stack: Vec<Vec<opcode::Instr>>,
pub(crate) saved_constants_stack: Vec<Vec<JsValue>>,
pub(crate) saved_immutables_stack: Vec<*const [JsValue]>,
pub(crate) save_stack: Vec<JsValue>,
}
@@ -147,7 +147,13 @@ pub struct Vm {
pub(crate) regs: [JsValue; 256],
pub(crate) pc: usize,
pub(crate) bytecode: Vec<opcode::Instr>,
pub(crate) constants: Vec<JsValue>,
/// 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<OnceLock<Vec<JsValue>>>,
/// 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<KernelCore>,
@@ -167,9 +173,8 @@ pub struct Vm {
pub object_prototype: P<JsObject>,
pub math_rng_state: u64,
pub(crate) sub_modules: Arc<Vec<CompiledModule>>,
pub(crate) sub_module_constants: Vec<Vec<JsValue>>,
pub(crate) saved_bytecode_stack: Vec<Vec<opcode::Instr>>,
pub(crate) saved_constants_stack: Vec<Vec<JsValue>>,
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<Vec<JsValue>> 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<Vec<JsValue>> = &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();
+3 -2
View File
@@ -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() };
+9 -7
View File
@@ -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() {
+11 -11
View File
@@ -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<JsValue, String> {
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;
+36 -69
View File
@@ -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<JsValue, JsError> {
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<Vec<JsValue>, 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<JsValue> {
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");