diff --git a/Cargo.lock b/Cargo.lock index 30a53ca2..3ab71678 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1095,7 +1095,7 @@ dependencies = [ [[package]] name = "oxide_builtins" -version = "0.8.19" +version = "0.8.20" dependencies = [ "chrono", "fast-float", @@ -1120,7 +1120,7 @@ dependencies = [ [[package]] name = "oxide_bytecode" -version = "0.8.19" +version = "0.8.20" dependencies = [ "num-bigint", "oxide_log", @@ -1128,7 +1128,7 @@ dependencies = [ [[package]] name = "oxide_cfg" -version = "0.8.19" +version = "0.8.20" dependencies = [ "oxide_bytecode", "oxide_emit", @@ -1139,7 +1139,7 @@ dependencies = [ [[package]] name = "oxide_cli" -version = "0.8.19" +version = "0.8.20" dependencies = [ "ansi_term", "bumpalo", @@ -1159,7 +1159,7 @@ dependencies = [ [[package]] name = "oxide_code_cache" -version = "0.8.19" +version = "0.8.20" dependencies = [ "lru", "oxide_bytecode", @@ -1168,7 +1168,7 @@ dependencies = [ [[package]] name = "oxide_compiler" -version = "0.8.19" +version = "0.8.20" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1187,7 +1187,7 @@ dependencies = [ [[package]] name = "oxide_dce" -version = "0.8.19" +version = "0.8.20" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1201,7 +1201,7 @@ dependencies = [ [[package]] name = "oxide_emit" -version = "0.8.19" +version = "0.8.20" dependencies = [ "num-bigint", "oxc_allocator", @@ -1216,7 +1216,7 @@ dependencies = [ [[package]] name = "oxide_ir" -version = "0.8.19" +version = "0.8.20" dependencies = [ "oxide_bytecode", "oxide_log", @@ -1225,7 +1225,7 @@ dependencies = [ [[package]] name = "oxide_kernel" -version = "0.8.19" +version = "0.8.20" dependencies = [ "bumpalo", "dashmap", @@ -1240,7 +1240,7 @@ dependencies = [ [[package]] name = "oxide_liveness" -version = "0.8.19" +version = "0.8.20" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1252,7 +1252,7 @@ dependencies = [ [[package]] name = "oxide_log" -version = "0.8.19" +version = "0.8.20" dependencies = [ "tracing", "tracing-appender", @@ -1261,7 +1261,7 @@ dependencies = [ [[package]] name = "oxide_parser" -version = "0.8.19" +version = "0.8.20" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1276,7 +1276,7 @@ dependencies = [ [[package]] name = "oxide_regalloc" -version = "0.8.19" +version = "0.8.20" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1292,7 +1292,7 @@ dependencies = [ [[package]] name = "oxide_runtime_api" -version = "0.8.19" +version = "0.8.20" dependencies = [ "num-bigint", "num-traits", @@ -1304,7 +1304,7 @@ dependencies = [ [[package]] name = "oxide_test262" -version = "0.8.19" +version = "0.8.20" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1321,7 +1321,7 @@ dependencies = [ [[package]] name = "oxide_types" -version = "0.8.19" +version = "0.8.20" dependencies = [ "bumpalo", "hashbrown 0.14.5", @@ -1333,7 +1333,7 @@ dependencies = [ [[package]] name = "oxide_vm" -version = "0.8.19" +version = "0.8.20" dependencies = [ "bumpalo", "chrono", diff --git a/Cargo.toml b/Cargo.toml index cf54f6d0..6dc272e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ members = [ ] [workspace.package] -version = "0.8.19" +version = "0.8.20" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_emit/src/expr/operator/mod.rs b/crates/oxide_emit/src/expr/operator/mod.rs index aa798598..44101ce9 100644 --- a/crates/oxide_emit/src/expr/operator/mod.rs +++ b/crates/oxide_emit/src/expr/operator/mod.rs @@ -138,7 +138,7 @@ impl Emitter { Expression::Identifier(ident) => { let name = ident.name.as_str(); // with 体内自由标识符:对象有该属性则删除对象属性(返回删除结果), - // 否则非严格语义返回 true。 + // 对象无该属性则回退外层绑定,按其可删除性定值。 if !ctx.with_stack.is_empty() && !ctx.is_with_internal_binding(name) { let obj_reg = ctx.innermost_with_obj().expect("with stack non-empty"); let key_idx = ctx.add_constant(Constant::String(name.to_string())); @@ -173,77 +173,19 @@ impl Emitter { )); ctx.inst(Inst::jmp(end_label)); ctx.labels.set_label_pos(fallback_label, ctx.insts.len()); - let true_idx = ctx.add_constant(Constant::Boolean(true)); - ctx.inst(Inst::load_const(Operand::Reg(result_reg), true_idx)); + // with 对象无该属性:引用解析到外层绑定,按外层绑定的可删除性 + // 定值(声明式绑定 → false;全局属性 → 运行期探针)。 + let binding_result = self.emit_delete_binding_result(name, ctx)?; + ctx.inst(Inst::new( + OpCode::LOAD_VAR, + Operand::Reg(result_reg), + Operand::Reg(binding_result), + Operand::None, + )); ctx.labels.set_label_pos(end_label, ctx.insts.len()); return Ok(result_reg); } - // 可删全局内置(可写全局名除宿主名 $262):运行期真删 - // 全局对象属性(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); - } - // DeleteBinding 三分类(13.5.1.2 步5 绑定引用走 base.DeleteBinding): - // - 局部绑定(捕获 cell/upvalue/lookup 命中函数或块作用域槽)、 - // 非 eval 脚本自身顶层已声明名(脚本 var/函数名 c:false)、eval - // 程序自身顶层 let/const(落 eval 自身 lexical 环境,declarative - // 环境记录 DeleteBinding 恒 false 且不物化全局属性)→ false 常数; - // - 其余——未声明名(引用不可解析)、隐式全局槽、eval 程序自身顶层 - // var/函数名(物化 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 程序自身顶层 var/函数名(顶层 - // var 名集 ∪ 顶层函数声明名,物化 c:true 全局属性) - // 可删;eval 顶层 let/const 不在名集内(自身 lexical - // 环境不可删),与非 eval 脚本自身顶层 var/函数名 - // (c:false)同保留 false。eval 起源随嵌套函数继承, - // 子 ctx 内 delete 同一名同样判可删。 - ctx.is_implicit_global_reg(binding.reg) - || (ctx.is_eval_origin && ctx.global_tier_names.contains(name)) - } 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)); - // 隐式全局槽被真删后,同程序后续裸读该名须走全局对象属性(顶层 var - // 的唯一存储,引擎侧不保留镜像副本):缺失属性读抛 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) + self.emit_delete_binding_result(name, ctx) } Expression::StaticMemberExpression(member) if matches!(&member.object, Expression::Super(_)) => { // super 属性引用不可删除:在求值引用之前直接抛 ReferenceError。 @@ -337,6 +279,96 @@ impl Emitter { } } + /// delete 标识符回退外层绑定的定值:可删全局内置镜像槽先行(真删加清槽), + /// 其余按 DeleteBinding 三分类发运行期探针(未声明名/隐式全局槽/eval 自身 + /// 顶层 var/函数名)或 false 常数(局部绑定/脚本自身顶层 var/函数名/eval + /// 自身顶层 let/const)。返回结果寄存器。 + /// + /// # 步骤 + /// 1. 可删全局内置镜像槽命中:发 DELETE_GLOBAL_PROP_C(真删加清槽)返结果寄存器 + /// 2. DeleteBinding 三分类:局部绑定与脚本自身顶层已声明名发 false 常数; + /// 未声明名/隐式全局槽/eval 自身顶层 var/函数名发全局对象运行期探针 + /// + /// # 边界与前提 + /// - 严格模式的 delete 标识符由语义分析提前拦截为早期错误,本臂在 strict 代码不可达 + /// + /// # 副作用 + /// - 探针臂真删隐式全局槽后登记 `implicit_global_reads`(同程序后续裸读该名 + /// 走全局对象属性,缺失抛 ReferenceError) + fn emit_delete_binding_result(&self, name: &str, ctx: &mut CompileCtx) -> Result { + // 可删全局内置(可写全局名除宿主名 $262):运行期真删全局对象属性 + // (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); + } + + // DeleteBinding 三分类(13.5.1.2 步5 绑定引用走 base.DeleteBinding): + // - 局部绑定(捕获 cell/upvalue/lookup 命中函数或块作用域槽)、 + // 非 eval 脚本自身顶层已声明名(脚本 var/函数名 c:false)、eval + // 程序自身顶层 let/const(落 eval 自身 lexical 环境,declarative + // 环境记录 DeleteBinding 恒 false 且不物化全局属性)→ false 常数; + // - 其余——未声明名(引用不可解析)、隐式全局槽、eval 程序自身顶层 + // var/函数名(物化 c:true 全局属性)——发全局对象运行期探针(与 + // Reflect.deleteProperty 同源 DeleteBinding 语义:缺失 → true; + // 不可配置 → false 且保留;可配置 → 真删且 true)。三类全局属性的 + // c 位已由各自写点物化,探针取值即规范值;独立编译的 eval 程序见 + // 不到调用方域变量,静态"当前程序内是否声明"粗于规范动态判定,故 + // 不可解析面一律运行期定值。 + 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 程序自身顶层 var/函数名(顶层 + // var 名集 ∪ 顶层函数声明名,物化 c:true 全局属性) + // 可删;eval 顶层 let/const 不在名集内(自身 lexical + // 环境不可删),与非 eval 脚本自身顶层 var/函数名 + // (c:false)同保留 false。eval 起源随嵌套函数继承, + // 子 ctx 内 delete 同一名同样判可删。 + ctx.is_implicit_global_reg(binding.reg) + || (ctx.is_eval_origin && ctx.global_tier_names.contains(name)) + } else { + // 函数/块作用域局部绑定:非属性引用,恒 false。 + false + } + } + } + }; + + // 探针臂:全局对象运行期定值(缺失 → true;不可配置 → false 且保留; + // 可配置 → 真删且 true)。 + 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)); + // 隐式全局槽被真删后,同程序后续裸读该名须走全局对象属性(顶层 var + // 的唯一存储,引擎侧不保留镜像副本):缺失属性读抛 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); + } + + // 声明式绑定与脚本自身顶层已声明名:恒 false。 + let idx = ctx.add_constant(Constant::Boolean(false)); + let reg = ctx.alloc_reg(); + ctx.inst(Inst::load_const(Operand::Reg(reg), idx)); + Ok(reg) + } + /// typeof 操作数求值:未声明标识符特判为 "undefined"。 /// /// # 步骤 diff --git a/crates/oxide_vm/tests/with_delete_result_tests.rs b/crates/oxide_vm/tests/with_delete_result_tests.rs new file mode 100644 index 00000000..c32ba8df --- /dev/null +++ b/crates/oxide_vm/tests/with_delete_result_tests.rs @@ -0,0 +1,61 @@ +//! with 内 delete 标识符结果值回归测试。 +//! +//! `with` 对象有该属性时删对象属性(返回删除结果);对象无该属性时引用 +//! 解析到外层绑定,按 DeleteBinding 定值(var/let/const 绑定恒 false; +//! 未声明名/隐式全局槽/可删全局内置走运行期探针真删)。覆盖:var 回退 +//! false、let 回退 false、属性删除分支保真、未声明名探针、可删全局内置 +//! 真删加清槽、with 内声明分支。 + +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}"); +} + +// ── var 回退:with 对象无该属性,外层 var c:false 恒 false ── +#[test] +fn with_delete_outer_var_returns_false() { + eval_truthy("var o = {}; o.x = 1; var d; with(o) { d = delete o; } d === false && o.x === 1"); +} + +// ── let 回退:外层 lexical 绑定 DeleteBinding 恒 false ── +#[test] +fn with_delete_outer_let_returns_false() { + eval_truthy("let l = 1; var o = {}; var d; with(o) { d = delete l; } d === false && l === 1"); +} + +// ── 属性删除分支保真:with 对象有该属性,真删返 true ── +#[test] +fn with_delete_object_property_returns_true() { + eval_truthy("var o = {p: 1}; var d; with(o) { d = delete p; } d === true && !('p' in o)"); +} + +// ── 未声明名回退:探针缺失臂返 true ── +#[test] +fn with_delete_undeclared_name_returns_true() { + eval_truthy("var o = {}; var d; with(o) { d = delete nomatch_w1; } d === true"); +} + +// ── 可删全局内置回退:真删全局属性并清镜像槽 ── +#[test] +fn with_delete_deletable_builtin_returns_true_and_removed() { + eval_truthy( + "var m = Math; var o = {}; var d; with(o) { d = delete Math; } \ + d === true && globalThis.Math === undefined", + ); +} + +// ── with 内声明分支:with 内 let 优先于对象属性,落局部绑定 false ── +#[test] +fn with_delete_internal_let_returns_false() { + eval_truthy("var o = {}; var d; with(o) { let p = 1; d = delete p; } d === false"); +}