mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
fix: 未声明标识符读取抛 ReferenceError(typeof 特判返回 undefined)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user