diff --git a/crates/oxide_emit/src/expr/assignment.rs b/crates/oxide_emit/src/expr/assignment.rs index f3310c05..5c2b2948 100644 --- a/crates/oxide_emit/src/expr/assignment.rs +++ b/crates/oxide_emit/src/expr/assignment.rs @@ -399,21 +399,29 @@ impl Emitter { let is_const = ctx.lookup_const_flag(name); let const_flag = if is_const { 1 } else { 0 }; let var_reg = ctx.lookup_or_global(name); - // 隐式全局判定以寄存器集合为准:读侧(旧值解析)已登记绑定, - // 写侧二次解析命中集合而非"新登记"。 - let is_implicit = ctx.implicit_global_writes.contains(&var_reg); - if is_implicit && ctx.is_strict { - // 严格模式未声明写:抛 ReferenceError,后续 LOAD_VAR 不可达。 - let _ = self.emit_strict_undeclared_write(name, ctx)?; - } else { - ctx.inst(Inst::new( - OpCode::STORE_VAR, - Operand::Reg(var_reg), - Operand::Reg(val_reg), - Operand::Imm(const_flag), - )); - if is_implicit { - self.emit_implicit_global_write(name, var_reg, ctx); + // 全局不可写内置槽:strict 抛 TypeError(put 失败);sloppy 跳过槽写, + // 表达式值 = RHS(尾部结果装载取 val_reg)。短路时序与 PutValue 一致: + // 短路未通过不到达此处、不抛错,通过后才在写点拦截。 + let readonly_builtin = ctx.targets_readonly_builtin(name, var_reg); + if readonly_builtin && ctx.is_strict { + let _ = self.emit_throw_error("TypeError", "cannot assign to read-only property", ctx)?; + } else if !readonly_builtin { + // 隐式全局判定以寄存器集合为准:读侧(旧值解析)已登记绑定, + // 写侧二次解析命中集合而非"新登记"。 + let is_implicit = ctx.implicit_global_writes.contains(&var_reg); + if is_implicit && ctx.is_strict { + // 严格模式未声明写:抛 ReferenceError,后续 LOAD_VAR 不可达。 + let _ = self.emit_strict_undeclared_write(name, ctx)?; + } else { + ctx.inst(Inst::new( + OpCode::STORE_VAR, + Operand::Reg(var_reg), + Operand::Reg(val_reg), + Operand::Imm(const_flag), + )); + if is_implicit { + self.emit_implicit_global_write(name, var_reg, ctx); + } } } } diff --git a/crates/oxide_emit/src/prepass.rs b/crates/oxide_emit/src/prepass.rs index d6c8de2a..c34c3596 100644 --- a/crates/oxide_emit/src/prepass.rs +++ b/crates/oxide_emit/src/prepass.rs @@ -268,6 +268,14 @@ impl Emitter { fn pre_scan_builtin_target(&self, target: &oxide_parser::SimpleAssignmentTarget, ctx: &mut CompileCtx) { match target { + oxide_parser::SimpleAssignmentTarget::AssignmentTargetIdentifier(id) => { + // 标识符目标与表达式位置标识符引用同口径预登记:目标为某内置名的 + // 唯一出现处(无读侧引用)时缺预登记会解析成隐式全局新槽,旧值读 + // 与短路判定拿到未预载值。 + if CompileCtx::is_known_builtin(id.name.as_str()) { + let _ = ctx.lookup_or_builtin(id.name.as_str()); + } + } oxide_parser::SimpleAssignmentTarget::StaticMemberExpression(member) => { self.pre_scan_builtin_expr(&member.object, ctx); } diff --git a/crates/oxide_vm/tests/readonly_global_builtin_tests.rs b/crates/oxide_vm/tests/readonly_global_builtin_tests.rs index 6377fb62..4918c96c 100644 --- a/crates/oxide_vm/tests/readonly_global_builtin_tests.rs +++ b/crates/oxide_vm/tests/readonly_global_builtin_tests.rs @@ -1,8 +1,9 @@ //! 不可写全局内置(undefined/NaN/Infinity)写路径拦截测试:写命中全局不可写 //! 内置绑定时编译期拦截——sloppy 静默 no-op(后续读回原值,全局对象不变), -//! strict 抛 TypeError。覆盖简单/复合/更新赋值、声明(var 无初始化与带初始化)、 -//! 解构赋值、for-in/for-of 标识符目标与嵌套函数形态;局部遮蔽绑定(var/let/ -//! 参数)与其他可写内置(Math)不受影响。 +//! strict 抛 TypeError。覆盖简单/复合/更新/逻辑赋值、声明(var 无初始化与 +//! 带初始化)、解构赋值、for-in/for-of 标识符目标与嵌套函数形态;局部遮蔽 +//! 绑定(var/let/参数)与其他可写内置(Math)不受影响。逻辑赋值额外钉住 +//! 短路时序:短路未通过不发生 put(strict 不抛),通过才在写点拦截。 use oxide_compiler::compiler::Compiler; use oxide_types::value::JsValue; @@ -160,6 +161,68 @@ fn postfix_update_returns_old_value() { assert!(r.is_bool() && !r.as_bool(), "Infinity-- 后缀形式应返回旧值 Infinity,实际 {:?}", r); } +// ── 逻辑赋值(||=/&&=/??=)标识符目标:短路时序与 PutValue 对齐——短路未通过 +// 不发生 put(strict 不抛、表达式值 = LHS 旧值),通过才在写点拦截 ── +#[test] +fn logical_assign_sloppy_matrix_keeps_value() { + // 三常量 × 三逻辑符:短路通过者静默 no-op 读回原值,短路未通过者不写。 + let cases: &[(&str, &str)] = &[ + ("undefined ||= 5; '' + undefined", "undefined"), + ("undefined &&= 5; '' + undefined", "undefined"), + ("undefined ??= 5; '' + undefined", "undefined"), + ("NaN ||= 1; '' + NaN", "NaN"), + ("NaN &&= 1; '' + NaN", "NaN"), + ("NaN ??= 1; '' + NaN", "NaN"), + ("Infinity ||= 2; '' + Infinity", "Infinity"), + ("Infinity &&= 2; '' + Infinity", "Infinity"), + ("Infinity ??= 2; '' + Infinity", "Infinity"), + ]; + for &(source, expected) in cases { + let s = eval_str(source); + assert_eq!(s, expected, "sloppy 逻辑赋值命中不可写内置应静默 no-op,读回原值,源码: {source}"); + } +} + +#[test] +fn logical_assign_strict_write_path_throws() { + // 短路通过(||=/??= 遇 falsy/nullish、&&= 遇 truthy)到达写点:put 失败抛 TypeError。 + let sources = [ + "'use strict'; undefined ||= 5", + "'use strict'; undefined ??= 5", + "'use strict'; NaN ||= 1", + "'use strict'; Infinity &&= 2", + ]; + for source in sources { + let result = eval(source); + assert_err_contains(result, "read-only"); + } +} + +#[test] +fn logical_assign_strict_short_circuit_no_throw() { + // 短路未通过不发生 put:strict 不抛错,表达式值 = LHS 旧值。 + let cases: &[(&str, &str)] = &[ + ("'use strict'; '' + (undefined &&= 5)", "undefined"), + ("'use strict'; '' + (NaN &&= 1)", "NaN"), + ("'use strict'; '' + (NaN ??= 1)", "NaN"), + ("'use strict'; '' + (Infinity ||= 2)", "Infinity"), + ("'use strict'; '' + (Infinity ??= 2)", "Infinity"), + ]; + for &(source, expected) in cases { + let s = eval_str(source); + assert_eq!( + s, expected, + "逻辑赋值短路未通过不 put,strict 不应抛错且表达式值 = LHS 旧值,源码: {source}" + ); + } +} + +#[test] +fn logical_or_assign_expression_value_is_rhs() { + let s = eval_str("(undefined ||= 5) + ':' + undefined"); + assert_eq!(s, "5:undefined", "逻辑赋值写点表达式值应为 RHS,槽值保持原值"); +} + // ── 解构赋值目标:命中项静默跳过,strict 抛错 ── #[test] fn destructure_assign_keeps_value() { @@ -192,6 +255,25 @@ fn for_of_target_strict_empty_no_throw() { assert!(r.is_bool() && r.as_bool(), "空集合 for-of 不发生 put,strict 不应抛错,实际 {:?}", r); } +// ── for-in 标识符目标:键写不污染槽值,写仅在 NEXT 产生键后发生 ── +#[test] +fn for_in_target_keeps_value() { + let s = eval_str("for (undefined in {a: 1}) {} '' + undefined"); + assert_eq!(s, "undefined", "sloppy for-in 目标 undefined 应静默 no-op,读回原值"); +} + +#[test] +fn for_in_target_strict_throws_type_error() { + let result = eval("'use strict'; for (undefined in {a: 1}) {}"); + assert_err_contains(result, "read-only"); +} + +#[test] +fn for_in_target_strict_empty_no_throw() { + let r = eval("'use strict'; for (undefined in {}) {} true").unwrap(); + assert!(r.is_bool() && r.as_bool(), "空对象 for-in 不发生 put,strict 不应抛错,实际 {:?}", r); +} + // ── for 头 var 无初始化:不抹预载槽值(NaN 保留) ── #[test] fn for_header_var_no_init_keeps_slot_value() {