mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-24 02:31:13 +08:00
fix: 修正捕获可见性过滤的前向捕获与隐式全局误判
This commit is contained in:
@@ -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(®) || 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<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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 未命中)。隐式全局槽(未声明名读写登记)同走属性路:
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user