mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
feat: 添加 session arena 与逃逸晋升
This commit is contained in:
@@ -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::<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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Vec<JsValue>>)
|
||||
/// prop_meta: *mut u8 (8 bytes, points to Box<Vec<Option<PropMetaEntry>>>)
|
||||
/// 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<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 {
|
||||
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.
|
||||
/// 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::<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]
|
||||
fn shape_id_roundtrip() {
|
||||
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null());
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -142,6 +142,7 @@ pub struct Vm {
|
||||
pub(crate) session: KernelSession,
|
||||
pub(crate) interned_strings: Vec<u32>,
|
||||
pub epoch: Epoch,
|
||||
pub(crate) session_epoch: bumpalo::Bump,
|
||||
pub object_prototype: P<JsObject>,
|
||||
pub math_rng_state: u64,
|
||||
pub(crate) sub_modules: Vec<CompiledModule>,
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
Reference in New Issue
Block a user