From 673d007b790c576323700f60d788bca22ba277e2 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Mon, 19 Oct 2026 01:12:24 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=A4=8D=E5=90=88=E8=B5=8B=E5=80=BC?= =?UTF-8?q?=E6=97=A7=E5=80=BC=E8=AF=BB=E6=94=B9=E6=8A=9B=E9=94=99=E8=AF=BB?= =?UTF-8?q?=E5=B9=B6=E8=A1=A5=E4=B8=A5=E6=A0=BC=E9=9A=90=E5=BC=8F=E5=85=A8?= =?UTF-8?q?=E5=B1=80=E5=86=99=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 42 ++++---- Cargo.toml | 2 +- crates/oxide_bytecode/src/opcode.rs | 13 +++ crates/oxide_emit/src/expr/assignment.rs | 88 ++++++++++++----- crates/oxide_emit/src/expr/identifier.rs | 9 +- crates/oxide_ir/src/inst.rs | 13 +++ crates/oxide_vm/src/dispatch/misc.rs | 95 +++++++++++++++++-- crates/oxide_vm/src/dispatch/property.rs | 71 +++++++++++++- crates/oxide_vm/src/vm/dispatch_loop.rs | 7 ++ crates/oxide_vm/src/vm/props.rs | 42 ++++++++ crates/oxide_vm/src/vm_dispatch_ctrl.rs | 47 +++++++++ .../tests/implicit_global_write_tests.rs | 16 ++-- .../with_fallback_implicit_global_tests.rs | 14 +-- 13 files changed, 382 insertions(+), 77 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cef2cdad..ba5b75ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1160,7 +1160,7 @@ dependencies = [ [[package]] name = "oxide_builtins" -version = "0.21.1" +version = "0.21.2" dependencies = [ "chrono", "fast-float", @@ -1185,7 +1185,7 @@ dependencies = [ [[package]] name = "oxide_bytecode" -version = "0.21.1" +version = "0.21.2" dependencies = [ "num-bigint", "oxide_log", @@ -1193,7 +1193,7 @@ dependencies = [ [[package]] name = "oxide_cfg" -version = "0.21.1" +version = "0.21.2" dependencies = [ "oxide_bytecode", "oxide_emit", @@ -1204,7 +1204,7 @@ dependencies = [ [[package]] name = "oxide_cli" -version = "0.21.1" +version = "0.21.2" dependencies = [ "ansi_term", "bumpalo", @@ -1228,7 +1228,7 @@ dependencies = [ [[package]] name = "oxide_code_cache" -version = "0.21.1" +version = "0.21.2" dependencies = [ "lru", "oxide_bytecode", @@ -1237,7 +1237,7 @@ dependencies = [ [[package]] name = "oxide_compiler" -version = "0.21.1" +version = "0.21.2" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1258,7 +1258,7 @@ dependencies = [ [[package]] name = "oxide_dce" -version = "0.21.1" +version = "0.21.2" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1272,7 +1272,7 @@ dependencies = [ [[package]] name = "oxide_emit" -version = "0.21.1" +version = "0.21.2" dependencies = [ "num-bigint", "oxc_allocator", @@ -1288,7 +1288,7 @@ dependencies = [ [[package]] name = "oxide_engine" -version = "0.21.1" +version = "0.21.2" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1300,7 +1300,7 @@ dependencies = [ [[package]] name = "oxide_fold" -version = "0.21.1" +version = "0.21.2" dependencies = [ "num-bigint", "oxide_bytecode", @@ -1312,7 +1312,7 @@ dependencies = [ [[package]] name = "oxide_ir" -version = "0.21.1" +version = "0.21.2" dependencies = [ "oxide_bytecode", "oxide_log", @@ -1321,7 +1321,7 @@ dependencies = [ [[package]] name = "oxide_kernel" -version = "0.21.1" +version = "0.21.2" dependencies = [ "bumpalo", "dashmap", @@ -1335,7 +1335,7 @@ dependencies = [ [[package]] name = "oxide_liveness" -version = "0.21.1" +version = "0.21.2" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1347,7 +1347,7 @@ dependencies = [ [[package]] name = "oxide_log" -version = "0.21.1" +version = "0.21.2" dependencies = [ "tracing", "tracing-appender", @@ -1356,11 +1356,11 @@ dependencies = [ [[package]] name = "oxide_net" -version = "0.21.1" +version = "0.21.2" [[package]] name = "oxide_parser" -version = "0.21.1" +version = "0.21.2" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1375,7 +1375,7 @@ dependencies = [ [[package]] name = "oxide_regalloc" -version = "0.21.1" +version = "0.21.2" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1391,7 +1391,7 @@ dependencies = [ [[package]] name = "oxide_runtime_api" -version = "0.21.1" +version = "0.21.2" dependencies = [ "num-bigint", "num-traits", @@ -1404,7 +1404,7 @@ dependencies = [ [[package]] name = "oxide_test262" -version = "0.21.1" +version = "0.21.2" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1424,7 +1424,7 @@ dependencies = [ [[package]] name = "oxide_types" -version = "0.21.1" +version = "0.21.2" dependencies = [ "bumpalo", "dashmap", @@ -1437,7 +1437,7 @@ dependencies = [ [[package]] name = "oxide_vm" -version = "0.21.1" +version = "0.21.2" dependencies = [ "bumpalo", "chrono", diff --git a/Cargo.toml b/Cargo.toml index baf2643a..a260fa25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ members = [ ] [workspace.package] -version = "0.21.1" +version = "0.21.2" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_bytecode/src/opcode.rs b/crates/oxide_bytecode/src/opcode.rs index 2b8b84df..b315b13c 100644 --- a/crates/oxide_bytecode/src/opcode.rs +++ b/crates/oxide_bytecode/src/opcode.rs @@ -765,6 +765,18 @@ define_opcodes! { VOID = 0xF3 => "VOID", def = Some(SlotSpec::Slot(Slot::Rd)), uses = [], pure = true, jump = false, term = false, ic = false, + // 严格模式隐式全局写:ext 字 = 键常量池下标(u16)、a 槽 = 值寄存器; + // 先 HasProperty 检查(全局环境记录 SetMutableBinding 语义),属性缺失抛 + // ReferenceError;存在时按 DEFINE_GLOBAL_PROP_C 同形定义。 + DEFINE_GLOBAL_PROP_C_STRICT = 0xF5 => "DEFINE_GLOBAL_PROP_C_STRICT", + def = None, uses = [SlotSpec::Slot(Slot::A)], + pure = false, jump = false, term = false, ic = false, + // 严格 PutValue 属性写(with 对象引用写回):rd = 基对象、a = 键、b = 值; + // 写时点 HasProperty 检查(含原型链),属性缺失抛 ReferenceError(读侧 + // getter 副作用可能已删除属性);存在时按 SET_PROP_DYNAMIC 同形 Set。 + SET_PROP_DYNAMIC_STRICT = 0xF4 => "SET_PROP_DYNAMIC_STRICT", + def = None, uses = [SlotSpec::Slot(Slot::Rd), SlotSpec::Slot(Slot::A), SlotSpec::Slot(Slot::B)], + pure = false, jump = false, term = false, ic = false, } /// 4 字节定长指令。 @@ -833,6 +845,7 @@ pub 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_STRICT | OpCode::DEFINE_GLOBAL_PROP_C_IF_ABSENT | OpCode::DELETE_GLOBAL_PROP_C | OpCode::DELETE_PROP_STATIC diff --git a/crates/oxide_emit/src/expr/assignment.rs b/crates/oxide_emit/src/expr/assignment.rs index 2a2ef077..feb7ceea 100644 --- a/crates/oxide_emit/src/expr/assignment.rs +++ b/crates/oxide_emit/src/expr/assignment.rs @@ -128,15 +128,13 @@ impl Emitter { let is_tier = self.is_global_tier_name(ctx, name); let is_implicit = ctx.is_implicit_global_reg(var_reg); let is_builtin = ctx.is_builtin(name) && !ctx.is_local_shadowing_builtin(name); - if is_implicit && ctx.is_strict { - // 严格模式未声明复合写:未解析引用不可 put,值无关抛 ReferenceError。 - return self.emit_strict_undeclared_write(name, ctx); - } - // RMW 前从全局对象属性取旧值:tier 名(顶层已声明 var)与未声明名同, - // 属性缺失按 undefined(GetBaseValue 语义,不抛);已知 builtin 名(非局部 - // 遮蔽)读 A 侧属性,属性缺失(delete 真删后)抛 ReferenceError,与裸读 - // 同形——镜像槽不反映删除,无法自区分缺位与在位 undefined。 - if is_tier || is_implicit { + // RMW 前从全局对象属性取旧值:tier 名(顶层已声明 var)属性缺失按 + // undefined(GetBaseValue 语义,不抛);已知 builtin 名(非局部遮蔽) + // 读 A 侧属性,属性缺失(delete 真删后)抛 ReferenceError,与裸读同形 + // ——镜像槽不反映删除,无法自区分缺位与在位 undefined。 + // 隐式全局(未声明名)旧值读是抛错读:复合赋值的赋值引用解析在 + // 读旧值时抛 ReferenceError(未声明引用不可 GetValue,两模式同)。 + if is_tier { let key_idx = ctx.add_constant(Constant::String(name.to_string())); ctx.inst(Inst::new( OpCode::LOAD_GLOBAL_TYPEOF, @@ -144,7 +142,9 @@ impl Emitter { Operand::Const(key_idx), Operand::None, )); - } else if is_builtin { + } else if is_implicit || is_builtin { + // 隐式全局(未声明名)与已知 builtin 名(非局部遮蔽)同形抛错读: + // 属性缺失(delete 真删后)抛 ReferenceError,与裸读同形。 let key_idx = ctx.add_constant(Constant::String(name.to_string())); ctx.inst(Inst::new( OpCode::LOAD_GLOBAL, @@ -159,7 +159,14 @@ impl Emitter { // 引擎侧不保留镜像副本)。 self.emit_tier_global_write(name, var_reg, ctx); } else if is_implicit { - self.emit_implicit_global_write(name, var_reg, ctx); + if ctx.is_strict { + // 严格隐式全局写:运行期 HasProperty 检查,属性缺失抛 + // ReferenceError(旧值读侧的 getter 副作用可能已删除属性)。 + let idx = ctx.add_constant(Constant::String(name.to_string())); + ctx.inst(Inst::define_global_prop_c_strict(Operand::Reg(var_reg), idx)); + } else { + self.emit_implicit_global_write(name, var_reg, ctx); + } } else if ctx.targets_writable_builtin(name, var_reg) { // 可写内置名:RMW 新值同步落全局对象属性。 self.emit_global_put_write(name, var_reg, ctx); @@ -514,12 +521,13 @@ impl Emitter { )); } else { let var_reg = ctx.lookup_or_global(name); - // 顶层已声明 var(tier)与未声明名同:旧值读前从全局对象 - // 属性取当前值,缺失按 undefined(不抛);已知 builtin 名 - // (非局部遮蔽)读 A 侧属性,属性缺失(delete 真删后)抛 - // ReferenceError,与裸读同形——镜像槽不反映删除,无法自 - // 区分缺位与在位 undefined。 - if self.is_global_tier_name(ctx, name) || ctx.is_implicit_global_reg(var_reg) { + // 顶层已声明 var(tier):旧值读前从全局对象属性取当前值, + // 缺失按 undefined(不抛);已知 builtin 名(非局部遮蔽)读 + // A 侧属性,属性缺失(delete 真删后)抛 ReferenceError,与 + // 裸读同形——镜像槽不反映删除,无法自区分缺位与在位 undefined。 + // 隐式全局(未声明名)旧值读是抛错读:逻辑赋值的赋值引用 + // 解析在读旧值时抛 ReferenceError(未声明引用不可 GetValue)。 + if self.is_global_tier_name(ctx, name) { let key_idx = ctx.add_constant(Constant::String(name.to_string())); ctx.inst(Inst::new( OpCode::LOAD_GLOBAL_TYPEOF, @@ -527,7 +535,11 @@ impl Emitter { Operand::Const(key_idx), Operand::None, )); - } else if ctx.is_builtin(name) && !ctx.is_local_shadowing_builtin(name) { + } else if ctx.is_implicit_global_reg(var_reg) + || (ctx.is_builtin(name) && !ctx.is_local_shadowing_builtin(name)) + { + // 隐式全局(未声明名)与已知 builtin 名(非局部遮蔽)同形抛错读: + // 属性缺失(delete 真删后)抛 ReferenceError,与裸读同形。 let key_idx = ctx.add_constant(Constant::String(name.to_string())); ctx.inst(Inst::new( OpCode::LOAD_GLOBAL, @@ -546,6 +558,15 @@ impl Emitter { self.emit_logical_assign_test(logical_op, result_reg, store_label, end_label, ctx)?; ctx.labels.set_label_pos(store_label, ctx.insts.len()); let val_reg = self.emit_expression(&assign.right, ctx)?; + // NamedEvaluation:RHS 是匿名函数/箭头/class 时按 LHS 标识符推断 + // name(仅短路通过、实际写回时才到达此处,与规范求值点一致)。 + if crate::is_anonymous_function_definition(&assign.right) { + if let Some(sub_mod) = ctx.nested.last_mut() { + if sub_mod.function_name.is_none() { + sub_mod.function_name = Some(name.to_string()); + } + } + } // 短路未通过才写:const 目标在此抛 TypeError(编译期拦截,值无关)。 self.emit_const_write_guard(name, ctx)?; // 函数名不可写绑定(sloppy):put 永不成功,跳过写与命名空间写穿, @@ -592,8 +613,10 @@ impl Emitter { 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)?; + // 严格隐式全局写:运行期 HasProperty 检查,属性缺失抛 + // ReferenceError(旧值读侧的 getter 副作用可能已删除属性)。 + let idx = ctx.add_constant(Constant::String(name.to_string())); + ctx.inst(Inst::define_global_prop_c_strict(Operand::Reg(val_reg), idx)); } else if is_tier { // 顶层已声明 var 逻辑赋值:新值写入全局对象属性(顶层 var 的唯一存储, // 引擎侧不保留镜像副本)。 @@ -691,12 +714,7 @@ impl Emitter { Operand::Reg(val_reg), )); ctx.inst(Inst::new(op, Operand::Reg(val_reg), Operand::Reg(rhs), Operand::None)); - ctx.inst(Inst::new( - OpCode::SET_PROP_DYNAMIC, - Operand::Reg(obj_reg), - Operand::Reg(key_reg), - Operand::Reg(val_reg), - )); + self.emit_put_prop_dynamic(obj_reg, key_reg, val_reg, ctx); ctx.inst(Inst::jmp(end_label)); // 对象无该属性:回退静态复合赋值,结果写入同一 val_reg。 @@ -846,4 +864,22 @@ impl Emitter { )); Ok(val_reg) } + + /// 严格 PutValue 属性写(with 对象引用写回):strict 模式发 + /// SET_PROP_DYNAMIC_STRICT(写时点 HasProperty 检查,属性缺失抛 + /// ReferenceError——读侧 getter 副作用可能已删除属性);sloppy 模式发 + /// 普通 SET_PROP_DYNAMIC(缺失物化属性)。 + pub(crate) fn emit_put_prop_dynamic(&self, obj_reg: u32, key_reg: u32, val_reg: u32, ctx: &mut CompileCtx) { + let op = if ctx.is_strict { + OpCode::SET_PROP_DYNAMIC_STRICT + } else { + OpCode::SET_PROP_DYNAMIC + }; + ctx.inst(Inst::new( + op, + Operand::Reg(obj_reg), + Operand::Reg(key_reg), + Operand::Reg(val_reg), + )); + } } diff --git a/crates/oxide_emit/src/expr/identifier.rs b/crates/oxide_emit/src/expr/identifier.rs index baa0087b..451275d3 100644 --- a/crates/oxide_emit/src/expr/identifier.rs +++ b/crates/oxide_emit/src/expr/identifier.rs @@ -329,12 +329,9 @@ impl Emitter { let end_label = ctx.next_label_id(); ctx.inst(Inst::jmp_if_false(has_reg, fallback_label)); - ctx.inst(Inst::new( - OpCode::SET_PROP_DYNAMIC, - Operand::Reg(obj_reg), - Operand::Reg(key_reg), - Operand::Reg(val_reg), - )); + // 写时点 HasProperty 检查(strict 缺失抛 ReferenceError,读侧 getter + // 副作用可能已删除属性),与复合赋值同形。 + self.emit_put_prop_dynamic(obj_reg, key_reg, val_reg, ctx); ctx.inst(Inst::jmp(end_label)); ctx.labels.set_label_pos(fallback_label, ctx.insts.len()); diff --git a/crates/oxide_ir/src/inst.rs b/crates/oxide_ir/src/inst.rs index 047d5b05..40898fd0 100644 --- a/crates/oxide_ir/src/inst.rs +++ b/crates/oxide_ir/src/inst.rs @@ -242,6 +242,19 @@ impl Inst { Self::with_ext(OpCode::DEFINE_GLOBAL_PROP_C, Operand::None, value, Operand::None, &[key_idx as u32]) } + /// 严格模式全局写:ext = [key_idx](键常量池下标),value 放 a 槽。运行期先做 + /// HasProperty 检查——属性缺失抛 ReferenceError(严格模式未声明标识符写), + /// 存在则定义可写/可枚举/可配置数据属性。 + pub fn define_global_prop_c_strict(value: Operand, key_idx: u16) -> Self { + Self::with_ext( + OpCode::DEFINE_GLOBAL_PROP_C_STRICT, + Operand::None, + value, + Operand::None, + &[key_idx as u32], + ) + } + /// 顶层 var 声明的全局属性 define-if-absent:target 为全局对象,value/key 为寄存器。 /// 既有属性零动作;缺失新建可写/可枚举/不可配置(脚本 GDI 序言,CreateGlobalVarBinding /// 对既有数据描述符零修改)。 diff --git a/crates/oxide_vm/src/dispatch/misc.rs b/crates/oxide_vm/src/dispatch/misc.rs index 48f9243c..8d800f17 100644 --- a/crates/oxide_vm/src/dispatch/misc.rs +++ b/crates/oxide_vm/src/dispatch/misc.rs @@ -1,8 +1,10 @@ use crate::vm::Vm; use crate::{vm_error, vm_trace}; use oxide_bytecode::opcode; +use oxide_runtime_api as coercion; use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_types::arguments_map::ArgumentsMapState; +use oxide_types::mem::P; use oxide_types::object::{JsObject, PropAttributes}; use oxide_types::private_key::{encode_symbol_key, make_int_key}; use oxide_types::value::{JsType, JsValue}; @@ -22,6 +24,28 @@ impl Vm { } } + /// 全局对象属性读(含访问器 getter 调用):命中属性时若为访问器则以 + /// 全局对象为 receiver 调用 getter(getter 副作用先于后续写生效),返回 + /// getter 结果;命中数据属性返回槽值;未命中返回 None。getter 抛错经 + /// `raise_call_error` 就地展开(pc 已改写),调用方须按 pc 变化停行。 + fn global_prop_read_with_getter(&mut self, global: &P, si: u32) -> Result, String> { + let Some(val) = self.resolve_property(global, si) else { + return Ok(None); + }; + let Some(getter) = self.resolve_property_getter(global, si) else { + return Ok(Some(val)); + }; + // SAFETY: 全局对象钉在 session 永久区,指针在 VM 生命周期内有效。 + let global_val = JsValue::from_js_object(global.as_ptr() as *mut JsObject); + match self.call_function_sync(getter, global_val, &[]) { + Ok(v) => Ok(Some(v)), + Err(err) => { + self.raise_call_error(&err)?; + Ok(None) + } + } + } + /// 未声明标识符读:查 global object 属性存在性,命中取属性值,未命中抛 ReferenceError。 /// /// # 步骤 @@ -44,7 +68,14 @@ impl Vm { let session = self.realm.session.borrow(); session.global_object().clone() }; - if let Some(val) = self.resolve_property(&global, si) { + let pc_before = self.pc; + let hit = self.global_prop_read_with_getter(&global, si)?; + if self.pc != pc_before { + // getter 在途抛错已展开到 catch/finally 入口:pc 由处理器接管, + // 不得续行(结果寄存器不得再写、不得抛未解析引用)。 + return Ok(true); + } + if let Some(val) = hit { self.regs[rd] = val; Ok(false) } else { @@ -71,9 +102,18 @@ impl Vm { return Err(format!("LOAD_GLOBAL_TYPEOF constant index {idx} is not a string key")); } let si = self.property_key_si(key_val)?; - let session = self.realm.session.borrow(); - let global = session.global_object(); - self.regs[rd] = self.resolve_property(global, si).unwrap_or(JsValue::undefined()); + let global = { + let session = self.realm.session.borrow(); + session.global_object().clone() + }; + // typeof 访问器属性同样触发 getter(getter 抛错时 typeof 不豁免,异常照抛)。 + let pc_before = self.pc; + let hit = self.global_prop_read_with_getter(&global, si)?; + if self.pc != pc_before { + // getter 在途抛错已展开:pc 由处理器接管,不得续行。 + return Ok(()); + } + self.regs[rd] = hit.unwrap_or(JsValue::undefined()); Ok(()) } @@ -105,12 +145,19 @@ impl Vm { self.regs[rd] = val; return Ok(false); } - // 回退全局对象属性读(未声明名与 known builtin 名同形 LOAD_GLOBAL)。 - let global_hit = { + // 回退全局对象属性读(未声明名与 known builtin 名同形 LOAD_GLOBAL); + // 访问器属性调用 getter(receiver = 全局对象),裸读必须触发 getter。 + let global = { let session = self.realm.session.borrow(); - let global = session.global_object().clone(); - self.resolve_property(&global, si) + session.global_object().clone() }; + let pc_before = self.pc; + let global_hit = self.global_prop_read_with_getter(&global, si)?; + if self.pc != pc_before { + // getter 在途抛错已展开到 catch/finally 入口:pc 由处理器接管, + // 不得续行(结果寄存器不得再写、不得抛未解析引用)。 + return Ok(true); + } if let Some(val) = global_hit { self.regs[rd] = val; Ok(false) @@ -196,6 +243,38 @@ impl Vm { Ok(()) } + /// ToPropertyKey:推导属性键值。Symbol 原样返回;对象经 ToPrimitive(string + /// hint) 后 ToString;其余原始值直接 ToString。 + /// + /// # 边界与前提 + /// - 对象键的 toString/valueOf 调用有用户副作用,可能抛异常;抛错经 + /// `raise_call_error` 就地展开(pc 已改写),调用方须按 pc 变化停行。 + /// + /// # 副作用 + /// - 成功路径返回推导后的字符串/Symbol 值;抛错路径经 `unwind` 改写 + /// pc / 异常通道并返回 undefined(调用方不得再使用)。 + pub(crate) fn derive_property_key_value(&mut self, val: JsValue) -> Result { + vm_trace!("derive_property_key_value: {:?}", val); + if val.is_symbol() { + return Ok(val); + } + let pc_before = self.pc; + let prim = if val.is_object() { + coercion::to_primitive(val, coercion::ToPrimitiveHint::String, self)? + } else { + val + }; + if self.pc != pc_before { + // ToPrimitive 在途抛错已展开:pc 由处理器接管,结果值不得使用。 + return Ok(JsValue::undefined()); + } + if prim.is_symbol() { + return Ok(prim); + } + let units = oxide_runtime_api::to_units_full(prim, self)?; + Ok(self.new_string_units_owned(units)) + } + pub(crate) fn dispatch_load_var(&mut self, rd: usize, a: usize) -> Result { vm_trace!("LOAD_VAR rd={} r{}={:?}", rd, a, self.regs[a]); // a==254 即读 this:derived 构造帧在 super() 成功前读 this 抛 ReferenceError。 diff --git a/crates/oxide_vm/src/dispatch/property.rs b/crates/oxide_vm/src/dispatch/property.rs index 8fd894a5..ed5cc748 100644 --- a/crates/oxide_vm/src/dispatch/property.rs +++ b/crates/oxide_vm/src/dispatch/property.rs @@ -24,6 +24,7 @@ impl Vm { OpCode::SET_PROP_BATCH => self.dispatch_set_prop_batch(rd, a, b), OpCode::GET_PROP_DYNAMIC => self.dispatch_get_prop_dynamic(rd, a, b), OpCode::SET_PROP_DYNAMIC => self.dispatch_set_prop_dynamic(rd, a, b), + OpCode::SET_PROP_DYNAMIC_STRICT => self.dispatch_set_prop_dynamic_strict(rd, a, b), OpCode::SET_ELEM => self.dispatch_set_elem(rd, a, b), OpCode::GET_PRIVATE => self.dispatch_get_private(rd, a, b), OpCode::SET_PRIVATE => self.dispatch_set_private(rd, a, b), @@ -648,8 +649,24 @@ impl Vm { fn dispatch_get_prop_dynamic(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { vm_trace!("GET_PROP_DYNAMIC rd={} a={} b={}", rd, a, b); - let prop_name_si = self.property_key_si(self.regs[a])?; let receiver = self.regs[rd]; + // 基 ToObject 先于键 ToPropertyKey(规范 GetV 步序):null/undefined 基在 + // 对象键 toString 副作用之前抛 TypeError。 + if receiver.is_null() || receiver.is_undefined() { + self.raise_type_error("GET_PROP_DYNAMIC on non-object")?; + return Ok(()); + } + let pc_before = self.pc; + let key_val = self.derive_property_key_value(self.regs[a])?; + if self.pc != pc_before { + // 键推导在途抛错已展开到 catch/finally 入口:pc 由处理器接管, + // 余下读路径不得续行(结果寄存器不得再写)。 + return Ok(()); + } + // 推导后的键写回键寄存器:复合/逻辑赋值的写侧(SET_PROP_DYNAMIC) + // 复用同一键,对象键的 ToPrimitive 副作用只触发一次。 + self.regs[a] = key_val; + let prop_name_si = self.property_key_si(key_val)?; // 数组元素 fast path:整数键 + 真数组 + 无元素 meta(无 hole/accessor/描述符) // + 界内 → 直读元素区,免 primitive_property_get / checked_object_ptr / // ordinary_get 调用链。判定条件与 ordinary_get_inner 数组分支严格等价。 @@ -695,7 +712,13 @@ impl Vm { let Some(obj_ptr) = self.set_prop_base(self.regs[rd])? else { return Ok(()); }; + let pc_before = self.pc; let prop_name_si = self.property_key_si(self.regs[a])?; + if self.pc != pc_before { + // 键推导在途抛错已展开到 catch/finally 入口:pc 由处理器接管, + // 余下写路径不得续行(不得写属性)。 + return Ok(()); + } // 数组元素 fast path:整数键 + 真数组 + 无 meta + 界内 → 直写元素区, // 免 __proto__ 查询 / ordinary_set_dispatch / get_own_property_slot 调用链。 // 与 ordinary_set_inner 数组分支等价(界内写不触发 length 扩展、无 shadow @@ -731,6 +754,52 @@ impl Vm { Ok(()) } + /// 严格 PutValue 属性写(with 对象引用写回):rd = 基对象、a = 键、b = 值。 + /// 写时点 HasProperty 检查(含原型链)——属性缺失抛 ReferenceError(读侧 + /// getter 副作用可能已删除属性);存在时按 SET_PROP_DYNAMIC 同形 Set。 + /// + /// # 步骤 + /// 1. 基对象检查(非对象抛 TypeError)。 + /// 2. 键推导(ToPropertyKey,在途抛错按 pc 变化停行)。 + /// 3. HasProperty 检查(在途抛错按 pc 变化停行);缺失抛 ReferenceError。 + /// 4. 存在走 ordinary_set_dispatch(strict 模式)。 + fn dispatch_set_prop_dynamic_strict(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { + vm_trace!("SET_PROP_DYNAMIC_STRICT rd={} a={} b={}", rd, a, b); + let Some(obj_ptr) = self.set_prop_base(self.regs[rd])? else { + return Ok(()); + }; + let pc_before = self.pc; + let prop_name_si = self.property_key_si(self.regs[a])?; + if self.pc != pc_before { + // 键推导在途抛错已展开到 catch/finally 入口:pc 由处理器接管, + // 余下写路径不得续行(不得写属性)。 + return Ok(()); + } + let obj = unsafe { &*obj_ptr }; + let pc_before = self.pc; + let exists = self.has_property(obj, prop_name_si)?; + if self.pc != pc_before { + // HasProperty 在途抛错已展开:pc 由处理器接管,不得续行。 + return Ok(()); + } + if !exists { + // 错误消息携带属性名(与全局未声明写同形);键推导后为字符串/符号, + // 非字符串按空名兜底(emit 侧保证字符串键)。 + let name = if self.regs[a].is_string() { + // SAFETY: 键推导后为字符串值;错误消息走 lossy 文本。 + unsafe { (*self.regs[a].as_string_ptr()).as_lossy_str() }.to_string() + } else { + String::new() + }; + self.raise_error_kind("ReferenceError", &format!("{name} is not defined"))?; + return Ok(()); + } + let obj = unsafe { &mut *obj_ptr }; + let value = self.regs[b]; + self.ordinary_set_dispatch(obj, prop_name_si, value, self.regs[rd], self.current_strict())?; + Ok(()) + } + fn dispatch_set_elem(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { vm_trace!("SET_ELEM rd={} a={} b={}", rd, a, b); let Some(obj_ptr) = self.checked_object_ptr(self.regs[rd], "Cannot create property on non-object")? else { diff --git a/crates/oxide_vm/src/vm/dispatch_loop.rs b/crates/oxide_vm/src/vm/dispatch_loop.rs index f390dc36..5c4ebc98 100644 --- a/crates/oxide_vm/src/vm/dispatch_loop.rs +++ b/crates/oxide_vm/src/vm/dispatch_loop.rs @@ -571,6 +571,12 @@ impl Vm { self.dispatch_define_global_prop_c(a, key_idx)?; } + OpCode::DEFINE_GLOBAL_PROP_C_STRICT => { + let key_idx = self.bytecode[self.pc] as u16; + self.pc += 1; + self.dispatch_define_global_prop_c_strict(a, key_idx)?; + } + OpCode::DEFINE_GLOBAL_PROP_IF_ABSENT => { self.dispatch_define_global_prop_if_absent(rd, a, b)?; } @@ -642,6 +648,7 @@ impl Vm { | OpCode::SET_PROP_BATCH | OpCode::GET_PROP_DYNAMIC | OpCode::SET_PROP_DYNAMIC + | OpCode::SET_PROP_DYNAMIC_STRICT | OpCode::SET_ELEM | OpCode::GET_PRIVATE | OpCode::SET_PRIVATE diff --git a/crates/oxide_vm/src/vm/props.rs b/crates/oxide_vm/src/vm/props.rs index 52aaa595..71c2ce35 100644 --- a/crates/oxide_vm/src/vm/props.rs +++ b/crates/oxide_vm/src/vm/props.rs @@ -161,6 +161,48 @@ impl Vm { /// /// 数组元素区的 hole(删除标记)视同不存在;原型链查找止于 /// `MAX_PROTO_CHAIN_DEPTH` 深度上限。 + /// 解析属性的访问器 getter(与 `resolve_property` 同形):沿 shape 链 + + /// 原型链,命中属性为访问器且 get 已定义时返回 getter 函数;数据属性或 + /// 无 getter 返回 None。代理层保守返 None(与 `resolve_property` 同口径)。 + pub(crate) fn resolve_property_getter(&self, obj: &JsObject, prop_name_si: u32) -> Option { + if obj.is_proxy_obj() { + return None; + } + if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { + let val = obj.get_prop_at(pos); + if !val.is_undefined() || obj.prop_vec_len() > pos as usize { + return obj + .prop_meta_at(pos) + .filter(|m| m.is_accessor && !m.get.is_undefined()) + .map(|m| m.get); + } + } + let mut proto = obj.proto(); + let mut depth = 0usize; + while proto.is_object() && depth < MAX_PROTO_CHAIN_DEPTH { + depth += 1; + let proto_obj = unsafe { &*proto.as_js_object_ptr() }; + if proto_obj.is_proxy_obj() { + return None; + } + if let Some(pos) = self + .kernel_core + .shape_forge() + .lookup_position(proto_obj.shape_id(), prop_name_si) + { + let val = proto_obj.get_prop_at(pos); + if !val.is_undefined() || proto_obj.prop_vec_len() > pos as usize { + return proto_obj + .prop_meta_at(pos) + .filter(|m| m.is_accessor && !m.get.is_undefined()) + .map(|m| m.get); + } + } + proto = proto_obj.proto(); + } + None + } + pub(crate) fn resolve_property(&self, obj: &JsObject, prop_name_si: u32) -> Option { vm_trace!("resolve_property: shape_id={} prop_name_si={}", obj.shape_id(), prop_name_si); // Proxy:本方法是不可变签名,无法同步调 get trap;代理无自身属性, diff --git a/crates/oxide_vm/src/vm_dispatch_ctrl.rs b/crates/oxide_vm/src/vm_dispatch_ctrl.rs index 8d72dff6..d95c9c35 100644 --- a/crates/oxide_vm/src/vm_dispatch_ctrl.rs +++ b/crates/oxide_vm/src/vm_dispatch_ctrl.rs @@ -424,6 +424,53 @@ impl Vm { return Err(format!("DEFINE_GLOBAL_PROP_C constant index {idx} is not a string key")); } let si = self.property_key_si(key_val)?; + self.define_global_prop_c_body(a, si, key_val) + } + + /// 严格模式隐式全局写:先 HasProperty 检查(全局环境记录 SetMutableBinding + /// 语义)——属性缺失抛 ReferenceError(未解析引用不可 put);存在时按 + /// [`dispatch_define_global_prop_c`] 同形定义。 + /// + /// # 步骤 + /// 1. 键常量解析为属性键 si(与 DEFINE_GLOBAL_PROP_C 同形)。 + /// 2. HasProperty 检查(含原型链;代理/deferred 命名空间触发可抛错, + /// 抛错经 pc 变化停行)。 + /// 3. 缺失抛 ReferenceError(`{name} is not defined`);存在走定义体。 + /// + /// # 边界与前提 + /// - 键常量必须是字符串(emit 侧保证);非字符串按错误返回。 + pub(crate) fn dispatch_define_global_prop_c_strict(&mut self, a: usize, key_idx: u16) -> Result<(), String> { + let idx = key_idx as usize; + vm_trace!("DEFINE_GLOBAL_PROP_C_STRICT 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_STRICT constant index {idx} is not a string key")); + } + let si = self.property_key_si(key_val)?; + let global_ptr = self.realm.session.borrow().global_object().as_ptr() as *mut JsObject; + // SAFETY: 全局对象钉在 session 永久区,指针在 VM 生命周期内有效。 + let obj = unsafe { &*global_ptr }; + let pc_before = self.pc; + let exists = self.has_property(obj, si)?; + if self.pc != pc_before { + // HasProperty 在途抛错已展开到 catch/finally 入口:pc 由处理器接管, + // 定义不得续行。 + return Ok(()); + } + if !exists { + // SAFETY: key_val 已校验为字符串值;错误消息走 lossy 文本。 + let name = unsafe { (*key_val.as_string_ptr()).as_lossy_str() }; + self.raise_error_kind("ReferenceError", &format!("{name} is not defined"))?; + return Ok(()); + } + self.define_global_prop_c_body(a, si, key_val) + } + + /// 全局对象属性定义体(DEFINE_GLOBAL_PROP_C 与严格变体共用):既有属性 + /// 按描述符分流(accessor 走 Set 语义、数据属性按可写性),缺失新建可写/ + /// 可枚举/可配置属性。 + fn define_global_prop_c_body(&mut self, a: usize, si: u32, key_val: JsValue) -> Result<(), String> { + let _ = key_val; let value = self.regs[a]; let global_ptr = self.realm.session.borrow().global_object().as_ptr() as *mut JsObject; // SAFETY: 全局对象钉在 session 永久区,指针在 VM 生命周期内有效。 diff --git a/crates/oxide_vm/tests/implicit_global_write_tests.rs b/crates/oxide_vm/tests/implicit_global_write_tests.rs index 2de38c2f..2361a4d8 100644 --- a/crates/oxide_vm/tests/implicit_global_write_tests.rs +++ b/crates/oxide_vm/tests/implicit_global_write_tests.rs @@ -86,17 +86,19 @@ fn sloppy_update_operator_undeclared_write_lands_on_global() { assert!(r.is_bool() && r.as_bool(), "z++ 未声明写应落全局 NaN(undefined+1),实际 {:?}", r); } +// ── 复合赋值:未声明名旧值读在 lval 求值步抛 ReferenceError(未解析引用不可 +// GetValue,两模式同),不物化全局 NaN ── #[test] -fn sloppy_compound_operator_undeclared_write_is_nan() { - let s = eval_str("w += 1; typeof w"); - assert_eq!(s, "number", "w += 1 未声明名应为 NaN(typeof number),实际 {s}"); +fn sloppy_compound_operator_undeclared_write_throws() { + let result = eval("w += 1"); + assert_err_contains(result, "w is not defined"); } -// ── 逻辑赋值:未声明名 ||= 短路建全局 ── +// ── 逻辑赋值:未声明名旧值读抛 ReferenceError,短路路径不可达,不物化全局 ── #[test] -fn sloppy_logical_assign_undeclared_write_lands_on_global() { - let r = eval_many(&["p ||= 5", "globalThis.p === 5"]).unwrap(); - assert!(r.is_bool() && r.as_bool(), "p ||= 5 未声明写应落全局为 5,实际 {:?}", r); +fn sloppy_logical_assign_undeclared_write_throws() { + let result = eval("p ||= 5"); + assert_err_contains(result, "p is not defined"); } // ── for-in 左侧未声明标识符:首个键后建全局 ── diff --git a/crates/oxide_vm/tests/with_fallback_implicit_global_tests.rs b/crates/oxide_vm/tests/with_fallback_implicit_global_tests.rs index f11642e9..7fa77d8d 100644 --- a/crates/oxide_vm/tests/with_fallback_implicit_global_tests.rs +++ b/crates/oxide_vm/tests/with_fallback_implicit_global_tests.rs @@ -59,12 +59,12 @@ fn with_fallback_declared_var_writes_outer_binding() { } #[test] -fn with_fallback_compound_undeclared_write_keeps_existing_behavior() { - // 复合赋值回退臂走静态复合路径:未声明名旧值按 undefined 读 - // (GetBaseValue 语义不抛),复合运算结果落隐式全局。 - assert_eq!( - eval("var o={}; with(o){x+=1;} isNaN(globalThis.x)"), - "true", - "with 回退未声明名复合赋值保持既有读 undefined 行为" +fn with_fallback_compound_undeclared_write_throws() { + // 复合赋值回退臂走静态复合路径:未声明名旧值读在 lval 求值步抛 + // ReferenceError(未解析引用不可 GetValue,两模式同),不物化隐式全局。 + let s = eval("var o={}; with(o){x+=1;}"); + assert!( + s.contains("ReferenceError") && s.contains("x is not defined"), + "with 回退未声明名复合赋值应抛 ReferenceError,实际 {s}" ); }