fix: 逻辑赋值标识符目标接入不可写全局内置拦截,目标位置内置预登记稳定槽解析

This commit is contained in:
2026-09-10 02:30:09 +08:00
parent 479238dc24
commit eabd0b3608
3 changed files with 116 additions and 18 deletions
+23 -15
View File
@@ -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);
}
}
}
}
+8
View File
@@ -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);
}
@@ -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() {