From 4452f1da69262d947ccba690df9dc299f1409be6 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Tue, 15 Sep 2026 05:03:28 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20delete=20=E6=A0=87=E8=AF=86=E7=AC=A6?= =?UTF-8?q?=E6=8C=89=20DeleteBinding=20=E4=B8=89=E5=88=86=E7=B1=BB?= =?UTF-8?q?=E5=8F=91=E8=BF=90=E8=A1=8C=E6=9C=9F=E6=8E=A2=E9=92=88=EF=BC=8C?= =?UTF-8?q?=E8=A1=A5=E7=9C=9F=E5=88=A0=E4=B8=8E=E8=AF=BB=E4=BE=A7=E6=95=88?= =?UTF-8?q?=E5=BA=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_emit/src/expr/operator/mod.rs | 72 +++++++++--- .../tests/delete_eval_var_env_tests.rs | 108 ++++++++++++++++++ 2 files changed, 164 insertions(+), 16 deletions(-) create mode 100644 crates/oxide_vm/tests/delete_eval_var_env_tests.rs diff --git a/crates/oxide_emit/src/expr/operator/mod.rs b/crates/oxide_emit/src/expr/operator/mod.rs index 6687a949..65e11ca0 100644 --- a/crates/oxide_emit/src/expr/operator/mod.rs +++ b/crates/oxide_emit/src/expr/operator/mod.rs @@ -126,10 +126,10 @@ impl Emitter { /// 删除并返回删除结果;非引用操作数返回 true。 /// /// # 步骤 - /// 1. 标识符:with 体动态解析、可删全局内置镜像槽先行;未声明名(含隐式 - /// 全局槽,引用不可解析或全局属性可配置)返回 true;其余已声明绑定 - /// (var/let/函数名/参数,非可配置或属性引用)返回 false。严格模式的 - /// delete 标识符是早期错误,由语义分析阶段拦截,不在此出现。 + /// 1. 标识符:with 体动态解析、可删全局内置镜像槽先行;其余按 DeleteBinding + /// 三分类(见标识符臂注释)——局部绑定与脚本自身顶层已声明名发 false + /// 常数,未声明名/隐式全局槽/eval 程序自身顶层已声明名发全局对象运行期 + /// 探针。严格模式的 delete 标识符是早期错误,由语义分析阶段拦截,不在此出现。 /// 2. 静态/计算成员与链式成员:从对象真删属性,结果为删除返回值。 /// 3. 其余形态(字面量/调用/new/this/一元形/序列/嵌套 delete 等):先求值 /// 操作数(副作用与操作数求值期错误不被吞),再发常量 true。 @@ -187,17 +187,53 @@ impl Emitter { ctx.inst(Inst::delete_global_prop_c(Operand::Reg(reg), Operand::Reg(slot_reg), key_idx)); return Ok(reg); } - // 已声明绑定(upvalue/捕获 cell/局部 var/let/函数名/参数/顶层 var) - // 是非属性引用或全局对象上不可配置绑定,返回 false;未声明名 - // (引用不可解析)与隐式全局槽(全局属性可配置)返回 true。 - // 严格模式的 delete 标识符由语义分析提前拦截为早期错误。 - let declared = ctx.captured_bindings.contains_key(name) - || ctx.current_upvalue_captures.iter().any(|u| u.name == name) - || match ctx.lookup(name) { - Err(_) => false, - Ok(reg) => !ctx.is_implicit_global_reg(reg), - }; - let idx = ctx.add_constant(Constant::Boolean(!declared)); + // DeleteBinding 三分类(13.5.1.2 步5 绑定引用走 base.DeleteBinding): + // - 局部绑定(捕获 cell/upvalue/lookup 命中函数或块作用域槽)与 + // 非 eval 脚本自身顶层已声明名(脚本 var/函数名 c:false)→ false 常数; + // - 其余——未声明名(引用不可解析)、隐式全局槽、eval 程序自身顶层 + // 已声明名(c:true)——发全局对象运行期探针(与 Reflect.deleteProperty + // 同源 DeleteBinding 语义:缺失 → true;不可配置 → false 且保留; + // 可配置 → 真删且 true)。三类全局属性的 c 位已由各自写点物化, + // 探针取值即规范值;独立编译的 eval 程序见不到调用方域变量,静态 + // "当前程序内是否声明"粗于规范动态判定,故不可解析面一律运行期定值。 + // 严格模式的 delete 标识符由语义分析提前拦截为早期错误,本臂在 + // strict 代码不可达。 + let local = ctx.captured_bindings.contains_key(name) + || ctx.current_upvalue_captures.iter().any(|u| u.name == name); + let probe = if local { + false + } else { + match ctx.scopes.symbols.lookup_any_binding(name) { + // 未声明名(引用不可解析):运行期按全局属性定值。 + None => true, + Some((binding, scope_idx)) => { + if scope_idx == 0 { + // 全局作用域:隐式全局槽(未声明名读写登记,属性 + // c:true)与 eval 程序自身顶层名(c:true)可删; + // 非 eval 脚本自身顶层 var/函数名(c:false)保留 false。 + ctx.is_implicit_global_reg(binding.reg) || ctx.is_eval_script + } else { + // 函数/块作用域局部绑定:非属性引用,恒 false。 + false + } + } + } + }; + if probe { + 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::None, key_idx)); + // 隐式全局槽被真删后,同程序后续裸读该名须走全局对象属性 + // (A 侧单一真值):缺失属性读抛 ReferenceError,delete 的 + // 真删效应在读侧可见。 + if let Some((binding, _)) = ctx.scopes.symbols.lookup_any_binding(name) { + if ctx.is_implicit_global_reg(binding.reg) { + ctx.implicit_global_reads.insert(binding.reg); + } + } + return Ok(reg); + } + let idx = ctx.add_constant(Constant::Boolean(false)); let reg = ctx.alloc_reg(); ctx.inst(Inst::load_const(Operand::Reg(reg), idx)); Ok(reg) @@ -309,8 +345,12 @@ impl Emitter { ctx.current_upvalue_captures.iter().any(|u| u.name == name) || ctx.captured_bindings.contains_key(name); // 顶层已声明 var:typeof 读全局对象属性(A 侧单一真值),缺失 → "undefined" // (非抛,IsUnresolvableReference 语义)。未声明名同走此路(lookup 未命中)。 + // 隐式全局槽(未声明名读写登记)同走属性路:delete 真删后缺失 → "undefined" + // 而非经镜像槽读旧值或抛 ReferenceError(typeof 对 unresolvable 引用不抛)。 + let binding = ctx.scopes.symbols.lookup_any_binding(name); + let implicit_global = binding.is_some_and(|(b, _)| ctx.is_implicit_global_reg(b.reg)); let is_tier = self.is_global_tier_name(ctx, name); - if !in_with_dynamic && !captured && (is_tier || ctx.scopes.symbols.lookup_any_binding(name).is_none()) { + if !in_with_dynamic && !captured && (is_tier || binding.is_none() || implicit_global) { let key_idx = ctx.add_constant(Constant::String(name.to_string())); let r = ctx.alloc_reg(); ctx.inst(Inst::new( diff --git a/crates/oxide_vm/tests/delete_eval_var_env_tests.rs b/crates/oxide_vm/tests/delete_eval_var_env_tests.rs new file mode 100644 index 00000000..761a6238 --- /dev/null +++ b/crates/oxide_vm/tests/delete_eval_var_env_tests.rs @@ -0,0 +1,108 @@ +//! delete 标识符 DeleteBinding 面:eval 程序独立编译见不到调用方域变量,静态 +//! "当前程序内是否声明"粗于规范动态判定,未声明名/隐式全局槽/eval 程序自身 +//! 顶层已声明名一律发全局对象运行期探针(缺失 → true;不可配置 → false 且保留; +//! 可配置 → 真删且 true)。局部绑定与非 eval 脚本自身顶层已声明名保留 false +//! 常数。覆盖:direct eval 对调用方脚本 var、eval 自身 var 真删、跨 eval 真删、 +//! 函数内隐式全局真删、读侧真删可见(删后裸读抛 ReferenceError)、typeof 对 +//! 删后缺失名不抛、删后重写同步、绿基线(脚本 var/顶层函数名/未声明缺失/builtin +//! 槽优先级/strict 调用方 indirect eval)。 + +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}"); +} + +// ── direct eval 对调用方脚本 var(c:false):false 且属性保留 ── +#[test] +fn eval_delete_caller_script_var_returns_false_and_kept() { + eval_truthy("var x = 1; var d = eval('delete x'); d === false && x === 1 && globalThis.x === 1"); +} + +// ── eval 程序自身顶层 var(c:true):true 且真删 ── +#[test] +fn eval_delete_own_var_returns_true_and_removed() { + eval_truthy("eval('var w = 5; delete w') === true && typeof w === 'undefined'"); +} + +// ── 跨 eval:前一 eval 建的 var(c:true),后一 eval 真删 ── +#[test] +fn eval_delete_var_from_previous_eval_returns_true_and_removed() { + eval_truthy("eval('var y2 = 5'); var d = eval('delete y2'); d === true && typeof y2 === 'undefined'"); +} + +// ── 函数内隐式全局(未声明写登记,c:true):true 且真删 ── +#[test] +fn fn_scope_delete_implicit_global_returns_true_and_removed() { + eval_truthy( + "q = 2; (function() { var d = delete q; return d === true && (function() { \ + try { void q; return false; } catch (e) { return e instanceof ReferenceError; } \ + })(); })() === true", + ); +} + +// ── 未声明缺失名:探针缺失臂 true(非引用目标语义保留) ── +#[test] +fn delete_undeclaring_missing_name_returns_true() { + eval_truthy("delete nomatch_xyz_probe === true"); +} + +// ── 绿基线:脚本自身顶层 var(c:false)false 常数,值保留 ── +#[test] +fn delete_script_top_var_returns_false_and_kept() { + eval_truthy("var y = 2; delete y === false && y === 2"); +} + +// ── 绿基线:函数作用域读脚本顶层 var,作用域链判定局部性,false ── +#[test] +fn delete_top_var_from_function_scope_returns_false() { + eval_truthy("var p = 1; (function() { return delete p; })() === false && p === 1"); +} + +// ── 绿基线:strict 调用方 + indirect eval(eval 代码不继承调用方严格性) ── +#[test] +fn strict_caller_indirect_eval_delete_undeclared_returns_true() { + eval_truthy("(function() { \"use strict\"; return (0, eval)(\"delete zqq_probe\"); })() === true"); +} + +// ── 可删全局内置镜像槽臂优先:探针臂不吞 builtin 槽(真删 + 清槽) ── +#[test] +fn deletable_builtin_slot_arm_precedes_probe() { + eval_truthy("var m = Math; delete Math === true && globalThis.Math === undefined"); +} + +// ── 读侧真删可见:删后同程序裸读抛 ReferenceError(A 侧单一真值) ── +#[test] +fn bare_read_after_real_delete_throws_reference_error() { + eval_truthy( + "x = 1; delete x === true && (function() { try { void x; return false; } \ + catch (e) { return e instanceof ReferenceError; } })() === true", + ); +} + +// ── typeof 对删后缺失名不抛(IsUnresolvableReference → "undefined") ── +#[test] +fn typeof_after_real_delete_returns_undefined_not_throw() { + eval_truthy("x = 1; delete x; typeof x === 'undefined'"); +} + +// ── 删后重写:寄存器与全局对象属性重新同步 ── +#[test] +fn reassign_after_real_delete_resyncs_both_sides() { + eval_truthy("x = 1; delete x; x = 5; x === 5 && globalThis.x === 5"); +} + +// ── 删前读值不受影响(读集登记仅自探针起) ── +#[test] +fn read_before_delete_unchanged() { + eval_truthy("x = 1; var r = x; delete x; r === 1"); +}