diff --git a/crates/oxide_builtins/src/module.rs b/crates/oxide_builtins/src/module.rs index f18e540a..92a4834e 100644 --- a/crates/oxide_builtins/src/module.rs +++ b/crates/oxide_builtins/src/module.rs @@ -7,7 +7,7 @@ use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_runtime_api::{NativeResult, VmHost}; -use oxide_types::object::{JsObject, PropAttributes}; +use oxide_types::object::{Cell, JsObject, PropAttributes}; use oxide_types::private_key::make_well_known_symbol_key; use oxide_types::value::JsValue; @@ -26,6 +26,326 @@ fn type_error(vm: &mut H, msg: &str) -> NativeResult { NativeResult::Err(crate::error::create_type_error(vm, msg)) } +/// 命名空间条目表的来源身份(provenance)。 +/// +/// `Local` 为本模块直接声明或本地再导出的导出;`Reexport` 预留跨模块 star 再导出 +/// 的来源身份,由 star 导出任务填入,避免另立 per-namespace 表形成两套判据。 +#[derive(Clone, Copy)] +pub enum ModuleNsOrigin { + Local, + Reexport { path_id: u32, binding_id: u32, direct: bool }, +} + +/// 单个导出名的活值状态。 +pub enum ModuleNsState { + /// 预注册后未初始化的一般导出;`initialized` 为 true 时 `value` 是当前活值。 + Value { value: JsValue, initialized: bool }, + /// 被捕获导出挂的共享 cell,活值由 cell 承载。 + Cell(*mut Cell), +} + +/// 命名空间条目:导出名 interned 键、活值状态与来源身份一一对应。 +pub struct ModuleNsEntry { + pub name_si: u32, + pub origin: ModuleNsOrigin, + pub state: ModuleNsState, +} + +/// 模块命名空间条目表:挂在 ns 对象 `native_data` 上,是 exotic [[Get]] 的权威 +/// 状态。键为 `PermInterner` interned 键,线性查找(导出名通常少于 50)。 +pub struct ModuleNsTable { + entries: Vec, +} + +/// 导出名状态查询结果。 +pub enum ModuleNsQuery { + /// 未初始化(预注册未赋值,或未初始化的共享 cell)。 + Uninitialized, + /// 已初始化活值(`Value` 条目或已初始化 cell)。 + Initialized(JsValue), +} + +/// 取 ns 对象的条目表指针;非 module namespace 或无表返回空指针。 +/// +/// # 注意事项 +/// - 返回指针由 ns 对象 `native_data` 持有,在该对象存活期内有效;消费方须在 +/// 同一 VM 世代内使用。 +fn ns_table_ptr(obj: &JsObject) -> *mut ModuleNsTable { + if !obj.is_module_namespace() { + return std::ptr::null_mut(); + } + obj.native_data() as *mut ModuleNsTable +} + +/// 在条目表中按 interned 键定位条目下标。 +fn entry_index(table: &ModuleNsTable, key_si: u32) -> Option { + table.entries.iter().position(|e| e.name_si == key_si) +} + +/// 分配空条目表并挂到 ns 对象的 `native_data`。 +fn install_ns_table(obj: &mut JsObject) -> *mut ModuleNsTable { + let table = Box::into_raw(Box::new(ModuleNsTable { entries: Vec::new() })); + obj.set_native_data(table as *mut u8); + table +} + +/// 追加一条未初始化的 `Value` 条目。 +/// +/// # 注意事项 +/// - 调用方保证 `name_si` 尚未入表(入口均为先查后插)。 +fn push_uninitialized_entry(table: *mut ModuleNsTable, name_si: u32) { + push_entry( + table, + name_si, + ModuleNsState::Value { + value: JsValue::undefined(), + initialized: false, + }, + ); +} + +/// 追加一条已初始化的 `Value` 条目。 +/// +/// # 注意事项 +/// - 调用方保证 `name_si` 尚未入表。 +fn push_initialized_entry(table: *mut ModuleNsTable, name_si: u32, value: JsValue) { + push_entry(table, name_si, ModuleNsState::Value { value, initialized: true }); +} + +/// 向条目表追加一条条目。 +/// +/// # 注意事项 +/// - table 归调用方持有的 ns 对象所有,在同一 VM 世代内有效。 +fn push_entry(table: *mut ModuleNsTable, name_si: u32, state: ModuleNsState) { + let entry = ModuleNsEntry { + name_si, + origin: ModuleNsOrigin::Local, + state, + }; + // SAFETY: table 归调用方持有的 ns 对象所有,在同一 VM 世代内有效。 + unsafe { + (*table).entries.push(entry); + } +} + +/// 模块命名空间字符串导出名的状态查询(不抛错、不分配、不写状态)。 +/// +/// # 边界与前提 +/// - 返回 `None`:非 module namespace / 无条目表 / 键非条目(含 symbol 键)。 +/// - 返回 `Uninitialized`:预注册未赋值,或条目挂在未初始化的共享 cell 上。 +/// - 返回 `Initialized(v)`:`Value` 已初始化,或 cell 已初始化。 +/// +/// # 注意事项 +/// - `Cell` 读依赖「cell 由 `alloc_cell` 分配并登记,至 `full_reset` 才统一释放」 +/// 的生命周期契约;同一 VM 世代内指针有效。 +pub fn module_ns_export(obj: &JsObject, key_si: u32) -> Option { + let table = ns_table_ptr(obj); + if table.is_null() { + return None; + } + // SAFETY: table 指向本 ns 对象持有的 Box,随对象存活。 + let table_ref = unsafe { &*table }; + let entry = table_ref.entries.get(entry_index(table_ref, key_si)?)?; + match &entry.state { + ModuleNsState::Value { value, initialized } => { + if *initialized { + Some(ModuleNsQuery::Initialized(*value)) + } else { + Some(ModuleNsQuery::Uninitialized) + } + } + ModuleNsState::Cell(cell) => { + if cell.is_null() { + return Some(ModuleNsQuery::Uninitialized); + } + // SAFETY: cell 由 alloc_cell 分配并登记,至 full_reset 才释放。 + let cell_ref = unsafe { &**cell }; + if cell_ref.is_initialized() { + Some(ModuleNsQuery::Initialized(cell_ref.value)) + } else { + Some(ModuleNsQuery::Uninitialized) + } + } + } +} + +/// 收集条目表持有的 JsValue 边(已初始化 `Value` 与全部 `Cell` 内值),供 GC mark。 +pub fn module_ns_native_edges(obj: &JsObject) -> Vec { + let table = ns_table_ptr(obj); + if table.is_null() { + return Vec::new(); + } + // SAFETY: table 归本 ns 对象持有,生命周期见 `module_ns_export`。 + let table_ref = unsafe { &*table }; + let mut edges = Vec::new(); + for entry in &table_ref.entries { + match &entry.state { + ModuleNsState::Value { value, initialized } => { + if *initialized { + edges.push(*value); + } + } + ModuleNsState::Cell(cell) => { + if !cell.is_null() { + // SAFETY: cell 生命周期见 `module_ns_export`。 + edges.push(unsafe { (**cell).value }); + } + } + } + } + edges +} + +/// 原地重写条目表内的 JsValue 边(对象搬移/epoch 晋升后的引用重定位)。 +/// +/// # 副作用 +/// - 改写 `Value` 条目的值与共享 cell 内的值;cell 指针本身稳定不变。 +pub fn rewrite_module_ns_native(obj: &mut JsObject, mut rewrite: F) +where + F: FnMut(JsValue) -> JsValue, +{ + let table = ns_table_ptr(obj); + if table.is_null() { + return; + } + // SAFETY: table 归本 ns 对象持有,原地改写其 JsValue 边。 + let table_ref = unsafe { &mut *table }; + for entry in table_ref.entries.iter_mut() { + match &mut entry.state { + ModuleNsState::Value { value, .. } => *value = rewrite(*value), + ModuleNsState::Cell(cell) => { + if !cell.is_null() { + // SAFETY: cell 生命周期见 `module_ns_export`;共享 cell 内值一并改写。 + let cell_ref = unsafe { &mut **cell }; + cell_ref.value = rewrite(cell_ref.value); + } + } + } + } +} + +/// 深拷贝条目表到 `dst`,并用 `rewrite` 改写其中的对象引用。 +/// +/// # 注意事项 +/// - `clone_for_session_epoch` 只浅拷贝 `native_data` 指针;本函数必须在新对象上 +/// 重建独立 Box,否则原件与克隆共享同一表会在释放时双放。 +/// - `Cell` 指针本身跨搬移稳定,只改写其内部值。 +pub fn clone_module_ns_native_with_rewrite(src: &JsObject, dst: &mut JsObject, mut rewrite: F) +where + F: FnMut(JsValue) -> JsValue, +{ + if !src.is_module_namespace() { + return; + } + let table = src.native_data() as *const ModuleNsTable; + if table.is_null() { + dst.set_native_data(std::ptr::null_mut()); + return; + } + // SAFETY: src.native_data 指向 src 持有的 Box。 + let table_ref = unsafe { &*table }; + let mut entries = Vec::with_capacity(table_ref.entries.len()); + for entry in &table_ref.entries { + let state = match &entry.state { + ModuleNsState::Value { value, initialized } => ModuleNsState::Value { + value: if value.is_object() { rewrite(*value) } else { *value }, + initialized: *initialized, + }, + ModuleNsState::Cell(cell) => { + if !cell.is_null() { + // SAFETY: cell 生命周期见 `module_ns_export`;晋升/搬移后改写其内值。 + let cell_ref = unsafe { &mut **cell }; + cell_ref.value = rewrite(cell_ref.value); + } + ModuleNsState::Cell(*cell) + } + }; + entries.push(ModuleNsEntry { + name_si: entry.name_si, + origin: entry.origin, + state, + }); + } + dst.set_native_data(Box::into_raw(Box::new(ModuleNsTable { entries })) as *mut u8); +} + +/// 只读核算条目表的 native 数据字节(不释放),与 `drop_module_ns_native` 的 +/// capacity 口径一致,供 GC 账目核算。 +pub fn module_ns_native_size(obj: &JsObject) -> u64 { + let table = ns_table_ptr(obj); + if table.is_null() { + return 0; + } + // SAFETY: table 归本 ns 对象持有,生命周期见 `module_ns_export`。 + let table_ref = unsafe { &*table }; + (std::mem::size_of::() + table_ref.entries.capacity() * std::mem::size_of::()) as u64 +} + +/// 释放条目表(`Box`),返回释放字节数供泄漏统计。 +/// +/// # 注意事项 +/// - 每对象的表至多释放一次;释放后 `native_data` 置空保证幂等。 +pub fn drop_module_ns_native(obj: &mut JsObject) -> u64 { + let table = ns_table_ptr(obj); + if table.is_null() { + return 0; + } + let bytes = module_ns_native_size(obj); + // SAFETY: table 由 install_ns_table 的 Box::into_raw 分配,本处恰好释放一次。 + unsafe { + drop(Box::from_raw(table)); + } + obj.set_native_data(std::ptr::null_mut()); + bytes +} + +/// `__modulePreRegister(ns, name)`:live 模块在 body 求值前预注册静态导出名。 +/// +/// # 步骤 +/// 1. 校验 ns 为 module namespace 对象,取导出名的 interned 键。 +/// 2. 无表时分配;键已有条目时 no-op(幂等)。 +/// 3. 经瞬态可扩展窗口在 ns 上定义真实数据属性 `undefined`(可写可枚举不可配置)。 +/// 4. 追加未初始化 `Value` 条目并按规范顺序重排导出键。 +/// +/// # 副作用 +/// - 在 ns 上定义真实属性槽、挂载/扩展条目表,并重排 `[[OwnPropertyKeys]]` 顺序。 +pub fn module_pre_register(vm: &mut H, args: &[u8]) -> NativeResult { + if args.len() < 3 { + return type_error(vm, "__modulePreRegister: 2 arguments required"); + } + let ns_val = vm.reg(args[1]); + let ns_ptr = match vm.checked_object_ptr(ns_val, "__modulePreRegister: target is not an object") { + Ok(Some(p)) => p, + Ok(None) => return type_error(vm, "__modulePreRegister: target is not an object"), + Err(e) => return NativeResult::Err(crate::error::create_error(vm, &e)), + }; + let name_val = vm.reg(args[2]); + let name_si = vm.property_key_si(name_val); + let obj = unsafe { &mut *ns_ptr }; + if !obj.is_module_namespace() { + return type_error(vm, "__modulePreRegister: target is not a module namespace"); + } + + // 幂等:同名条目已存在时不再定义真实槽或重复入表。 + let existing = ns_table_ptr(obj); + if !existing.is_null() && unsafe { entry_index(&*existing, name_si) }.is_some() { + return NativeResult::Ok(JsValue::undefined()); + } + + // 导出定义窗口:同 module_set,窗口内不运行 JS,错误路径先复位扩展性。 + obj.set_extensible(true); + let result = vm.define_data_property(obj, name_si, JsValue::undefined(), ns_attrs()); + obj.set_extensible(false); + if let Err(e) = result { + return NativeResult::Err(crate::error::create_error(vm, &e)); + } + + let table = if existing.is_null() { install_ns_table(obj) } else { existing }; + push_uninitialized_entry(table, name_si); + crate::object::sort_namespace_exports(vm, obj); + NativeResult::Ok(JsValue::undefined()) +} + /// `__moduleObject()`:创建模块命名空间对象(null 原型,带 @@toStringTag Symbol 键)。 /// /// # 副作用 @@ -44,7 +364,16 @@ pub fn module_object(vm: &mut H, _args: &[u8]) -> NativeResult { NativeResult::Ok(JsValue::from_js_object(obj)) } -/// `__moduleSet(ns, name, value)`:在命名空间上定义导出属性。 +/// `__moduleSet(ns, name, value)`:在命名空间上定义或更新导出属性。 +/// +/// # 步骤 +/// 1. live ns 命中既有条目:`Cell` 条目 no-op(活值由 cell 承载);`Value` 条目 +/// 更新活值并同步真实属性槽,键集不变故跳过重排。 +/// 2. 其余(无表 / 非条目 / 新键):经瞬态可扩展窗口定义或改值真实属性槽; +/// live ns 追加已初始化条目,最后按规范顺序重排导出键。 +/// +/// # 副作用 +/// - 修改条目表状态与 ns 属性槽;新键会重排 `[[OwnPropertyKeys]]` 顺序。 pub fn module_set(vm: &mut H, args: &[u8]) -> NativeResult { if args.len() < 4 { return type_error(vm, "__moduleSet: 3 arguments required"); @@ -59,6 +388,28 @@ pub fn module_set(vm: &mut H, args: &[u8]) -> NativeResult { let value = vm.reg(args[3]); let name_si = vm.property_key_si(name_val); let obj = unsafe { &mut *ns_ptr }; + + // live 条目路径:命中条目则更新活值并同步真实槽,键集不变无需重排。 + let table = ns_table_ptr(obj); + if !table.is_null() { + // SAFETY: table 归本 ns 对象持有,生命周期见 `module_ns_export`。 + if let Some(idx) = unsafe { entry_index(&*table, name_si) } { + // SAFETY: idx 来自刚完成的表内查找,仍在界内。 + let entry_state = unsafe { &mut (&mut (*table).entries)[idx].state }; + match entry_state { + ModuleNsState::Cell(_) => return NativeResult::Ok(JsValue::undefined()), + ModuleNsState::Value { value: slot, initialized } => { + *slot = value; + *initialized = true; + if let Some(pos) = vm.get_own_property_slot(obj, name_si) { + obj.set_prop_at(pos, value); + } + return NativeResult::Ok(JsValue::undefined()); + } + } + } + } + // 导出定义窗口:命名空间创建起不可扩展,定义新导出须瞬态放开扩展性。窗口内 // 不运行 JS,无可观察副作用;错误路径先复位以维持 non-extensible 不变式。 obj.set_extensible(true); @@ -67,6 +418,9 @@ pub fn module_set(vm: &mut H, args: &[u8]) -> NativeResult { if let Err(e) = result { return NativeResult::Err(crate::error::create_error(vm, &e)); } + if !table.is_null() { + push_initialized_entry(table, name_si, value); + } // 每次导出写入后重排:自导入在 body 内即可见有序的 [[OwnPropertyKeys]]。 crate::object::sort_namespace_exports(vm, obj); NativeResult::Ok(JsValue::undefined()) @@ -142,6 +496,8 @@ pub fn module_star(vm: &mut H, args: &[u8]) -> NativeResult { let default_si = vm.kernel_core().perm_interner().intern("default").0; let keys = crate::object::walk_own_keys(vm, src); let dst = unsafe { &mut *dst_ptr }; + // live dst:star 复制的新导出名须一并入表,否则 2b 的活值读/枚举看不到条目。 + let dst_table = ns_table_ptr(dst); for (name_si, pos) in keys { if name_si == default_si { continue; @@ -159,6 +515,10 @@ pub fn module_star(vm: &mut H, args: &[u8]) -> NativeResult { if let Err(e) = result { return NativeResult::Err(crate::error::create_error(vm, &e)); } + if !dst_table.is_null() { + // 2a 保持快照语义:star 名立即视为已初始化。 + push_initialized_entry(dst_table, name_si, value); + } } // star 复制后重排:直接导出与 star 导出的合并键序保持规范顺序。 crate::object::sort_namespace_exports(vm, dst); diff --git a/crates/oxide_emit/src/module.rs b/crates/oxide_emit/src/module.rs index d7178207..4d76d107 100644 --- a/crates/oxide_emit/src/module.rs +++ b/crates/oxide_emit/src/module.rs @@ -371,6 +371,9 @@ impl Emitter { // 导出名 → 本模块源绑定名:self-import 的局部名别名到该源绑定。star 转发的 // 自导入名不在表内(退化为下方非别名占位路径)。 let export_name_map = module_export_name_map(body, &ctx.module_self_import_specs); + // live 命名空间门控:仅自导入 `import * as ns from './self'` 的模块需要预注册 + // 真实导出槽,其余模块(含普通外部命名空间导入)编译产物逐字节不变。 + let mut has_self_ns_import = false; for stmt in body { if let Statement::ImportDeclaration(imp) = stmt { let dep_spec = imp.source.value.to_string(); @@ -456,6 +459,9 @@ impl Emitter { ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => { // 命名空间绑定引用 ns 对象本身:自导入时同一对象, // body 执行后导出自然可见。 + if is_self { + has_self_ns_import = true; + } self.emit_bind_target( s.local.name.as_str(), dep_ns_reg, @@ -471,6 +477,17 @@ impl Emitter { } } + // —— live 命名空间预注册:早于 hoisted 函数声明的 __moduleSet 与 body 求值, + // 按名序预注册全部本地导出名,使自导入 ns 在读点先观察到未初始化状态。 —— + if has_self_ns_import { + let mut export_names: Vec = own_export_names.into_iter().collect(); + export_names.sort(); + for name in export_names { + let name_reg = self.load_string_const(&name, ctx); + self.emit_module_call(ctx, "__modulePreRegister", &[ns_reg, name_reg])?; + } + } + // —— 别名捕获后处理:源绑定被嵌套函数捕获时,别名必须落到同一 cell, // 否则两处各持一份值。无捕获时别名读经源寄存器天然活。此后源被捕获的 // var 名在模块入口统一 MAKE_CELL(undefined),与脚本顶层实例化同语义。—— diff --git a/crates/oxide_emit/tests/module_link_tests.rs b/crates/oxide_emit/tests/module_link_tests.rs index 45267d08..929406c4 100644 --- a/crates/oxide_emit/tests/module_link_tests.rs +++ b/crates/oxide_emit/tests/module_link_tests.rs @@ -264,3 +264,46 @@ fn self_import_default_alias_has_no_placeholder_writeback() { // 不出现旧占位路径的额外回写。 assert_eq!(store_var_count(&ir), 1, "不应出现占位回写带来的第二条 STORE_VAR"); } + +/// 面 10:零 diff 守卫 G1——非 live 模块不发射 `__modulePreRegister`。 +/// live 门控为自导入 `import * as ns from './self'`;纯导出、default 表达式与 +/// 普通外部命名空间导入均不得出现预注册调用。 +#[test] +fn no_pre_register_for_non_live_modules() { + let (ir, _) = emit_module("export let x = 1;", "./entry.js", &[]).expect("导出声明应编译成功"); + assert_eq!(native_calls_to(&ir, "__modulePreRegister"), 0, "纯导出模块不得预注册"); + + let (ir, _) = emit_module("export default 42;", "./entry.js", &[]).expect("default 应编译成功"); + assert_eq!(native_calls_to(&ir, "__modulePreRegister"), 0, "default 表达式模块不得预注册"); + + let (ir, _) = emit_module( + "import * as ns from './dep.js'; ns;", + "./entry.js", + &[("./dep.js", ModuleKind::Js, "export var z = 1;")], + ) + .expect("外部命名空间导入应编译成功"); + assert_eq!(native_calls_to(&ir, "__modulePreRegister"), 0, "外部 ns 导入不得预注册"); +} + +/// 面 10b:live 模块按静态导出名逐个预注册,`__moduleObject` 仍为零参调用。 +#[test] +fn pre_register_for_self_namespace_import() { + let (ir, _) = emit_module( + "import * as ns from './self.js'; export const a = 1; export default 2;", + "./self.js", + &[("./self.js", ModuleKind::Js, "export const a = 1;")], + ) + .expect("self-ns 模块应编译成功"); + assert_eq!(native_calls_to(&ir, "__modulePreRegister"), 2, "a 与 default 各预注册一次"); + assert!(pool_strings(&ir).contains("a") && pool_strings(&ir).contains("default"), "导出名应入池"); + + // __moduleObject 保持零实参签名:仅一次调用且压缩实参计数为 0。 + let reg = builtin_reg(&ir, "__moduleObject").expect("应链接 __moduleObject"); + let object_calls: Vec<_> = ir + .insts + .iter() + .filter(|i| i.op == OpCode::CALL_NATIVE && matches!(i.rd, oxide_ir::operand::Operand::Reg(r) if r == reg)) + .collect(); + assert_eq!(object_calls.len(), 1, "__moduleObject 只应调用一次"); + assert_eq!(object_calls[0].ext.first().copied(), Some(0), "__moduleObject 应保持零实参"); +} diff --git a/crates/oxide_vm/src/bindings/bind_global.rs b/crates/oxide_vm/src/bindings/bind_global.rs index 39597388..c1f49358 100644 --- a/crates/oxide_vm/src/bindings/bind_global.rs +++ b/crates/oxide_vm/src/bindings/bind_global.rs @@ -28,6 +28,11 @@ pub fn bind_global(core: &Arc, session: &KernelSession, global: &mut ), // 模块求值 / 命名空间内部辅助(import 实现;非 JS 标准全局,以下划线开头避免撞名)。 ("__moduleObject", oxide_builtins::module::module_object:: as *const (), 0), + ( + "__modulePreRegister", + oxide_builtins::module::module_pre_register:: as *const (), + 2, + ), ("__moduleSet", oxide_builtins::module::module_set:: as *const (), 3), ("__moduleGet", oxide_builtins::module::module_get:: as *const (), 2), ( diff --git a/crates/oxide_vm/src/session_arena.rs b/crates/oxide_vm/src/session_arena.rs index be29524f..1b925a6e 100644 --- a/crates/oxide_vm/src/session_arena.rs +++ b/crates/oxide_vm/src/session_arena.rs @@ -6,7 +6,7 @@ use oxide_types::value::JsValue; use rustc_hash::FxBuildHasher; use crate::vm::Vm; -use oxide_builtins::{array_buffer, data_view, disposable_stack, map, regexp, set, typed_array}; +use oxide_builtins::{array_buffer, data_view, disposable_stack, map, module, regexp, set, typed_array}; impl Vm { pub(crate) fn is_session_escape_root_ptr(&self, target_ptr: *mut JsObject) -> bool { @@ -99,6 +99,12 @@ impl Vm { } else if src_ref.holds_compiled_regex() { // 已编译正则是 Box 深拷贝到新对象:源盒随 epoch 释放,互不共享。 regexp::clone_regexp_native(src_ref, dst_ref); + } else if src_ref.is_module_namespace() { + // 条目表深拷贝到新对象并改写值边:`clone_for_session_epoch` 只浅拷贝 + // native_data 指针,共享同一表会在释放时双放。 + module::clone_module_ns_native_with_rewrite(src_ref, dst_ref, |value| { + self.promote_value_if_epoch_object(value, forwarding) + }); } // 账目计入:对象头 + 堆数据(属性/元素/meta Vec + native 状态盒),用 clone 后的 dst 核算。 self.gc_state.session_bytes_allocated += @@ -205,6 +211,10 @@ impl Vm { crate::async_generator::rewrite_async_generator_native(obj, |value| { self.promote_value_if_epoch_object(value, forwarding) }); + } else if obj.is_module_namespace() { + module::rewrite_module_ns_native(obj, |value| { + self.promote_value_if_epoch_object(value, forwarding) + }); } } } diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index f439be25..372687a3 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -8,7 +8,7 @@ use oxide_types::value::JsValue; use rustc_hash::FxBuildHasher; use crate::vm::Vm; -use oxide_builtins::{array_buffer, data_view, disposable_stack, map, regexp, set, typed_array}; +use oxide_builtins::{array_buffer, data_view, disposable_stack, map, module, regexp, set, typed_array}; /// session 级 mark-sweep GC 的状态与统计。 /// @@ -113,6 +113,7 @@ impl SessionGc { bytes += map::map_native_size(obj); bytes += set::set_native_size(obj); + bytes += module::module_ns_native_size(obj); bytes += disposable_stack::disposable_stack_native_size(obj); bytes += array_buffer::array_buffer_native_size(obj); bytes += regexp::regexp_native_size(obj); @@ -186,6 +187,11 @@ impl SessionGc { Self::process_edge(value, vm, stack, live_strings, live_bigints); } } + if obj.is_module_namespace() { + for value in module::module_ns_native_edges(obj) { + Self::process_edge(value, vm, stack, live_strings, live_bigints); + } + } if obj.is_generator_obj() { for value in crate::generator::generator_native_edges(obj) { Self::process_edge(value, vm, stack, live_strings, live_bigints); @@ -347,6 +353,13 @@ impl SessionGc { } } } + if obj.is_module_namespace() { + for value in module::module_ns_native_edges(obj) { + if value.is_string() { + Self::mark_string_live(live, value.as_string_ptr_mut()); + } + } + } if obj.is_disposable_stack_obj() || obj.is_async_disposable_stack_obj() { for value in disposable_stack::dispose_edges(obj) { if value.is_string() { @@ -503,6 +516,7 @@ impl SessionGc { freed_bytes += map::drop_map_native(obj); freed_bytes += set::drop_set_native(obj); + freed_bytes += module::drop_module_ns_native(obj); freed_bytes += disposable_stack::drop_dispose_native(obj); freed_bytes += array_buffer::drop_array_buffer_native(obj); freed_bytes += regexp::drop_regexp_native(obj); @@ -608,6 +622,9 @@ impl SessionGc { } else if old_ref.holds_compiled_regex() { // 已编译正则是 Box 深拷贝到新对象:源 Box 由 drop 释放,互不共享。 regexp::clone_regexp_native(old_ref, new_ref); + } else if old_ref.is_module_namespace() { + // 条目表深拷贝:`clone_for_session_epoch` 只浅拷贝 native_data 指针。 + module::clone_module_ns_native_with_rewrite(old_ref, new_ref, |value| value); } forwarding.insert(old_ptr, new_ptr); freed_bytes += Self::drop_session_object_heap_data(old_ptr); @@ -654,6 +671,8 @@ impl SessionGc { crate::async_generator::rewrite_async_generator_native(obj, |value| { rewrite_forwarded_value(value, &forwarding) }); + } else if obj.is_module_namespace() { + module::rewrite_module_ns_native(obj, |value| rewrite_forwarded_value(value, &forwarding)); } } diff --git a/crates/oxide_vm/src/vm_props.rs b/crates/oxide_vm/src/vm_props.rs index fd4858c3..af61fb3d 100644 --- a/crates/oxide_vm/src/vm_props.rs +++ b/crates/oxide_vm/src/vm_props.rs @@ -49,6 +49,24 @@ impl Vm { let mut current = Some(obj); let mut depth = 0usize; while let Some(obj) = current { + // 模块命名空间 exotic [[Get]]:条目表是权威状态,预注册但未初始化的 + // 导出读抛 ReferenceError;非 live ns(无表)落普通属性路径零行为变化。 + if obj.is_module_namespace() { + if let Some(state) = oxide_builtins::module::module_ns_export(obj, prop_name_si) { + return match state { + oxide_builtins::module::ModuleNsQuery::Initialized(v) => Ok(v), + oxide_builtins::module::ModuleNsQuery::Uninitialized => { + let msg = "Cannot access module export before initialization"; + if self.native_call_depth == 0 { + self.raise_error_kind("ReferenceError", msg)?; + } else { + return Err(self.error_message_text("ReferenceError", msg)); + } + Ok(JsValue::undefined()) + } + }; + } + } if obj.is_array() && prop_name_si == length_si { return Ok(obj.logical_len_value()); } diff --git a/crates/oxide_vm/tests/module_namespace_tests.rs b/crates/oxide_vm/tests/module_namespace_tests.rs index ec1bd894..8c206ada 100644 --- a/crates/oxide_vm/tests/module_namespace_tests.rs +++ b/crates/oxide_vm/tests/module_namespace_tests.rs @@ -143,3 +143,46 @@ fn namespace_exotic_semantics() { "命名空间 exotic 语义不符" ); } + +/// live 自导入 ns 的条目语义:预注册后初始化前读抛 ReferenceError,`in` / +/// `Reflect.has` 已为 true;初始化后读到活值;枚举未初始化名不抛错且键数正确。 +/// 循环读 `ns.y` 同时钉住 IC 慢路径与普通路径读结果一致。 +#[test] +fn namespace_self_import_live_entries() { + let cwd = std::env::current_dir().expect("cwd"); + let dir = cwd.join("__module_namespace_live__"); + std::fs::create_dir_all(&dir).expect("mkdir"); + std::fs::write( + dir.join("main.mjs"), + "import * as ns from './main.mjs';\n \ + const init = [];\n \ + for (const k of ['x', 'y', 'default']) {\n \ + try { ns[k]; init.push('value'); } catch (e) { init.push(e instanceof ReferenceError); }\n \ + }\n \ + const hasBefore = ['x', 'y', 'default'].map(k => k in ns).join(',');\n \ + const namesBefore = Object.getOwnPropertyNames(ns).join(',');\n \ + const ownKeysCount = Reflect.ownKeys(ns).length;\n \ + export let x = 'sx';\n \ + export var y = 2;\n \ + export default 3;\n \ + let sum = 0;\n \ + for (let i = 0; i < 128; i++) { sum += ns.y; }\n \ + globalThis.__ns = [\n \ + init.join('|'),\n \ + hasBefore,\n \ + namesBefore,\n \ + ownKeysCount,\n \ + ns.x,\n \ + sum,\n \ + ns.default,\n \ + ].join(';');", + ) + .expect("write main"); + let _cleanup = Cleanup(dir.clone()); + + let result = run_namespace_module(&dir); + assert_eq!( + result, "true|true|true;true,true,true;default,x,y;4;sx;256;3", + "live 自导入 ns 的未初始化读 / 键存在性 / 活值读不符" + ); +}