From 79518ba8276e41c758928de71805e834701e9606 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Thu, 17 Sep 2026 19:17:16 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20strict=20=E5=88=A0=E9=99=A4=E9=9D=9E?= =?UTF-8?q?=E5=8F=AF=E9=85=8D=E7=BD=AE=E5=B1=9E=E6=80=A7=E6=8A=9B=E9=94=99?= =?UTF-8?q?=E5=B9=B6=E4=BF=AE=E6=AD=A3=E6=95=B0=E7=BB=84=E9=95=BF=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/object.rs | 56 +++++++--- crates/oxide_vm/src/dispatch/property.rs | 38 +++++-- crates/oxide_vm/src/vm/props.rs | 16 ++- .../tests/delete_prop_primitive_base_tests.rs | 20 ++++ .../oxide_vm/tests/property_delete_tests.rs | 100 ++++++++++++++++++ 5 files changed, 205 insertions(+), 25 deletions(-) diff --git a/crates/oxide_builtins/src/object.rs b/crates/oxide_builtins/src/object.rs index 5a2ed51d..6133921a 100644 --- a/crates/oxide_builtins/src/object.rs +++ b/crates/oxide_builtins/src/object.rs @@ -211,19 +211,34 @@ fn array_index_of(vm: &H, key_si: u32) -> Option { key.parse::().ok() } -/// 删除对象自身属性(字节码 delete 与 Reflect.deleteProperty 共用)。 +/// 自身属性删除结果三态:区分「属性缺失 / 已删除 / 不可配置」,让共享实现同时 +/// 服务 delete 运算符(不可配置在严格模式须抛 TypeError)与 +/// `Reflect.deleteProperty`(恒投影为布尔、不抛错)两个消费者。 +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum DeleteOutcome { + /// 属性不在对象自身(含原型链属性):删除按规范成功。 + Missing, + /// 属性存在且已删除。 + Deleted, + /// 属性存在但不可配置:删除失败。 + NonConfigurable, +} + +/// 删除对象自身属性并返回三态结果(字节码 delete 与 Reflect.deleteProperty 共用)。 /// /// 数组下标键在元素区(shape 链外),标记为 hole(值 undefined + hole meta), -/// length 不变;命名属性经 shape 链重建移除。属性不可配置时返回 false。 +/// length 不变;数组 length 虚拟属性与命名属性经 shape 链处理。仅对象自身属性 +/// 参与删除判定,原型链属性不影响结果。 /// /// # 步骤 -/// 1. 数组下标元素:检查 configurable,`mark_hole_at` 标记为 hole -/// 2. 命名属性:walk_own_keys 定位槽位,不可配置返回 false -/// 3. 数组先保存元素区(值 + meta),重建命名属性后恢复元素区 +/// 1. 数组 length 虚拟属性:不可配置,返回 `NonConfigurable` +/// 2. 数组下标元素:检查 configurable,`mark_hole_at` 标记为 hole +/// 3. 命名属性:walk_own_keys 定位槽位,不可配置返回 `NonConfigurable` +/// 4. 数组先保存元素区(值 + meta),重建命名属性后恢复元素区 /// /// # 边界与前提 -/// - 键不在对象自身(含原型链属性)返回 true -/// - 非 configurable 属性返回 false +/// - 键不在对象自身(含原型链属性)返回 `Missing` +/// - 非 configurable 属性返回 `NonConfigurable` /// - `key_si` 须已 intern /// /// # 副作用 @@ -232,7 +247,7 @@ fn array_index_of(vm: &H, key_si: u32) -> Option { /// # 注意事项 /// - 数组元素存在性以 `prop_meta_at` 的 hole 标记判定,删除后重新写入元素 /// 会自动清除 hole 标记恢复存在 -pub fn delete_own_property(vm: &mut H, obj: &mut JsObject, key_si: u32) -> bool { +pub fn delete_own_property_outcome(vm: &mut H, obj: &mut JsObject, key_si: u32) -> DeleteOutcome { // 数组下标元素在元素区,不参与 shape 链,单独删除(保持 length 不变)。 if obj.is_array() { if let Some(index) = array_index_of(vm, key_si) { @@ -240,16 +255,21 @@ pub fn delete_own_property(vm: &mut H, obj: &mut JsObject, key_si: u3 let meta = obj.prop_meta_at(index); // 已是 hole 视为不存在;非 configurable 不可删。 if meta.is_some_and(|m| m.is_hole()) { - return true; + return DeleteOutcome::Missing; } if meta.is_some_and(|m| !m.attributes.configurable()) { - return false; + return DeleteOutcome::NonConfigurable; } obj.mark_hole_at(index); obj.bump_generation(); - return true; + return DeleteOutcome::Deleted; } - return true; + return DeleteOutcome::Missing; + } + // 非下标键:length 是虚拟属性(无 shape 槽、不在元素区),但描述符声明 + // configurable:false,删除恒失败。 + if key_si == vm.kernel_core().perm_interner().intern("length").0 { + return DeleteOutcome::NonConfigurable; } } @@ -263,7 +283,7 @@ pub fn delete_own_property(vm: &mut H, obj: &mut JsObject, key_si: u3 // 属性缺失:delete 按规范返 true,镜像槽同步 undefined 保持 // "槽 = A 侧原始存储"不变式(与入口预载缺位语义幂等)。 vm.sync_global_builtin_mirror(obj, key_si, JsValue::undefined()); - return true; + return DeleteOutcome::Missing; }; // walk_own_keys 已返回绝对存储索引(数组含元素区偏移),直接使用。 if obj @@ -271,7 +291,7 @@ pub fn delete_own_property(vm: &mut H, obj: &mut JsObject, key_si: u3 .map(|meta| !meta.attributes.configurable()) .unwrap_or(false) { - return false; + return DeleteOutcome::NonConfigurable; } // 数组重建前保存元素区(值 + meta),重建后恢复到命名属性之前。 @@ -330,7 +350,13 @@ pub fn delete_own_property(vm: &mut H, obj: &mut JsObject, key_si: u3 // 删除成功:镜像槽同步 undefined(成员形删除 / Reflect.deleteProperty / // 0x9C 清槽同源收口,后两者对此幂等)。 vm.sync_global_builtin_mirror(obj, key_si, JsValue::undefined()); - true + DeleteOutcome::Deleted +} + +/// 删除对象自身属性并投影为布尔(`Reflect.deleteProperty` 语义):成功返回 true, +/// 属性不可配置返回 false,恒不抛错。 +pub fn delete_own_property(vm: &mut H, obj: &mut JsObject, key_si: u32) -> bool { + delete_own_property_outcome(vm, obj, key_si) != DeleteOutcome::NonConfigurable } /// JS `Object()` 构造逻辑:创建空对象(prototype 为 null,由 VM 补装内置原型)。 diff --git a/crates/oxide_vm/src/dispatch/property.rs b/crates/oxide_vm/src/dispatch/property.rs index 699e17ed..d050cd70 100644 --- a/crates/oxide_vm/src/dispatch/property.rs +++ b/crates/oxide_vm/src/dispatch/property.rs @@ -649,10 +649,10 @@ impl Vm { return Ok(false); }; let obj = unsafe { &mut *obj_ptr }; - // 统一走共享删除逻辑(与 Reflect.deleteProperty 一致);不可配置返回 false。 - let deleted = oxide_builtins::object::delete_own_property(self, obj, prop_name_si); - self.regs[rd] = JsValue::bool(deleted); - Ok(false) + // 统一走共享删除逻辑(与 Reflect.deleteProperty 同源);严格模式分派见 + // finish_member_delete。 + let outcome = oxide_builtins::object::delete_own_property_outcome(self, obj, prop_name_si); + self.finish_member_delete(rd, outcome) } pub(crate) fn dispatch_delete_prop_dynamic(&mut self, rd: usize, b: usize) -> Result { @@ -667,9 +667,33 @@ impl Vm { return Ok(false); }; let obj = unsafe { &mut *obj_ptr }; - // 统一走共享删除逻辑(与 Reflect.deleteProperty 一致);不可配置返回 false。 - let deleted = oxide_builtins::object::delete_own_property(self, obj, prop_name_si); - self.regs[rd] = JsValue::bool(deleted); + // 统一走共享删除逻辑(与 Reflect.deleteProperty 同源);严格模式分派见 + // finish_member_delete。 + let outcome = oxide_builtins::object::delete_own_property_outcome(self, obj, prop_name_si); + self.finish_member_delete(rd, outcome) + } + + /// 成员形 delete 的结果分派:不可配置属性在严格模式抛 TypeError(规范要求 + /// delete 运算符对 [[Delete]] 返回 false 的引用抛错),其余情形把「是否删除 + /// 成功」写入 `rd`。 + /// + /// # 步骤 + /// 1. 不可配置且当前上下文严格:抛 TypeError,返回 `Ok(true)` 令调用方重取 pc + /// 2. 否则按三态投影布尔结果写 `rd`(`Missing`/`Deleted` 为 true) + /// + /// # 边界与前提 + /// - 严格性取 `current_strict()`(覆盖帧栈顶 / inline 基线 / 顶层脚本三形态) + /// + /// # 副作用 + /// - 抛错路径经 `unwind` 改写 pc / 异常通道;成功路径写 `regs[rd]` + fn finish_member_delete( + &mut self, rd: usize, outcome: oxide_builtins::object::DeleteOutcome, + ) -> Result { + if outcome == oxide_builtins::object::DeleteOutcome::NonConfigurable && self.current_strict() { + self.raise_type_error("Cannot delete property")?; + return Ok(true); + } + self.regs[rd] = JsValue::bool(outcome != oxide_builtins::object::DeleteOutcome::NonConfigurable); Ok(false) } diff --git a/crates/oxide_vm/src/vm/props.rs b/crates/oxide_vm/src/vm/props.rs index 04ca1f89..8c7cb42a 100644 --- a/crates/oxide_vm/src/vm/props.rs +++ b/crates/oxide_vm/src/vm/props.rs @@ -117,19 +117,24 @@ impl Vm { /// true。 /// /// # 步骤 - /// 1. null/undefined 基抛 TypeError(调用方经 `?` 传播异常) + /// 1. null/undefined 基抛 TypeError 并早返回(异常已展开,不再写结果槽) /// 2. 字符串基按下标键区间判自身属性;非字符串基恒真 - /// 3. 布尔结果写 `rd` 槽(与对象基臂同槽位) + /// 3. 严格模式下删除失败(字符串下标/length 不可配置)抛 TypeError + /// 4. 布尔结果写 `rd` 槽(与对象基臂同槽位) /// /// # 边界与前提 /// - 调用点已证基非对象;对象基走既有对象臂,不进本函数 /// - 字符串长度口径按 UTF-16 码元(与字符串自身属性面一致);非规范数字串 /// 键(前导零等)与 symbol 键走"无自身属性"面 + /// + /// # 副作用 + /// - 抛错路径经 `unwind` 改写 pc / 异常通道;成功路径写 `regs[rd]` pub(crate) fn delete_prop_non_object_base( &mut self, base: JsValue, rd: usize, key_si: u32, ) -> Result { if base.is_null() || base.is_undefined() { - self.raise_error_kind("TypeError", "delete on non-object")?; + self.raise_type_error("delete on non-object")?; + return Ok(true); } let deleted = if base.is_string() { let len = unsafe { (*base.as_string_ptr()).units().len() }; @@ -141,6 +146,11 @@ impl Vm { } else { true }; + // 删除失败即命中不可配置自身属性,严格模式下 delete 运算符须抛 TypeError。 + if !deleted && self.current_strict() { + self.raise_type_error("Cannot delete property")?; + return Ok(true); + } self.regs[rd] = JsValue::bool(deleted); Ok(false) } diff --git a/crates/oxide_vm/tests/delete_prop_primitive_base_tests.rs b/crates/oxide_vm/tests/delete_prop_primitive_base_tests.rs index f9b817ad..3c167590 100644 --- a/crates/oxide_vm/tests/delete_prop_primitive_base_tests.rs +++ b/crates/oxide_vm/tests/delete_prop_primitive_base_tests.rs @@ -91,3 +91,23 @@ fn delete_undefined_base_throws_type_error() { fn delete_nan_base_keys_are_true() { truthy("delete NaN.x"); } + +// ── 严格模式:删除字符串不可配置下标属性抛 TypeError ── +#[test] +fn strict_delete_string_index_throws_type_error() { + truthy( + "\"use strict\"; \ + var threw = false; try { delete 'ab'[0]; } catch (e) { threw = e instanceof TypeError; } \ + threw", + ); +} + +// ── 严格模式:删除字符串 length 抛 TypeError ── +#[test] +fn strict_delete_string_length_throws_type_error() { + truthy( + "\"use strict\"; \ + var threw = false; try { delete 'abc'.length; } catch (e) { threw = e instanceof TypeError; } \ + threw", + ); +} diff --git a/crates/oxide_vm/tests/property_delete_tests.rs b/crates/oxide_vm/tests/property_delete_tests.rs index 1a19bdf0..b7ffb930 100644 --- a/crates/oxide_vm/tests/property_delete_tests.rs +++ b/crates/oxide_vm/tests/property_delete_tests.rs @@ -17,6 +17,11 @@ fn eval_many(lines: &[&str]) -> Result { eval(&source) } +fn truthy(source: &str) { + let result = eval(source).unwrap_or_else(|e| panic!("{source} -> {e}")); + assert!(result.is_bool() && result.as_bool(), "{source} -> 期望 true,得 {:?}", result); +} + // ── 删除可配置属性成功 ── #[test] fn delete_configurable_property_succeeds_and_returns_true() { @@ -93,3 +98,98 @@ fn delete_configurable_proto_property_on_child_returns_true() { r ); } + +// ── 严格模式:不可配置数据属性抛 TypeError 且属性保留 ── +#[test] +fn strict_delete_non_configurable_data_throws_type_error() { + truthy( + "\"use strict\"; var o = {}; \ + Object.defineProperty(o, 'x', {value: 1, configurable: false}); \ + var threw = false; try { delete o.x; } catch (e) { threw = e instanceof TypeError; } \ + threw && o.x === 1", + ); +} + +// ── 严格模式:不可配置访问器属性同样抛 TypeError 且 getter 保留 ── +#[test] +fn strict_delete_non_configurable_accessor_throws_type_error() { + truthy( + "\"use strict\"; var o = {}; \ + Object.defineProperty(o, 'x', {get: function () { return 1; }, configurable: false}); \ + var threw = false; try { delete o.x; } catch (e) { threw = e instanceof TypeError; } \ + threw && o.x === 1", + ); +} + +// ── 严格模式:删除全局不可配置内置属性(NaN)抛 TypeError ── +#[test] +fn strict_delete_global_nan_throws_type_error() { + truthy( + "\"use strict\"; var g = this; \ + var threw = false; try { delete g.NaN; } catch (e) { threw = e instanceof TypeError; } \ + threw && g.NaN !== undefined", + ); +} + +// ── 严格模式:删除 Math.LN2 抛 TypeError ── +#[test] +fn strict_delete_math_ln2_throws_type_error() { + truthy( + "\"use strict\"; \ + var threw = false; try { delete Math.LN2; } catch (e) { threw = e instanceof TypeError; } \ + threw && Math.LN2 !== undefined", + ); +} + +// ── 严格模式:可配置属性仍可删除 ── +#[test] +fn strict_delete_configurable_property_succeeds() { + truthy( + "\"use strict\"; var o = {}; \ + Object.defineProperty(o, 'x', {value: 1, configurable: true}); \ + delete o.x === true && o.x === undefined", + ); +} + +// ── 严格模式:删除不存在的属性返回 true(不得过抛) ── +#[test] +fn strict_delete_missing_property_returns_true() { + truthy("\"use strict\"; var o = {}; delete o.missing === true"); +} + +// ── 数组 length 虚拟属性:sloppy 删除返回 false 且 length 不变 ── +#[test] +fn sloppy_delete_array_length_returns_false() { + truthy("var a = [1, 2, 3]; a.x = 10; delete a.length === false && a.length === 3"); +} + +// ── 数组 length 虚拟属性:严格模式删除抛 TypeError 且 length 不变 ── +#[test] +fn strict_delete_array_length_throws_type_error() { + truthy( + "\"use strict\"; var a = [1, 2, 3]; \ + var threw = false; try { delete a.length; } catch (e) { threw = e instanceof TypeError; } \ + threw && a.length === 3", + ); +} + +// ── 冻结数组元素不可配置:严格模式删除抛 TypeError 且元素保留 ── +#[test] +fn strict_delete_frozen_array_element_throws_type_error() { + truthy( + "\"use strict\"; var a = [1, 2, 3]; Object.freeze(a); \ + var threw = false; try { delete a[0]; } catch (e) { threw = e instanceof TypeError; } \ + threw && a[0] === 1", + ); +} + +// ── Reflect.deleteProperty 恒不抛:不可配置与数组 length 均返回 false ── +#[test] +fn reflect_delete_non_configurable_and_array_length_return_false() { + truthy( + "var o = {}; Object.defineProperty(o, 'x', {value: 1, configurable: false}); \ + var a = [1, 2, 3]; \ + Reflect.deleteProperty(o, 'x') === false && o.x === 1 \ + && Reflect.deleteProperty(a, 'length') === false && a.length === 3", + ); +}