From a5aab0077f667d44515404ac2f5f6866b399b5e2 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Thu, 10 Sep 2026 05:56:10 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20for-in=20var=20=E5=A4=B4=E6=92=9E?= =?UTF-8?q?=E6=97=A2=E6=9C=89=E7=BB=91=E5=AE=9A=E5=A4=8D=E7=94=A8=E6=A7=BD?= =?UTF-8?q?=E4=BD=8D=E6=B6=88=E9=99=A4=E9=A1=B6=E5=B1=82=E7=BC=96=E8=AF=91?= =?UTF-8?q?=E6=8A=A5=E9=94=99=EF=BC=8C=E8=BF=AD=E4=BB=A3=E9=94=AE=E5=86=99?= =?UTF-8?q?=E4=B8=8D=E5=8F=AF=E5=86=99=E5=85=A8=E5=B1=80=E5=86=85=E7=BD=AE?= =?UTF-8?q?=E6=8E=A5=E5=8F=AA=E8=AF=BB=E6=8B=A6=E6=88=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oxide_emit/src/stmt/iteration/for_in.rs | 23 ++++++++++++++++--- .../oxide_vm/tests/for_in_ordering_tests.rs | 21 +++++++++++++++++ ...lobal_builtin_constant_descriptor_tests.rs | 18 +++++++++++++++ 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/crates/oxide_emit/src/stmt/iteration/for_in.rs b/crates/oxide_emit/src/stmt/iteration/for_in.rs index e800b405..dee29822 100644 --- a/crates/oxide_emit/src/stmt/iteration/for_in.rs +++ b/crates/oxide_emit/src/stmt/iteration/for_in.rs @@ -38,8 +38,25 @@ impl Emitter { let is_const = matches!(decl.kind, VariableDeclarationKind::Const); let name = bi.name.as_str(); let var_reg = ctx.alloc_reg(); - ctx.declare(name, var_reg, decl.kind, is_const)?; - if let Some(&cell_idx) = ctx.captured_bindings.get(name) { + // var 声明:绑定已存在(顶层 var 预声明、GDI 序言预登记 + // builtin 名、同 scope 先前声明)时复用既有槽位,不重复 + // 声明;let/const 同 scope 重复声明仍报错。 + let target_reg = if matches!(decl.kind, VariableDeclarationKind::Var) { + match ctx.declare(name, var_reg, decl.kind, is_const) { + Ok(()) => var_reg, + Err(_) => ctx.lookup(name).unwrap_or(var_reg), + } + } else { + ctx.declare(name, var_reg, decl.kind, is_const)?; + 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)?; + } + } else 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, @@ -50,7 +67,7 @@ impl Emitter { } else { ctx.inst(Inst::new( OpCode::STORE_VAR, - Operand::Reg(var_reg), + Operand::Reg(target_reg), Operand::Reg(key_reg), Operand::None, )); diff --git a/crates/oxide_vm/tests/for_in_ordering_tests.rs b/crates/oxide_vm/tests/for_in_ordering_tests.rs index d7ff938c..1c7e1037 100644 --- a/crates/oxide_vm/tests/for_in_ordering_tests.rs +++ b/crates/oxide_vm/tests/for_in_ordering_tests.rs @@ -79,3 +79,24 @@ fn for_in_still_enumerates_own_keys() { "for-in still enumerates the object's own enumerable keys" ); } + +#[test] +fn for_in_var_head_reuses_existing_var_binding() { + // var 头命中同 scope 已预声明的 var 绑定:复用既有槽位不重复声明, + // 迭代键逐迭代落入该绑定(终值 = 末键)。 + assert_eq!( + eval("var x = 0; for (var x in {a:1,b:2}) {} x === \"b\""), + "true", + "for-in var head reuses the predeclared same-scope var binding" + ); +} + +#[test] +fn for_in_var_head_fresh_name() { + // var 头无其他引用:提升的 var 槽接收迭代键。 + assert_eq!( + eval("for (var x in {a:1}) {} x === \"a\""), + "true", + "for-in var head binds iteration keys into the hoisted var slot" + ); +} diff --git a/crates/oxide_vm/tests/global_builtin_constant_descriptor_tests.rs b/crates/oxide_vm/tests/global_builtin_constant_descriptor_tests.rs index e5d836b9..367ec4de 100644 --- a/crates/oxide_vm/tests/global_builtin_constant_descriptor_tests.rs +++ b/crates/oxide_vm/tests/global_builtin_constant_descriptor_tests.rs @@ -186,3 +186,21 @@ fn function_declaration_builtin_name() { && d.enumerable === false && d.configurable === true", ); } + +#[test] +fn for_in_var_head_builtin_name_compiles_and_keeps_property() { + // 顶层 for-in var 头撞 builtin 名:序言预登记槽复用后可编译, + // 迭代键不触及全局属性(Math 对象保)。 + run_truthy("var m = Math; for (var Math in {a:1,b:2}) {} globalThis.Math === m"); +} + +#[test] +fn for_in_var_head_readonly_builtin_not_clobbered() { + // 顶层 for-in var 头撞三常量:迭代键写命中只读内置拦截—— + // sloppy 静默跳过(镜像槽与声明值均不污染),strict 迭代抛 TypeError。 + run_truthy("var n = NaN; for (var NaN in {a:1,b:2}) {} Number.isNaN(NaN) && Number.isNaN(n)"); + run_truthy( + "var i = Infinity; for (var Infinity in {a:1,b:2}) {} Infinity === i && Infinity === globalThis.Infinity", + ); + run_truthy("\"use strict\"; try { for (var NaN in {a:1}) {} } catch (e) { e instanceof TypeError }"); +}