diff --git a/crates/oxide_builtins/src/object.rs b/crates/oxide_builtins/src/object.rs index ebaf0df..692e51d 100644 --- a/crates/oxide_builtins/src/object.rs +++ b/crates/oxide_builtins/src/object.rs @@ -1,6 +1,6 @@ use oxide_kernel::shape_forge::{ShapeForge, EMPTY_SHAPE_ID}; use oxide_kernel::string_forge::PermInterner; -use oxide_types::object::{JsObject, PropAttributes}; +use oxide_types::object::{JsObject, PropAttributes, PropMetaEntry}; use oxide_types::private_key::is_private_name_key; use oxide_types::value::JsValue; @@ -54,6 +54,126 @@ pub(crate) fn walk_own_keys(vm: &H, obj: &JsObject) -> Vec<(u32, u32) keys } +/// 字符串键是否为数组下标("0"~"4294967294",无前导零)。 +fn array_index_of(vm: &H, key_si: u32) -> Option { + let key = vm.kernel_core().perm_interner().lookup(key_si)?; + if key.is_empty() || (key.len() > 1 && key.starts_with('0')) { + return None; + } + key.parse::().ok() +} + +/// 删除对象自身属性(字节码 delete 与 Reflect.deleteProperty 共用)。 +/// +/// 数组下标键在元素区(shape 链外),标记为 hole(值 undefined + hole meta), +/// length 不变;命名属性经 shape 链重建移除。属性不可配置时返回 false。 +/// +/// # 步骤 +/// 1. 数组下标元素:检查 configurable,`mark_hole_at` 标记为 hole +/// 2. 命名属性:walk_own_keys 定位槽位,不可配置返回 false +/// 3. 数组先保存元素区(值 + meta),重建命名属性后恢复元素区 +/// +/// # 边界与前提 +/// - 键不在对象自身(含原型链属性)返回 true +/// - 非 configurable 属性返回 false +/// - `key_si` 须已 intern +/// +/// # 副作用 +/// - 修改 obj 的 shape_id、属性表与 generation;数组元素区内容不变 +/// +/// # 注意事项 +/// - 数组元素存在性以 `prop_meta_at` 的 hole 标记判定,删除后重新写入元素 +/// 会自动清除 hole 标记恢复存在 +pub fn delete_own_property(vm: &mut H, obj: &mut JsObject, key_si: u32) -> bool { + // 数组下标元素在元素区,不参与 shape 链,单独删除(保持 length 不变)。 + if obj.is_array() { + if let Some(index) = array_index_of(vm, key_si) { + if index < obj.array_prop_count { + let meta = obj.prop_meta_at(index); + // 已是 hole 视为不存在;非 configurable 不可删。 + if meta.is_some_and(|m| m.is_hole()) { + return true; + } + if meta.is_some_and(|m| !m.attributes.configurable()) { + return false; + } + obj.mark_hole_at(index); + obj.bump_generation(); + return true; + } + return true; + } + } + + let keys = walk_own_keys(vm, obj); + let Some((_, delete_pos)) = keys.iter().find(|(si, _)| *si == key_si).copied() else { + return true; + }; + // delete_pos 是 shape 槽位;数组对象存储索引 = 元素数 + 槽位。 + let delete_store = if obj.is_array() { obj.array_prop_count + delete_pos } else { delete_pos }; + if obj + .prop_meta_at(delete_store) + .map(|meta| !meta.attributes.configurable()) + .unwrap_or(false) + { + return false; + } + + // 数组重建前保存元素区(值 + meta),重建后恢复到命名属性之前。 + let saved_elements: Vec = if obj.is_array() { + (0..obj.array_prop_count).map(|i| obj.get_prop_at(i)).collect() + } else { + Vec::new() + }; + let saved_element_meta: Option>> = if obj.is_array() { + obj.prop_meta_vec().map(|meta| meta[..obj.array_prop_count as usize].to_vec()) + } else { + None + }; + + let retained: Vec<(u32, JsValue, Option)> = keys + .into_iter() + .filter(|(_, pos)| *pos != delete_pos) + .map(|(si, pos)| { + let store = if obj.is_array() { obj.array_prop_count + pos } else { pos }; + (si, obj.get_prop_at(store), obj.prop_meta_at(store)) + }) + .collect(); + + // 重建 shape 链与属性表(数组先清空,含元素区,随后恢复)。 + obj.set_shape_id(EMPTY_SHAPE_ID); + obj.clear_props(); + for (si, value, meta) in retained { + let shape = vm.kernel_core().shape_forge().make_shape(obj.shape_id(), si); + obj.set_shape_id(shape); + let pos = obj.push_prop(value); + if let Some(meta) = meta { + if meta.is_accessor { + obj.set_accessor_meta(pos, meta.get, meta.set, meta.attributes); + } else { + obj.set_data_meta(pos, meta.attributes); + } + } + } + if obj.is_array() { + let n = saved_elements.len(); + obj.set_prop_count(n); + for (i, val) in saved_elements.into_iter().enumerate() { + obj.set_prop_at(i, val); + } + if let Some(saved_meta) = saved_element_meta { + let meta = obj.ensure_prop_meta(); + for (i, entry) in saved_meta.into_iter().enumerate() { + if let Some(entry) = entry { + meta[i] = Some(entry); + } + } + } + } + obj.bump_generation(); + true +} + /// JS `Object()` 构造逻辑:创建空对象(prototype 为 null,由 VM 补装内置原型)。 pub fn object_constructor(vm: &mut H, args: &[u8]) -> NativeResult { let val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; @@ -303,7 +423,8 @@ pub fn object_define_property(vm: &mut H, args: &[u8]) -> NativeResul "accessor descriptor get/set must be callable or undefined", )); } - if let Err(e) = vm.define_accessor_property(obj, si, get, set, PropAttributes::new(false, enumerable, configurable)) + if let Err(e) = + vm.define_accessor_property(obj, si, get, set, PropAttributes::new(false, enumerable, configurable)) { return NativeResult::Err(crate::error::create_type_error(vm, &e)); } @@ -320,7 +441,8 @@ pub fn object_define_property(vm: &mut H, args: &[u8]) -> NativeResul } else { writable_field.map(oxide_runtime_api::to_boolean).unwrap_or(false) }; - if let Err(e) = vm.define_data_property(obj, si, value, PropAttributes::new(writable, enumerable, configurable)) { + if let Err(e) = vm.define_data_property(obj, si, value, PropAttributes::new(writable, enumerable, configurable)) + { return NativeResult::Err(crate::error::create_type_error(vm, &e)); } } else { @@ -912,4 +1034,3 @@ pub fn object_values(vm: &mut H, args: &[u8]) -> NativeResult { } NativeResult::Ok(JsValue::from_js_object(arr)) } - diff --git a/crates/oxide_builtins/src/reflect.rs b/crates/oxide_builtins/src/reflect.rs index 72f8a88..234ac91 100644 --- a/crates/oxide_builtins/src/reflect.rs +++ b/crates/oxide_builtins/src/reflect.rs @@ -1,9 +1,9 @@ use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_types::mem::P; -use oxide_types::object::{JsObject, PropAttributes, PropMetaEntry}; +use oxide_types::object::{JsObject, PropAttributes}; use oxide_types::value::JsValue; -use crate::object::walk_own_keys; +use crate::object::{delete_own_property, walk_own_keys}; use oxide_runtime_api::{NativeResult, VmHost}; @@ -44,7 +44,8 @@ pub fn reflect_construct(vm: &mut H, args: &[u8]) -> NativeResult { } // 不可构造:箭头函数、native 方法(非构造器,OBJ_TYPE_CONSTRUCTOR 标记的除外)。 let nt = unsafe { &*new_target_ptr }; - if nt.is_arrow() || (nt.native_fn().is_some() && nt.type_tag != oxide_types::object::JsObject::OBJ_TYPE_CONSTRUCTOR) { + if nt.is_arrow() || (nt.native_fn().is_some() && nt.type_tag != oxide_types::object::JsObject::OBJ_TYPE_CONSTRUCTOR) + { return type_error(vm, "Reflect.construct newTarget is not a constructor"); } @@ -288,40 +289,3 @@ fn make_string_array(vm: &mut H, parts: &[String]) -> JsValue { unsafe { &mut *arr }.set_prop_count(parts.len()); JsValue::from_js_object(arr) } - -fn delete_own_property(vm: &mut H, obj: &mut JsObject, key_si: u32) -> bool { - let keys = walk_own_keys(vm, obj); - let Some((_, delete_pos)) = keys.iter().find(|(si, _)| *si == key_si).copied() else { - return true; - }; - if obj - .prop_meta_at(delete_pos) - .map(|meta| !meta.attributes.configurable()) - .unwrap_or(false) - { - return false; - } - - let retained: Vec<(u32, JsValue, Option)> = keys - .into_iter() - .filter(|(_, pos)| *pos != delete_pos) - .map(|(si, pos)| (si, obj.get_prop_at(pos), obj.prop_meta_at(pos))) - .collect(); - - obj.set_shape_id(EMPTY_SHAPE_ID); - obj.set_prop_count(0usize); - for (si, value, meta) in retained { - let shape = vm.kernel_core().shape_forge().make_shape(obj.shape_id(), si); - obj.set_shape_id(shape); - let pos = obj.push_prop(value); - if let Some(meta) = meta { - if meta.is_accessor { - obj.set_accessor_meta(pos, meta.get, meta.set, meta.attributes); - } else { - obj.set_data_meta(pos, meta.attributes); - } - } - } - obj.bump_generation(); - true -} diff --git a/crates/oxide_builtins/tests/object_boundary_tests.rs b/crates/oxide_builtins/tests/object_boundary_tests.rs index c8f3a80..86a8a50 100644 --- a/crates/oxide_builtins/tests/object_boundary_tests.rs +++ b/crates/oxide_builtins/tests/object_boundary_tests.rs @@ -236,3 +236,42 @@ fn test_property_is_enumerable_own_property() { let (_vm, result) = eval("var o = {x: 1}; o.propertyIsEnumerable('x')").unwrap(); assert!(result.is_bool() && result.as_bool()); } + +fn eval_str(source: &str) -> String { + let (_vm, result) = eval(source).unwrap(); + assert!(result.is_string(), "expected string result, got {result:?}"); + unsafe { &*result.as_string_ptr() }.as_str().to_string() +} + +#[test] +fn delete_prop_removes_own_property() { + // 删除命名属性后 hasOwnProperty / keys / 读取一致反映删除。 + assert_eq!( + eval_str("var o={a:1,b:2}; delete o.a; JSON.stringify([o.hasOwnProperty('a'), Object.keys(o).join(',')])"), + "[false,\"b\"]" + ); + // 删除数组元素:length 不变,槽位变 hole。 + assert_eq!( + eval_str("var a=[1,2,3]; delete a[1]; JSON.stringify([a.length, a.hasOwnProperty('1'), a[1] === undefined])"), + "[3,false,true]" + ); + // 值为 undefined 的元素不是 hole,hasOwnProperty 仍为 true。 + assert_eq!( + eval_str("var a=[undefined,1]; JSON.stringify([a.hasOwnProperty('0'), 0 in a])"), + "[true,true]" + ); + // 删除数组命名属性:元素区保留。 + assert_eq!( + eval_str("var a=[1,2]; a.custom=9; delete a.custom; JSON.stringify([a.hasOwnProperty('custom'), a[0]])"), + "[false,1]" + ); + // 不可配置属性返回 false 且保留。 + assert_eq!( + eval_str("var o={}; Object.defineProperty(o,'x',{value:1,configurable:false}); JSON.stringify([delete o.x, o.hasOwnProperty('x')])"), + "[false,true]" + ); + // 原型链属性删除为 no-op true。 + assert_eq!(eval_str("var o={}; JSON.stringify(delete o.hasOwnProperty)"), "true"); + // 删除后重新赋值数组元素回到元素区。 + assert_eq!(eval_str("var a=[1,2,3]; delete a[1]; a[1]=9; JSON.stringify([a[1], a.length])"), "[9,3]"); +} diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index 33dbda6..f424d71 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -160,13 +160,15 @@ impl PropAttributes { /// 单个属性的元数据条目。 /// /// 数据属性仅用 `attributes`;访问器属性额外携带 getter / setter -/// 的 [`JsValue`] 与 `is_accessor = true` 标记。 +/// 的 [`JsValue`] 与 `is_accessor = true` 标记。`hole` 标记数组元素被删除 +/// 后保留的稀疏空洞(数组元素区存在性判定依据)。 #[derive(Clone, Copy, Debug, PartialEq)] pub struct PropMetaEntry { pub attributes: PropAttributes, pub get: JsValue, pub set: JsValue, pub is_accessor: bool, + pub hole: bool, } impl PropMetaEntry { @@ -177,6 +179,7 @@ impl PropMetaEntry { get: JsValue::undefined(), set: JsValue::undefined(), is_accessor: false, + hole: false, } } @@ -187,8 +190,25 @@ impl PropMetaEntry { get, set, is_accessor: true, + hole: false, } } + + /// 构造数组元素删除后的 hole 标记条目。 + pub fn hole() -> Self { + Self { + attributes: PropAttributes::DEFAULT_DATA, + get: JsValue::undefined(), + set: JsValue::undefined(), + is_accessor: false, + hole: true, + } + } + + /// 是否数组元素 hole 标记(删除后保留的稀疏空洞)。 + pub fn is_hole(&self) -> bool { + self.hole + } } /// 可转换为 dense 属性下标的值。 @@ -772,6 +792,48 @@ impl JsObject { meta[pos] = Some(entry); } + /// 把数组元素槽标记为 hole(删除语义):值置 undefined 并写入 hole 标记, + /// `array_prop_count` 与元素区大小不变(length 保持不变)。 + pub fn mark_hole_at(&mut self, position: impl PropIndex) { + let pos = position.to_u32() as usize; + if pos >= self.prop_vec_len() { + self.set_prop_count(pos + 1); + } + self.set_prop_at(pos, JsValue::undefined()); + let meta = self.ensure_prop_meta(); + while meta.len() <= pos { + meta.push(None); + } + meta[pos] = Some(PropMetaEntry::hole()); + } + + /// 若指定下标是 hole 标记则清除(元素被重新写入时恢复为存在)。 + fn clear_hole_marker(&mut self, pos: usize) { + if let Some(meta) = self.prop_meta_vec_mut() { + if meta.get(pos).is_some_and(|entry| entry.is_some_and(|e| e.is_hole())) { + meta[pos] = None; + } + } + } + + /// 清空全部属性与数组元素区,`array_prop_count` 归零。 + /// + /// 供 shape 链重建(如 delete 重排属性表)使用:清空后以 `push_prop` / + /// `set_prop_count` 按新形状重填。不清除形状 ID,调用方自行处理。 + pub fn clear_props(&mut self) { + if !self.hash_props.is_null() { + // SAFETY: hash_props 在 ensure_hash_props/new_array 中由 Box> 创建。 + let vec = unsafe { &mut *(self.hash_props as *mut Vec) }; + vec.clear(); + } + if !self.prop_meta.is_null() { + // SAFETY: prop_meta 在 ensure_prop_meta 中由 Box>> 创建。 + let meta = unsafe { &mut *(self.prop_meta as *mut Vec>) }; + meta.clear(); + } + self.array_prop_count = 0; + } + /// 是否数组(header bit 29)。 pub fn is_array(&self) -> bool { (self.header >> 29) & 1 != 0 @@ -915,6 +977,7 @@ impl JsObject { } let vec = self.ensure_hash_props(); vec[pos] = val; + self.clear_hole_marker(pos); return; } { @@ -963,6 +1026,7 @@ impl JsObject { vec.push(JsValue::undefined()); } vec[idx] = val; + self.clear_hole_marker(idx); if let Some(meta) = self.prop_meta_vec_mut() { while meta.len() <= idx { meta.push(None); @@ -1367,4 +1431,3 @@ mod tests { assert!(obj.prop_meta_at(2).is_some()); } } - diff --git a/crates/oxide_vm/src/dispatch/property.rs b/crates/oxide_vm/src/dispatch/property.rs index 6fcb6d8..7cd9c3b 100644 --- a/crates/oxide_vm/src/dispatch/property.rs +++ b/crates/oxide_vm/src/dispatch/property.rs @@ -170,7 +170,8 @@ impl Vm { let obj = unsafe { &*obj_ptr }; let prop_name_si = self.property_key_si(self.regs[b]); let (cached_shape_id, cached_slot, cached_depth) = ic_helper::read_ic_entry(&self.bytecode, &mut self.pc); - if obj.has_prop_meta() { let val = self.ordinary_get_with_target(obj, prop_name_si, val, a as u8)?; + if obj.has_prop_meta() { + let val = self.ordinary_get_with_target(obj, prop_name_si, val, a as u8)?; if self.accessor_frame_target_reg.take().is_none() { self.regs[a] = val; } @@ -412,17 +413,13 @@ impl Vm { 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); + self.regs[rd] = JsValue::bool(true); + return Ok(false); }; let obj = unsafe { &mut *obj_ptr }; - if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { - let configurable = obj.prop_meta_at(pos).map(|m| m.attributes.configurable()).unwrap_or(true); - if !configurable { - return self.raise_type_error("cannot delete non-configurable property").map(|()| false); - } - obj.set_prop_shape(pos, JsValue::undefined()); - } - self.regs[rd] = JsValue::bool(true); + // 统一走共享删除逻辑(与 Reflect.deleteProperty 一致);不可配置返回 false。 + let deleted = oxide_builtins::object::delete_own_property(self, obj, prop_name_si); + self.regs[rd] = JsValue::bool(deleted); Ok(false) } @@ -430,18 +427,13 @@ impl Vm { vm_trace!("DELETE_PROP_DYNAMIC rd={}", rd); 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); + self.regs[rd] = JsValue::bool(true); + return Ok(false); }; let obj = unsafe { &mut *obj_ptr }; - if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { - let configurable = obj.prop_meta_at(pos).map(|m| m.attributes.configurable()).unwrap_or(true); - if !configurable { - return self.raise_type_error("cannot delete non-configurable property").map(|()| false); - } - obj.set_prop_shape(pos, JsValue::undefined()); - } - self.regs[rd] = JsValue::bool(true); + // 统一走共享删除逻辑(与 Reflect.deleteProperty 一致);不可配置返回 false。 + let deleted = oxide_builtins::object::delete_own_property(self, obj, prop_name_si); + self.regs[rd] = JsValue::bool(deleted); Ok(false) } } - diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index d1838a6..a068e25 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -663,7 +663,8 @@ impl Vm { } if obj.is_array() { if let Some(index) = self.array_index_from_property_key(prop_name_si) { - if index < obj.prop_vec_len() as u32 { + // 数组元素区:hole(删除标记)视为不存在。 + if index < obj.array_prop_count && !obj.prop_meta_at(index).is_some_and(|m| m.is_hole()) { return Some(obj.get_prop_at(index)); } } @@ -701,7 +702,8 @@ impl Vm { } if obj.is_array() { if let Some(index) = self.array_index_from_property_key(prop_name_si) { - if index < obj.prop_vec_len() as u32 { + // 数组元素区:hole(删除标记)视为不存在。 + if index < obj.array_prop_count && !obj.prop_meta_at(index).is_some_and(|m| m.is_hole()) { return Some(index); } } @@ -1840,4 +1842,3 @@ mod tests { assert_eq!(vm.reg(255), JsValue::int(13)); } } - diff --git a/crates/oxide_vm/src/vm_dispatch_misc.rs b/crates/oxide_vm/src/vm_dispatch_misc.rs index dbd5c2d..82d0fc6 100644 --- a/crates/oxide_vm/src/vm_dispatch_misc.rs +++ b/crates/oxide_vm/src/vm_dispatch_misc.rs @@ -37,9 +37,7 @@ impl Vm { } // native 方法(非构造器)不可 new。 if ctor_obj.native_fn().is_some() && ctor_obj.type_tag != oxide_types::object::JsObject::OBJ_TYPE_CONSTRUCTOR { - return self - .raise_type_error("object is not a constructor") - .map(|_| true); + return self.raise_type_error("object is not a constructor").map(|_| true); } let ext = self.bytecode[self.pc]; @@ -321,10 +319,14 @@ impl Vm { // 数组把整型下标元素存在 prop_vec,而 prop_vec 不属于 shape 链。 // 单独枚举它们(ES:数组下标是可枚举字符串键,按升序排在其它自有键之前)。 + // hole(删除标记)跳过——for-in 不枚举数组稀疏空洞。 if current.is_object() { let arr = unsafe { &*current.as_js_object_ptr() }; if arr.is_array() { - for i in 0..arr.prop_vec_len() { + for i in 0..arr.array_prop_count { + if arr.prop_meta_at(i).is_some_and(|m| m.is_hole()) { + continue; + } let is_enum = arr .prop_meta_at(i) .map(|m| m.attributes.enumerable()) diff --git a/crates/oxide_vm/src/vm_props.rs b/crates/oxide_vm/src/vm_props.rs index 3946fe1..23ba644 100644 --- a/crates/oxide_vm/src/vm_props.rs +++ b/crates/oxide_vm/src/vm_props.rs @@ -30,7 +30,8 @@ impl Vm { } if obj.is_array() { if let Some(index) = self.array_index_from_property_key(prop_name_si) { - if index < obj.prop_vec_len() as u32 { + // 数组元素区:hole(删除标记)视为不存在,落到原型链。 + if index < obj.array_prop_count && !obj.prop_meta_at(index).is_some_and(|m| m.is_hole()) { // 数组元素为访问器属性(defineProperty getter)时须触发 // getter,而非直接读数据槽。 if let Some(meta) = obj.prop_meta_at(index) { @@ -309,6 +310,13 @@ impl Vm { pub(crate) fn set_or_create_prop_value(&mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue) { vm_trace!("set_or_create_prop_value: shape_id={} prop_name_si={}", obj.shape_id(), prop_name_si); + // 数组下标键写入元素区(维护 array_prop_count),不进入 shape 链。 + if obj.is_array() { + if let Some(index) = self.array_index_from_property_key(prop_name_si) { + obj.set_prop_at(index, val); + return; + } + } if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { obj.set_prop_shape(pos, val); } else { @@ -328,7 +336,15 @@ impl Vm { if obj.is_array() { if let Some(index) = self.array_index_from_property_key(prop_name_si) { // 数组索引属性存元素区并维护 array_prop_count(元素数随索引增长)。 - return self.define_array_index_element(obj, index, val, attributes, false, JsValue::undefined(), JsValue::undefined()); + return self.define_array_index_element( + obj, + index, + val, + attributes, + false, + JsValue::undefined(), + JsValue::undefined(), + ); } } let pos = if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { @@ -411,14 +427,8 @@ impl Vm { /// 数组索引属性(`"0"`~`"4294967294"`)的 define 路径:存入元素区并维护 /// `array_prop_count`(length 随最高索引增长),meta 与元素槽对齐。 fn define_array_index_element( - &mut self, - obj: &mut JsObject, - index: u32, - val: JsValue, - attributes: PropAttributes, - is_accessor: bool, - get: JsValue, - set: JsValue, + &mut self, obj: &mut JsObject, index: u32, val: JsValue, attributes: PropAttributes, is_accessor: bool, + get: JsValue, set: JsValue, ) -> Result<(), String> { let pos = index as usize; if pos > oxide_types::object::MAX_DENSE_PROPS {