From 94beeb350b7ea20a11ab192f9c47841a389517ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=88=B1=E7=89=B9=E6=AF=8F=E7=BE=8A=E9=98=9F?= Date: Tue, 16 Jun 2026 12:36:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20session=20arena=20?= =?UTF-8?q?=E4=B8=8E=E9=80=83=E9=80=B8=E6=99=8B=E5=8D=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_types/src/mem.rs | 49 +++++ crates/oxide_types/src/object.rs | 139 +++++++++++- crates/oxide_vm/src/builtins/array.rs | 9 +- crates/oxide_vm/src/dispatch/property.rs | 36 ++-- crates/oxide_vm/src/lib.rs | 1 + crates/oxide_vm/src/session_arena.rs | 213 +++++++++++++++++++ crates/oxide_vm/src/vm.rs | 2 + crates/oxide_vm/src/vm_props.rs | 7 + crates/oxide_vm/src/vm_support.rs | 22 ++ crates/oxide_vm/tests/session_arena_tests.rs | 89 ++++++++ 10 files changed, 544 insertions(+), 23 deletions(-) create mode 100644 crates/oxide_vm/src/session_arena.rs create mode 100644 crates/oxide_vm/tests/session_arena_tests.rs diff --git a/crates/oxide_types/src/mem.rs b/crates/oxide_types/src/mem.rs index ad144b4..468cbc6 100644 --- a/crates/oxide_types/src/mem.rs +++ b/crates/oxide_types/src/mem.rs @@ -119,6 +119,26 @@ impl Epoch { self.bump.alloc_with(f) } + /// Return true when `ptr` lies inside one of the currently allocated bump chunks. + /// + /// This only compares addresses and does not dereference `ptr`. + #[inline] + pub fn is_epoch_ptr(&self, ptr: *const u8) -> bool { + let addr = ptr as usize; + if addr == 0 { + return false; + } + // SAFETY: this helper performs no allocations while walking the raw chunk + // iterator, so bumpalo's chunk list cannot change during iteration. + unsafe { + self.bump.iter_allocated_chunks_raw().any(|(base, len)| { + let start = base as usize; + let end = start.saturating_add(len); + addr >= start && addr < end + }) + } + } + /// O(1) mass deallocation. All previous allocations become invalid. /// Increments epoch ID to invalidate stale pointers (debug_assert guard). pub fn reset(&mut self) { @@ -138,3 +158,32 @@ impl Default for Epoch { Self::new() } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::object::JsObject; + use crate::shape::EMPTY_SHAPE_ID; + use crate::value::JsValue; + + #[test] + fn is_epoch_ptr_returns_true_for_epoch_object() { + let epoch = Epoch::new(); + let obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); + let ptr = epoch.alloc(obj); + + assert!(epoch.is_epoch_ptr(ptr.cast::())); + } + + #[test] + fn is_epoch_ptr_returns_false_for_heap_and_stack_pointers() { + let epoch = Epoch::new(); + let heap = PersistentHeap::new(); + let persistent = heap.promote(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); + let stack_value = 7i32; + + assert!(!epoch.is_epoch_ptr(persistent.as_ptr().cast::())); + assert!(!epoch.is_epoch_ptr((&stack_value as *const i32).cast::())); + assert!(!epoch.is_epoch_ptr(std::ptr::null())); + } +} diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index 5d5fda3..629419d 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -179,7 +179,8 @@ impl PropIndex for i32 { /// [31] is_function /// native_arg_count: u8 (1 byte) /// type_tag: u8 — OBJ_TYPE_* constant identifying wrapper/exotic object kind (1 byte) -/// _pad: [u8; 2] +/// is_session_epoch: u8 (1 byte) +/// _pad: u8 /// hash_props: *mut u8 (8 bytes, points to Box>) /// prop_meta: *mut u8 (8 bytes, points to Box>>) /// proto: JsValue (8 bytes) @@ -194,7 +195,8 @@ pub struct JsObject { header: u32, native_arg_count: u8, pub type_tag: u8, - _pad: [u8; 2], + is_session_epoch: u8, + _pad: u8, hash_props: *mut u8, prop_meta: *mut u8, proto: JsValue, @@ -256,7 +258,8 @@ impl JsObject { header: (shape_id & 0x00FF_FFFF) | (1 << 30), native_arg_count: 0, type_tag: 0, - _pad: [0; 2], + is_session_epoch: 0, + _pad: 0, hash_props: std::ptr::null_mut(), prop_meta: std::ptr::null_mut(), proto, @@ -275,7 +278,8 @@ impl JsObject { header: (shape_id & 0x00FF_FFFF) | (1 << 30) | (1 << 29), native_arg_count: 0, type_tag: 0, - _pad: [0; 2], + is_session_epoch: 0, + _pad: 0, hash_props: std::ptr::null_mut(), prop_meta: std::ptr::null_mut(), proto, @@ -292,6 +296,77 @@ impl JsObject { obj } + #[inline] + pub fn is_session_epoch(&self) -> bool { + self.is_session_epoch != 0 + } + + #[inline] + pub fn set_session_epoch(&mut self, value: bool) { + self.is_session_epoch = value as u8; + } + + pub fn clone_for_session_epoch(&self) -> Self { + let hash_props = self + .hash_props_vec() + .map(|props| Box::into_raw(Box::new(props.clone())) as *mut u8) + .unwrap_or(std::ptr::null_mut()); + let prop_meta = self + .prop_meta_vec() + .map(|meta| Box::into_raw(Box::new(meta.clone())) as *mut u8) + .unwrap_or(std::ptr::null_mut()); + + Self { + header: self.header, + native_arg_count: self.native_arg_count, + type_tag: self.type_tag, + is_session_epoch: 1, + _pad: self._pad, + hash_props, + prop_meta, + proto: self.proto, + generation: self.generation, + _pad2: self._pad2, + native_fn: self.native_fn, + sub_module_index: self.sub_module_index, + _pad3: self._pad3, + captured_this: self.captured_this, + home_object: self.home_object, + } + } + + pub fn rewrite_object_values(&mut self, mut rewrite: F) + where + F: FnMut(JsValue) -> JsValue, + { + if let Some(props) = self.hash_props_vec_mut() { + for value in props { + if value.is_object() { + *value = rewrite(*value); + } + } + } + if let Some(meta) = self.prop_meta_vec_mut() { + for entry in meta.iter_mut().flatten() { + if entry.get.is_object() { + entry.get = rewrite(entry.get); + } + if entry.set.is_object() { + entry.set = rewrite(entry.set); + } + } + } + if self.proto.is_object() { + self.proto = rewrite(self.proto); + } + if self.captured_this.is_object() { + self.captured_this = rewrite(self.captured_this); + } + if self.home_object.is_object() { + self.home_object = rewrite(self.home_object); + } + } + pub fn shape_id(&self) -> ShapeId { self.header & 0x00FF_FFFF } @@ -476,6 +551,15 @@ impl JsObject { } } + fn hash_props_vec_mut(&mut self) -> Option<&mut Vec> { + if self.hash_props.is_null() { + None + } else { + // SAFETY: hash_props was set from Box> in ensure_hash_props/new_array. + unsafe { Some(&mut *(self.hash_props as *mut Vec)) } + } + } + /// Get property value at position index in the vec. /// Returns JsValue::undefined() if hash_props not allocated or position out of bounds. pub fn get_prop_at(&self, position: impl PropIndex) -> JsValue { @@ -680,11 +764,58 @@ mod tests { assert!(obj.is_extensible()); assert!(!obj.is_array()); assert!(!obj.is_function()); + assert!(!obj.is_session_epoch()); assert_eq!(obj.generation(), 1); assert!(obj.hash_props_vec().is_none()); assert!(!obj.has_prop_meta()); } + #[test] + fn session_epoch_marker_roundtrip() { + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); + assert!(!obj.is_session_epoch()); + obj.set_session_epoch(true); + assert!(obj.is_session_epoch()); + obj.set_session_epoch(false); + assert!(!obj.is_session_epoch()); + } + + #[test] + fn session_epoch_marker_keeps_object_size_bound() { + let sz = std::mem::size_of::(); + assert!(sz <= 256, "JsObject grew unexpectedly: {sz}B"); + } + + #[test] + fn clone_for_session_epoch_marks_clone_and_does_not_alias_hash_props() { + let mut source = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); + source.set_prop_at(0, JsValue::int(1)); + + let mut clone = source.clone_for_session_epoch(); + assert!(clone.is_session_epoch()); + clone.set_prop_at(0, JsValue::int(2)); + + assert_eq!(source.get_prop_at(0), JsValue::int(1)); + assert_eq!(clone.get_prop_at(0), JsValue::int(2)); + } + + #[test] + fn clone_for_session_epoch_does_not_alias_prop_meta() { + let mut source = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); + source.set_prop_at(0, JsValue::undefined()); + source.set_accessor_meta(0, JsValue::int(10), JsValue::int(11), PropAttributes::DEFAULT_DATA); + + let mut clone = source.clone_for_session_epoch(); + clone.set_accessor_meta(0, JsValue::int(20), JsValue::int(21), PropAttributes::DEFAULT_DATA); + + let source_meta = source.prop_meta_at(0).expect("source meta"); + let clone_meta = clone.prop_meta_at(0).expect("clone meta"); + assert_eq!(source_meta.get, JsValue::int(10)); + assert_eq!(source_meta.set, JsValue::int(11)); + assert_eq!(clone_meta.get, JsValue::int(20)); + assert_eq!(clone_meta.set, JsValue::int(21)); + } + #[test] fn shape_id_roundtrip() { let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); diff --git a/crates/oxide_vm/src/builtins/array.rs b/crates/oxide_vm/src/builtins/array.rs index d5f5eb7..57e261d 100644 --- a/crates/oxide_vm/src/builtins/array.rs +++ b/crates/oxide_vm/src/builtins/array.rs @@ -127,14 +127,13 @@ pub fn array_is_array(vm: &mut Vm, args: &[u8]) -> NativeResult { pub fn array_push(vm: &mut Vm, args: &[u8]) -> NativeResult { let arr_ptr = array_ptr!(vm, args); - let arr = unsafe { &mut *arr_ptr }; - let mut len = arr.prop_count(); + let mut len = unsafe { &*arr_ptr }.prop_count(); for &arg_reg in args.iter().skip(1) { - let val = vm.reg(arg_reg); - arr.set_prop_at(len, val); + let val = vm.promote_if_needed_for_write_ptr(arr_ptr, vm.reg(arg_reg)); + unsafe { &mut *arr_ptr }.set_prop_at(len, val); len += 1; } - arr.set_prop_count(len); + unsafe { &mut *arr_ptr }.set_prop_count(len); NativeResult::Ok(JsValue::int(len as i32)) } diff --git a/crates/oxide_vm/src/dispatch/property.rs b/crates/oxide_vm/src/dispatch/property.rs index f7198f6..7316240 100644 --- a/crates/oxide_vm/src/dispatch/property.rs +++ b/crates/oxide_vm/src/dispatch/property.rs @@ -136,24 +136,25 @@ impl Vm { let prop_name_si = self.property_key_si(self.regs[b]); if self.kernel_core.string_forge().lookup(prop_name_si).as_deref() == Some("__proto__") { - let obj = unsafe { &mut *obj_ptr }; - if self.is_object_prototype(obj_ptr) && !self.regs[a].is_null() { + let proto_value = self.promote_if_needed_for_write_ptr(obj_ptr, self.regs[a]); + if self.is_object_prototype(obj_ptr) && !proto_value.is_null() { self.raise_type_error("Object.prototype.__proto__ is immutable")?; self.pc += 3; return Ok(()); } - obj.set_proto(self.regs[a]).map_err(|e| e.to_string())?; + let obj = unsafe { &mut *obj_ptr }; + obj.set_proto(proto_value).map_err(|e| e.to_string())?; self.pc += 3; return Ok(()); } - let obj = unsafe { &mut *obj_ptr }; let ext0 = self.bytecode[self.pc]; let ext1 = self.bytecode[self.pc + 1]; let _ext2 = self.bytecode[self.pc + 2]; self.pc += 3; - let value = self.regs[a]; + let value = self.promote_if_needed_for_write_ptr(obj_ptr, self.regs[a]); let receiver = self.regs[rd]; + let obj = unsafe { &mut *obj_ptr }; if obj.has_prop_meta() { self.ordinary_set_dispatch(obj, prop_name_si, value, receiver)?; return Ok(()); @@ -209,17 +210,20 @@ impl Vm { let Some(obj_ptr) = self.checked_object_ptr(self.regs[rd], "SET_PROP on non-object")? else { return Ok(()); }; - let obj = unsafe { &mut *obj_ptr }; let prop_name_si = self.property_key_si(self.regs[b]); if self.kernel_core.string_forge().lookup(prop_name_si).as_deref() == Some("__proto__") { - if self.is_object_prototype(obj_ptr) && !self.regs[a].is_null() { + let proto_value = self.promote_if_needed_for_write_ptr(obj_ptr, self.regs[a]); + if self.is_object_prototype(obj_ptr) && !proto_value.is_null() { self.raise_type_error("Object.prototype.__proto__ is immutable")?; return Ok(()); } - obj.set_proto(self.regs[a]).map_err(|e| e.to_string())?; + let obj = unsafe { &mut *obj_ptr }; + obj.set_proto(proto_value).map_err(|e| e.to_string())?; return Ok(()); } - self.ordinary_set_dispatch(obj, prop_name_si, self.regs[a], self.regs[rd])?; + let value = self.promote_if_needed_for_write_ptr(obj_ptr, self.regs[a]); + let obj = unsafe { &mut *obj_ptr }; + self.ordinary_set_dispatch(obj, prop_name_si, value, self.regs[rd])?; Ok(()) } @@ -240,17 +244,20 @@ impl Vm { let Some(obj_ptr) = self.checked_object_ptr(self.regs[rd], "SET_PROP_DYNAMIC on non-object")? else { return Ok(()); }; - let obj = unsafe { &mut *obj_ptr }; let prop_name_si = self.property_key_si(self.regs[a]); if self.kernel_core.string_forge().lookup(prop_name_si).as_deref() == Some("__proto__") { - if self.is_object_prototype(obj_ptr) && !self.regs[b].is_null() { + let proto_value = self.promote_if_needed_for_write_ptr(obj_ptr, self.regs[b]); + if self.is_object_prototype(obj_ptr) && !proto_value.is_null() { self.raise_type_error("Object.prototype.__proto__ is immutable")?; return Ok(()); } - obj.set_proto(self.regs[b]).map_err(|e| e.to_string())?; + let obj = unsafe { &mut *obj_ptr }; + obj.set_proto(proto_value).map_err(|e| e.to_string())?; return Ok(()); } - self.ordinary_set_dispatch(obj, prop_name_si, self.regs[b], self.regs[rd])?; + let value = self.promote_if_needed_for_write_ptr(obj_ptr, self.regs[b]); + let obj = unsafe { &mut *obj_ptr }; + self.ordinary_set_dispatch(obj, prop_name_si, value, self.regs[rd])?; Ok(()) } @@ -265,8 +272,9 @@ impl Vm { } else { 0 }; + let value = self.promote_if_needed_for_write_ptr(obj_ptr, self.regs[b]); let obj = unsafe { &mut *obj_ptr }; - obj.set_prop_at(idx, self.regs[b]); + obj.set_prop_at(idx, value); Ok(()) } } diff --git a/crates/oxide_vm/src/lib.rs b/crates/oxide_vm/src/lib.rs index 48ff300..4827f5e 100644 --- a/crates/oxide_vm/src/lib.rs +++ b/crates/oxide_vm/src/lib.rs @@ -5,6 +5,7 @@ pub mod builtins; pub mod coercion; mod dispatch; pub mod native; +mod session_arena; pub mod vm; mod vm_dispatch_ctrl; mod vm_dispatch_misc; diff --git a/crates/oxide_vm/src/session_arena.rs b/crates/oxide_vm/src/session_arena.rs new file mode 100644 index 0000000..8c43401 --- /dev/null +++ b/crates/oxide_vm/src/session_arena.rs @@ -0,0 +1,213 @@ +use std::collections::HashMap; + +use oxide_types::object::JsObject; +use oxide_types::value::JsValue; + +use crate::vm::Vm; + +impl Vm { + pub(crate) fn is_session_escape_root_ptr(&self, target_ptr: *mut JsObject) -> bool { + if target_ptr.is_null() { + return false; + } + let global_ptr = self.session.global_object().as_ptr() as *mut JsObject; + std::ptr::eq(target_ptr, global_ptr) || unsafe { (&*target_ptr).is_session_epoch() } + } + + pub(crate) fn promote_object(&mut self, src: *mut JsObject) -> *mut JsObject { + let mut forwarding = HashMap::new(); + self.promote_object_inner(src, &mut forwarding) + } + + pub(crate) fn promote_object_inner( + &mut self, src: *mut JsObject, forwarding: &mut HashMap<*mut JsObject, *mut JsObject>, + ) -> *mut JsObject { + if src.is_null() { + return src; + } + let src_ref = unsafe { &*src }; + if src_ref.is_session_epoch() || !self.epoch.is_epoch_ptr(src.cast::()) { + return src; + } + if let Some(dst) = forwarding.get(&src).copied() { + return dst; + } + + let clone = src_ref.clone_for_session_epoch(); + let dst = self.session_epoch.alloc(clone) as *mut JsObject; + forwarding.insert(src, dst); + + let dst_ref = unsafe { &mut *dst }; + dst_ref.rewrite_object_values(|value| self.promote_value_if_epoch_object(value, forwarding)); + dst + } + + pub(crate) fn promote_value_if_epoch_object( + &mut self, value: JsValue, forwarding: &mut HashMap<*mut JsObject, *mut JsObject>, + ) -> JsValue { + if !value.is_object() { + return value; + } + let ptr = value.as_js_object_ptr(); + if ptr.is_null() || !self.epoch.is_epoch_ptr(ptr.cast::()) { + return value; + } + JsValue::from_js_object(self.promote_object_inner(ptr, forwarding)) + } + + pub(crate) fn promote_if_needed_for_write_ptr(&mut self, target_ptr: *mut JsObject, value: JsValue) -> JsValue { + if !value.is_object() || !self.is_session_escape_root_ptr(target_ptr) { + return value; + } + let value_ptr = value.as_js_object_ptr(); + if value_ptr.is_null() || !self.epoch.is_epoch_ptr(value_ptr.cast::()) { + return value; + } + JsValue::from_js_object(self.promote_object(value_ptr)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; + use oxide_types::object::{PropAttributes, PropMetaEntry}; + + fn plain_object(vm: &mut Vm) -> *mut JsObject { + let proto = vm.session.builtin_world().object_proto.as_ptr() as *mut JsObject; + vm.epoch + .alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(proto))) + } + + fn is_epoch_object(vm: &Vm, value: JsValue) -> bool { + value.is_object() && vm.epoch.is_epoch_ptr(value.as_js_object_ptr().cast::()) + } + + #[test] + fn session_arena_promotion_preserves_cycle_identity() { + let mut vm = Vm::new(); + let root = plain_object(&mut vm); + unsafe { + (*root).set_prop_at(0, JsValue::from_js_object(root)); + } + + let promoted = vm.promote_object(root); + let promoted_obj = unsafe { &*promoted }; + + assert!(promoted_obj.is_session_epoch()); + assert!(std::ptr::eq(promoted_obj.get_prop_at(0).as_js_object_ptr(), promoted)); + } + + #[test] + fn session_arena_promotion_preserves_duplicate_child_reference() { + let mut vm = Vm::new(); + let root = plain_object(&mut vm); + let child = plain_object(&mut vm); + unsafe { + (*root).set_prop_at(0, JsValue::from_js_object(child)); + (*root).set_prop_at(1, JsValue::from_js_object(child)); + } + + let promoted = vm.promote_object(root); + let promoted_obj = unsafe { &*promoted }; + let left = promoted_obj.get_prop_at(0).as_js_object_ptr(); + let right = promoted_obj.get_prop_at(1).as_js_object_ptr(); + + assert!(std::ptr::eq(left, right)); + assert!(!std::ptr::eq(left, child)); + assert!(unsafe { (&*left).is_session_epoch() }); + } + + #[test] + fn session_arena_captured_and_home_object_links_promote() { + let mut vm = Vm::new(); + let root = plain_object(&mut vm); + let proto = plain_object(&mut vm); + let captured = plain_object(&mut vm); + let home = plain_object(&mut vm); + let getter = plain_object(&mut vm); + let setter = plain_object(&mut vm); + unsafe { + (*root).set_proto(JsValue::from_js_object(proto)).expect("proto"); + (*root).set_captured_this(JsValue::from_js_object(captured)); + (*root).set_home_object(JsValue::from_js_object(home)); + (*root).set_prop_at(0, JsValue::undefined()); + (*root).set_accessor_meta( + 0, + JsValue::from_js_object(getter), + JsValue::from_js_object(setter), + PropAttributes::DEFAULT_DATA, + ); + } + + let promoted = vm.promote_object(root); + let promoted_obj = unsafe { &*promoted }; + let meta = promoted_obj.prop_meta_at(0).expect("meta"); + + assert!(!is_epoch_object(&vm, promoted_obj.proto())); + assert!(!is_epoch_object(&vm, promoted_obj.captured_this())); + assert!(!is_epoch_object(&vm, promoted_obj.home_object())); + assert!(!is_epoch_object(&vm, meta.get)); + assert!(!is_epoch_object(&vm, meta.set)); + } + + #[test] + fn session_arena_barrier_promotes_global_root_write() { + let mut vm = Vm::new(); + let value = JsValue::from_js_object(plain_object(&mut vm)); + let global_ptr = vm.session.global_object().as_ptr() as *mut JsObject; + + let promoted = vm.promote_if_needed_for_write_ptr(global_ptr, value); + + assert!(promoted.is_object()); + assert!(!is_epoch_object(&vm, promoted)); + assert!(unsafe { (&*promoted.as_js_object_ptr()).is_session_epoch() }); + } + + #[test] + fn session_arena_barrier_promotes_already_session_target_write() { + let mut vm = Vm::new(); + let target_epoch = plain_object(&mut vm); + let target = vm.promote_object(target_epoch); + let value = JsValue::from_js_object(plain_object(&mut vm)); + + let promoted = vm.promote_if_needed_for_write_ptr(target, value); + + assert!(!is_epoch_object(&vm, promoted)); + assert!(unsafe { (&*promoted.as_js_object_ptr()).is_session_epoch() }); + } + + #[test] + fn session_arena_barrier_leaves_non_escape_target_write_unchanged() { + let mut vm = Vm::new(); + let target = plain_object(&mut vm); + let value = JsValue::from_js_object(plain_object(&mut vm)); + + let unchanged = vm.promote_if_needed_for_write_ptr(target, value); + + assert_eq!(unchanged, value); + } + + #[test] + fn session_arena_promotion_rewrites_accessor_metadata_values() { + let mut vm = Vm::new(); + let root = plain_object(&mut vm); + let getter = plain_object(&mut vm); + let setter = plain_object(&mut vm); + unsafe { + (*root).set_prop_at(0, JsValue::undefined()); + (*root).set_accessor_meta( + 0, + JsValue::from_js_object(getter), + JsValue::from_js_object(setter), + PropAttributes::DEFAULT_DATA, + ); + } + + let promoted = vm.promote_object(root); + let meta: PropMetaEntry = unsafe { &*promoted }.prop_meta_at(0).expect("meta"); + + assert!(!is_epoch_object(&vm, meta.get)); + assert!(!is_epoch_object(&vm, meta.set)); + } +} diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 0e5cd05..8d87874 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -142,6 +142,7 @@ pub struct Vm { pub(crate) session: KernelSession, pub(crate) interned_strings: Vec, pub epoch: Epoch, + pub(crate) session_epoch: bumpalo::Bump, pub object_prototype: P, pub math_rng_state: u64, pub(crate) sub_modules: Vec, @@ -835,6 +836,7 @@ impl Vm { 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"); diff --git a/crates/oxide_vm/src/vm_props.rs b/crates/oxide_vm/src/vm_props.rs index 34c0569..a404cc6 100644 --- a/crates/oxide_vm/src/vm_props.rs +++ b/crates/oxide_vm/src/vm_props.rs @@ -114,6 +114,7 @@ impl Vm { fn ordinary_set_inner( &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, use_frame_push: bool, ) -> Result<(), String> { + let val = self.promote_if_needed_for_write_ptr(obj as *mut JsObject, val); if let Some(pos) = self.get_own_property_slot(obj, prop_name_si) { if let Some(meta) = obj.prop_meta_at(pos) { if meta.is_accessor { @@ -216,6 +217,7 @@ impl Vm { pub(crate) fn set_member_prop( &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, ) -> Result<(), String> { + let val = self.promote_if_needed_for_write_ptr(obj as *mut JsObject, val); if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { if obj.has_prop_meta() { self.ordinary_set(obj, prop_name_si, val, receiver)?; @@ -230,6 +232,7 @@ impl Vm { } pub(crate) fn set_or_create_prop_value(&mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue) { + let val = self.promote_if_needed_for_write_ptr(obj as *mut JsObject, val); if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { obj.set_prop_at(pos, val); } else { @@ -243,6 +246,7 @@ impl Vm { pub(crate) fn define_data_property( &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, attributes: PropAttributes, ) -> Result<(), String> { + let val = self.promote_if_needed_for_write_ptr(obj as *mut JsObject, val); let pos = if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { pos } else { @@ -277,6 +281,9 @@ impl Vm { pub(crate) fn define_accessor_property( &mut self, obj: &mut JsObject, prop_name_si: u32, get: JsValue, set: JsValue, attributes: PropAttributes, ) -> Result<(), String> { + let target_ptr = obj as *mut JsObject; + let get = self.promote_if_needed_for_write_ptr(target_ptr, get); + let set = self.promote_if_needed_for_write_ptr(target_ptr, set); let pos = if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { pos } else { diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index e31efc5..2288d3d 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -30,6 +30,7 @@ impl Vm { session, interned_strings: Vec::new(), epoch: Epoch::new(), + session_epoch: bumpalo::Bump::new(), object_prototype: obj_proto, math_rng_state: 0, sub_modules: Vec::new(), @@ -67,6 +68,7 @@ impl Vm { session, interned_strings: Vec::new(), epoch: Epoch::new(), + session_epoch: bumpalo::Bump::new(), object_prototype: obj_proto, math_rng_state: 0, sub_modules: Vec::new(), @@ -117,6 +119,7 @@ impl Vm { self.bytecode.clear(); self.constants.clear(); self.epoch.reset(); + self.session_epoch.reset(); self.interned_strings.clear(); self.symbol_counter = 0; self.symbol_descriptions.clear(); @@ -404,6 +407,25 @@ mod tests { assert!(!vm.session.is_dirty_since_snapshot()); } + #[test] + fn session_epoch_survives_reset() { + let mut vm = Vm::new(); + let session_ptr = vm.session_epoch.alloc(123i32) as *mut i32; + + vm.reset(); + + assert!(unsafe { *session_ptr } == 123); + } + + #[test] + 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!(production.contains("fn clear_full_reset_state(&mut self)")); + assert!(production.contains("self.session_epoch.reset();")); + } + #[test] fn full_reset_refreshes_object_prototype_after_object_dirty() { let mut vm = Vm::new(); diff --git a/crates/oxide_vm/tests/session_arena_tests.rs b/crates/oxide_vm/tests/session_arena_tests.rs new file mode 100644 index 0000000..d4b77fd --- /dev/null +++ b/crates/oxide_vm/tests/session_arena_tests.rs @@ -0,0 +1,89 @@ +use oxide_compiler::compiler::Compiler; +use oxide_parser::Allocator; +use oxide_types::value::JsValue; +use oxide_vm::vm::Vm; + +fn run_source(vm: &mut Vm, source: &str) -> JsValue { + let allocator = Allocator::default(); + let program = oxide_parser::parse(&allocator, source).expect("parse failed"); + let module = Compiler::new().compile(&program).expect("compile failed"); + vm.run(&module).expect("vm run failed") +} + +fn global_prop(vm: &Vm, name: &str) -> JsValue { + let si = vm.kernel_core().string_forge().intern(name).0; + let global = vm.session().global_object(); + let pos = vm + .kernel_core() + .shape_forge() + .lookup_position(global.shape_id(), si) + .expect("global property slot"); + global.get_prop_at(pos) +} + +#[test] +fn test_basic_object_escape() { + let mut vm = Vm::new(); + run_source(&mut vm, "globalThis.x = {}; globalThis.x.y = 1"); + vm.reset(); + + let result = run_source(&mut vm, "globalThis.x.y"); + + assert_eq!(result, JsValue::int(1)); +} + +#[test] +fn test_array_escape() { + let mut vm = Vm::new(); + run_source(&mut vm, "globalThis.a = []; globalThis.a.push({ v: 2 })"); + vm.reset(); + + let result = run_source( + &mut vm, + "var i = 0; while (i < 1000) { var tmp = { v: i }; i = i + 1; } globalThis.a[0].v", + ); + + assert_eq!(result, JsValue::int(2)); +} + +#[test] +fn test_closure_captured_this_escape() { + let mut vm = Vm::new(); + run_source(&mut vm, "globalThis.marker = { v: 9 }; globalThis.f = () => this.marker.v"); + vm.reset(); + + let function = run_source(&mut vm, "globalThis.f"); + let result = run_source(&mut vm, "globalThis.marker.v"); + let function_obj = unsafe { &*function.as_js_object_ptr() }; + + assert!(function.is_object()); + assert!(function_obj.is_session_epoch()); + assert_eq!(global_prop(&vm, "f"), function); + assert_eq!(result, JsValue::int(9)); +} + +#[test] +fn test_transitive_escape() { + let mut vm = Vm::new(); + run_source(&mut vm, "globalThis.root = {}"); + vm.reset(); + run_source(&mut vm, "globalThis.root.child = { v: 3 }"); + vm.reset(); + + let result = run_source(&mut vm, "globalThis.root.child.v"); + + assert_eq!(result, JsValue::int(3)); +} + +#[test] +fn test_full_reset_clears_session_state() { + let mut vm = Vm::new(); + run_source(&mut vm, "globalThis.x = {}; globalThis.x.y = 1"); + vm.reset(); + assert_eq!(run_source(&mut vm, "globalThis.x.y"), JsValue::int(1)); + + vm.full_reset(); + + let result = run_source(&mut vm, "globalThis.x"); + assert!(result.is_undefined()); +}