fix: 未声明标识符读取抛 ReferenceError(typeof 特判返回 undefined)

This commit is contained in:
2026-08-16 00:10:08 +08:00
parent e448c642a5
commit d309379551
9 changed files with 287 additions and 6 deletions
+14 -2
View File
@@ -186,6 +186,11 @@ pub struct CompileCtx {
/// 本函数从父函数捕获的 const 绑定名:子 ctx 不继承父函数作用域符号表,
/// 捕获 const 信息随 upvalue 收集一并快照,供 const 写检查(编译期拦截)使用。
pub(crate) upvalue_const_flags: HashSet<String>,
/// 未声明标识符读所分配的全局槽寄存器集合:标识符首次读未命中任何作用域时,
/// `lookup_or_builtin` 按隐式全局登记并记录其寄存器,后续读取据此发射
/// LOAD_GLOBAL(运行期查 global object 属性,缺失抛 ReferenceError)。
/// 按寄存器而非名字记录:块作用域同名新绑定持不同槽位,不会被误判为隐式全局。
pub(crate) implicit_global_reads: HashSet<u32>,
/// 函数 `length` 属性值:首个带默认值形参之前的形参数(rest 不计)。
/// emit_params_prologue 前由编译入口从 param_specs 计算。
pub(crate) function_length: u32,
@@ -302,6 +307,7 @@ impl CompileCtx {
own_bindings: HashSet::new(),
captured_bindings: BTreeMap::new(),
upvalue_const_flags: HashSet::new(),
implicit_global_reads: HashSet::new(),
function_length: 0,
const_overflow: false,
with_stack: Vec::new(),
@@ -407,12 +413,18 @@ impl CompileCtx {
pub(crate) fn lookup_or_builtin(&mut self, name: &str) -> Result<u32, String> {
match self.scopes.symbols.lookup(name) {
Ok(reg) => Ok(reg),
// 未声明标识符按全局处理:`typeof X` 守卫等合法 JS 应在运行期判定
// 未定义(读取未定义全局返回 undefined),而非编译期报错。
// 未声明标识符按隐式全局登记:读取语义(读未声明应抛 ReferenceError)由
// 发射端按 implicit_global_reads 择 LOAD_GLOBAL 实现;`typeof X` 守卫等
// 合法 JS 在运行期判定未定义(typeof 特判走 LOAD_GLOBAL_TYPEOF)。
Err(err) if err.contains("is not defined") => {
let reg = self.alloc_reg();
self.scopes.symbols.pre_register_global(name, reg);
self.scopes.builtin_reg_map.push((name.to_string(), reg));
// 内置名(预扫描阶段在空符号表上登记)不是用户未声明读,不标记——
// 否则内置标识符读取全部改走 LOAD_GLOBAL(热路径退化 + 槽位被修剪)。
if !Self::is_known_builtin(name) {
self.implicit_global_reads.insert(reg);
}
Ok(reg)
}
Err(err) => Err(err),
+8 -1
View File
@@ -45,7 +45,14 @@ impl Emitter {
Err(err) => return Err(err),
};
let r = ctx.alloc_reg();
ctx.inst(Inst::new(OpCode::LOAD_VAR, Operand::Reg(r), Operand::Reg(var_reg), Operand::None));
if ctx.implicit_global_reads.contains(&var_reg) {
// 未声明标识符读:运行期查 global object 属性存在性,缺失抛
// ReferenceError(sloppy 写先登记全局槽的读不在此集合,仍走 LOAD_VAR)。
let key_idx = ctx.add_constant(Constant::String(name.to_string()));
ctx.inst(Inst::new(OpCode::LOAD_GLOBAL, Operand::Reg(r), Operand::Const(key_idx), Operand::None));
} else {
ctx.inst(Inst::new(OpCode::LOAD_VAR, Operand::Reg(r), Operand::Reg(var_reg), Operand::None));
}
Ok(r)
}
+44 -1
View File
@@ -209,7 +209,11 @@ impl Emitter {
_ => Err("invalid delete target".into()),
};
}
let arg = self.emit_expression(&un.argument, ctx)?;
let arg = if matches!(un.operator, UnaryOperator::Typeof) {
self.emit_typeof_operand(&un.argument, ctx)?
} else {
self.emit_expression(&un.argument, ctx)?
};
match un.operator {
UnaryOperator::UnaryNegation => {
ctx.inst(Inst::new(OpCode::NEG, Operand::Reg(arg), Operand::Reg(arg), Operand::None));
@@ -239,6 +243,45 @@ impl Emitter {
}
}
/// typeof 操作数求值:未声明标识符特判为 "undefined"。
///
/// # 步骤
/// 1. 剥括号后若为标识符且不在任何作用域/闭包捕获中、非 with 动态解析,
/// 发射 LOAD_GLOBAL_TYPEOF:运行期查 global object 属性,命中取真实值
/// (含不在 BUILTIN_GLOBALS 名单的真实全局,如 Temporal),缺失求值
/// undefined(IsUnresolvableReference 语义)。
/// 2. 其余(TDZ 绑定、已声明、with 内)走正常求值:TDZ 抛 ReferenceError、
/// with 动态回退未定义、已声明读真实值。
///
/// # 边界与前提
/// - 未声明标识符不能走 LOAD_GLOBAL(读未声明抛 ReferenceError),故必须在此特判;
/// - `typeof x; var x;` / `typeof x; let x;` 分别由 var 预声明(已初始化→undefined)
/// 与 TDZ 占位(未初始化→抛)覆盖,不落本分支。
fn emit_typeof_operand(&self, argument: &Expression, ctx: &mut CompileCtx) -> Result<u32, String> {
let mut arg_expr = argument;
while let Expression::ParenthesizedExpression(p) = arg_expr {
arg_expr = &p.expression;
}
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);
if !in_with_dynamic && !captured && ctx.scopes.symbols.lookup_any_binding(name).is_none() {
let key_idx = ctx.add_constant(Constant::String(name.to_string()));
let r = ctx.alloc_reg();
ctx.inst(Inst::new(
OpCode::LOAD_GLOBAL_TYPEOF,
Operand::Reg(r),
Operand::Const(key_idx),
Operand::None,
));
return Ok(r);
}
}
self.emit_expression(argument, ctx)
}
fn emit_conditional_expression(
&self, cond: &oxide_parser::ConditionalExpression, ctx: &mut CompileCtx,
) -> Result<u32, String> {
@@ -67,8 +67,9 @@ impl Emitter {
} else {
ctx.inst(Inst::new(OpCode::STORE_VAR, Operand::Reg(var_reg), Operand::Reg(var_reg), Operand::None));
}
// 脚本顶层函数声明:同步写全局对象,使 globalThis 可反射函数名。
if ctx.is_global_scope {
// 脚本顶层(非块内)函数声明:同步写全局对象,使 globalThis 可反射函数名。
// 块内函数声明是块级绑定,不得落全局对象(作用域隔离,块外不可见)。
if ctx.is_global_scope && ctx.scopes.symbols.scopes.len() == 1 {
self.emit_global_prop_write(&name, var_reg, ctx);
}
Ok(None)