diff --git a/crates/oxide_builtins/src/module.rs b/crates/oxide_builtins/src/module.rs index 92a4834e..03588103 100644 --- a/crates/oxide_builtins/src/module.rs +++ b/crates/oxide_builtins/src/module.rs @@ -14,6 +14,12 @@ use oxide_types::value::JsValue; /// `Symbol.toStringTag` 在 well-known symbol 表中的序号,命名空间标签键按其编码。 const TO_STRING_TAG_SYMBOL_ID: u32 = 9; +/// 未初始化导出被读时的 ReferenceError 文本。 +/// +/// exotic `[[Get]]` 与 `[[GetOwnProperty]]` 消费端(`Object.*`、for-in)共用同一 +/// 文本,错误类型由消费端的 `create_reference_error` / `create_from_text` 恢复。 +pub const NS_UNINITIALIZED_MESSAGE: &str = "Cannot access module export before initialization"; + /// 模块命名空间导出属性描述符:可写、可枚举、不可配置(module namespace exotic)。 /// /// 规范 10.4.6.5 返回 `{writable: true, enumerable: true, configurable: false}`; diff --git a/crates/oxide_builtins/src/object.rs b/crates/oxide_builtins/src/object.rs index 9ba76d01..50faf1a0 100644 --- a/crates/oxide_builtins/src/object.rs +++ b/crates/oxide_builtins/src/object.rs @@ -469,6 +469,25 @@ pub fn object_constructor(vm: &mut H, args: &[u8]) -> NativeResult { NativeResult::Ok(JsValue::from_js_object(obj)) } +/// 模块命名空间字符串导出按 exotic `[[GetOwnProperty]]` 读取活值状态: +/// - `Ok(Some(v))`:该键是已初始化导出,`v` 为条目活值; +/// - `Ok(None)`:非 module namespace / 非导出名 / symbol 键,调用方落普通属性路径; +/// - `Err(msg)`:导出名已预注册但未初始化,调用方抛 `msg` 对应的 ReferenceError。 +/// +/// # 注意事项 +/// - 只查条目表、不读真实属性槽:条目表是 live 命名空间的权威状态,真实槽可能 +/// 只是初始化前的 `undefined` 占位。 +fn namespace_export_get(obj: &JsObject, key_si: u32) -> Result, &'static str> { + if is_symbol_key(key_si) { + return Ok(None); + } + match crate::module::module_ns_export(obj, key_si) { + None => Ok(None), + Some(crate::module::ModuleNsQuery::Initialized(v)) => Ok(Some(v)), + Some(crate::module::ModuleNsQuery::Uninitialized) => Err(crate::module::NS_UNINITIALIZED_MESSAGE), + } +} + /// `Object.keys(obj)`:返回可枚举自身属性的字符串名数组。 pub fn object_keys(vm: &mut H, args: &[u8]) -> NativeResult { let obj_ptr = match require_obj_arg(vm, args, "keys") { @@ -485,6 +504,12 @@ pub fn object_keys(vm: &mut H, args: &[u8]) -> NativeResult { .unwrap_or(PropAttributes::DEFAULT_DATA.enumerable()) }) .collect(); + // EnumerableOwnProperties 对每个字符串键走 `? [[GetOwnProperty]]`:未初始化导出抛错。 + for (si, _offset) in &owned_keys { + if let Err(msg) = namespace_export_get(obj, *si) { + return NativeResult::Err(crate::error::create_reference_error(vm, msg)); + } + } let n = owned_keys.len(); let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject; let arr = vm.alloc_object(JsObject::new_array( @@ -580,7 +605,8 @@ pub fn object_assign(vm: &mut H, args: &[u8]) -> NativeResult { // 触发 getter;getter 抛错按规范中断整个 assign。 let val = match vm.ordinary_get(source, si, source_val) { Ok(v) => v, - Err(err) => return NativeResult::Err(crate::error::create_type_error(vm, &err)), + // ReferenceError 等错误文本经 create_from_text 恢复类型,不得降级为 TypeError。 + Err(err) => return NativeResult::Err(crate::error::create_from_text(vm, &err)), }; all_assignments.push((si, val)); } @@ -848,7 +874,12 @@ pub fn object_get_own_property_descriptor(vm: &mut H, args: &[u8]) -> let Some(offset) = vm.get_own_property_slot(obj, key) else { return NativeResult::Ok(JsValue::undefined()); }; - let found_value = obj.get_prop_at(offset); + // 模块命名空间字符串导出:`? [[Get]]` 读条目活值,未初始化抛 ReferenceError。 + let found_value = match namespace_export_get(obj, key) { + Ok(Some(value)) => value, + Ok(None) => obj.get_prop_at(offset), + Err(msg) => return NativeResult::Err(crate::error::create_reference_error(vm, msg)), + }; let found_meta = obj.prop_meta_at(offset); let sf_ptr = vm.kernel_core().perm_interner().as_ref() as *const PermInterner; @@ -1345,7 +1376,14 @@ pub fn object_proto_has_own_property(vm: &mut H, args: &[u8]) -> Nati Err(msg) => return NativeResult::Err(crate::error::create_type_error(vm, &msg)), }; let obj = unsafe { &*obj_val.as_js_object_ptr() }; - NativeResult::Ok(JsValue::bool(vm.get_own_property_slot(obj, key_si).is_some())) + if vm.get_own_property_slot(obj, key_si).is_none() { + return NativeResult::Ok(JsValue::bool(false)); + } + // HasOwnProperty 经 `? [[GetOwnProperty]]`:未初始化导出抛 ReferenceError。 + if let Err(msg) = namespace_export_get(obj, key_si) { + return NativeResult::Err(crate::error::create_reference_error(vm, msg)); + } + NativeResult::Ok(JsValue::bool(true)) } /// `Object.prototype.propertyIsEnumerable(key)`:指定自身属性是否可枚举。 @@ -1367,6 +1405,10 @@ pub fn object_proto_property_is_enumerable(vm: &mut H, args: &[u8]) - let Some(pos) = vm.get_own_property_slot(obj, key_si) else { return NativeResult::Ok(JsValue::bool(false)); }; + // PropertyIsEnumerable 同样先经 `? [[GetOwnProperty]]` 再取 enumerable 位。 + if let Err(msg) = namespace_export_get(obj, key_si) { + return NativeResult::Err(crate::error::create_reference_error(vm, msg)); + } let enumerable = obj .prop_meta_at(pos) .map(|meta| meta.attributes.enumerable()) @@ -1453,7 +1495,12 @@ pub fn object_entries(vm: &mut H, args: &[u8]) -> NativeResult { )); for (i, (si, offset)) in owned_keys.iter().enumerate() { let key_val = key_si_to_js_value(vm, *si); - let val = obj.get_prop_at(*offset); + // EnumerableOwnProperties 的 "key+value":值经条目活值查询,未初始化抛错。 + let val = match namespace_export_get(obj, *si) { + Ok(Some(value)) => value, + Ok(None) => obj.get_prop_at(*offset), + Err(msg) => return NativeResult::Err(crate::error::create_reference_error(vm, msg)), + }; let pair = vm.alloc_object(JsObject::new_array( EMPTY_SHAPE_ID, JsValue::from_js_object(array_proto), @@ -1636,7 +1683,12 @@ pub fn object_values(vm: &mut H, args: &[u8]) -> NativeResult { vm.epoch().bump(), )); for (i, (_si, offset)) in owned_keys.iter().enumerate() { - let val = obj.get_prop_at(*offset); + // EnumerableOwnProperties 的 "value":值经条目活值查询,未初始化抛错。 + let val = match namespace_export_get(obj, *_si) { + Ok(Some(value)) => value, + Ok(None) => obj.get_prop_at(*offset), + Err(msg) => return NativeResult::Err(crate::error::create_reference_error(vm, msg)), + }; unsafe { (*arr).set_prop_at(i, val); } diff --git a/crates/oxide_emit/src/compile_ctx.rs b/crates/oxide_emit/src/compile_ctx.rs index 53a6f932..cf8ccdf5 100644 --- a/crates/oxide_emit/src/compile_ctx.rs +++ b/crates/oxide_emit/src/compile_ctx.rs @@ -137,6 +137,10 @@ pub struct CompileCtx { pub(crate) for_head_store_registers: HashSet, /// 模块编译上下文:当前模块命名空间对象寄存器(`__moduleObject` 返回值)。 pub(crate) module_ns_reg: Option, + /// 写穿反演表:本模块源绑定名 → 引用它的导出名集合。仅自导入 ns 的 live 模块 + /// 填充;顶层对该源绑定的赋值据此同步命名空间条目。嵌套函数 ctx 为新建, + /// 不继承本字段,写穿天然只作用于模块顶层。 + pub(crate) module_local_exports: HashMap>, /// 已求值依赖模块的命名空间对象寄存器(按 import/export source 字符串索引)。 pub(crate) module_dep_ns_regs: HashMap, /// 自导入(import from 自身)的 source 字符串集合:绑定走别名语义,不能链接期快照。 @@ -226,6 +230,7 @@ impl CompileCtx { register_update_names: Vec::new(), for_head_store_registers: HashSet::new(), module_ns_reg: None, + module_local_exports: HashMap::new(), module_dep_ns_regs: HashMap::new(), module_self_import_specs: HashSet::new(), module_self_aliases: HashMap::new(), diff --git a/crates/oxide_emit/src/expr/assignment.rs b/crates/oxide_emit/src/expr/assignment.rs index 9eaa64ad..289d1511 100644 --- a/crates/oxide_emit/src/expr/assignment.rs +++ b/crates/oxide_emit/src/expr/assignment.rs @@ -66,6 +66,7 @@ impl Emitter { Operand::Reg(val_reg), Operand::Imm(uv as u16), )); + self.emit_module_write_through(name, val_reg, ctx)?; Ok(val_reg) } else if let Some(cell_idx) = captured_cell { let val_reg = ctx.alloc_reg(); @@ -86,6 +87,7 @@ impl Emitter { Operand::Reg(val_reg), Operand::Imm(cell_idx as u16), )); + self.emit_module_write_through(name, val_reg, ctx)?; Ok(val_reg) } else { let var_reg = ctx.lookup_or_global(name); @@ -128,6 +130,8 @@ impl Emitter { // 可写内置名:RMW 新值同步落全局对象属性。 self.emit_global_put_write(name, var_reg, ctx); } + // live 模块顶层复合赋值同样写穿命名空间条目。 + self.emit_module_write_through(name, var_reg, ctx)?; Ok(var_reg) } } @@ -586,12 +590,12 @@ impl Emitter { if with_dynamic { let is_const = ctx.lookup_const_flag(name); let const_flag: u16 = if is_const { 1 } else { 0 }; - self.emit_with_dynamic_write(name, val_reg, const_flag, ctx); + self.emit_with_dynamic_write(name, val_reg, const_flag, ctx)?; return Ok(val_reg); } let is_const = ctx.lookup_const_flag(name); let const_flag: u16 = if is_const { 1 } else { 0 }; - self.emit_identifier_store(name, val_reg, const_flag, ctx); + self.emit_identifier_store(name, val_reg, const_flag, ctx)?; Ok(val_reg) } } else if matches!( diff --git a/crates/oxide_emit/src/expr/identifier.rs b/crates/oxide_emit/src/expr/identifier.rs index cb93253f..cac60dae 100644 --- a/crates/oxide_emit/src/expr/identifier.rs +++ b/crates/oxide_emit/src/expr/identifier.rs @@ -153,14 +153,19 @@ impl Emitter { /// - cell 写穿共享单元,无运行时 guard;const 拦截由本入口编译期完成。 /// - 不可写全局内置(undefined/NaN/Infinity)的全局绑定写在此拦截:sloppy 跳过 /// 寄存器写(槽保留入口预载原值),strict 抛 TypeError;局部遮蔽绑定不受影响。 - pub(crate) fn emit_identifier_store(&self, name: &str, val_reg: u32, const_flag: u16, ctx: &mut CompileCtx) { + /// + /// # 副作用 + /// - live 模块顶层对导出源绑定的写入追加 `__moduleSet`,同步命名空间条目。 + pub(crate) fn emit_identifier_store( + &self, name: &str, val_reg: u32, const_flag: u16, ctx: &mut CompileCtx, + ) -> Result<(), String> { // 循环 update 段的 let/const 循环变量是 per-iteration 可变绑定(CreateMutableBinding), // 写寄存器而非 cell,且豁免 const 检查(register_update_names 覆盖全部循环头声明名)。 let in_loop_update = ctx.register_update_names.iter().any(|n| n == name); // const 再赋值编译期抛 TypeError:赋值路径(含闭包捕获 const 写 cell)统一拦截。 if const_flag != 0 && !in_loop_update { - let _ = self.emit_throw_error("TypeError", "Assignment to constant variable", ctx); - return; + let _ = self.emit_throw_error("TypeError", "Assignment to constant variable", ctx)?; + return Ok(()); } // 循环 update 段:被捕获绑定走寄存器而非 cell——C 风格 for 的 let/const // 循环变量每迭代新分配一个 cell,update 写寄存器供其拷入,不污染本迭代 @@ -172,14 +177,14 @@ impl Emitter { if ctx.is_strict { let _ = self.emit_throw_error("TypeError", "cannot assign to read-only property", ctx); } - return; + return Ok(()); } ctx.inst(Inst::new(OpCode::STORE_VAR, Operand::Reg(reg), Operand::Reg(val_reg), Operand::Imm(0))); // 可写内置名:值同步落全局对象属性,裸读(镜像)与反射不失步。 if ctx.targets_writable_builtin(name, reg) { self.emit_global_put_write(name, reg, ctx); } - return; + return Ok(()); } } // 目标若是 upvalue 引用,走 STORE_UPVALUE @@ -190,7 +195,8 @@ impl Emitter { Operand::Reg(val_reg), Operand::Imm(uv_idx as u16), )); - return; + self.emit_module_write_through(name, val_reg, ctx)?; + return Ok(()); } // 目标若是被捕获 cell,走 CELL_SET;仅当名字当前可解析为真实词法绑定时才写 // cell——捕获集按名保留的块级绑定在块退出后不可解析(隐式全局登记不算), @@ -203,7 +209,8 @@ impl Emitter { Operand::Reg(val_reg), Operand::Imm(cell_idx as u16), )); - return; + self.emit_module_write_through(name, val_reg, ctx)?; + return Ok(()); } } let var_reg = ctx.lookup_or_global(name); @@ -213,20 +220,20 @@ impl Emitter { if ctx.is_strict { let _ = self.emit_throw_error("TypeError", "cannot assign to read-only property", ctx); } - return; + 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 抛错,跳过寄存器写(值无关)。 - let _ = self.emit_strict_undeclared_write(name, ctx); - return; + self.emit_strict_undeclared_write(name, ctx)?; + return Ok(()); } if is_tier { // 顶层已声明 var 裸写:写入全局对象属性(顶层 var 的唯一存储,引擎侧不保留 // 镜像副本)。 self.emit_tier_global_write(name, val_reg, ctx); - return; + return Ok(()); } ctx.inst(Inst::new( OpCode::STORE_VAR, @@ -242,6 +249,9 @@ impl Emitter { // enumerable/configurable 位;否则内置名镜像槽与 globalThis 反射读到不同值。 self.emit_global_put_write(name, var_reg, ctx); } + // 模块顶层对导出源绑定的写入同步命名空间条目;非 live 模块空表直接返回。 + self.emit_module_write_through(name, val_reg, ctx)?; + Ok(()) } /// with 内动态写入:对象有该属性则写对象,否则回退静态写入。 @@ -250,7 +260,9 @@ impl Emitter { /// - 仅在 `with_stack` 非空且名字非 with 内部绑定时调用。 /// - 对象属性存在性判定与动态读取一致(`in` 含原型链)。 /// - 回退目标未在静态作用域声明时不登记全局(with 外不应可见)。 - pub(crate) fn emit_with_dynamic_write(&self, name: &str, val_reg: u32, const_flag: u16, ctx: &mut CompileCtx) { + pub(crate) fn emit_with_dynamic_write( + &self, name: &str, val_reg: u32, const_flag: u16, ctx: &mut CompileCtx, + ) -> Result<(), String> { let obj_reg = ctx.innermost_with_obj().expect("with stack non-empty"); let key_idx = ctx.add_constant(Constant::String(name.to_string())); let key_reg = ctx.alloc_reg(); @@ -274,9 +286,10 @@ impl Emitter { ctx.labels.set_label_pos(fallback_label, ctx.insts.len()); // 回退只写静态作用域已声明的绑定;未声明时丢弃值(隐式全局在 with 外不可解析)。 if ctx.scopes.symbols.lookup_any(name).is_some() { - self.emit_identifier_store(name, val_reg, const_flag, ctx); + self.emit_identifier_store(name, val_reg, const_flag, ctx)?; } ctx.labels.set_label_pos(end_label, ctx.insts.len()); + Ok(()) } pub(crate) fn emit_identifier_expression( diff --git a/crates/oxide_emit/src/module.rs b/crates/oxide_emit/src/module.rs index 4d76d107..2e3663c1 100644 --- a/crates/oxide_emit/src/module.rs +++ b/crates/oxide_emit/src/module.rs @@ -477,6 +477,21 @@ impl Emitter { } } + // —— 写穿反演:源绑定名 → 导出名集合。仅 live 模块(自导入 ns)填充: + // 非 live 模块不激活写穿,编译产物逐字节零 IR 变化。按名排序保证多导出名 + // 映射到同一源绑定时的发射顺序确定。—— + if has_self_ns_import { + let mut pairs: Vec<(&String, &String)> = + export_name_map.iter().map(|(exported, source)| (source, exported)).collect(); + pairs.sort(); + for (source, exported) in pairs { + ctx.module_local_exports + .entry(source.clone()) + .or_default() + .push(exported.clone()); + } + } + // —— live 命名空间预注册:早于 hoisted 函数声明的 __moduleSet 与 body 求值, // 按名序预注册全部本地导出名,使自导入 ns 在读点先观察到未初始化状态。 —— if has_self_ns_import { @@ -662,6 +677,30 @@ impl Emitter { Ok(()) } + /// 顶层对导出源绑定的写入同步到命名空间:按反演表把新值写入每个导出名。 + /// + /// # 边界与前提 + /// - 仅在模块顶层 live 模块激活:`module_local_exports` 只由自导入 ns 模块填充, + /// 嵌套函数 ctx 不继承该表,行为自然为零;`name` 非导出源时直接返回。 + /// - 调用方保证 `value_reg` 是写入后的当前值;const 抛错分支不得调用本函数。 + /// + /// # 副作用 + /// - 每个命中导出名发射一次 `__moduleSet` 调用,更新命名空间条目与真实槽值。 + pub(crate) fn emit_module_write_through( + &self, name: &str, value_reg: u32, ctx: &mut CompileCtx, + ) -> Result<(), String> { + let Some(ns_reg) = ctx.module_ns_reg else { + return Ok(()); + }; + let Some(export_names) = ctx.module_local_exports.get(name).cloned() else { + return Ok(()); + }; + for export_name in export_names { + self.emit_module_set(ctx, ns_reg, &export_name, value_reg)?; + } + Ok(()) + } + /// 别名捕获后处理:把源/别名对合并到同一 cell。 /// /// # 边界与前提 diff --git a/crates/oxide_emit/src/shared/binding.rs b/crates/oxide_emit/src/shared/binding.rs index 70dff001..0ce72e16 100644 --- a/crates/oxide_emit/src/shared/binding.rs +++ b/crates/oxide_emit/src/shared/binding.rs @@ -102,7 +102,7 @@ impl Emitter { } else if !ctx.with_stack.is_empty() && !ctx.is_with_internal_binding(name) { // with 内 var 初始化:对象有该属性则写对象,否则写提升槽(动态解析)。 // ponytail: with 内顶层 var 不写全局对象属性,with 语句本身已是稀见用例。 - self.emit_with_dynamic_write(name, src_reg, 0, ctx); + self.emit_with_dynamic_write(name, src_reg, 0, ctx)?; } else { // const 声明路径 STORE_VAR 恒 b=0,不查运行时 guard。 // guard 读 regs[rd] 判断槽是否已初始化,依赖槽初始为 undefined; diff --git a/crates/oxide_emit/tests/module_link_tests.rs b/crates/oxide_emit/tests/module_link_tests.rs index 929406c4..5f01e552 100644 --- a/crates/oxide_emit/tests/module_link_tests.rs +++ b/crates/oxide_emit/tests/module_link_tests.rs @@ -307,3 +307,22 @@ fn pre_register_for_self_namespace_import() { assert_eq!(object_calls.len(), 1, "__moduleObject 只应调用一次"); assert_eq!(object_calls[0].ext.first().copied(), Some(0), "__moduleObject 应保持零实参"); } + +/// 面 11:写穿发射门控——live 模块顶层重赋按导出名集合追加 `__moduleSet`; +/// 非 live 模块(无自导入 ns)重赋不得产生任何额外注册,IR 零变化。 +#[test] +fn write_through_only_for_live_modules() { + // 一个源绑定 a 背两个导出名 a/b:export 声明注册一次、再导出注册一次, + // a = 2 的写穿各写 a、b 一次,共 4 次 `__moduleSet`。 + let (ir, _) = emit_module( + "import * as ns from './self.js'; export let a = 1; export { a as b }; a = 2;", + "./self.js", + &[("./self.js", ModuleKind::Js, "export let a = 1;")], + ) + .expect("live 模块应编译成功"); + assert_eq!(native_calls_to(&ir, "__moduleSet"), 4, "live 模块重赋应写穿两个导出名"); + + // 非 live:重赋只写绑定槽,不得追加 `__moduleSet`。 + let (ir, _) = emit_module("export let a = 1; a = 2;", "./entry.js", &[]).expect("非 live 模块应编译成功"); + assert_eq!(native_calls_to(&ir, "__moduleSet"), 1, "非 live 模块重赋不得写穿"); +} diff --git a/crates/oxide_vm/src/vm_dispatch_misc.rs b/crates/oxide_vm/src/vm_dispatch_misc.rs index 799c9ae3..2072fe47 100644 --- a/crates/oxide_vm/src/vm_dispatch_misc.rs +++ b/crates/oxide_vm/src/vm_dispatch_misc.rs @@ -483,6 +483,27 @@ impl Vm { current = cur.proto(); } + // 模块命名空间 exotic:EnumerateObjectProperties 构建键表时逐键走 + // `? [[GetOwnProperty]]`,未初始化导出抛 ReferenceError。校验须在排序与 + // 迭代器建立之前(INIT 期),不遗留 for-in 迭代器;非 live ns 无条目表, + // `module_ns_export` 返回 None,落普通路径零行为变化。 + if obj_val.is_object() { + let ns_obj = unsafe { &*obj_val.as_js_object_ptr() }; + if ns_obj.is_module_namespace() { + for (_key_val, si) in &keys_vec { + if is_symbol_key(*si) { + continue; + } + if let Some(oxide_builtins::module::ModuleNsQuery::Uninitialized) = + oxide_builtins::module::module_ns_export(ns_obj, *si) + { + return self + .raise_error_kind("ReferenceError", oxide_builtins::module::NS_UNINITIALIZED_MESSAGE); + } + } + } + } + // ES 枚举序:整型下标键升序,其余按插入序。稳定排序保持非下标键间的插入序。 keys_vec.sort_by(|(_, a_si), (_, b_si)| { match (self.array_index_from_property_key(*a_si), self.array_index_from_property_key(*b_si)) { diff --git a/crates/oxide_vm/src/vm_props.rs b/crates/oxide_vm/src/vm_props.rs index af61fb3d..e5c1791f 100644 --- a/crates/oxide_vm/src/vm_props.rs +++ b/crates/oxide_vm/src/vm_props.rs @@ -56,7 +56,7 @@ impl Vm { return match state { oxide_builtins::module::ModuleNsQuery::Initialized(v) => Ok(v), oxide_builtins::module::ModuleNsQuery::Uninitialized => { - let msg = "Cannot access module export before initialization"; + let msg = oxide_builtins::module::NS_UNINITIALIZED_MESSAGE; if self.native_call_depth == 0 { self.raise_error_kind("ReferenceError", msg)?; } else { diff --git a/crates/oxide_vm/tests/module_namespace_tests.rs b/crates/oxide_vm/tests/module_namespace_tests.rs index 8c206ada..cbc55ee8 100644 --- a/crates/oxide_vm/tests/module_namespace_tests.rs +++ b/crates/oxide_vm/tests/module_namespace_tests.rs @@ -186,3 +186,85 @@ fn namespace_self_import_live_entries() { "live 自导入 ns 的未初始化读 / 键存在性 / 活值读不符" ); } + +/// 未初始化导出在枚举面与描述符面经 `? [[GetOwnProperty]]` 抛 ReferenceError; +/// `[[OwnPropertyKeys]]` 面(getOwnPropertyNames / Reflect.ownKeys)不读值故不抛。 +#[test] +fn namespace_uninitialized_enumeration_throws_reference_error() { + let cwd = std::env::current_dir().expect("cwd"); + let dir = cwd.join("__module_namespace_uninit_enum__"); + std::fs::create_dir_all(&dir).expect("mkdir"); + std::fs::write( + dir.join("main.mjs"), + "import * as ns from './main.mjs';\n \ + const threw = fn => { try { fn(); return false; } catch (e) { return e instanceof ReferenceError; } };\n \ + const names = Object.getOwnPropertyNames(ns).join(',');\n \ + const ownKeysCount = Reflect.ownKeys(ns).length;\n \ + globalThis.__ns = [\n \ + threw(() => Object.keys(ns)),\n \ + threw(() => Object.values(ns)),\n \ + threw(() => Object.entries(ns)),\n \ + threw(() => Object.assign({}, ns)),\n \ + threw(() => Object.prototype.hasOwnProperty.call(ns, 'default')),\n \ + threw(() => Object.getOwnPropertyDescriptor(ns, 'default')),\n \ + threw(() => Object.prototype.propertyIsEnumerable.call(ns, 'default')),\n \ + threw(() => { for (const k in ns) {} }),\n \ + names,\n \ + ownKeysCount,\n \ + ].join('|');\n \ + export default 0;", + ) + .expect("write main"); + let _cleanup = Cleanup(dir.clone()); + + let result = run_namespace_module(&dir); + assert_eq!( + result, "true|true|true|true|true|true|true|true|default|2", + "未初始化导出的枚举/描述符抛错或 ownKeys 反向守卫不符" + ); +} + +/// 顶层对导出源绑定的重赋写穿命名空间:一个源绑定背两个导出名时同值更新, +/// 复合赋值路径与简单赋值路径一致。 +#[test] +fn namespace_assignment_write_through_multiple_export_names() { + let cwd = std::env::current_dir().expect("cwd"); + let dir = cwd.join("__module_namespace_write_through__"); + std::fs::create_dir_all(&dir).expect("mkdir"); + std::fs::write( + dir.join("main.mjs"), + "import * as ns from './main.mjs';\n \ + export let a = 1;\n \ + export { a as b };\n \ + export let c = 1;\n \ + a = 2;\n \ + c += 2;\n \ + globalThis.__ns = [ns.a, ns.b, ns.c].join('|');", + ) + .expect("write main"); + let _cleanup = Cleanup(dir.clone()); + + let result = run_namespace_module(&dir); + assert_eq!(result, "2|2|3", "导出源绑定重赋未写穿命名空间活值"); +} + +/// G2 反向守卫:外部导入的命名空间无条目表(非 live),枚举与 for-in 行为不变。 +#[test] +fn external_namespace_enumeration_stays_unaffected() { + let cwd = std::env::current_dir().expect("cwd"); + let dir = cwd.join("__module_namespace_external__"); + std::fs::create_dir_all(&dir).expect("mkdir"); + std::fs::write(dir.join("dep.mjs"), "export const z = 1;").expect("write dep"); + std::fs::write( + dir.join("main.mjs"), + "import * as ns from './dep.mjs';\n \ + let n = 0;\n \ + for (const k in ns) n++;\n \ + globalThis.__ns = [Object.keys(ns).join(','), n, Object.values(ns).join(',')].join('|');", + ) + .expect("write main"); + let _cleanup = Cleanup(dir.clone()); + + let result = run_namespace_module(&dir); + assert_eq!(result, "z|1|1", "非 live 外部命名空间枚举行为不应变化"); +}