2026.9.8.1: a package's host modules are ordered by what they import, and the design document aligned with what shipped - #589
Merged
Conversation
Sunrisepeak
force-pushed
the
feat/compile-input-declared-dependency
branch
from
September 7, 2026 23:35
84f3680 to
2727444
Compare
一个 host-module 包贡献的那些单元,过去按**路径**排序:lib root 在前,其余按
`std::set<path>` 的字母序。而 `build_program.cppm` 是一边编译一边累积模块旗标的,
每个单元只看得见排在它前面的那些 BMI。于是 `rules/spirv.cppm` 排在
`src/declare.cppm` 之前,一个 import 了本包共享单元的成员先被编译,失败于:
failed to read compiled module: No such file or directory
note: imports must be built before being imported
**两个方向都复现过**:把共享单元改个名让它的路径排在前面,同一个包就构建通过。所以
成因是那次排序,不是别的。
它的代价不是一次失败,而是一个基于误读的设计决定
------------------------------------------------
`mcpp:plugins` 把这个失败读成「第二个单元根本不会被编译成 host module」,于是把成员
共享的一切都折进 lib root,让它从约二十行涨到约七百行(该包设计文档 §13.1.1 把这笔
代价记了下来,连同那个错误的成因)。真正的成因是一次排序。
修法
----
按 import 图拓扑排序,并以**路径序做稳定次序**:没有包内 import 时结果与今天逐字相同,
只有在今天已经坏掉的情形下才不同 —— 对既有包严格更安全。
`declared_imports` 与它旁边的 `declared_interface_name` 同形:按行、认注释、刻意浅。
它回答的是「本包哪些单元必须排在这个之前」,任何不是本包单元的名字由调用方丢弃。它不是
模块扫描器的替代品。
环留给编译器报。那是 ill-formed C++,编译器会点名那两个单元;在这里拒绝只会把同一个
事实报在更差的位置,而顺序错这件事无论如何已经不可能发生了。
判据
----
e2e 633 两条腿:
1. 一条**完全逆序**的链 —— 三个单元的 import 序与路径序恰好相反。路径排序下每一个
都排在它 import 的东西前面;import 排序下每一个都排在后面。除了顺序没有别的东西
决定它建不建得起来。判据是三个模块共同算出的那个值,经 `mcpp::warning` 输出 ——
那是构建成功时唯一会被打印的构建程序通道。
2. 包内无 import 的情形必须保持原次序。不是装饰:一个忽略原次序的拓扑排序可以吐出
任意合法序,于是今天能用的包会被无理由重排,而且是静默的。
把修复退回路径排序,第一条腿如实变红。
…pported
同一个解析器往上约四十行就把这两个键读进 `featureDeviceExtensions` 与
`featureRuleModule`,prepare 在消费者激活该 feature 时会读它们 —— 它们正是「新增一门
设备语言不需要引擎发版」的全部依据。它们只是没被加进 `kKnownFeatureKeys`,于是引擎对
一个它刚刚用过的键打印:
[features].rules-spirv has unsupported key 'rule_module' (ignored).
Supported keys: defines, flags, forward, implies, provides, requires, sources.
比消息错更坏:它在**建议包作者删掉让规则生效的那两行**。
没被发现,是因为规则包平时走的 host-module 路径不打印 schema 警告;而**普通**构建会
打印。当一个规则包开始产出 `tools = [...]` 要的二进制,它的普通构建就成了常态,这条
噪音于是浮到每个用户面前。
`.agents/docs/2026-09-07-module-first-heterogeneous-surface.md` 已经变成一个标题下的 **四份文档**:第 1–12 节是写在早于发布的 base commit 上的设计,第 13 节是计划与实施 记录,第 14 节是现状登记。读到前面就停下的人会被误导,而这份文档正是别人理解这套设计 的入口。 十四处就地更正,分三类 ---------------------- **被设计后撤回的语法。** `[rules]` 这张 manifest 表从未实现也不打算实现(该文 13.1.2 给了论证,`grep '"rules"' modules/manifest/src/toml.cppm` 为空),而 §5.2、§5.5、§6、 §8.D 仍在按它写。指向真正发布的形状:`options::module_name`、`options::surface`、 读 `MCPP_LANGUAGE_MODULES` 的 `default_surface()`。 **以不同形状发布的机制。** §4.1 提的是「给 embed 一个由图调起的宿主工具二进制」; 发布的是在 build.mcpp 里调用的库形态 —— 而它在 plan 期写 `.S`,正是 14.1 那条边存在 的原因。§4.3 写下了正确的要求,又加了「与 depfile 那一项共用同一个修法」,而那五个字 是错的:实测三条通道,只有 GNU as 报 `.incbin`,clang 的集成汇编器根本没有依赖输出。 **已关闭却仍列为 open 的条目。** §1 的三条里两条已发布;§3.5 还在断言那个字段不存在; §7 的四条里三条已解决;§12 描述的 Slang 规则已经发布。 不删掉,因为一个论证被否决也是它记录的一部分,一个设计在接触现实时变的形,同样是。 状态只在一处陈述:「哪些规则传了 depfile」活在 §14.2,不在别处 —— 三份拷贝就是它变化 时要记得改的三处。 §14 是唯一的现状登记表 ---------------------- 14.1 与 14.2 标为 CLOSED 并保留分析(14.1 的两半是可迁移的:一条要求被写下然后丢失, 以及第一次测量问错了工具)。新增 14.7(§5.6 承诺、§8.C 说要在同一次改动里加、结果既 没做也没登记的 `--no-accel` 诊断)与 14.8(L0 的现状,以及它的代价为什么是引擎侧的)。 新增 13.2.1 记这一批的跨仓波次。 14.1 按最终形态重写:缺陷在已发布生态里的沙箱复现、它为什么不是新发现、问错工具那次 测量、以及为什么最后**不需要**新引擎通道 —— 成因是生成跑在错误的时刻,不是引擎表达 不了那条边。
Sunrisepeak
force-pushed
the
feat/compile-input-declared-dependency
branch
from
September 8, 2026 00:29
2727444 to
0f86448
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two engine defects, and a design document brought back into line with what shipped
No new protocol surface. An earlier draft of this branch added one —
mcpp::recompile_if_changed, protocol 9 — and it was withdrawn, because a prototype showed the dependency it existed for is expressible with the graph primitive the engine already has. That account is kept in.agents/docs/…§14.1; what remains here is two real defects and the documentation.1. A package's host modules were ordered by their paths
The units a host-module package contributes were compiled in the order their paths sorted: the lib root, then everything else out of a
std::set<path>.build_program.cppmaccumulates module flags as it goes, so each unit sees only the BMIs ahead of it.rules/spirv.cppmsorts beforesrc/declare.cppm, so a member importing a unit its own package shares was compiled first:Reproduced in both directions. Renaming the shared unit so its path sorted first made the same package build. So the cause is the sort and nothing else.
What it cost was not a failure but a design decision made against the wrong cause
mcpp:pluginsread this as "a second unit is not compiled as a host module at all" and folded everything its members shared into the lib root — twenty lines to about seven hundred. Its design document recorded that as a cost paid, together with the mistaken cause.The fix
Topological order over declared imports, with path order as the stable tie-break: a package with no internal imports is ordered exactly as it is today, so this can only change builds that were already broken.
A cycle is left to the compiler on purpose — it is ill-formed C++, the compiler names both units, and refusing here would report the same fact in a worse place.
declared_importssits besidedeclared_interface_nameand matches its discipline: line-based, comment-aware, deliberately shallow. It answers "which units of THIS package must precede this one"; every other name is discarded by the caller. It is not a module scanner.Criterion
tests/e2e/633, two legs:mcpp::warning— the one build-program channelmcpp buildprints on success.Reverting the fix to a path sort turns leg 1 red, as it should.
2.
device_extensionsandrule_modulewere read, then reported as unsupportedBoth are parsed about forty lines above the check that rejects them, stored in
featureDeviceExtensions/featureRuleModule, and read by prepare when a consumer activates the feature — they are the whole basis for "a new device language costs no engine release". They were simply missing fromkKnownFeatureKeys:Worse than a wrong message: it advises a package author to delete the two lines that make their rule work.
It went unnoticed because the host-module path a rule package is normally loaded through does not print schema warnings; an ordinary build of the same package does. A rule package that produces a binary for
tools = [...]makes ordinary builds of rule packages routine, which is how this surfaced.The design document, aligned
.agents/docs/2026-09-07-module-first-heterogeneous-surface.mdhad become four documents under one heading — a design argued against base commits that predate the release, a plan, an implementation record, and an open-items register — and the first of those still described a syntax that was designed and withdrawn. Someone who opens it to understand this design is misinformed by section 5 before reaching section 13, which says the opposite.Fourteen in-place corrections, in three kinds:
options::module_name,options::surface,default_surface()readingMCPP_LANGUAGE_MODULES— never[rules], whichgrep '"rules"' modules/manifest/src/toml.cppmconfirms does not existNothing is deleted: an argument's rejection is part of its record, and so is the shape a design took on contact with reality. A status is stated once — "which rules pass a depfile" lives in §14.2 and nowhere else, because three copies are three places to forget when it changes.
§14 is the only register of what is open. §14.1 and §14.2 are now CLOSED with their analysis kept; §14.7 and §14.8 are new; §13.2.1 records this batch's cross-repository waves.
The two transferable findings in §14.1
A requirement folded into another item's fix disappears when that item ships. §4.3 wrote the requirement down before any code existed, then added five words pointing at the depfile item. The depfile shipped, the item was marked done, and
.incbinwent with it — silently, with every test green, because the test written for the other item passed.The first measurement asked the wrong tool.
-MDon a compiler driver is a preprocessor channel, and.incbinis not an include, so a very convincing "no" described a channel that was never a candidate. The assembler has its own, and the two disagree: GNU as names the embedded file in--MD; clang's integrated assembler has no dependency output of any kind. Taking the assembler's word would have left the dependency tracked under GCC and silently absent under Clang — worse than absent under both.Ecosystem
mcpp:plugins0.4.0 depends on the ordering fix: it splits the surface into a pure generator (importing onlystd, so the same code compiles intomcpp-embed) and a build-program half insrc/declare.cppm, which the members import. Its CI downloads a released mcpp, so the order is not interchangeable — this merges and releases first.Local: the full e2e suite and the unit suite green; the plugins branch verified against a locally built engine, including a control leg that goes red when the mechanism is removed.