fix: 顶层模块求值完成值归空记录,依赖命名空间返回口径不变

This commit is contained in:
2026-09-14 21:20:17 +00:00
parent ca5c27574e
commit 5cfd3162ba
2 changed files with 114 additions and 4 deletions
+16 -4
View File
@@ -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<String>, ctx: &mut CompileCtx,
path_stack: &mut Vec<String>, 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))
}
@@ -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<ResolvedModule, String> {
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:?}");
}