diff --git a/crates/oxide_bytecode/src/opcode.rs b/crates/oxide_bytecode/src/opcode.rs index 491bcc3c..1be40570 100644 --- a/crates/oxide_bytecode/src/opcode.rs +++ b/crates/oxide_bytecode/src/opcode.rs @@ -572,6 +572,19 @@ define_opcodes! { DELETE_GLOBAL_PROP_C = 0x9C => "DELETE_GLOBAL_PROP_C", def = Some(SlotSpec::Slot(Slot::Rd)), uses = [SlotSpec::Slot(Slot::A)], pure = false, jump = false, term = false, ic = false, + // 全局函数绑定声明检查(GDI step 9,脚本顶层函数声明):rd=目标对象(顶层 + // This=全局对象)、b=键寄存器。CanDeclareGlobalFunction 三臂:缺失 → + // IsExtensible、c:true → 过、data{w:true,e:true} → 过,余抛 TypeError。 + // 检查阶段发射于任何绑定创建之前(先于 GDI var 序言),失败即无部分绑定。 + CAN_DECLARE_GLOBAL_FUNC = 0x9D => "CAN_DECLARE_GLOBAL_FUNC", + def = None, uses = [SlotSpec::Slot(Slot::Rd), SlotSpec::Slot(Slot::B)], + pure = false, jump = false, term = false, ic = false, + // 创建全局函数绑定(CreateGlobalFunctionBinding,脚本面 deletable=false): + // rd=目标对象、a=值、b=键,与 DEFINE_GLOBAL_PROP 同形。缺失或既有 c:true → + // 定义可写/可枚举/不可配置数据属性;既有 c:false 可写数据 → 仅更新值保描述符。 + DEFINE_GLOBAL_FUNC_BIND = 0x9E => "DEFINE_GLOBAL_FUNC_BIND", + def = None, uses = [SlotSpec::Slot(Slot::Rd), SlotSpec::Slot(Slot::A), SlotSpec::Slot(Slot::B)], + pure = false, jump = false, term = false, ic = false, // ── 成员复合赋值:位/移位 (0x64-0x69) ── COMPOUND_MEMBER_BIT_AND = 0x64 => "COMPOUND_MEMBER_BIT_AND", diff --git a/crates/oxide_emit/src/closure.rs b/crates/oxide_emit/src/closure.rs index e43d482b..ac8c0bc9 100644 --- a/crates/oxide_emit/src/closure.rs +++ b/crates/oxide_emit/src/closure.rs @@ -163,6 +163,20 @@ impl Emitter { names } + /// 同 [`collect_top_level_function_names`] 的收集面,但按**声明序**返回 Vec + /// (供 GDI 检查阶段逆序遍历逐名去重,规范"逆序首见 = 源序最后声明")。 + pub(crate) fn collect_top_level_function_names_ordered(&self, stmts: &[Statement]) -> Vec { + let mut names = Vec::new(); + for stmt in stmts { + if let Statement::FunctionDeclaration(fd) = stmt { + if let Some(id) = &fd.id { + names.push(id.name.to_string()); + } + } + } + names + } + /// 收集当前函数作用域声明的绑定名(参数 + 变量/函数声明,含嵌套 block,不含嵌套函数体)。 pub(crate) fn collect_own_binding_names(&self, param_names: &[&str], stmts: &[Statement]) -> HashSet { let mut names = HashSet::new(); diff --git a/crates/oxide_emit/src/emit.rs b/crates/oxide_emit/src/emit.rs index 0642d558..0615b125 100644 --- a/crates/oxide_emit/src/emit.rs +++ b/crates/oxide_emit/src/emit.rs @@ -1646,6 +1646,21 @@ impl Emitter { } } + /// 顶层函数声明的 A 侧同步写:eval 脚本沿用 [`emit_global_prop_write`] 的 + /// 0x98 分支(属性可配置,创建检查面不覆盖 eval 动态臂);普通脚本经顶层 + /// This 走 0x9E(CreateGlobalFunctionBinding 三臂,声明检查由 GDI 序言的 + /// 0x9D 段预先完成)。 + pub(crate) fn emit_global_func_bind_write(&self, name: &str, val_reg: u32, ctx: &mut CompileCtx) { + if ctx.is_eval_script { + self.emit_global_prop_write(name, val_reg, ctx); + return; + } + let idx = ctx.add_constant(Constant::String(name.to_string())); + let key_reg = ctx.alloc_reg(); + ctx.inst(Inst::load_const(Operand::Reg(key_reg), idx)); + ctx.inst(Inst::define_global_func_bind(Operand::This, Operand::Reg(val_reg), Operand::Reg(key_reg))); + } + /// 顶层 var 声明带初始化的 A 侧同步:PutValue 语义——既有不可写数据属性 /// strict 抛 TypeError / sloppy 静默 no-op;可写照原描述符仅更值。脚本与 /// eval 均经 session 解析全局对象,不依赖顶层 this。 @@ -1699,6 +1714,31 @@ impl Emitter { } } + /// GDI step 9 检查阶段:顶层函数声明名按声明逆序逐名去重,每名发一条 0x9D + /// (CanDeclareGlobalFunction 运行期判定,撞既有不可配置非可写数据属性或 + /// 不可扩展上的缺失名抛 TypeError)。发射位必须位于 GDI var 序言之先—— + /// 检查失败时任何绑定(含 var 序言新建属性)不得实例化。 + /// + /// # 边界与前提 + /// - 仅普通脚本调用(`is_eval_script` 面由调用点门控);eval 动态臂与 + /// 80 门禁(三常量编译期面)零触碰。 + /// - 仅遍历语句列表直接子级具名函数声明(生成器/异步声明同节点类型,天然 + /// 覆盖);块内函数声明与 `export default function` 不入全局检查面。 + pub(crate) fn emit_gdi_func_decl_checks(&self, stmts: &[Statement], ctx: &mut CompileCtx) { + let names = self.collect_top_level_function_names_ordered(stmts); + let mut seen = HashSet::new(); + for name in names.iter().rev() { + // 逆序首见即源序最后声明者(规范去重口径),重名只查一次。 + if !seen.insert(name.as_str()) { + continue; + } + let idx = ctx.add_constant(Constant::String(name.clone())); + let key_reg = ctx.alloc_reg(); + ctx.inst(Inst::load_const(Operand::Reg(key_reg), idx)); + ctx.inst(Inst::can_declare_global_func(Operand::This, Operand::Reg(key_reg))); + } + } + /// 把完整程序编译为顶层模块体的 IRFunction。 /// /// 调用方为 `oxide_compiler::Compiler::compile`:本函数完成 emit 半程, @@ -1739,6 +1779,13 @@ impl Emitter { } } + // GDI step 9 函数臂检查阶段(运行期,先于任何绑定实例化):普通脚本 + // 顶层函数声明撞既有不可配置全局属性抛 TypeError,sloppy/strict 同形。 + // eval 面零触碰:静态三常量面由上方 80 门禁编译期拦,动态臂另归口。 + if !ctx.is_eval_script { + self.emit_gdi_func_decl_checks(&program.body, &mut ctx); + } + self.predeclare_function_declarations(&program.body, &mut ctx); // 预注册 builtin 引用(先于任何临时寄存器),builtin 槽不进入临时寄存器池。 diff --git a/crates/oxide_emit/src/stmt/declaration/function_decl.rs b/crates/oxide_emit/src/stmt/declaration/function_decl.rs index 0396a40b..74f9963a 100644 --- a/crates/oxide_emit/src/stmt/declaration/function_decl.rs +++ b/crates/oxide_emit/src/stmt/declaration/function_decl.rs @@ -73,7 +73,7 @@ impl Emitter { // 严格 eval 代码函数声明绑定 eval 自身 lexical 环境、不落全局对象:抑制 A // 侧写(局部绑定读面未暴露),同时避免不可写全局内置在 0x98 上误抛 strict。 if ctx.is_global_scope && ctx.scopes.symbols.scopes.len() == 1 && !(ctx.is_eval_script && ctx.is_strict) { - self.emit_global_prop_write(&name, var_reg, ctx); + self.emit_global_func_bind_write(&name, var_reg, ctx); } Ok(None) } diff --git a/crates/oxide_ir/src/contract.rs b/crates/oxide_ir/src/contract.rs index 71403466..3c6005d1 100644 --- a/crates/oxide_ir/src/contract.rs +++ b/crates/oxide_ir/src/contract.rs @@ -740,11 +740,18 @@ mod tests { 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, OpCode::DEFINE_GLOBAL_PROP_IF_ABSENT], + &[ + OpCode::DEFINE_PROP, + OpCode::DEFINE_GLOBAL_PROP, + OpCode::DEFINE_GLOBAL_PROP_IF_ABSENT, + OpCode::DEFINE_GLOBAL_FUNC_BIND, + ], None, &[1, 2, 3], false, ); + // 全局函数绑定声明检查:a 槽无操作数,use 集只含 rd/b。 + assert_contract(OpCode::CAN_DECLARE_GLOBAL_FUNC, None, &[1, 3], false); assert_group(&[OpCode::DEFINE_GLOBAL_PROP_C, OpCode::DEFINE_GLOBAL_PROP_C_IF_ABSENT], None, &[2], false); // delete 全局内置:结果写 rd、镜像槽寄存器是 a 槽 use assert_contract(OpCode::DELETE_GLOBAL_PROP_C, Some(1), &[2], false); diff --git a/crates/oxide_ir/src/inst.rs b/crates/oxide_ir/src/inst.rs index 1c959779..fb5c9b33 100644 --- a/crates/oxide_ir/src/inst.rs +++ b/crates/oxide_ir/src/inst.rs @@ -255,6 +255,20 @@ impl Inst { ) } + /// 顶层函数声明的全局绑定声明检查(GDI step 9):target 为全局对象,key 为 + /// 声明名寄存器。运行期按 CanDeclareGlobalFunction 判定——撞既有不可配置 + /// 非可写数据属性(或不可扩展上的缺失名)抛 TypeError。 + pub fn can_declare_global_func(target: Operand, key: Operand) -> Self { + Self::new(OpCode::CAN_DECLARE_GLOBAL_FUNC, target, Operand::None, key) + } + + /// 创建全局函数绑定(CreateGlobalFunctionBinding,脚本面 deletable=false): + /// target 为全局对象,value/key 为寄存器。缺失或既有可配置属性定义可写/可枚举/ + /// 不可配置数据属性;既有不可配置可写数据属性仅更新值保描述符。 + pub fn define_global_func_bind(target: Operand, value: Operand, key: Operand) -> Self { + Self::new(OpCode::DEFINE_GLOBAL_FUNC_BIND, target, value, key) + } + /// 静态删除属性:obj 同时放 rd/a 槽,const_idx 为属性名常量下标。 pub fn delete_prop_static(obj: Operand, const_idx: u32) -> Self { Self::with_ext(OpCode::DELETE_PROP_STATIC, obj, obj, Operand::None, &[const_idx]) diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index a9c54298..ca4360e0 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -2195,6 +2195,14 @@ impl Vm { self.dispatch_delete_global_prop_c(rd, a, key_idx)?; } + OpCode::CAN_DECLARE_GLOBAL_FUNC => { + self.dispatch_can_declare_global_func(rd, b)?; + } + + OpCode::DEFINE_GLOBAL_FUNC_BIND => { + self.dispatch_define_global_func_bind(rd, a, b)?; + } + OpCode::DEFINE_PROP_ATTRS => { let attrs = self.bytecode[self.pc] as u8; self.pc += 1; diff --git a/crates/oxide_vm/src/vm_dispatch_ctrl.rs b/crates/oxide_vm/src/vm_dispatch_ctrl.rs index a00633ab..1182a02b 100644 --- a/crates/oxide_vm/src/vm_dispatch_ctrl.rs +++ b/crates/oxide_vm/src/vm_dispatch_ctrl.rs @@ -437,6 +437,123 @@ impl Vm { } } + /// 顶层函数声明的全局绑定声明检查(GDI step 9,CanDeclareGlobalFunction): + /// rd=全局对象,b=声明名寄存器。 + /// + /// 三臂判定(与规范同形,sloppy/strict 无区分): + /// - 属性缺失:全局对象不可扩展 → TypeError。 + /// - 既有可配置属性(数据或 accessor):通过。 + /// - 既有不可配置属性:仅可写且可枚举的数据描述符通过,余(不可写数据 / + /// 不可枚举数据 / accessor)抛 TypeError。 + /// + /// # 边界与前提 + /// - rd 槽固定为顶层 `this`:本形以所有脚本(含严格模式)顶层 this 即全局 + /// 对象为成立前提。 + /// - rd 非对象(非对象 this)时按 no-op 处理,不抛错(同 0x99 族)。 + /// - 检查阶段发射于任何绑定实例化之前,抛错即整脚本无部分绑定。 + pub(crate) fn dispatch_can_declare_global_func(&mut self, rd: usize, b: usize) -> Result<(), String> { + vm_trace!("CAN_DECLARE_GLOBAL_FUNC rd={} key={}", rd, b); + let obj_val = self.regs[rd]; + if !obj_val.is_object() { + return Ok(()); + } + let key_val = self.regs[b]; + if !key_val.is_string() { + return Err(format!("CAN_DECLARE_GLOBAL_FUNC key register {b} is not a string")); + } + let si = self.property_key_si(key_val)?; + // SAFETY: key_val 已校验为字符串值;错误消息走 lossy 文本(不可解析名 + // 含孤立 surrogate 时映射为 FFFD,仅为展示用途)。 + let name = unsafe { (*key_val.as_string_ptr()).as_lossy_str() }; + // SAFETY: rd 持当前帧存活对象值(顶层 this 为全局对象),指针在本指令 + // 派发期间有效。 + let obj = unsafe { &mut *obj_val.as_js_object_ptr() }; + if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), si) { + if let Some(current) = obj.prop_meta_at(pos) { + if !current.attributes.configurable() + && (current.is_accessor || !current.attributes.writable() || !current.attributes.enumerable()) + { + return self.raise_error_kind( + "TypeError", + &format!("Cannot declare function '{name}': global property is not configurable"), + ); + } + } + // 无 meta 的 shape 槽按可配置缺省通过。 + return Ok(()); + } + // 属性缺失:唯一失败面是全局对象不可扩展(两模式均抛 TypeError)。 + if !obj.is_extensible() { + return self.raise_error_kind( + "TypeError", + &format!("Cannot declare function '{name}': global object is not extensible"), + ); + } + Ok(()) + } + + /// 创建全局函数绑定(CreateGlobalFunctionBinding,脚本面 deletable=false): + /// rd=全局对象,a=函数值,b=声明名寄存器。 + /// + /// 三臂(与规范同形): + /// - 属性缺失:全局对象不可扩展 → TypeError(两模式均抛);否则新建可写/ + /// 可枚举/不可配置数据属性。 + /// - 既有可配置属性(数据或 accessor):全重配为 {value, 可写, 可枚举, + /// 不可配置}(c:true 定义必成)。 + /// - 既有不可配置属性:可写数据属性仅更新值保描述符;不可写数据或 accessor + /// → TypeError(对称防御——正常面已由 0x9D 检查阶段预先排除)。 + /// + /// # 边界与前提 + /// - rd 槽固定为顶层 `this`(同 0x99 族前提);rd 非对象时 no-op。 + pub(crate) fn dispatch_define_global_func_bind(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { + vm_trace!("DEFINE_GLOBAL_FUNC_BIND rd={} value={} key={}", rd, a, b); + let obj_val = self.regs[rd]; + if !obj_val.is_object() { + return Ok(()); + } + let key_val = self.regs[b]; + if !key_val.is_string() { + return Err(format!("DEFINE_GLOBAL_FUNC_BIND key register {b} is not a string")); + } + let si = self.property_key_si(key_val)?; + let value = self.regs[a]; + // SAFETY: key_val 已校验为字符串值;错误消息走 lossy 文本(不可解析名 + // 含孤立 surrogate 时映射为 FFFD,仅为展示用途)。 + let name = unsafe { (*key_val.as_string_ptr()).as_lossy_str() }; + // SAFETY: rd 持当前帧存活对象值(顶层 this 为全局对象),指针在本指令 + // 派发期间有效。 + let obj = unsafe { &mut *obj_val.as_js_object_ptr() }; + if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), si) { + if let Some(current) = obj.prop_meta_at(pos) { + if current.attributes.configurable() { + // 既有可配置属性:全重配 {可写, 可枚举, 不可配置}。 + return match self.define_data_property(obj, si, value, PropAttributes::new(true, true, false)) { + Ok(()) => Ok(()), + Err(msg) => self.raise_error_kind("TypeError", &msg), + }; + } + if !current.is_accessor && current.attributes.writable() { + // 不可配置可写数据:保描述符仅更值(0x98 同形)。 + return match self.define_data_property(obj, si, value, current.attributes) { + Ok(()) => Ok(()), + Err(msg) => self.raise_error_kind("TypeError", &msg), + }; + } + // 不可写数据 / accessor:检查阶段后不可达,对称防御抛 TypeError。 + return self.raise_error_kind( + "TypeError", + &format!("Cannot declare function '{name}': global property is not writable"), + ); + } + // 无 meta 的 shape 槽按可配置缺省走全重配。 + } + // 属性缺失:新建可写/可枚举/不可配置;唯一失败面是全局对象不可扩展。 + match self.define_data_property(obj, si, value, PropAttributes::new(true, true, false)) { + 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/tests/global_builtin_constant_descriptor_tests.rs b/crates/oxide_vm/tests/global_builtin_constant_descriptor_tests.rs index 193731d5..9f58b1f3 100644 --- a/crates/oxide_vm/tests/global_builtin_constant_descriptor_tests.rs +++ b/crates/oxide_vm/tests/global_builtin_constant_descriptor_tests.rs @@ -188,13 +188,13 @@ fn control_shapes_unchanged() { #[test] fn function_declaration_builtin_name() { - // 边界:function Math(){}——顶层函数声明取函数值(规范 var 绑定语义), - // 描述符保 e:false/c:true、值可调用。 + // 边界:function Math(){}——顶层函数声明取函数值(CreateGlobalFunctionBinding + // 重配臂:既有 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", + && d.enumerable === true && d.configurable === false", ); } diff --git a/crates/oxide_vm/tests/global_fn_decl_builtin_tests.rs b/crates/oxide_vm/tests/global_fn_decl_builtin_tests.rs index 1748e979..31015a67 100644 --- a/crates/oxide_vm/tests/global_fn_decl_builtin_tests.rs +++ b/crates/oxide_vm/tests/global_fn_decl_builtin_tests.rs @@ -1,10 +1,12 @@ -//! 顶层函数声明撞不可写全局内置(undefined/NaN/Infinity):脚本面声明不更新绑定 -//! (A 侧保留原常量,读/调/typeof 面收敛全局对象,用户名与模块面不受影响); -//! eval 代码面——sloppy 函数声明在建立全局绑定前抛 TypeError(程序首指令 abrupt, -//! 无部分创建,A 侧保留),strict 函数声明为局部绑定不抛、不落全局。 +//! 顶层函数声明撞不可配置全局属性(三常量 c:false):脚本面 GDI 检查阶段在任何 +//! 绑定实例化前抛 TypeError(sloppy/strict 同形),A 侧保留原常量、无部分 var +//! 绑定,用户名与模块面不受影响;eval 代码面——sloppy 函数声明在建立全局绑定前抛 +//! TypeError(程序首指令 abrupt,无部分创建,A 侧保留),strict 函数声明为局部 +//! 绑定不抛、不落全局。 use std::sync::Arc; +use oxide_bytecode::module::CompiledModule; use oxide_compiler::compiler::Compiler; use oxide_emit::module::{ModuleSourceLoader, ResolvedModule}; use oxide_types::value::JsValue; @@ -45,6 +47,33 @@ fn script_str(vm: &mut Vm, source: &str) -> Option { vm.lookup_str(r) } +/// 单脚本未捕获异常文本(脚本正常完成时 panic)。 +fn script_err_text(source: &str) -> String { + let allocator = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&allocator, source).expect("parse"); + let module = Compiler::new().compile(&program).expect("compile"); + let mut vm = Vm::new(); + match vm.run(&Arc::new(module)) { + Err(e) => e, + Ok(v) => panic!("expected uncaught error, got: {v:?}\nsource: {source}"), + } +} + +/// 声明脚本跑完(必须抛)后续跑探针脚本,返回探针的字符串化完成值——抛后 +/// A 侧保留面的判别探针。 +fn err_then_script_str(decl: &str, probe: &str) -> String { + let compile = |src: &str| -> Arc { + let allocator = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&allocator, src).expect("parse"); + Arc::new(Compiler::new().compile(&program).expect("compile")) + }; + let mut vm = Vm::new(); + let err = vm.run(&compile(decl)); + assert!(err.is_err(), "声明脚本应抛未捕获异常,实际: {err:?}\ndecl: {decl}"); + let result = vm.run(&compile(probe)).expect("probe run"); + vm.lookup_str(result).unwrap_or_else(|| format!("{result}")) +} + /// 无依赖模块加载器:本组测试模块源无 import,resolve 不被调用。 struct NoopLoader; @@ -56,57 +85,61 @@ impl ModuleSourceLoader for NoopLoader { } } -// ── 脚本面:三常量函数声明静默 no-op,绑定不更新 ── +// ── 脚本面:三常量函数声明 GDI 检查阶段抛 TypeError,A 侧保留 ── #[test] -fn script_fn_decl_three_constants_sloppy_typeof_keeps_original_type() { - assert_eq!(eval_str("function undefined(){} typeof undefined"), "undefined"); - assert_eq!(eval_str("function NaN(){} typeof NaN"), "number"); - assert_eq!(eval_str("function Infinity(){} typeof Infinity"), "number"); +fn script_fn_decl_three_constants_sloppy_throws_type_error() { + // GDI step 9 检查阶段先于任何代码运行:撞三常量(c:false)抛声明期 TypeError, + // sloppy 面。 + let err = script_err_text("function undefined(){} typeof undefined"); + assert!(err.contains("TypeError"), "sloppy undefined 声明应抛 TypeError,实际: {err}"); + let err = script_err_text("function NaN(){} typeof NaN"); + assert!(err.contains("TypeError"), "sloppy NaN 声明应抛 TypeError,实际: {err}"); + let err = script_err_text("function Infinity(){} typeof Infinity"); + assert!(err.contains("TypeError"), "sloppy Infinity 声明应抛 TypeError,实际: {err}"); + // A 侧保留:抛后探针脚本读三常量仍见原类型。 + assert_eq!( + err_then_script_str( + "function undefined(){} typeof undefined", + "typeof undefined + ':' + typeof NaN + ':' + typeof Infinity", + ), + "undefined:number:number", + ); } #[test] -fn script_fn_decl_three_constants_strict_no_throw_typeof_keeps_original_type() { - assert_eq!(eval_str("'use strict'; function undefined(){} typeof undefined"), "undefined"); - assert_eq!(eval_str("'use strict'; function NaN(){} typeof NaN"), "number"); - assert_eq!(eval_str("'use strict'; function Infinity(){} typeof Infinity"), "number"); +fn script_fn_decl_three_constants_strict_throws_type_error() { + // GDI 无严格性区分:strict 面同形抛 TypeError。 + let err = script_err_text("'use strict'; function undefined(){} typeof undefined"); + assert!(err.contains("TypeError"), "strict undefined 声明应抛 TypeError,实际: {err}"); + let err = script_err_text("'use strict'; function NaN(){} typeof NaN"); + assert!(err.contains("TypeError"), "strict NaN 声明应抛 TypeError,实际: {err}"); + let err = script_err_text("'use strict'; function Infinity(){} typeof Infinity"); + assert!(err.contains("TypeError"), "strict Infinity 声明应抛 TypeError,实际: {err}"); } #[test] fn script_fn_decl_nan_infinity_self_equality_preserved() { - let r = eval("'use strict'; function NaN(){}; NaN !== NaN").unwrap(); - assert!(r.is_bool() && r.as_bool(), "声明后 NaN 自反不等性应保留,实际 {r:?}"); - let r = eval("'use strict'; function Infinity(){}; Infinity === Infinity").unwrap(); - assert!(r.is_bool() && r.as_bool(), "声明后 Infinity 自相等性应保留,实际 {r:?}"); + // 抛点在前置检查阶段,函数体与尾表达式均不可达;A 侧值不动,探针脚本的 + // 自反不等/自相等性仍成立。 + let err = script_err_text("'use strict'; function NaN(){}; NaN !== NaN"); + assert!(err.contains("TypeError"), "NaN 声明应抛 TypeError,实际: {err}"); + assert_eq!(err_then_script_str("function NaN(){}", "NaN !== NaN && Infinity === Infinity",), "true",); } #[test] fn script_fn_decl_global_descriptor_value_and_writability_preserved() { - // undefined:值保留原常量(=== undefined),writable 保持 false。 - let r = eval( - "function undefined(){} var d = Object.getOwnPropertyDescriptor(globalThis, 'undefined'); d.value === undefined && d.writable === false", - ) - .unwrap(); - assert!(r.is_bool() && r.as_bool(), "undefined 描述符应保留原常量值与不可写,实际 {r:?}"); - // NaN:值保留原常量(自反不等),writable 保持 false。 - let r = eval( - "function NaN(){} var d = Object.getOwnPropertyDescriptor(globalThis, 'NaN'); d.value !== d.value && d.writable === false", - ) - .unwrap(); - assert!(r.is_bool() && r.as_bool(), "NaN 描述符应保留原常量值与不可写,实际 {r:?}"); - // Infinity:值保留原常量,writable 保持 false。 - let r = eval( - "function Infinity(){} var d = Object.getOwnPropertyDescriptor(globalThis, 'Infinity'); d.value === Infinity && d.writable === false", - ) - .unwrap(); - assert!(r.is_bool() && r.as_bool(), "Infinity 描述符应保留原常量值与不可写,实际 {r:?}"); - // 声明不改变描述符:三常量属性不可枚举,Object.keys 不新增名字。 - let r = eval( - "function undefined(){} function NaN(){} function Infinity(){} \ - Object.keys(globalThis).filter(function (k) { return k === 'undefined' || k === 'NaN' || k === 'Infinity'; }).length === 0", - ) - .unwrap(); - assert!(r.is_bool() && r.as_bool(), "声明不应新增可枚举全局名,实际 {r:?}"); + // 抛后 A 侧描述符不变:值原常量、不可写/不可枚举/不可配置;同脚本 var 绑定 + // 无部分创建(检查阶段先于 GDI 序言);Object.keys 不新增三常量名。 + let s = err_then_script_str( + "var x; function undefined(){}", + "var d = Object.getOwnPropertyDescriptor(globalThis, 'undefined'); \ + (d.value === undefined && d.writable === false && d.enumerable === false \ + && d.configurable === false) \ + && Object.getOwnPropertyDescriptor(globalThis, 'x') === undefined \ + && Object.keys(globalThis).filter(function (k) { return k === 'undefined' || k === 'NaN' || k === 'Infinity'; }).length === 0", + ); + assert_eq!(s, "true", "A 侧描述符应保留且无部分 var 绑定,实际: {s}"); } #[test] diff --git a/crates/oxide_vm/tests/global_var_binding_tests.rs b/crates/oxide_vm/tests/global_var_binding_tests.rs index 705df8f1..abf21900 100644 --- a/crates/oxide_vm/tests/global_var_binding_tests.rs +++ b/crates/oxide_vm/tests/global_var_binding_tests.rs @@ -64,6 +64,18 @@ fn phase2_err_text(phase1: &str, phase2: &str) -> String { } } +/// phase2 必须抛未捕获异常的变体;返回同 VM 续跑探针脚本的字符串化完成值 +/// (抛后 A 侧保留面的判别探针)。非字符串完成值走 Display(布尔形探针返回 +/// "true"/"false")。 +fn phase2_err_then_probe(phase1: &str, phase2: &str, probe: &str) -> String { + let mut vm = Vm::new(); + vm.run(&Arc::new(compile(phase1))).expect("run1"); + vm.reset(); + assert!(vm.run(&Arc::new(compile(phase2))).is_err(), "phase2 应抛未捕获异常: {phase2}"); + let result = vm.run(&Arc::new(compile(probe))).expect("probe run"); + vm.lookup_str(result).unwrap_or_else(|| format!("{result}")) +} + #[test] fn top_level_var_lands_on_global_this() { eval_truthy("var x = 5; globalThis.x === 5"); @@ -425,13 +437,23 @@ fn top_level_function_shadow_and_var_no_init() { eval_truthy("function f(){return 1;} var f = 5; f === 5 && globalThis.f === 5"); } -/// 声明处描述符感知写:既有不可写不可配置属性声明不更值(定义不变量静默 -/// no-op),裸读与反射一致见既有值。 +/// 函数臂撞既有不可配置不可写数据属性:GDI 检查阶段抛 TypeError(两模式同形), +/// 无部分创建——抛后值与描述符均保留(corpus 无部分绑定形)。 #[test] fn top_level_function_decl_respects_nonwritable_property() { - assert_two_phases_truthy( + let err = phase2_err_text( "Object.defineProperty(globalThis, 'f', {value: 1, writable: false, configurable: false})", - "function f(){} f === 1 && globalThis.f === 1 && typeof f === 'number'", + "function f(){}", + ); + assert!(err.contains("TypeError"), "声明应抛 TypeError,实际: {err}"); + assert_eq!( + phase2_err_then_probe( + "Object.defineProperty(globalThis, 'f', {value: 1, writable: false, configurable: false})", + "function f(){}", + "f === 1 && globalThis.f === 1 && typeof f === 'number'", + ), + "true", + "抛后 A 侧应保留原值与描述符", ); } @@ -445,18 +467,18 @@ fn top_level_function_survives_reset() { )); } -/// builtin 名形零污染:可写 builtin 名(Math)声明更值保描述符;不可写 -/// builtin 名(undefined)声明不更新,裸读与反射一致见既有值。 +/// builtin 名形:c:true builtin 名(Math)声明经 CreateGlobalFunctionBinding +/// 重配臂落标准函数描述符(可写/可枚举/不可配置,值可调用);不可配置 +/// builtin 名(undefined,c:false)声明在 GDI 检查阶段抛 TypeError。 #[test] fn top_level_function_decl_builtin_name_zero_pollution() { eval_truthy("function Math(){ return 7; } Math() === 7 && globalThis.Math() === 7 && Math === globalThis.Math"); eval_truthy( "function Math(){} var d = Object.getOwnPropertyDescriptor(globalThis, 'Math'); \ - d.writable === true && d.enumerable === false && d.configurable === true", - ); - eval_truthy( - "function undefined(){ return 1; } typeof undefined === 'undefined' && globalThis.undefined === undefined", + d.writable === true && d.enumerable === true && d.configurable === false", ); + let err = phase2_err_text("0", "function undefined(){ return 1; }"); + assert!(err.contains("TypeError"), "undefined 声明应抛 TypeError,实际: {err}"); } // ── 声明带初始化的 A 侧写是 PutValue:strict 撞既有不可写用户属性抛 TypeError ──