diff --git a/crates/oxide_emit/src/module.rs b/crates/oxide_emit/src/module.rs index b5451268..80f8ed7a 100644 --- a/crates/oxide_emit/src/module.rs +++ b/crates/oxide_emit/src/module.rs @@ -128,11 +128,20 @@ fn is_hoisted_function_decl(stmt: &Statement) -> bool { impl Emitter { /// 编译 ES module:顶层 body + 递归依赖。 - /// `module_path` 为模块文件的规范路径(依赖解析基准 = 其父目录)。 + /// `module_path` 为模块文件路径;入口即归一为规范绝对路径(依赖解析基准 = + /// 其父目录),与加载器 resolve 的绝对规范口径对齐,使自导入身份比较稳定。 pub fn emit_program_module( &self, program: &oxide_parser::Program, module_path: &str, loader: &mut dyn ModuleSourceLoader, ) -> Result { crate::emit_debug!("emit_program_module: {} stmts", program.body.len()); + // 顶层模块路径归一为规范绝对路径:自导入身份比较(resolved.path == + // module_path)与 base_dir 需与加载器 resolve 的 canonicalize 绝对口径 + // 一致——若保留相对发现路径,自导入比较恒 false、自导入被当外部依赖 + // 重编译。文件不存在(虚拟/内存模块)时 canonicalize 失败,保留原路径。 + let canonical = std::fs::canonicalize(module_path) + .map(|p| p.to_string_lossy().into_owned()) + .unwrap_or_else(|_| module_path.to_string()); + let module_path = canonical.as_str(); let mut ctx = CompileCtx::new(); // 模块顶层 var/function 不写全局对象(模块作用域绑定)。 ctx.is_global_scope = false; diff --git a/crates/oxide_vm/tests/self_import_path_tests.rs b/crates/oxide_vm/tests/self_import_path_tests.rs new file mode 100644 index 00000000..ca512778 --- /dev/null +++ b/crates/oxide_vm/tests/self_import_path_tests.rs @@ -0,0 +1,77 @@ +//! 自导入路径口径:顶层模块相对发现路径 vs 加载器 resolve 的 canonicalize 绝对 +//! 路径,二者须同口径,自导入才按自身命名空间绑定、整模块只求值一次。 +//! 误判为外部依赖会把自导入重新编译求值一遍,模块被求值两次。 + +use std::sync::Arc; + +use oxide_compiler::compiler::Compiler; +use oxide_emit::module::{ModuleKind, ModuleSourceLoader, ResolvedModule}; +use oxide_types::value::JsValue; +use oxide_vm::vm::Vm; + +/// 自导入模块源:`import * as self from './selfm.js'` 导入自身,每次求值给 +/// global 计数加一,导出 self 命名空间引用以实化自导入绑定。 +const MODULE_SRC: &str = "import * as self from './selfm.js';\n \ + globalThis.__evals = (globalThis.__evals || 0) + 1;\n \ + export const self_ref = self;"; + +/// 复刻 test262 加载器:base_dir/specifier join 后 canonicalize 为绝对规范路径 +/// 并读源——与顶层相对发现路径构成「绝对 vs 相对」口径差,正是本面复现条件。 +struct AbsLoader; + +impl ModuleSourceLoader for AbsLoader { + fn resolve( + &mut self, base_dir: &str, specifier: &str, _attributes: &[(&str, &str)], + ) -> Result { + let full = std::path::Path::new(base_dir).join(specifier); + let canonical = full.canonicalize().map_err(|e| format!("cannot resolve {specifier}: {e}"))?; + let source = std::fs::read_to_string(&canonical).map_err(|e| format!("cannot read {specifier}: {e}"))?; + Ok(ResolvedModule { + source, + path: canonical.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(vm: &mut Vm, source: &str) -> JsValue { + let allocator = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&allocator, source).expect("parse"); + let module = Compiler::new().compile(&program).expect("compile"); + vm.run(&Arc::new(module)).expect("probe run") +} + +#[test] +fn self_import_module_evaluated_once() { + // 临时模块落在 CWD 相对路径下,使顶层发现路径可为相对形式、canonicalize + // 又可达——与 runner「相对发现路径 + 绝对 resolve」的口径差同构。 + let cwd = std::env::current_dir().expect("cwd"); + let dir = cwd.join("__selfimport_repro__"); + let file = dir.join("selfm.js"); + std::fs::create_dir_all(&dir).expect("mkdir"); + std::fs::write(&file, MODULE_SRC).expect("write module"); + let _cleanup = Cleanup(dir.clone()); + + let allocator = oxide_parser::Allocator::default(); + let source = std::fs::read_to_string(&file).expect("read module"); + let program = oxide_parser::parse_module(&allocator, &source).expect("parse module"); + // 顶层模块路径用相对发现路径(不 canonicalize),复刻 runner 侧口径。 + let module = Compiler::new() + .compile_module(&program, "__selfimport_repro__/selfm.js", &mut AbsLoader) + .expect("compile module"); + + let mut vm = Vm::new(); + vm.run(&Arc::new(module)).expect("module run"); + // 自导入须绑定自身命名空间,整模块只求值一次;误判为外部依赖会二次求值。 + let r = probe(&mut vm, "globalThis.__evals"); + assert!(r.is_int() && r.as_int() == 1, "自导入模块应只求值一次,实际 __evals={r:?}"); +}