fix: 脚本顶层 var/function 声明落 globalThis(DEFINE_GLOBAL_PROP 非可配置数据属性)

This commit is contained in:
2026-08-10 03:52:17 +08:00
parent a5a910a24e
commit e03ec0f635
11 changed files with 187 additions and 13 deletions
+4 -1
View File
@@ -175,8 +175,11 @@ define_opcodes! {
// rest 参数数组:rd=目标寄存器,b=固定形参数(rest 之前的形参个数)。
CREATE_REST_ARRAY = 0x6E => "CREATE_REST_ARRAY",
// ── define 语义属性写入 (0x6D) ──
// ── define 语义属性写入 (0x6D, 0x6F) ──
DEFINE_PROP = 0x6D => "DEFINE_PROP",
// 全局 var 绑定属性定义:rd=目标对象、a=值、b=键,数据属性可写/可枚举/不可配置
// (脚本顶层 var/function 声明同步到 globalThis 用,属性描述符与规范一致)。
DEFINE_GLOBAL_PROP = 0x6F => "DEFINE_GLOBAL_PROP",
// ── 成员复合赋值:位/移位 (0x64-0x69) ──
COMPOUND_MEMBER_BIT_AND = 0x64 => "COMPOUND_MEMBER_BIT_AND",
+22
View File
@@ -166,6 +166,9 @@ pub struct CompileCtx {
pub(crate) is_generator: bool,
/// 本函数是否为异步函数体(`async function` / async 箭头),`assemble_ir` 回写到 IR。
pub(crate) is_async: bool,
/// 是否为脚本顶层模块(emit_program 的根上下文)。顶层 var/function 声明需
/// 同步写全局对象属性(脚本环境记录的 var 可经 globalThis 反射);函数体为 false。
pub(crate) is_global_scope: bool,
pub(crate) static_block_this_reg: Option<u8>,
pub(crate) field_buffer: Option<FieldBuffer>,
/// 类构造器模块中 `@@field_keys` upvalue 下标(实例字段 computed key 数组)。
@@ -274,6 +277,7 @@ impl CompileCtx {
in_static_method: false,
is_generator: false,
is_async: false,
is_global_scope: false,
static_block_this_reg: None,
field_buffer: None,
field_keys_uv: None,
@@ -1245,12 +1249,30 @@ impl Emitter {
}
}
/// 把脚本顶层 var/function 绑定的当前值同步写入全局对象属性,使顶层声明
/// 可经 `globalThis` 反射(脚本环境记录的 var 绑定全局对象属性)。
///
/// # 边界与前提
/// - 仅顶层模块上下文调用:顶层 `this`(物理寄存器 254)恒为全局对象。
/// - let/const/class 不落全局对象,不得调用本函数。
///
/// # 副作用
/// - 定义全局对象数据属性(可写/可枚举/不可配置),属性缺失时新建。
pub(crate) fn emit_global_prop_write(&self, name: &str, val_reg: u32, ctx: &mut CompileCtx) {
let idx = ctx.add_constant(Constant::String(name.to_string()));
let key_reg = ctx.alloc_reg();
ctx.inst(Inst::load_const(Operand::Reg(key_reg), idx));
ctx.inst(Inst::define_global_prop(Operand::This, Operand::Reg(val_reg), Operand::Reg(key_reg)));
}
/// 把完整程序编译为顶层模块体的 IRFunction。
///
/// 调用方为 `oxide_compiler::Compiler::compile`:本函数完成 emit 半程,
/// 随后由 `oxide_ir::lower::lower` 降为字节码。
pub fn emit_program(&self, program: &oxide_parser::Program) -> Result<IRFunction, String> {
let mut ctx = CompileCtx::new();
// 脚本顶层:var/function 声明需落到全局对象,let/const/class 不进全局。
ctx.is_global_scope = true;
ctx.pre_register_builtins();
self.predeclare_function_declarations(&program.body, &mut ctx);
+12 -4
View File
@@ -74,6 +74,7 @@ impl Emitter {
));
} else if !ctx.with_stack.is_empty() && !ctx.is_with_internal_binding(name) {
// with 内 var 初始化:对象有该属性则写对象,否则写提升槽(动态解析)。
// ponytail: with 内顶层 var 不写全局对象属性,with 语句本身已是稀见用例。
self.emit_with_dynamic_write(name, src_reg, 0, ctx);
} else {
// const 声明路径 STORE_VAR 恒 b=0,不查运行时 guard。
@@ -88,6 +89,10 @@ impl Emitter {
));
}
ctx.init_var(name);
// 脚本顶层 var:同步写全局对象,使 globalThis.x 反射声明值。
if ctx.is_global_scope && matches!(kind, VariableDeclarationKind::Var) {
self.emit_global_prop_write(name, src_reg, ctx);
}
Ok(())
}
@@ -240,9 +245,7 @@ impl Emitter {
}
/// 向 excluded 数组下标 `idx` 写入一个键:`key_val` 为运行时寄存器值,否则用 `key_str` 常量。
fn emit_push_excluded_key(
&self, arr: u32, key_val: Option<u32>, key_str: &str, idx: usize, ctx: &mut CompileCtx,
) {
fn emit_push_excluded_key(&self, arr: u32, key_val: Option<u32>, key_str: &str, idx: usize, ctx: &mut CompileCtx) {
let val_reg = match key_val {
Some(kv) => kv,
None => {
@@ -255,7 +258,12 @@ impl Emitter {
let elem_idx = ctx.alloc_reg();
let cidx = ctx.add_constant(Constant::Int(idx as i32));
ctx.inst(Inst::load_const(Operand::Reg(elem_idx), cidx));
ctx.inst(Inst::new(OpCode::SET_ELEM, Operand::Reg(arr), Operand::Reg(elem_idx), Operand::Reg(val_reg)));
ctx.inst(Inst::new(
OpCode::SET_ELEM,
Operand::Reg(arr),
Operand::Reg(elem_idx),
Operand::Reg(val_reg),
));
}
fn emit_assignment_maybe_default(
@@ -60,6 +60,10 @@ 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 {
self.emit_global_prop_write(&name, var_reg, ctx);
}
Ok(None)
}
}
@@ -61,6 +61,10 @@ impl Emitter {
ctx.inst(Inst::new(OpCode::STORE_VAR, Operand::Reg(target_reg), Operand::Reg(tmp), Operand::None));
}
ctx.init_var(bi.name.as_str());
// 脚本顶层 var 无初始化:仍须写全局对象属性(值为 undefined)。
if ctx.is_global_scope && matches!(decl.kind, VariableDeclarationKind::Var) {
self.emit_global_prop_write(bi.name.as_str(), tmp, ctx);
}
r = Some(var_reg);
}
}
+6 -6
View File
@@ -66,6 +66,7 @@ impl Inst {
| OpCode::DELETE_PROP_DYNAMIC
| OpCode::DEFINE_ACCESSOR
| OpCode::DEFINE_PROP
| OpCode::DEFINE_GLOBAL_PROP
| OpCode::SET_HOME_OBJECT
// SPILL 写 VM spill 栈而非寄存器(恢复由 UNSPILL 写 rd)
| OpCode::SPILL
@@ -231,11 +232,9 @@ impl Inst {
// HALT:隐式读 reg 0(顶层返回值)
OpCode::HALT => uses.push(0),
// RETURN/THROW/YIELD/YIELD_STAR/AWAIT:读 rd(None→0)
OpCode::RETURN
| OpCode::THROW
| OpCode::YIELD
| OpCode::YIELD_STAR
| OpCode::AWAIT => push_operand(&mut uses, &self.rd),
OpCode::RETURN | OpCode::THROW | OpCode::YIELD | OpCode::YIELD_STAR | OpCode::AWAIT => {
push_operand(&mut uses, &self.rd)
}
// 加载族:a 槽是 Const/Imm 立即数,无寄存器 use
OpCode::LOAD_CONST
| OpCode::CREATE_CLOSURE
@@ -264,7 +263,8 @@ impl Inst {
| OpCode::SET_ELEM
| OpCode::INIT_PRIVATE
| OpCode::DEFINE_ACCESSOR
| OpCode::DEFINE_PROP => {
| OpCode::DEFINE_PROP
| OpCode::DEFINE_GLOBAL_PROP => {
push_operand(&mut uses, &self.rd);
push_operand(&mut uses, &self.a);
push_operand(&mut uses, &self.b);
+6
View File
@@ -197,6 +197,12 @@ impl Inst {
Self::new(OpCode::DEFINE_PROP, target, value, key)
}
/// 定义全局 var 绑定数据属性:target 为全局对象,value/key 为寄存器。
/// 属性可写/可枚举/不可配置(脚本顶层 var/function 声明的属性描述符)。
pub fn define_global_prop(target: Operand, value: Operand, key: Operand) -> Self {
Self::new(OpCode::DEFINE_GLOBAL_PROP, target, value, key)
}
/// 静态删除属性:obj 同时放 rd/a 槽,const_idx 为属性名常量下标。
pub fn delete_prop_static(obj: Operand, const_idx: u32) -> Self {
Self::with_ext(OpCode::DELETE_PROP_STATIC, obj, obj, Operand::None, &[const_idx])
+5 -2
View File
@@ -458,7 +458,7 @@ fn run_test_inner(
if is_async {
// 异步测试:注入 print 捕获 + $DONE(asyncTests 用 $DONE 报告结果)。
// 结果写入 globalThis 全局字符串,run() 结束后由 runner 读取并按 marker 判定。
// (顶层 var 不落 global 对象,须显式写 globalThis。)
// print 为顶层函数声明,经全局对象反射落 globalThis;此处仅初始化累加器。
append_source_chunk(
&mut code,
"async capture",
@@ -578,7 +578,10 @@ fn judge_async_result(
return TestResult::fail(
path.to_path_buf(),
dur,
format!("expected runtime error ({}), got: async complete", meta.negative.as_ref().unwrap().error_type),
format!(
"expected runtime error ({}), got: async complete",
meta.negative.as_ref().unwrap().error_type
),
);
}
return TestResult::pass(path.to_path_buf(), dur, "async ok");
+4
View File
@@ -1318,6 +1318,10 @@ impl Vm {
self.dispatch_define_prop(rd, a, b)?;
}
OpCode::DEFINE_GLOBAL_PROP => {
self.dispatch_define_global_prop(rd, a, b)?;
}
OpCode::RETURN => match self.dispatch_return(rd) {
Ok(Some(result)) => return Ok(result),
Ok(None) => {}
+20
View File
@@ -178,6 +178,26 @@ impl Vm {
}
}
/// 定义全局 var 绑定数据属性:rd=全局对象,a=值,b=键。
/// 属性可写/可枚举/不可配置(CreateGlobalVarBinding 的属性描述符)。
///
/// # 边界与前提
/// - 已有同名不可写属性(NaN/undefined/Infinity 等内置)时静默跳过:规范中
/// var 绑定仍建立(寄存器侧),全局对象属性保持原样,不抛错。
/// - 已有同名可写数据属性(重复 var 声明)时按定义更新值。
pub(crate) fn dispatch_define_global_prop(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> {
vm_trace!("DEFINE_GLOBAL_PROP rd={} value={} key={}", rd, a, b);
let obj_val = self.regs[rd];
if !obj_val.is_object() {
return Ok(());
}
let prop_name_si = self.property_key_si(self.regs[b]);
let value = self.regs[a];
let obj = unsafe { &mut *obj_val.as_js_object_ptr() };
let _ = self.define_data_property(obj, prop_name_si, value, PropAttributes::new(true, true, false));
Ok(())
}
/// break 完成:`crossed`(rd 槽)为 emit 词法算出的逃出 finally 域数。
/// 逐个穿越 finally 后跳转到目标;crossed 为 0 时直接跳转。
pub(crate) fn dispatch_break(&mut self, instr: u32) {
@@ -0,0 +1,100 @@
//! 脚本顶层 var/function 声明落 globalThis 的运行时行为测试。
use oxide_compiler::compiler::Compiler;
use oxide_parser::Allocator;
use oxide_vm::vm::Vm;
fn eval_truthy(source: &str) {
let allocator = Allocator::default();
let program = oxide_parser::parse(&allocator, source).expect("parse");
let module = Compiler::new().compile(&program).expect("compile");
let mut vm = Vm::new();
let result = vm.run(&module).expect("run");
assert!(result.is_bool() && result.as_bool(), "expected true, got: {result:?}\nsource: {source}");
}
fn eval_string(source: &str) -> String {
let allocator = Allocator::default();
let program = oxide_parser::parse(&allocator, source).expect("parse");
let module = Compiler::new().compile(&program).expect("compile");
let mut vm = Vm::new();
let result = vm.run(&module).expect("run");
vm.lookup_str(result).unwrap_or_default()
}
#[test]
fn top_level_var_lands_on_global_this() {
eval_truthy("var x = 5; globalThis.x === 5");
eval_truthy("var y = 10; typeof globalThis.y === 'number'");
}
#[test]
fn top_level_function_lands_on_global_this() {
eval_truthy("function foo(){ return 1; } globalThis.foo() === 1");
eval_truthy("function foo(){ return 1; } typeof globalThis.foo === 'function'");
}
#[test]
fn let_const_class_do_not_land_on_global_this() {
eval_truthy("let z = 5; typeof globalThis.z === 'undefined'");
eval_truthy("const c = 1; typeof globalThis.c === 'undefined'");
eval_truthy("class C {} typeof globalThis.C === 'undefined'");
}
#[test]
fn function_local_var_does_not_land_on_global_this() {
eval_truthy("function f(){ var v = 1; } f(); typeof globalThis.v === 'undefined'");
}
#[test]
fn block_level_var_lands_on_global_this() {
eval_truthy("{ var b = 2; } globalThis.b === 2");
}
#[test]
fn redeclaration_updates_global_property() {
eval_truthy("var x = 1; var x = 2; globalThis.x === 2");
}
#[test]
fn var_global_property_is_non_configurable() {
eval_truthy("var x = 1; Object.getOwnPropertyDescriptor(globalThis, 'x').configurable === false");
eval_truthy("var x = 1; Object.getOwnPropertyDescriptor(globalThis, 'x').enumerable === true");
eval_truthy("var x = 1; delete globalThis.x === false");
eval_truthy("var x = 1; delete globalThis.x; typeof globalThis.x === 'number'");
}
#[test]
fn var_name_shows_in_get_own_property_names() {
eval_truthy("var x = 1; Object.getOwnPropertyNames(globalThis).indexOf('x') >= 0");
eval_truthy("function foo(){} Object.getOwnPropertyNames(globalThis).indexOf('foo') >= 0");
}
#[test]
fn function_declared_var_lands_after_user_assignment() {
eval_truthy("globalThis.x = 99; var x = 5; globalThis.x === 5");
}
#[test]
fn async_harness_done_pattern() {
// asyncHelpers.js 依赖顶层 function $DONE 落 globalThis。
eval_truthy("function $DONE(){} Object.prototype.hasOwnProperty.call(globalThis, '$DONE')");
eval_truthy("function $DONE(){} typeof globalThis.$DONE === 'function'");
}
#[test]
fn var_without_initializer_creates_undefined_property() {
eval_truthy("var x; typeof globalThis.x === 'undefined'");
eval_truthy("var x; Object.prototype.hasOwnProperty.call(globalThis, 'x')");
}
#[test]
fn function_internal_reference_still_works() {
eval_truthy("function foo(){ return 1; } var r = foo(); r === 1");
}
#[test]
fn descriptor_attributes_match_spec() {
let d = eval_string("var x = 1; JSON.stringify(Object.getOwnPropertyDescriptor(globalThis, 'x'))");
assert_eq!(d, r#"{"value":1,"writable":true,"enumerable":true,"configurable":false}"#);
}