diff --git a/crates/oxide_bytecode/src/opcode.rs b/crates/oxide_bytecode/src/opcode.rs index c5fb14a8..491bcc3c 100644 --- a/crates/oxide_bytecode/src/opcode.rs +++ b/crates/oxide_bytecode/src/opcode.rs @@ -566,6 +566,12 @@ define_opcodes! { NEW_SESSION_OBJECT = 0x9B => "NEW_SESSION_OBJECT", def = Some(SlotSpec::Slot(Slot::Rd)), uses = [], pure = true, jump = false, term = false, ic = false, + // 删除全局内置数据属性(delete 标识符):ext 字 = 键常量池下标(u16)、 + // rd = 布尔结果槽、a 槽 = 内置镜像槽寄存器(删除成功时该槽写 undefined; + // 0 = 无槽)。全局对象由 VM 运行期从 session 取(不依赖 this 寄存器)。 + DELETE_GLOBAL_PROP_C = 0x9C => "DELETE_GLOBAL_PROP_C", + def = Some(SlotSpec::Slot(Slot::Rd)), uses = [SlotSpec::Slot(Slot::A)], + pure = false, jump = false, term = false, ic = false, // ── 成员复合赋值:位/移位 (0x64-0x69) ── COMPOUND_MEMBER_BIT_AND = 0x64 => "COMPOUND_MEMBER_BIT_AND", diff --git a/crates/oxide_emit/src/emit.rs b/crates/oxide_emit/src/emit.rs index 74e514a0..af5ae036 100644 --- a/crates/oxide_emit/src/emit.rs +++ b/crates/oxide_emit/src/emit.rs @@ -707,6 +707,24 @@ impl CompileCtx { BUILTIN_GLOBALS.contains(&name) && !NON_WRITABLE_GLOBAL_BUILTINS.contains(&name) } + /// 可删除全局内置名:可写全局名再除 globalThis(规范 configurable:false, + /// 不可删)与两个宿主名($262/TypedArray,非规范全局,删除会破坏宿主面)—— + /// 自只读/可写单一真源派生,不另立名单。余名描述符皆 + /// {writable:true, configurable:true},delete 真删且返 true。 + pub(crate) fn is_deletable_global_builtin(name: &str) -> bool { + Self::is_writable_builtin_global(name) && !matches!(name, "globalThis" | "$262" | "TypedArray") + } + + /// delete 标识符是否解析到可删除的全局内置镜像槽:名可删(上谓词)且预注册 + /// 镜像槽在册时返回槽寄存器。局部 var/let/参数遮蔽同名时该名不注册镜像槽 + /// (预扫描解析到局部绑定),不命中;槽值在 run/帧入口由全局属性预载。 + pub(crate) fn global_builtin_delete_slot(&self, name: &str) -> Option { + if !Self::is_deletable_global_builtin(name) { + return None; + } + self.scopes.builtin_reg_map.iter().find(|(n, _)| n == name).map(|(_, r)| *r) + } + pub(crate) fn is_known_builtin(name: &str) -> bool { BUILTIN_GLOBALS.contains(&name) } @@ -1845,4 +1863,21 @@ mod tests { assert!(readonly ^ writable, "builtin 名只读/可写归属漂移:{name}"); } } + + /// 漂移守卫:可删名集自可写划分派生(可删 ⊆ 可写、只读三常量不入可删), + /// 且 globalThis/宿主名恒不可删——防派生口径改动时 delete 面单边漂移。 + #[test] + fn deletable_global_builtins_derive_from_writable_partition() { + for name in BUILTIN_GLOBALS { + let deletable = crate::CompileCtx::is_deletable_global_builtin(name); + let writable = crate::CompileCtx::is_writable_builtin_global(name); + assert!(!deletable || writable, "可删名缺失于可写划分:{name}"); + if NON_WRITABLE_GLOBAL_BUILTINS.contains(name) { + assert!(!deletable, "只读三常量名误入可删集:{name}"); + } + } + for name in ["globalThis", "$262", "TypedArray"] { + assert!(!crate::CompileCtx::is_deletable_global_builtin(name), "不可删名误入可删集:{name}"); + } + } } diff --git a/crates/oxide_emit/src/expr/operator/mod.rs b/crates/oxide_emit/src/expr/operator/mod.rs index bcf36350..4cbf3318 100644 --- a/crates/oxide_emit/src/expr/operator/mod.rs +++ b/crates/oxide_emit/src/expr/operator/mod.rs @@ -130,8 +130,18 @@ impl Emitter { ctx.labels.set_label_pos(end_label, ctx.insts.len()); return Ok(result_reg); } - // 严格模式的 delete 标识符由 oxc_semantic 提前拦截为早期错误; - // 非严格语义返回 false(标识符不可删除)。 + // 可删全局内置(可写全局名除 globalThis 与宿主名):运行期真删 + // 全局对象属性(c:true 数据描述符)并返 true;删除成功时清镜像 + // 槽,裸读与 globalThis 反射不失步。 + if let Some(slot_reg) = ctx.global_builtin_delete_slot(name) { + let key_idx = ctx.add_constant(Constant::String(name.to_string())); + let reg = ctx.alloc_reg(); + ctx.inst(Inst::delete_global_prop_c(Operand::Reg(reg), Operand::Reg(slot_reg), key_idx)); + return Ok(reg); + } + // 其余标识符(三常量 c:false / 局部遮蔽 / var 名等)非严格语义 + // 返回 false;严格模式的 delete 标识符由 oxc_semantic 提前拦截 + // 为早期错误(未声明名 delete 应返 true 的缺失是既有缺口)。 let idx = ctx.add_constant(Constant::Boolean(false)); let reg = ctx.alloc_reg(); ctx.inst(Inst::load_const(Operand::Reg(reg), idx)); diff --git a/crates/oxide_ir/src/contract.rs b/crates/oxide_ir/src/contract.rs index 6ff93790..71403466 100644 --- a/crates/oxide_ir/src/contract.rs +++ b/crates/oxide_ir/src/contract.rs @@ -746,6 +746,8 @@ mod tests { false, ); assert_group(&[OpCode::DEFINE_GLOBAL_PROP_C, OpCode::DEFINE_GLOBAL_PROP_C_IF_ABSENT], None, &[2], false); + // delete 全局内置:结果写 rd、镜像槽寄存器是 a 槽 use + assert_contract(OpCode::DELETE_GLOBAL_PROP_C, Some(1), &[2], false); assert_contract(OpCode::DEFINE_PROP_ATTRS, None, &[1, 2, 3], false); assert_contract(OpCode::DEFINE_ACCESSOR_ATTRS, None, &[1, 2, 3], false); assert_group(&[OpCode::CREATE_ARGUMENTS, OpCode::CREATE_REST_ARRAY], Some(1), &[], false); diff --git a/crates/oxide_ir/src/inst.rs b/crates/oxide_ir/src/inst.rs index 92cd7b36..1c959779 100644 --- a/crates/oxide_ir/src/inst.rs +++ b/crates/oxide_ir/src/inst.rs @@ -260,6 +260,13 @@ impl Inst { Self::with_ext(OpCode::DELETE_PROP_STATIC, obj, obj, Operand::None, &[const_idx]) } + /// 删除全局内置数据属性(delete 标识符):ext = [key_idx](键常量池下标), + /// `result` 放 rd 槽(布尔删除结果),`slot` 放 a 槽(内置镜像槽寄存器,删除 + /// 成功时写 undefined;无槽传 None)。全局对象由 VM 运行期从 session 解析。 + pub fn delete_global_prop_c(result: Operand, slot: Operand, key_idx: u16) -> Self { + Self::with_ext(OpCode::DELETE_GLOBAL_PROP_C, result, slot, Operand::None, &[key_idx as u32]) + } + /// 对象 rest 展开:`{...src, 排除 excluded_idx 常量列出的键}` 存入 `rest`。 /// `excl_arr` 为可选运行时 excluded 键数组寄存器(computed key 求值结果), /// None 时仅用编译期 excluded_idx 常量。 diff --git a/crates/oxide_vm/src/dispatch/property.rs b/crates/oxide_vm/src/dispatch/property.rs index 7a9202a2..b13e56b3 100644 --- a/crates/oxide_vm/src/dispatch/property.rs +++ b/crates/oxide_vm/src/dispatch/property.rs @@ -660,4 +660,43 @@ impl Vm { self.regs[rd] = JsValue::bool(deleted); Ok(false) } + + /// 删除全局内置数据属性(delete 标识符):ext 字 = 键常量池下标(u16), + /// `rd` = 布尔结果槽,`slot` = 内置镜像槽寄存器(0 = 无槽)。全局对象由 + /// session 解析——函数体内 delete 不依赖 this 寄存器。 + /// + /// # 步骤 + /// 1. 键常量 → intern si → 对全局对象走共享删除逻辑(不可配置 → false 且属性 + /// 保留;缺失 → true;可配置 → 真删并返回 true) + /// 2. 删除成功且 a 槽为有效镜像槽时槽写 undefined + /// + /// # 副作用 + /// - 删除成功时修改全局对象 shape/属性表/generation + /// - 删除成功时写镜像槽寄存器(该槽随后裸读回 undefined) + /// + /// # 边界与前提 + /// - 键常量必须是字符串(emit 侧保证);非字符串按错误返回 + /// - 镜像槽仅在 run/帧入口重载,不清槽则删除后裸读回入口预载的旧值—— + /// 清槽与真删同生共死:删除失败(c:false)属性保留,槽值不动 + pub(crate) fn dispatch_delete_global_prop_c(&mut self, rd: usize, slot: usize, key_idx: u16) -> Result<(), String> { + vm_trace!("DELETE_GLOBAL_PROP_C slot={} idx={}", slot, key_idx); + let idx = key_idx as usize; + let key_val = self.immutables().get(idx).copied().unwrap_or(JsValue::undefined()); + if !key_val.is_string() { + return Err(format!("DELETE_GLOBAL_PROP_C constant index {idx} is not a string key")); + } + let si = self.property_key_si(key_val)?; + let global_ptr = self.session.global_object().as_ptr() as *mut JsObject; + // SAFETY: 全局对象钉在 session 永久区,指针在 VM 生命周期内有效。 + let obj = unsafe { &mut *global_ptr }; + // 统一走共享删除逻辑(与 Reflect.deleteProperty 同源);不可配置返回 + // false 且属性保留。 + let deleted = oxide_builtins::object::delete_own_property(self, obj, si); + // 仅删除成功才清镜像槽(无槽 0 不写,reg 0 是返回槽不可误写)。 + if deleted && slot > 0 { + self.regs[slot] = JsValue::undefined(); + } + self.regs[rd] = JsValue::bool(deleted); + Ok(()) + } } diff --git a/crates/oxide_vm/src/ic_helper.rs b/crates/oxide_vm/src/ic_helper.rs index 38469c54..eb30d048 100644 --- a/crates/oxide_vm/src/ic_helper.rs +++ b/crates/oxide_vm/src/ic_helper.rs @@ -68,6 +68,7 @@ fn ext_word_count(bytecode: &[Instr], pc: usize) -> usize { | OpCode::DEFINE_PROP_ATTRS | OpCode::DEFINE_GLOBAL_PROP_C | OpCode::DEFINE_GLOBAL_PROP_C_IF_ABSENT + | OpCode::DELETE_GLOBAL_PROP_C | OpCode::DELETE_PROP_STATIC | OpCode::REST_OBJECT | OpCode::INIT_PRIVATE => 1, diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 57e00f74..c8c320e4 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -2129,6 +2129,12 @@ impl Vm { self.dispatch_define_global_prop_c_if_absent(a, key_idx)?; } + OpCode::DELETE_GLOBAL_PROP_C => { + let key_idx = self.bytecode[self.pc] as u16; + self.pc += 1; + self.dispatch_delete_global_prop_c(rd, a, key_idx)?; + } + OpCode::DEFINE_PROP_ATTRS => { let attrs = self.bytecode[self.pc] as u8; self.pc += 1; diff --git a/crates/oxide_vm/tests/global_builtin_delete_tests.rs b/crates/oxide_vm/tests/global_builtin_delete_tests.rs new file mode 100644 index 00000000..34178421 --- /dev/null +++ b/crates/oxide_vm/tests/global_builtin_delete_tests.rs @@ -0,0 +1,94 @@ +//! delete 全局内置标识符(DELETE_GLOBAL_PROP_C)回归测试。 +//! +//! 可删全局内置(可写全局名除 globalThis 与宿主名,描述符 +//! {writable:true, configurable:true})的 delete 标识符运行期真删全局对象属性 +//! 并返 true,删除成功时清镜像槽(裸读与 globalThis 反射不失步)。覆盖:删除返 +//! 值、属性真删、镜像槽清、删后重写、重复删、三常量 c:false 拒绝、globalThis 拒 +//! 绝、成员形不受影响、参数遮蔽不命中。 + +use std::sync::Arc; + +use oxide_compiler::compiler::Compiler; +use oxide_parser::Allocator; +use oxide_vm::vm::Vm; + +fn eval_truthy(source: &str) { + let allocator = Allocator::default(); + let program = oxide_parser::parse(&allocator, source).expect("parse"); + let module = Compiler::new().compile(&program).expect("compile"); + let mut vm = Vm::new(); + let result = vm.run(&Arc::new(module)).expect("run"); + assert!(result.is_bool() && result.as_bool(), "expected true, got: {result:?}\nsource: {source}"); +} + +// ── 删除返值与属性真删 ── +#[test] +fn delete_math_returns_true_and_property_removed() { + eval_truthy("var m = Math; var d = delete Math; d === true && globalThis.Math === undefined"); +} + +#[test] +fn delete_writable_builtin_names() { + for name in ["Object", "JSON", "Symbol", "Promise", "parseInt"] { + let src = format!("var m = {name}; var d = delete {name}; d === true && globalThis.{name} === undefined"); + eval_truthy(&src); + } +} + +// ── 镜像槽清:删除后裸读回 undefined ── +#[test] +fn delete_math_then_bare_read_via_mirror() { + eval_truthy("var m = Math; delete Math; Math === undefined"); +} + +#[test] +fn fn_body_delete_clears_frame_mirror() { + eval_truthy( + "function f() { var m = Math; delete Math; \ + return Math === undefined && globalThis.Math === undefined; } f() === true", + ); +} + +// ── 删后重写:两侧再同步 ── +#[test] +fn delete_then_reassign_both_sides() { + eval_truthy("var m = Math; delete Math; Math = 5; Math === 5 && globalThis.Math === 5"); +} + +// ── 重复删:属性已缺失仍返 true ── +#[test] +fn delete_twice_second_returns_true() { + eval_truthy("var m = Math; delete Math; delete Math === true && globalThis.Math === undefined"); +} + +// ── 三常量 c:false:拒绝且属性保留 ── +#[test] +fn delete_three_constants_refused_property_kept() { + eval_truthy( + "delete undefined === false && delete NaN === false && delete Infinity === false \ + && Object.getOwnPropertyDescriptor(globalThis, 'undefined') !== undefined \ + && Object.getOwnPropertyDescriptor(globalThis, 'NaN') !== undefined \ + && Object.getOwnPropertyDescriptor(globalThis, 'Infinity') !== undefined", + ); +} + +// ── globalThis c:false:拒绝且属性保留 ── +#[test] +fn delete_global_this_refused_property_kept() { + eval_truthy( + "delete globalThis === false \ + && Object.getOwnPropertyDescriptor(globalThis, 'globalThis') !== undefined", + ); +} + +// ── 成员形不受影响(既有 DELETE_PROP_STATIC 路径) ── +#[test] +fn member_delete_unchanged() { + eval_truthy("delete Math.LN2 === false"); +} + +// ── 参数遮蔽:不注册镜像槽,delete 落非严格 false ── +#[test] +fn parameter_shadow_delete_false() { + eval_truthy("function f(Math) { return delete Math; } f(1) === false"); +}