feat: 添加 session arena 与逃逸晋升

This commit is contained in:
2026-06-16 12:36:52 +08:00
parent bb34b74821
commit a9d8d0972c
10 changed files with 544 additions and 23 deletions
+49
View File
@@ -119,6 +119,26 @@ impl Epoch {
self.bump.alloc_with(f) 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. /// O(1) mass deallocation. All previous allocations become invalid.
/// Increments epoch ID to invalidate stale pointers (debug_assert guard). /// Increments epoch ID to invalidate stale pointers (debug_assert guard).
pub fn reset(&mut self) { pub fn reset(&mut self) {
@@ -138,3 +158,32 @@ impl Default for Epoch {
Self::new() 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::<u8>()));
}
#[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::<u8>()));
assert!(!epoch.is_epoch_ptr((&stack_value as *const i32).cast::<u8>()));
assert!(!epoch.is_epoch_ptr(std::ptr::null()));
}
}
+135 -4
View File
@@ -179,7 +179,8 @@ impl PropIndex for i32 {
/// [31] is_function /// [31] is_function
/// native_arg_count: u8 (1 byte) /// native_arg_count: u8 (1 byte)
/// type_tag: u8 — OBJ_TYPE_* constant identifying wrapper/exotic object kind (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<Vec<JsValue>>) /// hash_props: *mut u8 (8 bytes, points to Box<Vec<JsValue>>)
/// prop_meta: *mut u8 (8 bytes, points to Box<Vec<Option<PropMetaEntry>>>) /// prop_meta: *mut u8 (8 bytes, points to Box<Vec<Option<PropMetaEntry>>>)
/// proto: JsValue (8 bytes) /// proto: JsValue (8 bytes)
@@ -194,7 +195,8 @@ pub struct JsObject {
header: u32, header: u32,
native_arg_count: u8, native_arg_count: u8,
pub type_tag: u8, pub type_tag: u8,
_pad: [u8; 2], is_session_epoch: u8,
_pad: u8,
hash_props: *mut u8, hash_props: *mut u8,
prop_meta: *mut u8, prop_meta: *mut u8,
proto: JsValue, proto: JsValue,
@@ -256,7 +258,8 @@ impl JsObject {
header: (shape_id & 0x00FF_FFFF) | (1 << 30), header: (shape_id & 0x00FF_FFFF) | (1 << 30),
native_arg_count: 0, native_arg_count: 0,
type_tag: 0, type_tag: 0,
_pad: [0; 2], is_session_epoch: 0,
_pad: 0,
hash_props: std::ptr::null_mut(), hash_props: std::ptr::null_mut(),
prop_meta: std::ptr::null_mut(), prop_meta: std::ptr::null_mut(),
proto, proto,
@@ -275,7 +278,8 @@ impl JsObject {
header: (shape_id & 0x00FF_FFFF) | (1 << 30) | (1 << 29), header: (shape_id & 0x00FF_FFFF) | (1 << 30) | (1 << 29),
native_arg_count: 0, native_arg_count: 0,
type_tag: 0, type_tag: 0,
_pad: [0; 2], is_session_epoch: 0,
_pad: 0,
hash_props: std::ptr::null_mut(), hash_props: std::ptr::null_mut(),
prop_meta: std::ptr::null_mut(), prop_meta: std::ptr::null_mut(),
proto, proto,
@@ -292,6 +296,77 @@ impl JsObject {
obj 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<F>(&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 { pub fn shape_id(&self) -> ShapeId {
self.header & 0x00FF_FFFF self.header & 0x00FF_FFFF
} }
@@ -476,6 +551,15 @@ impl JsObject {
} }
} }
fn hash_props_vec_mut(&mut self) -> Option<&mut Vec<JsValue>> {
if self.hash_props.is_null() {
None
} else {
// SAFETY: hash_props was set from Box<Vec<JsValue>> in ensure_hash_props/new_array.
unsafe { Some(&mut *(self.hash_props as *mut Vec<JsValue>)) }
}
}
/// Get property value at position index in the vec. /// Get property value at position index in the vec.
/// Returns JsValue::undefined() if hash_props not allocated or position out of bounds. /// Returns JsValue::undefined() if hash_props not allocated or position out of bounds.
pub fn get_prop_at(&self, position: impl PropIndex) -> JsValue { pub fn get_prop_at(&self, position: impl PropIndex) -> JsValue {
@@ -680,11 +764,58 @@ mod tests {
assert!(obj.is_extensible()); assert!(obj.is_extensible());
assert!(!obj.is_array()); assert!(!obj.is_array());
assert!(!obj.is_function()); assert!(!obj.is_function());
assert!(!obj.is_session_epoch());
assert_eq!(obj.generation(), 1); assert_eq!(obj.generation(), 1);
assert!(obj.hash_props_vec().is_none()); assert!(obj.hash_props_vec().is_none());
assert!(!obj.has_prop_meta()); 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::<JsObject>();
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] #[test]
fn shape_id_roundtrip() { fn shape_id_roundtrip() {
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null());
+4 -5
View File
@@ -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 { pub fn array_push(vm: &mut Vm, args: &[u8]) -> NativeResult {
let arr_ptr = array_ptr!(vm, args); let arr_ptr = array_ptr!(vm, args);
let arr = unsafe { &mut *arr_ptr }; let mut len = unsafe { &*arr_ptr }.prop_count();
let mut len = arr.prop_count();
for &arg_reg in args.iter().skip(1) { for &arg_reg in args.iter().skip(1) {
let val = vm.reg(arg_reg); let val = vm.promote_if_needed_for_write_ptr(arr_ptr, vm.reg(arg_reg));
arr.set_prop_at(len, val); unsafe { &mut *arr_ptr }.set_prop_at(len, val);
len += 1; len += 1;
} }
arr.set_prop_count(len); unsafe { &mut *arr_ptr }.set_prop_count(len);
NativeResult::Ok(JsValue::int(len as i32)) NativeResult::Ok(JsValue::int(len as i32))
} }
+22 -14
View File
@@ -136,24 +136,25 @@ impl Vm {
let prop_name_si = self.property_key_si(self.regs[b]); let prop_name_si = self.property_key_si(self.regs[b]);
if self.kernel_core.string_forge().lookup(prop_name_si).as_deref() == Some("__proto__") { if self.kernel_core.string_forge().lookup(prop_name_si).as_deref() == Some("__proto__") {
let obj = unsafe { &mut *obj_ptr }; let proto_value = self.promote_if_needed_for_write_ptr(obj_ptr, self.regs[a]);
if self.is_object_prototype(obj_ptr) && !self.regs[a].is_null() { if self.is_object_prototype(obj_ptr) && !proto_value.is_null() {
self.raise_type_error("Object.prototype.__proto__ is immutable")?; self.raise_type_error("Object.prototype.__proto__ is immutable")?;
self.pc += 3; self.pc += 3;
return Ok(()); 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; self.pc += 3;
return Ok(()); return Ok(());
} }
let obj = unsafe { &mut *obj_ptr };
let ext0 = self.bytecode[self.pc]; let ext0 = self.bytecode[self.pc];
let ext1 = self.bytecode[self.pc + 1]; let ext1 = self.bytecode[self.pc + 1];
let _ext2 = self.bytecode[self.pc + 2]; let _ext2 = self.bytecode[self.pc + 2];
self.pc += 3; 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 receiver = self.regs[rd];
let obj = unsafe { &mut *obj_ptr };
if obj.has_prop_meta() { if obj.has_prop_meta() {
self.ordinary_set_dispatch(obj, prop_name_si, value, receiver)?; self.ordinary_set_dispatch(obj, prop_name_si, value, receiver)?;
return Ok(()); 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 { let Some(obj_ptr) = self.checked_object_ptr(self.regs[rd], "SET_PROP on non-object")? else {
return Ok(()); return Ok(());
}; };
let obj = unsafe { &mut *obj_ptr };
let prop_name_si = self.property_key_si(self.regs[b]); let prop_name_si = self.property_key_si(self.regs[b]);
if self.kernel_core.string_forge().lookup(prop_name_si).as_deref() == Some("__proto__") { if self.kernel_core.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")?; self.raise_type_error("Object.prototype.__proto__ is immutable")?;
return Ok(()); 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(()); 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(()) 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 { let Some(obj_ptr) = self.checked_object_ptr(self.regs[rd], "SET_PROP_DYNAMIC on non-object")? else {
return Ok(()); return Ok(());
}; };
let obj = unsafe { &mut *obj_ptr };
let prop_name_si = self.property_key_si(self.regs[a]); let prop_name_si = self.property_key_si(self.regs[a]);
if self.kernel_core.string_forge().lookup(prop_name_si).as_deref() == Some("__proto__") { if self.kernel_core.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")?; self.raise_type_error("Object.prototype.__proto__ is immutable")?;
return Ok(()); 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(()); 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(()) Ok(())
} }
@@ -265,8 +272,9 @@ impl Vm {
} else { } else {
0 0
}; };
let value = self.promote_if_needed_for_write_ptr(obj_ptr, self.regs[b]);
let obj = unsafe { &mut *obj_ptr }; let obj = unsafe { &mut *obj_ptr };
obj.set_prop_at(idx, self.regs[b]); obj.set_prop_at(idx, value);
Ok(()) Ok(())
} }
} }
+1
View File
@@ -5,6 +5,7 @@ pub mod builtins;
pub mod coercion; pub mod coercion;
mod dispatch; mod dispatch;
pub mod native; pub mod native;
mod session_arena;
pub mod vm; pub mod vm;
mod vm_dispatch_ctrl; mod vm_dispatch_ctrl;
mod vm_dispatch_misc; mod vm_dispatch_misc;
+213
View File
@@ -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::<u8>()) {
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::<u8>()) {
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::<u8>()) {
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::<u8>())
}
#[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));
}
}
+2
View File
@@ -142,6 +142,7 @@ pub struct Vm {
pub(crate) session: KernelSession, pub(crate) session: KernelSession,
pub(crate) interned_strings: Vec<u32>, pub(crate) interned_strings: Vec<u32>,
pub epoch: Epoch, pub epoch: Epoch,
pub(crate) session_epoch: bumpalo::Bump,
pub object_prototype: P<JsObject>, pub object_prototype: P<JsObject>,
pub math_rng_state: u64, pub math_rng_state: u64,
pub(crate) sub_modules: Vec<CompiledModule>, pub(crate) sub_modules: Vec<CompiledModule>,
@@ -835,6 +836,7 @@ impl Vm {
if !func_val.is_object() || !home_val.is_object() { if !func_val.is_object() || !home_val.is_object() {
throw_err!(self, TypeError, "SET_HOME_OBJECT expects function and 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() }; let func_obj = unsafe { &mut *func_val.as_js_object_ptr() };
if !func_obj.is_function() { if !func_obj.is_function() {
throw_err!(self, TypeError, "SET_HOME_OBJECT target is not a function"); throw_err!(self, TypeError, "SET_HOME_OBJECT target is not a function");
+7
View File
@@ -114,6 +114,7 @@ impl Vm {
fn ordinary_set_inner( fn ordinary_set_inner(
&mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, use_frame_push: bool, &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, use_frame_push: bool,
) -> Result<(), String> { ) -> 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(pos) = self.get_own_property_slot(obj, prop_name_si) {
if let Some(meta) = obj.prop_meta_at(pos) { if let Some(meta) = obj.prop_meta_at(pos) {
if meta.is_accessor { if meta.is_accessor {
@@ -216,6 +217,7 @@ impl Vm {
pub(crate) fn set_member_prop( pub(crate) fn set_member_prop(
&mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue,
) -> Result<(), String> { ) -> 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 let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) {
if obj.has_prop_meta() { if obj.has_prop_meta() {
self.ordinary_set(obj, prop_name_si, val, receiver)?; 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) { 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) { if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) {
obj.set_prop_at(pos, val); obj.set_prop_at(pos, val);
} else { } else {
@@ -243,6 +246,7 @@ impl Vm {
pub(crate) fn define_data_property( pub(crate) fn define_data_property(
&mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, attributes: PropAttributes, &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, attributes: PropAttributes,
) -> Result<(), String> { ) -> 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) { let pos = if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) {
pos pos
} else { } else {
@@ -277,6 +281,9 @@ impl Vm {
pub(crate) fn define_accessor_property( pub(crate) fn define_accessor_property(
&mut self, obj: &mut JsObject, prop_name_si: u32, get: JsValue, set: JsValue, attributes: PropAttributes, &mut self, obj: &mut JsObject, prop_name_si: u32, get: JsValue, set: JsValue, attributes: PropAttributes,
) -> Result<(), String> { ) -> 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) { let pos = if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) {
pos pos
} else { } else {
+22
View File
@@ -30,6 +30,7 @@ impl Vm {
session, session,
interned_strings: Vec::new(), interned_strings: Vec::new(),
epoch: Epoch::new(), epoch: Epoch::new(),
session_epoch: bumpalo::Bump::new(),
object_prototype: obj_proto, object_prototype: obj_proto,
math_rng_state: 0, math_rng_state: 0,
sub_modules: Vec::new(), sub_modules: Vec::new(),
@@ -67,6 +68,7 @@ impl Vm {
session, session,
interned_strings: Vec::new(), interned_strings: Vec::new(),
epoch: Epoch::new(), epoch: Epoch::new(),
session_epoch: bumpalo::Bump::new(),
object_prototype: obj_proto, object_prototype: obj_proto,
math_rng_state: 0, math_rng_state: 0,
sub_modules: Vec::new(), sub_modules: Vec::new(),
@@ -117,6 +119,7 @@ impl Vm {
self.bytecode.clear(); self.bytecode.clear();
self.constants.clear(); self.constants.clear();
self.epoch.reset(); self.epoch.reset();
self.session_epoch.reset();
self.interned_strings.clear(); self.interned_strings.clear();
self.symbol_counter = 0; self.symbol_counter = 0;
self.symbol_descriptions.clear(); self.symbol_descriptions.clear();
@@ -404,6 +407,25 @@ mod tests {
assert!(!vm.session.is_dirty_since_snapshot()); 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] #[test]
fn full_reset_refreshes_object_prototype_after_object_dirty() { fn full_reset_refreshes_object_prototype_after_object_dirty() {
let mut vm = Vm::new(); let mut vm = Vm::new();
@@ -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());
}