From 5cfd3162bac1ae4844d5e5d9f4defa46a69fb8ba Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Mon, 14 Sep 2026 21:20:17 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=A1=B6=E5=B1=82=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E6=B1=82=E5=80=BC=E5=AE=8C=E6=88=90=E5=80=BC=E5=BD=92=E7=A9=BA?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=EF=BC=8C=E4=BE=9D=E8=B5=96=E5=91=BD=E5=90=8D?= =?UTF-8?q?=E7=A9=BA=E9=97=B4=E8=BF=94=E5=9B=9E=E5=8F=A3=E5=BE=84=E4=B8=8D?= =?UTF-8?q?=E5=8F=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_emit/src/module.rs | 20 +++- .../tests/module_top_completion_tests.rs | 98 +++++++++++++++++++ 2 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 crates/oxide_vm/tests/module_top_completion_tests.rs diff --git a/crates/oxide_emit/src/module.rs b/crates/oxide_emit/src/module.rs index f566f3a9..7b7e05e7 100644 --- a/crates/oxide_emit/src/module.rs +++ b/crates/oxide_emit/src/module.rs @@ -148,15 +148,18 @@ impl Emitter { // ES module 顶层恒严格模式(模块代码是严格模式代码,嵌套函数经父 ctx 继承)。 ctx.is_strict = true; let mut path_stack = vec![module_path.to_string()]; - self.emit_module_into_ctx(program, module_path, loader, &mut path_stack, &mut ctx)?; + self.emit_module_into_ctx(program, module_path, loader, &mut path_stack, &mut ctx, true)?; Ok(ctx.assemble_ir(ParamLayout { base: 0, count: 0 }, None)) } /// 模块体 emit(顶层与依赖模块共用入口)。 + /// + /// `top_level` 区分收尾返回口径:顶层模块求值完成值按规范为空记录 + /// (对外 undefined),依赖模块经 `__moduleEval` 以返回值作命名空间对象。 #[allow(clippy::too_many_arguments)] fn emit_module_into_ctx( &self, program: &oxide_parser::Program, module_path: &str, loader: &mut dyn ModuleSourceLoader, - path_stack: &mut Vec, ctx: &mut CompileCtx, + path_stack: &mut Vec, ctx: &mut CompileCtx, top_level: bool, ) -> Result<(), String> { let body = &program.body; @@ -387,7 +390,16 @@ impl Emitter { // —— 收尾:封冻命名空间并返回(顶层 RETURN 亦终止 run)—— self.emit_module_call(ctx, "__moduleSeal", &[ns_reg])?; - ctx.inst(Inst::ret(Operand::Reg(ns_reg), 0, 0)); + if top_level { + // 顶层模块求值完成值为空记录:对外表现 undefined(依赖面不得 + // 同口径——__moduleEval 以依赖返回值作命名空间,contract 不可破)。 + let undef_idx = ctx.add_constant(Constant::Undefined); + let r = ctx.alloc_reg(); + ctx.inst(Inst::load_const(Operand::Reg(r), undef_idx)); + ctx.inst(Inst::ret(Operand::Reg(r), 0, 0)); + } else { + ctx.inst(Inst::ret(Operand::Reg(ns_reg), 0, 0)); + } Ok(()) } @@ -406,7 +418,7 @@ impl Emitter { ctx.is_global_scope = false; // 依赖模块经 parse_module 解析,顶层恒严格模式。 ctx.is_strict = true; - self.emit_module_into_ctx(&program, &resolved.path, loader, path_stack, &mut ctx)?; + self.emit_module_into_ctx(&program, &resolved.path, loader, path_stack, &mut ctx, false)?; Ok(ctx.assemble_ir(ParamLayout { base: 0, count: 0 }, None)) } diff --git a/crates/oxide_vm/tests/module_top_completion_tests.rs b/crates/oxide_vm/tests/module_top_completion_tests.rs new file mode 100644 index 00000000..017eb64b --- /dev/null +++ b/crates/oxide_vm/tests/module_top_completion_tests.rs @@ -0,0 +1,98 @@ +//! 模块顶层完成值口径:顶层模块求值完成值为空记录(run 返回 undefined), +//! 依赖模块仍经 __moduleEval 以返回值作命名空间对象(contract 不可破)。 + +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; + +/// 文件加载器:base_dir/specifier join 后读源(依赖模块经真实文件解析)。 +struct FileLoader; + +impl ModuleSourceLoader for FileLoader { + fn resolve( + &mut self, base_dir: &str, specifier: &str, _attributes: &[(&str, &str)], + ) -> Result { + let full = std::path::Path::new(base_dir).join(specifier); + 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(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 top_level_module_run_returns_undefined() { + // 顶层模块求值完成值按规范为空记录:对外表现 undefined,不再是命名空间对象。 + let allocator = oxide_parser::Allocator::default(); + let program = oxide_parser::parse_module(&allocator, "export const a = 1; 5").expect("parse module"); + let module = Compiler::new() + .compile_module(&program, "test.mjs", &mut FileLoader) + .expect("compile module"); + let mut vm = Vm::new(); + let result = vm.run(&Arc::new(module)).expect("module run"); + assert!(result.is_undefined(), "顶层模块 run 完成值应为 undefined,实际 {result:?}"); +} + +#[test] +fn top_level_module_without_exports_run_returns_undefined() { + // 无导出顶层模块(空命名空间)同口径:run 返 undefined。 + let allocator = oxide_parser::Allocator::default(); + let program = oxide_parser::parse_module(&allocator, "const x = 42;").expect("parse module"); + let module = Compiler::new() + .compile_module(&program, "test.mjs", &mut FileLoader) + .expect("compile module"); + let mut vm = Vm::new(); + let result = vm.run(&Arc::new(module)).expect("module run"); + assert!(result.is_undefined(), "顶层模块 run 完成值应为 undefined,实际 {result:?}"); +} + +#[test] +fn dependent_module_namespace_contract_intact() { + // 依赖模块返回值仍被 __moduleEval 作命名空间消费:导入绑定值经依赖 + // 命名空间正确流入顶层模块(顶层 run 完成值归 undefined 不波及依赖面)。 + let cwd = std::env::current_dir().expect("cwd"); + let dir = cwd.join("__module_top_completion__"); + std::fs::create_dir_all(&dir).expect("mkdir"); + std::fs::write(dir.join("dep.mjs"), "export const v = 7; export function f() { return 'f'; }").expect("write dep"); + std::fs::write( + dir.join("top.mjs"), + "import { v, f } from './dep.mjs';\n \ + globalThis.__dep = v === 7 && f() === 'f';\n \ + export const w = 9;", + ) + .expect("write top"); + let _cleanup = Cleanup(dir.clone()); + + let allocator = oxide_parser::Allocator::default(); + let source = std::fs::read_to_string(dir.join("top.mjs")).expect("read top"); + let program = oxide_parser::parse_module(&allocator, &source).expect("parse module"); + let module = Compiler::new() + .compile_module(&program, dir.join("top.mjs").to_string_lossy().as_ref(), &mut FileLoader) + .expect("compile module"); + let mut vm = Vm::new(); + vm.run(&Arc::new(module)).expect("module run"); + // 依赖命名空间 contract:导入值经依赖求值返回值正确流入顶层模块作用域。 + let r = probe(&mut vm, "globalThis.__dep === true"); + assert!(r.is_bool() && r.as_bool(), "依赖命名空间值应正确流入,实际 {r:?}"); +}