diff --git a/crates/oxide_bytecode/src/opcode.rs b/crates/oxide_bytecode/src/opcode.rs index c3a932f2..d4dce56d 100644 --- a/crates/oxide_bytecode/src/opcode.rs +++ b/crates/oxide_bytecode/src/opcode.rs @@ -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 diff --git a/crates/oxide_emit/src/emit.rs b/crates/oxide_emit/src/emit.rs index 792c54f9..7bba8bb3 100644 --- a/crates/oxide_emit/src/emit.rs +++ b/crates/oxide_emit/src/emit.rs @@ -186,6 +186,11 @@ pub struct CompileCtx { /// 本函数从父函数捕获的 const 绑定名:子 ctx 不继承父函数作用域符号表, /// 捕获 const 信息随 upvalue 收集一并快照,供 const 写检查(编译期拦截)使用。 pub(crate) upvalue_const_flags: HashSet, + /// 未声明标识符读所分配的全局槽寄存器集合:标识符首次读未命中任何作用域时, + /// `lookup_or_builtin` 按隐式全局登记并记录其寄存器,后续读取据此发射 + /// LOAD_GLOBAL(运行期查 global object 属性,缺失抛 ReferenceError)。 + /// 按寄存器而非名字记录:块作用域同名新绑定持不同槽位,不会被误判为隐式全局。 + pub(crate) implicit_global_reads: HashSet, /// 函数 `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 { 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), diff --git a/crates/oxide_emit/src/expr/identifier.rs b/crates/oxide_emit/src/expr/identifier.rs index 2561698c..5de025c5 100644 --- a/crates/oxide_emit/src/expr/identifier.rs +++ b/crates/oxide_emit/src/expr/identifier.rs @@ -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) } diff --git a/crates/oxide_emit/src/expr/operator/mod.rs b/crates/oxide_emit/src/expr/operator/mod.rs index 980753a0..00caea1e 100644 --- a/crates/oxide_emit/src/expr/operator/mod.rs +++ b/crates/oxide_emit/src/expr/operator/mod.rs @@ -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 { + 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 { diff --git a/crates/oxide_emit/src/stmt/declaration/function_decl.rs b/crates/oxide_emit/src/stmt/declaration/function_decl.rs index 39a0f336..3f9e164f 100644 --- a/crates/oxide_emit/src/stmt/declaration/function_decl.rs +++ b/crates/oxide_emit/src/stmt/declaration/function_decl.rs @@ -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) diff --git a/crates/oxide_ir/src/contract.rs b/crates/oxide_ir/src/contract.rs index 3775c8e1..bc44c79e 100644 --- a/crates/oxide_ir/src/contract.rs +++ b/crates/oxide_ir/src/contract.rs @@ -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); diff --git a/crates/oxide_vm/src/dispatch/misc.rs b/crates/oxide_vm/src/dispatch/misc.rs index a86549f0..0ccef61f 100644 --- a/crates/oxide_vm/src/dispatch/misc.rs +++ b/crates/oxide_vm/src/dispatch/misc.rs @@ -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 { + 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]); diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index d110448a..f8a2351c 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -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)?; } diff --git a/crates/oxide_vm/tests/undeclared_read_tests.rs b/crates/oxide_vm/tests/undeclared_read_tests.rs new file mode 100644 index 00000000..09377b84 --- /dev/null +++ b/crates/oxide_vm/tests/undeclared_read_tests.rs @@ -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 { + 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 { + 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); +}