From da5e6d13bac70badf0f802ea198df925d394c8d1 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Mon, 14 Sep 2026 23:58:10 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20for-in/of=20=E8=AF=8D=E6=B3=95=E5=A4=B4?= =?UTF-8?q?=E5=BB=BA=E7=8B=AC=E7=AB=8B=20TDZ=20=E7=8E=AF=E5=A2=83=EF=BC=8C?= =?UTF-8?q?=E6=8D=95=E8=8E=B7=E4=B8=8E=E5=8F=B3=E5=80=BC=E7=9B=B4=E8=AF=BB?= =?UTF-8?q?=E5=BD=92=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oxide_emit/src/stmt/iteration/for_in.rs | 104 ++++++++++++++++++ .../oxide_emit/src/stmt/iteration/for_of.rs | 23 ++++ crates/oxide_vm/src/dispatch/call.rs | 11 +- .../oxide_vm/tests/for_in_head_lex_tests.rs | 97 ++++++++++++++++ 4 files changed, 231 insertions(+), 4 deletions(-) create mode 100644 crates/oxide_vm/tests/for_in_head_lex_tests.rs diff --git a/crates/oxide_emit/src/stmt/iteration/for_in.rs b/crates/oxide_emit/src/stmt/iteration/for_in.rs index 8a6ae7e9..1518f329 100644 --- a/crates/oxide_emit/src/stmt/iteration/for_in.rs +++ b/crates/oxide_emit/src/stmt/iteration/for_in.rs @@ -1,4 +1,10 @@ //! for-in 语句 emit:`emit_for_in_statement` 遍历可枚举属性名。 +//! +//! 词法头(let/const/using)按规范建模为独立环境:右值区头名绑定未初始化 +//! cell(读与捕获均抛 ReferenceError),体区绑定每迭代 fresh cell;捕获映射 +//! 于语句结束后恢复,for-of 经同一套覆盖复用。 + +use std::collections::HashSet; use crate::{CompileCtx, Emitter}; use oxide_bytecode::opcode::OpCode; @@ -6,7 +12,93 @@ use oxide_ir::inst::Inst; use oxide_ir::operand::Operand; use oxide_parser::{ForStatementLeft, Statement, VariableDeclarationKind}; +/// for-in/for-of 词法头的捕获映射覆盖:每头名一条记录,依次为名字、旧条目 +/// (无旧条目为 None)、右值区 TDZ cell、体区 fresh cell(切换后填入)。 +pub(crate) struct ForHeadEnv { + entries: Vec<(String, Option, u8, u8)>, +} + impl Emitter { + /// 收集 for-in/for-of 词法声明头(let/const/using,含解构 pattern 叶)的 + /// 头名(排序保证稳定);var 头与赋值头无 TDZ 环境,返回 None。 + pub(crate) fn collect_for_head_lexical_names(&self, left: &ForStatementLeft) -> Option> { + let ForStatementLeft::VariableDeclaration(decl) = left else { + return None; + }; + if matches!(decl.kind, VariableDeclarationKind::Var) { + return None; + } + let mut names = HashSet::new(); + for d in &decl.declarations { + self.collect_binding_pattern_names(&d.id, &mut names); + } + let mut v: Vec = names.into_iter().collect(); + v.sort(); + Some(v) + } + + /// 把词法头名覆盖到捕获映射(右值区阶段):保存旧条目、分配 TDZ cell 并 + /// 发射未初始化 MAKE_CELL、头名映射 TDZ cell。右值区内对头名的直读与 + /// 该区内创建闭包的捕获均命中未初始化 cell(读抛 ReferenceError)。 + /// + /// # 步骤 + /// 1. 逐头名:移除旧条目并记入恢复清单。 + /// 2. 按现有最大 cell 索引顺序分配 TDZ cell(与类 brand 合成 cell 同口径)。 + /// 3. 发射未初始化 MAKE_CELL 并覆盖映射。 + /// + /// # 副作用 + /// - 捕获映射被改动;须经 `switch_for_head_env_to_body` 与 + /// `restore_for_head_env` 成对收尾,防头名泄漏进兄弟语句的捕获映射。 + pub(crate) fn begin_for_head_env(&self, names: Vec, ctx: &mut CompileCtx) -> Result { + let mut entries = Vec::with_capacity(names.len()); + let mut next = ctx.captured_bindings.values().copied().max().map_or(0, |m| m.saturating_add(1)); + for name in names { + let saved = ctx.captured_bindings.remove(&name); + let tdz_idx = next; + next = next.saturating_add(1); + ctx.captured_bindings.insert(name.clone(), tdz_idx); + // 未初始化标志走 b 槽:a 槽 Imm 的高字节会覆写 b 槽,故标志并入 + // 立即数高字节,dispatch 侧按 a/b 分槽取回。 + let undef_reg = self.emit_undefined(ctx); + ctx.inst(Inst::new( + OpCode::MAKE_CELL, + Operand::Reg(undef_reg), + Operand::Imm(tdz_idx as u16 | 0x0100), + Operand::None, + )); + entries.push((name, saved, tdz_idx, 0)); + } + Ok(ForHeadEnv { entries }) + } + + /// 右值区求值完毕后把覆盖从 TDZ cell 切到体区 fresh cell:每头名分配新 + /// cell 并改写映射,头发射捕获臂与体区读/捕获自然命中(每迭代 + /// MAKE_CELL_FRESH 建新 cell,本迭代闭包捕获新 cell)。 + pub(crate) fn switch_for_head_env_to_body(&self, env: &mut ForHeadEnv, ctx: &mut CompileCtx) { + let mut next = ctx.captured_bindings.values().copied().max().map_or(0, |m| m.saturating_add(1)); + for (name, _, _, body_idx) in &mut env.entries { + let idx = next; + next = next.saturating_add(1); + *body_idx = idx; + ctx.captured_bindings.insert(name.clone(), idx); + } + } + + /// 语句收尾恢复捕获映射:旧条目放回(无旧条目则移除),头名不进入后续 + /// 兄弟语句的捕获判定。 + pub(crate) fn restore_for_head_env(&self, env: ForHeadEnv, ctx: &mut CompileCtx) { + for (name, saved, _, _) in env.entries { + match saved { + Some(old) => { + ctx.captured_bindings.insert(name, old); + } + None => { + ctx.captured_bindings.remove(&name); + } + } + } + } + pub(crate) fn emit_for_in_statement(&self, stmt: &Statement, ctx: &mut CompileCtx) -> Result, String> { let Statement::ForInStatement(fi) = stmt else { return Ok(None); @@ -14,7 +106,16 @@ impl Emitter { ctx.push_scope(); let start_label = ctx.next_label_id(); let end_label = ctx.next_label_id(); + // 词法头 TDZ 环境:右值区头名绑定未初始化 cell,右值区求值后切到 + // 每迭代 fresh cell,体发射后恢复捕获映射。 + let mut head_env = self + .collect_for_head_lexical_names(&fi.left) + .map(|names| self.begin_for_head_env(names, ctx)) + .transpose()?; let obj_reg = self.emit_expression(&fi.right, ctx)?; + if let Some(env) = &mut head_env { + self.switch_for_head_env_to_body(env, ctx); + } ctx.inst(Inst::new(OpCode::FOR_IN_INIT, Operand::None, Operand::Reg(obj_reg), Operand::None)); ctx.labels.set_label_pos(start_label, ctx.insts.len()); ctx.push_loop(end_label, start_label, crate::emit_ctx::LoopKind::ForIn); @@ -124,6 +225,9 @@ impl Emitter { _ => return Err("unsupported for-in left-hand side".into()), } let body_result = self.emit_statement(&fi.body, ctx)?; + if let Some(env) = head_env { + self.restore_for_head_env(env, ctx); + } ctx.inst(Inst::jmp(start_label)); ctx.labels.set_label_pos(end_label, ctx.insts.len()); ctx.inst(Inst::new(OpCode::FOR_IN_CLEANUP, Operand::None, Operand::None, Operand::None)); diff --git a/crates/oxide_emit/src/stmt/iteration/for_of.rs b/crates/oxide_emit/src/stmt/iteration/for_of.rs index 349f12f4..b4486261 100644 --- a/crates/oxide_emit/src/stmt/iteration/for_of.rs +++ b/crates/oxide_emit/src/stmt/iteration/for_of.rs @@ -26,7 +26,16 @@ impl Emitter { ctx.push_scope(); let start_label = ctx.next_label_id(); let end_label = ctx.next_label_id(); + // 词法头 TDZ 环境:右值区头名绑定未初始化 cell,右值区求值后切到 + // 每迭代 fresh cell,体发射后恢复捕获映射(同 for-in 覆盖)。 + let mut head_env = self + .collect_for_head_lexical_names(&fo.left) + .map(|names| self.begin_for_head_env(names, ctx)) + .transpose()?; let iter_src_reg = self.emit_expression(&fo.right, ctx)?; + if let Some(env) = &mut head_env { + self.switch_for_head_env_to_body(env, ctx); + } ctx.inst(Inst::new(OpCode::FOR_OF_INIT, Operand::None, Operand::Reg(iter_src_reg), Operand::None)); ctx.labels.set_label_pos(start_label, ctx.insts.len()); ctx.push_loop(end_label, start_label, crate::emit_ctx::LoopKind::ForOf); @@ -38,6 +47,9 @@ impl Emitter { ctx.inst(Inst::new(OpCode::FOR_OF_NEXT, Operand::Reg(val_reg), Operand::None, Operand::None)); self.emit_for_of_left_assignment(&fo.left, val_reg, ctx)?; let body_result = self.emit_statement(&fo.body, ctx)?; + if let Some(env) = head_env { + self.restore_for_head_env(env, ctx); + } ctx.inst(Inst::jmp(start_label)); ctx.labels.set_label_pos(end_label, ctx.insts.len()); ctx.inst(Inst::new(OpCode::FOR_OF_CLOSE, Operand::None, Operand::None, Operand::None)); @@ -62,7 +74,15 @@ impl Emitter { ctx.push_scope(); let start_label = ctx.next_label_id(); let end_label = ctx.next_label_id(); + // 词法头 TDZ 环境:同同步 for-of(右值区未初始化 cell → 体区 fresh cell)。 + let mut head_env = self + .collect_for_head_lexical_names(&fo.left) + .map(|names| self.begin_for_head_env(names, ctx)) + .transpose()?; let iter_src_reg = self.emit_expression(&fo.right, ctx)?; + if let Some(env) = &mut head_env { + self.switch_for_head_env_to_body(env, ctx); + } ctx.inst(Inst::new( OpCode::FOR_AWAIT_OF_INIT, Operand::None, @@ -89,6 +109,9 @@ impl Emitter { ctx.inst(Inst::new(OpCode::FOR_OF_NEXT, Operand::Reg(val_reg), Operand::None, Operand::None)); self.emit_for_of_left_assignment(&fo.left, val_reg, ctx)?; let body_result = self.emit_statement(&fo.body, ctx)?; + if let Some(env) = head_env { + self.restore_for_head_env(env, ctx); + } ctx.inst(Inst::jmp(start_label)); ctx.labels.set_label_pos(end_label, ctx.insts.len()); ctx.inst(Inst::new(OpCode::FOR_AWAIT_OF_CLOSE, Operand::None, Operand::None, Operand::None)); diff --git a/crates/oxide_vm/src/dispatch/call.rs b/crates/oxide_vm/src/dispatch/call.rs index 7239a70c..4c389970 100644 --- a/crates/oxide_vm/src/dispatch/call.rs +++ b/crates/oxide_vm/src/dispatch/call.rs @@ -199,7 +199,10 @@ impl Vm { #[allow(dead_code)] pub(crate) fn dispatch_make_cell(&mut self, rd: usize, instr: u32) -> Result<(), String> { - let cell_idx = opcode::imm16(instr) as usize; + // a 槽为 cell 索引;b 槽承载未初始化标志(1 = 绑定永不被初始化, + // 读写抛 ReferenceError,for-in/for-of 词法头 TDZ 环境建模用)。 + let cell_idx = opcode::a(instr) as usize; + let initialized = opcode::b(instr) == 0; let value = self.regs[rd]; let current = self.cell_stack.last_mut().unwrap(); while current.len() <= cell_idx { @@ -207,14 +210,14 @@ impl Vm { } if current[cell_idx].is_null() { // 无占位 cell:新建。 - current[cell_idx] = self.gc_state.alloc_cell(value, true); + current[cell_idx] = self.gc_state.alloc_cell(value, initialized); } else { // 更新占位 cell(CREATE_CLOSURE 已建),使闭包 upvalue 指向的 - // cell 值跟随初始化;赋值即解除 TDZ。 + // cell 值跟随初始化;初始化标志随指令 b 槽。 unsafe { let cell = &mut *current[cell_idx]; cell.value = value; - cell.set_initialized(true); + cell.set_initialized(initialized); } } Ok(()) diff --git a/crates/oxide_vm/tests/for_in_head_lex_tests.rs b/crates/oxide_vm/tests/for_in_head_lex_tests.rs new file mode 100644 index 00000000..e51c8220 --- /dev/null +++ b/crates/oxide_vm/tests/for_in_head_lex_tests.rs @@ -0,0 +1,97 @@ +//! for-in/for-of 词法头独立环境钉:头名与顶层 var 同名的捕获错位(Gap-1) +//! 与右值区 TDZ 环境缺失(Gap-2)两面的引擎侧形态锁。 +//! +//! 完成值一律收敛为布尔(JsValue 的 Display 只暴露 number/bool,不暴露 +//! 字符串内容),字符串比较在引擎内完成。 + +use std::sync::Arc; + +use oxide_compiler::compiler::Compiler; +use oxide_parser::Allocator; +use oxide_vm::vm::Vm; + +fn eval(source: &str) -> String { + let allocator = Allocator::default(); + let program = match oxide_parser::parse(&allocator, source) { + Ok(p) => p, + Err(e) => return format!("parse error: {}", e[0].message), + }; + let module = match Compiler::new().compile(&program) { + Ok(m) => m, + Err(e) => return format!("compile error: {e}"), + }; + let mut vm = Vm::new(); + match vm.run(&Arc::new(module)) { + Ok(result) => format!("{result}"), + Err(e) => format!("vm error: {e}"), + } +} + +// ── Gap-1:头名与顶层 var 同名,体内闭包捕获头绑定(非全局属性)── + +#[test] +fn for_in_let_head_same_name_as_top_var_closure_reads_head() { + // let 头与顶层 var 同名:循环后调用体内闭包读头绑定,值 = 末迭代键; + // 读错成顶层 var 的全局属性('top')即红。 + assert_eq!( + eval("var x = \"top\"; var p; for (let x in {a:1,b:2}) { p = function(){ return x; }; } p() === \"b\""), + "true", + "closure in body captures the let head binding, not the same-named top-level var" + ); +} + +#[test] +fn for_in_nested_lexical_heads_inner_closure_reads_inner_head() { + // 双层词法头同名遮蔽:内层头捕获优先,循环后读内层头末值('r')。 + assert_eq!( + eval( + "var x = \"top\"; var p; for (let x in {a:1}) { \ + for (let x of ['q','r']) { p = function(){ return x; }; } } \ + p() === \"r\"" + ), + "true", + "nested lexical heads shadow each other; the inner head wins in the inner body" + ); +} + +#[test] +fn for_of_let_head_same_name_as_top_var_closure_reads_head() { + // for-of 孪生主形:同步与 await 两臂共用同一覆盖。 + assert_eq!( + eval("var x = \"top\"; var p; for (let x of ['q','r']) { p = function(){ return x; }; } p() === \"r\""), + "true", + "for-of let head closure captures the head binding, not the top-level var" + ); +} + +// ── Gap-2:右值区对头名的读(直读/闭包捕获)必须抛 ReferenceError ── + +#[test] +fn for_in_rhs_direct_read_of_let_head_throws_reference_error() { + // 右值区直读头名:头名在 TDZ 环境中未初始化,读抛 ReferenceError; + // 穿透外层已初始化同名绑定(不抛)即红。 + assert_eq!( + eval( + "let x = 1; var r = 0; \ + try { for (let x in { x }) { r = 2; } } \ + catch (e) { r = (e instanceof ReferenceError) ? 1 : 3; } r === 1" + ), + "true", + "direct read of the uninitialized let head in the RHS zone throws ReferenceError" + ); +} + +#[test] +fn for_of_rhs_closure_capturing_let_head_throws_reference_error() { + // 右值区创建、捕获头名的闭包:循环后调用抛 ReferenceError; + // 闭包捕获外层已初始化同名 cell(不抛)即红。 + assert_eq!( + eval( + "let x = 1; var probe; var r = 0; \ + for (let x in { a: (probe = function(){ return x; }) }) { r = 2; } \ + try { probe(); r = 3; } catch (e) { r = (e instanceof ReferenceError) ? 1 : 4; } r === 1" + ), + "true", + "closure created in the RHS zone captures the TDZ head cell and throws on read" + ); +}