diff --git a/crates/oxide_emit/src/compile_ctx.rs b/crates/oxide_emit/src/compile_ctx.rs index a2dd1337..64e1b5e0 100644 --- a/crates/oxide_emit/src/compile_ctx.rs +++ b/crates/oxide_emit/src/compile_ctx.rs @@ -92,6 +92,11 @@ pub struct CompileCtx { /// 捕获判断(MAKE_CELL / CELL_GET / CELL_SET)与子函数 upvalue cell_idx 统一查此映射, /// 消除符号表时序依赖与 cell 索引错位。 pub(crate) captured_bindings: BTreeMap, + /// 正在发射的 C 风格 for 头 let/const 声明名(含解构叶)。头名不像块级 + /// let/const 那样在块入口预声明,init 表达式内创建的嵌套函数引用同头尚未 + /// declare 的前向名时,父捕获可见性过滤据此放行;init 发射完毕后移除, + /// 嵌套 for 头取并集。 + pub(crate) pending_for_head_names: HashSet, /// 顶层已声明 var 名与顶层函数声明名(脚本全局声明实例化提升名):裸读走全局 /// 对象属性(LOAD_GLOBAL)、裸写走感知全局对象属性描述符的写;值的唯一存储是 /// 全局对象属性(顶层 var 的唯一存储),引擎侧不再保留镜像副本。 @@ -201,6 +206,7 @@ impl CompileCtx { block_fn_suppressed: HashSet::new(), block_fn_entry_mats: Vec::new(), captured_bindings: BTreeMap::new(), + pending_for_head_names: HashSet::new(), global_tier_names: HashSet::new(), upvalue_const_flags: HashSet::new(), implicit_global_reads: HashSet::new(), @@ -347,6 +353,17 @@ impl CompileCtx { self.implicit_global_writes.contains(®) || self.implicit_global_reads.contains(®) } + /// 名字在当前作用域链内是否存在真实词法绑定(排除隐式全局登记),命中时返回其寄存器。 + /// + /// 未声明名的隐式全局登记(写侧 `lookup_or_global`、读侧 `lookup_or_builtin` 经 + /// `pre_register_global`)会在全局作用域插入占位绑定,使 `lookup_any_binding` 按名 + /// 命中;但它不是词法声明。捕获名退出作用域后若曾被登记为隐式全局,按名命中会 + /// 误读残留 cell——捕获 cell 的可见性判定统一经本入口。 + pub(crate) fn visible_binding_reg(&self, name: &str) -> Option { + let (binding, _) = self.scopes.symbols.lookup_any_binding(name)?; + (!self.is_implicit_global_reg(binding.reg)).then_some(binding.reg) + } + pub(crate) fn lookup_const_flag(&self, name: &str) -> bool { // upvalue 捕获的 const:子 ctx 符号表不含父函数作用域绑定,查快照标志。 self.scopes.symbols.lookup_is_const(name) || self.upvalue_const_flags.contains(name) diff --git a/crates/oxide_emit/src/expr/identifier.rs b/crates/oxide_emit/src/expr/identifier.rs index 416d2f77..cb93253f 100644 --- a/crates/oxide_emit/src/expr/identifier.rs +++ b/crates/oxide_emit/src/expr/identifier.rs @@ -22,10 +22,11 @@ impl Emitter { } // 捕获集是函数级名字并集:块级 let/const 的 cell 在块退出后仍存留,但名字 - // 已不在作用域链内。仅当名字当前可解析时才走 cell,否则落下方全局解析 - // (未声明读经 LOAD_GLOBAL 抛 ReferenceError),不得按名误读已失效的 cell。 + // 已不在作用域链内。仅当名字当前可解析为真实词法绑定时才走 cell(隐式全局 + // 登记不算),否则落下方全局解析(未声明读经 LOAD_GLOBAL 抛 ReferenceError), + // 不得按名误读已失效的 cell。 if let Some(&cell_idx) = ctx.captured_bindings.get(name) { - if let Some(binding_reg) = ctx.scopes.symbols.lookup_any_binding(name).map(|(binding, _)| binding.reg) { + if let Some(binding_reg) = ctx.visible_binding_reg(name) { let r = ctx.alloc_reg(); ctx.inst(Inst::new( OpCode::CELL_GET, @@ -101,14 +102,14 @@ impl Emitter { Operand::None, )); } else if let Some(&cell_idx) = ctx.captured_bindings.get(name) { - // 回退分支的捕获 cell 同样只在名字当前可解析时才有效;否则与静态解析 - // 一致:顶层 tier 名读全局对象属性,其余读 undefined(with 回退不抛 - // 未解析引用)。 - if let Some((binding, _)) = ctx.scopes.symbols.lookup_any_binding(name) { + // 回退分支的捕获 cell 同样只在名字当前可解析为真实词法绑定时才有效; + // 否则与静态解析一致:顶层 tier 名读全局对象属性,其余读 undefined + // (with 回退不抛未解析引用)。 + if let Some(binding_reg) = ctx.visible_binding_reg(name) { ctx.inst(Inst::new( OpCode::CELL_GET, Operand::Reg(result_reg), - Operand::Reg(binding.reg), + Operand::Reg(binding_reg), Operand::Imm(cell_idx as u16), )); } else if self.is_global_tier_name(ctx, name) { @@ -191,11 +192,11 @@ impl Emitter { )); return; } - // 目标若是被捕获 cell,走 CELL_SET;仅当名字当前可解析时才写 cell——捕获集 - // 按名保留的块级绑定在块退出后不可解析,须落下方全局写(sloppy 物化隐式 - // 全局属性,strict 抛 ReferenceError)。 + // 目标若是被捕获 cell,走 CELL_SET;仅当名字当前可解析为真实词法绑定时才写 + // cell——捕获集按名保留的块级绑定在块退出后不可解析(隐式全局登记不算), + // 须落下方全局写(sloppy 物化隐式全局属性,strict 抛 ReferenceError)。 if let Some(&cell_idx) = ctx.captured_bindings.get(name) { - if ctx.scopes.symbols.lookup_any_binding(name).is_some() { + if ctx.visible_binding_reg(name).is_some() { ctx.inst(Inst::new( OpCode::CELL_SET, Operand::None, diff --git a/crates/oxide_emit/src/expr/operator/mod.rs b/crates/oxide_emit/src/expr/operator/mod.rs index 394858aa..fe794243 100644 --- a/crates/oxide_emit/src/expr/operator/mod.rs +++ b/crates/oxide_emit/src/expr/operator/mod.rs @@ -351,9 +351,9 @@ impl Emitter { let binding = ctx.scopes.symbols.lookup_any_binding(name); // 捕获集按名保留块级绑定,但块退出后名字已不可解析:此时不再视为 // captured(typeof 对未解析引用求值 "undefined",不得 CELL_GET 误读 - // 已失效 cell)。 + // 已失效 cell)。隐式全局登记同样不算真实绑定。 let captured = ctx.current_upvalue_captures.iter().any(|u| u.name == name) - || (ctx.captured_bindings.contains_key(name) && binding.is_some()); + || (ctx.captured_bindings.contains_key(name) && ctx.visible_binding_reg(name).is_some()); // 顶层已声明 var:typeof 读全局对象属性(顶层 var 的唯一存储,引擎侧不保留 // 镜像副本),缺失 → "undefined"(非抛,IsUnresolvableReference 语义)。未声明名 // 同走此路(lookup 未命中)。隐式全局槽(未声明名读写登记)同走属性路: diff --git a/crates/oxide_emit/src/function_body.rs b/crates/oxide_emit/src/function_body.rs index 22a4197d..bf506fa5 100644 --- a/crates/oxide_emit/src/function_body.rs +++ b/crates/oxide_emit/src/function_body.rs @@ -572,10 +572,15 @@ impl Emitter { // 捕获集是函数级名字并集,块级 let/const 在块退出后 cell 仍存留但名字 // 已不可解析,块外嵌套函数按名命中会错误读取已失效的 cell。父自身 // upvalue 代表的祖父绑定在父函数体内恒可见,不参与过滤。 + // C 风格 for 头 let/const 名在 init 内尚未 declare,但同头声明会为其 + // 建 cell,前向捕获须按父捕获集保留(见 `pending_for_head_names`)。 let visible_parent_captured: BTreeMap = parent_ctx .captured_bindings .iter() - .filter(|(n, _)| parent_ctx.scopes.symbols.lookup_any_binding(n).is_some()) + .filter(|(n, _)| { + parent_ctx.visible_binding_reg(n.as_str()).is_some() + || parent_ctx.pending_for_head_names.contains(n.as_str()) + }) .map(|(n, &i)| (n.clone(), i)) .collect(); ctx.current_upvalue_captures = collect_upvalue_names( diff --git a/crates/oxide_emit/src/stmt/iteration/for_.rs b/crates/oxide_emit/src/stmt/iteration/for_.rs index ab91d584..f8200b8f 100644 --- a/crates/oxide_emit/src/stmt/iteration/for_.rs +++ b/crates/oxide_emit/src/stmt/iteration/for_.rs @@ -53,6 +53,17 @@ impl Emitter { if let Some(expr) = init.as_expression() { self.emit_expression(expr, ctx)?; } else if let ForStatementInit::VariableDeclaration(decl) = init { + // C 风格 for 头 let/const 名在 init 表达式发射期间视为可见:头名不像 + // 块级 let/const 那样在块入口预声明,init 内闭包创建点头名尚不在符号表, + // 可见性过滤会误删其前向捕获(见 `CompileCtx::pending_for_head_names`)。 + let saved_pending = ctx.pending_for_head_names.clone(); + if !matches!(decl.kind, VariableDeclarationKind::Var) { + for d in &decl.declarations { + let mut head_names = Vec::new(); + self.collect_pattern_binding_names(&d.id, &mut head_names); + ctx.pending_for_head_names.extend(head_names); + } + } for d in &decl.declarations { let is_const = matches!(decl.kind, VariableDeclarationKind::Const); if let Some(init_expr) = &d.init { @@ -116,6 +127,7 @@ impl Emitter { } } } + ctx.pending_for_head_names = saved_pending; } } ctx.labels.set_label_pos(start_label, ctx.insts.len()); diff --git a/crates/oxide_vm/tests/block_scope_leave_tests.rs b/crates/oxide_vm/tests/block_scope_leave_tests.rs index 1a983810..35d5809c 100644 --- a/crates/oxide_vm/tests/block_scope_leave_tests.rs +++ b/crates/oxide_vm/tests/block_scope_leave_tests.rs @@ -131,3 +131,39 @@ fn read_of_uncaptured_popped_block_let_throws_reference_error() { "true" ); } + +#[test] +fn for_head_forward_capture_of_self_is_visible() { + // C 风格 for 头 init 内闭包对同头头名的前向引用须命中父捕获 cell:头名不像 + // 块级 let 那样预声明,可见性过滤不得将其误删。 + assert_eq!(eval("var r; for (let i = (() => i); !r; ) { r = i; } r() === r"), "true"); +} + +#[test] +fn for_head_forward_capture_of_later_declarator_is_visible() { + // 多声明符头:首声明符 init 内闭包引用后一声明符头名,须读到其初始化值。 + assert_eq!(eval("var g; for (let f = () => j, j = 1; ; ) { g = f; break; } g() === 1"), "true"); +} + +#[test] +fn sloppy_write_then_read_of_popped_block_capture_name_keeps_global_value() { + // 块外 sloppy 简单写物化隐式全局属性后,同名裸读须读回该属性(99), + // 不得因隐式全局登记被误判为可见而命中残留捕获 cell(1)。 + assert_eq!(eval("{ let aa = 1; f = () => aa; } aa = 99; aa"), "99"); +} + +#[test] +fn repeated_bare_read_of_invisible_capture_name_throws_each_time() { + // 不可见捕获名二次裸读:读侧隐式全局登记不构成真实词法绑定,两次均须抛 + // ReferenceError,不得第二次命中残留 cell 返回旧值。 + assert_eq!( + eval( + "var r; { let bb = 7; r = () => bb; } \ + var first = false, second = false; \ + try { bb; } catch (e) { first = e instanceof ReferenceError; } \ + try { bb; } catch (e) { second = e instanceof ReferenceError; } \ + first && second" + ), + "true" + ); +}