diff --git a/crates/oxide_builtins/src/module.rs b/crates/oxide_builtins/src/module.rs index 03588103..4cd4c870 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::{Cell, JsObject, PropAttributes}; +use oxide_types::object::{Cell, JsObject, PropAttributes, PropMetaEntry}; use oxide_types::private_key::make_well_known_symbol_key; use oxide_types::value::JsValue; @@ -34,12 +34,14 @@ fn type_error(vm: &mut H, msg: &str) -> NativeResult { /// 命名空间条目表的来源身份(provenance)。 /// -/// `Local` 为本模块直接声明或本地再导出的导出;`Reexport` 预留跨模块 star 再导出 -/// 的来源身份,由 star 导出任务填入,避免另立 per-namespace 表形成两套判据。 +/// `Local` 为本模块直接声明的导出;`Reexport` 描述跨模块再导出的 `(模块, 绑定)` +/// 身份,`direct` 区分显式再导出(`export { x } from`)与 star 复制的间接导出; +/// `Ambiguous` 是 star 冲突后的省略哨兵,该名不再出现在命名空间且一旦歧义恒歧义。 #[derive(Clone, Copy)] pub enum ModuleNsOrigin { Local, Reexport { path_id: u32, binding_id: u32, direct: bool }, + Ambiguous, } /// 单个导出名的活值状态。 @@ -95,7 +97,7 @@ fn install_ns_table(obj: &mut JsObject) -> *mut ModuleNsTable { table } -/// 追加一条未初始化的 `Value` 条目。 +/// 追加一条未初始化的 `Value` 条目(来源为本地导出)。 /// /// # 注意事项 /// - 调用方保证 `name_si` 尚未入表(入口均为先查后插)。 @@ -103,6 +105,7 @@ fn push_uninitialized_entry(table: *mut ModuleNsTable, name_si: u32) { push_entry( table, name_si, + ModuleNsOrigin::Local, ModuleNsState::Value { value: JsValue::undefined(), initialized: false, @@ -110,24 +113,12 @@ fn push_uninitialized_entry(table: *mut ModuleNsTable, name_si: u32) { ); } -/// 追加一条已初始化的 `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, - }; +fn push_entry(table: *mut ModuleNsTable, name_si: u32, origin: ModuleNsOrigin, state: ModuleNsState) { + let entry = ModuleNsEntry { name_si, origin, state }; // SAFETY: table 归调用方持有的 ns 对象所有,在同一 VM 世代内有效。 unsafe { (*table).entries.push(entry); @@ -152,6 +143,10 @@ pub fn module_ns_export(obj: &JsObject, key_si: u32) -> Option { // SAFETY: table 指向本 ns 对象持有的 Box,随对象存活。 let table_ref = unsafe { &*table }; let entry = table_ref.entries.get(entry_index(table_ref, key_si)?)?; + // 歧义哨兵条目:该名已从命名空间省略,查询一律视为非导出(落普通路径)。 + if matches!(entry.origin, ModuleNsOrigin::Ambiguous) { + return None; + } match &entry.state { ModuleNsState::Value { value, initialized } => { if *initialized { @@ -370,16 +365,109 @@ pub fn module_object(vm: &mut H, _args: &[u8]) -> NativeResult { NativeResult::Ok(JsValue::from_js_object(obj)) } -/// `__moduleSet(ns, name, value)`:在命名空间上定义或更新导出属性。 +/// 写入命名空间导出:命中条目则更新活值、真实属性槽与来源身份;否则经瞬态可扩展 +/// 窗口定义真实属性槽,并在 ns 有条目表时追加条目。 /// -/// # 步骤 -/// 1. live ns 命中既有条目:`Cell` 条目 no-op(活值由 cell 承载);`Value` 条目 -/// 更新活值并同步真实属性槽,键集不变故跳过重排。 -/// 2. 其余(无表 / 非条目 / 新键):经瞬态可扩展窗口定义或改值真实属性槽; -/// live ns 追加已初始化条目,最后按规范顺序重排导出键。 +/// # 边界与前提 +/// - `Cell` 条目 no-op:被捕获导出的活值由共享 cell 承载,直接写槽会与 cell 分裂。 +/// - 条目存在但真实槽缺失(歧义省略后显式导出重新定义):落新键定义路径补回槽。 /// /// # 副作用 -/// - 修改条目表状态与 ns 属性槽;新键会重排 `[[OwnPropertyKeys]]` 顺序。 +/// - 修改条目表状态/来源身份与 ns 属性槽;新键或删槽重定义时重排 +/// `[[OwnPropertyKeys]]` 顺序。 +fn set_export( + vm: &mut H, obj: &mut JsObject, name_si: u32, value: JsValue, origin: ModuleNsOrigin, +) -> Result<(), String> { + let table = ns_table_ptr(obj); + let existing = if table.is_null() { + None + } else { + // SAFETY: table 归本 ns 对象持有,生命周期见 `module_ns_export`。 + unsafe { entry_index(&*table, name_si) } + }; + + // Cell 条目 no-op:被捕获导出的活值由共享 cell 承载,写槽会与 cell 分裂。 + if let Some(idx) = existing { + // SAFETY: idx 来自刚完成的表内查找,仍在界内。 + if matches!(unsafe { &(&(*table).entries)[idx].state }, ModuleNsState::Cell(_)) { + return Ok(()); + } + } + + // 条目已存在且真实槽仍在:原地更新活值/来源,键集不变无需重排。 + if let Some(idx) = existing { + if let Some(pos) = vm.get_own_property_slot(obj, name_si) { + obj.set_prop_at(pos, value); + // SAFETY: idx 来自刚完成的表内查找,仍在界内。 + unsafe { + (&mut (*table).entries)[idx].origin = origin; + (&mut (*table).entries)[idx].state = ModuleNsState::Value { value, initialized: true }; + } + return Ok(()); + } + // 歧义省略删槽后显式导出重新定义:落下方定义路径补回真实槽。 + } + + // 导出定义窗口:命名空间创建起不可扩展,定义新导出须瞬态放开扩展性。窗口内 + // 不运行 JS,无可观察副作用;错误路径先复位以维持 non-extensible 不变式。 + obj.set_extensible(true); + let result = vm.define_data_property(obj, name_si, value, ns_attrs()); + obj.set_extensible(false); + result?; + + if !table.is_null() { + if let Some(idx) = existing { + // SAFETY: idx 来自刚完成的表内查找;重定义后同步条目,勿重复入表。 + unsafe { + (&mut (*table).entries)[idx].origin = origin; + (&mut (*table).entries)[idx].state = ModuleNsState::Value { value, initialized: true }; + } + } else { + push_entry(table, name_si, origin, ModuleNsState::Value { value, initialized: true }); + } + } + // 每次键集变化后重排:自导入在 body 内即可见有序的 [[OwnPropertyKeys]]。 + crate::object::sort_namespace_exports(vm, obj); + Ok(()) +} + +/// 从命名空间属性表移除一个导出槽(歧义省略用),保留其余字符串与 Symbol 键及其 +/// 描述符。 +/// +/// # 注意事项 +/// - 命名空间导出属性为 non-configurable,本函数不经 `[[Delete]]`,直接重建属性表 +/// 以同时保住 `@@toStringTag` Symbol 键。 +fn remove_export_slot(vm: &mut H, obj: &mut JsObject, name_si: u32) { + let mut retained: Vec<(u32, JsValue, Option)> = crate::object::walk_own_keys(vm, obj) + .into_iter() + .filter(|(si, _)| *si != name_si) + .map(|(si, pos)| (si, obj.get_prop_at(pos), obj.prop_meta_at(pos))) + .collect(); + let symbols: Vec<(u32, JsValue, Option)> = crate::object::walk_own_symbol_keys(vm, obj) + .into_iter() + .map(|(si, pos)| (si, obj.get_prop_at(pos), obj.prop_meta_at(pos))) + .collect(); + obj.set_shape_id(EMPTY_SHAPE_ID); + obj.clear_props(); + for (si, value, meta) in retained.drain(..).chain(symbols) { + let shape = vm.kernel_core().shape_forge().make_shape(obj.shape_id(), si); + obj.set_shape_id(shape); + let pos = obj.push_prop(value); + if let Some(meta) = meta { + if meta.is_accessor { + obj.set_accessor_meta(pos, meta.get, meta.set, meta.attributes); + } else { + obj.set_data_meta(pos, meta.attributes); + } + } + } + obj.bump_generation(); +} + +/// `__moduleSet(ns, name, value)`:在命名空间上定义或更新本地导出属性。 +/// +/// # 副作用 +/// - 同 `set_export`;本地导出来源身份恒为 `Local`,覆盖先前 star 条目时复位来源。 pub fn module_set(vm: &mut H, args: &[u8]) -> NativeResult { if args.len() < 4 { return type_error(vm, "__moduleSet: 3 arguments required"); @@ -394,41 +482,56 @@ 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); - let result = vm.define_data_property(obj, name_si, value, ns_attrs()); - obj.set_extensible(false); - if let Err(e) = result { + if let Err(e) = set_export(vm, obj, name_si, value, ModuleNsOrigin::Local) { return NativeResult::Err(crate::error::create_error(vm, &e)); } - if !table.is_null() { - push_initialized_entry(table, name_si, value); + NativeResult::Ok(JsValue::undefined()) +} + +/// `__moduleSetReexport(ns, name, value, dep_path, imported)`:注册显式再导出 +/// (`export { x } from` / `export * as ns from` / 导入绑定再导出),记录跨模块 +/// 来源身份 `(dep_path, imported)`,使同一绑定经不同路径转发时来源 token 相等。 +/// +/// # 副作用 +/// - 同 `module_set`;ns 无条目表时惰性安装,来源标为 `direct` 再导出。 +pub fn module_set_reexport(vm: &mut H, args: &[u8]) -> NativeResult { + if args.len() < 6 { + return type_error(vm, "__moduleSetReexport: 5 arguments required"); + } + let ns_val = vm.reg(args[1]); + let ns_ptr = match vm.checked_object_ptr(ns_val, "__moduleSetReexport: target is not an object") { + Ok(Some(p)) => p, + Ok(None) => return type_error(vm, "__moduleSetReexport: target is not an object"), + Err(e) => return NativeResult::Err(crate::error::create_error(vm, &e)), + }; + let name_val = vm.reg(args[2]); + let value = vm.reg(args[3]); + let path_val = vm.reg(args[4]); + let imported_val = vm.reg(args[5]); + let name_si = vm.property_key_si(name_val); + let Some(dep_path) = vm.lookup_str(path_val) else { + return type_error(vm, "__moduleSetReexport: module path is not a string"); + }; + let Some(imported) = vm.lookup_str(imported_val) else { + return type_error(vm, "__moduleSetReexport: imported name is not a string"); + }; + let obj = unsafe { &mut *ns_ptr }; + if !obj.is_module_namespace() { + return type_error(vm, "__moduleSetReexport: target is not a module namespace"); + } + if ns_table_ptr(obj).is_null() { + install_ns_table(obj); + } + let path_id = vm.kernel_core().perm_interner().intern(&dep_path).0; + let binding_id = vm.kernel_core().perm_interner().intern(&imported).0; + let origin = ModuleNsOrigin::Reexport { + path_id, + binding_id, + direct: true, + }; + if let Err(e) = set_export(vm, obj, name_si, value, origin) { + return NativeResult::Err(crate::error::create_error(vm, &e)); } - // 每次导出写入后重排:自导入在 body 内即可见有序的 [[OwnPropertyKeys]]。 - crate::object::sort_namespace_exports(vm, obj); NativeResult::Ok(JsValue::undefined()) } @@ -481,13 +584,55 @@ pub fn module_link_get(vm: &mut H, args: &[u8]) -> NativeResult { } } -/// `__moduleStar(dst, src)`:把 src 命名空间的全部导出(除 default)复制到 dst。 +/// 取 src 命名空间某导出名的来源身份。 +/// +/// # 边界与前提 +/// - src 条目表命中且来源为 `Reexport`:沿用其 `(模块, 绑定)`,`direct` 归一为 +/// false(经 star 复制到 dst 的是间接提供)。 +/// - 其余(无表 / `Local` / 条目缺失):回退 `(src_path_id, name_si)`。只声明本地 +/// 导出的源不建条目表,回退 token 使不同源的同名本地导出身份不同。 +fn star_source_origin(src: &JsObject, name_si: u32, src_path_id: u32) -> ModuleNsOrigin { + let table = ns_table_ptr(src); + if !table.is_null() { + // SAFETY: table 归 src 持有,生命周期见 `module_ns_export`。 + if let Some(idx) = unsafe { entry_index(&*table, name_si) } { + // SAFETY: idx 来自刚完成的表内查找,仍在界内。 + if let ModuleNsOrigin::Reexport { path_id, binding_id, .. } = unsafe { (&(*table).entries)[idx].origin } { + return ModuleNsOrigin::Reexport { + path_id, + binding_id, + direct: false, + }; + } + } + } + ModuleNsOrigin::Reexport { + path_id: src_path_id, + binding_id: name_si, + direct: false, + } +} + +/// `__moduleStar(dst, src, src_path)`:把 src 命名空间的可枚举导出(除 default) +/// 复制到 dst,并按来源身份解析同名冲突。 +/// +/// # 步骤 +/// 1. 每个 src 导出名取来源 `(模块, 绑定)`;`default` 跳过。 +/// 2. dst 条目为 `Ambiguous`:恒跳过(一旦歧义恒省略)。 +/// 3. dst 条目为显式导出(`Local` / direct `Reexport`):显式恒胜,跳过。 +/// 4. dst 条目为间接 `Reexport`:`(模块, 绑定)` 相同则保留;不同则删槽并记 +/// `Ambiguous`。 +/// 5. dst 无条目:真实槽已存在视为显式导出跳过;否则复制槽并记间接来源。 +/// +/// # 副作用 +/// - 在 dst 上定义/删除真实属性槽,惰性安装条目表,并按规范顺序重排导出键。 pub fn module_star(vm: &mut H, args: &[u8]) -> NativeResult { - if args.len() < 3 { - return type_error(vm, "__moduleStar: 2 arguments required"); + if args.len() < 4 { + return type_error(vm, "__moduleStar: 3 arguments required"); } let dst_val = vm.reg(args[1]); let src_val = vm.reg(args[2]); + let src_path_val = vm.reg(args[3]); let dst_ptr = match vm.checked_object_ptr(dst_val, "__moduleStar: target is not an object") { Ok(Some(p)) => p, Ok(None) => return type_error(vm, "__moduleStar: target is not an object"), @@ -498,32 +643,73 @@ pub fn module_star(vm: &mut H, args: &[u8]) -> NativeResult { Ok(None) => return type_error(vm, "__moduleStar: source is not an object"), Err(e) => return NativeResult::Err(crate::error::create_error(vm, &e)), }; + let Some(src_path) = vm.lookup_str(src_path_val) else { + return type_error(vm, "__moduleStar: source path is not a string"); + }; + let src_path_id = vm.kernel_core().perm_interner().intern(&src_path).0; let src = unsafe { &*src_ptr }; 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; } - // export * 冲突省略:名称已被本命名空间直接导出或先前 star 占用时跳过 - // (规范 StarExportEntries 求值后,ambiguous 名称从命名空间省略)。 - if vm.get_own_property_slot(dst, name_si).is_some() { - continue; - } - let value = src.get_prop_at(pos); - // star 导出同 module_set:定义新导出走瞬态可扩展窗口,错误路径先复位。 - dst.set_extensible(true); - let result = vm.define_data_property(dst, name_si, value, ns_attrs()); - dst.set_extensible(false); - 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); + let src_origin = star_source_origin(src, name_si, src_path_id); + // live dst:star 复制的新导出名须一并入表,否则活值读/枚举看不到条目。 + let dst_table = ns_table_ptr(dst); + let dst_idx = if dst_table.is_null() { + None + } else { + unsafe { entry_index(&*dst_table, name_si) } + }; + match dst_idx { + Some(idx) => { + // SAFETY: idx 来自刚完成的表内查找,仍在界内。 + let dst_origin = unsafe { (&(*dst_table).entries)[idx].origin }; + match dst_origin { + ModuleNsOrigin::Ambiguous => continue, + ModuleNsOrigin::Local | ModuleNsOrigin::Reexport { direct: true, .. } => continue, + ModuleNsOrigin::Reexport { path_id, binding_id, .. } => { + let ModuleNsOrigin::Reexport { + path_id: sp, binding_id: sb, .. + } = src_origin + else { + continue; + }; + if path_id == sp && binding_id == sb { + continue; + } + // 不同绑定同名:删除导出槽并记歧义哨兵,清空可回收的活值边。 + remove_export_slot(vm, dst, name_si); + // SAFETY: idx 仍指向本表条目,删除槽不改动条目表。 + unsafe { + (&mut (*dst_table).entries)[idx].origin = ModuleNsOrigin::Ambiguous; + (&mut (*dst_table).entries)[idx].state = ModuleNsState::Value { + value: JsValue::undefined(), + initialized: false, + }; + } + } + } + } + None => { + // 真实槽已存在但无条目:视为先前显式导出(本地导出不建表),显式恒胜。 + if vm.get_own_property_slot(dst, name_si).is_some() { + continue; + } + let value = src.get_prop_at(pos); + let table = if dst_table.is_null() { install_ns_table(dst) } else { dst_table }; + // star 导出同 module_set:定义新导出走瞬态可扩展窗口,错误路径先复位。 + dst.set_extensible(true); + let result = vm.define_data_property(dst, name_si, value, ns_attrs()); + dst.set_extensible(false); + if let Err(e) = result { + return NativeResult::Err(crate::error::create_error(vm, &e)); + } + // 快照语义:star 名立即视为已初始化。 + push_entry(table, name_si, src_origin, ModuleNsState::Value { value, initialized: true }); + } } } // star 复制后重排:直接导出与 star 导出的合并键序保持规范顺序。 diff --git a/crates/oxide_builtins/src/object.rs b/crates/oxide_builtins/src/object.rs index 50faf1a0..417f4d1e 100644 --- a/crates/oxide_builtins/src/object.rs +++ b/crates/oxide_builtins/src/object.rs @@ -110,7 +110,7 @@ pub fn key_si_to_js_value(vm: &mut H, si: u32) -> JsValue { /// 与 [`walk_own_keys`] 互补:只返回 Symbol 键。返回 `(属性键 si, 绝对存储索引)`: /// 槽位计数须含全部非空节点(含字符串键)并对数组加元素区偏移,与物理存储及 /// [`walk_own_keys`] 的口径一致,消费方(delete 重建)才能按槽位取回正确值。 -fn walk_own_symbol_keys(vm: &H, obj: &JsObject) -> Vec<(u32, u32)> { +pub(crate) fn walk_own_symbol_keys(vm: &H, obj: &JsObject) -> Vec<(u32, u32)> { let mut keys: Vec<(u32, u32)> = Vec::new(); let shape_id = obj.shape_id(); let mut shape_ids = Vec::new(); diff --git a/crates/oxide_emit/src/compile_ctx.rs b/crates/oxide_emit/src/compile_ctx.rs index cf8ccdf5..2e5065fd 100644 --- a/crates/oxide_emit/src/compile_ctx.rs +++ b/crates/oxide_emit/src/compile_ctx.rs @@ -143,6 +143,11 @@ pub struct CompileCtx { pub(crate) module_local_exports: HashMap>, /// 已求值依赖模块的命名空间对象寄存器(按 import/export source 字符串索引)。 pub(crate) module_dep_ns_regs: HashMap, + /// 依赖模块规范路径(按 import/export source 字符串索引):再导出来源身份用。 + pub(crate) module_dep_paths: HashMap, + /// 非自导入的导入局部名 → (依赖模块规范路径, 导入名)。命名空间导入的导入名为 + /// `oxide_kernel::MODULE_NAMESPACE_BINDING`;再导出据此重分类为间接导出。 + pub(crate) module_import_origins: HashMap, /// 自导入(import from 自身)的 source 字符串集合:绑定走别名语义,不能链接期快照。 pub(crate) module_self_import_specs: HashSet, /// 自导入别名:导出名 → 本地绑定槽寄存器(export 语句执行时回写绑定值)。 @@ -232,6 +237,8 @@ impl CompileCtx { module_ns_reg: None, module_local_exports: HashMap::new(), module_dep_ns_regs: HashMap::new(), + module_dep_paths: HashMap::new(), + module_import_origins: HashMap::new(), module_self_import_specs: HashSet::new(), module_self_aliases: HashMap::new(), module_alias_pairs: Vec::new(), diff --git a/crates/oxide_emit/src/module.rs b/crates/oxide_emit/src/module.rs index 2e3663c1..af50eed0 100644 --- a/crates/oxide_emit/src/module.rs +++ b/crates/oxide_emit/src/module.rs @@ -18,6 +18,7 @@ use oxide_bytecode::opcode::OpCode; use oxide_ir::inst::Inst; use oxide_ir::operand::Operand; use oxide_ir::{IRFunction, ParamLayout}; +use oxide_kernel::MODULE_NAMESPACE_BINDING; use oxide_parser::{ BindingPattern, Declaration, ExportDefaultDeclarationKind, Expression, ImportAttributeKey, ImportDeclarationSpecifier, ModuleExportName, Statement, VariableDeclarationKind, WithClause, @@ -353,6 +354,7 @@ impl Emitter { self.emit_module_call(ctx, "__moduleEval", &[fn_reg])? }; ctx.module_dep_ns_regs.insert(spec.clone(), dep_ns_reg); + ctx.module_dep_paths.insert(spec.clone(), resolved.path.clone()); if resolved.path == module_path { // 自导入:绑定走别名语义(源导出在 body 执行中才就绪), // 不能像普通依赖那样链接期快照。 @@ -382,11 +384,17 @@ impl Emitter { .get(&dep_spec) .ok_or_else(|| format!("module dependency missing: {dep_spec}"))?; let is_self = ctx.module_self_import_specs.contains(&dep_spec); + // 再导出重分类依据:非自导入局部名 → (依赖路径, 导入名)。 + let dep_path = ctx.module_dep_paths.get(&dep_spec).cloned().unwrap_or_default(); if let Some(specifiers) = &imp.specifiers { for sp in specifiers { match sp { ImportDeclarationSpecifier::ImportSpecifier(s) => { let imported = module_export_name_str(&s.imported); + if !is_self { + ctx.module_import_origins + .insert(s.local.name.to_string(), (dep_path.clone(), imported.clone())); + } let alias_source = is_self.then(|| export_name_map.get(&imported)).flatten(); if is_self && !own_export_names.contains(&imported) && !has_unnamed_star { return Err(format!("requested module export is not exported: {imported}")); @@ -417,6 +425,12 @@ impl Emitter { } } ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => { + if !is_self { + ctx.module_import_origins.insert( + s.local.name.to_string(), + (dep_path.clone(), DEFAULT_EXPORT_NAME.to_string()), + ); + } let alias_source = is_self.then(|| export_name_map.get(DEFAULT_EXPORT_NAME)).flatten().cloned(); if is_self && !own_export_names.contains(DEFAULT_EXPORT_NAME) { @@ -461,6 +475,11 @@ impl Emitter { // body 执行后导出自然可见。 if is_self { has_self_ns_import = true; + } else { + ctx.module_import_origins.insert( + s.local.name.to_string(), + (dep_path.clone(), MODULE_NAMESPACE_BINDING.to_string()), + ); } self.emit_bind_target( s.local.name.as_str(), @@ -677,6 +696,17 @@ impl Emitter { Ok(()) } + /// 发 `__moduleSetReexport`:注册带 `(dep_path, imported)` 来源身份的再导出。 + fn emit_module_set_reexport( + &self, ctx: &mut CompileCtx, ns_reg: u32, export_name: &str, value_reg: u32, dep_path: &str, imported: &str, + ) -> Result<(), String> { + let name_reg = self.load_string_const(export_name, ctx); + let path_reg = self.load_string_const(dep_path, ctx); + let imported_reg = self.load_string_const(imported, ctx); + self.emit_module_call(ctx, "__moduleSetReexport", &[ns_reg, name_reg, value_reg, path_reg, imported_reg])?; + Ok(()) + } + /// 顶层对导出源绑定的写入同步到命名空间:按反演表把新值写入每个导出名。 /// /// # 边界与前提 @@ -803,26 +833,43 @@ impl Emitter { _ => return Err("unsupported export declaration".into()), } } else { - let dep_ns_opt = exp - .source - .as_ref() - .map(|src| ctx.module_dep_ns_regs.get(src.value.as_str()).copied()); + let dep_source = exp.source.as_ref().map(|src| src.value.to_string()); for spec in &exp.specifiers { let local_name = module_export_name_str(&spec.local); let exported_name = module_export_name_str(&spec.exported); - let val_reg = match dep_ns_opt { - Some(Some(dep_ns_reg)) => { - let name_reg = self.load_string_const(&local_name, ctx); - self.emit_module_call(ctx, "__moduleLinkGet", &[dep_ns_reg, name_reg])? - } - Some(None) => { - return Err(format!( - "export from unknown module: {}", - exp.source.as_ref().unwrap().value - )) - } - None => self.load_var_reg(&local_name, ctx)?, - }; + // 有 source:来源身份为 (依赖模块路径, 导入名)。 + if let Some(source) = &dep_source { + let dep_ns_reg = ctx + .module_dep_ns_regs + .get(source.as_str()) + .copied() + .ok_or_else(|| format!("export from unknown module: {source}"))?; + let dep_path = ctx + .module_dep_paths + .get(source.as_str()) + .cloned() + .ok_or_else(|| format!("export from unknown module: {source}"))?; + let name_reg = self.load_string_const(&local_name, ctx); + let val_reg = self.emit_module_call(ctx, "__moduleLinkGet", &[dep_ns_reg, name_reg])?; + self.emit_module_set_reexport( + ctx, + ns_reg, + &exported_name, + val_reg, + &dep_path, + &local_name, + )?; + continue; + } + // 无 source 但局部名是导入绑定:按规范重分类为间接导出,来源为 + // (依赖模块路径, 导入名);命名空间导入的导入名为共享哨兵。 + if let Some((dep_path, imported)) = ctx.module_import_origins.get(&local_name).cloned() { + let val_reg = self.load_var_reg(&local_name, ctx)?; + self.emit_module_set_reexport(ctx, ns_reg, &exported_name, val_reg, &dep_path, &imported)?; + continue; + } + // 本地导出:来源恒为 Local。 + let val_reg = self.load_var_reg(&local_name, ctx)?; self.emit_module_set(ctx, ns_reg, &exported_name, val_reg)?; } } @@ -925,12 +972,26 @@ impl Emitter { .module_dep_ns_regs .get(source) .ok_or_else(|| format!("export * from unknown module: {source}"))?; + let dep_path = ctx + .module_dep_paths + .get(source) + .cloned() + .ok_or_else(|| format!("export * from unknown module: {source}"))?; if let Some(exported) = &exp.exported { - // export * as ns from '...':命名空间再导出。 + // export * as ns from '...':命名空间再导出,绑定身份用共享哨兵。 let exported_name = module_export_name_str(exported); - self.emit_module_set(ctx, ns_reg, &exported_name, dep_ns_reg)?; + self.emit_module_set_reexport( + ctx, + ns_reg, + &exported_name, + dep_ns_reg, + &dep_path, + MODULE_NAMESPACE_BINDING, + )?; } else { - self.emit_module_call(ctx, "__moduleStar", &[ns_reg, dep_ns_reg])?; + // 来源传播:star 复制按 (dep_path, 绑定) 判同名冲突。 + let path_reg = self.load_string_const(&dep_path, ctx); + self.emit_module_call(ctx, "__moduleStar", &[ns_reg, dep_ns_reg, path_reg])?; } } _ => return Err("unsupported module export statement".into()), diff --git a/crates/oxide_kernel/src/lib.rs b/crates/oxide_kernel/src/lib.rs index 135640b2..f57c3e6d 100644 --- a/crates/oxide_kernel/src/lib.rs +++ b/crates/oxide_kernel/src/lib.rs @@ -22,3 +22,10 @@ pub mod string_forge; /// kernel 对外的三个核心类型:配置、共享核心、会话。 pub use kernel::{KernelConfig, KernelCore, KernelSession}; + +/// 模块命名空间再导出的绑定身份哨兵。 +/// +/// `export * as ns from mod` 与 `import * as ns from mod; export { ns }` 两路 +/// 转发的是同一个命名空间对象,来源身份须用同一绑定名才能判为同一绑定;该字面量 +/// 含 NUL,不可能与任何静态导出名相同。编译器与运行时共享此常量,禁止另写字面量。 +pub const MODULE_NAMESPACE_BINDING: &str = "\u{0}namespace"; diff --git a/crates/oxide_vm/src/bindings/bind_global.rs b/crates/oxide_vm/src/bindings/bind_global.rs index c1f49358..96ea8742 100644 --- a/crates/oxide_vm/src/bindings/bind_global.rs +++ b/crates/oxide_vm/src/bindings/bind_global.rs @@ -34,13 +34,18 @@ pub fn bind_global(core: &Arc, session: &KernelSession, global: &mut 2, ), ("__moduleSet", oxide_builtins::module::module_set:: as *const (), 3), + ( + "__moduleSetReexport", + oxide_builtins::module::module_set_reexport:: as *const (), + 5, + ), ("__moduleGet", oxide_builtins::module::module_get:: as *const (), 2), ( "__moduleLinkGet", oxide_builtins::module::module_link_get:: as *const (), 2, ), - ("__moduleStar", oxide_builtins::module::module_star:: as *const (), 2), + ("__moduleStar", oxide_builtins::module::module_star:: as *const (), 3), ("__moduleSeal", oxide_builtins::module::module_seal:: as *const (), 1), ("__moduleEval", oxide_builtins::module::module_eval:: as *const (), 1), ("__moduleData", oxide_builtins::module::module_data:: as *const (), 2), diff --git a/crates/oxide_vm/tests/module_star_export_tests.rs b/crates/oxide_vm/tests/module_star_export_tests.rs new file mode 100644 index 00000000..8c91a9e0 --- /dev/null +++ b/crates/oxide_vm/tests/module_star_export_tests.rs @@ -0,0 +1,241 @@ +//! star 再导出的同名冲突省略:第三方观察者模块导入 hub 命名空间,验证歧义名 +//! 在 `in` / `Object.keys` / for-in 三面一致省略,同绑定重复提供与显式导出不误删。 +//! +//! 观察者形态(hub 不自导入)与自别名回写解耦:本文件只钉省/留判定本身。 + +use std::sync::Arc; + +use oxide_compiler::compiler::Compiler; +use oxide_emit::module::{ModuleKind, ModuleSourceLoader, ResolvedModule}; +use oxide_vm::vm::Vm; + +/// 文件加载器:base_dir/specifier join 后读源(依赖模块经真实文件解析)。 +struct FileLoader; + +impl ModuleSourceLoader for FileLoader { + fn resolve( + &mut self, base_dir: &str, specifier: &str, _attributes: &[(&str, &str)], + ) -> Result { + let joined = std::path::Path::new(base_dir).join(specifier); + let full = std::fs::canonicalize(&joined).map_err(|e| format!("cannot read {specifier}: {e}"))?; + let source = std::fs::read_to_string(&full).map_err(|e| format!("cannot read {specifier}: {e}"))?; + Ok(ResolvedModule { + source, + path: full.to_string_lossy().into_owned(), + kind: ModuleKind::Js, + }) + } +} + +/// 测试结束删除临时目录,避免在 crate 树残留文件。 +struct Cleanup(std::path::PathBuf); +impl Drop for Cleanup { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } +} + +/// 同一 VM 续跑脚本并返回字符串完成值(读 global 探针用)。 +fn probe_str(vm: &mut Vm, 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 value = vm.run(&Arc::new(module)).expect("probe run"); + vm.lookup_str(value).unwrap_or_default() +} + +/// 编译并运行带依赖的模块,返回 `main.mjs` 写入 `globalThis.__r` 的探针字符串。 +fn run_observer_module(dir: &std::path::Path) -> String { + let allocator = oxide_parser::Allocator::default(); + let source = std::fs::read_to_string(dir.join("main.mjs")).expect("read main"); + let program = oxide_parser::parse_module(&allocator, &source).expect("parse module"); + let module = Compiler::new() + .compile_module(&program, dir.join("main.mjs").to_string_lossy().as_ref(), &mut FileLoader) + .expect("compile module"); + let mut vm = Vm::new(); + vm.run(&Arc::new(module)).expect("module run"); + probe_str(&mut vm, "globalThis.__r") +} + +/// 建目录并写入给定文件(文件名, 源码)。 +fn fixture_dir(tag: &str, files: &[(&str, &str)]) -> std::path::PathBuf { + let cwd = std::env::current_dir().expect("cwd"); + let dir = cwd.join(format!("__module_star_{tag}__")); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).expect("mkdir"); + for (name, source) in files { + std::fs::write(dir.join(name), source).expect("write fixture"); + } + dir +} + +/// 双星源各自本地声明同名 `both`:歧义名从命名空间省略,`in` / `Object.keys` / +/// for-in 三面一致;唯一名与显式导出照常保留。 +#[test] +fn star_ambiguous_local_binding_is_omitted() { + let dir = fixture_dir( + "ambiguous", + &[ + ("s1.mjs", "export var first = 'a';\nexport var both = 1;"), + ("s2.mjs", "export var second = 'b';\nexport var both = 2;"), + ("hub.mjs", "export * from './s1.mjs';\nexport * from './s2.mjs';"), + ( + "main.mjs", + "import * as ns from './hub.mjs';\n \ + let forks = 0;\n \ + for (const k in ns) forks += (k === 'both' ? 10 : 1);\n \ + globalThis.__r = [\n \ + 'first' in ns,\n \ + 'second' in ns,\n \ + 'both' in ns,\n \ + ns.both === undefined,\n \ + Object.keys(ns).join(','),\n \ + Object.getOwnPropertyNames(ns).includes('both'),\n \ + forks,\n \ + ns.first,\n \ + ns.second,\n \ + ].join('|');", + ), + ], + ); + let _cleanup = Cleanup(dir.clone()); + assert_eq!( + run_observer_module(&dir), + "true|true|false|true|first,second|false|2|a|b", + "歧义名应从命名空间三面一致省略,唯一名保留" + ); +} + +/// 双星源经 `export * as foo from` 转发同一命名空间对象:同 `(模块, 绑定)` 保留。 +#[test] +fn star_same_namespace_reexport_binding_is_kept() { + let dir = fixture_dir( + "star_as", + &[ + ("empty.mjs", ""), + ("s1.mjs", "export * as foo from './empty.mjs';"), + ("s2.mjs", "export * as foo from './empty.mjs';"), + ("hub.mjs", "export * from './s1.mjs';\nexport * from './s2.mjs';"), + ( + "main.mjs", + "import * as ns from './hub.mjs';\n globalThis.__r = [typeof ns.foo, 'foo' in ns].join('|');", + ), + ], + ); + let _cleanup = Cleanup(dir.clone()); + assert_eq!(run_observer_module(&dir), "object|true", "同命名空间再导出不应被误删"); +} + +/// 双星源经 `import * as foo; export { foo }` 转发同一命名空间对象:同绑定保留。 +#[test] +fn star_same_namespace_import_reexport_binding_is_kept() { + let dir = fixture_dir( + "import_star_as", + &[ + ("empty.mjs", ""), + ("s1.mjs", "import * as foo from './empty.mjs';\nexport { foo };"), + ("s2.mjs", "import * as foo from './empty.mjs';\nexport { foo };"), + ("hub.mjs", "export * from './s1.mjs';\nexport * from './s2.mjs';"), + ( + "main.mjs", + "import * as ns from './hub.mjs';\n globalThis.__r = [typeof ns.foo, 'foo' in ns].join('|');", + ), + ], + ); + let _cleanup = Cleanup(dir.clone()); + assert_eq!(run_observer_module(&dir), "object|true", "导入命名空间再导出不应被误删"); +} + +/// 双星源经 `export { foo } from base` / `import { foo } from base; export { foo }` +/// 转发同一绑定:`(模块, 绑定)` 相同,保留。 +#[test] +fn star_same_named_reexport_binding_is_kept() { + let dir = fixture_dir( + "propagates", + &[ + ("base.mjs", "export const foo = 2;"), + ("s1.mjs", "export { foo } from './base.mjs';"), + ("s2.mjs", "import { foo } from './base.mjs';\nexport { foo };"), + ("hub.mjs", "export * from './s1.mjs';\nexport * from './s2.mjs';"), + ( + "main.mjs", + "import * as ns from './hub.mjs';\n globalThis.__r = [ns.foo, 'foo' in ns].join('|');", + ), + ], + ); + let _cleanup = Cleanup(dir.clone()); + assert_eq!(run_observer_module(&dir), "2|true", "同源绑定经不同路径转发不应被误删"); +} + +/// 三源:S1/S2 本地同名歧义后,S3 与 S1 同模块(同绑定 token)也不得重新加入。 +#[test] +fn star_ambiguous_stays_omitted_with_third_source() { + let dir = fixture_dir( + "three_sources", + &[ + ("s1.mjs", "export var x = 1;\nexport var keep = 's1';"), + ("s2.mjs", "export var x = 2;"), + ( + "hub.mjs", + "export * from './s1.mjs';\nexport * from './s2.mjs';\nexport * from './s1.mjs';", + ), + ( + "main.mjs", + "import * as ns from './hub.mjs';\n globalThis.__r = ['x' in ns, ns.keep].join('|');", + ), + ], + ); + let _cleanup = Cleanup(dir.clone()); + assert_eq!(run_observer_module(&dir), "false|s1", "一旦歧义不得因后续同绑定源重新加入"); +} + +/// 显式本地导出在 star 歧义省略之后执行:显式导出重新定义该名并胜出。 +#[test] +fn star_ambiguous_then_local_export_revives_name() { + let dir = fixture_dir( + "revive", + &[ + ("s1.mjs", "export var both = 1;"), + ("s2.mjs", "export var both = 2;"), + ("hub.mjs", "export * from './s1.mjs';\nexport * from './s2.mjs';\nexport var both = 'local';"), + ( + "main.mjs", + "import * as ns from './hub.mjs';\n globalThis.__r = [ns.both, 'both' in ns, Object.keys(ns).join(',')].join('|');", + ), + ], + ); + let _cleanup = Cleanup(dir.clone()); + assert_eq!(run_observer_module(&dir), "local|true|both", "star 歧义后显式导出应重新定义并胜出"); +} + +/// 显式本地导出与 star 源同名:无论语句先后,显式导出恒胜。 +#[test] +fn star_local_export_wins_over_star_source() { + let dir = fixture_dir( + "local_first", + &[ + ("dep.mjs", "export var x = 'dep';\nexport var d = 1;"), + ("hub.mjs", "export var x = 'local';\nexport * from './dep.mjs';"), + ( + "main.mjs", + "import * as ns from './hub.mjs';\n globalThis.__r = [ns.x, 'x' in ns, ns.d].join('|');", + ), + ], + ); + let _cleanup = Cleanup(dir.clone()); + assert_eq!(run_observer_module(&dir), "local|true|1", "本地导出先于 star 时恒胜"); + + let dir = fixture_dir( + "local_last", + &[ + ("dep.mjs", "export var x = 'dep';\nexport var d = 1;"), + ("hub.mjs", "export * from './dep.mjs';\nexport var x = 'local';"), + ( + "main.mjs", + "import * as ns from './hub.mjs';\n globalThis.__r = [ns.x, 'x' in ns, ns.d].join('|');", + ), + ], + ); + let _cleanup = Cleanup(dir.clone()); + assert_eq!(run_observer_module(&dir), "local|true|1", "本地导出后于 star 时覆盖 star 值"); +}