From f3c3620b706f88b5a4efb642f4a27eb0bc18bf8e Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Wed, 14 Oct 2026 07:04:41 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20source-phase=20=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E7=BB=91=E5=AE=9A=20ModuleSource=20=E5=AE=BF=E4=B8=BB=E5=86=85?= =?UTF-8?q?=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 2 +- crates/oxide_builtins/src/module.rs | 105 +++++++++++++ crates/oxide_emit/src/module.rs | 25 ++- crates/oxide_emit/tests/module_link_tests.rs | 50 ++++++ crates/oxide_test262/src/runner.rs | 10 ++ crates/oxide_types/src/lib.rs | 8 + crates/oxide_vm/src/bindings/bind_global.rs | 5 + crates/oxide_vm/src/bindings/mod.rs | 4 + crates/oxide_vm/src/test262_host.rs | 86 ++++++++++- .../tests/module_source_phase_tests.rs | 145 ++++++++++++++++++ 10 files changed, 435 insertions(+), 5 deletions(-) create mode 100644 crates/oxide_vm/tests/module_source_phase_tests.rs diff --git a/Cargo.toml b/Cargo.toml index 5bf76730..f5a95f46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ members = [ ] [workspace.package] -version = "0.19.4" +version = "0.19.5" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_builtins/src/module.rs b/crates/oxide_builtins/src/module.rs index 772e30af..64f27450 100644 --- a/crates/oxide_builtins/src/module.rs +++ b/crates/oxide_builtins/src/module.rs @@ -1070,6 +1070,111 @@ pub fn module_data(vm: &mut H, args: &[u8]) -> NativeResult { NativeResult::Ok(JsValue::from_js_object(obj)) } +/// `%AbstractModuleSource%` 构造器入口:被调用(含 `new`)恒抛 TypeError。 +/// +/// 规范 28.1.1.1:该内建不可被构造,实例只能由宿主(source-phase 导入的 +/// ModuleSource 提供)产生。 +pub fn abstract_module_source_ctor(vm: &mut H, _args: &[u8]) -> NativeResult { + NativeResult::Err(crate::error::create_type_error(vm, "AbstractModuleSource is not a constructor")) +} + +/// `%AbstractModuleSource%.prototype` 上 `Symbol.toStringTag` 访问器的 getter: +/// 返回实例的模块源类名,非实例(非对象 this 或无类名标记)返回 undefined。 +/// +/// # 步骤 +/// 1. `this` 非对象 → undefined(规范 28.3.3.2 第 2 步)。 +/// 2. 查全局符号注册表取类名符号键;未登记或 `this` 无该 own 属性 → undefined。 +/// 3. 有类名标记 → 返回其值(宿主模块源类名,如 `AbstractModuleSource`)。 +/// +/// # 边界与前提 +/// - 类名标记是 `__moduleSourceObject` 在实例上定义的符号键 own 属性(引擎内 +/// 表示宿主 `[[ModuleSourceClassName]]` 内部槽),非实例对象无此属性。 +pub fn abstract_module_source_to_string_tag(vm: &mut H, args: &[u8]) -> NativeResult { + // 访问器 getter 的 `this` 在 args[0](receiver 槽 253),实参区从 args[1] 起。 + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + if !this_val.is_object() { + return NativeResult::Ok(JsValue::undefined()); + } + let Some(idx) = vm.symbol_lookup_global(MODULE_SOURCE_CLASS_NAME_SYMBOL_KEY) else { + return NativeResult::Ok(JsValue::undefined()); + }; + let key = encode_symbol_key(vm.realm_id(), idx); + let obj = unsafe { &*this_val.as_js_object_ptr() }; + if vm.get_own_property_slot(obj, key).is_none() { + return NativeResult::Ok(JsValue::undefined()); + } + match vm.ordinary_get(obj, key, this_val) { + Ok(v) => NativeResult::Ok(v), + Err(e) => NativeResult::Err(crate::error::create_error(vm, &e)), + } +} + +/// ModuleSource 类名符号的宿主注册表键(`Symbol.for` 全局注册表口径)。 +const MODULE_SOURCE_CLASS_NAME_SYMBOL_KEY: &str = "oxide.moduleSourceClassName"; + +/// `__moduleSourceObject()`:返回一个 ModuleSource 对象(`%AbstractModuleSource%` +/// 实例),供 source-phase 导入的局部名绑定。 +/// +/// # 步骤 +/// 1. 经 `$262.AbstractModuleSource.prototype` 取内建原型(宿主绑定在绑定期 +/// 安装);宿主或内建缺失时抛 TypeError。 +/// 2. 以该原型分配新对象。 +/// 3. 定义类名符号 own 属性(非枚举),供 `@@toStringTag` getter 读回类名。 +/// +/// # 边界与前提 +/// - 每次调用产生新实例:宿主对同一 `` 模块记录的 ModuleSource +/// 身份由依赖求值缓存(per-module 状态)承载,对象本体不跨调用共享。 +/// +/// # 副作用 +/// - 分配新对象并定义符号键属性;首次调用时登记类名符号入全局注册表。 +pub fn module_source_object(vm: &mut H, _args: &[u8]) -> NativeResult { + let global = vm.global_object(); + let global_val = JsValue::from_js_object(global.as_ptr() as *mut JsObject); + let host_si = vm.perm_intern("$262"); + let host = match vm.ordinary_get(&global, host_si, global_val) { + Ok(v) => v, + Err(e) => return NativeResult::Err(crate::error::create_error(vm, &e)), + }; + if !host.is_object() { + return type_error(vm, "__moduleSourceObject: $262 host is not bound"); + } + let host_obj = unsafe { &*host.as_js_object_ptr() }; + let ctor_si = vm.perm_intern("AbstractModuleSource"); + let ctor = match vm.ordinary_get(host_obj, ctor_si, host) { + Ok(v) => v, + Err(e) => return NativeResult::Err(crate::error::create_error(vm, &e)), + }; + if !ctor.is_object() { + return type_error(vm, "__moduleSourceObject: AbstractModuleSource is not bound"); + } + let ctor_obj = unsafe { &*ctor.as_js_object_ptr() }; + let proto_si = vm.perm_intern("prototype"); + let proto = match vm.ordinary_get(ctor_obj, proto_si, ctor) { + Ok(v) => v, + Err(e) => return NativeResult::Err(crate::error::create_error(vm, &e)), + }; + if !proto.is_object() { + return type_error(vm, "__moduleSourceObject: AbstractModuleSource.prototype is not an object"); + } + + let obj = vm.alloc_object(JsObject::new_empty(EMPTY_SHAPE_ID, proto)); + let obj_ref = unsafe { &mut *obj }; + let idx = match vm.symbol_lookup_global(MODULE_SOURCE_CLASS_NAME_SYMBOL_KEY) { + Some(idx) => idx, + None => { + let idx = vm.symbol_intern(Some(MODULE_SOURCE_CLASS_NAME_SYMBOL_KEY.to_string())); + vm.symbol_register_global(MODULE_SOURCE_CLASS_NAME_SYMBOL_KEY.to_string(), idx); + idx + } + }; + let key = encode_symbol_key(vm.realm_id(), idx); + let class_name = vm.new_string("AbstractModuleSource"); + if let Err(e) = vm.define_data_property(obj_ref, key, class_name, PropAttributes::new(true, false, true)) { + return NativeResult::Err(crate::error::create_error(vm, &e)); + } + NativeResult::Ok(JsValue::from_js_object(obj)) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/oxide_emit/src/module.rs b/crates/oxide_emit/src/module.rs index 5a5b5cc5..41aa967d 100644 --- a/crates/oxide_emit/src/module.rs +++ b/crates/oxide_emit/src/module.rs @@ -7,7 +7,9 @@ //! defer 导入(`import defer * as ns`)的依赖不求值:编译入子模块树并发 //! `__moduleDeferObject` 构造 deferred namespace 对象(逐名预注册静态导出名), //! 绑定该对象;自导入/祖先 defer 回边发哨兵形态(不递归、不编译)。 -//! 未支持:嵌套闭包内读 import 名(仍为链接期快照)、source-phase; +//! source-phase 导入(`import source x from`)的局部名绑定目标模块的 ModuleSource +//! 对象(宿主内建),再导出链按 `~source~` 来源身份转发。 +//! 未支持:嵌套闭包内读 import 名(仍为链接期快照); //! star 转发与经再导出链的活值仍为快照;eager 环导入在编译期跳过。 use std::collections::{HashMap, HashSet}; @@ -28,7 +30,7 @@ use oxide_parser::{ ImportAttributeKey, ImportDeclarationSpecifier, ImportPhase, ModuleExportName, ObjectPropertyKind, SimpleAssignmentTarget, Statement, VariableDeclarationKind, WithClause, }; -use oxide_types::MODULE_NAMESPACE_BINDING; +use oxide_types::{MODULE_NAMESPACE_BINDING, MODULE_SOURCE_BINDING}; /// 数据模块种类(非 JS 源码模块)。 #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -1241,6 +1243,25 @@ impl Emitter { } } ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => { + if imp.phase == Some(ImportPhase::Source) { + // source-phase 导入:局部名绑定目标模块的 ModuleSource + // 对象(宿主提供的 %AbstractModuleSource% 实例), + // 非默认导出。再导出链的来源身份按 `~source~` 哨兵 + // 登记,使同名异源经 star 转发时身份可比。 + ctx.module_import_origins.insert( + s.local.name.to_string(), + (dep_path.clone(), MODULE_SOURCE_BINDING.to_string()), + ); + let val_reg = self.emit_module_call(ctx, "__moduleSourceObject", &[])?; + self.emit_bind_target( + s.local.name.as_str(), + val_reg, + VariableDeclarationKind::Const, + false, + ctx, + )?; + continue; + } if !is_self { ctx.module_import_origins.insert( s.local.name.to_string(), diff --git a/crates/oxide_emit/tests/module_link_tests.rs b/crates/oxide_emit/tests/module_link_tests.rs index 5595f558..dc840ff9 100644 --- a/crates/oxide_emit/tests/module_link_tests.rs +++ b/crates/oxide_emit/tests/module_link_tests.rs @@ -663,3 +663,53 @@ fn defer_only_dep_without_tla_no_hoisting() { assert_eq!(native_calls_to(&ir, "__moduleWaitAll"), 0, "无 TLA 模块不得发聚合 gate"); assert!(!ir.is_async, "无 TLA 模块的导入方不得标记异步"); } + +/// 面 33:source-phase 导入(`import source x from`)局部名绑定 ModuleSource 对象—— +/// 发 `__moduleSourceObject` 调用,不走默认导入的 `__moduleLinkGet` 取 default 路径。 +#[test] +fn import_source_phase_binds_module_source_object() { + let (ir, _) = emit_module( + "import source x from \"./src.js\"; var y = x;", + "./entry.js", + &[("./src.js", ModuleKind::Js, "")], + ) + .expect("source-phase 导入应编译成功"); + assert!( + builtin_names(&ir).contains("__moduleSourceObject"), + "source-phase 导入应发 __moduleSourceObject 调用" + ); + assert_eq!(native_calls_to(&ir, "__moduleSourceObject"), 1, "局部名应绑定一次 ModuleSource"); + assert_eq!(native_calls_to(&ir, "__moduleLinkGet"), 0, "source-phase 不得走 default 导出链接"); +} + +/// 面 34:source-phase 导入再导出(`export { x }`)按 `~source~` 来源身份转发—— +/// 经 `__moduleSetReexport` 注册,来源路径与绑定名入池。 +#[test] +fn import_source_phase_reexport_registrs_source_origin() { + let (ir, _) = emit_module( + "import source x from \"./src.js\"; export { x };", + "./entry.js", + &[("./src.js", ModuleKind::Js, "")], + ) + .expect("source-phase 导入再导出应编译成功"); + assert!( + builtin_names(&ir).contains("__moduleSetReexport"), + "source-phase 再导出应走 __moduleSetReexport" + ); + assert_eq!(native_calls_to(&ir, "__moduleSetReexport"), 1, "再导出应注册一次来源身份"); + assert_eq!(native_calls_to(&ir, "__moduleSet"), 0, "source-phase 再导出不得走本地注册"); +} + +/// 面 35:普通导入(phase 缺省)产物零 diff 守卫——source-phase 分支不得改变 +/// 默认导入的发射形态(仍走 `__moduleLinkGet` 取 default)。 +#[test] +fn default_import_unchanged_by_source_phase_branch() { + let (ir, _) = emit_module( + "import d from \"./dep.js\"; var y = d;", + "./entry.js", + &[("./dep.js", ModuleKind::Js, "export default 42;")], + ) + .expect("默认导入应编译成功"); + assert!(builtin_names(&ir).contains("__moduleLinkGet"), "默认导入应经 __moduleLinkGet 链接"); + assert_eq!(native_calls_to(&ir, "__moduleSourceObject"), 0, "普通默认导入不得发 source 对象调用"); +} diff --git a/crates/oxide_test262/src/runner.rs b/crates/oxide_test262/src/runner.rs index 3e12db1e..e451a8e1 100644 --- a/crates/oxide_test262/src/runner.rs +++ b/crates/oxide_test262/src/runner.rs @@ -78,6 +78,16 @@ impl oxide_emit::module::ModuleSourceLoader for Test262ModuleLoader { &mut self, base_dir: &str, specifier: &str, attributes: &[(&str, &str)], ) -> Result { use oxide_emit::module::{ModuleKind, ResolvedModule}; + // `` 是 test262 宿主哨兵说明符(INTERPRETING 约定):解析为 + // 合成空模块,ModuleSource 对象由宿主在 `import source` 绑定时提供,不读磁盘。 + // 路径回填哨兵本身(稳定规范形态),使自导入身份比较与循环检测不误判。 + if specifier == "" { + return Ok(ResolvedModule { + source: String::new(), + path: "".to_string(), + kind: ModuleKind::Js, + }); + } let kind = attributes .iter() .find(|(k, _)| *k == "type") diff --git a/crates/oxide_types/src/lib.rs b/crates/oxide_types/src/lib.rs index 542551e8..fe7c1de5 100644 --- a/crates/oxide_types/src/lib.rs +++ b/crates/oxide_types/src/lib.rs @@ -30,3 +30,11 @@ pub use error::{JsError, JsErrorKind}; /// 转发的是同一个命名空间对象,来源身份须用同一绑定名才能判为同一绑定;该字面量 /// 含 NUL,不可能与任何静态导出名相同。编译器与运行时共享此常量,禁止另写字面量。 pub const MODULE_NAMESPACE_BINDING: &str = "\u{0}namespace"; + +/// 模块 source-phase 再导出的绑定身份哨兵(`~source~` 的引擎内表示)。 +/// +/// `import source x from mod; export { x }` 与 `export * from` 两路转发的是 +/// 同一目标模块的 ModuleSource 对象,来源身份须用同一绑定名才能判为同一绑定 +/// (同名异源才歧义)。该字面量含 NUL,不可能与任何静态导出名相同;编译器与 +/// 运行时共享此常量,禁止另写字面量。 +pub const MODULE_SOURCE_BINDING: &str = "\u{0}source"; diff --git a/crates/oxide_vm/src/bindings/bind_global.rs b/crates/oxide_vm/src/bindings/bind_global.rs index 20c3fd53..3d569fce 100644 --- a/crates/oxide_vm/src/bindings/bind_global.rs +++ b/crates/oxide_vm/src/bindings/bind_global.rs @@ -63,6 +63,11 @@ pub fn bind_global(env: BindEnv<'_>, global: &mut JsObject) { 2, ), ("__moduleData", oxide_builtins::module::module_data:: as *const (), 2), + ( + "__moduleSourceObject", + oxide_builtins::module::module_source_object:: as *const (), + 0, + ), ("__moduleWaitAll", module_wait_all as *const (), 0), ("eval", oxide_builtins::eval::eval:: as *const (), 1), ( diff --git a/crates/oxide_vm/src/bindings/mod.rs b/crates/oxide_vm/src/bindings/mod.rs index e792b898..15ecee6f 100644 --- a/crates/oxide_vm/src/bindings/mod.rs +++ b/crates/oxide_vm/src/bindings/mod.rs @@ -104,6 +104,10 @@ impl<'a> BindEnv<'a> { pub fn world(&self) -> &BuiltinWorld { self.session.builtin_world() } + /// 本绑定环境的 realm 编号(well-known 符号键编码用)。 + pub(crate) fn realm_id(&self) -> u32 { + self.realm_id + } /// 共享 shape forge。 fn shape_forge(&self) -> &ShapeForge { self.core.shape_forge().as_ref() diff --git a/crates/oxide_vm/src/test262_host.rs b/crates/oxide_vm/src/test262_host.rs index e146b8ae..6223b5d6 100644 --- a/crates/oxide_vm/src/test262_host.rs +++ b/crates/oxide_vm/src/test262_host.rs @@ -7,10 +7,11 @@ use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_runtime_api::{to_units_full, NativeResult}; -use oxide_types::object::{JsObject, NativeFnPtr}; +use oxide_types::object::{JsObject, NativeFnPtr, PropAttributes}; +use oxide_types::private_key::{encode_symbol_key, WELL_KNOWN_SYMBOL_TO_STRING_TAG}; use oxide_types::value::JsValue; -use crate::bindings::{apply_binding_table, bind_global_value, BindEnv}; +use crate::bindings::{apply_binding_table, bind_accessor_getter_key, bind_global_value, BindEnv}; use crate::vm::Vm; /// `$262` 未实现方法统一抛错:消息带 `not supported`,runner 据此按能力缺失跳过。 @@ -96,6 +97,82 @@ pub fn is_html_dda_call(_vm: &mut Vm, _args: &[u8]) -> NativeResult { NativeResult::Ok(JsValue::null()) } +/// 构造 `%AbstractModuleSource%` 内建(source-phase 导入的 ModuleSource 实例族): +/// 构造器调用恒抛 TypeError,原型带 `constructor` 与 `@@toStringTag` 访问器。 +/// +/// # 步骤 +/// 1. 建原型对象(Object.prototype 为原型):`constructor` 指回构造器 +/// (可写不可枚举可配置),`@@toStringTag` 装原生 getter(不可枚举可配置)。 +/// 2. 建构造器函数对象(Function.prototype 为原型,可构造,原生入口抛 TypeError): +/// `prototype` 指回原型(不可写不可枚举不可配置)、`name` = "AbstractModuleSource"、 +/// `length` = 0(均不可写不可枚举可配置)。 +/// 3. 原型与构造器登记进 world 释放表(session 收尾统一释放)。 +/// +/// # 注意事项 +/// - 每次绑定新建对象(与 `$262` 本体同一约定):选择性重建后旧内建对象随宿主 +/// 对象泄漏,全量初始化与 full_reset 频率低,可接受。 +fn build_abstract_module_source_ctor(env: BindEnv<'_>) -> *mut JsObject { + let sf = env.core.perm_interner().as_ref(); + let sh = env.core.shape_forge().as_ref(); + let world = env.world(); + + // 原型对象:constructor 槽先装(getter 安装会再扩 shape 槽)。 + let object_proto = world.object_proto.as_ptr() as *mut JsObject; + let proto = Box::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(object_proto))); + let proto_ptr = Box::into_raw(proto); + + // 构造器函数对象:原生入口恒抛 TypeError(规范 28.1.1.1 不可构造)。 + let fn_proto_val = JsValue::from_js_object(world.function_proto.as_ptr() as *mut JsObject); + let mut ctor = Box::new(JsObject::new_empty(EMPTY_SHAPE_ID, fn_proto_val)); + ctor.set_function(true); + ctor.type_tag = JsObject::OBJ_TYPE_CONSTRUCTOR; + ctor.set_native_fn(Some(unsafe { + NativeFnPtr::from_raw(oxide_builtins::module::abstract_module_source_ctor:: as *const ()) + })); + ctor.set_native_arg_count(0); + + // 属性面:prototype / name / length 三槽(描述符按规范内建函数约定)。 + let si_prototype = sf.intern("prototype").0; + let si_name = sf.intern("name").0; + let si_length = sf.intern("length").0; + let shape1 = sh.make_shape(EMPTY_SHAPE_ID, si_prototype); + let shape2 = sh.make_shape(shape1, si_name); + let shape3 = sh.make_shape(shape2, si_length); + ctor.set_shape_id(shape3); + ctor.ensure_hash_props().push(JsValue::from_js_object(proto_ptr)); + ctor.ensure_hash_props() + .push(JsValue::perm_string(sf.string_ptr(sf.intern("AbstractModuleSource").0))); + ctor.ensure_hash_props().push(JsValue::int(0)); + ctor.set_data_meta(0, PropAttributes::new(false, false, false)); + ctor.set_data_meta(1, PropAttributes::new(false, false, true)); + ctor.set_data_meta(2, PropAttributes::new(false, false, true)); + let ctor_ptr = Box::into_raw(ctor); + + // 原型 constructor 槽:可写不可枚举可配置(与一般内建原型约定一致)。 + let proto_ref = unsafe { &mut *proto_ptr }; + let si_constructor = sf.intern("constructor").0; + let ctor_shape = sh.make_shape(proto_ref.shape_id(), si_constructor); + proto_ref.set_shape_id(ctor_shape); + proto_ref.ensure_hash_props().push(JsValue::from_js_object(ctor_ptr)); + let ctor_pos = proto_ref.prop_vec_len().saturating_sub(1) as u32; + proto_ref.set_data_meta(ctor_pos, PropAttributes::new(true, false, true)); + proto_ref.bump_generation(); + + // 原型 @@toStringTag 访问器:getter 按实例类名标记返回值,非实例返 undefined。 + let tag_key = encode_symbol_key(env.realm_id(), WELL_KNOWN_SYMBOL_TO_STRING_TAG); + bind_accessor_getter_key( + env, + proto_ref, + tag_key, + "get [Symbol.toStringTag]", + oxide_builtins::module::abstract_module_source_to_string_tag:: as *const (), + ); + + world.track_leaked_object(ctor_ptr); + world.track_leaked_object(proto_ptr); + ctor_ptr +} + /// 把 `$262` 宿主对象绑定到 global:对象本体 + 标准方法 + `global` 数据属性。 /// /// # 步骤 @@ -103,6 +180,7 @@ pub fn is_html_dda_call(_vm: &mut Vm, _args: &[u8]) -> NativeResult { /// 2. 绑定 evalScript/detachArrayBuffer/createRealm/gc/agent 方法。 /// 3. `global` 数据属性指向当前 session global;`$262` 本体挂到 global。 /// 4. `IsHTMLDDA` 宿主对象挂到 `$262`(可调用,被调用时返回 null)。 +/// 5. `AbstractModuleSource` 内建挂到 `$262`(source-phase 导入的 ModuleSource 实例族)。 /// /// # 注意事项 /// - 宿主对象登记进 world 释放表,session 收尾时统一释放(与 Reflect/Iterator @@ -124,6 +202,10 @@ pub fn bind_test262_host(env: BindEnv<'_>, global: &mut JsObject) { ); let global_this = JsValue::from_js_object(global as *mut JsObject); bind_global_value(env.core, &mut host, "global", global_this); + // %AbstractModuleSource% 内建:source-phase 导入的 ModuleSource 实例族 + // (构造器调用抛 TypeError,原型承载 @@toStringTag 访问器)。 + let amms_ctor = build_abstract_module_source_ctor(env); + bind_global_value(env.core, &mut host, "AbstractModuleSource", JsValue::from_js_object(amms_ctor)); // [[IsHTMLDDA]] 宿主值(B.3.4):可调用(被调用时返回 null), // ToBoolean/typeof/宽松相等按 undefined 处理,Type 与其余强制转换按普通对象处理。 let mut dda = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(object_proto)); diff --git a/crates/oxide_vm/tests/module_source_phase_tests.rs b/crates/oxide_vm/tests/module_source_phase_tests.rs new file mode 100644 index 00000000..b677fe13 --- /dev/null +++ b/crates/oxide_vm/tests/module_source_phase_tests.rs @@ -0,0 +1,145 @@ +//! source-phase 导入(`import source x from`)的端到端语义钉:局部名绑定 +//! %AbstractModuleSource% 实例、`` 宿主哨兵说明符解析、再导出链 +//! 与 star 转发的 `~source~` 来源身份(同源同绑定不歧义)、`$262.AbstractModuleSource` +//! 内建面(构造器抛 TypeError、原型描述符、`@@toStringTag` getter 行为)。 + +use std::collections::HashMap; +use std::sync::Arc; + +use oxide_compiler::compiler::Compiler; +use oxide_emit::module::{ModuleKind, ModuleSourceLoader, ResolvedModule}; +use oxide_vm::vm::Vm; + +/// 内存版加载器:specifier 精确查表;`` 哨兵说明符解析为合成空模块 +/// (与 test262 runner 的宿主口径一致),ModuleSource 对象由宿主在绑定时提供。 +struct MemSourceLoader { + table: HashMap, +} + +impl MemSourceLoader { + fn new(pairs: &[(&str, &str)]) -> Self { + Self { + table: pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(), + } + } +} + +impl ModuleSourceLoader for MemSourceLoader { + fn resolve( + &mut self, _base_dir: &str, specifier: &str, _attributes: &[(&str, &str)], + ) -> Result { + if specifier == "" { + return Ok(ResolvedModule { + source: String::new(), + path: "".to_string(), + kind: ModuleKind::Js, + }); + } + self.table + .get(specifier) + .map(|source| ResolvedModule { + source: source.clone(), + path: specifier.to_string(), + kind: ModuleKind::Js, + }) + .ok_or_else(|| format!("MemSourceLoader 无 {specifier}")) + } +} + +/// 编译并运行模块,返回探针字符串(探针脚本接在同一 VM 上续跑)。 +fn run_module(main: &str, deps: &[(&str, &str)], probe: &str) -> String { + let allocator = oxide_parser::Allocator::default(); + let program = oxide_parser::parse_module(&allocator, main).expect("parse module"); + let mut loader = MemSourceLoader::new(deps); + let module = Compiler::new() + .compile_module(&program, "main.mjs", &mut loader) + .expect("compile module"); + let mut vm = Vm::new(); + vm.run(&Arc::new(module)).expect("module run"); + let allocator = oxide_parser::Allocator::default(); + let probe_prog = oxide_parser::parse(&allocator, probe).expect("parse probe"); + let probe_module = Compiler::new().compile(&probe_prog).expect("compile probe"); + let value = vm.run(&Arc::new(probe_module)).expect("probe run"); + vm.lookup_str(value).unwrap_or_default() +} + +/// 面 1:`import source x from ''` 局部名绑定 ModuleSource 对象—— +/// 对象形态、`instanceof $262.AbstractModuleSource` 成立。 +#[test] +fn import_source_binding_is_module_source_object() { + let r = run_module( + "import source x from '';\n \ + globalThis.__t = [typeof x, x instanceof $262.AbstractModuleSource].join(',');", + &[], + "globalThis.__t", + ); + assert_eq!(r, "object,true", "source-phase 局部名应绑定对象且为 %AbstractModuleSource% 实例"); +} + +/// 面 2:再导出链(fixture `import source x; export { x }`)——命名导入与命名空间 +/// [[Get]] 均取目标模块的 ModuleSource 对象。 +#[test] +fn reexported_source_binding_named_import_and_namespace_get() { + let r = run_module( + "import { x } from './fixture.js';\n \ + import * as ns from './fixture.js';\n \ + globalThis.__t = [x instanceof $262.AbstractModuleSource, ns.x === x].join(',');", + &[("./fixture.js", "import source x from ''; export { x };")], + "globalThis.__t", + ); + assert_eq!(r, "true,true", "再导出链的命名导入与命名空间读应得同一 ModuleSource 对象"); +} + +/// 面 3:两 fixture 各 `import source mod from ''; export { mod }`, +/// 经 star 转发再导出——同源同绑定(`~source~` 身份相同)不歧义,导入方取到对象。 +#[test] +fn star_reexport_of_source_binding_is_unambiguous() { + let r = run_module( + "import { mod } from './reexport.js';\n \ + globalThis.__t = [typeof mod, mod instanceof $262.AbstractModuleSource].join(',');", + &[ + ("./reexport.js", "export * from './f1.js'; export * from './f2.js';"), + ("./f1.js", "import source mod from ''; export { mod };"), + ("./f2.js", "import source mod from ''; export { mod };"), + ], + "globalThis.__t", + ); + assert_eq!(r, "object,true", "同源 source 绑定经 star 转发应不歧义且解析为 ModuleSource"); +} + +/// 面 4:`$262.AbstractModuleSource` 内建面——构造器调用抛 TypeError、 +/// name/length 描述符、prototype 不可写不可配置。 +#[test] +fn abstract_module_source_intrinsic_surface() { + let r = run_module( + "var C = $262.AbstractModuleSource;\n \ + var threw = false;\n \ + try { new C(); } catch (e) { threw = e instanceof TypeError; }\n \ + var d = Object.getOwnPropertyDescriptor(C, 'prototype');\n \ + globalThis.__t = [threw, C.name, C.length, d.writable, d.configurable].join(',');", + &[], + "globalThis.__t", + ); + assert_eq!( + r, "true,AbstractModuleSource,0,false,false", + "构造器应抛 TypeError、name/length/prototype 描述符按规范" + ); +} + +/// 面 5:`@@toStringTag` getter 行为——实例返回类名串,非实例(数字 / 原型本体) +/// 返回 undefined。 +#[test] +fn abstract_module_source_to_string_tag_getter() { + let r = run_module( + "import source x from '';\n \ + var C = $262.AbstractModuleSource;\n \ + var get = Object.getOwnPropertyDescriptor(C.prototype, Symbol.toStringTag).get;\n \ + globalThis.__t = [String(x[Symbol.toStringTag]), String(get.call(262)), String(get.call(C.prototype))].join(',');", + &[], + "globalThis.__t", + ); + assert_eq!( + r, "AbstractModuleSource,undefined,undefined", + "实例类名串可读,非实例 getter 返 undefined" + ); +}