fix: 修正捕获可见性过滤的前向捕获与隐式全局误判

This commit is contained in:
2026-09-17 20:07:23 +00:00
parent 12aa250542
commit e86ec8d7d2
6 changed files with 86 additions and 15 deletions
+17
View File
@@ -92,6 +92,11 @@ pub struct CompileCtx {
/// 捕获判断(MAKE_CELL / CELL_GET / CELL_SET)与子函数 upvalue cell_idx 统一查此映射,
/// 消除符号表时序依赖与 cell 索引错位。
pub(crate) captured_bindings: BTreeMap<String, u8>,
/// 正在发射的 C 风格 for 头 let/const 声明名(含解构叶)。头名不像块级
/// let/const 那样在块入口预声明,init 表达式内创建的嵌套函数引用同头尚未
/// declare 的前向名时,父捕获可见性过滤据此放行;init 发射完毕后移除,
/// 嵌套 for 头取并集。
pub(crate) pending_for_head_names: HashSet<String>,
/// 顶层已声明 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(&reg) || self.implicit_global_reads.contains(&reg)
}
/// 名字在当前作用域链内是否存在真实词法绑定(排除隐式全局登记),命中时返回其寄存器。
///
/// 未声明名的隐式全局登记(写侧 `lookup_or_global`、读侧 `lookup_or_builtin` 经
/// `pre_register_global`)会在全局作用域插入占位绑定,使 `lookup_any_binding` 按名
/// 命中;但它不是词法声明。捕获名退出作用域后若曾被登记为隐式全局,按名命中会
/// 误读残留 cell——捕获 cell 的可见性判定统一经本入口。
pub(crate) fn visible_binding_reg(&self, name: &str) -> Option<u32> {
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)
+13 -12
View File
@@ -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,
+2 -2
View File
@@ -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 未命中)。隐式全局槽(未声明名读写登记)同走属性路:
+6 -1
View File
@@ -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<String, u8> = 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(
@@ -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());
@@ -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"
);
}