From b77a9322e5cd3c7bd9b6ee28d0b173c01332b19c Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Wed, 9 Sep 2026 23:17:01 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=B8=8D=E5=8F=AF=E5=86=99=E5=85=A8?= =?UTF-8?q?=E5=B1=80=E5=86=85=E7=BD=AE=EF=BC=88undefined/NaN/Infinity?= =?UTF-8?q?=EF=BC=89=E5=AF=84=E5=AD=98=E5=99=A8=E9=95=9C=E5=83=8F=E5=86=99?= =?UTF-8?q?=E7=BC=96=E8=AF=91=E6=9C=9F=E6=8B=A6=E6=88=AA=E2=80=94=E2=80=94?= =?UTF-8?q?sloppy=20=E9=9D=99=E9=BB=98=20no-op=E3=80=81strict=20=E6=8A=9B?= =?UTF-8?q?=20TypeError?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_emit/src/emit.rs | 19 ++ crates/oxide_emit/src/expr/assignment.rs | 23 ++ crates/oxide_emit/src/expr/identifier.rs | 17 ++ crates/oxide_emit/src/expr/operator/mod.rs | 35 ++- crates/oxide_emit/src/shared/binding.rs | 36 ++++ crates/oxide_emit/src/stmt/iteration/for_.rs | 39 ++-- .../oxide_emit/src/stmt/iteration/for_in.rs | 29 ++- .../oxide_emit/src/stmt/iteration/for_of.rs | 29 ++- .../tests/readonly_global_builtin_tests.rs | 200 ++++++++++++++++++ 9 files changed, 394 insertions(+), 33 deletions(-) create mode 100644 crates/oxide_vm/tests/readonly_global_builtin_tests.rs diff --git a/crates/oxide_emit/src/emit.rs b/crates/oxide_emit/src/emit.rs index 774232e0..f89b44cc 100644 --- a/crates/oxide_emit/src/emit.rs +++ b/crates/oxide_emit/src/emit.rs @@ -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, 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) } diff --git a/crates/oxide_emit/src/expr/assignment.rs b/crates/oxide_emit/src/expr/assignment.rs index fd7b0672..f3310c05 100644 --- a/crates/oxide_emit/src/expr/assignment.rs +++ b/crates/oxide_emit/src/expr/assignment.rs @@ -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 { @@ -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。 diff --git a/crates/oxide_emit/src/expr/identifier.rs b/crates/oxide_emit/src/expr/identifier.rs index b3358046..a7559398 100644 --- a/crates/oxide_emit/src/expr/identifier.rs +++ b/crates/oxide_emit/src/expr/identifier.rs @@ -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 抛错,跳过寄存器写(值无关)。 diff --git a/crates/oxide_emit/src/expr/operator/mod.rs b/crates/oxide_emit/src/expr/operator/mod.rs index 545b8cdb..abae1d3a 100644 --- a/crates/oxide_emit/src/expr/operator/mod.rs +++ b/crates/oxide_emit/src/expr/operator/mod.rs @@ -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 { @@ -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。 diff --git a/crates/oxide_emit/src/shared/binding.rs b/crates/oxide_emit/src/shared/binding.rs index 070fcd4f..e1218dc5 100644 --- a/crates/oxide_emit/src/shared/binding.rs +++ b/crates/oxide_emit/src/shared/binding.rs @@ -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,跳过寄存器写(值无关)。 diff --git a/crates/oxide_emit/src/stmt/iteration/for_.rs b/crates/oxide_emit/src/stmt/iteration/for_.rs index ddbc2e79..3c0dba69 100644 --- a/crates/oxide_emit/src/stmt/iteration/for_.rs +++ b/crates/oxide_emit/src/stmt/iteration/for_.rs @@ -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 段写豁免所需)与被捕获的绑定 diff --git a/crates/oxide_emit/src/stmt/iteration/for_in.rs b/crates/oxide_emit/src/stmt/iteration/for_in.rs index 354c7422..e800b405 100644 --- a/crates/oxide_emit/src/stmt/iteration/for_in.rs +++ b/crates/oxide_emit/src/stmt/iteration/for_in.rs @@ -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); + } } } } diff --git a/crates/oxide_emit/src/stmt/iteration/for_of.rs b/crates/oxide_emit/src/stmt/iteration/for_of.rs index 8712e7db..1028b628 100644 --- a/crates/oxide_emit/src/stmt/iteration/for_of.rs +++ b/crates/oxide_emit/src/stmt/iteration/for_of.rs @@ -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); + } } } } diff --git a/crates/oxide_vm/tests/readonly_global_builtin_tests.rs b/crates/oxide_vm/tests/readonly_global_builtin_tests.rs new file mode 100644 index 00000000..6377fb62 --- /dev/null +++ b/crates/oxide_vm/tests/readonly_global_builtin_tests.rs @@ -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 { + 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 { + eval(&lines.join("; ")) +} + +fn assert_err_contains(result: Result, 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 无初始化应保留槽值"); +}