From f73f5b8005ad4e06dc28d569bc7d6754d4d45364 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Thu, 17 Sep 2026 18:48:55 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20for-in/of=20=E8=A7=A3=E6=9E=84=E5=A3=B0?= =?UTF-8?q?=E6=98=8E=E5=A4=B4=E6=94=AF=E6=8C=81=E4=B8=8E=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=80=BC=20TDZ?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oxide_emit/src/stmt/iteration/for_in.rs | 28 ++- .../tests/for_in_dstr_head_tdz_tests.rs | 183 ++++++++++++++++++ 2 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 crates/oxide_vm/tests/for_in_dstr_head_tdz_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 0abffa43..2232ce0d 100644 --- a/crates/oxide_emit/src/stmt/iteration/for_in.rs +++ b/crates/oxide_emit/src/stmt/iteration/for_in.rs @@ -76,13 +76,34 @@ impl Emitter { /// 右值区求值完毕后把覆盖从 TDZ cell 切到体区 fresh cell:每头名分配新 /// cell 并改写映射,头发射捕获臂与体区读/捕获自然命中。每迭代新分配一个 /// cell;机制见 `compile_ctx.rs` 的 `register_update_names` 字段文档。 + /// + /// 切换时为每头名预发未初始化 `MAKE_CELL`:体区绑定点(`MAKE_CELL_FRESH`) + /// 在解构默认值表达式求值之后,此窗口内读头名须命中未初始化 cell 抛 + /// ReferenceError;绑定点的 fresh cell 随后覆盖该占位 cell。 + /// + /// # 边界与前提 + /// - 无头名(空 pattern)时不发射指令。 + /// + /// # 副作用 + /// - 改动捕获映射并发射指令;cell 进 GC 根,每循环入口固定条数。 pub(crate) fn switch_for_head_env_to_body(&self, env: &mut ForHeadEnv, ctx: &mut CompileCtx) { + if env.entries.is_empty() { + return; + } let mut next = ctx.captured_bindings.values().copied().max().map_or(0, |m| m.saturating_add(1)); + // 默认值窗口的 TDZ 占位:未初始化标志折入 MAKE_CELL 立即数高字节。 + let undef_reg = self.emit_undefined(ctx); 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); + ctx.inst(Inst::new( + OpCode::MAKE_CELL, + Operand::Reg(undef_reg), + Operand::Imm(idx as u16 | 0x0100), + Operand::None, + )); } } @@ -135,10 +156,10 @@ impl Emitter { ForStatementLeft::VariableDeclaration(decl) => { // let/const 声明对被捕获绑定用 fresh cell(每迭代新 cell);var 单绑定。 let fresh_cell = !matches!(decl.kind, VariableDeclarationKind::Var); + let is_const = matches!(decl.kind, VariableDeclarationKind::Const); for d in &decl.declarations { match &d.id { oxide_parser::BindingPattern::BindingIdentifier(bi) => { - let is_const = matches!(decl.kind, VariableDeclarationKind::Const); let name = bi.name.as_str(); let var_reg = ctx.alloc_reg(); // var 声明:绑定已存在(顶层 var 预声明、 @@ -183,7 +204,10 @@ impl Emitter { } ctx.init_var(name); } - _ => return Err("destructuring not supported".into()), + // 解构头:数组 pattern 对迭代键字符串解构、对象 pattern 先 + // ToObject 再按属性读;叶名绑定复用通用路径(含 let/const + // 声明、fresh cell 与顶层 var 全局属性反射)。 + _ => self.emit_binding_pattern(&d.id, key_reg, decl.kind, is_const, fresh_cell, ctx)?, } } } diff --git a/crates/oxide_vm/tests/for_in_dstr_head_tdz_tests.rs b/crates/oxide_vm/tests/for_in_dstr_head_tdz_tests.rs new file mode 100644 index 00000000..a913d9d2 --- /dev/null +++ b/crates/oxide_vm/tests/for_in_dstr_head_tdz_tests.rs @@ -0,0 +1,183 @@ +//! for-in/for-of 解构声明头的引擎侧形态锁:解构头绑定发射(for-in 支持缺口) +//! 与解构默认值表达式的 TDZ 窗口(默认值求值早于体区 fresh cell 创建)。 +//! +//! 完成值一律收敛为布尔(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}"), + } +} + +// ── for-in 解构声明头绑定 ── + +#[test] +fn for_in_array_pattern_head_binds_each_key() { + // 数组 pattern 对迭代键字符串解构:单字符键首元素即键本身。 + assert_eq!( + eval("var r; for (let [x] in {i:1}) { r = x; } r === \"i\""), + "true", + "for-in array pattern head binds the iteration key" + ); +} + +#[test] +fn for_in_object_pattern_head_compiles() { + // 对象 pattern 头:编译通过且迭代体执行一次(属性值受原始字符串装箱面限制, + // 只钉绑定发射不报编译错与单次迭代)。 + assert_eq!( + eval("var n = 0; for (let {length: L} in {abc:1}) { n += 1; } n === 1"), + "true", + "for-in object pattern head compiles and iterates once" + ); +} + +#[test] +fn for_in_dstr_head_body_closure_shadows_outer() { + // 体闭包捕获头绑定(值为末迭代键);外层同名 let 不受影响。 + assert_eq!( + eval("let x = \"outside\"; var p; for (let [x] in {i:1}) { p = function(){ return x; }; } p() === \"i\" && x === \"outside\""), + "true", + "body closure captures the destructured head binding, outer name unchanged" + ); +} + +#[test] +fn for_in_dstr_head_default_closure_reads_bound_key() { + // 先绑定的叶在默认值表达式中被闭包捕获:后续调用读到本迭代键。 + assert_eq!( + eval("var pd; for (let [x, _ = pd = function(){ return x; }] in {i:1}) {} pd() === \"i\""), + "true", + "default value closure reads the already-bound leading element" + ); +} + +#[test] +fn for_in_dstr_head_scope_head_lex_close_shape() { + // scope-head-lex-close 三段断言:右值区闭包读头名抛 ReferenceError; + // 声明默认值闭包与体闭包读本迭代键。 + assert_eq!( + eval( + "let x = \"outside\"; var pd, pe, pb; \ + for (let [x, _ = pd = function(){ return x; }] in { i: pe = function(){ typeof x; } }) \ + pb = function(){ return x; }; \ + var ok = false; \ + try { pe(); } catch (e) { ok = (e instanceof ReferenceError); } \ + ok && pd() === \"i\" && pb() === \"i\" && x === \"outside\"" + ), + "true", + "for-in destructuring head TDZ: RHS closure throws, declaration/body closures bind the key" + ); +} + +#[test] +fn for_in_nested_dstr_head_binds_inner_elements() { + // 嵌套 pattern 头:外层键解构后内层叶继续解构。 + assert_eq!( + eval("var r; for (let [a, [b]] in {xy:1}) { r = a + b; } r === \"xy\""), + "true", + "nested destructuring head binds inner elements from the key" + ); +} + +// ── 解构默认值窗口的 TDZ(体区 cell 由绑定点前的未初始化占位保护)── + +#[test] +fn for_of_dstr_default_self_read_throws_reference_error() { + // `[x = x]`:默认值读本叶头名,体区 cell 尚未初始化,抛 ReferenceError。 + assert_eq!( + eval("var r = 0; try { for (let [x = x] of [[]]) {} r = 2; } catch (e) { r = (e instanceof ReferenceError) ? 1 : 3; } r === 1"), + "true", + "for-of default value reading its own leaf throws ReferenceError" + ); +} + +#[test] +fn for_of_object_dstr_default_self_read_throws_reference_error() { + // 对象 pattern 默认值直读本叶:同上 TDZ 面。 + assert_eq!( + eval("var r = 0; try { for (let {x = x} of [{}]) {} r = 2; } catch (e) { r = (e instanceof ReferenceError) ? 1 : 3; } r === 1"), + "true", + "for-of object pattern default self-read throws ReferenceError" + ); +} + +#[test] +fn for_of_dstr_default_later_leaf_self_read_throws_reference_error() { + // 后叶默认值读后叶名:前叶已绑定不影响后叶的未初始化状态。 + assert_eq!( + eval("var r = 0; try { for (let [a, b = b] of [[1]]) {} r = 2; } catch (e) { r = (e instanceof ReferenceError) ? 1 : 3; } r === 1"), + "true", + "for-of later-leaf default self-read throws ReferenceError" + ); +} + +#[test] +fn for_in_dstr_default_self_read_throws_reference_error() { + // for-in 空字符串键:数组 pattern 取不到元素,默认值读本叶头名抛 ReferenceError。 + assert_eq!( + eval("var r = 0; try { for (let [x = x] in {\"\":1}) {} r = 2; } catch (e) { r = (e instanceof ReferenceError) ? 1 : 3; } r === 1"), + "true", + "for-in default value reading its own leaf throws ReferenceError" + ); +} + +#[test] +fn for_in_dstr_default_bound_leaf_no_throw() { + // 默认值仅在元素为 undefined 时求值:键为单字符时元素已绑定,不触发 TDZ。 + assert_eq!( + eval("var r; for (let [x = x] in {i:1}) { r = x; } r === \"i\""), + "true", + "for-in default not evaluated when the element is defined" + ); +} + +// ── 绿守卫:既有语义不被破坏 ── + +#[test] +fn for_of_simple_lexical_head_per_iteration_closure() { + // 简单 for-of 词法头每迭代新绑定:体内闭包读末值。 + assert_eq!( + eval("var p; for (let x of [1,2]) { p = function(){ return x; }; } p() === 2"), + "true", + "for-of simple lexical head keeps per-iteration bindings" + ); +} + +#[test] +fn for_in_simple_lexical_head_per_iteration_closure() { + // 简单 for-in 词法头每迭代新绑定:体内闭包读末迭代键。 + assert_eq!( + eval("var p; for (let x in {a:1,b:2}) { p = function(){ return x; }; } p() === \"b\""), + "true", + "for-in simple lexical head keeps per-iteration bindings" + ); +} + +#[test] +fn for_in_dstr_var_head_no_lexical_tdz() { + // var 解构头无 TDZ 环境:默认值求值不被未初始化 cell 拦截。 + assert_eq!( + eval("var r; for (var [x = \"d\"] in {\"\":1}) { r = x; } r === \"d\""), + "true", + "var destructuring head has no lexical TDZ" + ); +}