mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-24 02:31:13 +08:00
feat: 星号导出歧义名从命名空间省略
This commit is contained in:
@@ -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<H: VmHost>(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<ModuleNsQuery> {
|
||||
// SAFETY: table 指向本 ns 对象持有的 Box<ModuleNsTable>,随对象存活。
|
||||
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<H: VmHost>(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<H: VmHost>(
|
||||
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<H: VmHost>(vm: &mut H, obj: &mut JsObject, name_si: u32) {
|
||||
let mut retained: Vec<(u32, JsValue, Option<PropMetaEntry>)> = 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<PropMetaEntry>)> = 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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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 导出的合并键序保持规范顺序。
|
||||
|
||||
@@ -110,7 +110,7 @@ pub fn key_si_to_js_value<H: VmHost>(vm: &mut H, si: u32) -> JsValue {
|
||||
/// 与 [`walk_own_keys`] 互补:只返回 Symbol 键。返回 `(属性键 si, 绝对存储索引)`:
|
||||
/// 槽位计数须含全部非空节点(含字符串键)并对数组加元素区偏移,与物理存储及
|
||||
/// [`walk_own_keys`] 的口径一致,消费方(delete 重建)才能按槽位取回正确值。
|
||||
fn walk_own_symbol_keys<H: VmHost>(vm: &H, obj: &JsObject) -> Vec<(u32, u32)> {
|
||||
pub(crate) fn walk_own_symbol_keys<H: VmHost>(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();
|
||||
|
||||
@@ -143,6 +143,11 @@ pub struct CompileCtx {
|
||||
pub(crate) module_local_exports: HashMap<String, Vec<String>>,
|
||||
/// 已求值依赖模块的命名空间对象寄存器(按 import/export source 字符串索引)。
|
||||
pub(crate) module_dep_ns_regs: HashMap<String, u32>,
|
||||
/// 依赖模块规范路径(按 import/export source 字符串索引):再导出来源身份用。
|
||||
pub(crate) module_dep_paths: HashMap<String, String>,
|
||||
/// 非自导入的导入局部名 → (依赖模块规范路径, 导入名)。命名空间导入的导入名为
|
||||
/// `oxide_kernel::MODULE_NAMESPACE_BINDING`;再导出据此重分类为间接导出。
|
||||
pub(crate) module_import_origins: HashMap<String, (String, String)>,
|
||||
/// 自导入(import from 自身)的 source 字符串集合:绑定走别名语义,不能链接期快照。
|
||||
pub(crate) module_self_import_specs: HashSet<String>,
|
||||
/// 自导入别名:导出名 → 本地绑定槽寄存器(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(),
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -34,13 +34,18 @@ pub fn bind_global(core: &Arc<KernelCore>, session: &KernelSession, global: &mut
|
||||
2,
|
||||
),
|
||||
("__moduleSet", oxide_builtins::module::module_set::<crate::vm::Vm> as *const (), 3),
|
||||
(
|
||||
"__moduleSetReexport",
|
||||
oxide_builtins::module::module_set_reexport::<crate::vm::Vm> as *const (),
|
||||
5,
|
||||
),
|
||||
("__moduleGet", oxide_builtins::module::module_get::<crate::vm::Vm> as *const (), 2),
|
||||
(
|
||||
"__moduleLinkGet",
|
||||
oxide_builtins::module::module_link_get::<crate::vm::Vm> as *const (),
|
||||
2,
|
||||
),
|
||||
("__moduleStar", oxide_builtins::module::module_star::<crate::vm::Vm> as *const (), 2),
|
||||
("__moduleStar", oxide_builtins::module::module_star::<crate::vm::Vm> as *const (), 3),
|
||||
("__moduleSeal", oxide_builtins::module::module_seal::<crate::vm::Vm> as *const (), 1),
|
||||
("__moduleEval", oxide_builtins::module::module_eval::<crate::vm::Vm> as *const (), 1),
|
||||
("__moduleData", oxide_builtins::module::module_data::<crate::vm::Vm> as *const (), 2),
|
||||
|
||||
@@ -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<ResolvedModule, String> {
|
||||
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 值");
|
||||
}
|
||||
Reference in New Issue
Block a user