Files
OxideJS-New/crates/oxide_emit/tests/module_link_tests.rs
T

476 lines
23 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 模块链接面的 IR 级断言:内存版 `ModuleSourceLoader` +
//! `emit_program_module`,钉死 import prelude / export 就地注册 / star 再导出 /
//! 循环导入编译期报错 / 数据模块 / defer 依赖(不求值、mixed eager 序、
//! 自导入与祖先回边哨兵)的现状行为。
//!
//! 路径口径:加载器 `path` 回填表键原样字符串,与入口 `module_path` 入参逐字符
//! 一致(虚拟路径 canonicalize 失败保留原路径),自导入身份比较与循环检测依赖
//! 此口径。断言不依赖寄存器号与指令序。
use std::collections::HashMap;
use std::collections::HashSet;
use oxide_bytecode::opcode::OpCode;
use oxide_emit::module::{ModuleKind, ModuleSourceLoader, ResolvedModule};
use oxide_emit::{Constant, Emitter};
use oxide_ir::IRFunction;
/// 内存版加载器:specifier 精确查表;`path` 原样回填表键。
/// `resolved` 记录每次 resolve 的 (specifier, 属性对),供链接期观察。
struct MemLoader {
table: HashMap<String, (ModuleKind, String)>,
resolved: Vec<(String, Vec<(String, String)>)>,
}
impl ModuleSourceLoader for MemLoader {
fn resolve(
&mut self, _base_dir: &str, specifier: &str, attributes: &[(&str, &str)],
) -> Result<ResolvedModule, String> {
self.resolved.push((
specifier.to_string(),
attributes.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(),
));
self.table
.get(specifier)
.cloned()
.map(|(kind, source)| ResolvedModule {
source,
path: specifier.to_string(),
kind,
})
.ok_or_else(|| format!("MemLoader 无 {specifier}"))
}
}
/// 夹具:parse_module + emit_program_module,返回 IR 与加载器。
fn emit_module(src: &str, path: &str, deps: &[(&str, ModuleKind, &str)]) -> Result<(IRFunction, MemLoader), String> {
let alloc = oxide_parser::Allocator::default();
let program =
oxide_parser::parse_module(&alloc, src).map_err(|errs| format!("parse_module 失败 {src:?}: {errs:?}"))?;
let mut loader = MemLoader {
table: deps.iter().map(|(s, k, t)| (s.to_string(), (*k, t.to_string()))).collect(),
resolved: Vec::new(),
};
let ir = Emitter::new().emit_program_module(&program, path, &mut loader)?;
Ok((ir, loader))
}
fn builtin_names(ir: &IRFunction) -> HashSet<String> {
ir.builtin_reg_map.iter().map(|(n, _)| n.clone()).collect()
}
fn builtin_reg(ir: &IRFunction, name: &str) -> Option<u32> {
ir.builtin_reg_map.iter().find(|(n, _)| n == name).map(|(_, r)| *r)
}
/// 以某 built-in 为 callee(rd 槽)的 CALL_NATIVE 计数。
fn native_calls_to(ir: &IRFunction, name: &str) -> usize {
let Some(reg) = builtin_reg(ir, name) else {
return 0;
};
ir.insts
.iter()
.filter(|i| i.op == OpCode::CALL_NATIVE && matches!(i.rd, oxide_ir::operand::Operand::Reg(r) if r == reg))
.count()
}
fn pool_strings(ir: &IRFunction) -> HashSet<String> {
ir.constants
.iter()
.filter_map(|c| if let Constant::String(s) = c { Some(s.clone()) } else { None })
.collect()
}
/// 在顶层 nested 中按 `function_name` 查找模块。
fn named_module<'a>(ir: &'a IRFunction, name: &str) -> Option<&'a IRFunction> {
ir.nested.iter().find(|m| m.function_name.as_deref() == Some(name))
}
/// 面 1:import 绑定 prelude 经 `__moduleLinkGet` 链接,依赖模块编入子模块树。
#[test]
fn import_binding_prelude() {
let (ir, _) = emit_module(
"import { x } from \"./dep.js\"; var y = x;",
"./entry.js",
&[("./dep.js", ModuleKind::Js, "export var x = 42;")],
)
.expect("模块编译应成功");
assert!(builtin_names(&ir).contains("__moduleLinkGet"), "prelude 应链接 __moduleLinkGet");
assert!(native_calls_to(&ir, "__moduleLinkGet") >= 1, "命名导入应发 __moduleLinkGet 调用");
assert_eq!(ir.nested.len(), 1, "依赖模块应编入子模块树");
let dep = &ir.nested[0];
assert!(!dep.insts.is_empty());
assert!(builtin_names(dep).contains("__moduleSet"), "依赖的 export 应就地注册");
}
/// 面 2:export 声明就地注册导出值到命名空间(`__moduleSet` 逐名成对)。
#[test]
fn export_in_place_register() {
let (ir, _) = emit_module("export function f() {} export const c = 2;", "./entry.js", &[]).expect("模块编译应成功");
assert!(builtin_names(&ir).contains("__moduleSet"));
assert_eq!(native_calls_to(&ir, "__moduleSet"), 2, "f 与 c 各注册一次");
let pool = pool_strings(&ir);
assert!(pool.contains("f") && pool.contains("c"), "导出名应成对入池: {pool:?}");
}
/// 面 3:`export * from` 匿名 star 再导出经 `__moduleStar` 整体转发(现状口径)。
#[test]
fn export_all_from() {
let (ir, _) = emit_module(
"export * from \"./dep.js\";",
"./entry.js",
&[("./dep.js", ModuleKind::Js, "export var z = 1;")],
)
.expect("star 再导出应编译通过");
assert!(builtin_names(&ir).contains("__moduleStar"), "匿名 star 应走 __moduleStar");
assert_eq!(ir.nested.len(), 1);
}
/// 面 4:循环导入在编译期报错,错误文本即循环检测点产物。
#[test]
fn circular_import_compile_error() {
let src_a = "import { y } from \"./b.js\"; export var x = 1;";
let result = emit_module(
src_a,
"./a.js",
&[
("./a.js", ModuleKind::Js, src_a),
("./b.js", ModuleKind::Js, "import { x } from \"./a.js\"; export var y = 2;"),
],
);
assert_eq!(
result.err().as_deref(),
Some("circular module import not supported: ./a.js"),
"Err 应来自循环检测点(自导入身份比较命中)"
);
}
/// 面 5:json 数据模块经 `__moduleData` 物化,数据源文本入子模块常量池。
#[test]
fn json_data_module() {
let json = r#"{"k":1}"#;
let (ir, loader) = emit_module(
"import data from \"./d.json\" assert { type: \"json\" }; data;",
"./entry.js",
&[("./d.json", ModuleKind::Json, json)],
)
.expect("数据模块编译应成功");
// import 属性应原样传给加载器。
let attrs = loader
.resolved
.iter()
.find(|(s, _)| s == "./d.json")
.map(|(_, a)| a.clone())
.expect("d.json 应被 resolve");
assert_eq!(attrs, vec![("type".to_string(), "json".to_string())]);
assert_eq!(ir.nested.len(), 1);
let data = &ir.nested[0];
assert!(builtin_names(data).contains("__moduleData"), "数据模块应链接 __moduleData");
let pool = pool_strings(data);
assert!(pool.contains("json") && pool.contains(json), "json 源文本应入池: {pool:?}");
}
/// 面 6:`export default` 匿名函数/生成器/箭头/类经 SetFunctionName 落隐式名 "default"。
/// 带方法的类必须命中构造器,而不是构造器之后 push 的方法子模块。
#[test]
fn export_default_anonymous_implicit_name() {
let (ir, _) = emit_module("export default function() {}", "./entry.js", &[]).expect("匿名函数声明应编译成功");
assert!(named_module(&ir, "default").is_some(), "匿名函数声明应落隐式名");
let (ir, _) = emit_module("export default function* () {}", "./entry.js", &[]).expect("匿名生成器声明应编译成功");
assert!(named_module(&ir, "default").is_some_and(|m| m.is_generator), "匿名生成器应落隐式名");
let (ir, _) = emit_module("export default (function() {})", "./entry.js", &[]).expect("括号函数表达式应编译成功");
assert!(named_module(&ir, "default").is_some(), "匿名函数表达式应落隐式名");
let (ir, _) = emit_module("export default (() => {})", "./entry.js", &[]).expect("箭头表达式应编译成功");
assert!(named_module(&ir, "default").is_some_and(|m| m.is_arrow), "匿名箭头应落隐式名");
let (ir, _) = emit_module("export default class { m() {} }", "./entry.js", &[]).expect("匿名类声明应编译成功");
let ctor = ir.nested.iter().find(|m| m.is_class_constructor).expect("类构造器应在 nested");
assert_eq!(ctor.function_name.as_deref(), Some("default"), "匿名类构造器应落隐式名");
assert!(named_module(&ir, "m").is_some(), "方法模块应保留自身名");
let (ir, _) = emit_module("export default (class { m() {} })", "./entry.js", &[]).expect("括号类表达式应编译成功");
let ctor = ir.nested.iter().find(|m| m.is_class_constructor).expect("类构造器应在 nested");
assert_eq!(ctor.function_name.as_deref(), Some("default"), "匿名类表达式构造器应落隐式名");
assert!(named_module(&ir, "m").is_some(), "方法模块应保留自身名");
}
/// 面 7:`export default` 具名变体与非常量表达式不被隐式名覆盖。
#[test]
fn export_default_implicit_name_named_unchanged() {
let (ir, _) = emit_module("export default class Foo { m() {} }", "./entry.js", &[]).expect("具名类声明应编译成功");
let ctor = ir.nested.iter().find(|m| m.is_class_constructor).expect("类构造器应在 nested");
assert_eq!(ctor.function_name.as_deref(), Some("Foo"));
let (ir, _) =
emit_module("export default (class Bar { m() {} })", "./entry.js", &[]).expect("具名类表达式应编译成功");
let ctor = ir.nested.iter().find(|m| m.is_class_constructor).expect("类构造器应在 nested");
assert_eq!(ctor.function_name.as_deref(), Some("Bar"));
let (ir, _) = emit_module("export default (function f() {})", "./entry.js", &[]).expect("具名函数表达式应编译成功");
assert!(named_module(&ir, "f").is_some(), "具名函数表达式应保留自身名");
let (ir, _) = emit_module("export default 42", "./entry.js", &[]).expect("字面量应编译成功");
assert!(named_module(&ir, "default").is_none(), "非函数值不应注入隐式名");
}
/// STORE_VAR 指令计数(合成绑定惰性化守卫用)。
fn store_var_count(ir: &IRFunction) -> usize {
ir.insts.iter().filter(|i| i.op == OpCode::STORE_VAR).count()
}
/// 面 8:无 self-import 的 default 表达式模块零 diff。
/// `export default 42` 不建合成 `*default*` 绑定,故除 export 注册外无任何
/// STORE_VAR(字面量经 LOAD_CONST 直接注册)。若合成绑定被无条件建立,会多出
/// 一条 STORE_VAR,本守卫即失败。
#[test]
fn default_expression_module_has_no_synthetic_binding() {
let (ir, _) = emit_module("export default 42", "./entry.js", &[]).expect("字面量应编译成功");
assert_eq!(store_var_count(&ir), 0, "default 表达式模块不应建合成绑定槽");
assert_eq!(native_calls_to(&ir, "__moduleSet"), 1, "default 只注册一次导出");
}
/// 面 8b:无 self-import 的 `export let x = 1` 不因 default 面多出槽位/指令。
/// 该模块唯一 STORE_VAR 是 x 自身的声明初始化;零 diff 守卫要求不出现第二个槽。
#[test]
fn named_export_without_self_import_keeps_single_store() {
let (ir, _) = emit_module("export let x = 1;", "./entry.js", &[]).expect("导出声明应编译成功");
assert_eq!(store_var_count(&ir), 1, "x 声明只应有一条 STORE_VAR");
assert_eq!(native_calls_to(&ir, "__moduleSet"), 1, "x 只注册一次导出");
}
/// 面 8c:无 source 再导出 `export { x }` 不引入合成绑定。
/// 局部 x 由普通声明承载,再导出只读值注册;无 self-import 时零额外 STORE_VAR。
#[test]
fn local_reexport_without_self_import_keeps_single_store() {
let (ir, _) = emit_module("let x = 1; export { x };", "./entry.js", &[]).expect("本地再导出应编译成功");
assert_eq!(store_var_count(&ir), 1, "x 声明只应有一条 STORE_VAR");
}
/// 面 9:self-import 别名以源绑定槽位承载,导出注册不再回写占位槽。
/// `export default 42` 有 self-import default 时合成绑定存在,但别名与源共享
/// 槽位:export 语句只发一次 LOAD_CONST + `__moduleSet`,无额外的回写 STORE_VAR。
#[test]
fn self_import_default_alias_has_no_placeholder_writeback() {
let (ir, _) = emit_module(
"import d from './self.js'; export default 42;",
"./self.js",
&[("./self.js", ModuleKind::Js, "export default 42;")],
)
.expect("self-import default 应编译成功");
assert_eq!(native_calls_to(&ir, "__moduleSet"), 1, "default 只注册一次导出");
// 别名与合成源绑定共享槽位:仅 default 表达式的绑定写入一条 STORE_VAR,
// 不出现旧占位路径的额外回写。
assert_eq!(store_var_count(&ir), 1, "不应出现占位回写带来的第二条 STORE_VAR");
}
/// 面 10:零 diff 守卫 G1——非 live 模块不发射 `__modulePreRegister`。
/// live 门控为自导入 `import * as ns from './self'`;纯导出、default 表达式与
/// 普通外部命名空间导入均不得出现预注册调用。
#[test]
fn no_pre_register_for_non_live_modules() {
let (ir, _) = emit_module("export let x = 1;", "./entry.js", &[]).expect("导出声明应编译成功");
assert_eq!(native_calls_to(&ir, "__modulePreRegister"), 0, "纯导出模块不得预注册");
let (ir, _) = emit_module("export default 42;", "./entry.js", &[]).expect("default 应编译成功");
assert_eq!(native_calls_to(&ir, "__modulePreRegister"), 0, "default 表达式模块不得预注册");
let (ir, _) = emit_module(
"import * as ns from './dep.js'; ns;",
"./entry.js",
&[("./dep.js", ModuleKind::Js, "export var z = 1;")],
)
.expect("外部命名空间导入应编译成功");
assert_eq!(native_calls_to(&ir, "__modulePreRegister"), 0, "外部 ns 导入不得预注册");
}
/// 面 10b:live 模块按静态导出名逐个预注册,`__moduleObject` 仍为零参调用。
#[test]
fn pre_register_for_self_namespace_import() {
let (ir, _) = emit_module(
"import * as ns from './self.js'; export const a = 1; export default 2;",
"./self.js",
&[("./self.js", ModuleKind::Js, "export const a = 1;")],
)
.expect("self-ns 模块应编译成功");
assert_eq!(native_calls_to(&ir, "__modulePreRegister"), 2, "a 与 default 各预注册一次");
assert!(pool_strings(&ir).contains("a") && pool_strings(&ir).contains("default"), "导出名应入池");
// __moduleObject 保持零实参签名:仅一次调用且压缩实参计数为 0。
let reg = builtin_reg(&ir, "__moduleObject").expect("应链接 __moduleObject");
let object_calls: Vec<_> = ir
.insts
.iter()
.filter(|i| i.op == OpCode::CALL_NATIVE && matches!(i.rd, oxide_ir::operand::Operand::Reg(r) if r == reg))
.collect();
assert_eq!(object_calls.len(), 1, "__moduleObject 只应调用一次");
assert_eq!(object_calls[0].ext.first().copied(), Some(0), "__moduleObject 应保持零实参");
}
/// 面 11:写穿发射门控——live 模块顶层重赋按导出名集合追加 `__moduleSet`;
/// 非 live 模块(无自导入 ns)重赋不得产生任何额外注册,IR 零变化。
#[test]
fn write_through_only_for_live_modules() {
// 一个源绑定 a 背两个导出名 a/b:export 声明注册一次、再导出注册一次,
// a = 2 的写穿各写 a、b 一次,共 4 次 `__moduleSet`。
let (ir, _) = emit_module(
"import * as ns from './self.js'; export let a = 1; export { a as b }; a = 2;",
"./self.js",
&[("./self.js", ModuleKind::Js, "export let a = 1;")],
)
.expect("live 模块应编译成功");
assert_eq!(native_calls_to(&ir, "__moduleSet"), 4, "live 模块重赋应写穿两个导出名");
// 非 live:重赋只写绑定槽,不得追加 `__moduleSet`。
let (ir, _) = emit_module("export let a = 1; a = 2;", "./entry.js", &[]).expect("非 live 模块应编译成功");
assert_eq!(native_calls_to(&ir, "__moduleSet"), 1, "非 live 模块重赋不得写穿");
}
/// 面 12:可重赋依赖激活活读与 Cell——依赖预注册并挂共享 cell,导入方顶层读走
/// `__moduleGet`;const-only 依赖零变化,导入方仍走链接期快照、依赖不预注册。
#[test]
fn reassignable_dep_activates_live_read_and_cell() {
let (ir, _) = emit_module(
"import { x } from './dep.js'; x;",
"./entry.js",
&[("./dep.js", ModuleKind::Js, "export const x = 1;")],
)
.expect("const-only 依赖应编译成功");
assert_eq!(native_calls_to(&ir, "__moduleGet"), 0, "const-only 依赖导入方不得活读");
assert!(native_calls_to(&ir, "__moduleLinkGet") >= 1, "导入绑定仍走链接期快照");
let dep = &ir.nested[0];
assert_eq!(native_calls_to(dep, "__modulePreRegister"), 0, "const-only 依赖不得预注册");
assert_eq!(native_calls_to(dep, "__moduleSetCell"), 0, "const-only 依赖不得挂 cell");
let (ir, _) = emit_module(
"import { x } from './dep.js'; x;",
"./entry.js",
&[(
"./dep.js",
ModuleKind::Js,
"var x = 1; export { x }; globalThis.f = function(){ x = 2; };",
)],
)
.expect("可重赋依赖应编译成功");
let dep = &ir.nested[0];
assert!(native_calls_to(dep, "__modulePreRegister") >= 1, "可重赋依赖应预注册导出");
assert!(native_calls_to(dep, "__moduleSetCell") >= 1, "被捕获导出应挂共享 cell");
assert!(native_calls_to(&ir, "__moduleGet") >= 1, "导入方顶层读应活读");
}
/// 面 13:入口模块的可重赋导出不激活预注册(`module_live_dep` 仅依赖模块成立)。
#[test]
fn entry_module_reassignable_export_not_pre_registered() {
let (ir, _) = emit_module("export let x = 1; x = 2;", "./entry.js", &[]).expect("入口模块应编译成功");
assert_eq!(native_calls_to(&ir, "__modulePreRegister"), 0, "入口模块不得预注册");
assert_eq!(native_calls_to(&ir, "__moduleSetCell"), 0, "入口模块不得挂 cell");
}
/// 面 14:defer-only 依赖不求值——无 `__moduleEval`,发 `__moduleDeferObject`
/// 构造 deferred 对象,逐名 `__modulePreRegister` 预注册静态导出名。
#[test]
fn defer_only_dep_not_evaluated() {
let (ir, _) = emit_module(
"import defer * as ns from \"./dep.js\"; ns;",
"./entry.js",
&[("./dep.js", ModuleKind::Js, "export var a = 1; export const b = 2;")],
)
.expect("defer-only 依赖应编译成功");
assert_eq!(native_calls_to(&ir, "__moduleEval"), 0, "defer-only 依赖不得求值");
assert_eq!(native_calls_to(&ir, "__moduleDeferObject"), 1, "deferred 对象应构造一次");
assert_eq!(native_calls_to(&ir, "__modulePreRegister"), 2, "a 与 b 各预注册一次");
let pool = pool_strings(&ir);
assert!(pool.contains("a") && pool.contains("b"), "导出名应入池: {pool:?}");
assert_eq!(ir.nested.len(), 1, "依赖模块应编入子模块树");
}
/// 面 15:mixed 依赖按 eager 语句序求值,defer 语句不计入求值序。
/// 入口的 eager 序为 [setup, dep-2, dep-1](dep-1 为 mixed);dep-1 内的
/// defer-only dep-1.2 不求值。
#[test]
fn mixed_dep_evaluates_in_eager_order() {
let (ir, _) = emit_module(
"import \"./setup.js\"; import defer * as ns1 from \"./dep-1.js\"; import \"./dep-2.js\"; import \"./dep-1.js\";",
"./entry.js",
&[
("./setup.js", ModuleKind::Js, "globalThis.evaluations = [];"),
(
"./dep-1.js",
ModuleKind::Js,
"import \"./dep-1.1.js\"; import defer * as ns_1_2 from \"./dep-1.2.js\"; globalThis.evaluations.push(1); export { ns_1_2 };",
),
("./dep-2.js", ModuleKind::Js, "globalThis.evaluations.push(2);"),
("./dep-1.1.js", ModuleKind::Js, "globalThis.evaluations.push(1.1);"),
("./dep-1.2.js", ModuleKind::Js, "globalThis.evaluations.push(1.2);"),
],
)
.expect("mixed 依赖应编译成功");
// 入口:3 个 eager 依赖(setup, dep-2, dep-1)各求值一次。
assert_eq!(native_calls_to(&ir, "__moduleEval"), 3, "入口应求值 3 个 eager 依赖");
// 入口:dep-1 为 mixed,deferred 对象构造一次。
assert_eq!(native_calls_to(&ir, "__moduleDeferObject"), 1, "mixed dep-1 应构造 deferred 对象一次");
// dep-1 是第 3 个 nested 模块(eager_order = [setup, dep-2, dep-1])。
let dep1 = &ir.nested[2];
// dep-1:1 个 eager 依赖(dep-1.1)求值,defer-only 依赖(dep-1.2)不求值。
assert_eq!(native_calls_to(dep1, "__moduleEval"), 1, "dep-1 应只求值 dep-1.1");
// dep-1:dep-1.2 为 defer-only,deferred 对象构造一次。
assert_eq!(native_calls_to(dep1, "__moduleDeferObject"), 1, "dep-1.2 应构造 deferred 对象一次");
// dep-1.2 是 dep-1 的第 2 个 nested 模块(eager_order = [dep-1.1], defer_order = [dep-1.1, dep-1.2])。
let dep12 = &dep1.nested[1];
assert_eq!(native_calls_to(dep12, "__moduleEval"), 0, "dep-1.2 不得求值");
}
/// 面 16:自导入 defer 走哨兵分支——不递归、无环检测 Err、发
/// `__moduleDeferObject(undefined, undefined)`。
#[test]
fn self_import_defer_sentinel() {
let (ir, _) = emit_module(
"import defer * as self from \"./self.js\"; self;",
"./self.js",
&[("./self.js", ModuleKind::Js, "export var foo = 1;")],
)
.expect("自导入 defer 应编译成功");
assert_eq!(native_calls_to(&ir, "__moduleEval"), 0, "自导入 defer 不得求值");
assert_eq!(native_calls_to(&ir, "__moduleDeferObject"), 1, "哨兵 deferred 对象应构造一次");
assert_eq!(ir.nested.len(), 0, "自导入 defer 不得递归");
}
/// 面 17:祖先 defer 回边(dep → main)走哨兵分支——编译成功、无环检测 Err、
/// 不递归。main eager 导入 dep,dep defer 导入 main(导入图含环)。
#[test]
fn ancestor_defer_back_edge_sentinel() {
let main_src = "import \"./dep.js\"; globalThis.result = 1;";
let dep_src = "import defer * as main from \"./main.js\"; main.foo;";
let (ir, _) = emit_module(
main_src,
"./main.js",
&[("./main.js", ModuleKind::Js, main_src), ("./dep.js", ModuleKind::Js, dep_src)],
)
.expect("祖先 defer 回边应编译成功");
// main:1 个 eager 依赖(dep)求值。
assert_eq!(native_calls_to(&ir, "__moduleEval"), 1, "main 应求值 dep");
// main:无 defer,无 deferred 对象。
assert_eq!(native_calls_to(&ir, "__moduleDeferObject"), 0, "main 不得构造 deferred 对象");
// dep(nested):1 个 defer-only 依赖(main)哨兵,不求值。
let dep = &ir.nested[0];
assert_eq!(native_calls_to(dep, "__moduleEval"), 0, "dep 不得求值 main");
assert_eq!(native_calls_to(dep, "__moduleDeferObject"), 1, "dep 应为 main 构造哨兵 deferred 对象");
// dep:不递归(main 未编入 dep 的 nested)。
assert_eq!(dep.nested.len(), 0, "dep 不得递归进 main");
}
/// 面 18:零 diff 守卫——非 defer 模块无 `__moduleDeferObject`、无依赖预注册。
#[test]
fn no_defer_object_for_non_defer_modules() {
let (ir, _) = emit_module(
"import * as ns from \"./dep.js\"; ns;",
"./entry.js",
&[("./dep.js", ModuleKind::Js, "export var z = 1;")],
)
.expect("非 defer 模块应编译成功");
assert_eq!(native_calls_to(&ir, "__moduleDeferObject"), 0, "非 defer 模块不得构造 deferred 对象");
assert_eq!(native_calls_to(&ir, "__modulePreRegister"), 0, "非 defer 模块不得预注册依赖");
}