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

This commit is contained in:
2026-08-16 00:10:08 +08:00
parent 69152ad608
commit 75153a909d
9 changed files with 287 additions and 6 deletions
+11
View File
@@ -368,6 +368,17 @@ define_opcodes! {
LOAD_CONST = 0x32 => "LOAD_CONST",
def = Some(SlotSpec::Slot(Slot::Rd)), uses = [],
pure = true, jump = false, term = false, ic = false,
// 未声明标识符读:rd=目标,a/b=常量池 key 下标(imm16,与 LOAD_CONST 同构),
// 运行期查 global object 属性存在性,缺失抛 ReferenceError(热路径零开销:
// 已声明/内置标识符仍走 LOAD_VAR,本指令只命中真正未声明的读)。
LOAD_GLOBAL = 0x95 => "LOAD_GLOBAL",
def = Some(SlotSpec::Slot(Slot::Rd)), uses = [],
pure = false, jump = false, term = false, ic = false,
// typeof 未声明标识符特判:同 LOAD_GLOBAL 查 global object 属性,但缺失时
// 求值为 undefined 而非抛 ReferenceError(IsUnresolvableReference 语义)。
LOAD_GLOBAL_TYPEOF = 0x96 => "LOAD_GLOBAL_TYPEOF",
def = Some(SlotSpec::Slot(Slot::Rd)), uses = [],
pure = false, jump = false, term = false, ic = false,
// ── 调用 (0x40-0x4F) ──
CALL = 0x40 => "CALL", // 结果隐式写 reg 0;rd=callee, a=this, b=首参, ext[0]=nargs
+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)
+2
View File
@@ -738,6 +738,8 @@ mod tests {
assert_contract(OpCode::LOAD_VAR, Some(1), &[2], true);
assert_contract(OpCode::STORE_VAR, Some(1), &[2], false);
assert_contract(OpCode::LOAD_CONST, Some(1), &[], true);
assert_contract(OpCode::LOAD_GLOBAL, Some(1), &[], false);
assert_contract(OpCode::LOAD_GLOBAL_TYPEOF, Some(1), &[], false);
assert_contract(OpCode::LOAD_UPVALUE, Some(1), &[], true);
assert_contract(OpCode::CREATE_CLOSURE, Some(1), &[], true);
assert_contract(OpCode::STORE_UPVALUE, None, &[2], false);
+50
View File
@@ -21,6 +21,56 @@ impl Vm {
}
}
/// 未声明标识符读:查 global object 属性存在性,命中取属性值,未命中抛 ReferenceError。
///
/// # 步骤
/// 1. imm16 取常量池 key(标识符名字符串),解析为属性键 si。
/// 2. `resolve_property` 沿 shape 链 + 原型链解析 global 属性(覆盖全部属性存储)。
/// 3. 命中写 rd;未命中 raise ReferenceError(`{name} is not defined`)。
///
/// # 边界与前提
/// - key 常量必须是字符串(emit 侧保证);非字符串按错误返回。
/// - 属性存在但值为 undefined(显式赋 undefined)时返回 undefined,不抛。
pub(crate) fn dispatch_load_global(&mut self, rd: usize, instr: u32) -> Result<bool, String> {
let idx = (instr >> 16) as usize;
vm_trace!("LOAD_GLOBAL rd={} idx={}", rd, idx);
let key_val = self.immutables().get(idx).copied().unwrap_or(JsValue::undefined());
if !key_val.is_string() {
return Err(format!("LOAD_GLOBAL constant index {idx} is not a string key"));
}
let si = self.property_key_si(key_val)?;
let global = self.session.global_object();
if let Some(val) = self.resolve_property(global, si) {
self.regs[rd] = val;
Ok(false)
} else {
// SAFETY: key_val 已校验为字符串值,as_str 桥接 JsString 内容。
let name = unsafe { (*key_val.as_string_ptr()).as_str() };
self.raise_error_kind("ReferenceError", &format!("{name} is not defined"))?;
Ok(true)
}
}
/// typeof 未声明标识符读:同 [`dispatch_load_global`] 查 global object 属性,
/// 但未命中求值为 undefined(IsUnresolvableReference)而非抛 ReferenceError。
///
/// # 步骤
/// 1. imm16 取常量池 key(标识符名字符串),解析为属性键 si。
/// 2. `resolve_property` 沿 shape 链 + 原型链解析 global 属性。
/// 3. 命中写 rd;未命中写 undefined。
pub(crate) fn dispatch_load_global_typeof(&mut self, rd: usize, instr: u32) -> Result<(), String> {
let idx = (instr >> 16) as usize;
vm_trace!("LOAD_GLOBAL_TYPEOF rd={} idx={}", rd, idx);
let key_val = self.immutables().get(idx).copied().unwrap_or(JsValue::undefined());
if !key_val.is_string() {
return Err(format!("LOAD_GLOBAL_TYPEOF constant index {idx} is not a string key"));
}
let si = self.property_key_si(key_val)?;
let global = self.session.global_object();
self.regs[rd] = self.resolve_property(global, si).unwrap_or(JsValue::undefined());
Ok(())
}
#[inline(always)]
pub(crate) fn dispatch_typeof(&mut self, rd: usize, a: usize) {
vm_trace!("TYPEOF rd={} r{}={:?}", rd, a, self.regs[a]);
+10
View File
@@ -1423,6 +1423,16 @@ impl Vm {
self.dispatch_load_const(rd, instr)?;
}
OpCode::LOAD_GLOBAL => match self.dispatch_load_global(rd, instr) {
Ok(true) => continue,
Ok(false) => {}
Err(e) => return Err(e),
},
OpCode::LOAD_GLOBAL_TYPEOF => {
self.dispatch_load_global_typeof(rd, instr)?;
}
OpCode::CREATE_CLOSURE => {
self.dispatch_create_closure(rd, instr)?;
}
@@ -0,0 +1,145 @@
//! 未声明标识符读的 ReferenceError 语义(B031 Step B 落地)。
//!
//! 覆盖:读未声明抛 ReferenceError;typeof 未声明返回 "undefined";
//! sloppy 写后读正常;harness 全局 var 声明安全;内置标识符不误判。
use oxide_compiler::compiler::Compiler;
use oxide_types::value::JsValue;
use oxide_vm::vm::Vm;
fn eval(source: &str) -> Result<JsValue, String> {
let allocator = oxide_parser::Allocator::default();
let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse: {:?}", e))?;
let module = Compiler::new().compile(&program).map_err(|e| format!("Compile: {}", e))?;
let mut vm = Vm::new();
vm.run(&module)
}
/// 求值并取字符串结果内容(字符串值走 lookup_str,非字符串走 Display)。
fn eval_str(source: &str) -> Result<String, String> {
let mut vm = Vm::new();
let allocator = oxide_parser::Allocator::default();
let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse: {:?}", e))?;
let module = Compiler::new().compile(&program).map_err(|e| format!("Compile: {}", e))?;
let result = vm.run(&module)?;
Ok(vm.lookup_str(result).unwrap_or_default())
}
#[test]
fn undeclared_read_throws_reference_error() {
// 未声明标识符读取:ReferenceError 且消息含名字(与 V8 一致)。
let err = eval("x").unwrap_err();
assert!(err.contains("ReferenceError") && err.contains("x is not defined"), "got: {}", err);
}
#[test]
fn undeclared_read_throws_inside_block() {
let err = eval("{ x; }").unwrap_err();
assert!(err.contains("ReferenceError") && err.contains("x is not defined"), "got: {}", err);
}
#[test]
fn undeclared_read_throws_inside_function() {
let err = eval("function f(){ return x; } f()").unwrap_err();
assert!(err.contains("ReferenceError") && err.contains("x is not defined"), "got: {}", err);
}
#[test]
fn sloppy_write_then_read_ok() {
// sloppy 写先登记全局槽:写后读命中符号表,G6 不破坏写读一致性。
let result = eval("x = 5; x").unwrap();
assert_eq!(result.as_int(), 5);
}
#[test]
fn typeof_undeclared_is_undefined() {
let result = eval_str("typeof x").unwrap();
assert_eq!(result, "undefined");
}
#[test]
fn typeof_undeclared_parenthesized_is_undefined() {
// 括号不改变标识符引用语义,typeof (x) 同样返回 "undefined"。
let result = eval_str("typeof (x)").unwrap();
assert_eq!(result, "undefined");
}
#[test]
fn typeof_undeclared_inside_function_is_undefined() {
let result = eval_str("function f(){ return typeof q; } f()").unwrap();
assert_eq!(result, "undefined");
}
#[test]
fn typeof_before_var_decl_is_undefined() {
// var 提升后声明点前读:槽值 undefined,typeof 返回 "undefined"。
let result = eval_str("var x; typeof x").unwrap();
assert_eq!(result, "undefined");
}
#[test]
fn typeof_undeclared_isolated_from_block_scope() {
// 块内 let 不同作用域(已弹出),外层 typeof 仍视为未声明。
let result = eval_str("{ let x; } typeof x").unwrap();
assert_eq!(result, "undefined");
}
#[test]
fn typeof_tdz_binding_throws() {
// G2 回归防护:typeof 对 TDZ 绑定抛 ReferenceError(非 "undefined")。
let err = eval("typeof x; let x;").unwrap_err();
assert!(err.contains("ReferenceError"), "got: {}", err);
}
#[test]
fn typeof_after_sloppy_write_is_value_type() {
// 写路径登记的全局槽走 LOAD_VAR,typeof 读到真实值。
let result = eval_str("x = 5; typeof x").unwrap();
assert_eq!(result, "number");
}
#[test]
fn typeof_captured_upvalue_ok() {
// 闭包捕获的 upvalue 不走未声明分支,typeof 读到外层值。
let result = eval_str("function outer(){ var x = 1; return function(){ return typeof x; }; } outer()()").unwrap();
assert_eq!(result, "number");
}
#[test]
fn harness_global_var_decl_safe() {
// harness 以 var 声明注入的全局($DONE 等)不误抛。
let result = eval("var $DONE = 1; $DONE").unwrap();
assert_eq!(result.as_int(), 1);
}
#[test]
fn builtin_global_not_misjudged() {
// 内置标识符(builtin_reg_map 命中 global)不误判为未声明。
let result = eval_str("JSON.stringify({a:1})").unwrap();
assert_eq!(result, r#"{"a":1}"#);
}
#[test]
fn builtin_undefined_nan_infinity_reads_ok() {
// undefined/NaN/Infinity 是 global object 属性,读取不抛。
let result = eval("undefined").unwrap();
assert!(result.is_undefined());
let result = eval("NaN").unwrap();
assert!(result.is_double() && result.as_double().is_nan());
let result = eval("Infinity").unwrap();
assert!(result.is_double() && result.as_double().is_infinite());
}
#[test]
fn global_object_property_read_fresh() {
// 读未声明标识符时运行期查 global object:属性后置创建也能读到(非帧入口快照)。
let result = eval("globalThis.foo = 42; foo").unwrap();
assert_eq!(result.as_int(), 42);
}
#[test]
fn block_shadow_after_implicit_read_ok() {
// 块作用域同名新绑定持不同槽位,不被隐式全局集合误判(按寄存器记录)。
let result = eval("globalThis.b = 42; b; { let b = 7; b; }").unwrap();
assert_eq!(result.as_int(), 7);
}