diff --git a/crates/oxide_emit/src/emit.rs b/crates/oxide_emit/src/emit.rs index f89b44cc..b98d2a0b 100644 --- a/crates/oxide_emit/src/emit.rs +++ b/crates/oxide_emit/src/emit.rs @@ -1627,11 +1627,21 @@ impl Emitter { // 全局声明实例化序言:脚本求值前为顶层 var 名创建全局对象属性(值 undefined), // 使声明语句执行前的读取(typeof、反射、自引用)可经全局对象见绑定;同名 // 函数声明的属性由首个 sub-pass 以函数值覆盖,声明语句保持值更新语义。 + // builtin 名的全局属性运行期预存(session 绑定):序言值写是单 opcode 值 + // 混叠,直接写 undefined 会把现存值(NaN 等)抹掉,故以 builtin 镜像槽 + // (run 起点预载全局属性值)作为写入值——描述符按 CreateGlobalVarBinding + // 翻 writable,值幂等保留;裸读走镜像、反射见预存属性。 let gdi_var_names = self.collect_var_binding_names(&program.body); if !gdi_var_names.is_empty() { let undef_reg = self.emit_undefined(&mut ctx); for name in &gdi_var_names { - self.emit_global_prop_write(name, undef_reg, &mut ctx); + let value_reg = if CompileCtx::is_known_builtin(name) { + // 镜像槽未预登记时(解构 pattern 名等)就地登记,run 起点预载。 + ctx.lookup_or_builtin(name).unwrap_or(undef_reg) + } else { + undef_reg + }; + self.emit_global_prop_write(name, value_reg, &mut ctx); } } diff --git a/crates/oxide_emit/src/prepass.rs b/crates/oxide_emit/src/prepass.rs index c34c3596..2b98525f 100644 --- a/crates/oxide_emit/src/prepass.rs +++ b/crates/oxide_emit/src/prepass.rs @@ -372,6 +372,19 @@ impl Emitter { } } + /// 预声明单个 `var` 名:仅顶层(全局作用域)的 builtin 名落 builtin 镜像槽 + /// (run 起点预载全局属性值),使 GDI 序言与声明点同步都用入口原值而非 + /// fresh undefined 槽,现存值(NaN 等)不被抹;函数体内 builtin 名仍是局部 + /// var 遮蔽(fresh 槽,不命中只读内置拦截);其余名 fresh var 槽。 + fn predeclare_var_name(&self, name: &str, ctx: &mut CompileCtx) { + if ctx.is_global_scope && CompileCtx::is_known_builtin(name) { + let _ = ctx.lookup_or_builtin(name); + } else { + let reg = ctx.alloc_reg(); + let _ = ctx.declare_initialized(name, reg, VariableDeclarationKind::Var, false); + } + } + /// 预声明语句列表中的全部 `var` 绑定,使先发的提升函数声明能解析它们。 /// 顶层 `var` 名在编译闭包捕获它的函数体时必须可见。 pub(crate) fn predeclare_var_declarations(&self, statements: &[Statement], ctx: &mut CompileCtx) { @@ -383,8 +396,7 @@ impl Emitter { } for d in &decl.declarations { if let oxide_parser::BindingPattern::BindingIdentifier(bi) = &d.id { - let reg = ctx.alloc_reg(); - let _ = ctx.declare_initialized(bi.name.as_str(), reg, VariableDeclarationKind::Var, false); + self.predeclare_var_name(bi.name.as_str(), ctx); } } } @@ -406,13 +418,7 @@ impl Emitter { if matches!(decl.kind, VariableDeclarationKind::Var) { for d in &decl.declarations { if let oxide_parser::BindingPattern::BindingIdentifier(bi) = &d.id { - let reg = ctx.alloc_reg(); - let _ = ctx.declare_initialized( - bi.name.as_str(), - reg, - VariableDeclarationKind::Var, - false, - ); + self.predeclare_var_name(bi.name.as_str(), ctx); } } } @@ -446,9 +452,7 @@ impl Emitter { } for d in &decl.declarations { if let oxide_parser::BindingPattern::BindingIdentifier(bi) = &d.id { - let reg = ctx.alloc_reg(); - let _ = - ctx.declare_initialized(bi.name.as_str(), reg, VariableDeclarationKind::Var, false); + self.predeclare_var_name(bi.name.as_str(), ctx); } } } diff --git a/crates/oxide_emit/src/stmt/declaration/var.rs b/crates/oxide_emit/src/stmt/declaration/var.rs index 8830ef1c..f7559768 100644 --- a/crates/oxide_emit/src/stmt/declaration/var.rs +++ b/crates/oxide_emit/src/stmt/declaration/var.rs @@ -84,8 +84,13 @@ impl Emitter { ctx.init_var(bi.name.as_str()); // 脚本顶层 var:全局对象属性同步绑定当前值(首次声明即 undefined, // 先前已写入则保留写入值,声明不是覆盖性赋值)。被捕获绑定的值 - // 存在 cell 而非槽,从 cell 同步。 - if ctx.is_global_scope && matches!(decl.kind, VariableDeclarationKind::Var) { + // 存在 cell 而非槽,从 cell 同步。builtin 名不同步:描述符翻 + // writable 已由 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()) { diff --git a/crates/oxide_kernel/src/kernel.rs b/crates/oxide_kernel/src/kernel.rs index 9d03e000..3a9f5400 100644 --- a/crates/oxide_kernel/src/kernel.rs +++ b/crates/oxide_kernel/src/kernel.rs @@ -524,14 +524,16 @@ impl KernelSession { let si_undef = core.perm_interner.intern("undefined").0; let si_infinity = core.perm_interner.intern("Infinity").0; - // 全局三常量描述符:{ writable:false, enumerable:false, configurable:false }, - // 规范要求不可写不可枚举不可配置,否则会泄漏进 Object.keys(globalThis)。 + // 全局三常量描述符:{ writable:false, enumerable:false, configurable:true }。 + // e:false 是枚举面守卫(Object.keys / for-in 由 enumerable 决定); + // c:true 是 delete / defineProperty 可观察的规范值(delete globalThis.undefined + // 属性真删、返回 true)。 let nan_shape = core.shape_forge.make_shape(EMPTY_SHAPE_ID, si_nan); global_obj.set_shape_id(nan_shape); global_obj.ensure_hash_props().push(JsValue::float(f64::NAN)); global_obj.set_data_meta( global_obj.prop_vec_len().saturating_sub(1) as u32, - oxide_types::object::PropAttributes::new(false, false, false), + oxide_types::object::PropAttributes::new(false, false, true), ); let undef_shape = core.shape_forge.make_shape(nan_shape, si_undef); @@ -539,7 +541,7 @@ impl KernelSession { global_obj.ensure_hash_props().push(JsValue::undefined()); global_obj.set_data_meta( global_obj.prop_vec_len().saturating_sub(1) as u32, - oxide_types::object::PropAttributes::new(false, false, false), + oxide_types::object::PropAttributes::new(false, false, true), ); let inf_shape = core.shape_forge.make_shape(undef_shape, si_infinity); @@ -547,7 +549,7 @@ impl KernelSession { global_obj.ensure_hash_props().push(JsValue::float(f64::INFINITY)); global_obj.set_data_meta( global_obj.prop_vec_len().saturating_sub(1) as u32, - oxide_types::object::PropAttributes::new(false, false, false), + oxide_types::object::PropAttributes::new(false, false, true), ); P::new(global_obj) diff --git a/crates/oxide_vm/src/vm_dispatch_ctrl.rs b/crates/oxide_vm/src/vm_dispatch_ctrl.rs index 346f8849..a6abe464 100644 --- a/crates/oxide_vm/src/vm_dispatch_ctrl.rs +++ b/crates/oxide_vm/src/vm_dispatch_ctrl.rs @@ -275,12 +275,16 @@ impl Vm { } /// 定义全局 var 绑定数据属性:rd=全局对象,a=值,b=键。 - /// 属性可写/可枚举/不可配置(CreateGlobalVarBinding 的属性描述符)。 + /// + /// 描述符语义(CreateGlobalVarBinding): + /// - 已有可配置属性:保持原 enumerable/configurable、置 writable、更新值—— + /// 重复声明不漂移既有描述符(内置属性 enumerable 不泄漏、值语义不变)。 + /// - 已有不可配置属性:走定义不变量检查(末尾统一路径)——描述符冲突或 + /// 不可写属性值变更时静默 no-op,否则仅更新值、描述符不变。 + /// - 属性缺失:新建可写/可枚举/不可配置数据属性。 /// /// # 边界与前提 - /// - 已有同名不可写属性(NaN/undefined/Infinity 等内置)时静默跳过:规范中 - /// var 绑定仍建立(寄存器侧),全局对象属性保持原样,不抛错。 - /// - 已有同名可写数据属性(重复 var 声明)时按定义更新值。 + /// - rd 非对象(非对象 this)时按 no-op 处理,不抛错。 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]; @@ -290,6 +294,20 @@ impl Vm { 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() }; + // 已有可配置属性:var 绑定描述符不漂移——保 enumerable/configurable, + // 置 writable,更新值(三常量与内置对象声明撞名时值语义按规范落槽)。 + if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { + if let Some(current) = obj.prop_meta_at(pos) { + if current.attributes.configurable() { + let attrs = + PropAttributes::new(true, current.attributes.enumerable(), current.attributes.configurable()); + let _ = self.define_data_property(obj, prop_name_si, value, attrs); + return Ok(()); + } + } + } + // 属性缺失或不可配置既有属性:统一走定义,不可配置时由定义侧不变量 + // 检查决定更新值或静默 no-op。 let _ = self.define_data_property(obj, prop_name_si, value, PropAttributes::new(true, true, false)); Ok(()) } diff --git a/crates/oxide_vm/tests/global_builtin_constant_descriptor_tests.rs b/crates/oxide_vm/tests/global_builtin_constant_descriptor_tests.rs new file mode 100644 index 00000000..d151d5e4 --- /dev/null +++ b/crates/oxide_vm/tests/global_builtin_constant_descriptor_tests.rs @@ -0,0 +1,180 @@ +//! 全局三常量(undefined/NaN/Infinity)描述符与 var 声明交互的回归测试。 +//! +//! 覆盖规范全局对象属性描述符 { writable:false, enumerable:false, configurable:true }: +//! 描述符读、delete 真删、delete 后镜像裸读、var 无初始化声明的描述符翻转与值 +//! 保持(NaN/Infinity 不被抹成 undefined)、var 初始化声明的值落槽、枚举面不 +//! 泄漏、任务只读拦截与 strict TypeError、eval 路径、for-in 枚举面。 + +use oxide_compiler::compiler::Compiler; +use oxide_parser::Allocator; +use oxide_vm::vm::Vm; + +fn compile(source: &str) -> oxide_bytecode::module::CompiledModule { + let allocator = Allocator::default(); + let program = oxide_parser::parse(&allocator, source).expect("parse"); + Compiler::new().compile(&program).expect("compile") +} + +fn run_truthy(source: &str) { + let mut vm = Vm::new(); + let result = vm.run(&compile(source)).expect("run"); + assert!(result.is_bool() && result.as_bool(), "expected true, got: {result:?}\nsource: {source}"); +} + +fn run_string(source: &str) -> String { + let mut vm = Vm::new(); + let result = vm.run(&compile(source)).expect("run"); + if result.is_undefined() { + return "undefined".to_string(); + } + vm.lookup_str(result).unwrap_or_default() +} + +/// 求值并返回完成值字符串;运行期异常返回错误文本(供 TypeError 断言)。 +fn eval(source: &str) -> String { + let mut vm = Vm::new(); + match vm.run(&compile(source)) { + Ok(result) => vm.lookup_str(result).unwrap_or_default(), + Err(e) => e, + } +} + +const CONSTANTS: [&str; 3] = ["undefined", "NaN", "Infinity"]; + +#[test] +fn three_constants_descriptor_matches_spec() { + // #1:描述符 { writable:false, enumerable:false, configurable:true } ×3 常量。 + for name in CONSTANTS { + let src = format!( + "var d = Object.getOwnPropertyDescriptor(globalThis, '{name}'); \ + d.writable === false && d.enumerable === false && d.configurable === true" + ); + run_truthy(&src); + } +} + +#[test] +fn delete_three_constants_removes_property() { + // #2:configurable → delete 返 true 且属性真删(描述符读为 undefined)×3 常量。 + for name in CONSTANTS { + let src = format!( + "delete globalThis.{name} === true \ + && Object.getOwnPropertyDescriptor(globalThis, '{name}') === undefined" + ); + run_truthy(&src); + } +} + +#[test] +fn delete_then_bare_read_via_mirror() { + // #3:删后裸读走镜像预载(入口值),无 ReferenceError。 + assert_eq!(run_string("delete globalThis.undefined; typeof undefined"), "undefined"); +} + +#[test] +fn var_no_init_flips_writable_keeps_value() { + // #4:var 无初始化声明——CreateGlobalVarBinding 翻 writable、保 enumerable/ + // configurable,值保持(三常量现值 undefined,幂等)。 + run_truthy( + "var undefined; var d = Object.getOwnPropertyDescriptor(globalThis, 'undefined'); \ + d.value === undefined && d.writable === true && d.enumerable === false \ + && d.configurable === true", + ); +} + +#[test] +fn var_no_init_preserves_builtin_values() { + // #5:var 声明不得把现存值抹成 undefined(clobber 回归点)×NaN/Infinity。 + run_truthy("var NaN; Number.isNaN(globalThis.NaN) && globalThis.NaN !== undefined"); + run_truthy("var Infinity; globalThis.Infinity === Infinity && globalThis.Infinity !== undefined"); +} + +#[test] +fn var_no_init_builtin_not_enumerable() { + // #6:描述符保 enumerable:false——不泄漏进 Object.keys(回归点)。 + run_truthy("var NaN; Object.keys(globalThis).includes('NaN') === false"); +} + +#[test] +fn var_with_init_readonly_intercept_unchanged() { + // #7:var undefined = 3——只读拦截不变(sloppy 静默 no-op,读仍 undefined)。 + assert_eq!(run_string("var undefined = 3; undefined"), "undefined"); +} + +#[test] +fn strict_var_readonly_builtin_type_error() { + // #8:strict 下 var undefined = 3 抛 TypeError(拦截路径不受描述符翻动影响)。 + let result = eval("'use strict'; var undefined = 3;"); + assert!(result.contains("TypeError"), "expected TypeError, got: {result}"); +} + +#[test] +fn var_builtin_object_descriptor_kept() { + // #9:var Math 无初始化——值与描述符均不漂移(Math 对象保留,e/c 不变)。 + run_truthy( + "var Math; var d = Object.getOwnPropertyDescriptor(globalThis, 'Math'); \ + d.value === Math && d.writable === true && d.enumerable === false \ + && d.configurable === true", + ); +} + +#[test] +fn var_builtin_object_with_init() { + // #10:var Math = 6——值落槽、描述符保 e:false/c:true、不泄漏进枚举。 + run_truthy( + "var Math = 6; var d = Object.getOwnPropertyDescriptor(globalThis, 'Math'); \ + d.value === 6 && d.writable === true && d.enumerable === false \ + && d.configurable === true && Object.keys(globalThis).includes('Math') === false", + ); +} + +#[test] +fn eval_var_readonly_builtin() { + // #11:eval var 路径(0x98 判 writable 而非 configurable)——sloppy no-op、 + // strict(eval 脚本内)TypeError,描述符不变。 + assert_eq!(run_string("eval('var undefined = 3'); undefined"), "undefined"); + let strict = eval("eval('\"use strict\"; var undefined = 3')"); + assert!(strict.contains("TypeError"), "expected TypeError, got: {strict}"); + run_truthy( + "eval('var undefined = 3'); \ + var d = Object.getOwnPropertyDescriptor(globalThis, 'undefined'); \ + d.writable === false && d.configurable === true", + ); +} + +#[test] +fn eval_new_name_still_configurable() { + // #12:eval var 新名属性 configurable:true(0x98 新建分支不变)。 + run_truthy( + "eval('var __g78x = 1'); \ + Object.getOwnPropertyDescriptor(globalThis, '__g78x').configurable === true", + ); +} + +#[test] +fn for_in_does_not_enumerate_three_constants() { + // #13:for-in 仅 enumerable 面——三常量 e:false 不泄漏(不变)。 + run_truthy( + "var ks = []; for (var k in globalThis) ks.push(k); \ + ks.includes('undefined') || ks.includes('NaN') || ks.includes('Infinity') === false", + ); +} + +#[test] +fn control_shapes_unchanged() { + // #14:对照形——delete Math(基线即 c:true)与 typeof NaN 不变。 + run_truthy("delete globalThis.Math === true"); + assert_eq!(run_string("typeof NaN"), "number"); +} + +#[test] +fn function_declaration_builtin_name() { + // 边界:function Math(){}——顶层函数声明取函数值(规范 var 绑定语义), + // 描述符保 e:false/c:true、值可调用。 + run_truthy( + "function Math(){ return 1; } \ + var d = Object.getOwnPropertyDescriptor(globalThis, 'Math'); \ + Math() === 1 && d.value === Math && d.writable === true \ + && d.enumerable === false && d.configurable === true", + ); +} diff --git a/crates/oxide_vm/tests/global_prop_desc_tests.rs b/crates/oxide_vm/tests/global_prop_desc_tests.rs index efd05779..fe686b73 100644 --- a/crates/oxide_vm/tests/global_prop_desc_tests.rs +++ b/crates/oxide_vm/tests/global_prop_desc_tests.rs @@ -1,6 +1,6 @@ //! 内置全局属性描述符测试:规范要求全局构造器/命名空间对象属性 //! enumerable=false(不泄漏进 Object.keys(globalThis) / for-in), -//! 全局 NaN/undefined/Infinity 三常量不可写不可枚举不可配置。 +//! 全局 NaN/undefined/Infinity 三常量不可写不可枚举可配置。 use oxide_compiler::compiler::Compiler; use oxide_parser::Allocator; @@ -61,11 +61,11 @@ fn for_in_global_this_excludes_builtins() { } #[test] -fn nan_undefined_infinity_are_non_writable_non_configurable() { +fn nan_undefined_infinity_are_non_writable_configurable() { for name in ["NaN", "undefined", "Infinity"] { let src = format!( "var d = Object.getOwnPropertyDescriptor(globalThis, '{name}'); \ - d.writable === false && d.enumerable === false && d.configurable === false" + d.writable === false && d.enumerable === false && d.configurable === true" ); eval_truthy(&src); }