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] =?UTF-8?q?refactor:=20Vm=20=E7=8A=B6=E6=80=81=E5=88=86?= =?UTF-8?q?=E7=BB=84=E3=80=81dispatch=20=E6=8B=86=E5=88=86=E4=B8=BA?= =?UTF-8?q?=E5=88=86=E5=8F=91=E8=A1=A8=E3=80=81IC=20=E8=BE=85=E5=8A=A9?= =?UTF-8?q?=E5=87=BD=E6=95=B0=E7=8B=AC=E7=AB=8B=E6=88=90=E6=A8=A1=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]