fix: 不可写全局内置(undefined/NaN/Infinity)寄存器镜像写编译期拦截——sloppy 静默 no-op、strict 抛 TypeError

This commit is contained in:
2026-09-09 23:17:01 +08:00
parent 2457f283fe
commit b77a9322e5
9 changed files with 394 additions and 33 deletions
+19
View File
@@ -134,6 +134,11 @@ const BUILTIN_GLOBALS: &[&str] = &[
"$262",
];
/// 不可写全局内置名:全局对象上的数据属性为 {writable:false, configurable:false},
/// 对它们的 put 永不成功。写路径命中这些名字的全局绑定(非局部遮蔽)时编译期
/// 拦截:sloppy 静默丢弃、strict 抛 TypeError;其余内置名属性可写,写路径放行。
const NON_WRITABLE_GLOBAL_BUILTINS: &[&str] = &["undefined", "NaN", "Infinity"];
pub(crate) struct FieldBuffer {
pub(crate) insts: Vec<Inst>,
pub(crate) labels: Vec<(LabelId, usize)>,
@@ -657,6 +662,20 @@ impl CompileCtx {
false
}
/// 写目标是否为不可写全局内置(undefined/NaN/Infinity 的全局绑定)。
/// 名字在名单内且解析寄存器是全局内置槽(builtin_reg_map 登记),或名字落到
/// 全局作用域绑定(顶层 var 预声明/隐式全局,含继承的全局槽)——局部
/// var/let/参数遮蔽同名时解析到局部寄存器/局部作用域,两条件均不命中。
pub(crate) fn targets_readonly_builtin(&self, name: &str, reg: u32) -> bool {
if !NON_WRITABLE_GLOBAL_BUILTINS.contains(&name) {
return false;
}
if self.scopes.builtin_reg_map.iter().any(|(n, r)| n == name && *r == reg) {
return true;
}
matches!(self.scopes.symbols.lookup_any_binding(name), Some((_, 0)))
}
pub(crate) fn is_known_builtin(name: &str) -> bool {
BUILTIN_GLOBALS.contains(&name)
}
+23
View File
@@ -15,6 +15,8 @@ impl Emitter {
/// # 边界与前提
/// - `op` 必须是 COMPOUND_* 族(rd 兼作 lhs 源,a 为 rhs)。
/// - upvalue / cell 场景忽略 const 语义(共享单元由闭包机制保证)。
/// - 不可写全局内置槽:值照算(表达式值 = 运算结果)但跳过槽写;strict 抛
/// TypeError。局部遮蔽绑定不受影响。
fn emit_compound_identifier_static(
&self, name: &str, op: OpCode, rhs: u32, ctx: &mut CompileCtx,
) -> Result<u32, String> {
@@ -27,6 +29,16 @@ impl Emitter {
// update 写寄存器供下一迭代 fresh 拷贝,不污染本迭代闭包捕获的 cell)。
if ctx.register_update_names.iter().any(|n| n == name) {
if let Some(reg) = ctx.scopes.symbols.lookup_any(name) {
if ctx.targets_readonly_builtin(name, reg) {
if ctx.is_strict {
return self.emit_throw_error("TypeError", "cannot assign to read-only property", ctx);
}
// 全局不可写内置:值照算(表达式值 = 运算结果),跳过槽写。
let val_reg = ctx.alloc_reg();
ctx.inst(Inst::new(OpCode::LOAD_VAR, Operand::Reg(val_reg), Operand::Reg(reg), Operand::None));
ctx.inst(Inst::new(op, Operand::Reg(val_reg), Operand::Reg(rhs), Operand::None));
return Ok(val_reg);
}
ctx.inst(Inst::new(op, Operand::Reg(reg), Operand::Reg(rhs), Operand::None));
return Ok(reg);
}
@@ -72,6 +84,17 @@ impl Emitter {
Ok(val_reg)
} else {
let var_reg = ctx.lookup_or_global(name);
if ctx.targets_readonly_builtin(name, var_reg) {
// 全局不可写内置槽:strict 抛 TypeError(put 失败);sloppy 值照算
// (表达式值 = 运算结果),跳过槽写。
if ctx.is_strict {
return self.emit_throw_error("TypeError", "cannot assign to read-only property", ctx);
}
let val_reg = ctx.alloc_reg();
ctx.inst(Inst::new(OpCode::LOAD_VAR, Operand::Reg(val_reg), Operand::Reg(var_reg), Operand::None));
ctx.inst(Inst::new(op, Operand::Reg(val_reg), Operand::Reg(rhs), Operand::None));
return Ok(val_reg);
}
let is_implicit = ctx.implicit_global_writes.contains(&var_reg);
if is_implicit && ctx.is_strict {
// 严格模式未声明复合写:未解析引用不可 put,值无关抛 ReferenceError。
+17
View File
@@ -126,6 +126,8 @@ impl Emitter {
/// - `const_flag` 为 1 时编译期直接抛 TypeError(值无关),覆盖普通/upvalue/cell 全写路径;
/// 不发射写指令(THROW 后不可达)。
/// - cell 写穿共享单元,无运行时 guard;const 拦截由本入口编译期完成。
/// - 不可写全局内置(undefined/NaN/Infinity)的全局绑定写在此拦截:sloppy 跳过
/// 寄存器写(槽保留入口预载原值),strict 抛 TypeError;局部遮蔽绑定不受影响。
pub(crate) fn emit_identifier_store(&self, name: &str, val_reg: u32, const_flag: u16, ctx: &mut CompileCtx) {
// 循环 update 段的 let/const 循环变量是 per-iteration 可变绑定(CreateMutableBinding),
// 写寄存器而非 cell,且豁免 const 检查(register_update_names 覆盖全部循环头声明名)。
@@ -139,6 +141,13 @@ impl Emitter {
// update 写寄存器供下一迭代 fresh 拷贝,不污染本迭代闭包捕获的 cell)。
if in_loop_update {
if let Some(reg) = ctx.scopes.symbols.lookup_any(name) {
if ctx.targets_readonly_builtin(name, reg) {
// 全局不可写内置:put 永不成功——sloppy 静默跳过,strict 抛错。
if ctx.is_strict {
let _ = self.emit_throw_error("TypeError", "cannot assign to read-only property", ctx);
}
return;
}
ctx.inst(Inst::new(OpCode::STORE_VAR, Operand::Reg(reg), Operand::Reg(val_reg), Operand::Imm(0)));
return;
}
@@ -164,6 +173,14 @@ impl Emitter {
return;
}
let var_reg = ctx.lookup_or_global(name);
if ctx.targets_readonly_builtin(name, var_reg) {
// 全局不可写内置槽:sloppy 跳过寄存器写(槽保留运行入口预载原值,
// 静默 no-op);strict 在 RHS 已求值后抛 TypeError(put 失败)。
if ctx.is_strict {
let _ = self.emit_throw_error("TypeError", "cannot assign to read-only property", ctx);
}
return;
}
let is_implicit = ctx.implicit_global_writes.contains(&var_reg);
if is_implicit && ctx.is_strict {
// 严格模式未声明写:发射 ReferenceError 抛错,跳过寄存器写(值无关)。
+34 -1
View File
@@ -506,6 +506,10 @@ impl Emitter {
/// 标识符自增/自减的静态路径:upvalue / 被捕获 cell 走显式读-增减-写回,
/// 普通槽用 INC_PRE/POST 或 DEC_PRE/POST。返回结果寄存器。
///
/// # 边界与前提
/// - 不可写全局内置槽:值照算(前缀返回新值、后缀返回旧值)但跳过槽写;
/// strict 抛 TypeError。局部遮蔽绑定不受影响。
fn emit_identifier_update_static(
&self, name: &str, update: &oxide_parser::UpdateExpression, ctx: &mut CompileCtx,
) -> Result<u32, String> {
@@ -522,13 +526,24 @@ impl Emitter {
// update 写寄存器供下一迭代 fresh 拷贝,不污染本迭代闭包捕获的 cell)。
if ctx.register_update_names.iter().any(|n| n == name) {
if let Some(reg) = ctx.scopes.symbols.lookup_any(name) {
let result_reg = ctx.alloc_reg();
let op = match (update.operator, update.prefix) {
(UpdateOperator::Increment, true) => OpCode::INC_PRE,
(UpdateOperator::Increment, false) => OpCode::INC_POST,
(UpdateOperator::Decrement, true) => OpCode::DEC_PRE,
(UpdateOperator::Decrement, false) => OpCode::DEC_POST,
};
if ctx.targets_readonly_builtin(name, reg) {
if ctx.is_strict {
return self.emit_throw_error("TypeError", "cannot assign to read-only property", ctx);
}
// 全局不可写内置:值照算(前缀返回新值、后缀返回旧值),跳过槽写。
let tmp_reg = ctx.alloc_reg();
ctx.inst(Inst::new(OpCode::LOAD_VAR, Operand::Reg(tmp_reg), Operand::Reg(reg), Operand::None));
let result_reg = ctx.alloc_reg();
ctx.inst(Inst::new(op, Operand::Reg(tmp_reg), Operand::Reg(result_reg), Operand::Reg(result_reg)));
return Ok(result_reg);
}
let result_reg = ctx.alloc_reg();
ctx.inst(Inst::new(op, Operand::Reg(reg), Operand::Reg(result_reg), Operand::Reg(result_reg)));
return Ok(result_reg);
}
@@ -599,6 +614,24 @@ impl Emitter {
Ok(if update.prefix { new_reg } else { old_reg })
} else {
let var_reg = ctx.lookup_or_global(name);
if ctx.targets_readonly_builtin(name, var_reg) {
// 全局不可写内置槽:strict 抛 TypeError(put 失败);sloppy 值照算
// (前缀返回新值、后缀返回旧值),跳过槽写。
if ctx.is_strict {
return self.emit_throw_error("TypeError", "cannot assign to read-only property", ctx);
}
let tmp_reg = ctx.alloc_reg();
ctx.inst(Inst::new(OpCode::LOAD_VAR, Operand::Reg(tmp_reg), Operand::Reg(var_reg), Operand::None));
let result_reg = ctx.alloc_reg();
let op = match (update.operator, update.prefix) {
(UpdateOperator::Increment, true) => OpCode::INC_PRE,
(UpdateOperator::Increment, false) => OpCode::INC_POST,
(UpdateOperator::Decrement, true) => OpCode::DEC_PRE,
(UpdateOperator::Decrement, false) => OpCode::DEC_POST,
};
ctx.inst(Inst::new(op, Operand::Reg(tmp_reg), Operand::Reg(result_reg), Operand::Reg(result_reg)));
return Ok(result_reg);
}
let is_implicit = ctx.implicit_global_writes.contains(&var_reg);
if is_implicit && ctx.is_strict {
// 严格模式未声明更新写:值无关抛 ReferenceError。
+36
View File
@@ -50,6 +50,10 @@ impl Emitter {
/// 绑定单个标识符到槽位。`fresh_cell` 为 true 时对被捕获绑定用 MAKE_CELL_FRESH
/// (循环每迭代绑定:无条件新建 cell,本迭代闭包捕获新 cell)。
///
/// # 边界与前提
/// - 声明撞全局不可写内置(undefined/NaN/Infinity):sloppy 静默跳过写(槽保留
/// 入口预载原值),strict 抛 TypeError;局部遮蔽绑定不受影响。
pub(crate) fn emit_bind_target(
&self, name: &str, src_reg: u32, kind: VariableDeclarationKind, is_const: bool, fresh_cell: bool,
ctx: &mut CompileCtx,
@@ -74,6 +78,14 @@ impl Emitter {
var_reg
}
};
if ctx.targets_readonly_builtin(name, target_reg) {
// 声明撞全局不可写内置:声明不更新既有全局绑定——sloppy 静默跳过
// (槽保留入口预载原值),strict 在初始化求值后抛 TypeError。
if ctx.is_strict {
self.emit_throw_error("TypeError", "cannot assign to read-only property", ctx)?;
}
return Ok(());
}
if let Some(&cell_idx) = ctx.captured_bindings.get(name) {
let op = if fresh_cell { OpCode::MAKE_CELL_FRESH } else { OpCode::MAKE_CELL };
ctx.inst(Inst::new(op, Operand::Reg(src_reg), Operand::Imm(cell_idx as u16), Operand::None));
@@ -115,6 +127,14 @@ impl Emitter {
self.emit_identifier_tdz_guard(name, ctx)?;
self.emit_const_write_guard(name, ctx)?;
let var_reg = ctx.lookup_or_global(name);
if ctx.targets_readonly_builtin(name, var_reg) {
// 全局不可写内置:sloppy 静默跳过写(槽保留入口预载原值);
// strict 抛 TypeError(put 失败)。
if ctx.is_strict {
self.emit_throw_error("TypeError", "cannot assign to read-only property", ctx)?;
}
return Ok(());
}
let is_implicit = ctx.implicit_global_writes.contains(&var_reg);
if is_implicit && ctx.is_strict {
// 严格模式未声明写:抛 ReferenceError,跳过寄存器写(值无关)。
@@ -309,6 +329,14 @@ impl Emitter {
self.emit_identifier_tdz_guard(name, ctx)?;
self.emit_const_write_guard(name, ctx)?;
let var_reg = ctx.lookup_or_global(name);
if ctx.targets_readonly_builtin(name, var_reg) {
// 全局不可写内置:sloppy 静默跳过写(槽保留入口预载原值);
// strict 抛 TypeError(put 失败)。
if ctx.is_strict {
self.emit_throw_error("TypeError", "cannot assign to read-only property", ctx)?;
}
return Ok(());
}
let is_implicit = ctx.implicit_global_writes.contains(&var_reg);
if is_implicit && ctx.is_strict {
// 严格模式未声明写:抛 ReferenceError,跳过寄存器写(值无关)。
@@ -385,6 +413,14 @@ impl Emitter {
self.emit_identifier_tdz_guard(name, ctx)?;
self.emit_const_write_guard(name, ctx)?;
let var_reg = ctx.lookup_or_global(name);
if ctx.targets_readonly_builtin(name, var_reg) {
// 全局不可写内置:sloppy 静默跳过该属性写(其余属性继续赋值);
// strict 抛 TypeError(put 失败)。
if ctx.is_strict {
self.emit_throw_error("TypeError", "cannot assign to read-only property", ctx)?;
}
continue;
}
let is_implicit = ctx.implicit_global_writes.contains(&var_reg);
if is_implicit && ctx.is_strict {
// 严格模式未声明写:抛 ReferenceError,跳过寄存器写(值无关)。
+23 -16
View File
@@ -64,12 +64,15 @@ impl Emitter {
for name in &names {
if ctx.captured_bindings.contains_key(name) {
if let Some(reg) = ctx.scopes.symbols.lookup_any(name) {
ctx.inst(Inst::new(
OpCode::STORE_VAR,
Operand::Reg(reg),
Operand::Reg(val_reg),
Operand::None,
));
// 全局不可写内置的捕获写被声明路径拦截,同步写一并跳过。
if !ctx.targets_readonly_builtin(name, reg) {
ctx.inst(Inst::new(
OpCode::STORE_VAR,
Operand::Reg(reg),
Operand::Reg(val_reg),
Operand::None,
));
}
}
}
}
@@ -78,21 +81,25 @@ impl Emitter {
let tmp = ctx.alloc_reg();
ctx.inst(Inst::load_const(Operand::Reg(tmp), idx));
let var_reg = ctx.alloc_reg();
let target_reg = if matches!(decl.kind, VariableDeclarationKind::Var) {
// 无初始化 var 是纯声明而非赋值:绑定已预先存在(提升引用或
// 先前写入)时保留槽值,仅首次声明把 undefined 物化进槽。
let (target_reg, already_bound) = if matches!(decl.kind, VariableDeclarationKind::Var) {
match ctx.declare(bi.name.as_str(), var_reg, decl.kind, is_const) {
Ok(()) => var_reg,
Err(_) => ctx.lookup(bi.name.as_str()).unwrap_or(var_reg),
Ok(()) => (var_reg, false),
Err(_) => (ctx.lookup(bi.name.as_str()).unwrap_or(var_reg), true),
}
} else {
ctx.declare(bi.name.as_str(), var_reg, decl.kind, is_const)?;
var_reg
(var_reg, false)
};
ctx.inst(Inst::new(
OpCode::STORE_VAR,
Operand::Reg(target_reg),
Operand::Reg(tmp),
Operand::None,
));
if !already_bound {
ctx.inst(Inst::new(
OpCode::STORE_VAR,
Operand::Reg(target_reg),
Operand::Reg(tmp),
Operand::None,
));
}
ctx.init_var(bi.name.as_str());
}
// 记录 let/const 循环头声明名(update 段写豁免所需)与被捕获的绑定
+21 -8
View File
@@ -64,15 +64,28 @@ impl Emitter {
ForStatementLeft::AssignmentTargetIdentifier(id_ref) => {
let name = id_ref.name.as_str();
let var_reg = ctx.lookup_or_global(name);
let is_implicit = ctx.implicit_global_writes.contains(&var_reg);
// 未声明写仅在本迭代实际产生键后才发生(for-in NEXT 之后),空集合不抛。
if is_implicit && ctx.is_strict {
// 严格模式未声明写:抛 ReferenceError,跳过寄存器写(值无关)。
self.emit_strict_undeclared_write(name, ctx)?;
// 写仅在本迭代实际产生键后发生(for-in NEXT 之后),空集合不抛。
if ctx.targets_readonly_builtin(name, var_reg) {
// 全局不可写内置:sloppy 静默跳过写(槽保留入口预载原值);
// strict 在本迭代抛 TypeError(put 失败)。
if ctx.is_strict {
self.emit_throw_error("TypeError", "cannot assign to read-only property", ctx)?;
}
} else {
ctx.inst(Inst::new(OpCode::STORE_VAR, Operand::Reg(var_reg), Operand::Reg(key_reg), Operand::None));
if is_implicit {
self.emit_implicit_global_write(name, var_reg, ctx);
let is_implicit = ctx.implicit_global_writes.contains(&var_reg);
if is_implicit && ctx.is_strict {
// 严格模式未声明写:抛 ReferenceError,跳过寄存器写(值无关)。
self.emit_strict_undeclared_write(name, ctx)?;
} else {
ctx.inst(Inst::new(
OpCode::STORE_VAR,
Operand::Reg(var_reg),
Operand::Reg(key_reg),
Operand::None,
));
if is_implicit {
self.emit_implicit_global_write(name, var_reg, ctx);
}
}
}
}
+21 -8
View File
@@ -108,15 +108,28 @@ impl Emitter {
ForStatementLeft::AssignmentTargetIdentifier(id_ref) => {
let name = id_ref.name.as_str();
let var_reg = ctx.lookup_or_global(name);
let is_implicit = ctx.implicit_global_writes.contains(&var_reg);
// 未声明写仅在本迭代实际产生值后才发生(for-of NEXT 之后),空集合不抛。
if is_implicit && ctx.is_strict {
// 严格模式未声明写:抛 ReferenceError,跳过寄存器写(值无关)。
self.emit_strict_undeclared_write(name, ctx)?;
// 写仅在本迭代实际产生值后发生(for-of NEXT 之后),空集合不抛。
if ctx.targets_readonly_builtin(name, var_reg) {
// 全局不可写内置:sloppy 静默跳过写(槽保留入口预载原值);
// strict 在本迭代抛 TypeError(put 失败)。
if ctx.is_strict {
self.emit_throw_error("TypeError", "cannot assign to read-only property", ctx)?;
}
} else {
ctx.inst(Inst::new(OpCode::STORE_VAR, Operand::Reg(var_reg), Operand::Reg(val_reg), Operand::None));
if is_implicit {
self.emit_implicit_global_write(name, var_reg, ctx);
let is_implicit = ctx.implicit_global_writes.contains(&var_reg);
if is_implicit && ctx.is_strict {
// 严格模式未声明写:抛 ReferenceError,跳过寄存器写(值无关)。
self.emit_strict_undeclared_write(name, ctx)?;
} else {
ctx.inst(Inst::new(
OpCode::STORE_VAR,
Operand::Reg(var_reg),
Operand::Reg(val_reg),
Operand::None,
));
if is_implicit {
self.emit_implicit_global_write(name, var_reg, ctx);
}
}
}
}
@@ -0,0 +1,200 @@
//! 不可写全局内置(undefined/NaN/Infinity)写路径拦截测试:写命中全局不可写
//! 内置绑定时编译期拦截——sloppy 静默 no-op(后续读回原值,全局对象不变),
//! strict 抛 TypeError。覆盖简单/复合/更新赋值、声明(var 无初始化与带初始化)、
//! 解构赋值、for-in/for-of 标识符目标与嵌套函数形态;局部遮蔽绑定(var/let/
//! 参数)与其他可写内置(Math)不受影响。
use oxide_compiler::compiler::Compiler;
use oxide_types::value::JsValue;
use oxide_vm::vm::Vm;
fn eval(source: &str) -> Result<JsValue, String> {
let allocator = oxide_parser::Allocator::default();
let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?;
let module = Compiler::new().compile(&program).map_err(|e| format!("Compile error: {}", e))?;
let mut vm = Vm::new();
vm.run(&module)
}
fn eval_many(lines: &[&str]) -> Result<JsValue, String> {
eval(&lines.join("; "))
}
fn assert_err_contains(result: Result<JsValue, String>, expected: &str) {
match result {
Ok(_) => panic!("expected error containing '{}', got Ok", expected),
Err(e) => assert!(e.contains(expected), "expected error containing '{}', got: {}", expected, e),
}
}
/// 求值并把结果按字符串提取(`JsValue` Display 不携带字符串内容)。
fn eval_str(source: &str) -> String {
let allocator = oxide_parser::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(&module).expect("run");
vm.lookup_str(result).unwrap_or_else(|| format!("{result}"))
}
// ── sloppy 顶层:三个不可写内置赋值后读回原值(静默 no-op) ──
#[test]
fn sloppy_assign_undefined_silently_keeps_value() {
let s = eval_str("undefined = 5; '' + undefined");
assert_eq!(s, "undefined", "sloppy 顶层 undefined 赋值应静默 no-op,读回原值");
}
#[test]
fn sloppy_assign_nan_silently_keeps_value() {
let s = eval_str("NaN = 1; '' + NaN");
assert_eq!(s, "NaN", "sloppy 顶层 NaN 赋值应静默 no-op,读回原值");
}
#[test]
fn sloppy_assign_infinity_silently_keeps_value() {
let s = eval_str("Infinity = 2; '' + Infinity");
assert_eq!(s, "Infinity", "sloppy 顶层 Infinity 赋值应静默 no-op,读回原值");
}
// ── strict 顶层:赋值抛 TypeError(不可写属性 put 失败) ──
#[test]
fn strict_assign_undefined_throws_type_error() {
let result = eval("'use strict'; undefined = 5");
assert_err_contains(result, "read-only");
}
#[test]
fn strict_assign_nan_throws_type_error() {
let result = eval("'use strict'; NaN = 1");
assert_err_contains(result, "read-only");
}
// ── 可写内置(Math)重定义放行:不得被过度拦截 ──
#[test]
fn writable_builtin_reassignment_still_works() {
let r = eval("Math = 5; Math === 5").unwrap();
assert!(r.is_bool() && r.as_bool(), "Math 可写属性重定义应保持可用,实际 {:?}", r);
}
// ── var 无初始化:声明不抹槽值(保留内置原值) ──
#[test]
fn var_no_init_undefined_keeps_value() {
let s = eval_str("var undefined; '' + undefined");
assert_eq!(s, "undefined", "顶层 var undefined 无初始化应保留原值");
}
#[test]
fn var_no_init_nan_keeps_value() {
let s = eval_str("var NaN; '' + NaN");
assert_eq!(s, "NaN", "顶层 var NaN 无初始化应保留原值");
}
// ── 带初始化 var 撞全局不可写内置:sloppy 静默、strict 抛错 ──
#[test]
fn var_with_init_sloppy_silently_keeps_value() {
let s = eval_str("var undefined = 3; '' + undefined");
assert_eq!(s, "undefined", "sloppy var undefined = 3 应静默 no-op,读回原值");
}
#[test]
fn var_with_init_strict_throws_type_error() {
let result = eval("'use strict'; var undefined = 3");
assert_err_contains(result, "read-only");
}
// ── 函数内无遮蔽写:命中继承的全局槽,sloppy 静默、strict 抛错 ──
#[test]
fn function_scoped_write_silently_keeps_value() {
let s = eval_str("function g(){ undefined = 1; return undefined; } '' + g()");
assert_eq!(s, "undefined", "函数内未遮蔽的 undefined 写应静默 no-op,读回原值");
}
#[test]
fn function_scoped_write_strict_throws_type_error() {
let r = eval_many(&[
"var threw = false",
"function g(){ 'use strict'; try { undefined = 1 } catch (e) { threw = true } }",
"g()",
"threw",
])
.unwrap();
assert!(r.is_bool() && r.as_bool(), "严格函数内 undefined 写应抛 TypeError,实际 {:?}", r);
}
// ── 全局对象属性不变:寄存器镜像写不得穿透到 global object ──
#[test]
fn global_object_property_untouched() {
let r = eval("undefined = 5; globalThis.undefined === undefined").unwrap();
assert!(r.is_bool() && r.as_bool(), "寄存器镜像写不应修改全局对象属性,实际 {:?}", r);
}
// ── 局部遮蔽绑定保持可写(var/参数同名不命中全局拦截) ──
#[test]
fn local_var_shadow_writable() {
let r = eval("function g(){ var undefined = 3; undefined = 5; return undefined; } g()").unwrap();
assert!(r.is_int() && r.as_int() == 5, "函数内 var undefined 局部遮蔽应可写,实际 {:?}", r);
}
#[test]
fn param_shadow_writable() {
let r = eval("function g(undefined){ undefined = 1; return undefined; } g(9)").unwrap();
assert!(r.is_int() && r.as_int() == 1, "参数名 undefined 局部遮蔽应可写,实际 {:?}", r);
}
// ── 复合/更新赋值:槽值保留,表达式值按规范计算 ──
#[test]
fn compound_assign_keeps_value() {
let s = eval_str("undefined += 1; '' + undefined");
assert_eq!(s, "undefined", "sloppy undefined += 1 应静默 no-op,读回原值");
}
#[test]
fn update_assign_keeps_value() {
let s = eval_str("undefined++; '' + undefined");
assert_eq!(s, "undefined", "sloppy undefined++ 应静默 no-op,读回原值");
}
#[test]
fn postfix_update_returns_old_value() {
let r = eval("var v = Infinity--; isFinite(v)").unwrap();
assert!(r.is_bool() && !r.as_bool(), "Infinity-- 后缀形式应返回旧值 Infinity,实际 {:?}", r);
}
// ── 解构赋值目标:命中项静默跳过,strict 抛错 ──
#[test]
fn destructure_assign_keeps_value() {
let s = eval_str("({undefined} = {undefined: 9}); '' + undefined");
assert_eq!(s, "undefined", "sloppy 解构赋值命中 undefined 应静默 no-op,读回原值");
}
#[test]
fn destructure_assign_strict_throws_type_error() {
let result = eval("'use strict'; ({undefined} = {undefined: 9})");
assert_err_contains(result, "read-only");
}
// ── for-in/for-of 标识符目标:迭代写不污染槽值 ──
#[test]
fn for_of_target_keeps_value() {
let s = eval_str("for (undefined of [1, 2]) {} '' + undefined");
assert_eq!(s, "undefined", "sloppy for-of 目标 undefined 应静默 no-op,读回原值");
}
#[test]
fn for_of_target_strict_throws_type_error() {
let result = eval("'use strict'; for (undefined of [1]) {}");
assert_err_contains(result, "read-only");
}
#[test]
fn for_of_target_strict_empty_no_throw() {
let r = eval("'use strict'; for (undefined of []) {} true").unwrap();
assert!(r.is_bool() && r.as_bool(), "空集合 for-of 不发生 put,strict 不应抛错,实际 {:?}", r);
}
// ── for 头 var 无初始化:不抹预载槽值(NaN 保留) ──
#[test]
fn for_header_var_no_init_keeps_slot_value() {
let s = eval_str("for (var NaN; false;) {} '' + NaN");
assert_eq!(s, "NaN", "for 头 var NaN 无初始化应保留槽值");
}