From 44c16cab8a583f68d5e9b4090a667cfb04dd831e Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sat, 12 Sep 2026 15:48:32 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=A1=B6=E5=B1=82=20var=20=E8=AF=BB?= =?UTF-8?q?=E5=86=99=E7=9B=B4=E8=BF=9E=E5=85=A8=E5=B1=80=E5=AF=B9=E8=B1=A1?= =?UTF-8?q?=E5=B1=9E=E6=80=A7=E5=8D=95=E4=B8=80=E7=9C=9F=E5=80=BC=E2=80=94?= =?UTF-8?q?=E2=80=94GDI=20=E5=BA=8F=E8=A8=80=20+=20=E5=A3=B0=E6=98=8E?= =?UTF-8?q?=E5=A4=84=E5=90=8C=E6=AD=A5=E5=88=A0=E9=99=A4=EF=BC=8C=E7=B1=BB?= =?UTF-8?q?=E5=8E=9F=E5=9E=8B=20session=20=E7=9B=B4=E5=88=86=E9=98=B2?= =?UTF-8?q?=E6=99=8B=E5=8D=87=E5=85=8B=E9=9A=86=20identity=20=E5=88=86?= =?UTF-8?q?=E8=A3=82=EF=BC=8C=E8=87=AA=E5=A2=9E=E5=87=8F=20int=20=E4=BF=9D?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_bytecode/src/opcode.rs | 20 +++++ crates/oxide_emit/src/class/prototype.rs | 9 ++- crates/oxide_emit/src/emit.rs | 67 ++++++++++++++-- crates/oxide_emit/src/expr/assignment.rs | 22 +++-- crates/oxide_emit/src/expr/identifier.rs | 22 ++++- crates/oxide_emit/src/expr/operator/mod.rs | 17 ++-- crates/oxide_emit/src/shared/binding.rs | 18 +++++ crates/oxide_emit/src/stmt/declaration/var.rs | 32 -------- .../oxide_emit/src/stmt/iteration/for_in.rs | 10 +++ crates/oxide_ir/src/contract.rs | 11 ++- crates/oxide_ir/src/inst.rs | 19 +++++ crates/oxide_vm/src/dispatch/arithmetic.rs | 44 ++++++++++ crates/oxide_vm/src/dispatch/misc.rs | 21 +++++ crates/oxide_vm/src/ic_helper.rs | 1 + crates/oxide_vm/src/session_gc.rs | 14 +++- crates/oxide_vm/src/vm.rs | 14 ++++ crates/oxide_vm/src/vm_dispatch_ctrl.rs | 64 +++++++++++++++ crates/oxide_vm/src/vm_support.rs | 11 ++- crates/oxide_vm/tests/gc_runtime_tests.rs | 19 +++-- .../tests/global_var_binding_tests.rs | 80 +++++++++++++++++++ crates/oxide_vm/tests/session_arena_tests.rs | 6 +- 21 files changed, 447 insertions(+), 74 deletions(-) diff --git a/crates/oxide_bytecode/src/opcode.rs b/crates/oxide_bytecode/src/opcode.rs index 9403b9e4..c5fb14a8 100644 --- a/crates/oxide_bytecode/src/opcode.rs +++ b/crates/oxide_bytecode/src/opcode.rs @@ -546,6 +546,26 @@ define_opcodes! { DEFINE_GLOBAL_PROP_C = 0x98 => "DEFINE_GLOBAL_PROP_C", def = None, uses = [SlotSpec::Slot(Slot::A)], pure = false, jump = false, term = false, ic = false, + // 顶层 var 声明的全局属性 define-if-absent(GDI 序言):rd=目标对象(顶层 + // This=全局对象)、a=值、b=键。既有属性(数据或 accessor)零动作—— + // CreateGlobalVarBinding 对既有数据描述符零修改(可写/不可写/配置位均不更新); + // 缺失新建可写/可枚举/不可配置数据属性(脚本 var 的 c:false 描述符)。 + DEFINE_GLOBAL_PROP_IF_ABSENT = 0x99 => "DEFINE_GLOBAL_PROP_IF_ABSENT", + def = None, uses = [SlotSpec::Slot(Slot::Rd), SlotSpec::Slot(Slot::A), SlotSpec::Slot(Slot::B)], + pure = false, jump = false, term = false, ic = false, + // eval 脚本顶层 var 声明的全局属性 define-if-absent:ext 字 = 键常量池下标 + // (u16)、a 槽 = 值寄存器;全局对象由 session 解析(不依赖 this)。既有属性 + // 零动作,缺失新建可写/可枚举/可配置(eval 传 deterministic=true)。 + DEFINE_GLOBAL_PROP_C_IF_ABSENT = 0x9A => "DEFINE_GLOBAL_PROP_C_IF_ABSENT", + def = None, uses = [SlotSpec::Slot(Slot::A)], + pure = false, jump = false, term = false, ic = false, + // session 直分普通对象:寿命 session 级(session GC mark/sweep 回收),逃逸写 + // 不触发晋升克隆。类原型等内建形对象要求与构造器 prototype 属性两侧 identity + // 一致——epoch 分配会在逃逸写晋升时克隆出第二份分裂 identity(同函数 + // prototype 子对象的 session 分配同因)。 + NEW_SESSION_OBJECT = 0x9B => "NEW_SESSION_OBJECT", + def = Some(SlotSpec::Slot(Slot::Rd)), uses = [], + pure = true, jump = false, term = false, ic = false, // ── 成员复合赋值:位/移位 (0x64-0x69) ── COMPOUND_MEMBER_BIT_AND = 0x64 => "COMPOUND_MEMBER_BIT_AND", diff --git a/crates/oxide_emit/src/class/prototype.rs b/crates/oxide_emit/src/class/prototype.rs index a4ad498a..5b6df8c3 100644 --- a/crates/oxide_emit/src/class/prototype.rs +++ b/crates/oxide_emit/src/class/prototype.rs @@ -11,7 +11,14 @@ impl Emitter { &self, ctor_reg: u32, proto_reg: u32, super_reg: Option, sub_idx: u16, ctx: &mut CompileCtx, ) -> Result<(), String> { ctx.inst(Inst::create_closure(Operand::Reg(ctor_reg), sub_idx)); - ctx.inst(Inst::new(OpCode::NEW_OBJECT, Operand::Reg(proto_reg), Operand::None, Operand::None)); + // 类原型 session 直分:实例 [[Prototype]] 与 Class.prototype 两侧须同一 + // 对象,epoch 分配在逃逸写晋升时克隆出第二份分裂 identity。 + ctx.inst(Inst::new( + OpCode::NEW_SESSION_OBJECT, + Operand::Reg(proto_reg), + Operand::None, + Operand::None, + )); if let Some(super_reg) = super_reg { let proto_key_idx = ctx.add_constant(Constant::String("prototype".to_string())); let parent_proto_key_reg = ctx.alloc_reg(); diff --git a/crates/oxide_emit/src/emit.rs b/crates/oxide_emit/src/emit.rs index 0db842f0..3170cb37 100644 --- a/crates/oxide_emit/src/emit.rs +++ b/crates/oxide_emit/src/emit.rs @@ -207,6 +207,10 @@ pub struct CompileCtx { /// 捕获判断(MAKE_CELL / CELL_GET / CELL_SET)与子函数 upvalue cell_idx 统一查此映射, /// 消除符号表时序依赖与 cell 索引错位。 pub(crate) captured_bindings: BTreeMap, + /// 顶层已声明 var 名(脚本全局 GDI 提升名):裸读走全局对象属性(LOAD_GLOBAL)、 + /// 裸写走描述符感知 A 侧写,单一真值不落镜像 cell。仅脚本顶层 emit_program 计算, + /// 逐层继承给嵌套函数(嵌套函数的局部同名遮蔽不属此集,由作用域索引判定)。 + pub(crate) global_tier_names: HashSet, /// 本函数从父函数捕获的 const 绑定名:子 ctx 不继承父函数作用域符号表, /// 捕获 const 信息随 upvalue 收集一并快照,供 const 写检查(编译期拦截)使用。 pub(crate) upvalue_const_flags: HashSet, @@ -344,6 +348,7 @@ impl CompileCtx { current_upvalue_captures: Vec::new(), own_bindings: HashSet::new(), captured_bindings: BTreeMap::new(), + global_tier_names: HashSet::new(), upvalue_const_flags: HashSet::new(), implicit_global_reads: HashSet::new(), implicit_global_writes: HashSet::new(), @@ -1140,6 +1145,9 @@ impl Emitter { // 一并继承——读侧登记的全局槽与写侧同属一个槽,写判定跨嵌套函数一致。 ctx.implicit_global_writes = parent_ctx.implicit_global_writes.clone(); ctx.implicit_global_reads = parent_ctx.implicit_global_reads.clone(); + // 顶层已声明 var 名集随作用域继承:嵌套函数裸读/写顶层 var 直连全局对象 + // (经继承的 scope-0 绑定 + 作用域索引判定,不被局部同名遮蔽误判)。 + ctx.global_tier_names = parent_ctx.global_tier_names.clone(); for (name, reg) in extra_bindings { ctx.scopes.symbols.scopes[0].bindings.insert( (*name).to_string(), @@ -1578,6 +1586,45 @@ impl Emitter { } } + /// 名字是否为顶层已声明 var(A 侧单一真值):在顶层 var 名集内,且当前解析 + /// 绑定落在全局作用域(scope 0)——嵌套函数内的局部同名遮蔽命中更高作用域, + /// 判定为局部而非顶层,走既有 cell/寄存器路径。 + pub(crate) fn is_global_tier_name(&self, ctx: &CompileCtx, name: &str) -> bool { + ctx.global_tier_names.contains(name) && matches!(ctx.scopes.symbols.lookup_any_binding(name), Some((_, 0))) + } + + /// 顶层已声明 var 的裸写落到全局对象属性(A 侧单一真值):顶层普通脚本经 + /// This=全局对象走 0x6F;顶层 eval 与嵌套函数 This≠全局对象,经 session 解析 + /// 走 0x98(c:true,不升级既有 c:false 描述符,只更值)。 + pub(crate) fn emit_tier_global_write(&self, name: &str, val_reg: u32, ctx: &mut CompileCtx) { + let idx = ctx.add_constant(Constant::String(name.to_string())); + if ctx.is_global_scope && !ctx.is_eval_script { + 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))); + } else { + ctx.inst(Inst::define_global_prop_c(Operand::Reg(val_reg), idx)); + } + } + + /// GDI 序言:顶层 var 全局属性 define-if-absent。既有属性(数据或 accessor) + /// 零动作——CreateGlobalVarBinding 对既有数据描述符零修改(不更值);缺失新建。 + /// 顶层普通脚本经 This 走 0x99;eval 经 session 走 0x9A。 + pub(crate) fn emit_global_prop_write_if_absent(&self, name: &str, val_reg: u32, ctx: &mut CompileCtx) { + let idx = ctx.add_constant(Constant::String(name.to_string())); + if ctx.is_eval_script { + ctx.inst(Inst::define_global_prop_c_if_absent(Operand::Reg(val_reg), idx)); + } else { + let key_reg = ctx.alloc_reg(); + ctx.inst(Inst::load_const(Operand::Reg(key_reg), idx)); + ctx.inst(Inst::define_global_prop_if_absent( + Operand::This, + Operand::Reg(val_reg), + Operand::Reg(key_reg), + )); + } + } + /// 把完整程序编译为顶层模块体的 IRFunction。 /// /// 调用方为 `oxide_compiler::Compiler::compile`:本函数完成 emit 半程, @@ -1610,7 +1657,12 @@ impl Emitter { // 闭包捕获分析(AST 级,emit 前确定) ctx.own_bindings = self.collect_own_binding_names(&[], &program.body); + // 顶层已声明 var 名(A 侧单一真值):裸读走全局对象属性、裸写走描述符感知 + // A 侧写,不落镜像 cell——从捕获集剔除,使嵌套函数经继承 scope-0 直连全局。 + // 仅在此顶层调用点过滤:嵌套函数的局部同名遮蔽是独立绑定,其调用点不过滤。 + ctx.global_tier_names = self.collect_var_binding_names(&program.body); ctx.captured_bindings = self.collect_captured_bindings(&program.body, &[], &ctx.own_bindings); + ctx.captured_bindings.retain(|n, _| !ctx.global_tier_names.contains(n)); // 顶层 var 入口实例化:被捕获的 var 名统一 MAKE_CELL(undefined),使 var // 声明语句执行前创建的闭包读取到 undefined(脚本 GlobalDeclarationInstantiation @@ -1637,12 +1689,13 @@ impl Emitter { // 全局声明实例化序言:脚本求值前为顶层 var 名创建全局对象属性(值 undefined), // 使声明语句执行前的读取(typeof、反射、自引用)可经全局对象见绑定;同名 // 函数声明的属性由首个 sub-pass 以函数值覆盖,声明语句保持值更新语义。 - // builtin 名的全局属性运行期预存(session 绑定):序言值写是单 opcode 值 - // 混叠,直接写 undefined 会把现存值(NaN 等)抹掉,故以 builtin 镜像槽 - // (run 起点预载全局属性值)作为写入值——CreateGlobalVarBinding 不更新 - // 既有数据描述符(0x6F 不可写分支 no-op),值幂等保留;裸读走镜像、 - // 反射见预存属性。 - let gdi_var_names = self.collect_var_binding_names(&program.body); + // CreateGlobalVarBinding 对既有属性零动作:define-if-absent 只在属性缺失时 + // 新建,可写/不可写/可配置既有属性(含值)一律保留。builtin 名的全局属性 + // 运行期预存(session 绑定):缺失分支写入值取 builtin 镜像槽(run 起点 + // 预载全局属性值),既有属性零动作,值幂等保留。 + // 复用顶层 var 名集(global_tier_names,与捕获集剔除同源);克隆到本地 + // 避免序言循环内 &mut ctx 与名集借用冲突。 + let gdi_var_names = ctx.global_tier_names.clone(); if !gdi_var_names.is_empty() { let undef_reg = self.emit_undefined(&mut ctx); for name in &gdi_var_names { @@ -1652,7 +1705,7 @@ impl Emitter { } else { undef_reg }; - self.emit_global_prop_write(name, value_reg, &mut ctx); + self.emit_global_prop_write_if_absent(name, value_reg, &mut ctx); } } diff --git a/crates/oxide_emit/src/expr/assignment.rs b/crates/oxide_emit/src/expr/assignment.rs index 69befe1a..25458ebf 100644 --- a/crates/oxide_emit/src/expr/assignment.rs +++ b/crates/oxide_emit/src/expr/assignment.rs @@ -95,15 +95,15 @@ impl Emitter { ctx.inst(Inst::new(op, Operand::Reg(val_reg), Operand::Reg(rhs), Operand::None)); return Ok(val_reg); } + let is_tier = self.is_global_tier_name(ctx, name); let is_implicit = ctx.is_implicit_global_reg(var_reg); if is_implicit && ctx.is_strict { // 严格模式未声明复合写:未解析引用不可 put,值无关抛 ReferenceError。 return self.emit_strict_undeclared_write(name, ctx); } - // 未声明名槽是入口快照(读侧登记的槽从不被读刷新,同脚本后续写 - // 可使其脱节):RMW 前从全局对象属性取旧值,属性缺失按 undefined - // (sloppy 未解析引用 GetBaseValue 语义,不抛)。 - if is_implicit { + // RMW 前从全局对象属性取旧值:tier 名(顶层已声明 var)与未声明名同, + // 属性缺失按 undefined(GetBaseValue 语义,不抛)。 + if is_tier || is_implicit { let key_idx = ctx.add_constant(Constant::String(name.to_string())); ctx.inst(Inst::new( OpCode::LOAD_GLOBAL_TYPEOF, @@ -113,6 +113,10 @@ impl Emitter { )); } ctx.inst(Inst::new(op, Operand::Reg(var_reg), Operand::Reg(rhs), Operand::None)); + if is_tier { + // 顶层已声明 var 复合赋值:新值落全局对象属性(A 侧单一真值)。 + self.emit_tier_global_write(name, var_reg, ctx); + } if is_implicit { self.emit_implicit_global_write(name, var_reg, ctx); } @@ -380,9 +384,9 @@ impl Emitter { )); } else { let var_reg = ctx.lookup_or_global(name); - // 未声明名槽是入口快照(读侧登记的槽从不被读刷新): - // 旧值读前从全局对象属性取当前值,缺失按 undefined(不抛)。 - if ctx.is_implicit_global_reg(var_reg) { + // 顶层已声明 var(tier)与未声明名同:旧值读前从全局对象 + // 属性取当前值,缺失按 undefined(不抛)。 + if self.is_global_tier_name(ctx, name) || ctx.is_implicit_global_reg(var_reg) { let key_idx = ctx.add_constant(Constant::String(name.to_string())); ctx.inst(Inst::new( OpCode::LOAD_GLOBAL_TYPEOF, @@ -431,10 +435,14 @@ impl Emitter { } else if !readonly_builtin { // 隐式全局判定以寄存器集合为准:读侧(旧值解析)已登记绑定, // 写侧二次解析命中集合而非"新登记"。 + let is_tier = self.is_global_tier_name(ctx, name); let is_implicit = ctx.is_implicit_global_reg(var_reg); if is_implicit && ctx.is_strict { // 严格模式未声明写:抛 ReferenceError,后续 LOAD_VAR 不可达。 let _ = self.emit_strict_undeclared_write(name, ctx)?; + } else if is_tier { + // 顶层已声明 var 逻辑赋值:新值落全局对象属性(A 侧单一真值)。 + self.emit_tier_global_write(name, val_reg, ctx); } else { ctx.inst(Inst::new( OpCode::STORE_VAR, diff --git a/crates/oxide_emit/src/expr/identifier.rs b/crates/oxide_emit/src/expr/identifier.rs index 83a8c2c7..6cadbddd 100644 --- a/crates/oxide_emit/src/expr/identifier.rs +++ b/crates/oxide_emit/src/expr/identifier.rs @@ -45,9 +45,10 @@ impl Emitter { Err(err) => return Err(err), }; let r = ctx.alloc_reg(); - if ctx.implicit_global_reads.contains(&var_reg) { - // 未声明标识符读:运行期查 global object 属性存在性,缺失抛 - // ReferenceError(sloppy 写先登记全局槽的读不在此集合,仍走 LOAD_VAR)。 + // 顶层已声明 var 裸读与未声明标识符读同形:读全局对象属性(A 侧单一真值), + // 缺失抛 ReferenceError(unresolvable)。tier 名属性由 GDI 序言创建(仅删除后 + // 缺失);未声明名属性缺失即 unresolvable。已声明 var 不再落镜像 cell/槽。 + if self.is_global_tier_name(ctx, name) || ctx.implicit_global_reads.contains(&var_reg) { 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 { @@ -109,6 +110,15 @@ impl Emitter { Operand::Imm(cell_idx as u16), )); } + } else if self.is_global_tier_name(ctx, name) { + // 顶层已声明 var:with 对象无该属性时回退读全局对象属性(A 侧单一真值)。 + let key_idx = ctx.add_constant(Constant::String(name.to_string())); + ctx.inst(Inst::new( + OpCode::LOAD_GLOBAL, + Operand::Reg(result_reg), + Operand::Const(key_idx), + Operand::None, + )); } else if let Some(reg) = ctx.scopes.symbols.lookup_any(name) { ctx.inst(Inst::new(OpCode::LOAD_VAR, Operand::Reg(result_reg), Operand::Reg(reg), Operand::None)); } else { @@ -181,12 +191,18 @@ impl Emitter { } return; } + let is_tier = self.is_global_tier_name(ctx, name); let is_implicit = ctx.is_implicit_global_reg(var_reg); if is_implicit && ctx.is_strict { // 严格模式未声明写:发射 ReferenceError 抛错,跳过寄存器写(值无关)。 let _ = self.emit_strict_undeclared_write(name, ctx); return; } + if is_tier { + // 顶层已声明 var 裸写:落全局对象属性(A 侧单一真值),不写镜像槽。 + self.emit_tier_global_write(name, val_reg, ctx); + return; + } ctx.inst(Inst::new( OpCode::STORE_VAR, Operand::Reg(var_reg), diff --git a/crates/oxide_emit/src/expr/operator/mod.rs b/crates/oxide_emit/src/expr/operator/mod.rs index ce1f23cc..2c941c23 100644 --- a/crates/oxide_emit/src/expr/operator/mod.rs +++ b/crates/oxide_emit/src/expr/operator/mod.rs @@ -267,7 +267,10 @@ impl Emitter { 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() { + // 顶层已声明 var:typeof 读全局对象属性(A 侧单一真值),缺失 → "undefined" + // (非抛,IsUnresolvableReference 语义)。未声明名同走此路(lookup 未命中)。 + let is_tier = self.is_global_tier_name(ctx, name); + if !in_with_dynamic && !captured && (is_tier || 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( @@ -632,15 +635,15 @@ impl Emitter { ctx.inst(Inst::new(op, Operand::Reg(tmp_reg), Operand::Reg(result_reg), Operand::Reg(result_reg))); return Ok(result_reg); } + let is_tier = self.is_global_tier_name(ctx, name); let is_implicit = ctx.is_implicit_global_reg(var_reg); if is_implicit && ctx.is_strict { // 严格模式未声明更新写:值无关抛 ReferenceError。 return self.emit_strict_undeclared_write(name, ctx); } - // 未声明名槽是入口快照(读侧登记的槽从不被读刷新,同脚本后续写 - // 可使其脱节):RMW 前从全局对象属性取旧值,属性缺失按 undefined - // (sloppy 未解析引用 GetBaseValue 语义,不抛)。 - if is_implicit { + // RMW 前从全局对象属性取旧值:tier 名(顶层已声明 var)与未声明名同, + // 属性缺失按 undefined(update 旧值角落,GetBaseValue 语义,不抛)。 + if is_tier || is_implicit { let key_idx = ctx.add_constant(Constant::String(name.to_string())); ctx.inst(Inst::new( OpCode::LOAD_GLOBAL_TYPEOF, @@ -657,6 +660,10 @@ impl Emitter { (UpdateOperator::Decrement, false) => OpCode::DEC_POST, }; ctx.inst(Inst::new(op, Operand::Reg(var_reg), Operand::Reg(result_reg), Operand::Reg(result_reg))); + if is_tier { + // 顶层已声明 var 自增/自减:新值落全局对象属性(A 侧单一真值)。 + self.emit_tier_global_write(name, var_reg, ctx); + } if is_implicit { self.emit_implicit_global_write(name, var_reg, ctx); } diff --git a/crates/oxide_emit/src/shared/binding.rs b/crates/oxide_emit/src/shared/binding.rs index 9d05bf44..a60624da 100644 --- a/crates/oxide_emit/src/shared/binding.rs +++ b/crates/oxide_emit/src/shared/binding.rs @@ -135,12 +135,18 @@ impl Emitter { } return Ok(()); } + let is_tier = self.is_global_tier_name(ctx, name); let is_implicit = ctx.is_implicit_global_reg(var_reg); if is_implicit && ctx.is_strict { // 严格模式未声明写:抛 ReferenceError,跳过寄存器写(值无关)。 self.emit_strict_undeclared_write(name, ctx)?; return Ok(()); } + if is_tier { + // 顶层已声明 var 解构赋值目标:值落全局对象属性(A 侧单一真值)。 + self.emit_tier_global_write(name, src_reg, ctx); + return Ok(()); + } ctx.inst(Inst::new( OpCode::STORE_VAR, Operand::Reg(var_reg), @@ -337,12 +343,18 @@ impl Emitter { } return Ok(()); } + let is_tier = self.is_global_tier_name(ctx, name); let is_implicit = ctx.is_implicit_global_reg(var_reg); if is_implicit && ctx.is_strict { // 严格模式未声明写:抛 ReferenceError,跳过寄存器写(值无关)。 self.emit_strict_undeclared_write(name, ctx)?; return Ok(()); } + if is_tier { + // 顶层已声明 var 解构赋值目标:值落全局对象属性(A 侧单一真值)。 + self.emit_tier_global_write(name, src_reg, ctx); + return Ok(()); + } ctx.inst(Inst::new( OpCode::STORE_VAR, Operand::Reg(var_reg), @@ -421,12 +433,18 @@ impl Emitter { } continue; } + let is_tier = self.is_global_tier_name(ctx, name); let is_implicit = ctx.is_implicit_global_reg(var_reg); if is_implicit && ctx.is_strict { // 严格模式未声明写:抛 ReferenceError,跳过寄存器写(值无关)。 self.emit_strict_undeclared_write(name, ctx)?; return Ok(()); } + if is_tier { + // 顶层已声明 var 对象解构赋值:值落全局对象属性(A 侧单一真值)。 + self.emit_tier_global_write(name, prop_reg, ctx); + continue; + } ctx.inst(Inst::new( OpCode::STORE_VAR, Operand::Reg(var_reg), diff --git a/crates/oxide_emit/src/stmt/declaration/var.rs b/crates/oxide_emit/src/stmt/declaration/var.rs index ec0d8379..a170ba33 100644 --- a/crates/oxide_emit/src/stmt/declaration/var.rs +++ b/crates/oxide_emit/src/stmt/declaration/var.rs @@ -82,38 +82,6 @@ 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, - // 先前已写入则保留写入值,声明不是覆盖性赋值)。被捕获绑定的值 - // 存在 cell 而非槽,从 cell 同步。builtin 名不同步:CreateGlobalVarBinding - // 不更新既有数据描述符,GDI 序言已以镜像值同步(幂等),未初始化 - // 的槽/cell 是 undefined,再同步会把现存值(NaN 等)抹掉。 - if ctx.is_global_scope - && matches!(decl.kind, VariableDeclarationKind::Var) - && !CompileCtx::is_known_builtin(bi.name.as_str()) - { - let src = if let Some(&cell_idx) = ctx.captured_bindings.get(bi.name.as_str()) { - let r = ctx.alloc_reg(); - if let Some((binding, _)) = ctx.scopes.symbols.lookup_any_binding(bi.name.as_str()) { - ctx.inst(Inst::new( - OpCode::CELL_GET, - Operand::Reg(r), - Operand::Reg(binding.reg), - Operand::Imm(cell_idx as u16), - )); - } else { - ctx.inst(Inst::new( - OpCode::CELL_GET, - Operand::Reg(r), - Operand::None, - Operand::Imm(cell_idx as u16), - )); - } - r - } else { - target_reg - }; - self.emit_global_prop_write(bi.name.as_str(), src, ctx); - } r = Some(var_reg); } } diff --git a/crates/oxide_emit/src/stmt/iteration/for_in.rs b/crates/oxide_emit/src/stmt/iteration/for_in.rs index b5e46ee1..d71d90c3 100644 --- a/crates/oxide_emit/src/stmt/iteration/for_in.rs +++ b/crates/oxide_emit/src/stmt/iteration/for_in.rs @@ -71,6 +71,11 @@ impl Emitter { Operand::Reg(key_reg), Operand::None, )); + // 顶层 for-in var 头:迭代值落全局对象属性(A 侧单一真值)。 + // builtin 名(writable 如 Math)保留既有全局属性值,不 clobber。 + if self.is_global_tier_name(ctx, name) && !CompileCtx::is_known_builtin(name) { + self.emit_tier_global_write(name, key_reg, ctx); + } } ctx.init_var(name); } @@ -89,10 +94,15 @@ impl Emitter { self.emit_throw_error("TypeError", "cannot assign to read-only property", ctx)?; } } else { + let is_tier = self.is_global_tier_name(ctx, name); let is_implicit = ctx.is_implicit_global_reg(var_reg); if is_implicit && ctx.is_strict { // 严格模式未声明写:抛 ReferenceError,跳过寄存器写(值无关)。 self.emit_strict_undeclared_write(name, ctx)?; + } else if is_tier && !CompileCtx::is_known_builtin(name) { + // 顶层 for-in 赋值头:迭代值落全局对象属性(A 侧单一真值)。 + // builtin 名保留既有全局属性值,不 clobber。 + self.emit_tier_global_write(name, key_reg, ctx); } else { ctx.inst(Inst::new( OpCode::STORE_VAR, diff --git a/crates/oxide_ir/src/contract.rs b/crates/oxide_ir/src/contract.rs index 3e08203c..6ff93790 100644 --- a/crates/oxide_ir/src/contract.rs +++ b/crates/oxide_ir/src/contract.rs @@ -737,10 +737,15 @@ mod tests { assert_contract(OpCode::SET_PROP_BATCH, None, &[1, 2, 3], false); assert_contract(OpCode::IC_SET_PROP, None, &[1, 2, 3], false); assert_contract(OpCode::SET_ELEM, None, &[1, 2, 3], false); - assert_group(&[OpCode::NEW_OBJECT, OpCode::NEW_ARRAY], Some(1), &[], true); + assert_group(&[OpCode::NEW_OBJECT, OpCode::NEW_SESSION_OBJECT, OpCode::NEW_ARRAY], Some(1), &[], true); // define 语义属性写入 / arguments / rest - assert_group(&[OpCode::DEFINE_PROP, OpCode::DEFINE_GLOBAL_PROP], None, &[1, 2, 3], false); - assert_contract(OpCode::DEFINE_GLOBAL_PROP_C, None, &[2], false); + assert_group( + &[OpCode::DEFINE_PROP, OpCode::DEFINE_GLOBAL_PROP, OpCode::DEFINE_GLOBAL_PROP_IF_ABSENT], + None, + &[1, 2, 3], + false, + ); + assert_group(&[OpCode::DEFINE_GLOBAL_PROP_C, OpCode::DEFINE_GLOBAL_PROP_C_IF_ABSENT], None, &[2], false); assert_contract(OpCode::DEFINE_PROP_ATTRS, None, &[1, 2, 3], false); assert_contract(OpCode::DEFINE_ACCESSOR_ATTRS, None, &[1, 2, 3], false); assert_group(&[OpCode::CREATE_ARGUMENTS, OpCode::CREATE_REST_ARRAY], Some(1), &[], false); diff --git a/crates/oxide_ir/src/inst.rs b/crates/oxide_ir/src/inst.rs index 042c8aa8..92cd7b36 100644 --- a/crates/oxide_ir/src/inst.rs +++ b/crates/oxide_ir/src/inst.rs @@ -236,6 +236,25 @@ impl Inst { Self::with_ext(OpCode::DEFINE_GLOBAL_PROP_C, Operand::None, value, Operand::None, &[key_idx as u32]) } + /// 顶层 var 声明的全局属性 define-if-absent:target 为全局对象,value/key 为寄存器。 + /// 既有属性零动作;缺失新建可写/可枚举/不可配置(脚本 GDI 序言,CreateGlobalVarBinding + /// 对既有数据描述符零修改)。 + pub fn define_global_prop_if_absent(target: Operand, value: Operand, key: Operand) -> Self { + Self::new(OpCode::DEFINE_GLOBAL_PROP_IF_ABSENT, target, value, key) + } + + /// eval 脚本顶层 var 声明的全局属性 define-if-absent:ext = [key_idx](键常量池 + /// 下标),value 放 a 槽。既有属性零动作;缺失新建可写/可枚举/可配置。 + pub fn define_global_prop_c_if_absent(value: Operand, key_idx: u16) -> Self { + Self::with_ext( + OpCode::DEFINE_GLOBAL_PROP_C_IF_ABSENT, + Operand::None, + value, + Operand::None, + &[key_idx as u32], + ) + } + /// 静态删除属性: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]) diff --git a/crates/oxide_vm/src/dispatch/arithmetic.rs b/crates/oxide_vm/src/dispatch/arithmetic.rs index 9261515a..c778534c 100644 --- a/crates/oxide_vm/src/dispatch/arithmetic.rs +++ b/crates/oxide_vm/src/dispatch/arithmetic.rs @@ -27,6 +27,26 @@ fn int_sub(a: i32, b: i32) -> JsValue { } } +/// int 自增:结果落在 i32 范围则保 int,否则升 double(与 int_add 同族语义, +/// 保持与 ADD 路径一致的 int 保持行为)。 +#[inline(always)] +fn int_inc(a: i32) -> JsValue { + match a.checked_add(1) { + Some(v) => JsValue::int(v), + None => JsValue::float(a as f64 + 1.0), + } +} + +/// int 自减:结果落在 i32 范围则保 int,否则升 double(与 int_sub 同族语义, +/// 保持与 SUB 路径一致的 int 保持行为)。 +#[inline(always)] +fn int_dec(a: i32) -> JsValue { + match a.checked_sub(1) { + Some(v) => JsValue::int(v), + None => JsValue::float(a as f64 - 1.0), + } +} + /// int+int 乘法:结果落在 i32 范围则保 int,否则升 double。 #[inline(always)] fn int_mul(a: i32, b: i32) -> JsValue { @@ -538,6 +558,12 @@ impl Vm { self.regs[a] = result; return Ok(()); } + if self.regs[rd].is_int() { + let result = int_inc(self.regs[rd].as_int()); + self.regs[rd] = result; + self.regs[a] = result; + return Ok(()); + } let n = self.coerce_number_bounded(self.regs[rd])?; let result = JsValue::float(n + 1.0); self.regs[rd] = result; @@ -554,6 +580,12 @@ impl Vm { self.regs[rd] = self.new_bigint(v + 1); return Ok(()); } + if self.regs[rd].is_int() { + let old = self.regs[rd]; + self.regs[a] = old; + self.regs[rd] = int_inc(old.as_int()); + return Ok(()); + } let n = self.coerce_number_bounded(self.regs[rd])?; self.regs[a] = JsValue::float(n); self.regs[rd] = JsValue::float(n + 1.0); @@ -570,6 +602,12 @@ impl Vm { self.regs[a] = result; return Ok(()); } + if self.regs[rd].is_int() { + let result = int_dec(self.regs[rd].as_int()); + self.regs[rd] = result; + self.regs[a] = result; + return Ok(()); + } let n = self.coerce_number_bounded(self.regs[rd])?; let result = JsValue::float(n - 1.0); self.regs[rd] = result; @@ -586,6 +624,12 @@ impl Vm { self.regs[rd] = self.new_bigint(v - 1); return Ok(()); } + if self.regs[rd].is_int() { + let old = self.regs[rd]; + self.regs[a] = old; + self.regs[rd] = int_dec(old.as_int()); + return Ok(()); + } let n = self.coerce_number_bounded(self.regs[rd])?; self.regs[a] = JsValue::float(n); self.regs[rd] = JsValue::float(n - 1.0); diff --git a/crates/oxide_vm/src/dispatch/misc.rs b/crates/oxide_vm/src/dispatch/misc.rs index f103af89..14292d87 100644 --- a/crates/oxide_vm/src/dispatch/misc.rs +++ b/crates/oxide_vm/src/dispatch/misc.rs @@ -238,6 +238,27 @@ impl Vm { Ok(()) } + /// 创建 session 直分的空普通对象([[Prototype]] = Object.prototype):与 + /// `create_function_object` 的 prototype 子对象同策略——若按 epoch 分配,写入 + /// 全局等逃逸根时晋升屏障会深克隆进 session,持有者(构造器 prototype 属性) + /// 与克隆体指针分裂、严格相等恒 false。 + /// + /// # 副作用 + /// - 登记 session 对象表并计入堆账目;回收由 session GC mark/sweep 承担。 + pub(crate) fn dispatch_new_session_object(&mut self, rd: usize) -> Result<(), String> { + vm_trace!("NEW_SESSION_OBJECT rd={}", rd); + let proto_ptr = &*self.object_prototype as *const JsObject as *mut JsObject; + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(proto_ptr)); + obj.set_session_epoch(true); + let obj_ptr = self.gc_state.session_epoch.alloc(obj) as *mut JsObject; + self.gc_state.session_object_ptrs.push(obj_ptr); + // 直 session 分配计入堆账目(与 promote 同式:对象头 + 对象堆数据)。 + self.gc_state.session_bytes_allocated += std::mem::size_of::() + + crate::session_gc::SessionGc::object_heap_data_bytes(unsafe { &*obj_ptr }) as usize; + self.regs[rd] = JsValue::object(obj_ptr as *mut u8); + Ok(()) + } + /// 创建 arguments 对象:索引属性取当前帧(或 inline 同步调用)的完整实参, /// 附 length / callee 属性。第一版为 unmapped(非严格)语义,索引与形参不同步。 /// diff --git a/crates/oxide_vm/src/ic_helper.rs b/crates/oxide_vm/src/ic_helper.rs index ce9d2b79..38469c54 100644 --- a/crates/oxide_vm/src/ic_helper.rs +++ b/crates/oxide_vm/src/ic_helper.rs @@ -67,6 +67,7 @@ fn ext_word_count(bytecode: &[Instr], pc: usize) -> usize { | OpCode::DEFINE_ACCESSOR_DYNAMIC | OpCode::DEFINE_PROP_ATTRS | OpCode::DEFINE_GLOBAL_PROP_C + | OpCode::DEFINE_GLOBAL_PROP_C_IF_ABSENT | OpCode::DELETE_PROP_STATIC | OpCode::REST_OBJECT | OpCode::INIT_PRIVATE => 1, diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index 58457e4f..6c6c72b5 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -1826,8 +1826,9 @@ mod tests { #[test] fn closure_cell_survives_object_sweep() { let mut vm = vm_with_threshold(1); + // counter 函数作用域局部:被 inc 经 cell 捕获(顶层 var 直连全局属性不走 cell)。 vm.run(&Arc::new(compile( - "var counter = 0; function inc() { return ++counter; } globalThis.inc = inc; 0", + "function outer() { var counter = 0; function inc() { return ++counter; } globalThis.inc = inc; return 0; } outer(); 0", ))) .expect("run1"); assert!(!vm.gc_state.session_cell_ptrs.borrow().is_empty(), "run1 应分配 upvalue cell"); @@ -1880,15 +1881,20 @@ mod tests { #[test] fn cells_freed_by_full_reset_and_reallocatable() { let mut vm = Vm::new(); - vm.run(&Arc::new(compile("var x = 1; function f() { return x; } globalThis.f = f; f()"))) - .expect("run1"); + // x/y 函数作用域局部:被 f/g 经 cell 捕获(顶层 var 直连全局属性不走 cell)。 + vm.run(&Arc::new(compile( + "function outer() { var x = 1; function f() { return x; } globalThis.f = f; return f(); } outer()", + ))) + .expect("run1"); assert!(!vm.gc_state.session_cell_ptrs.borrow().is_empty()); vm.full_reset(); assert!(vm.gc_state.session_cell_ptrs.borrow().is_empty(), "full_reset 应释放全部 cell"); let result = vm - .run(&Arc::new(compile("var y = 2; function g() { return y; } g()"))) + .run(&Arc::new(compile( + "function outer() { var y = 2; function g() { return y; } return g(); } outer()", + ))) .expect("run2"); assert_eq!(format!("{result}"), "2", "重置后新 cell 应正常分配与读取"); } diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index be7d3ca4..20141fb1 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -2040,6 +2040,16 @@ impl Vm { self.dispatch_define_global_prop_c(a, key_idx)?; } + OpCode::DEFINE_GLOBAL_PROP_IF_ABSENT => { + self.dispatch_define_global_prop_if_absent(rd, a, b)?; + } + + OpCode::DEFINE_GLOBAL_PROP_C_IF_ABSENT => { + let key_idx = self.bytecode[self.pc] as u16; + self.pc += 1; + self.dispatch_define_global_prop_c_if_absent(a, key_idx)?; + } + OpCode::DEFINE_PROP_ATTRS => { let attrs = self.bytecode[self.pc] as u8; self.pc += 1; @@ -2078,6 +2088,10 @@ impl Vm { self.dispatch_new_object(rd, instr)?; } + OpCode::NEW_SESSION_OBJECT => { + self.dispatch_new_session_object(rd)?; + } + OpCode::CREATE_ARGUMENTS => { self.dispatch_create_arguments(rd)?; } diff --git a/crates/oxide_vm/src/vm_dispatch_ctrl.rs b/crates/oxide_vm/src/vm_dispatch_ctrl.rs index 434da12e..1088a477 100644 --- a/crates/oxide_vm/src/vm_dispatch_ctrl.rs +++ b/crates/oxide_vm/src/vm_dispatch_ctrl.rs @@ -365,6 +365,70 @@ impl Vm { } } + /// 顶层 var 声明的全局属性 define-if-absent:rd=全局对象,a=值,b=键。 + /// + /// CreateGlobalVarBinding 对既有属性零动作:属性已存在(数据或 accessor)直接 + /// 返回——可写/不可写/配置位与值均不更新。缺失新建可写/可枚举/不可配置数据 + /// 属性;全局对象不可扩展时静默 no-op(GDI 面,基线同形)。 + /// + /// # 边界与前提 + /// - rd 非对象(非对象 this)时按 no-op 处理,不抛错。 + pub(crate) fn dispatch_define_global_prop_if_absent( + &mut self, rd: usize, a: usize, b: usize, + ) -> Result<(), String> { + vm_trace!("DEFINE_GLOBAL_PROP_IF_ABSENT 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() }; + // 既有属性(数据或 accessor):CreateGlobalVarBinding 零动作。 + if self + .kernel_core + .shape_forge() + .lookup_position(obj.shape_id(), prop_name_si) + .is_some() + { + return Ok(()); + } + // 缺失:新建可写/可枚举/不可配置;不可扩展静默(GDI 面,基线同形)。 + let _ = self.define_data_property(obj, prop_name_si, value, PropAttributes::new(true, true, false)); + Ok(()) + } + + /// eval 脚本顶层 var 声明的全局属性 define-if-absent:ext 字 = 键常量池下标 + /// (u16),a 槽 = 值寄存器。全局对象由 session 解析(不依赖 this)。 + /// + /// CreateGlobalVarBinding 对既有属性零动作:属性已存在直接返回。缺失新建 + /// 可写/可枚举/可配置属性;全局对象不可扩展 → TypeError(两模式均抛)。 + /// + /// # 边界与前提 + /// - 键常量必须是字符串(emit 侧保证);非字符串按错误返回。 + pub(crate) fn dispatch_define_global_prop_c_if_absent(&mut self, a: usize, key_idx: u16) -> Result<(), String> { + let idx = key_idx as usize; + vm_trace!("DEFINE_GLOBAL_PROP_C_IF_ABSENT value={} idx={}", a, idx); + let key_val = self.immutables().get(idx).copied().unwrap_or(JsValue::undefined()); + if !key_val.is_string() { + return Err(format!("DEFINE_GLOBAL_PROP_C_IF_ABSENT constant index {idx} is not a string key")); + } + let si = self.property_key_si(key_val)?; + let value = self.regs[a]; + let global_ptr = self.session.global_object().as_ptr() as *mut JsObject; + // SAFETY: 全局对象钉在 session 永久区,指针在 VM 生命周期内有效。 + let obj = unsafe { &mut *global_ptr }; + // 既有属性:CreateGlobalVarBinding 零动作。 + if self.kernel_core.shape_forge().lookup_position(obj.shape_id(), si).is_some() { + return Ok(()); + } + // 缺失:新建;唯一失败面是全局对象不可扩展(两模式均抛 TypeError)。 + match self.define_data_property(obj, si, value, PropAttributes::new(true, true, true)) { + Ok(()) => Ok(()), + Err(msg) => self.raise_error_kind("TypeError", &msg), + } + } + /// break 完成:`crossed`(rd 槽)为 emit 词法算出的逃出 finally 域数。 /// 逐个穿越 finally 后跳转到目标;crossed 为 0 时直接跳转。 /// ext 字携带逃出的迭代器层数:无 finally 穿越时立即关闭后跳转,有 finally diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index 89214c1f..a8fea581 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -1068,9 +1068,10 @@ mod tests { #[test] fn generator_captured_upvalue_survives_sweep() { let mut vm = vm_with_low_threshold(); + // x 函数作用域局部:被生成器 g 经 cell 捕获(顶层 var 直连全局属性不走 cell)。 let _ = run_source( &mut vm, - "var x = 0; function* g(){ x++; yield x; x++; yield x; } globalThis.it = g(); globalThis.it.next(); 0", + "function outer() { var x = 0; function* g(){ x++; yield x; x++; yield x; } globalThis.it = g(); globalThis.it.next(); return 0; } outer(); 0", ); vm.maybe_collect_session_gc(); @@ -1085,8 +1086,12 @@ mod tests { #[test] fn generator_promoted_clone_owns_independent_state_box() { let mut vm = Vm::new(); - // `var it = g(); it.next(); it`:it 为 epoch 生成器对象(未逃逸不 promote)。 - let it = run_source(&mut vm, "function* g(){ yield 1; yield 2; } var it = g(); it.next(); it"); + // `(function(){ var it = g(); it.next(); return it; })()`:it 为函数局部(非顶层 + // var,不经全局属性逃逸),保持 epoch 生成器对象(未 promote)。 + let it = run_source( + &mut vm, + "function* g(){ yield 1; yield 2; } (function(){ var it = g(); it.next(); return it; })()", + ); assert!(it.is_object()); let epoch_ptr = it.as_js_object_ptr(); let epoch_box = unsafe { (*epoch_ptr).native_data() }; diff --git a/crates/oxide_vm/tests/gc_runtime_tests.rs b/crates/oxide_vm/tests/gc_runtime_tests.rs index 2b30a502..8afb05b2 100644 --- a/crates/oxide_vm/tests/gc_runtime_tests.rs +++ b/crates/oxide_vm/tests/gc_runtime_tests.rs @@ -270,11 +270,12 @@ fn closure_cells_freed_by_full_reset_without_double_free() { fn dead_closure_upvalues_freed_by_sweep_without_double_free() { let mut vm = vm_with_threshold(4096); // run1:两个 3-upvalue 闭包逃逸 global(成 session 对象),同 run 内调用 - // 存活闭包确认 upvalue 语义完好。 + // 存活闭包确认 upvalue 语义完好。捕获变量置于函数作用域:顶层 var 按全局 + // 对象属性单一真值读取,不产生 cell 捕获。 let first = compile( - "var a = 1; var b = 2; var c = 3; \ + "(function(){var a = 1; var b = 2; var c = 3; \ globalThis.dead = function() { return a + b + c; }; \ - globalThis.live = function() { return a + b + c; }; globalThis.live()", + globalThis.live = function() { return a + b + c; }; return globalThis.live();})()", ); let result = vm.run(&Arc::new(first)).expect("run1"); assert_eq!(format!("{}", result), "6"); @@ -299,8 +300,8 @@ fn dead_closure_upvalues_freed_by_sweep_without_double_free() { // 重置后引擎健康:重建新 3-upvalue 闭包,同 run 调用语义正确。 let fourth = compile( - "var x = 10; var y = 20; var z = 30; \ - globalThis.f = function() { return x + y + z; }; globalThis.f()", + "(function(){var x = 10; var y = 20; var z = 30; \ + globalThis.f = function() { return x + y + z; }; return globalThis.f();})()", ); let result = vm.run(&Arc::new(fourth)).expect("run4"); assert_eq!(format!("{}", result), "60"); @@ -309,7 +310,8 @@ fn dead_closure_upvalues_freed_by_sweep_without_double_free() { // 各走独立 VM 的「创建 → 撤根 → 完整收集」。两次收集的字节账目差即死 // arrow 的 upvalue 列表 Box 字节数——对象本体、length/name 属性区、函数 // 名 session 串在 A/B 恒等,差分相消(各 1 个死对象 + 1 条死串)。 - // 死分支不释放 upvalue 列表(泄漏)时差分为 0,本断言转红。 + // 捕获变量置于函数作用域(顶层 var 按全局对象属性读取,不产生 cell + // 捕获)。死分支不释放 upvalue 列表(泄漏)时差分为 0,本断言转红。 let collect_freed = |src: &str| -> u64 { let mut v = vm_with_threshold(4096); v.run(&Arc::new(compile(src))).expect("run create"); @@ -320,7 +322,10 @@ fn dead_closure_upvalues_freed_by_sweep_without_double_free() { assert!(stats.last_collection_objects_dead >= 1, "arrow 闭包应经 sweep 死分支"); stats.last_collection_bytes_freed }; - let freed_with_captures = collect_freed("var a = 1; var b = 2; var c = 3; globalThis.arrow = () => a + b + c; 0"); + let freed_with_captures = collect_freed( + "(function(){var a = 1; var b = 2; var c = 3; \ + globalThis.arrow = () => a + b + c; return 0;})()", + ); let freed_no_captures = collect_freed("globalThis.arrow = () => 7; 0"); let upvalue_box_min = (std::mem::size_of::>() + 3 * std::mem::size_of::<*mut oxide_types::object::Cell>()) as u64; diff --git a/crates/oxide_vm/tests/global_var_binding_tests.rs b/crates/oxide_vm/tests/global_var_binding_tests.rs index a577acc2..466426f9 100644 --- a/crates/oxide_vm/tests/global_var_binding_tests.rs +++ b/crates/oxide_vm/tests/global_var_binding_tests.rs @@ -243,3 +243,83 @@ fn map_box_value_survives_reset() { "var fill; for (var i = 0; i < 200; i++) { let t = {junk: i, pad: 'x'.repeat(32)}; fill = t; } globalThis.m.get(globalThis.key).v === 2", )); } + +/// 顶层 var 单一真值:裸读、globalThis 别名、GOPD 描述符三者见同一值—— +/// 声明后写点落全局对象属性,镜像寄存器不再参与读。 +#[test] +fn top_level_var_read_write_single_source() { + eval_truthy("var x = 1; x = 2; globalThis.x === 2"); + eval_truthy("var x = 1; x = 2; x === 2 && globalThis.x === 2"); + eval_truthy("var x = 1; x = 2; Object.getOwnPropertyDescriptor(globalThis, 'x').value === 2 && x === 2"); + // 属性被直写后裸读见新值:读走全局对象属性而非镜像槽。 + eval_truthy("var v = 1; globalThis.v = 2; v === 2"); + // 复合赋值旧值取属性值:属性被外部更新后 RMW 基址须跟随属性。 + eval_truthy("var c = 5; globalThis.c = 100; c += 2; c === 102 && globalThis.c === 102"); +} + +/// 顶层 var 的 typeof 读全局对象属性:声明语句前 GDI 序言已建属性(值 +/// undefined);声明后见真实值。 +#[test] +fn top_level_var_typeof_reads_global_property() { + eval_truthy("(function(){ seen = (typeof t === 'undefined'); })(); var t; seen === true"); + eval_truthy("var t2 = 's'; typeof t2 === 'string' && globalThis.t2 === 's'"); +} + +/// 顶层 var 自增/自减、复合/逻辑赋值:新值落全局对象属性。 +#[test] +fn update_and_compound_assign_write_global_property() { + eval_truthy("var u = 3; ++u; u === 4 && globalThis.u === 4"); + eval_truthy("var p = 3; p++; p === 4 && globalThis.p === 4"); + eval_truthy("var c = 5; c += 2; c === 7 && globalThis.c === 7"); + eval_truthy("var l = 0; l ||= 9; l === 9 && globalThis.l === 9"); + eval_truthy("var n = 1; n &&= 0; n === 0 && globalThis.n === 0"); +} + +/// 顶层 var 作解构赋值目标:值落全局对象属性。 +#[test] +fn destructuring_assign_writes_global_property() { + eval_truthy("var d1; ({d1} = {d1: 5}); d1 === 5 && globalThis.d1 === 5"); + eval_truthy("var d2; [d2] = [7]; d2 === 7 && globalThis.d2 === 7"); +} + +/// 顶层 for-in 头的迭代值落全局对象属性:声明头与赋值头两形。 +#[test] +fn for_in_head_writes_global_property() { + eval_truthy("for (var k in {a: 1}) {} k === 'a' && globalThis.k === 'a'"); + eval_truthy("var k2; for (k2 in {a: 1, b: 2}) {} k2 === 'b' && globalThis.k2 === 'b'"); +} + +/// 嵌套函数局部同名遮蔽不穿透顶层绑定:遮蔽侧走局部槽,顶层属性不受影响; +/// 无遮蔽时嵌套裸读/裸写直连全局对象属性(不经 cell)。 +#[test] +fn nested_local_shadow_does_not_pierce_tier() { + eval_truthy("var s = 1; (function(){ var s = 2; return s; })() === 2 && s === 1 && globalThis.s === 1"); + eval_truthy( + "var s2 = 1; (function(){ var s2 = 9; s2 = 10; return s2; })() === 10 && s2 === 1 && globalThis.s2 === 1", + ); + eval_truthy("var n = 42; (function(){ return n; })() === 42"); + // 严格嵌套函数 this=undefined:写点不依赖 this,经 session 全局对象落属性。 + eval_truthy("\"use strict\"; var w = 1; (function(){ \"use strict\"; w = 5; })(); w === 5 && globalThis.w === 5"); +} + +/// 直接 eval:var 声明在全局对象建可配置属性;eval 内裸写穿透全局对象 +/// 属性,外层(顶层 var 名)读见同一值。 +#[test] +fn direct_eval_var_and_write_visible_to_outer() { + eval_truthy("eval('var y = 5'); y === 5 && globalThis.y === 5"); + eval_truthy("var q = 1; eval('q = 55'); q === 55 && globalThis.q === 55"); + eval_truthy("eval('var ev = 1'); Object.getOwnPropertyDescriptor(globalThis, 'ev').configurable === true"); + // 嵌套函数内直接 eval:写点经 session 解析全局对象(不依赖 this)。 + eval_truthy("var z = 1; (function(){ \"use strict\"; eval('z = 77'); })(); z === 77 && globalThis.z === 77"); +} + +/// 既有属性与顶层 var 声明交互:序言 define-if-absent 零动作,声明值写 +/// 是描述符感知写——既有不可写属性保值不覆盖。既有属性须先于 GDI 存在, +/// 分两阶段:阶段一建属性(该程序无 var,GDI 不触碰),阶段二声明。 +#[test] +fn tier_declaration_respects_existing_property() { + assert!(eval_two_phases( + "Object.defineProperty(globalThis, 'rw', {value: 7, writable: false, configurable: true})", + "var rw = 99; rw === 7 && globalThis.rw === 7", + )); +} diff --git a/crates/oxide_vm/tests/session_arena_tests.rs b/crates/oxide_vm/tests/session_arena_tests.rs index 16369657..08880402 100644 --- a/crates/oxide_vm/tests/session_arena_tests.rs +++ b/crates/oxide_vm/tests/session_arena_tests.rs @@ -92,13 +92,15 @@ fn test_full_reset_clears_session_state() { /// NEW_OBJECT 字面量必须登记进 epoch 追踪表:`{}` 写入超内联容量的属性会分配 /// hash_props 堆 Box,reset 时经追踪表统一释放;不登记则每个字面量泄漏一个 Box。 +/// 对象变量置于函数作用域:顶层 var 在声明写即逃逸 promote,后续属性写落 +/// session 克隆(其堆数据不属 epoch 源对象,reset 不释放)。 #[test] fn new_object_literal_tracked_and_freed_on_reset() { let mut vm = Vm::new(); let result = run_source( &mut vm, - "var o = {}; for (var i = 0; i < 40; i++) { o['k' + i] = i; } \ - delete o.k7; o.k8", + "(function(){var o = {}; for (var i = 0; i < 40; i++) { o['k' + i] = i; } \ + delete o.k7; return o.k8;})()", ); assert_eq!(format!("{}", result), "8", "属性写入/删除后读取应正常"); assert!(vm.epoch_object_count() > 0, "NEW_OBJECT 应登记 epoch 追踪表");