fix: delete 真正删除属性(shape 链重建 + 数组元素 hole 标记)

字节码 delete 原先只清值为 undefined,属性残留在 shape 链,hasOwnProperty/
keys/枚举仍可见。统一走共享 delete_own_property(与 Reflect.deleteProperty
一致):
- 命名属性经 shape 链重建移除,数组重建前保存并恢复元素区
- 数组下标元素标记为 hole(PropMetaEntry.hole),length 不变,
  存在性检查(resolve_property/get_own_property_slot/ordinary_get/for-in)
  判 hole,重赋值自动清除标记
- 不可配置属性统一返回 false(sloppy 语义,strict 抛错留待 emit 追踪严格模式)
- set_or_create_prop_value 数组下标键路由元素区,修复 delete 后重赋值误入
  shape 链

test262 Object +212 PASS/-102 FAIL,Array +149 PASS/-60 FAIL,无回归。
This commit is contained in:
2026-08-08 02:25:13 +08:00
parent 8f5446aa8a
commit 0b68baa802
8 changed files with 275 additions and 83 deletions
+125 -4
View File
@@ -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<H: VmHost>(vm: &H, obj: &JsObject) -> Vec<(u32, u32)
keys
}
/// 字符串键是否为数组下标("0"~"4294967294",无前导零)。
fn array_index_of<H: VmHost>(vm: &H, key_si: u32) -> Option<u32> {
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::<u32>().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<H: VmHost>(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<JsValue> = 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<Vec<Option<PropMetaEntry>>> = 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<PropMetaEntry>)> = 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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}
NativeResult::Ok(JsValue::from_js_object(arr))
}
+4 -40
View File
@@ -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<H: VmHost>(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<H: VmHost>(vm: &mut H, parts: &[String]) -> JsValue {
unsafe { &mut *arr }.set_prop_count(parts.len());
JsValue::from_js_object(arr)
}
fn delete_own_property<H: VmHost>(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<PropMetaEntry>)> = 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
}
@@ -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]");
}