diff --git a/crates/oxide_emit/src/expr/identifier.rs b/crates/oxide_emit/src/expr/identifier.rs index e1793c4b..416d2f77 100644 --- a/crates/oxide_emit/src/expr/identifier.rs +++ b/crates/oxide_emit/src/expr/identifier.rs @@ -21,19 +21,20 @@ impl Emitter { } } + // 捕获集是函数级名字并集:块级 let/const 的 cell 在块退出后仍存留,但名字 + // 已不在作用域链内。仅当名字当前可解析时才走 cell,否则落下方全局解析 + // (未声明读经 LOAD_GLOBAL 抛 ReferenceError),不得按名误读已失效的 cell。 if let Some(&cell_idx) = ctx.captured_bindings.get(name) { - let r = ctx.alloc_reg(); - if let Some((binding, _)) = ctx.scopes.symbols.lookup_any_binding(name) { + if let Some(binding_reg) = ctx.scopes.symbols.lookup_any_binding(name).map(|(binding, _)| binding.reg) { + let r = ctx.alloc_reg(); ctx.inst(Inst::new( OpCode::CELL_GET, Operand::Reg(r), - Operand::Reg(binding.reg), + Operand::Reg(binding_reg), Operand::Imm(cell_idx as u16), )); - } else { - ctx.inst(Inst::new(OpCode::CELL_GET, Operand::Reg(r), Operand::None, Operand::Imm(cell_idx as u16))); + return Ok(r); } - return Ok(r); } let var_reg = match ctx.lookup_or_builtin(name) { @@ -100,6 +101,9 @@ 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) { ctx.inst(Inst::new( OpCode::CELL_GET, @@ -107,13 +111,17 @@ impl Emitter { Operand::Reg(binding.reg), Operand::Imm(cell_idx as u16), )); - } else { + } else if self.is_global_tier_name(ctx, name) { + let key_idx = ctx.add_constant(Constant::String(name.to_string())); ctx.inst(Inst::new( - OpCode::CELL_GET, + OpCode::LOAD_GLOBAL, Operand::Reg(result_reg), + Operand::Const(key_idx), Operand::None, - Operand::Imm(cell_idx as u16), )); + } else { + let undef_idx = ctx.add_constant(Constant::Undefined); + ctx.inst(Inst::load_const(Operand::Reg(result_reg), undef_idx)); } } else if self.is_global_tier_name(ctx, name) { // 顶层已声明 var:with 对象无该属性时回退读全局对象属性(顶层 var 的唯一 @@ -183,15 +191,19 @@ impl Emitter { )); return; } - // 目标若是被捕获 cell,走 CELL_SET + // 目标若是被捕获 cell,走 CELL_SET;仅当名字当前可解析时才写 cell——捕获集 + // 按名保留的块级绑定在块退出后不可解析,须落下方全局写(sloppy 物化隐式 + // 全局属性,strict 抛 ReferenceError)。 if let Some(&cell_idx) = ctx.captured_bindings.get(name) { - ctx.inst(Inst::new( - OpCode::CELL_SET, - Operand::None, - Operand::Reg(val_reg), - Operand::Imm(cell_idx as u16), - )); - return; + if ctx.scopes.symbols.lookup_any_binding(name).is_some() { + ctx.inst(Inst::new( + OpCode::CELL_SET, + Operand::None, + Operand::Reg(val_reg), + Operand::Imm(cell_idx as u16), + )); + return; + } } let var_reg = ctx.lookup_or_global(name); if ctx.targets_readonly_builtin(name, var_reg) { diff --git a/crates/oxide_emit/src/expr/operator/mod.rs b/crates/oxide_emit/src/expr/operator/mod.rs index 76cb4f5c..394858aa 100644 --- a/crates/oxide_emit/src/expr/operator/mod.rs +++ b/crates/oxide_emit/src/expr/operator/mod.rs @@ -348,14 +348,17 @@ impl Emitter { if let Expression::Identifier(ident) = arg_expr { let name = ident.name.as_str(); let in_with_dynamic = !ctx.with_stack.is_empty() && !ctx.is_with_internal_binding(name); - let captured = - ctx.current_upvalue_captures.iter().any(|u| u.name == name) || ctx.captured_bindings.contains_key(name); + let binding = ctx.scopes.symbols.lookup_any_binding(name); + // 捕获集按名保留块级绑定,但块退出后名字已不可解析:此时不再视为 + // captured(typeof 对未解析引用求值 "undefined",不得 CELL_GET 误读 + // 已失效 cell)。 + let captured = ctx.current_upvalue_captures.iter().any(|u| u.name == name) + || (ctx.captured_bindings.contains_key(name) && binding.is_some()); // 顶层已声明 var:typeof 读全局对象属性(顶层 var 的唯一存储,引擎侧不保留 // 镜像副本),缺失 → "undefined"(非抛,IsUnresolvableReference 语义)。未声明名 // 同走此路(lookup 未命中)。隐式全局槽(未声明名读写登记)同走属性路: // delete 真删后缺失 → "undefined" 而非经引擎侧镜像副本读旧值或抛 // ReferenceError(typeof 对 unresolvable 引用不抛)。 - let binding = ctx.scopes.symbols.lookup_any_binding(name); let implicit_global = binding.is_some_and(|(b, _)| ctx.is_implicit_global_reg(b.reg)); let is_tier = self.is_global_tier_name(ctx, name); if !in_with_dynamic && !captured && (is_tier || binding.is_none() || implicit_global) { diff --git a/crates/oxide_emit/src/function_body.rs b/crates/oxide_emit/src/function_body.rs index e6e7dc30..22a4197d 100644 --- a/crates/oxide_emit/src/function_body.rs +++ b/crates/oxide_emit/src/function_body.rs @@ -4,7 +4,7 @@ //! 两类型经 lib.rs 重导出供各语法域构造实参;编译链为 `Emitter` 方法, //! 捕获分析经自由函数调用 capture 族,与所在文件无关。 -use std::collections::HashSet; +use std::collections::{BTreeMap, HashSet}; use oxide_bytecode::module::{Constant, UpvalueCapture}; use oxide_bytecode::opcode::OpCode; @@ -568,10 +568,20 @@ impl Emitter { body_context, FunctionBodyContext::Ordinary | FunctionBodyContext::Arrow | FunctionBodyContext::ClassElement ) { + // 只有创建点父作用域链内可解析的父捕获名才可能成为子函数 upvalue: + // 捕获集是函数级名字并集,块级 let/const 在块退出后 cell 仍存留但名字 + // 已不可解析,块外嵌套函数按名命中会错误读取已失效的 cell。父自身 + // upvalue 代表的祖父绑定在父函数体内恒可见,不参与过滤。 + let visible_parent_captured: BTreeMap = parent_ctx + .captured_bindings + .iter() + .filter(|(n, _)| parent_ctx.scopes.symbols.lookup_any_binding(n).is_some()) + .map(|(n, &i)| (n.clone(), i)) + .collect(); ctx.current_upvalue_captures = collect_upvalue_names( body_stmts, &capture_exprs, - &parent_ctx.captured_bindings, + &visible_parent_captured, &parent_ctx.current_upvalue_captures, &ctx.own_bindings, ); diff --git a/crates/oxide_vm/tests/block_scope_leave_tests.rs b/crates/oxide_vm/tests/block_scope_leave_tests.rs new file mode 100644 index 00000000..1a983810 --- /dev/null +++ b/crates/oxide_vm/tests/block_scope_leave_tests.rs @@ -0,0 +1,133 @@ +//! 块作用域退出后的名字解析钉:块级 `let`/`const` 退出后名字不可解析,读取 +//! 须落全局解析(ReferenceError / typeof 为 "undefined"),不得按名命中残留 +//! 捕获 cell 读到已失效的值。 +//! +//! 覆盖同函数直读、嵌套函数捕获、`typeof`、严格模式写与 direct eval 各面, +//! 并锁住合法块内捕获与参数遮蔽不回归。 + +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}"), + } +} + +#[test] +fn catch_nested_function_bare_read_of_popped_block_let_throws_reference_error() { + // 语料主形:catch 内嵌套函数裸读 try 内已退出块的 let,须抛 ReferenceError + // 而非命中残留 cell 读到 18。 + assert_eq!( + eval( + "var ref = false; \ + try { { let xx = 18; throw 25; } } catch (e) { \ + (function () { try { xx; } catch (e2) { ref = (e2 instanceof ReferenceError); } })(); \ + } ref" + ), + "true" + ); +} + +#[test] +fn typeof_popped_block_let_in_nested_function_is_undefined() { + // 块外嵌套函数对已退出块 let 的 typeof:未解析引用求值 "undefined", + // 不得命中残留 cell 得到 "number"。 + assert_eq!(eval("{ let zz = 7; } (function () { return typeof zz === \"undefined\"; })()"), "true"); +} + +#[test] +fn direct_read_of_popped_block_let_throws_reference_error_with_legal_capture() { + // 同函数内既有合法块内捕获,块外直读仍须抛 ReferenceError;捕获闭包本身 + // 读到块内值不受影响。 + assert_eq!( + eval( + "var f; { let aa = 1; f = function () { return aa; }; } \ + var ref = false; try { aa; } catch (e) { ref = (e instanceof ReferenceError); } \ + ref && f() === 1" + ), + "true" + ); +} + +#[test] +fn typeof_popped_block_let_with_legal_capture_is_undefined() { + // 同函数 typeof:块外 typeof 已退出块 let 为 "undefined"(非抛非旧值)。 + assert_eq!( + eval("{ let aa = 1; var f = function () { return aa; }; } typeof aa === \"undefined\""), + "true" + ); +} + +#[test] +fn strict_write_to_popped_block_let_throws_reference_error() { + // 严格模式对已退出块 let 的块外写须抛 ReferenceError,不得按名 CELL_SET + // 污染残留 cell;块内闭包捕获值不变。 + assert_eq!( + eval( + "\"use strict\"; var f; { let aa = 1; f = function () { return aa; }; } \ + var ref = false; try { aa = 99; } catch (e) { ref = (e instanceof ReferenceError); } \ + ref && f() === 1" + ), + "true" + ); +} + +#[test] +fn eval_of_popped_block_let_throws_reference_error() { + // direct eval 面锁:调用方块外的 eval('xx') 看不到已退出块绑定,抛 + // ReferenceError(当前已绿,锁不回归)。 + assert_eq!( + eval( + "var ref = false; \ + try { { let xx = 18; throw 25; } } catch (e) { \ + (function () { try { eval(\"xx\"); } catch (e2) { ref = (e2 instanceof ReferenceError); } })(); \ + } ref" + ), + "true" + ); +} + +#[test] +fn closure_created_inside_block_reads_let_after_block() { + // 合法块内捕获守卫:块内创建的闭包在块外调用仍读到块绑定值。 + assert_eq!(eval("var r; { let bb = 7; r = function () { return bb; }; } r() === 7"), "true"); +} + +#[test] +fn chained_closure_reads_grandparent_block_let() { + // 链式捕获守卫:跨两层闭包读取祖父块内绑定不受可见性过滤影响。 + assert_eq!(eval("var h; { let cc = 5; h = () => () => cc; } h()() === 5"), "true"); +} + +#[test] +fn catch_block_let_shadows_parameter() { + // 参数遮蔽守卫:catch/try 块内 let 只遮蔽块内引用,catch 外仍读参数值。 + assert_eq!( + eval("(function (x) { try { let x = \"in\"; throw 0; } catch (e) { return x === \"out\"; } })(\"out\")"), + "true" + ); +} + +#[test] +fn read_of_uncaptured_popped_block_let_throws_reference_error() { + // 无捕获对照守卫:无任何闭包捕获时块外直读同样抛 ReferenceError。 + assert_eq!( + eval("var ref = false; { let aa = 1; } try { aa; } catch (e) { ref = (e instanceof ReferenceError); } ref"), + "true" + ); +}