From bf08803071637bccaa4c016b625a0d7857fa7f73 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sat, 12 Sep 2026 21:36:10 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=A1=B6=E5=B1=82=20var=20=E5=A3=B0?= =?UTF-8?q?=E6=98=8E=E5=B8=A6=E5=88=9D=E5=A7=8B=E5=8C=96=E6=92=9E=E4=B8=8D?= =?UTF-8?q?=E5=8F=AF=E5=86=99=E5=85=A8=E5=B1=80=E7=94=A8=E6=88=B7=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=20strict=20=E6=8A=9B=20TypeError=E2=80=94=E2=80=94?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E5=90=8D=E5=A3=B0=E6=98=8E=E5=A4=84=E5=86=99?= =?UTF-8?q?=E7=82=B9=E6=94=B9=20session=20=E8=A7=A3=E6=9E=90=20PutValue=20?= =?UTF-8?q?=E8=AF=AD=E4=B9=89=EF=BC=8Cbuiltin=20=E5=90=8D=E4=BF=9D=20GDI?= =?UTF-8?q?=20=E5=86=99=E7=82=B9=EF=BC=9B=E5=BA=8F=E8=A8=80=E7=BC=BA?= =?UTF-8?q?=E5=A4=B1=E5=88=86=E6=94=AF=E5=85=A8=E5=B1=80=E4=B8=8D=E5=8F=AF?= =?UTF-8?q?=E6=89=A9=E5=B1=95=E4=B8=A4=E6=A8=A1=E5=BC=8F=E5=AF=B9=E9=BD=90?= =?UTF-8?q?=E6=8A=9B=20TypeError?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_emit/src/emit.rs | 14 ++ crates/oxide_emit/src/shared/binding.rs | 8 +- crates/oxide_vm/src/vm_dispatch_ctrl.rs | 14 +- ...lobal_builtin_constant_descriptor_tests.rs | 9 + .../tests/global_var_binding_tests.rs | 161 ++++++++++++++++++ 5 files changed, 201 insertions(+), 5 deletions(-) diff --git a/crates/oxide_emit/src/emit.rs b/crates/oxide_emit/src/emit.rs index 3170cb37..69f121e9 100644 --- a/crates/oxide_emit/src/emit.rs +++ b/crates/oxide_emit/src/emit.rs @@ -1586,6 +1586,20 @@ impl Emitter { } } + /// 顶层 var 声明带初始化的 A 侧同步:PutValue 语义——既有不可写数据属性 + /// strict 抛 TypeError / sloppy 静默 no-op;可写照原描述符仅更值。脚本与 + /// eval 均经 session 解析全局对象,不依赖顶层 this。 + /// + /// # 边界与前提 + /// - 仅顶层模块上下文调用;builtin 名不走本写点(保持 GDI 零动作写点)。 + /// + /// # 副作用 + /// - 更新全局对象数据属性,失败面抛 TypeError(strict 不可写)。 + pub(crate) fn emit_global_put_write(&self, name: &str, val_reg: u32, ctx: &mut CompileCtx) { + let idx = ctx.add_constant(Constant::String(name.to_string())); + ctx.inst(Inst::define_global_prop_c(Operand::Reg(val_reg), idx)); + } + /// 名字是否为顶层已声明 var(A 侧单一真值):在顶层 var 名集内,且当前解析 /// 绑定落在全局作用域(scope 0)——嵌套函数内的局部同名遮蔽命中更高作用域, /// 判定为局部而非顶层,走既有 cell/寄存器路径。 diff --git a/crates/oxide_emit/src/shared/binding.rs b/crates/oxide_emit/src/shared/binding.rs index a60624da..9c79657e 100644 --- a/crates/oxide_emit/src/shared/binding.rs +++ b/crates/oxide_emit/src/shared/binding.rs @@ -108,7 +108,13 @@ 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); + if CompileCtx::is_known_builtin(name) { + // builtin 名保持 GDI 零动作写点(序言镜像值语义),不扩 strict 面。 + self.emit_global_prop_write(name, src_reg, ctx); + } else { + // 用户名既有属性面:声明带初始化是 PutValue,0x98 带 strict 分派。 + self.emit_global_put_write(name, src_reg, ctx); + } } // REPL 持久模式:let/const 也写全局对象属性,使跨轮次读取可见。 if ctx.repl_persist && ctx.is_global_scope && !matches!(kind, VariableDeclarationKind::Var) { diff --git a/crates/oxide_vm/src/vm_dispatch_ctrl.rs b/crates/oxide_vm/src/vm_dispatch_ctrl.rs index 1ef8b258..a00633ab 100644 --- a/crates/oxide_vm/src/vm_dispatch_ctrl.rs +++ b/crates/oxide_vm/src/vm_dispatch_ctrl.rs @@ -371,9 +371,12 @@ impl Vm { /// /// CreateGlobalVarBinding 对既有属性零动作:属性已存在(数据或 accessor)直接 /// 返回——可写/不可写/配置位与值均不更新。缺失新建可写/可枚举/不可配置数据 - /// 属性;全局对象不可扩展时静默 no-op(GDI 面,基线同形)。 + /// 属性;全局对象不可扩展 → TypeError(两模式均抛,GDI CanDeclareGlobalVar + /// 面,同形 eval 变体)。 /// /// # 边界与前提 + /// - rd 槽固定为顶层 `this`:本形以所有脚本(含严格模式)顶层 this 即全局 + /// 对象为成立前提。 /// - rd 非对象(非对象 this)时按 no-op 处理,不抛错。 pub(crate) fn dispatch_define_global_prop_if_absent( &mut self, rd: usize, a: usize, b: usize, @@ -395,9 +398,12 @@ impl Vm { { return Ok(()); } - // 缺失:新建可写/可枚举/不可配置;不可扩展静默(GDI 面,基线同形)。 - let _ = self.define_data_property(obj, prop_name_si, value, PropAttributes::new(true, true, false)); - Ok(()) + // 缺失:新建可写/可枚举/不可配置;唯一失败面是全局对象不可扩展(两模式 + // 均抛 TypeError,GDI CanDeclareGlobalVar 面,同形 eval 变体)。 + match self.define_data_property(obj, prop_name_si, value, PropAttributes::new(true, true, false)) { + Ok(()) => Ok(()), + Err(msg) => self.raise_error_kind("TypeError", &msg), + } } /// eval 脚本顶层 var 声明的全局属性 define-if-absent:ext 字 = 键常量池下标 diff --git a/crates/oxide_vm/tests/global_builtin_constant_descriptor_tests.rs b/crates/oxide_vm/tests/global_builtin_constant_descriptor_tests.rs index 11ac3968..ea22316c 100644 --- a/crates/oxide_vm/tests/global_builtin_constant_descriptor_tests.rs +++ b/crates/oxide_vm/tests/global_builtin_constant_descriptor_tests.rs @@ -138,6 +138,15 @@ fn var_builtin_object_with_init() { ); } +#[test] +fn strict_var_builtin_object_with_init() { + // #10 同形(strict):builtin 名保 GDI 写点,行为与模式无关。 + run_truthy( + "'use strict'; var Math = 6; \ + Object.getOwnPropertyDescriptor(globalThis, 'Math').value === 6", + ); +} + #[test] fn eval_var_readonly_builtin() { // #11:eval var 路径(0x98 判 writable 而非 configurable)——sloppy no-op、 diff --git a/crates/oxide_vm/tests/global_var_binding_tests.rs b/crates/oxide_vm/tests/global_var_binding_tests.rs index e0d847b5..7ff06853 100644 --- a/crates/oxide_vm/tests/global_var_binding_tests.rs +++ b/crates/oxide_vm/tests/global_var_binding_tests.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use oxide_compiler::compiler::Compiler; use oxide_parser::Allocator; +use oxide_types::value::JsValue; use oxide_vm::vm::Vm; fn eval_truthy(source: &str) { @@ -39,6 +40,30 @@ fn eval_two_phases(phase1: &str, phase2: &str) -> bool { result.is_bool() && result.as_bool() } +/// 两阶段 run-Err 变体:phase2 返回运行结果,供未捕获异常(TypeError 等)断言。 +fn run_two_phases(phase1: &str, phase2: &str) -> Result { + let mut vm = Vm::new(); + vm.run(&Arc::new(compile(phase1))).expect("run1"); + vm.reset(); + vm.run(&Arc::new(compile(phase2))) +} + +/// 两阶段真值断言:phase2 须以 true 完成。 +fn assert_two_phases_truthy(phase1: &str, phase2: &str) { + match run_two_phases(phase1, phase2) { + Ok(v) => assert!(v.is_bool() && v.as_bool(), "expected true, got: {v:?}\nphase2: {phase2}"), + Err(e) => panic!("phase2 run failed: {e}\nphase2: {phase2}"), + } +} + +/// 取 phase2 未捕获异常文本(phase2 正常完成时 panic)。 +fn phase2_err_text(phase1: &str, phase2: &str) -> String { + match run_two_phases(phase1, phase2) { + Err(e) => e, + Ok(v) => panic!("expected uncaught error, got: {v:?}\nphase2: {phase2}"), + } +} + #[test] fn top_level_var_lands_on_global_this() { eval_truthy("var x = 5; globalThis.x === 5"); @@ -343,3 +368,139 @@ fn tier_declaration_respects_existing_property() { "var rw = 99; rw === 7 && globalThis.rw === 7", )); } + +// ── 声明带初始化的 A 侧写是 PutValue:strict 撞既有不可写用户属性抛 TypeError ── + +#[test] +fn strict_var_init_on_nonwritable_user_property_throws() { + // strict 声明初始化撞既有不可写(可配置)用户属性抛 TypeError; + // 序言对既有属性零动作(抛点仅在声明处 PutValue)。 + let err = phase2_err_text( + "Object.defineProperty(globalThis, 'u', {value: 1, writable: false, configurable: true})", + "'use strict'; var u = 5;", + ); + assert!(err.contains("TypeError"), "应抛 TypeError,实际: {err}"); + assert!(err.contains("read-only"), "消息应指明只读属性,实际: {err}"); +} + +#[test] +fn strict_var_init_on_writable_user_property_updates_value() { + // 既有可写用户属性(strict):PutValue 更新值不抛,裸读与反射一致。 + assert_two_phases_truthy( + "Object.defineProperty(globalThis, 'u', {value: 1, writable: true, configurable: true})", + "'use strict'; var u = 5; u === 5 && globalThis.u === 5", + ); +} + +#[test] +fn sloppy_var_init_on_writable_user_property_updates_value() { + // 同上(sloppy):可写属性值更新。 + assert_two_phases_truthy( + "Object.defineProperty(globalThis, 'u', {value: 1, writable: true, configurable: true})", + "var u = 5; u === 5 && globalThis.u === 5", + ); +} + +#[test] +fn strict_var_init_on_nonconfigurable_nonwritable_user_property_throws() { + // c:false + w:false:序言零动作,声明 PutValue strict 抛 TypeError。 + let err = phase2_err_text( + "Object.defineProperty(globalThis, 'u', {value: 1, writable: false, configurable: false})", + "'use strict'; var u = 5;", + ); + assert!(err.contains("TypeError") && err.contains("read-only"), "实际: {err}"); +} + +#[test] +fn sloppy_var_init_on_nonconfigurable_nonwritable_user_property_silent() { + // c:false + w:false(sloppy):静默 no-op,值不覆盖。 + assert_two_phases_truthy( + "Object.defineProperty(globalThis, 'u', {value: 1, writable: false, configurable: false})", + "var u = 5; u === 1 && globalThis.u === 1", + ); +} + +#[test] +fn strict_var_no_init_on_nonwritable_user_property_no_throw() { + // 无初始化声明无 PutValue(防过抛守卫):strict 不抛,既有值不动。 + assert_two_phases_truthy( + "Object.defineProperty(globalThis, 'u', {value: 1, writable: false, configurable: true})", + "'use strict'; var u; u === 1 && globalThis.u === 1", + ); +} + +#[test] +fn strict_for_of_var_head_on_nonwritable_user_property_throws() { + // for-of var 声明头与声明带 init 同走 PutValue 写点:strict 迭代值撞 + // 不可写抛 TypeError;sloppy 静默 no-op。 + let err = phase2_err_text( + "Object.defineProperty(globalThis, 'p', {value: 1, writable: false, configurable: true})", + "'use strict'; for (var p of [3]) {}", + ); + assert!(err.contains("TypeError") && err.contains("read-only"), "实际: {err}"); + assert_two_phases_truthy( + "Object.defineProperty(globalThis, 'p', {value: 1, writable: false, configurable: true})", + "for (var p of [3]) {} p === 1 && globalThis.p === 1", + ); +} + +#[test] +fn strict_eval_var_init_on_nonwritable_user_property_throws() { + // eval 对照:eval 脚本声明处写点本就带 strict 分派,改前后同形。 + let err = phase2_err_text( + "Object.defineProperty(globalThis, 'u', {value: 1, writable: false, configurable: true})", + "eval('\"use strict\"; var u = 5')", + ); + assert!(err.contains("TypeError") && err.contains("read-only"), "实际: {err}"); + assert_two_phases_truthy( + "Object.defineProperty(globalThis, 'u', {value: 1, writable: false, configurable: true})", + "eval('var u = 5'); globalThis.u === 1", + ); +} + +// ── GDI 序言缺失分支:全局不可扩展 → 两模式 TypeError(CanDeclareGlobalVar 面) ── + +#[test] +fn nonextensible_global_strict_var_prologue_throws_before_body() { + // 序言期抛:体副作用不执行(touched 未写入)。 + let mut vm = Vm::new(); + vm.run(&Arc::new(compile("Object.preventExtensions(globalThis)"))) + .expect("run1"); + vm.reset(); + let result = vm.run(&Arc::new(compile("'use strict'; var missing; touched = 1;"))); + let err = match result { + Err(e) => e, + Ok(v) => panic!("应抛 TypeError,实际: {v:?}"), + }; + assert!(err.contains("TypeError"), "应抛 TypeError,实际: {err}"); + // 体未执行:隐式全局写点未触达,touched 未创建。 + vm.reset(); + let after = vm.run(&Arc::new(compile("typeof touched"))).expect("run3"); + assert_eq!(vm.lookup_str(after).unwrap_or_default(), "undefined"); +} + +#[test] +fn nonextensible_global_sloppy_var_prologue_throws() { + // var 臂无 strict 门:sloppy 同形抛 TypeError。 + let err = phase2_err_text("Object.preventExtensions(globalThis)", "var missing;"); + assert!(err.contains("TypeError"), "应抛 TypeError,实际: {err}"); +} + +#[test] +fn nonextensible_global_eval_var_throws_both_modes() { + // eval 脚本序言不可扩展抛:sloppy/strict 两模式同抛(回归钉)。 + let err_sloppy = phase2_err_text("Object.preventExtensions(globalThis)", "eval('var missing')"); + assert!(err_sloppy.contains("TypeError"), "sloppy eval 应抛 TypeError,实际: {err_sloppy}"); + let err_strict = phase2_err_text("Object.preventExtensions(globalThis)", "eval('\"use strict\"; var missing')"); + assert!(err_strict.contains("TypeError"), "strict eval 应抛 TypeError,实际: {err_strict}"); +} + +#[test] +fn nonextensible_global_var_existing_property_no_throw() { + // 既有属性不进缺失分支:零动作不抛(防过抛守卫)。 + assert_two_phases_truthy( + "Object.defineProperty(globalThis, 'ex', {value: 1, writable: false, configurable: true}); \ + Object.preventExtensions(globalThis); true", + "var ex; ex === 1 && globalThis.ex === 1", + ); +}