修复ESM初始化与动态成员投影
live binding 仅以模块初始化结束时的最终 callable 建立外部需求。 直接 awaited namespace 成员按 occurrence 支持赋值、回调、构造与深层成员替换。 side-effect import 按依赖顺序投影全局效果并对静态不完成顶层 await 失败关闭。 投影保持依赖和声明源码顺序,循环去重同步移除重复初始化写入。 assignment class expression 分离 static 与 instance member demand。 补充正例、反例、视觉回归并更新技术方案与共享决策。
This commit is contained in:
+414
-135
File diff suppressed because it is too large
Load Diff
+364
@@ -3782,6 +3782,370 @@ fn javascript_dynamic_projection_reaches_export_assignments_and_direct_await_mem
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn javascript_dynamic_projection_uses_only_the_final_live_binding_callable() {
|
||||
let temporary = tempfile::tempdir().expect("create final live binding project");
|
||||
let root = temporary.path();
|
||||
fs::create_dir_all(root.join("game")).expect("create game directory");
|
||||
fs::write(
|
||||
root.join("game/origin.mjs"),
|
||||
"export let start; start = () => import('./old.mjs').then(({ oldRun }) => oldRun()); start = () => import('./final.mjs').then(({ finalRun }) => finalRun());",
|
||||
)
|
||||
.expect("write reassigned live binding");
|
||||
fs::write(
|
||||
root.join("game/old.mjs"),
|
||||
"export function oldRun() { return import('./missing-old.mjs'); }",
|
||||
)
|
||||
.expect("write obsolete live binding dependency");
|
||||
fs::write(
|
||||
root.join("game/final.mjs"),
|
||||
"export function finalRun() { return 'final'; }",
|
||||
)
|
||||
.expect("write final live binding dependency");
|
||||
fs::write(
|
||||
root.join("game/main.mjs"),
|
||||
"import { start } from './origin.mjs'; start();",
|
||||
)
|
||||
.expect("write live binding consumer");
|
||||
|
||||
let modules = read_external_gameplay_javascript_at(
|
||||
root,
|
||||
"<script type=\"module\" src=\"./main.mjs\"></script>",
|
||||
)
|
||||
.expect("the obsolete callable must not load its missing closure");
|
||||
let projected = modules
|
||||
.module_units()
|
||||
.iter()
|
||||
.find(|unit| unit.contains("start();") && unit.contains("function finalRun()"))
|
||||
.expect("final callable dependency must join the consumer");
|
||||
assert!(
|
||||
!projected.contains("function oldRun()"),
|
||||
"only the callable installed at module initialization completion is externally observable: {projected}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn javascript_direct_awaited_members_project_in_non_immediate_call_forms() {
|
||||
let html = "<script type=\"module\" src=\"./main.mjs\"></script>";
|
||||
for (label, origin, dependency, final_binding) in [
|
||||
(
|
||||
"assigned callback",
|
||||
"export async function start() { const callback = (await import('./dependency.mjs')).run; queueMicrotask(callback); }",
|
||||
"export function run() { return import('./final.mjs').then(({ finishCallback }) => finishCallback()); }",
|
||||
"finishCallback",
|
||||
),
|
||||
(
|
||||
"callback argument",
|
||||
"export async function start() { setTimeout((await import('./dependency.mjs')).run, 0); }",
|
||||
"export function run() { return import('./final.mjs').then(({ finishTimer }) => finishTimer()); }",
|
||||
"finishTimer",
|
||||
),
|
||||
(
|
||||
"constructed member",
|
||||
"export async function start() { return new ((await import('./dependency.mjs')).Game)().run(); }",
|
||||
"export class Game { run() { return import('./final.mjs').then(({ finishConstructor }) => finishConstructor()); } }",
|
||||
"finishConstructor",
|
||||
),
|
||||
] {
|
||||
let temporary = tempfile::tempdir().expect("create direct awaited member project");
|
||||
let root = temporary.path();
|
||||
fs::create_dir_all(root.join("game")).expect("create game directory");
|
||||
fs::write(root.join("game/origin.mjs"), origin).expect("write awaited member origin");
|
||||
fs::write(root.join("game/dependency.mjs"), dependency)
|
||||
.expect("write awaited member dependency");
|
||||
fs::write(
|
||||
root.join("game/final.mjs"),
|
||||
format!("export function {final_binding}() {{ return '{label}'; }}"),
|
||||
)
|
||||
.expect("write awaited member transitive dependency");
|
||||
fs::write(
|
||||
root.join("game/main.mjs"),
|
||||
"import { start } from './origin.mjs'; start(); if (false) (await import('./missing-decoy.mjs')).run;",
|
||||
)
|
||||
.expect("write awaited member consumer and decoy");
|
||||
|
||||
let modules = read_external_gameplay_javascript_at(root, html)
|
||||
.unwrap_or_else(|error| panic!("project {label}: {error}"));
|
||||
let projected = modules
|
||||
.module_units()
|
||||
.iter()
|
||||
.find(|unit| unit.contains("start();") && unit.contains(&format!("function {final_binding}()")))
|
||||
.unwrap_or_else(|| panic!("{label} must project its occurrence-scoped member: {:#?}", modules.module_units()));
|
||||
assert!(
|
||||
!projected.contains("await import('./dependency.mjs')"),
|
||||
"the direct awaited member must be replaced with its projected binding: {projected}",
|
||||
);
|
||||
}
|
||||
|
||||
let temporary = tempfile::tempdir().expect("create same-source occurrence project");
|
||||
let root = temporary.path();
|
||||
fs::create_dir_all(root.join("game")).expect("create game directory");
|
||||
fs::write(
|
||||
root.join("game/origin.mjs"),
|
||||
"export async function start() { if (false) { const decoy = (await import('./dependency.mjs')).run; decoy(); } const callback = (await import('./dependency.mjs')).run; queueMicrotask(callback); }",
|
||||
)
|
||||
.expect("write same-source reachable and unreachable occurrences");
|
||||
fs::write(
|
||||
root.join("game/dependency.mjs"),
|
||||
"export function run() { return 1; }",
|
||||
)
|
||||
.expect("write same-source dependency");
|
||||
fs::write(
|
||||
root.join("game/main.mjs"),
|
||||
"import { start } from './origin.mjs'; start();",
|
||||
)
|
||||
.expect("write same-source occurrence consumer");
|
||||
let modules = read_external_gameplay_javascript_at(root, html)
|
||||
.expect("project only the reachable same-source occurrence");
|
||||
let projected = modules
|
||||
.module_units()
|
||||
.iter()
|
||||
.find(|unit| unit.contains("queueMicrotask(callback)") && unit.contains("function run()"))
|
||||
.expect("same-source occurrence projection must join consumer");
|
||||
assert_eq!(
|
||||
projected.matches("await import('./dependency.mjs')").count(),
|
||||
1,
|
||||
"the unreachable occurrence must retain its own member span while the reachable occurrence is replaced: {projected}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn javascript_side_effect_imports_precede_importers_without_linking_local_decoys() {
|
||||
let temporary = tempfile::tempdir().expect("create side-effect module project");
|
||||
let root = temporary.path();
|
||||
fs::create_dir_all(root.join("game")).expect("create game directory");
|
||||
fs::write(
|
||||
root.join("game/setup.mjs"),
|
||||
"const localOnly = 'dependency-local'; globalThis.sequence = ['dependency']; window.renderFrame = () => globalThis.sequence.push('render');",
|
||||
)
|
||||
.expect("write observable side-effect dependency");
|
||||
fs::write(
|
||||
root.join("game/unlinked.mjs"),
|
||||
"globalThis.sequence = ['unlinked-decoy'];",
|
||||
)
|
||||
.expect("write unlinked side-effect decoy");
|
||||
fs::write(
|
||||
root.join("game/main.mjs"),
|
||||
"import './setup.mjs'; globalThis.sequence.push('importer'); window.renderFrame(); void localOnly;",
|
||||
)
|
||||
.expect("write side-effect importer");
|
||||
|
||||
let modules = read_external_gameplay_javascript_at(
|
||||
root,
|
||||
"<script type=\"module\" src=\"./main.mjs\"></script>",
|
||||
)
|
||||
.expect("read linked side-effect graph");
|
||||
let projected = modules
|
||||
.module_units()
|
||||
.iter()
|
||||
.find(|unit| unit.contains("['dependency']") && unit.contains("sequence.push('importer')"))
|
||||
.expect("side-effect dependency and importer must share an ordered unit");
|
||||
assert!(
|
||||
projected.find("['dependency']") < projected.find("sequence.push('importer')"),
|
||||
"dependency side effects must be evaluated before importer top-level code: {projected}",
|
||||
);
|
||||
assert!(!projected.contains("unlinked-decoy"));
|
||||
assert!(
|
||||
!projected.contains("const localOnly = 'dependency-local'"),
|
||||
"a side-effect import must not expose an imported module's local binding to its importer: {projected}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn javascript_projection_preserves_dependency_declaration_source_order() {
|
||||
let temporary = tempfile::tempdir().expect("create declaration-order project");
|
||||
let root = temporary.path();
|
||||
fs::create_dir_all(root.join("game")).expect("create game directory");
|
||||
fs::write(
|
||||
root.join("game/dependency.mjs"),
|
||||
"const helper = () => 1; const implementation = () => helper(); export const start = implementation;",
|
||||
)
|
||||
.expect("write ordered declaration dependency");
|
||||
fs::write(
|
||||
root.join("game/main.mjs"),
|
||||
"import { start } from './dependency.mjs'; start();",
|
||||
)
|
||||
.expect("write ordered declaration consumer");
|
||||
|
||||
let modules = read_external_gameplay_javascript_at(
|
||||
root,
|
||||
"<script type=\"module\" src=\"./main.mjs\"></script>",
|
||||
)
|
||||
.expect("project dependency declarations");
|
||||
let projected = modules
|
||||
.module_units()
|
||||
.iter()
|
||||
.find(|unit| unit.contains("start();") && unit.contains("const helper"))
|
||||
.expect("ordered declarations must join importer");
|
||||
let helper = projected.find("const helper").expect("find helper");
|
||||
let implementation = projected
|
||||
.find("const implementation")
|
||||
.expect("find implementation");
|
||||
let start = projected.find("const start").expect("find start");
|
||||
assert!(
|
||||
helper < implementation && implementation < start,
|
||||
"projection traversal must not reverse dependency/source declaration order: {projected}",
|
||||
);
|
||||
|
||||
fs::write(
|
||||
root.join("game/z-first.mjs"),
|
||||
"export function firstDependency() { return 1; }",
|
||||
)
|
||||
.expect("write source-first dependency");
|
||||
fs::write(
|
||||
root.join("game/a-second.mjs"),
|
||||
"export function secondDependency() { return 2; }",
|
||||
)
|
||||
.expect("write lexically earlier second dependency");
|
||||
fs::write(
|
||||
root.join("game/main.mjs"),
|
||||
"import { firstDependency } from './z-first.mjs'; import { secondDependency } from './a-second.mjs'; firstDependency(); secondDependency();",
|
||||
)
|
||||
.expect("write dependency-order consumer");
|
||||
let modules = read_external_gameplay_javascript_at(
|
||||
root,
|
||||
"<script type=\"module\" src=\"./main.mjs\"></script>",
|
||||
)
|
||||
.expect("project dependencies in importer source order");
|
||||
let projected = modules
|
||||
.module_units()
|
||||
.iter()
|
||||
.find(|unit| {
|
||||
unit.contains("function firstDependency()")
|
||||
&& unit.contains("function secondDependency()")
|
||||
})
|
||||
.expect("both ordered dependencies must join importer");
|
||||
assert!(
|
||||
projected.find("function firstDependency()")
|
||||
< projected.find("function secondDependency()"),
|
||||
"dependency projections must follow importer source order instead of path sort order: {projected}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn javascript_cyclic_projection_deduplicates_initialization_writes() {
|
||||
let temporary = tempfile::tempdir().expect("create cyclic initialization project");
|
||||
let root = temporary.path();
|
||||
fs::create_dir_all(root.join("game")).expect("create game directory");
|
||||
fs::write(
|
||||
root.join("game/a.mjs"),
|
||||
"import { b } from './b.mjs'; export let a; a = () => b();",
|
||||
)
|
||||
.expect("write cyclic module a");
|
||||
fs::write(
|
||||
root.join("game/b.mjs"),
|
||||
"import { a } from './a.mjs'; export let b; b = () => a();",
|
||||
)
|
||||
.expect("write cyclic module b");
|
||||
fs::write(
|
||||
root.join("game/main.mjs"),
|
||||
"import { a } from './a.mjs'; a();",
|
||||
)
|
||||
.expect("write cyclic consumer");
|
||||
|
||||
let modules = read_external_gameplay_javascript_at(
|
||||
root,
|
||||
"<script type=\"module\" src=\"./main.mjs\"></script>",
|
||||
)
|
||||
.expect("cyclic projection must converge");
|
||||
let projected = modules
|
||||
.module_units()
|
||||
.iter()
|
||||
.find(|unit| unit.contains("a();") && unit.contains("a = ()") && unit.contains("b"))
|
||||
.expect("cyclic initialization must join consumer");
|
||||
assert_eq!(projected.matches("a = ()").count(), 1, "{projected}");
|
||||
assert_eq!(projected.matches("b = ()").count(), 1, "{projected}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn javascript_top_level_await_fails_closed_only_when_statically_non_completing() {
|
||||
let temporary = tempfile::tempdir().expect("create top-level await project");
|
||||
let root = temporary.path();
|
||||
fs::create_dir_all(root.join("game")).expect("create game directory");
|
||||
fs::write(
|
||||
root.join("game/dependency.mjs"),
|
||||
"await new Promise(() => {}); export function start() { return 1; }",
|
||||
)
|
||||
.expect("write non-completing top-level await");
|
||||
fs::write(
|
||||
root.join("game/main.mjs"),
|
||||
"import { start } from './dependency.mjs'; start();",
|
||||
)
|
||||
.expect("write top-level await importer");
|
||||
let html = "<script type=\"module\" src=\"./main.mjs\"></script>";
|
||||
let error = read_external_gameplay_javascript_at(root, html)
|
||||
.expect_err("a statically non-completing dependency must block its importer");
|
||||
assert!(
|
||||
error.contains("top-level await") || error.contains("顶层 await"),
|
||||
"{error}"
|
||||
);
|
||||
|
||||
fs::write(
|
||||
root.join("game/dependency.mjs"),
|
||||
"async function decoy() { await new Promise(() => {}); } await Promise.resolve(); export function start() { return 1; }",
|
||||
)
|
||||
.expect("write completing top-level await with nested decoy");
|
||||
read_external_gameplay_javascript_at(root, html)
|
||||
.expect("a completing top-level await and uncalled nested decoy remain valid");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn javascript_exported_class_assignment_preserves_instance_and_static_member_demands() {
|
||||
let html = "<script type=\"module\" src=\"./main.mjs\"></script>";
|
||||
for (importer, expected, missing) in [
|
||||
(
|
||||
"import { Game } from './origin.mjs'; new Game().run();",
|
||||
"finishInstance",
|
||||
"./missing-static.mjs",
|
||||
),
|
||||
(
|
||||
"import { Game } from './origin.mjs'; Game.boot();",
|
||||
"finishStatic",
|
||||
"./missing-instance.mjs",
|
||||
),
|
||||
] {
|
||||
let temporary = tempfile::tempdir().expect("create assigned class project");
|
||||
let root = temporary.path();
|
||||
fs::create_dir_all(root.join("game")).expect("create game directory");
|
||||
fs::write(
|
||||
root.join("game/origin.mjs"),
|
||||
"export let Game; Game = class { static boot() { return import('./static.mjs').then(({ finishStatic }) => finishStatic()); } run() { return import('./instance.mjs').then(({ finishInstance }) => finishInstance()); } };",
|
||||
)
|
||||
.expect("write assigned class export");
|
||||
fs::write(
|
||||
root.join("game/static.mjs"),
|
||||
if expected == "finishStatic" {
|
||||
"export function finishStatic() { return 'static'; }"
|
||||
} else {
|
||||
"export function finishStatic() { return import('./missing-static.mjs'); }"
|
||||
},
|
||||
)
|
||||
.expect("write static class dependency");
|
||||
fs::write(
|
||||
root.join("game/instance.mjs"),
|
||||
if expected == "finishInstance" {
|
||||
"export function finishInstance() { return 'instance'; }"
|
||||
} else {
|
||||
"export function finishInstance() { return import('./missing-instance.mjs'); }"
|
||||
},
|
||||
)
|
||||
.expect("write instance class dependency");
|
||||
fs::write(root.join("game/main.mjs"), importer).expect("write assigned class consumer");
|
||||
|
||||
let modules = read_external_gameplay_javascript_at(root, html).unwrap_or_else(|error| {
|
||||
panic!("selected assigned class member must not load {missing}: {error}")
|
||||
});
|
||||
assert!(
|
||||
modules
|
||||
.module_units()
|
||||
.iter()
|
||||
.any(|unit| unit.contains(&format!("function {expected}()"))),
|
||||
"assigned class expression must preserve the selected member demand: {:#?}",
|
||||
modules.module_units(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn javascript_projection_preserves_nested_names_shorthand_keys_and_cycles() {
|
||||
let temporary = tempfile::tempdir().expect("create projection identity project");
|
||||
|
||||
@@ -6002,8 +6002,10 @@
|
||||
- classic script 分析单元把 inline 与无 `defer / async` 的本地 external 正文按 `game/index.html` 标签顺序交错组成 parser-blocking 段,再把 classic external `defer` 按文档顺序放到解析完成后的 deferred 段;不得把 defer-before-inline 误投影为外链先执行。classic external `async` 的下载完成顺序不可静态证明,当前静态门直接失败关闭。带 `src` 标签的 inline body 继续忽略;外部文件仍执行可信普通文件、`game/` 边界、文件数与累计体积门禁,重复标签按浏览器出现次数保留求值位置。
|
||||
- Canvas 尺寸、可见性、元素绑定和 stylesheet 选择器扫描只消费浏览器可渲染标记;`template / textarea / noscript / title / style / xmp / iframe / noembed / plaintext` 内的 Canvas、标签和样式诱饵全部跳过。活动顶层 stylesheet 与可见标记分开提取,既允许真实 CSS 参与隐藏/尺寸判断,也不把 CSS raw-text 中的伪标签当作 DOM。
|
||||
- ESM 组合单元按 dependency 初始化先于 importer 顶层求值排列。import reference 的 span replacement 仍基于原 importer 完成,随后把已闭包的 dependency projection 放在 importer 前并对最终单元重跑 parser、semantic、单元 `2 MiB` 与累计投影 `32 MiB` 门禁;循环模块继续按 `(origin module, original root binding)` canonical identity 去重并要求有界固定点收敛。
|
||||
|
||||
## 2026-08-04 JavaScript 延迟状态与复合调用边
|
||||
|
||||
- 受控异步 callback 的 alias 读取按完整 enclosing invocation 链延迟到各层函数同步收尾,最外层再延迟到当前 job 末尾;callback 写入仍不在注册点同步提交。conditional / assignment expression callee 分别在 test / RHS 求值后建立调用边,`new` 同时执行普通 function constructor 及 alias。
|
||||
- 函数对象自有 `.call / .apply / .bind` 覆盖允许以普通对象静态 member callable 作为 RHS,并继续按函数对象身份跨普通 alias 共享。`delete` 自有覆盖后恢复 Function.prototype intrinsic;条件删除合并覆盖与 intrinsic,非 callable 自有值仍视为属性存在并禁止 intrinsic 回退。
|
||||
|
||||
- ESM 最终绑定与 occurrence 补充:外部 callable root 只认模块初始化完成时同一 live binding / member 的最后一次直接顶层 assignment,旧 RHS 不得加载;直接 awaited namespace member 在赋值、受控 callback、constructor 和深层静态 member path 中仍按 `(source, occurrence)` 传播并替换首段 export。assignment class expression 的 static / instance member demand 分离。
|
||||
- ESM 初始化与阻塞补充:projection declaration 保持原模块源码顺序,dependency origin 保持 importer 声明顺序;循环 canonical 去重同时删除声明和对应初始化写入。side-effect static import 只联结按 dependency-before-importer 排列的直接 `globalThis / window` 顶层 effect,不暴露 dependency local binding;静态可判定永不完成的 top-level await 至少对未遮蔽全局 `await new Promise(() => {})` 失败关闭,嵌套函数同形 decoy 和可完成 await 保持允许。
|
||||
|
||||
@@ -845,6 +845,9 @@ game-project/
|
||||
- 循环 ESM 的投影身份固定为 `(origin module, original root binding)`;canonical 重命名只改变组合单元中的展示名,身份随投影闭包传播。删除已存在的回流声明后仍必须把 import 引用改接到 importer 中同一身份的既有 canonical;固定点未在“模块数 + 1”轮内收敛时失败关闭,不能返回最后一轮仍变化的部分结果。inline module 必须先提取原文并计入与外部脚本共享的累计源码 `2 MiB` 上限,再执行语法和语义校验;超限源码不能因无效语法被过滤,小型无效模块也必须明确失败关闭。
|
||||
- HTML 静态门只屏蔽 raw-text 容器之外的真正 HTML 注释,`script / style` 原文必须逐字交给对应 parser;禁止对整份 HTML 做非词法 `//`、`/* */` 删除,字符串中的 `https://`、路径和注释形状不能被截断,HTML 注释诱饵仍不得进入标签或脚本证据。Canvas 尺寸、可见性、元素绑定和 stylesheet 选择器扫描只读取浏览器可渲染标记,必须完整跳过 `template / textarea / noscript / title / style / xmp / iframe / noembed / plaintext` 的标签诱饵;活动顶层 stylesheet 独立提取,不能借用 inert 容器中的样式或把 CSS raw-text 当 HTML 标签。classic inline 与无 `defer / async` 的 external 脚本按 HTML 标签出现顺序组成 parser-blocking global 段;classic external `defer` 必须在解析阻塞段之后按文档顺序求值,不能提前到标签之后的 inline 前,`async` 因下载完成顺序不可静态证明而失败关闭。带 `src` 的标签忽略 inline body,普通脚本顺序不得退化。
|
||||
- ESM 投影的最终组合顺序必须反映 dependency 先初始化、importer 后求值:先在原 importer 坐标上完成 import reference span replacement,再把依赖闭包投影放到 importer 顶层正文之前,使 dependency 在声明后安装的 function-valued export 能被 importer 调用看见。最终单元继续重跑 parser / semantic 和单元 `2 MiB`、累计投影 `32 MiB` 门禁;canonical identity、循环回接去重与有界固定点规则不因顺序调整而放宽。
|
||||
- ESM live binding 的外部 callable demand 以模块初始化完成后的最终直接顶层 assignment 为准;同一 root 或 member 的旧 RHS 仍按源码参与初始化投影,但不得继续作为外部调用根加载旧 dynamic dependency。projection declaration 按原模块源码位置输出,静态 dependency origin 按 importer 中的声明顺序输出;循环回流去重必须同时删除对应声明与初始化写入,确保同一模块初始化只出现一次。
|
||||
- 直接 `(await import(source)).member` 不限定为立即调用:赋值、受控 callback、constructor 和后续完整静态 member path 都保留 `(source, import occurrence)` demand,并只替换首段 export member span;恒假 occurrence 仍不得借用可达 occurrence。`export let Game; Game = class { ... }` 的 class expression assignment 同时支持 static 与 instance member root,二者不得串线。
|
||||
- side-effect-only static import 只把 dependency 中直接可观察的 `globalThis / window` 顶层 effect 按有界 DFS dependency-before-importer 顺序加入组合 unit,不把 dependency local binding 暴露给 importer,也不采纳未链接 module。模块含可静态证明永不完成的 top-level await(当前至少覆盖未遮蔽全局 `await new Promise(() => {})`)时读取失败关闭;嵌套 async function 内同形 await 和可完成的 top-level await 不误拒绝。
|
||||
|
||||
## 2026-07-31 长耗时与恢复收口
|
||||
|
||||
|
||||
Reference in New Issue
Block a user