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());