fix: delete 标识符按 DeleteBinding 三分类发运行期探针,补真删与读侧效应

This commit is contained in:
2026-09-15 05:03:28 +00:00
parent aafd07728c
commit 4452f1da69
2 changed files with 164 additions and 16 deletions
+56 -16
View File
@@ -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(
@@ -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");
}