From f59f713147d6ed791e2f4c9dbe081f4ba11580b2 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 04:03:14 +0800 Subject: [PATCH 01/11] 2026.9.21.2: the macros mcpp owns are spelt in upper case, and the borrowed name is withdrawn `__mcpp_target___` becomes `__MCPP_TARGET___` and `__openkal__` becomes `__OPENKAL__`. The spelling is still the triple's own `os` field, now upper-cased; the engine still learns no operating-system name. THE CONVENTION SPLITS BY WHAT A NAME IS, NOT BY WHO WRITES IT. A vendor or product name is upper --- `__APPLE__`, `_WIN32`, `__MINGW32__`, `__GNUC__`. A kind-of-system name is lower --- `__linux__`, `__unix__`, `__gnu_linux__`. Every row this engine OWNS is of the first kind: it names mcpp, or it names openkal. The kind-of-system question is answered by `__linux__` and its family, which mcpp SUPPLIES rather than owns and which therefore keep their lower-case spelling, for that exact reason. The lower-case spelling shipped in 2026.9.21.1 on the reverse reading: that these names sit beside `__linux__` in real guards, so matching it was consistency. THAT CONFUSES ADJACENCY WITH KIND. `__APPLE__` sits in those same guards and is upper, because it belongs to somebody. `__MCPP_` is a prefix, not the whole rule: `__OPENKAL__` is owned and names *openkal* rather than mcpp. The assertion in `test_predefines.cpp` therefore checks the SPELLING CONVENTION --- upper case, `__`-wrapped --- instead of the prefix, which would have judged `__OPENKAL__` a violation when it is not. WITHDRAWING A MACRO IS DECIDED BY A COUNT, NOT BY READING An entry in this contract is a published interface, and withdrawing one is SILENT: a `#if` selects the other branch and compiles. No mechanism available to a build tool makes that loud. So a withdrawal is decided by a measurement that ENUMERATES READERS, and what it finds sets how many steps the withdrawal takes. The two this project has performed came out differently: withdrawn readers found steps __CYGWIN__ 4 third-party members, 3 + 6 sites in the two headers we INSTALL __mcpp_target___, __openkal__ none 1 The second row's denominator is every repository of this ecosystem, swept by file type: no source file and no manifest reads either name, and the only occurrences are this engine's emitter, its tests, and prose. Both names were invented here, so no upstream code can hold one; exposure was three days for `__openkal__` and a single release for the target macro. The rule did not change between the two rows. The count did. A new assertion takes its denominator from the target registry rather than from a list written beside the test: every canonical triple is parsed and its emitted macro checked to be a valid identifier. The spelling comes from the `os` field, so an `os` carrying a dot or a version suffix would produce a macro no compiler accepts, and the failure would land in a user's build. __CYGWIN__ IS WITHDRAWN --- THE LAST STEP OF A THREE-REPOSITORY SEQUENCE The Windows `presents = "posix"` realisation now adds `-U__CYGWIN__` and `-U__CYGWIN32__` to the compile line. `--target=x86_64-pc-cygwin` stays: it is what supplies `__unix__` and suppresses `_WIN32`, which is what presenting POSIX means. Only the borrowed NAME was unwanted. 1. 2026.9.21.1 defined mcpp's own name beside the borrowed one; this release re-spells it, while it still has no consumer. Purely additive. 2. `openkal-musl@0.19.0` and `openkal-llvm-runtime@0.14.0` read the new name and keep `|| defined(__CYGWIN__)`, so they build on an engine from either side of this change. PUBLISHED BEFORE THIS RELEASE. 3. this release stops defining the borrowed name. THE ORDERING IS MEASURED, NOT ASSERTED. It holds between repositories, so no test in this one can check it. Building an openkal program for `x86_64-windows-gnu` against the PUBLISHED `openkal-llvm-runtime@0.13.0` on this engine fails on libunwind's two `static_assert`s --- `x86_64 registers do not fit into unw_context_t`, `UnwindCursor<> does not fit in unw_cursor_t`. That is step three taken first, and it is why step two ships first. `setjmp.h` is the silent half: its own comment says a mismatch is reported by nothing until the record overruns. THE RESIDUAL WINDOW IS NAMED RATHER THAN CLAIMED AWAY. A project pinning `openkal-musl` at 0.18.0 or earlier EXACTLY, and upgrading past this release, gets that silent `#else`. Moving the index's `latest` onto 0.19.0 first keeps the window to exact pins; nothing available here closes it, because the engine cannot know which macros a package's installed headers read. Bootstrap pin advances from 2026.9.20.1 to 2026.9.21.1. --- .xlings.json | 2 +- CHANGELOG.md | 73 +++++++++++++ docs/21-the-target-triple.md | 81 +++++++++----- docs/22-target-side.md | 13 ++- docs/24-openkal-cross.md | 8 +- docs/zh/21-the-target-triple.md | 59 ++++++---- docs/zh/22-target-side.md | 11 +- docs/zh/24-openkal-cross.md | 8 +- mcpp.toml | 2 +- modules/toolchain-model/src/model.cppm | 2 +- modules/versioning/src/version.cppm | 2 +- src/build/cache_key.cppm | 4 +- src/build/prepare.cppm | 16 +-- src/toolchain/cenv.cppm | 103 ++++++++++++------ src/toolchain/predefines.cppm | 91 +++++++++++----- ...i_environment_declared_by_the_c_library.sh | 4 +- tests/unit/test_cache_key.cpp | 4 +- tests/unit/test_cenv.cpp | 48 +++++--- tests/unit/test_predefines.cpp | 80 +++++++++++--- 19 files changed, 437 insertions(+), 174 deletions(-) diff --git a/.xlings.json b/.xlings.json index 27cf18b7..e05750f1 100644 --- a/.xlings.json +++ b/.xlings.json @@ -1,5 +1,5 @@ { "workspace": { - "mcpp": "2026.9.20.1" + "mcpp": "2026.9.21.1" } } diff --git a/CHANGELOG.md b/CHANGELOG.md index d8ad1587..a1143fd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,79 @@ ## [Unreleased] +## [2026.9.21.2] - 2026-09-21 + +### 引擎拥有的宏改为全大写 + +`__mcpp_target___` 成为 `__MCPP_TARGET___`,`__openkal__` 成为 `__OPENKAL__`。 +拼法仍取自三元组自己的 `os` 字段,只是转为大写,引擎依旧不认识任何操作系统名。 + +**业界的约定是按「名字是什么」分的,不是按谁写的分。** 厂商名与产品名用大写 +(`__APPLE__`、`_WIN32`、`__MINGW32__`、`__GNUC__`),系统种类名用小写(`__linux__`、 +`__unix__`、`__gnu_linux__`)。mcpp **拥有**的每一行都属于第一类:它命名的是 mcpp,或者 +是 openkal。「是哪一种系统」由 `__linux__` 那一族回答,而那一族是 mcpp **供给**而非拥有 +的,所以它们保持小写。 + +小写拼法在 2026.9.21.1 发布过一版,产生它的推理是:这些名字在真实守卫里与 `__linux__` +并排出现,跟它一致看起来就像一致性。**那是把「相邻」当成了「同类」。** `__APPLE__` 出现 +在同样那些守卫里却是大写,因为它属于某个人。 + +**`__MCPP_` 是前缀而不是规则的全部。** `__OPENKAL__` 同样是 mcpp 拥有的,但它命名的是 +*openkal* 而不是 mcpp。`test_predefines.cpp` 里那条断言因此改为检查**拼法约定**(大写、 +`__` 包裹),而不再检查前缀——按前缀断言会把 `__OPENKAL__` 判成违规,而它不是。 + +### 撤销一个宏的判据是一次数读者的测量 + +这张表里的一条是**已发布的接口**,而撤销一条是**静默**的:一个 `#if` 选了另一条分支, +照常编译。构建工具手里没有任何机制能让它变响。所以撤销不由**读**来决定,而由**一次枚举 +读者的测量**来决定,数出多少就决定要走几步。 + +| 撤销的名字 | 数出的读者 | 步数 | +|---|---|---| +| `__CYGWIN__` | 四个第三方成员,外加本生态**安装出去**的两个头里的六处 | 三步 | +| `__mcpp_target___`、`__openkal__` | 零 | 一步 | + +第二行的分母是全生态每一个仓库,逐文件类型扫过:没有一个源文件、没有一份清单读它们, +出现的地方只有引擎自己的发出处、它的测试与散文。两个名字都是在这里发明的,所以不可能有 +上游代码握着它们;暴露窗口 `__openkal__` 是三天,目标宏是一个发布。**两次之间规则没变, +变的是数目。** + +新增判据 `EveryTargetInTheRegistryYieldsAValidIdentifier`:分母取自目标注册表本身而不是 +测试旁边写死的一张名单,逐行解析并断言发出的宏是合法标识符——拼法既然取自 `os` 字段,一个 +带点或带版本后缀的 `os` 就会产出编不过的宏,而失败会落在用户的构建里而不是这里。 + +### `__CYGWIN__` 撤销完成(三仓序列的最后一步) + +Windows 上 `presents = "posix"` 的实现现在在编译行加 `-U__CYGWIN__` / `-U__CYGWIN32__`。 +`--target=x86_64-pc-cygwin` 本身保留——它供给 `__unix__` 并压掉 `_WIN32`,那正是「呈现 +POSIX」的含义;不需要的只是那个**借来的名字**。 + +30 成员测量判定这个名字的代价是四个成员(`archive`、`sqlite3`、`mimalloc`、`c-ares` 各自 +停在 `#include `,经由 `#if defined(_WIN32) || defined(__CYGWIN__)`)。上游用 +它表达「Win32 可用」,mimalloc 把这句话写在守卫自己的注释里。 + +**序列按仓库排序,而这个次序就是安全性论证本身:** + +1. 2026.9.21.1 在借来的名字旁边加上 mcpp 自己的名字;2026.9.21.2 趁它还没有消费者时改成 + 大写。每一步都是纯增量。 +2. `openkal-musl@0.19.0` 与 `openkal-llvm-runtime@0.14.0` 改读新名字,并保留 + `|| defined(__CYGWIN__)`,于是在本次改动两侧的引擎上都能构建。**先于第三步发布。** +3. 本版停止定义借来的名字。 + +先做第三步——本分支的一个更早修订就这么做过——会让**已发布**的那两个安装头全部落进各自的 +`#else`。libunwind 的 `static_assert` 会响亮地红;`setjmp.h` 那一处不会,它自己的注释写着 +「a mismatch nothing reports until the record overruns」。 + +**残余窗口被点名而不是被说没有。** 把 `openkal-musl` **精确**钉在 0.18.0 或更早、同时把 +引擎升过本版的工程,会拿到那个静默的 `#else`。在本版之前把索引的 `latest` 移到 0.19.0, +是把窗口压到「精确钉」的办法;这里没有任何机制能把它关掉,因为引擎无从知道一个包安装出去 +的头读了哪些宏。 + +### 自举钉 + +`.xlings.json` 的 workspace pin 从 2026.9.20.1 走到 2026.9.21.1。 + + ## [2026.9.21.1] - 2026-09-21 ### 引擎定义的宏成为一份规范,而规范就是那个模块 diff --git a/docs/21-the-target-triple.md b/docs/21-the-target-triple.md index 6305a0c6..b2ec66d6 100644 --- a/docs/21-the-target-triple.md +++ b/docs/21-the-target-triple.md @@ -621,8 +621,8 @@ fails a build rather than drifting into a release. | macro | when | owned | |---|---|---| -| `__mcpp_target___` | one per build, spelt from the triple's `os` field | yes | -| `__openkal__` | the resolved `kernel-abi` layer's interface name is `openkal` | yes | +| `__MCPP_TARGET___` | one per build, spelt from the triple's `os` field | yes | +| `__OPENKAL__` | the resolved `kernel-abi` layer's interface name is `openkal` | yes | | `__unix__` | the `[c-abi]` realisation supplies it where the toolchain would not | no | **An engine should not define macros, and each row has to justify itself.** A @@ -638,12 +638,12 @@ manifest; a macro is none of those. Two justifications have survived: spelled in its manifest, but a header it INSTALLS is read by an application's own compile, which those defines never reach. -**`__mcpp_target___` — the rule.** Defined for every target-side +**`__MCPP_TARGET___` — the rule.** Defined for every target-side translation unit, always, one per build. The spelling is the triple's own `os` -field, so `x86_64-linux-gnu` gives `__mcpp_target_linux__`, `x86_64-windows-gnu` -gives `__mcpp_target_windows__`, and `riscv64-none-elf` gives -`__mcpp_target_none__`. The engine learns no operating-system name: a target -added to the triple parser gets its macro with no engine change. +field, upper-cased, so `x86_64-linux-gnu` gives `__MCPP_TARGET_LINUX__`, +`x86_64-windows-gnu` gives `__MCPP_TARGET_WINDOWS__`, and `riscv64-none-elf` +gives `__MCPP_TARGET_NONE__`. The engine learns no operating-system name: a +target added to the triple parser gets its macro with no engine change. *Allowed:* learning the target's operating system where the C environment presented above it has suppressed the platform's own macros, and sizing a @@ -660,31 +660,58 @@ emission would make its absence ambiguous: "not Windows" and "Windows, but nothing hid its macros" would read the same. A macro whose absence means one thing is worth one `-D`. -**Naming.** `__mcpp_`-prefixed, `__`-suffixed, lowercase, words separated by -`_`. Two conventions exist in the wild — vendor and product names are upper -(`__APPLE__`, `_WIN32`, `__MINGW32__`), kind-of-system names are lower -(`__linux__`, `__unix__`) — and these name kinds of target, sitting beside -that second family in real guards: `#if defined(__linux__) || -defined(__mcpp_target_windows__)`. - -The `__mcpp_` prefix is load-bearing. A name mcpp owns means what mcpp says it -means. The alternative was tried: `__CYGWIN__` was left defined so that code -needing "PE object format, POSIX C environment" would have a name, and a -30-member measurement found four members reading it as *Win32 is available* -and reaching `#include ` — which is what upstream means by it. **A -borrowed name means what the lender's history made it mean**, not what the -borrower intended. +**Naming.** UPPER CASE, `__`-wrapped, words separated by `_`; the names mcpp +owns carry `__MCPP_`. + +The convention in the wild splits by **what a name is**, not by who writes it. +A vendor or product name is upper — `__APPLE__`, `_WIN32`, `__MINGW32__`, +`__GNUC__`. A kind-of-system name is lower — `__linux__`, `__unix__`, +`__gnu_linux__`. Every row mcpp OWNS is of the first kind: it names mcpp, or +it names openkal. The kind-of-system question is answered by `__linux__` and +its family, which mcpp SUPPLIES rather than owns and which therefore keep +their lower-case spelling, for that exact reason. + +The lower-case spelling shipped first, and the reasoning that produced it is +worth keeping because the mistake is easy to repeat: these names sit beside +`__linux__` in real guards, so matching it looked like consistency. **That +confuses adjacency with kind.** `__APPLE__` sits in those same guards and is +upper, because it belongs to somebody — as does every other name in them that +belongs to somebody. + +The `__MCPP_` prefix is load-bearing, and it is a prefix rather than the whole +rule: a name mcpp owns means what mcpp says it means, while `__OPENKAL__` is +owned and names *openkal* rather than mcpp. The alternative — borrowing — +was tried. `__CYGWIN__` was left defined so that code needing "PE object +format, POSIX C environment" would have a name, and a 30-member measurement +found four members reading it as *Win32 is available* and reaching `#include +`, which is what upstream means by it. **A borrowed name means what +the lender's history made it mean**, not what the borrower intended. `__unix__` is the exception that proves the rule: mcpp SUPPLIES it rather than owning it, so it keeps the standard spelling and mcpp may not change its meaning. Read it exactly as on any other POSIX system. -**Stability.** An entry here is a published interface. Removing one, or -narrowing when it is defined, is a breaking change for source this project -does not control, and the failure is usually SILENT — a `#if` selects the -other branch and compiles. Withdrawal is therefore a sequence, never an edit: -add the replacement, let consumers move onto it while still accepting the old -name, and only then stop defining it. +**Stability.** An entry here is a published interface, and withdrawing one is +SILENT: a `#if` selects the other branch and compiles, on a machine nobody is +watching. No mechanism available to a build tool makes that loud — a +preprocessor cannot be told to complain about a name it does not find. + +So a withdrawal is never decided by reading. It is decided by **a measurement +that enumerates readers**, and what the measurement finds sets how many steps +the withdrawal takes. Both withdrawals this project has performed are on the +record, and they came out differently: + +| withdrawn | readers found | steps | +|---|---|---| +| `__CYGWIN__` (2026.9.21.1) | four third-party members, plus six sites in the two headers this ecosystem installs | three: publish the replacement, move consumers, stop defining | +| `__mcpp_target___`, `__openkal__` (2026.9.21.2) | none — no source file and no manifest in any repository of this ecosystem | one | + +The rule did not change between those two; the count did. The second row is +cheap for a reason worth stating: both names were invented here, so no +upstream code can be holding one, and the exposure was three days for +`__openkal__` and a single release for the target macro. A withdrawal argued +from anything other than a count is an argument this project has already got +wrong once. ## Custom Targets diff --git a/docs/22-target-side.md b/docs/22-target-side.md index 3f9972fa..04e8962c 100644 --- a/docs/22-target-side.md +++ b/docs/22-target-side.md @@ -377,7 +377,7 @@ that names no C library: | Linux | `posix` / `arch-default` | the default triple already satisfies it | | macOS | `posix` / `arch-default` | one token, `-D__unix__` — Apple's clang predefines `__APPLE__`/`__MACH__` on its default triple, never `__unix__` | | freestanding | `posix` / `arch-default` | the same one token, `-D__unix__`, for the same reason: nothing here defines it either | -| Windows | `posix` / `arch-default` | Cygwin-flavoured: `--target=x86_64-pc-cygwin` on the compile line only; plus `-D__mcpp_target_windows__` (see the note below); `data-model` becomes LP64 as a consequence of the triple, not a separate flag | +| Windows | `posix` / `arch-default` | Cygwin-flavoured: `--target=x86_64-pc-cygwin` on the compile line only; plus `-D__MCPP_TARGET_WINDOWS__` (see the note below); `data-model` becomes LP64 as a consequence of the triple, not a separate flag | | any | `builtins = "iso"` | turns off code-generation idioms that assume a platform C library — `-fno-builtin-memset_pattern16` on Apple targets is the one this survey measured; see `src/toolchain/cenv.cppm` for what else was checked and found not to apply | | anything else | | refused, naming the target, the request and what is missing — never a silent downgrade | @@ -418,8 +418,9 @@ preprocessor sees and how wide `long` is. Realisation therefore touches only the **compile** line; the **link** line keeps the triple the graph resolved, because nothing about the object format changed. -**`__mcpp_target_windows__` is defined here, and `__CYGWIN__` still is too -(2026.9.21.1).** +**`__MCPP_TARGET_WINDOWS__` is defined here, and `__CYGWIN__` still is too +(2026.9.21.1; the name was spelt in lower case in that one release and +re-spelt in 2026.9.21.2, before it had a consumer).** The substitution suppresses `_WIN32` on purpose — that is what presenting POSIX means. But the **ABI did not change with the environment**: the calling @@ -432,7 +433,7 @@ INSTALLED headers in this ecosystem size records by that fact: | openkal-llvm-runtime | `__libunwind_config.h` | `unw_context_t`, `unw_cursor_t` | An installed header is read by an **application's own compile**, so neither -can use a package-private define. `__mcpp_target_windows__` is mcpp's own +can use a package-private define. `__MCPP_TARGET_WINDOWS__` is mcpp's own name for the question they ask — is this target Windows, whatever C environment is presented above it — and being mcpp's own, its meaning is not decided by anyone else's history. It is emitted only under this substitution; @@ -453,7 +454,7 @@ loudly; `setjmp.h`'s equivalent would not have — its own comment says *a mismatch nothing reports until the record overruns*. So the withdrawal is three steps, and each intermediate state builds: -1. this release defines `__mcpp_target_windows__` as well — purely additive; +1. 2026.9.21.1 defines `__MCPP_TARGET_WINDOWS__` as well — purely additive; 2. those packages read the new name, keeping `|| defined(__CYGWIN__)` so they build on both engines; 3. a later release stops defining the borrowed one. @@ -664,7 +665,7 @@ cache an ordinary dependency compile reuses across projects and across an keyed per package from exactly the axes that reach that package's own compile command line (`mcpp.build.cache_key`). The realised environment reaches a package's command line entirely through an engine BROADCAST (the -same channel `targetSideUsage` and `-D__openkal__` use, never the package's +same channel `targetSideUsage` and `-D__OPENKAL__` use, never the package's own declared `[build] cflags`/`cxxflags`), so the key's own derivation had to be told to read the broadcast, not only the declaration — found exactly that way (coordinator report, openkal-musl spike): upgrading `mcpp` in diff --git a/docs/24-openkal-cross.md b/docs/24-openkal-cross.md index a2b69875..1afaf489 100644 --- a/docs/24-openkal-cross.md +++ b/docs/24-openkal-cross.md @@ -44,7 +44,7 @@ meant "is the Windows CRT present." | Family | States | Defined by | Example | |---|---|---|---| -| kernel ABI | `kal_*` is callable, and behaves the same on every platform | the layer providing `mcpp:kernel-abi=openkal` | `__openkal__` | +| kernel ABI | `kal_*` is callable, and behaves the same on every platform | the layer providing `mcpp:kernel-abi=openkal` | `__OPENKAL__` | | C environment | the shape of the C environment source sees | the layer providing `mcpp:c-abi=`, via [`[c-abi]`](22-target-side.md#the-c-environment-a-c-abi-package-presents-mcpp-2026918) | `__unix__`, `_WIN32`, `__MINGW32__` | | system & architecture | the underlying OS and processor | the target triple | `__linux__`, `__APPLE__`, `__x86_64__` | @@ -58,7 +58,7 @@ combination, "PE format with a POSIX-presenting C environment," is therefore leaves DEFINED rather than folding into the three rows above — see [22's own note](22-target-side.md#the-c-environment-a-c-abi-package-presents-mcpp-2026918) for the full trade-off. Since 2026.9.21.1 `__CYGWIN__` is no longer the only -name for that fact: mcpp defines `__mcpp_target___` for every target +name for that fact: mcpp defines `__MCPP_TARGET___` for every target (`docs/21`, "The macros mcpp defines"), so source needing to know the target under a presented environment has a name mcpp owns. `__CYGWIN__` remains defined while the ecosystem's installed headers move onto the new name, and @@ -67,7 +67,7 @@ than as what they are — a fact about the TARGET, not about the environment presented above it — is exactly the shape of confusion this whole section exists to head off. -**`__openkal__` — the rule.** The engine defines it, for every target-side +**`__OPENKAL__` — the rule.** The engine defines it, for every target-side unit, whenever the resolved `kernel-abi` layer's interface name is `openkal` — read from the LAYER's value, never from a package name, so a second implementation (`openkal-macos`, `openkal-opensbi`, …) needs no @@ -85,7 +85,7 @@ write `cfg(c-abi = "…")` or `cfg(kernel-abi = "…")` in the manifest instead for the predicate grammar). **Platform units.** A package that itself needs the platform's own -environment never reads `__openkal__` or any other macro to work that out — +environment never reads `__OPENKAL__` or any other macro to work that out — the boundary is stated in the manifest, not inferred from source, and everything crossing it is still fixed-width (SPEC §5.4). Two different packages reach `[package] c-environment = "platform"` (docs/22) by two diff --git a/docs/zh/21-the-target-triple.md b/docs/zh/21-the-target-triple.md index d1a92825..e7fabc2c 100644 --- a/docs/zh/21-the-target-triple.md +++ b/docs/zh/21-the-target-triple.md @@ -553,8 +553,8 @@ x86_64 宿主行得通的路线,而平台模拟器会直接拒绝异构 guest | 宏 | 何时定义 | 是否 mcpp 拥有 | |---|---|---| -| `__mcpp_target___` | 每次构建一个,拼法取自三元组的 `os` 字段 | 是 | -| `__openkal__` | 解析出的 `kernel-abi` 层接口名是 `openkal` | 是 | +| `__MCPP_TARGET___` | 每次构建一个,拼法取自三元组的 `os` 字段 | 是 | +| `__OPENKAL__` | 解析出的 `kernel-abi` 层接口名是 `openkal` | 是 | | `__unix__` | `[c-abi]` 实现供给它,在工具链不会定义它的地方 | 否 | **引擎原则上不该定义任何宏,每一行都要为自己辩护。** 包在清单里陈述需要什么,引擎以 @@ -566,11 +566,11 @@ x86_64 宿主行得通的路线,而平台模拟器会直接拒绝异构 guest 2. **读者是已安装的头。** 包自己的构建 define 可以写在清单里,但它**安装出去**的头会被 **应用程序自己的编译**读到,那些 define 永远到不了那里。 -**`__mcpp_target___` 的规则。** 为每一个目标侧翻译单元定义,总是定义,每次构建一个。 -拼法取自三元组自己的 `os` 字段:`x86_64-linux-gnu` 给出 `__mcpp_target_linux__`, -`x86_64-windows-gnu` 给出 `__mcpp_target_windows__`,`riscv64-none-elf` 给出 -`__mcpp_target_none__`。引擎不认识任何操作系统名:三元组解析器新增一个目标,它的宏随之 -存在,引擎无需改动。 +**`__MCPP_TARGET___` 的规则。** 为每一个目标侧翻译单元定义,总是定义,每次构建一个。 +拼法取自三元组自己的 `os` 字段并转为大写:`x86_64-linux-gnu` 给出 +`__MCPP_TARGET_LINUX__`,`x86_64-windows-gnu` 给出 `__MCPP_TARGET_WINDOWS__`, +`riscv64-none-elf` 给出 `__MCPP_TARGET_NONE__`。引擎不认识任何操作系统名:三元组解析器新增 +一个目标,它的宏随之存在,引擎无需改动。 *允许:* 当上面呈现的 C 环境压掉了平台自己的宏时,用它得知目标的操作系统;以及按目标的 ABI 给记录定尺寸。呈现 POSIX 的 Windows 目标**有意**没有 `_WIN32`,而调用约定仍是 Win64 @@ -583,23 +583,44 @@ ABI 给记录定尺寸。呈现 POSIX 的 Windows 目标**有意**没有 `_WIN32 「不是 Windows」与「是 Windows,但没有东西压掉它的宏」会读成同一件事。一个缺席只有一种 含义的宏,值一个 `-D`。 -**命名。** `__mcpp_` 前缀、`__` 后缀、小写、单词用 `_` 分隔。业界并存两套约定——厂商与 -产品名用大写(`__APPLE__`、`_WIN32`、`__MINGW32__`),系统种类名用小写(`__linux__`、 -`__unix__`)——而这些命名的是目标的种类,在真实守卫里与第二族并排出现: -`#if defined(__linux__) || defined(__mcpp_target_windows__)`。 +**命名。** **全大写**、`__` 包裹、单词用 `_` 分隔;mcpp 拥有的名字带 `__MCPP_`。 -`__mcpp_` 这个前缀是承重的。**一个 mcpp 拥有的名字,语义由 mcpp 自己定。** 另一条路试过: -`__CYGWIN__` 曾被保留定义,为的是让需要「PE 目标文件格式 + POSIX C 环境」的代码有个名字, -而 30 成员测量发现四个成员把它读成「**Win32 可用**」并走向 `#include `——那正是 -上游用它表达的意思。**一个借来的名字,语义由借出方的历史决定**,不由借用方的意图决定。 +业界的约定是按**名字是什么**分的,不是按谁写的分。厂商名与产品名用大写——`__APPLE__`、 +`_WIN32`、`__MINGW32__`、`__GNUC__`;系统种类名用小写——`__linux__`、`__unix__`、 +`__gnu_linux__`。mcpp **拥有**的每一行都属于第一类:它命名的是 mcpp,或者是 openkal。 +「是哪一种系统」这个问题由 `__linux__` 那一族回答,而那一族是 mcpp **供给**而非拥有的, +正因如此它们保持小写拼法。 + +小写拼法先发布过,而产生它的那段推理值得留着,因为这个错很容易再犯一次:这些名字在真实 +守卫里与 `__linux__` 并排出现,于是「跟它一致」看起来就像一致性。**那是把「相邻」当成了 +「同类」。** `__APPLE__` 出现在同样那些守卫里,却是大写,因为它属于某个人——那些守卫里 +凡是属于某个人的名字都是大写。 + +`__MCPP_` 这个前缀是承重的,但它是**前缀**而不是规则的全部:**一个 mcpp 拥有的名字,语义 +由 mcpp 自己定**,而 `__OPENKAL__` 同样是拥有的,它命名的是 *openkal* 而不是 mcpp。另一条 +路——**借用**——试过:`__CYGWIN__` 曾被保留定义,为的是让需要「PE 目标文件格式 + POSIX C +环境」的代码有个名字,而 30 成员测量发现四个成员把它读成「**Win32 可用**」并走向 +`#include `——那正是上游用它表达的意思。**一个借来的名字,语义由借出方的历史 +决定**,不由借用方的意图决定。 `__unix__` 是印证这条规则的例外:mcpp **供给**它而不拥有它,所以它保持标准拼法,mcpp 也 不得改变它的含义。读它就按在任何别的 POSIX 系统上那样读。 -**稳定性。** 这张表里的一条是**已发布的接口**。删掉一条、或收窄它被定义的条件,对本项目 -控制不了的源码就是破坏性变更,而失败通常是**静默**的——一个 `#if` 选了另一条分支,照常 -编译。所以撤销永远是一个**序列**,不是一次编辑:先加替代品,让消费者迁移过去并仍接受旧 -名字,之后才停止定义它。 +**稳定性。** 这张表里的一条是**已发布的接口**,而撤销一条是**静默**的:一个 `#if` 选了 +另一条分支,照常编译,发生在没人看着的机器上。构建工具手里没有任何机制能让它变响—— +预处理器无法被告知「为一个它找不到的名字报错」。 + +所以撤销从来不由**读**来决定,而由**一次枚举读者的测量**来决定,测量数出多少,就决定这次 +撤销要走几步。本项目做过的两次撤销都在记录里,而它们的结果不同: + +| 撤销的名字 | 数出的读者 | 步数 | +|---|---|---| +| `__CYGWIN__`(2026.9.21.1) | 四个第三方成员,外加本生态**安装出去**的两个头里的六处 | 三步:发布替代品、迁移消费者、最后停止定义 | +| `__mcpp_target___`、`__openkal__`(2026.9.21.2) | 零——本生态任何仓库里没有一个源文件、没有一份清单读它们 | 一步 | + +两次之间规则没变,变的是数目。第二行便宜是有原因的,而这个原因值得写出来:两个名字都是 +在这里发明的,所以不可能有上游代码握着它们;暴露窗口 `__openkal__` 是三天,目标宏是一个 +发布。**一次不由数目支撑的撤销,正是本项目已经犯错过一次的那种论证。** ## 自定义目标 diff --git a/docs/zh/22-target-side.md b/docs/zh/22-target-side.md index 6f9c9758..8d1b7bb4 100644 --- a/docs/zh/22-target-side.md +++ b/docs/zh/22-target-side.md @@ -309,7 +309,7 @@ libunwind 的 `assembly.h`,正是按这个宏来选寄存器保存集的)。mcpp | Linux | `posix` / `arch-default` | 默认三元组已经满足 | | macOS | `posix` / `arch-default` | 一个令牌,`-D__unix__`——Apple 的 clang 默认三元组预定义的是 `__APPLE__`/`__MACH__`,从来不是 `__unix__` | | 裸机(freestanding) | `posix` / `arch-default` | 同样一个令牌,`-D__unix__`,原因相同:这里同样没有任何东西定义它 | -| Windows | `posix` / `arch-default` | 采用 Cygwin 式语义:仅在编译行加 `--target=x86_64-pc-cygwin`;并加 `-D__mcpp_target_windows__`(见下方说明);`data-model` 变为 LP64 是三元组切换的结果,不是另一个开关 | +| Windows | `posix` / `arch-default` | 采用 Cygwin 式语义:仅在编译行加 `--target=x86_64-pc-cygwin`;并加 `-D__MCPP_TARGET_WINDOWS__`(见下方说明);`data-model` 变为 LP64 是三元组切换的结果,不是另一个开关 | | 任意目标 | `builtins = "iso"` | 关闭代码生成阶段假定平台 C 库在场的惯用法识别——本轮实测到的唯一一例是 Apple 目标上的 `-fno-builtin-memset_pattern16`;`src/toolchain/cenv.cppm` 记录了还核实过哪些、结论是不适用 | | 其余情况 | | 明确拒绝,点名目标、请求与缺什么——不静默降级 | @@ -337,7 +337,8 @@ Windows 一行是旗舰情形:`x86_64-w64-windows-gnu` 与 `x86_64-pc-cygwin` 同样的 PE 格式、同样的 Win64 调用约定、同样的 SEH——差别只在预处理器看到什么、`long` 有多宽。 因此实现只触及**编译**行;**链接**行保持图解析出的三元组,因为目标文件格式没有变化。 -**这里定义 `__mcpp_target_windows__`,而 `__CYGWIN__` 仍然定义着(2026.9.21.1)。** +**这里定义 `__MCPP_TARGET_WINDOWS__`,而 `__CYGWIN__` 仍然定义着(2026.9.21.1;这个名字在 +那一版是小写拼法,2026.9.21.2 在它还没有消费者时改成了大写)。** 这次替换有意压掉 `_WIN32`——那正是「呈现 POSIX」的含义。但**ABI 并没有跟着环境一起变**: 调用约定仍是 Win64,寄存器保存区仍按它的大小。生态里有两个**已安装的**头按这个事实定尺寸: @@ -348,7 +349,7 @@ Windows 一行是旗舰情形:`x86_64-w64-windows-gnu` 与 `x86_64-pc-cygwin` | openkal-llvm-runtime | `__libunwind_config.h` | `unw_context_t`、`unw_cursor_t` | 已安装的头会被**应用程序自己的编译**读到,所以两者都用不了包私有的 define。 -`__mcpp_target_windows__` 是 mcpp 为它们所问的那个问题给出的**自己的**名字——这个目标是不是 +`__MCPP_TARGET_WINDOWS__` 是 mcpp 为它们所问的那个问题给出的**自己的**名字——这个目标是不是 Windows,无论上面呈现的是什么 C 环境——既然是自己的名字,它的语义就不由别人的历史决定。 它只在这次替换下发出;普通的 Windows 构建仍然有 `_WIN64`。 @@ -362,7 +363,7 @@ Windows,无论上面呈现的是什么 C 环境——既然是自己的名字, libunwind 的 `static_assert` 响亮地红了;`setjmp.h` 那一处不会——它自己的注释写着 「a mismatch nothing reports until the record overruns」。所以撤销分三步,每个中间状态都能构建: -1. 本版**增加** `__mcpp_target_windows__`——纯增量; +1. 2026.9.21.1 **增加** `__MCPP_TARGET_WINDOWS__`——纯增量; 2. 那两个包改读新名字,保留 `|| defined(__CYGWIN__)` 以便在两种引擎上都能构建; 3. 之后的版本再停止定义借来的那个。 @@ -517,7 +518,7 @@ tcsetattr = { form = "accepted-no-effect", note = "openkal 不命名的那些字 `~/.mcpp/build-cache/v1`——一次普通依赖编译跨项目、也跨 `mcpp` 升级复用的缓存——是与上面 构建指纹分开的另一套机制,按包逐一取键,只取真正到达该包自身编译命令行的那些轴 (`mcpp.build.cache_key`)。解析出的环境到达一个包的命令行,完全是通过引擎的**广播** -(与 `targetSideUsage`、`-D__openkal__` 同一条通道),从来不经过包自己声明的 +(与 `targetSideUsage`、`-D__OPENKAL__` 同一条通道),从来不经过包自己声明的 `[build] cflags`/`cxxflags`——所以键的推导本身也得被告知去读广播后的值,而不只是声明的值。 这个缺口正是这样被发现的(协调者反馈,openkal-musl 尖峰实验):原地升级 `mcpp`、缓存目录 未清理时,给按新环境构建的镜像喂了按**旧**解析环境编译出的目标文件——一个镜像里混了两种 diff --git a/docs/zh/24-openkal-cross.md b/docs/zh/24-openkal-cross.md index 87c16442..01bc5973 100644 --- a/docs/zh/24-openkal-cross.md +++ b/docs/zh/24-openkal-cross.md @@ -35,7 +35,7 @@ C++ 运行时 —— 成为一组由依赖图解析、并由当前运行的编 | 宏族 | 陈述 | 定义者 | 例子 | |---|---|---|---| -| 内核 ABI | `kal_*` 可调用,且在每个平台上行为一致 | 提供 `mcpp:kernel-abi=openkal` 的层 | `__openkal__` | +| 内核 ABI | `kal_*` 可调用,且在每个平台上行为一致 | 提供 `mcpp:kernel-abi=openkal` 的层 | `__OPENKAL__` | | C 环境 | 源码看到的 C 环境形状 | 提供 `mcpp:c-abi=` 的层,通过 [`[c-abi]`](22-target-side.md#c-abi-包陈述它呈现的-c-环境mcpp-2026918) | `__unix__`、`_WIN32`、`__MINGW32__` | | 系统与架构 | 底层操作系统与处理器 | 目标三元组 | `__linux__`、`__APPLE__`、`__x86_64__` | @@ -47,13 +47,13 @@ C++ 运行时 —— 成为一组由依赖图解析、并由当前运行的编 三行里的任何一行——完整的权衡说明见 [22 自己的说明](22-target-side.md#c-abi-包陈述它呈现的-c-环境mcpp-2026918)。自 2026.9.21.1 起,`__CYGWIN__` 不再是那个事实的唯一名字:mcpp 为每个目标定义 -`__mcpp_target___`(见 `docs/21`「mcpp 定义的宏」),于是需要在被呈现的环境之下得知 +`__MCPP_TARGET___`(见 `docs/21`「mcpp 定义的宏」),于是需要在被呈现的环境之下得知 目标的源码,有了一个 mcpp 自己拥有的名字。`__CYGWIN__` 在生态的已安装头迁移到新名字期间 仍然定义着,之后才撤销。把两者中的任何一个当成第四个 C 环境宏来读,而不是它们实际是的 样子——一个关于**目标**的事实,而不是关于其上呈现的环境——正是这一整节想要提前避免的 那种混淆。 -**`__openkal__` 的规则。** 只要解析出的 `kernel-abi` 层的接口名是 `openkal`,引擎就为目标侧的 +**`__OPENKAL__` 的规则。** 只要解析出的 `kernel-abi` 层的接口名是 `openkal`,引擎就为目标侧的 每一个编译单元定义它——取自层的取值,绝不取自包名,因此第二个 `mcpp:kernel-abi=openkal` 的 实现不需要引擎改动。 @@ -65,7 +65,7 @@ C++ 运行时 —— 成为一组由依赖图解析、并由当前运行的编 `cfg(c-abi = "…")` 或 `cfg(kernel-abi = "…")`(谓词语法见 [22 —— 对已解析目标侧的适配](22-target-side.md#对已解析目标侧的适配))。 -**平台单元。** 自己需要平台原生环境的包,从不读 `__openkal__` 或别的宏去推断这件事——边界 +**平台单元。** 自己需要平台原生环境的包,从不读 `__OPENKAL__` 或别的宏去推断这件事——边界 是清单里写明的,不是从源码推断出来的,跨越它的一切仍然只能是定宽类型(SPEC §5.4)。两类 不同的包,通过两条不同的路径拿到 `[package] c-environment = "platform"`(docs/22): diff --git a/mcpp.toml b/mcpp.toml index a842076b..1fa98a77 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.9.21.1" +version = "2026.9.21.2" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/modules/toolchain-model/src/model.cppm b/modules/toolchain-model/src/model.cppm index 252247b3..80ff7a9c 100644 --- a/modules/toolchain-model/src/model.cppm +++ b/modules/toolchain-model/src/model.cppm @@ -184,7 +184,7 @@ struct Toolchain { std::vector cEnvExpectDefined; std::vector cEnvExpectUndefined; // The resolved `kernel-abi` is `openkal` — the engine defines - // `__openkal__` for every target-side unit (design §2.1, §3.4). Read from + // `__OPENKAL__` for every target-side unit (design §2.1, §3.4). Read from // the LAYER's value, never from a package name, so a second `openkal` // implementation needs no engine change. bool kernelAbiIsOpenkal = false; diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index f1afeecb..aea504b0 100644 --- a/modules/versioning/src/version.cppm +++ b/modules/versioning/src/version.cppm @@ -31,6 +31,6 @@ import std; export namespace mcpp { -inline constexpr std::string_view MCPP_VERSION = "2026.9.21.1"; +inline constexpr std::string_view MCPP_VERSION = "2026.9.21.2"; } // namespace mcpp diff --git a/src/build/cache_key.cppm b/src/build/cache_key.cppm index 5fcaadde..2374869f 100644 --- a/src/build/cache_key.cppm +++ b/src/build/cache_key.cppm @@ -76,7 +76,7 @@ export namespace mcpp::build::cache_key { // 3 (design 2026-09-18, coordinator review): `fill_package_config` used to // read only a package's OWN declared cflags/cxxflags, never the engine // broadcast channel (`privateBuild`) the realised [c-abi] environment and -// `-D__openkal__` actually travel through — so an entry written before this +// `-D__OPENKAL__` actually travel through — so an entry written before this // fix can be WRONG in a way probe_cached's normal "did the inputs change" // check cannot see: the compiled object and the recorded key silently // disagreed about what was compiled from the moment they were written, not @@ -533,7 +533,7 @@ void fill_package_config(PackageAxes& out, // are what its compile command line actually carries, because several // engine broadcasts append to `privateBuild` and never touch `bc` — // `targetSideUsage` (the resolved target side's own include dirs/flags), - // `-D__openkal__`, and the realised [c-abi] environment (design + // `-D__OPENKAL__`, and the realised [c-abi] environment (design // 2026-09-18) chief among them. A cache key built from `bc` alone cannot // tell two builds of the SAME package apart when only one of them // realised, say, LP64 instead of LLP64 — found exactly that way diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 285557da..2af61b11 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -10778,7 +10778,7 @@ prepare_build(bool print_fingerprint, resolvedTargetSide = tsd::resolve(in); targetSideResolved = true; - // `__openkal__` — design 2026-09-18 §2.1, §3.4. Read from the + // `__OPENKAL__` — design 2026-09-18 §2.1, §3.4. Read from the // resolved LAYER's interface name, never from a package name, so a // second implementation of `mcpp:kernel-abi=openkal` needs no engine // change. Applies to every target-side unit unconditionally — even @@ -11111,7 +11111,7 @@ prepare_build(bool print_fingerprint, } } - // `__openkal__` AND THE REALISED [c-abi] ENVIRONMENT — design + // `__OPENKAL__` AND THE REALISED [c-abi] ENVIRONMENT — design // 2026-09-18 §2.1, §3.2-§3.4. Broadcast into every package's OWN // `privateBuild`, the same channel `targetSideUsage` just used above: // it reaches that package's C/C++ compiles AND its dependency scan @@ -11120,7 +11120,7 @@ prepare_build(bool print_fingerprint, // itself and the driver's own defaults still come last. // // `c-environment = "platform"` (§3.4) opts a package OUT of the - // [c-abi] REALISATION ONLY — `__openkal__` still reaches it, because + // [c-abi] REALISATION ONLY — `__OPENKAL__` still reaches it, because // the exception is about the C environment a package's headers see, // not about whether its own code may call `kal_*`. The base command // line these tokens are appended to is untouched either way, which is @@ -11137,7 +11137,7 @@ prepare_build(bool print_fingerprint, // only their -D/-U/-I words, on purpose — a -std= or -O token meant // for the C compiler has no meaning for GAS), so the object-format // and wchar-width tokens have to be named again here, into the - // channel `unit_asm_flags` passes through UNFILTERED. `__openkal__` + // channel `unit_asm_flags` passes through UNFILTERED. `__OPENKAL__` // needs no second copy: it is a -D, and the -D/-U/-I filter already // carries it from `cflags` into every assembly unit. // @@ -11172,7 +11172,7 @@ prepare_build(bool print_fingerprint, if (tc && (tc->kernelAbiIsOpenkal || !tc->cEnvTokens.empty() || !tc->cEnvBuiltinsTokens.empty())) { for (auto& p : packages) { - // `__openkal__` is emitted above, with the rest of the + // `__OPENKAL__` is emitted above, with the rest of the // engine's own defines; it is NOT subject to the // `c-environment = "platform"` exception below, because that // exception is about which C environment a package's headers @@ -12489,14 +12489,14 @@ prepare_build(bool print_fingerprint, for (auto& f : mcpp::toolchain::graph_runtime_compile_flags(*tc)) flags += " " + f; } - // `__openkal__` AND THE REALISED [c-abi] ENVIRONMENT REACH THE STD + // `__OPENKAL__` AND THE REALISED [c-abi] ENVIRONMENT REACH THE STD // MODULE TOO (design §3.4: "环境作用于目标侧的全部编译单元... 以及图中 // 所有普通包"). The std module's own command is assembled here rather // than through `mcpp.toolchain.hostflags`'s shared string (see the // comment above), so it needs the same broadcast the ordinary // per-package loop gives every other unit — this is that same rule, // stated once more at the one site it cannot reach on its own. - if (tc->kernelAbiIsOpenkal) flags += " -D__openkal__"; + if (tc->kernelAbiIsOpenkal) flags += " -D__OPENKAL__"; if (pkg.manifest.cEnvironment != "platform") { for (auto& t : tc->cEnvTokens) flags += " " + t; for (auto& t : tc->cEnvBuiltinsTokens) flags += " " + t; @@ -12749,7 +12749,7 @@ prepare_build(bool print_fingerprint, fpi.cppStandard = m->package.standard; fpi.compileFlags = canonical_compile_flags(*m) + canonical_package_build_metadata(packages); - // [c-abi] REALISATION AND `__openkal__` PARTICIPATE IN THE FINGERPRINT + // [c-abi] REALISATION AND `__OPENKAL__` PARTICIPATE IN THE FINGERPRINT // (design 2026-09-18 §3.4, gap #4 of the design's own self-review). Two // builds whose C library declares `data-model = "lp64"` and `"llp64"` // compile the SAME source, against the SAME manifest, into objects whose diff --git a/src/toolchain/cenv.cppm b/src/toolchain/cenv.cppm index 3872e752..7a279389 100644 --- a/src/toolchain/cenv.cppm +++ b/src/toolchain/cenv.cppm @@ -28,9 +28,11 @@ // freestanding posix / arch-default the same one token, `-D__unix__`, for the // same reason: nothing here defines it either // Windows posix / arch-default Cygwin-flavoured: `--target=x86_64-pc-cygwin`, -// plus `-D__mcpp_target_windows__`; -// `__CYGWIN__` stays defined for now (see -// the note below the table — a sequence) +// plus `-U__CYGWIN__`/`-U__CYGWIN32__` +// (see the note below the table — the last +// step of a three-repository sequence); +// `__MCPP_TARGET_WINDOWS__` is the name that +// replaces them, broadcast by `mcpp.build.prepare` // * builtins = iso turn off the platform-C-library idioms the // code generator assumes (§3.2.1) — Apple's // `memset_pattern16` is the one measured case @@ -70,37 +72,48 @@ // identity links exactly like one compiled under the MinGW one. Only the // preprocessor saw a different environment; the linker never has to know. // -// `__CYGWIN__`/`__CYGWIN32__` ARE STILL DEFINED, AND `__mcpp_target_windows__` -// IS DEFINED BESIDE THEM. This module has held both answers about the -// borrowed name, and the sequence between them is the point. +// `__CYGWIN__`/`__CYGWIN32__` ARE WITHDRAWN, AND `__MCPP_TARGET_WINDOWS__` +// STANDS IN THEIR PLACE. This module has held both answers about the borrowed +// name, and the sequence between them is the point. // // A middle revision kept `__CYGWIN__` defined so that code needing to know // the object format would have a name, and wrote its own condition for -// reversal: a trade-off "for the 30-member measurement to settle — if +// reversal: a trade-off "for the 30-member measurement to settle --- if // defining it produces more new failures than it fixes, the answer flips". // // The measurement settled the THIRD-PARTY half (2026-09-20, 60 member-target // combinations): keeping it costs four members, each stopping at // `#include ` reached through `#if defined(_WIN32) || -// defined(__CYGWIN__)`. Upstream means "Win32 is available" by the name — +// defined(__CYGWIN__)`. Upstream means "Win32 is available" by the name --- // mimalloc says so in the guard's own comment, sqlite3 lists it under // `SQLITE_OS_WIN`. A BORROWED NAME MEANS WHAT THE LENDER'S HISTORY MADE IT // MEAN, not what the borrower intended by it. // -// WITHDRAWING IT WAS TRIED IN THIS BRANCH AND BROKE THIS ECOSYSTEM'S OWN +// WITHDRAWING IT WAS TRIED TOO EARLY ONCE, AND BROKE THIS ECOSYSTEM'S OWN // INSTALLED HEADERS. `openkal-musl`'s `bits/setjmp.h` and // `openkal-llvm-runtime`'s `__libunwind_config.h` both read `__CYGWIN__` to // size a Win64 register save area, deliberately: they are INSTALLED, an // application's own compile reads them, and a package-private define cannot // reach that compile. libunwind's `static_assert` failed loudly under -// cross-verification; setjmp.h's equivalent would not have — its own comment -// says "a mismatch nothing reports until the record overruns". +// cross-verification; setjmp.h's equivalent would not have --- its own +// comment says "a mismatch nothing reports until the record overruns". // // The measurement had counted third-party readers of the macro and not ours. -// So the withdrawal is a sequence: this release adds `__mcpp_target_windows__` -// (mcpp's own name, meaning what mcpp says it means), the two packages move -// onto it while still accepting `__CYGWIN__`, and only then does a release -// stop defining the borrowed one. Each intermediate state builds. +// So the withdrawal became a sequence, and it ran to completion across three +// repositories: +// +// 1. 2026.9.21.1 defined mcpp's own name beside the borrowed one, and +// 2026.9.21.2 re-spelt it `__MCPP_TARGET_WINDOWS__` while it still had +// no consumer. Purely additive at every point. +// 2. `openkal-musl@0.19.0` and `openkal-llvm-runtime@0.14.0` read the new +// name and keep `|| defined(__CYGWIN__)`, so they build on an engine +// from either side of this change. Published BEFORE step three. +// 3. this release stops defining the borrowed name. +// +// Each intermediate state builds, and the ordering between repositories --- +// packages first, engine last --- is the safety argument; see the withdrawal +// note at the emission site for the residual window it does not close. + export module mcpp.toolchain.cenv; import std; @@ -305,35 +318,59 @@ inline std::expected realise( // what says it anyway is NOT emitted here. `_WIN32`/`_WIN64` are // suppressed on purpose --- that is what presenting POSIX means // --- while the ABI is unchanged, and source that sizes a Win64 - // record still has to know. `__mcpp_target___` answers that, + // record still has to know. `__MCPP_TARGET___` answers that, // for every target and not only this one, and it is broadcast in // `mcpp.build.prepare` rather than realised here: it is a fact // about the TARGET, true whether or not any `[c-abi]` block // exists, so deriving it from a declaration would make its // absence ambiguous. See docs/21, "The macros mcpp defines". // - // `__CYGWIN__` IS STILL DEFINED, AND THAT IS A SEQUENCE, NOT A - // DECISION TO KEEP IT. The 30-member measurement settled that the - // borrowed name costs four members (archive, sqlite3, mimalloc, - // c-ares stop at `#include ` through - // `#if defined(_WIN32) || defined(__CYGWIN__)`; upstream means - // "Win32 is available" by it, as mimalloc's own guard comment - // says). Withdrawing it is right and is step three of three: + // `__CYGWIN__` IS WITHDRAWN HERE, AND THAT IS THE LAST STEP OF + // A SEQUENCE RATHER THAN A DECISION TAKEN ON ITS OWN. The + // 30-member measurement settled that the borrowed name costs four + // members (archive, sqlite3, mimalloc, c-ares stop at + // `#include ` through `#if defined(_WIN32) || + // defined(__CYGWIN__)`; upstream means "Win32 is available" by + // it, as mimalloc's own guard comment says). The three steps, in + // the order they were taken: + // + // 1. 2026.9.21.1 defined mcpp's own name beside the borrowed + // one; 2026.9.21.2 re-spelt it `__MCPP_TARGET_WINDOWS__`. + // Purely additive: every published package kept working. + // 2. `openkal-musl@0.19.0` and `openkal-llvm-runtime@0.14.0` + // read the new name, keeping `|| defined(__CYGWIN__)` so + // they build on either engine. PUBLISHED BEFORE THIS STEP. + // 3. this release stops defining it. + // + // STEP TWO SHIPPING FIRST IS THE WHOLE SAFETY ARGUMENT, and it is + // an ordering between REPOSITORIES that no test in this one can + // check. Taking step three first --- which an earlier revision of + // this branch did --- leaves every published copy of those two + // installed headers falling to its `#else`: the wrong record + // size. libunwind's `static_assert` catches its half loudly; + // `setjmp.h` says in its own comment that nothing reports the + // other until the record overruns. // - // 1. this release: define `__mcpp_target_windows__` as well. - // Purely additive; every published package keeps working. - // 2. the two packages above read the new name, keeping - // `|| defined(__CYGWIN__)` so they build on both engines. - // 3. a later release stops defining `__CYGWIN__`. + // THE RESIDUAL WINDOW IS NAMED RATHER THAN CLAIMED AWAY. A + // project pinning `openkal-musl` at 0.18.0 or earlier EXACTLY, + // and upgrading the engine past this release, gets that silent + // `#else`. Moving the index's `latest` onto 0.19.0 before this + // release is what keeps the window to exact pins; no mechanism + // available here closes it, because the engine cannot know which + // macros a package's installed headers read. // - // Step three taken first would leave every PUBLISHED copy of - // those headers falling to its `#else` --- the wrong record size, - // reported by nothing. A loud failure in four third-party members - // is the better state to hold for one release. + // `-U` RATHER THAN A DIFFERENT TRIPLE. The Cygwin identity is + // what supplies `__unix__` and suppresses `_WIN32`, which is the + // whole point of `presents = "posix"`; only the borrowed NAME is + // unwanted. Undefining it on the compile line keeps every other + // property of the substitution intact. r.tokens.push_back("--target=x86_64-pc-cygwin"); + r.tokens.push_back("-U__CYGWIN__"); + r.tokens.push_back("-U__CYGWIN32__"); r.expectDefined.push_back("__unix__"); - r.expectDefined.push_back("__CYGWIN__"); r.expectUndefined.push_back("_WIN32"); + r.expectUndefined.push_back("__CYGWIN__"); + r.expectUndefined.push_back("__CYGWIN32__"); cygwinIdentity = true; } else if (decl.presents == CAbiPresents::Windows) { // Already the base triple's own identity — nothing to add. diff --git a/src/toolchain/predefines.cppm b/src/toolchain/predefines.cppm index e44e1db3..76966c3c 100644 --- a/src/toolchain/predefines.cppm +++ b/src/toolchain/predefines.cppm @@ -30,18 +30,25 @@ // // ── NAMING ───────────────────────────────────────────────────────────────── // -// `__mcpp_`-prefixed, `__`-suffixed, lowercase, words separated by `_`. -// -// Lowercase rather than upper, and the reason is consistency rather than -// taste. Two conventions exist in the wild: VENDOR AND PRODUCT names are -// upper (`__APPLE__`, `_WIN32`, `__MINGW32__`), KIND-OF-SYSTEM names are -// lower (`__linux__`, `__unix__`, `__gnu_linux__`). These name kinds of -// target, they sit beside that second family in real guards --- `#if -// defined(__linux__) || defined(__mcpp_target_windows__)` --- and this -// project already published `__openkal__` in that spelling. One rule, stated -// and checked, is worth more than either convention argued in isolation. -// -// THE `__mcpp_` PREFIX IS LOAD-BEARING. A name mcpp owns means what mcpp says +// UPPER CASE, `__`-wrapped, words separated by `_`; the names mcpp owns carry +// `__MCPP_`. +// +// The convention in the wild splits by WHAT A NAME IS, not by who writes it. +// A vendor or product name is upper --- `__APPLE__`, `_WIN32`, `__MINGW32__`, +// `__GNUC__`. A kind-of-system name is lower --- `__linux__`, `__unix__`, +// `__gnu_linux__`. Every row this table OWNS is of the first kind: it names +// mcpp, or it names openkal. The kind-of-system question is answered by +// `__linux__` and its family, which mcpp SUPPLIES rather than owns and which +// therefore keep their lower-case spelling, for that exact reason. +// +// The lower-case spelling shipped first, and the reasoning that produced it +// is worth keeping because the mistake is easy to repeat: these names sit +// beside `__linux__` in real guards, so matching it looked like consistency. +// THAT CONFUSES ADJACENCY WITH KIND. `__APPLE__` sits in those same guards +// and is upper, because it belongs to somebody; so does every other name in +// them that belongs to somebody. Adjacency is not a naming rule. +// +// THE `__MCPP_` PREFIX IS LOAD-BEARING. A name mcpp owns means what mcpp says // it means. The alternative was tried: `__CYGWIN__` was left defined so that // code needing "PE object format, POSIX C environment" would have a name, and // a 30-member measurement found four members reading it as "Win32 is @@ -52,12 +59,33 @@ // // ── STABILITY ────────────────────────────────────────────────────────────── // -// An entry here is a published interface. Removing one, or narrowing when it -// is defined, is a breaking change for source this project does not control, -// and the failure is usually SILENT --- a `#if` selects the other branch and -// compiles. Withdrawal is therefore a sequence, never an edit: add the -// replacement, let consumers move onto it while still accepting the old name, -// and only then stop defining it. +// An entry here is a published interface, and withdrawing one is SILENT: a +// `#if` selects the other branch and compiles, on a machine nobody is +// watching. No mechanism available to a build tool makes that loud --- a +// preprocessor cannot be told to complain about a name it does not find. +// +// So a withdrawal is never decided by reading. It is decided by A MEASUREMENT +// THAT ENUMERATES READERS, and what the measurement finds sets how many steps +// the withdrawal takes. Both withdrawals this project has performed are on +// the record, and they came out differently: +// +// `__CYGWIN__` (2026.9.21.1). The enumeration found four third-party +// members reading it, and --- the half a first, sloppier count missed --- +// six sites in the two headers this ecosystem INSTALLS. Readers exist, so +// the withdrawal was a sequence: publish the replacement, move the +// consumers onto it while the old name is still defined, stop last. +// +// `__mcpp_target___` and `__openkal__` (2026.9.21.2). The enumeration +// found NONE. Across every repository of this ecosystem, not one source +// file and not one manifest read either name; the only occurrences were +// this engine's own emitter, its tests, and prose. Exposure was three days +// for `__openkal__` and a single release for the target macro, and both are +// names invented here, so no upstream code can be holding one. Zero readers +// collapses the sequence to one step. +// +// The rule did not change between those two; the count did. A withdrawal +// argued from anything other than a count is an argument this project has +// already got wrong once. export module mcpp.toolchain.predefines; import std; @@ -73,7 +101,7 @@ enum class When { }; struct Entry { - std::string_view spelling; // `` stands for the triple's `os` field + std::string_view spelling; // `` stands for the triple's `os` field When when; bool owned; // false: a standard name mcpp SUPPLIES, not owns std::string_view allowed; @@ -85,7 +113,7 @@ struct Entry { // wait for forever. `test_predefines.cpp` compares the two directions. inline constexpr std::array kContract {{ { - "__mcpp_target___", When::TargetOs, /*owned=*/true, + "__MCPP_TARGET___", When::TargetOs, /*owned=*/true, "learning the target's operating system when the C environment " "presented above it has suppressed the platform's own macros, and " "sizing a record by the target's ABI", @@ -93,7 +121,7 @@ inline constexpr std::array kContract {{ "for `cfg(os = ...)` in a package this project controls", }, { - "__openkal__", When::ResolvedLayer, /*owned=*/true, + "__OPENKAL__", When::ResolvedLayer, /*owned=*/true, "gating whether a call site invokes `kal_*` at all; its meaning is " "identical on every target", "selecting a header, inferring whether `_WIN32` is real, working " @@ -109,6 +137,19 @@ inline constexpr std::array kContract {{ }, }}; +// ASCII upper case. The triple's `os` field is a closed vocabulary of +// lower-case identifier tokens --- `linux`, `windows`, `macos`, `ios`, +// `emscripten`, `none` --- every one of them assigned from a literal in the +// parser, so this always yields a valid identifier. That is a property of +// another module, so `test_predefines.cpp` asserts it over the TARGET +// REGISTRY rather than over this sentence. +inline std::string upper(std::string_view s) { + std::string out(s); + for (char& c : out) + if (c >= 'a' && c <= 'z') c = static_cast(c - 'a' + 'A'); + return out; +} + // THE TOKENS FOR ONE BUILD, for the entries this module emits. // // `When::Realisation` entries are absent by construction: they belong to @@ -124,9 +165,9 @@ inline std::vector define_tokens(std::string_view targetOs, bool kernelAbiIsOpenkal) { std::vector out; if (!targetOs.empty()) - out.push_back(std::format("-D__mcpp_target_{}__=1", targetOs)); + out.push_back(std::format("-D__MCPP_TARGET_{}__=1", upper(targetOs))); if (kernelAbiIsOpenkal) - out.push_back("-D__openkal__"); + out.push_back("-D__OPENKAL__"); return out; } @@ -134,8 +175,8 @@ inline std::vector define_tokens(std::string_view targetOs, // can compare the table against `define_tokens` without knowing either. inline std::string spelling_for(const Entry& e, std::string_view targetOs) { std::string s(e.spelling); - if (const auto at = s.find(""); at != std::string::npos) - s.replace(at, 4, targetOs); + if (const auto at = s.find(""); at != std::string::npos) + s.replace(at, 4, upper(targetOs)); return s; } diff --git a/tests/e2e/741_c_abi_environment_declared_by_the_c_library.sh b/tests/e2e/741_c_abi_environment_declared_by_the_c_library.sh index 94e6997d..e2abb14a 100755 --- a/tests/e2e/741_c_abi_environment_declared_by_the_c_library.sh +++ b/tests/e2e/741_c_abi_environment_declared_by_the_c_library.sh @@ -167,7 +167,7 @@ def joined(argv_iter): # made it mean. The object-format question keeps no macro at all. consumer = joined(args_for("main.cpp")) missing = [tok for tok in ("--target=x86_64-pc-cygwin", "-fno-short-wchar", - "-D__mcpp_target_windows__=1") + "-D__MCPP_TARGET_WINDOWS__=1") if tok not in consumer] # The last of those comes from `mcpp.toolchain.predefines`, not from the # `[c-abi]` realisation -- a fact about the TARGET rather than about the @@ -206,7 +206,7 @@ if not asm_args: sys.exit(1) asm_joined = joined(iter(asm_args)) missing = [tok for tok in ("--target=x86_64-pc-cygwin", "-fno-short-wchar", - "-D__mcpp_target_windows__=1") + "-D__MCPP_TARGET_WINDOWS__=1") if tok not in asm_joined] if missing: print(f"FAIL: the assembly unit is missing realised tokens {missing} " diff --git a/tests/unit/test_cache_key.cpp b/tests/unit/test_cache_key.cpp index d2740777..d6529059 100644 --- a/tests/unit/test_cache_key.cpp +++ b/tests/unit/test_cache_key.cpp @@ -389,7 +389,7 @@ TEST(CacheKey, AHostedTargetHasNoTargetImpliedFlagsEitherWay) { // `manifest.buildConfig.cflags`/`cxxflags` at all — it is an ENGINE // BROADCAST, appended into `PackageRoot::privateBuild.cflags`/`cxxflags`/ // `asmflags` by `prepare.cppm` once the target side resolves (the same -// channel `targetSideUsage` and `-D__openkal__` use). `fill_package_config` +// channel `targetSideUsage` and `-D__OPENKAL__` use). `fill_package_config` // used to read only `manifest.buildConfig.cflags`/`cxxflags` — the // package's OWN declaration — so two builds of the identical package, // realising two DIFFERENT C environments (LP64 vs LLP64, say), produced the @@ -430,7 +430,7 @@ TEST(CacheKey, TwoDifferentRealisedCEnvironmentsDoNotShareASlot) { // A DURABLE GUARD FOR THE WHOLE CLASS THE PREVIOUS TEST FOUND ONE INSTANCE // OF — this predates the c-abi wave (`targetSideUsage`'s own broadcast, and -// `-D__openkal__`, had the identical exposure before [c-abi] existed) and +// `-D__OPENKAL__`, had the identical exposure before [c-abi] existed) and // will recur: `PackageRoot::privateBuild` (`mcpp.modgraph.scanner // ::UsageRequirements`) is the engine's OWN channel for "this reaches a // package's compile command line even though the package wrote nothing" — diff --git a/tests/unit/test_cenv.cpp b/tests/unit/test_cenv.cpp index 98a0ae4d..d03e6069 100644 --- a/tests/unit/test_cenv.cpp +++ b/tests/unit/test_cenv.cpp @@ -68,32 +68,43 @@ TEST(CEnv, MacosPosixArchDefaultDefinesUnix) { // Suppressing `_WIN32` is the point of presenting POSIX, but the ABI did not // change with the environment --- the register save areas are still Win64's, // and two INSTALLED headers in this ecosystem size records by that fact. -// `__mcpp_target___` answers it, for every target rather than only this +// `__MCPP_TARGET___` answers it, for every target rather than only this // one, and it belongs to `mcpp.toolchain.predefines`: a fact about the // TARGET is true whether or not any `[c-abi]` block exists, so realising it // from a declaration would make its absence ambiguous. This test therefore // asserts that the realisation does NOT carry it --- the separation is the // property, and a token appearing in both places would mean two owners. // -// `__CYGWIN__` IS STILL DEFINED, AND THAT IS A SEQUENCE. Withdrawing it is -// right --- the 30-member measurement settled that it costs four members, -// which read it as "Win32 is available" and reach `#include ` --- -// and withdrawing it FIRST was tried in this branch and broke the two headers -// above, because they read it too and nothing else names the target for them. -// libunwind's `static_assert` failed loudly; setjmp.h's equivalent would not -// have. So: add the name here, move those packages onto it, and only then -// stop defining the borrowed one. This test pins the first step. +// `__CYGWIN__` IS WITHDRAWN, AND THAT WAS THE LAST STEP OF A SEQUENCE. The +// 30-member measurement settled that the borrowed name costs four members, +// which read it as "Win32 is available" and reach `#include `. +// Withdrawing it FIRST was tried in this branch and broke the two headers +// above, because they read it too and nothing else named the target for them; +// libunwind's `static_assert` failed loudly, setjmp.h's equivalent would not +// have. So the order was: name it here, publish packages that read the new +// name, and only then stop defining the borrowed one. +// +// WHAT THIS TEST CAN AND CANNOT PIN. It pins the engine's half --- the +// tokens, and that the probe will verify them. It cannot pin the ORDERING, +// which holds between repositories: that `openkal-musl@0.19.0` and +// `openkal-llvm-runtime@0.14.0` were published BEFORE this release. That was +// measured instead, by building an openkal program for `x86_64-windows-gnu` +// against the PUBLISHED 0.13.0 on this engine, which fails on libunwind's +// two `static_assert`s exactly as predicted. An ordering between +// repositories has no assertion in either of them. TEST(CEnv, WindowsPosixArchDefaultSubstitutesTheCygwinTriple) { auto d = decl(ts::CAbiPresents::Posix, ts::CAbiDataModel::ArchDefault, 32); auto r = cenv::realise(d, "windows", "x86_64", false); ASSERT_TRUE(r.has_value()) << r.error(); EXPECT_TRUE(has(r->tokens, "--target=x86_64-pc-cygwin")); // The target's identity is not this module's to state: see above. - EXPECT_FALSE(has(r->tokens, "-D__mcpp_target_windows__=1")); - // `__CYGWIN__` is not withdrawn yet, and that is asserted, because the - // step that withdraws it must be a deliberate edit to this line rather - // than a silent drift. - EXPECT_FALSE(has(r->tokens, "-U__CYGWIN__")); + EXPECT_FALSE(has(r->tokens, "-D__MCPP_TARGET_WINDOWS__=1")); + // The borrowed name is undefined on the compile line. `-U` rather than a + // different triple: the Cygwin identity is what supplies `__unix__` and + // suppresses `_WIN32`, which is the point of `presents = "posix"`; only + // the NAME was unwanted. + EXPECT_TRUE(has(r->tokens, "-U__CYGWIN__")); + EXPECT_TRUE(has(r->tokens, "-U__CYGWIN32__")); // wchar 32 differs from Cygwin's own default (16) — the flag is added. EXPECT_TRUE(has(r->tokens, "-fno-short-wchar")); EXPECT_EQ(r->expectLongBytes, 8); @@ -103,9 +114,12 @@ TEST(CEnv, WindowsPosixArchDefaultSubstitutesTheCygwinTriple) { // THE PROBE CHECKS THESE, which is what keeps the tokens from being ones // the compiler could ignore: `cenv_probe::verify` compares the realised // configuration's predefines against these lists and refuses on a - // mismatch. A `-D` that did not take effect is a verification failure, - // not a silent one. - ASSERT_TRUE(has(r->expectDefined, "__CYGWIN__")); + // mismatch. A `-U` that did not take effect is a verification failure, + // not a silent one --- and silence is the whole risk here, since a `#if` + // that finds the name still defined simply compiles the other branch. + ASSERT_TRUE(has(r->expectUndefined, "__CYGWIN__")); + ASSERT_TRUE(has(r->expectUndefined, "__CYGWIN32__")); + EXPECT_FALSE(has(r->expectDefined, "__CYGWIN__")); } // wchar = 16 on the Cygwin substitution matches Cygwin's own default, so no diff --git a/tests/unit/test_predefines.cpp b/tests/unit/test_predefines.cpp index e07bfa75..32d7c7dc 100644 --- a/tests/unit/test_predefines.cpp +++ b/tests/unit/test_predefines.cpp @@ -12,12 +12,16 @@ #include #include +#include +#include #include #include import mcpp.toolchain.predefines; +import mcpp.toolchain.triple; namespace pd = mcpp::toolchain::predefines; +namespace triple = mcpp::toolchain::triple; namespace { @@ -75,9 +79,9 @@ TEST(Predefines, TheTargetMacroIsSpeltFromTheTripleField) { // The engine learns no operating-system name: a target added to the // triple parser gets its macro without an edit to this module. EXPECT_EQ(pd::define_tokens("freebsd", false).front(), - "-D__mcpp_target_freebsd__=1"); + "-D__MCPP_TARGET_FREEBSD__=1"); EXPECT_EQ(pd::define_tokens("none", false).front(), - "-D__mcpp_target_none__=1"); + "-D__MCPP_TARGET_NONE__=1"); } TEST(Predefines, OneMacroPerTargetAndItIsAlwaysPresent) { @@ -86,7 +90,7 @@ TEST(Predefines, OneMacroPerTargetAndItIsAlwaysPresent) { for (auto const& os : {"linux", "windows", "macos", "ios", "android", "none"}) { auto ms = emitted(os, false); const auto n = std::ranges::count_if(ms, [](std::string const& m) { - return m.starts_with("__mcpp_target_"); + return m.starts_with("__MCPP_TARGET_"); }); EXPECT_EQ(n, 1) << "os=" << os; } @@ -96,20 +100,20 @@ TEST(Predefines, OpenkalIsTiedToTheResolvedLayerAndNotToAnyTarget) { for (auto const& os : {"linux", "windows", "macos"}) { auto without = emitted(os, false); auto with = emitted(os, true); - EXPECT_EQ(std::ranges::find(without, "__openkal__"), without.end()); - EXPECT_NE(std::ranges::find(with, "__openkal__"), with.end()); + EXPECT_EQ(std::ranges::find(without, "__OPENKAL__"), without.end()); + EXPECT_NE(std::ranges::find(with, "__OPENKAL__"), with.end()); } } TEST(Predefines, AnEmptyTargetOsEmitsNoTargetMacro) { - // A triple that failed to parse must not produce `__mcpp_target___`. + // A triple that failed to parse must not produce `__MCPP_TARGET___`. auto ms = emitted("", false); EXPECT_TRUE(ms.empty()); } TEST(Predefines, EveryContractRowStatesBothWhatIsAllowedAndWhatIsNot) { // A rule with only a permission invites every use its author did not - // think of; the `__openkal__` entry has had a forbidden list since it was + // think of; the `__OPENKAL__` entry has had a forbidden list since it was // written, and the rest follow it. for (auto const& e : pd::kContract) { EXPECT_FALSE(e.allowed.empty()) << e.spelling; @@ -117,17 +121,61 @@ TEST(Predefines, EveryContractRowStatesBothWhatIsAllowedAndWhatIsNot) { } } -TEST(Predefines, EveryOwnedNameCarriesTheMcppPrefix) { - // A name mcpp owns means what mcpp says it means. A name it merely - // supplies -- `__unix__` -- is the standard one and must NOT be renamed - // into this project's namespace. +TEST(Predefines, EveryOwnedNameIsSpeltInThisProjectsConvention) { + // Upper case, `__`-wrapped. The convention splits by what a name IS: a + // vendor or product name is upper (`__APPLE__`, `_WIN32`), a + // kind-of-system name is lower (`__linux__`). Every owned row names a + // vendor or a product --- mcpp itself, or openkal --- so every one of + // them is upper. + // + // THE PREFIX IS NOT THE INVARIANT, and asserting it was would be wrong: + // `__OPENKAL__` is owned and names openkal, not mcpp. What every owned + // row shares is the spelling convention, which is what is checked here. + // A row mcpp merely SUPPLIES keeps the standard spelling, whatever it is, + // and must never be pulled into this project's namespace. for (auto const& e : pd::kContract) { - if (e.owned) - EXPECT_TRUE(e.spelling.starts_with("__mcpp_") || e.spelling == "__openkal__") - << e.spelling << " is owned and is spelt outside the namespace"; - else - EXPECT_FALSE(e.spelling.starts_with("__mcpp_")) + if (!e.owned) { + EXPECT_FALSE(e.spelling.starts_with("__MCPP_")) << e.spelling << " is supplied rather than owned, so it keeps " "the standard spelling"; + continue; + } + EXPECT_TRUE(e.spelling.starts_with("__") && e.spelling.ends_with("__")) + << e.spelling << " is owned and is not `__`-wrapped"; + for (char c : e.spelling) + EXPECT_FALSE(c >= 'a' && c <= 'z') + << e.spelling << " is owned and carries a lower-case letter"; } } + +TEST(Predefines, EveryTargetInTheRegistryYieldsAValidIdentifier) { + // The spelling is derived from the triple's own `os` field, which is why + // a target added to the parser needs no edit to `predefines.cppm`. The + // price of that is that an `os` carrying a character no identifier may + // hold --- a dot, a dash, a version suffix --- would produce a macro no + // compiler accepts, and the failure would land in the user's build rather + // than here. + // + // THE DENOMINATOR IS THE REGISTRY, not a list of names written beside + // this test. A list is a sample, and a sample cannot report the row + // somebody adds next year. + std::size_t rows = 0; + for (auto const& row : triple::known_targets()) { + const auto t = triple::parse(row.canonical); + ASSERT_TRUE(t.has_value()) << row.canonical << " does not parse"; + const auto toks = pd::define_tokens(t->os, false); + ASSERT_EQ(toks.size(), 1u) << row.canonical << " (os=" << t->os << ")"; + const std::string m = macro_of(toks.front()); + EXPECT_FALSE(m.empty()); + EXPECT_TRUE(std::isalpha(static_cast(m.front())) + || m.front() == '_') + << row.canonical << " -> " << m; + for (char c : m) + EXPECT_TRUE(std::isalnum(static_cast(c)) || c == '_') + << row.canonical << " -> " << m + << " is not a valid identifier"; + ++rows; + } + // A registry that enumerated nothing would pass every assertion above. + EXPECT_GT(rows, 10u) << "the target registry produced " << rows << " rows"; +} From 46e180ec2f27bf3b18f6d1293cc774a0db1d1a7b Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 04:12:50 +0800 Subject: [PATCH 02/11] an unanswered requirement says so, and the openkal branch this engine is verified against is overridable E3: A REQUIREMENT NOBODY ANSWERED READ EXACTLY LIKE A CONFIRMED ONE. Three situations exist and two of them build: the resolved implementation states a `provides-interfaces` list containing the requirement (build), states a list without it (refused), or states nothing at all (build). The third is deliberate --- the key postdates the implementations, and a graph that has not adopted it must keep building --- but until now it produced output identical to the first, so a consumer inspecting a green build could not tell "checked and agreed" from "never asked". note kernel-abi interfaces: fakekernel@0.1.0 states none, 2 requirements unchecked The note names the implementation taken from the RESOLVED layer: a reader told only that something went unchecked cannot act on it. `tests/e2e/743` gains two legs rather than one. Leg C asserts the note is present, leg D that it is absent when the provider does state its list. WITHOUT LEG D, LEG C PASSES AGAINST AN ENGINE THAT PRINTS THE LINE UNCONDITIONALLY, which measures nothing. Leg C also asserts the COUNT, the one part of the message the fixture determines: a note reporting "1" or "0" would satisfy every assertion that matches only an identifier. THE OPENKAL BRANCH IS OVERRIDABLE FOR ONE RUN This repository already has one half of a cross-verification protocol: the ecosystem repositories build against an mcpp PR branch through `MCPP_SOURCE_REF`, so an engine change is measured against them before it merges. The reverse was hard-coded to `main`, which makes a change that REQUIRES a coordinated ecosystem commit unverifiable until after that commit lands --- and unmergeable until then, since `openkal-cross` is the job that fails. `__CYGWIN__`'s withdrawal is the case that showed it. `openkal-cross` builds `openkal-llvm-runtime@main`, whose installed header read only the borrowed name, so this engine's own CI reproduced the ordering constraint as a red cell. The cell is CORRECT --- the packages must publish first --- but verifying the engine before that publish needs the input. Left empty, nothing changes. E2 (the link-time set difference) is recorded in the plan as withdrawn from this release, with its blocker corrected: it is not the workload the plan claimed but a design decision. `SURFACE.txt` is not installed by any package, and docs/22 states that the engine knows no member of either set --- so where an interface-to-symbol map lives, and whether the engine can read one without learning this ecosystem's vocabulary, has to be answered before the feature has a shape. --- ...kal-ecosystem-completion-and-acceptance.md | 98 ++++++++++++++++--- .github/workflows/openkal-cross.yml | 22 ++++- CHANGELOG.md | 15 +++ docs/22-target-side.md | 17 ++++ docs/zh/22-target-side.md | 13 +++ src/build/prepare.cppm | 37 +++++-- ...nterfaces_are_resolved_not_preprocessed.sh | 58 ++++++++++- 7 files changed, 236 insertions(+), 24 deletions(-) diff --git a/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md b/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md index 0b20cc7d..2f40a714 100644 --- a/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md +++ b/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md @@ -61,8 +61,9 @@ status: active #### E1 — P3:撤掉 `__CYGWIN__` 借用 -**状态**:**分三步,本版落地第一步**(mcpp 2026.9.21.1)。下文保留论证;落地形态与 -被交叉验证挡下的那一版见本节末。 +**状态**:**三步全部落地**。第一步 mcpp 2026.9.21.1(加名字)+ 2026.9.21.2(改大写); +第二步 `openkal-musl@0.19.0` 与 `openkal-llvm-runtime@0.14.0`;第三步 mcpp 2026.9.21.2 +停止定义借来的名字。下文保留论证;落地形态与被交叉验证挡下的那一版见本节末。 保留 `__CYGWIN__` 的本意是给「PE 格式 + POSIX C 环境」一个名字。实证否定了这个用法: @@ -121,17 +122,61 @@ status: active **静默**的——`setjmp.h` 自己的注释写着「a mismatch nothing reports until the record overruns」。libunwind 那处有 `static_assert` 才响,属于运气。 -**落地形态(第一步)**:`cenv.cppm` 的 Windows+Posix 分支加 `-D__mcpp_target_windows__=1`, -`expectDefined` 加该项与 `__CYGWIN__`——探针核对,一个没生效的 `-D` 是校验失败不是沉默。 -实测:三个通道(`.c`/`.cpp`/`.S`)全部到达;预定义为 `__unix__` + `__CYGWIN__` + -`__mcpp_target_windows__`,`_WIN32` 仍不存在。 +**落地形态(第一步)**:`cenv.cppm` 的 Windows+Posix 分支加 `-D__MCPP_TARGET_WINDOWS__=1` +(2026.9.21.1 发的是小写拼法,2026.9.21.2 在它还没有消费者时改成大写——命名约定见 +`src/toolchain/predefines.cppm` 与 `docs/21`),`expectDefined` 加该项——探针核对,一个 +没生效的 `-D` 是校验失败不是沉默。实测:三个通道(`.c`/`.cpp`/`.S`)全部到达;预定义为 +`__unix__` + `__MCPP_TARGET_WINDOWS__`,`_WIN32` 仍不存在。 -**为什么名字是 `__mcpp_target_windows__` 而不是设计里写的 `__mcpp_format_pe__`**:两个 +**为什么名字是 `__MCPP_TARGET_WINDOWS__` 而不是设计里写的 `__mcpp_format_pe__`**:两个 消费者要的不是「目标文件格式」,是**调用约定**(Win64 的寄存器保存区大小)。在这个目标上 两者同变,但按消费者实际问的那个问题命名更诚实。 -**第二、三步**:两个包改读新名字并保留 `|| defined(__CYGWIN__)`(在两种引擎上都能构建); -之后的版本再停止定义借来的那个。**先做第三步会让已发布的那两个头静默落进 `#else`。** +**第二、三步(已落地)**:两个包改读新名字并保留 `|| defined(__CYGWIN__)`,于是在本次改动 +两侧的引擎上都能构建;2026.9.21.2 的 `cenv.cppm` 在编译行加 `-U__CYGWIN__` / +`-U__CYGWIN32__`,`expectUndefined` 同步。 + +**次序是被测出来的,不是被断言的。** 它成立于**仓库之间**,所以任何一个仓库里的测试都 +检查不到它。在 2026.9.21.2 的引擎上,拿**已发布的** `openkal-llvm-runtime@0.13.0` 为 +`x86_64-windows-gnu` 构建一个 openkal 程序: + +``` +static assertion failed: x86_64 registers do not fit into unw_context_t +static assertion failed: UnwindCursor<> does not fit in unw_cursor_t +``` + +那就是「先做第三步」的读数,也就是第二步必须先发布的理由。`setjmp.h` 是沉默的那一半—— +它自己的注释写着「a mismatch nothing reports until the record overruns」。 + +**残余窗口被点名而不是被说没有**:把 `openkal-musl` **精确**钉在 0.18.0 或更早、同时把 +引擎升过 2026.9.21.2 的工程,会拿到那个静默的 `#else`。在引擎发布之前把索引的 `latest` +移到 0.19.0,是把窗口压到「精确钉」的办法;这里没有任何机制能把它关掉,因为引擎无从知道 +一个包安装出去的头读了哪些宏。 + +#### E1b — 引擎拥有的宏改为全大写(2026.9.21.2) + +**状态**:已落地,与 E1 第三步同一个 PR(用户要求:不分开,免得发布周期太长)。 + +`__mcpp_target___` → `__MCPP_TARGET___`,`__openkal__` → `__OPENKAL__`。 + +**约定按「名字是什么」分,不按谁写的分。** 厂商名与产品名大写(`__APPLE__`、`_WIN32`、 +`__MINGW32__`、`__GNUC__`),系统种类名小写(`__linux__`、`__unix__`)。mcpp **拥有**的 +每一行都属于第一类。小写那一版的推理是「它们在守卫里与 `__linux__` 并排,所以跟它一致」 +——**那是把「相邻」当成了「同类」**,而 `__APPLE__` 在同样那些守卫里却是大写。 + +**撤销小写拼法的代价是量出来的。** 分母是全生态每一个仓库,逐文件类型扫过: + +| 撤销的名字 | 安装头里的读者 | 包源码/清单里的读者 | 第三方读者 | 暴露 | 步数 | +|---|---|---|---|---|---| +| `__CYGWIN__` | **6 处**(两个头) | — | **4 个成员** | 上游二十年 | 3 | +| `__mcpp_target___` | 0 | 0 | 0(按构造) | 1 个发布 | 1 | +| `__openkal__` | 0 | 0 | 0(按构造) | 3 天 | 1 | + +「按构造」是指:两个名字都是在这里发明的,不可能有上游代码握着它们。**两次撤销之间规则 +没变,变的是数目**——而一次不由数目支撑的撤销,正是本项目已经犯错过一次的那种论证 +(E1 的第一个形态)。 + +用户先要求「两个拼法并存、文档推荐大写」,在看到代价读数后改为「代价不大就取消」。取消。 #### E2 — P7-L3:链接期集合差 @@ -144,12 +189,41 @@ overruns」。libunwind 那处有 `static_assert` 才响,属于运气。 **判据**:一个程序引用了实现未提供的接口里的符号,链接期被点名拒绝,且**声明正确时 零额外链接开销**。 -**阻塞**:需要把 `SURFACE.txt` 的「接口 → 符号」映射带到链接期。材料齐(`SURFACE.txt`、 -`kal_interfaces()`),是工作量不是未知数。 +**阻塞(2026-09-21 更正)**:**不是工作量,是一个设计决定。** 原文写「材料齐,是工作量 +不是未知数」,实查否掉了这个前提: + +1. **`SURFACE.txt` 不被安装。** `mcpplibs/openkal/mcpp.toml` 里没有任何一条把它装出去, + 所以链接期的引擎拿不到它。 +2. **`docs/22` 有一条明写的原则挡在前面**:「**引擎不认识两个集合里的任何一个成员**。对 + 它们做的唯一操作是集合差,所以规范可以新增一个接口而不需要发布 mcpp。」要在链接期 + 点名**接口**,引擎就必须持有「接口 → 符号」的映射;映射放进引擎会直接违反这条。 + +所以真正要先回答的是:**映射住在哪,而引擎能不能读它却不学会这个生态的词汇。** 两条候选: + +| 形态 | 代价 | +|---|---| +| 规范包安装 `SURFACE.txt`,清单新增一个键指向它 | 引擎要学会一种文件格式;新键 → 索引 floor 一轮 | +| 只报「解析出的实现声明了这 N 个接口,缺的符号不在它的导出里」 | 零新键、零新格式;**但点不出接口名**,弱于本项判据 | + +第二条是今天就能做的,第一条才满足判据。**在做出这个决定之前,把 E2 排进任何一个 PR 都是 +在实现一个还没选定的形状**——这正是「凭印象写下的『为什么不行』会否掉正确修法」的镜像: +凭印象写下的「为什么可以」同样会带来一个错的实现。 + +**后果**:E2 退出本轮(2026.9.21.2)。它不阻塞验收(A1/A2/A3 不依赖它),批次表已把它放在 +第四批。 #### E3 — 未被回答的 requirement 没有提示 -**状态**:未实现。记录:记忆 `an-unanswered-requirement-looks-like-a-confirmed-one`。 +**状态**:**已落地**(mcpp 2026.9.21.2,与 E1/E1b 同一个 PR)。实际输出: + +``` + note kernel-abi interfaces: fakekernel@0.1.0 states none, 2 requirements unchecked +``` + +判据落在 `tests/e2e/743` 的第三、四条腿上。**第四条腿是必需的**:没有它,第三条腿在一个 +无条件打印这行的引擎上同样会绿——两条腿互为对照,这正是「判据通过了但什么都没测到」 +那一类缺陷的形状。断言里包含**数目**(该夹具声明两条),因为数目是夹具唯一决定的那部分, +一个报「1」或「0」的提示仍会匹配所有只认标识符的断言。 三种情形两种绿:提供方声明且包含 → 构建(**已确认**);声明但不包含 → 拒绝; **什么都没声明 → 构建(从没被检查过)**。第三种是有意的(`provides-interfaces` 晚于 diff --git a/.github/workflows/openkal-cross.yml b/.github/workflows/openkal-cross.yml index 21d7cefa..b2bc67d3 100644 --- a/.github/workflows/openkal-cross.yml +++ b/.github/workflows/openkal-cross.yml @@ -57,6 +57,11 @@ name: openkal cross-build (3 hosts × 3 targets) on: pull_request: workflow_dispatch: + inputs: + openkal_ref: + description: "Branch of the openkal packages to build against (empty = main)" + required: false + default: "" concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} @@ -77,7 +82,22 @@ env: # engine against a tree the ecosystem had left behind — and the two # regressions found today both hid behind exactly this shape, a pin that # keeps a check green by keeping it out of date. - OPENKAL_BRANCH: main + # + # IT IS OVERRIDABLE FOR ONE RUN, AND THAT IS THE OTHER HALF OF A PROTOCOL + # THIS REPOSITORY ALREADY HAS ONE HALF OF. The ecosystem repositories build + # against an mcpp PR branch through `MCPP_SOURCE_REF`, so an engine change + # is measured against them before it merges. The reverse was hard-coded to + # `main`, which makes a change that REQUIRES a coordinated ecosystem commit + # unverifiable until after that commit lands --- and unmergeable until then, + # since this job is the one that fails. + # + # `__CYGWIN__`'s withdrawal is the case that showed it: this job builds + # `openkal-llvm-runtime@main`, whose installed header read only the borrowed + # name, so the engine's own CI reproduced the ordering constraint as a red + # cell. The constraint is real and the cell is correct --- the packages must + # publish first --- but verifying the engine BEFORE that publish needs this + # input. Left empty, nothing changes. + OPENKAL_BRANCH: ${{ github.event.inputs.openkal_ref || 'main' }} jobs: build: diff --git a/CHANGELOG.md b/CHANGELOG.md index a1143fd2..e3de8ab3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,21 @@ POSIX」的含义;不需要的只是那个**借来的名字**。 是把窗口压到「精确钉」的办法;这里没有任何机制能把它关掉,因为引擎无从知道一个包安装出去 的头读了哪些宏。 +### 一条没人回答的 requirement 会说出来 + +一共三种情形、其中两种能构建:提供方陈述了 `provides-interfaces` 且包含该需求(构建); +陈述了但不包含(拒绝);**什么都没陈述(构建,而在这行提示之前是静默的)**。第三种是有意的 +——`provides-interfaces` 晚于那些实现出现,一个还没采纳它的图必须照常构建——但它让 +**「是」与「没问成」同读数**。 + +``` + note kernel-abi interfaces: fakekernel@0.1.0 states none, 2 requirements unchecked +``` + +提示**点名解析出的那个实现**:一个只被告知「有东西没检查」的读者无法据此行动。提供方确实 +陈述了列表时这行不出现——`tests/e2e/743` 双向断言,新增的第四条腿正是为此:没有它,第三条腿 +在一个无条件打印这行的引擎上同样会绿,那样就什么都没测到。 + ### 自举钉 `.xlings.json` 的 workspace pin 从 2026.9.20.1 走到 2026.9.21.1。 diff --git a/docs/22-target-side.md b/docs/22-target-side.md index 04e8962c..f7c661e9 100644 --- a/docs/22-target-side.md +++ b/docs/22-target-side.md @@ -608,6 +608,23 @@ A graph whose implementation carries no `provides-interfaces` builds unchanged; the key postdates the packages, and the link still reports an absence in the vocabulary it always did. +**It says so, though (mcpp 2026.9.21.2).** Three situations exist and two of +them build — the provider states a list containing the requirement, states a +list without it (refused), or states nothing at all — and until this note the +first and the third produced identical output: + +``` + note kernel-abi interfaces: fakekernel@0.1.0 states none, 2 requirements unchecked +``` + +**A requirement nobody answered reads exactly like a confirmed one**, which is +the failure this closes: a consumer inspecting a green build could not tell +"checked and agreed" from "never asked". The note names the resolved +implementation, because a reader told only that something went unchecked +cannot act on it. It is absent whenever the provider does state its list, and +`tests/e2e/743` asserts both directions — without the second leg the first +would pass against an engine that printed the line unconditionally. + **What a C library does not supply (`[c-abi-absent]`, mcpp 2026.9.20.1).** The set of names a C library supplies is not enumerable in a manifest — POSIX has about twelve hundred — and enumerating it is the mistake §3.3 records diff --git a/docs/zh/22-target-side.md b/docs/zh/22-target-side.md index 8d1b7bb4..5405f82b 100644 --- a/docs/zh/22-target-side.md +++ b/docs/zh/22-target-side.md @@ -477,6 +477,19 @@ requires-interfaces = ["openkal.fs", "openkal.net"] **一个什么都没陈述的提供者,不是一个什么都不提供的提供者。** 实现方没有写 `provides-interfaces` 的图照常构建;这个键晚于那些包出现,而链接仍以它一贯的词汇报告缺席。 +**但它会说出来(mcpp 2026.9.21.2)。** 一共三种情形、其中两种能构建——提供方陈述了列表且 +包含该需求;陈述了列表但不包含(拒绝);什么都没陈述——而在这行提示之前,第一种与第三种 +产生一模一样的输出: + +``` + note kernel-abi interfaces: fakekernel@0.1.0 states none, 2 requirements unchecked +``` + +**一条没人回答的需求,读起来和一条被确认的需求一模一样**,这正是它要关掉的缺口:一个看着 +绿色构建的消费者,分不清「问过并且一致」与「从来没问成」。提示点名解析出的那个实现,因为 +一个只被告知「有东西没检查」的读者无法据此行动。提供方确实陈述了列表时这行不出现, +`tests/e2e/743` 双向断言——没有第二条腿的话,第一条腿在一个无条件打印这行的引擎上同样会绿。 + **一个 C 库不供给什么(`[c-abi-absent]`,mcpp 2026.9.20.1)。** 一个 C 库供给的名字集合 在清单里不可枚举——POSIX 约有一千二百个——而枚举它正是 §3.3 记录下撤回的那个错误。 例外是可枚举的: diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 2af61b11..d99f9fc4 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -11237,15 +11237,26 @@ prepare_build(bool print_fingerprint, providerId = pkg.manifest.package.name; break; } + // A REQUIREMENT NOBODY ANSWERED IS SAID SO, because otherwise + // "yes" and "never asked" are the same reading. + // + // Three situations exist and two of them build: the provider + // states a list and it contains the requirement (build); it + // states a list and does not (refuse, below); it states nothing + // at all (build, and until this note, in silence). The third is + // deliberate --- `provides-interfaces` is younger than the + // implementations that exist, and a graph that has not adopted it + // must keep building --- but a consumer reading a green build + // cannot tell it from the first. One line closes that, and it + // costs nothing to a graph where the provider does declare. + std::size_t uncheckedRequirements = 0; for (auto& pkg : packages) { const auto& need = pkg.manifest.kernelAbiRequiresInterfaces; if (need.empty()) continue; - // A consumer that names interfaces while no package in the - // graph states what it provides is not refused: the provider - // predates this key, and a graph that has not yet adopted it - // must keep building. The link still reports the absence, in - // the vocabulary it always did. - if (providerId.empty()) continue; + if (providerId.empty()) { + uncheckedRequirements += need.size(); + continue; + } auto missing = mcpp::targetside::interfaces_not_provided( need, providedInterfaces); if (missing.empty()) continue; @@ -11288,6 +11299,20 @@ prepare_build(bool print_fingerprint, providedInterfaces.size() == 1 ? "" : "s", pkg.manifest.package.name)); } + + if (uncheckedRequirements > 0) { + // THE IMPLEMENTATION IS NAMED FROM THE RESOLVED LAYER, not + // from whichever package happened to be first: the note has + // to say WHOSE silence this is, or a reader cannot act on it. + const auto& impl = resolvedTargetSide.kernelAbi.impl; + mcpp::ui::info("note", std::format( + "kernel-abi interfaces: {} states none, {} requirement{} " + "unchecked", + impl.empty() ? std::string("the resolved implementation") + : impl, + uncheckedRequirements, + uncheckedRequirements == 1 ? "" : "s")); + } } if (auto why = tsd::check_layering(resolvedTargetSide)) { diff --git a/tests/e2e/743_kernel_abi_interfaces_are_resolved_not_preprocessed.sh b/tests/e2e/743_kernel_abi_interfaces_are_resolved_not_preprocessed.sh index d0020318..d9a9c3fb 100755 --- a/tests/e2e/743_kernel_abi_interfaces_are_resolved_not_preprocessed.sh +++ b/tests/e2e/743_kernel_abi_interfaces_are_resolved_not_preprocessed.sh @@ -15,13 +15,17 @@ # that is why every macro-shaped answer to it has had to be replaced by the # next one. # -# Three legs: +# Four legs: # A a requirement the implementation provides: builds. # B a requirement it does not: refused, naming the interface and both # packages, with nothing compiled. -# C a graph whose implementation states nothing: builds. The key postdates -# the provider, and a graph that has not adopted it must keep building -- -# the link still reports the absence in the vocabulary it always did. +# C a graph whose implementation states nothing: builds, AND SAYS SO. The +# key postdates the provider, and a graph that has not adopted it must +# keep building -- but a green build in which nobody checked reads +# exactly like one in which everybody agreed, so the note names it. +# D the mirror of C: when the provider does state a list, that note is +# ABSENT. Without this leg C would pass against an engine that printed +# the note unconditionally, which measures nothing. set -e MCPP="${MCPP:-mcpp}" @@ -188,6 +192,50 @@ out=$("$MCPP" build 2>&1) || { echo "$out" >&2 exit 1 } -echo "OK: C (a provider that states nothing is not refused)" +# AND THE SILENCE IS NAMED. Three situations exist and two build: the +# provider lists the interface (A), it lists others (B, refused), it lists +# nothing (here). Without this line the first and the third produce the same +# output, so a reader cannot tell "checked and agreed" from "never asked". +echo "$out" | grep -q "kernel-abi interfaces" || { + echo "FAIL: a build in which no provider stated a list must say so --" \ + "otherwise an unanswered requirement reads as a confirmed one" >&2 + echo "$out" >&2 + exit 1 +} +echo "$out" | grep -q "fakekernel" || { + echo "FAIL: the note must name WHOSE silence this is; a reader told only" \ + "that something went unchecked cannot act on it" >&2 + echo "$out" | grep -m1 "kernel-abi interfaces" >&2 + exit 1 +} +# THE COUNT, BECAUSE IT IS THE ONE PART THE FIXTURE DETERMINES. The root +# declares two requirements; a note that said "1" or "0" would still match +# every assertion above while reporting something other than what was asked. +echo "$out" | grep -q "2 requirements unchecked" || { + echo "FAIL: the note must report how many requirements went unchecked;" \ + "this graph asked for two" >&2 + echo "$out" | grep -m1 "kernel-abi interfaces" >&2 + exit 1 +} +echo "OK: C (a provider that states nothing is not refused, and is named)" + +# ── D. the mirror: a provider that states a list produces no such note ──── +rm -rf target +write_impl '"openkal.abort", "openkal.stream", "openkal.memory", "openkal.fs"' +write_root '"openkal.fs", "openkal.stream"' +out=$("$MCPP" build 2>&1) || { + echo "FAIL: leg D's graph is leg A's and must build" >&2 + echo "$out" >&2 + exit 1 +} +echo "$out" | grep -q "kernel-abi interfaces" && { + echo "FAIL: the note belongs to the case where nothing was checked. A" \ + "graph whose provider DID state its list was checked, and printing" \ + "it here would make leg C pass against an engine that always" \ + "printed it" >&2 + echo "$out" | grep -m1 "kernel-abi interfaces" >&2 + exit 1 +} +echo "OK: D (a provider that states its list draws no note)" echo "OK" From 6845a82b823f4d7cf7ccc5504a064c685a94e4bb Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 04:19:04 +0800 Subject: [PATCH 03/11] plan: C2 is two different things, and only one of them is C2 Reading the measured diagnostics and the recipes apart: curl lib/setopt.c:31: 'linux/tcp.h' file not found cmp-module asio/detail/config.hpp:899: 'linux/version.h' file not found CURL IS A RECIPE DEFECT, the same shape as C3/expat. `compat.curl.lua` generates `#define HAVE_LINUX_TCP_H 1` inside `#if defined(__linux__)`. openkal runs on the Linux kernel, so `__linux__` is CORRECT; what is wrong is the recipe reading it as "glibc's whole Linux userspace is installed". The same block asserts `HAVE_GLIBC_STRERROR_R` --- openkal-musl is musl, so that one is actively false --- along with `HAVE_SYS_EVENTFD_H`, `HAVE_FSETXATTR` and a hard-coded host path in `CURL_CA_BUNDLE`. The honest test is `__has_include()`: standard C, and it asks the question being asked rather than inferring which headers exist from which kernel. CMP-MODULE IS THE REAL C2. asio's `#include ` sits outside every `ASIO_DISABLE_*` guard, so no configuration macro prevents it, and no manifest key reaches inside a third-party header --- which is justification one in `predefines.cppm`. `members.toml` already has an `[excluded]` table whose stated semantics are exactly "cannot be built in any openkal graph". What is missing is a second reason category, not a second table. E2's blocker is corrected in the same pass: not the workload the plan claimed but a design decision, since SURFACE.txt is installed by nothing and docs/22 states the engine knows no member of either set. --- ...kal-ecosystem-completion-and-acceptance.md | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md b/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md index 2f40a714..30e471c4 100644 --- a/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md +++ b/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md @@ -255,6 +255,33 @@ static assertion failed: UnwindCursor<> does not fit in unw_cursor_t #### C2 — `linux/` uapi 头(curl, cmp-module) +**2026-09-21 更正:这是两件不同的事,本文初稿把它们并成了一类。** 逐条读了实测诊断与 +配方之后: + +| 成员 | 诊断 | 真正的归属 | +|---|---|---| +| `curl` | `lib/setopt.c:31: 'linux/tcp.h' file not found` | **配方缺陷**,与 C3/expat 同形状 | +| `cmp-module` | `asio/detail/config.hpp:899: 'linux/version.h' file not found` | **真的 C2** | + +**curl 是配方缺陷。** `pkgs/c/compat.curl.lua` 生成的 `curl_config.h` 里有 +`#define HAVE_LINUX_TCP_H 1`,位于 `#if defined(__linux__)` 之内。openkal 跑在 Linux +内核上,`__linux__` **是对的**;错的是配方把它读成了「glibc 的整套 Linux userspace 头 +都装好了」。同一个块里还有 `HAVE_GLIBC_STRERROR_R`(openkal-musl 是 musl,不是 glibc, +这一条**主动是错的**)、`HAVE_SYS_EVENTFD_H`、`HAVE_FSETXATTR`、以及一条写死的宿主路径 +`CURL_CA_BUNDLE "/etc/ssl/certs/..."`。**诚实的判据是 `__has_include()`** +——它是 C 标准的、问的正是要问的那件事,而不是从「哪个内核」推断「哪些头存在」。 + +**cmp-module 才是 C2。** asio 的 `detail/config.hpp` 写的是: + +```c +#if defined(__linux__) +# include /* 在所有 ASIO_DISABLE_* 守卫之外 */ +``` + +那个 `#include` **不受任何配置宏控制**——`-DASIO_DISABLE_EPOLL` 挡不住它。**源码不归 +我们改,而没有任何清单键伸得进第三方的 `.c`/`.hpp` 里**(这正是 `predefines.cppm` 记的 +第一条理由)。所以这个成员在面向 Linux 的 openkal 图里按构造建不起来。 + **形态**:程序 `#include ` 一类。openkal 不是 Linux,没有 uapi 头, **这是正确的**。 @@ -269,6 +296,13 @@ static assertion failed: UnwindCursor<> does not fit in unw_cursor_t **归属**:mcpp-index(测量的成员表),不是 C 库。 +**已有机制比初稿以为的多一半。** `members.toml` 里已经有 `[excluded]` 表,它的语义正是 +「在任何 openkal 图里都建不起来」——`cmp-module` 恰好符合。缺的不是表,是**第二个理由 +类别**:现有六条都是「宿主程序 / 厂商二进制」,而 asio 这条是「上游源码无条件包含平台 +头」。判据不变:声明存在时该成员从分母里移除,且理由可追溯到那条声明。 + +**curl 不进这张表**,它要修配方。两者分开之后,九条失败里这一类只剩一条。 + #### C3 — `arc4random_buf`(expat)——**已修复,且归属与初稿不同** **初稿写的是**「musl 有 `arc4random_buf`,openkal-musl 的移植未导出」。**这是错的。** From a7b52d940512d7de6a95fcfb123622d9beb372a6 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 04:28:17 +0800 Subject: [PATCH 04/11] plan: the cross-verification protocol has a structural hole, found while using it Both ecosystem packages went green against the mcpp PR branch, AND NEITHER GREEN TOUCHED `__MCPP_TARGET_WINDOWS__`: openkal-musl linux/gcc, linux/llvm, macos/llvm; the cross-link row is Linux<->macOS openkal-llvm-runtime x86_64-linux-gnu and riscv64-none-elf No package's CI in this ecosystem builds `x86_64-windows-gnu`, and the whole subject of this change is a macro that exists only on Windows targets. The greens are real and they measure something else --- a criterion that ran, passed, and whose object was not present. The only job that builds that target is mcpp's own `openkal-cross`, and it had the ecosystem branch hard-coded to `main`, so an engine change requiring a coordinated ecosystem commit was unverifiable until after that commit landed. `openkal_ref` closes it; empty means today's behaviour. Consequence for ordering: the example in that job reaches the runtime through `path = "../.."` and the runtime's manifest pins `openkal-musl = "0.19.0"` from the INDEX, so musl 0.19.0 has to be registered before the input can be used. --- ...kal-ecosystem-completion-and-acceptance.md | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md b/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md index 30e471c4..22d9a96c 100644 --- a/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md +++ b/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md @@ -153,6 +153,33 @@ static assertion failed: UnwindCursor<> does not fit in unw_cursor_t 移到 0.19.0,是把窗口压到「精确钉」的办法;这里没有任何机制能把它关掉,因为引擎无从知道 一个包安装出去的头读了哪些宏。 +#### E1c — 交叉验证协议的一个结构性漏洞:没有人建 Windows-over-openkal + +**2026-09-21 实测发现,在用协议验 E1 的过程中。** 两个生态包对着 mcpp 的 PR 分支跑的 +交叉验证都绿了,而**那两个绿与 `__MCPP_TARGET_WINDOWS__` 无关**: + +| 仓库 | 它的 CI 实际建的目标 | +|---|---| +| `openkal-musl` | 矩阵三行:linux/gcc、linux/llvm、macos/llvm;cross-link 那条是 Linux↔macOS | +| `openkal-llvm-runtime` | `x86_64-linux-gnu` + `riscv64-none-elf`(裸机,qemu) | + +**整个生态里没有一个包的 CI 建 `x86_64-windows-gnu`。** 而这次改动的全部主题,是一个 +**只在 Windows 目标上存在**的宏。所以两个包的绿是真的,只是它测的不是这件事—— +[[a-passing-criterion-that-measured-nothing]] 的又一个形态:判据跑了、通过了、而它的 +对象根本不在场。 + +**唯一建那个目标的是 mcpp 自己的 `openkal-cross`**(3 宿主 × 3 目标)。而它把要验的 +生态分支写死成 `OPENKAL_BRANCH: main`——于是一个**需要生态协同提交**的引擎改动,在那个 +提交落地之前既无法验证、也无法合入(因为正是这个 job 会红)。 + +**修法**:`openkal-cross.yml` 增加 `workflow_dispatch` 输入 `openkal_ref`,留空时行为 +不变。这是协议的另一半:生态仓库早就能用 `MCPP_SOURCE_REF` 指到 mcpp 的 PR 分支,反向 +一直没有。 + +**次序上的后果**:`openkal-cross` 里那个示例通过 `path = "../.."` 依赖 runtime,而 +runtime 的清单从**索引**钉 `openkal-musl = "0.19.0"`。所以用 `openkal_ref` 验之前, +musl 0.19.0 必须先登记进索引。 + #### E1b — 引擎拥有的宏改为全大写(2026.9.21.2) **状态**:已落地,与 E1 第三步同一个 PR(用户要求:不分开,免得发布周期太长)。 From 102bfde03a5b453ec5b5c992a1ff14eecfec7573 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 04:32:03 +0800 Subject: [PATCH 05/11] plan: correct the Windows-coverage finding --- the runtime does have that leg Read job by job instead of from the first job of a run: openkal-llvm-runtime `host-dimension` has a Windows-host row that builds every target, including x86_64-windows-gnu, and it resolved openkal-musl@0.19.0 on the PR engine. The Windows leg of E1 WAS verified. openkal-musl linux/gcc, linux/llvm, macos/llvm, a Linux<->macOS cross-link, and a start-on-the-other-system job. No Windows cell at all. I concluded from the first job alone that no package in the ecosystem builds Windows-over-openkal. That was wrong and the conclusion it cast doubt on stands. The real gap is smaller and still a gap: the package whose change this round is `bits/setjmp.h` --- an installed header whose only branch is about Windows --- has no Windows cell of its own. It is verified today only transitively, because the runtime's CI pulls musl in as a path dependency and compiles it along the way. --- ...kal-ecosystem-completion-and-acceptance.md | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md b/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md index 22d9a96c..b71301b9 100644 --- a/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md +++ b/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md @@ -155,18 +155,23 @@ static assertion failed: UnwindCursor<> does not fit in unw_cursor_t #### E1c — 交叉验证协议的一个结构性漏洞:没有人建 Windows-over-openkal -**2026-09-21 实测发现,在用协议验 E1 的过程中。** 两个生态包对着 mcpp 的 PR 分支跑的 -交叉验证都绿了,而**那两个绿与 `__MCPP_TARGET_WINDOWS__` 无关**: +**2026-09-21 实测发现,在用协议验 E1 的过程中。** 逐 job 核对两个包的交叉验证运行: -| 仓库 | 它的 CI 实际建的目标 | -|---|---| -| `openkal-musl` | 矩阵三行:linux/gcc、linux/llvm、macos/llvm;cross-link 那条是 Linux↔macOS | -| `openkal-llvm-runtime` | `x86_64-linux-gnu` + `riscv64-none-elf`(裸机,qemu) | - -**整个生态里没有一个包的 CI 建 `x86_64-windows-gnu`。** 而这次改动的全部主题,是一个 -**只在 Windows 目标上存在**的宏。所以两个包的绿是真的,只是它测的不是这件事—— -[[a-passing-criterion-that-measured-nothing]] 的又一个形态:判据跑了、通过了、而它的 -对象根本不在场。 +| 仓库 | 它的 CI 建的目标 | 有没有 Windows 目标 | +|---|---|---| +| `openkal-llvm-runtime` | `runtime`(linux + riscv 裸机)、`host-dimension` 矩阵(macOS 宿主、**Windows 宿主 → 每个目标**)、两个「产物在那个系统上跑」 | **有** | +| `openkal-musl` | linux/gcc、linux/llvm、macos/llvm、cross-link(Linux↔macOS)、「跨建的产物在那边启动」 | **没有** | + +**先写下更正,因为我第一次读错了。** 只看 runtime 运行里的第一个 job,我下过结论说 +「整个生态没有一个包建 Windows-over-openkal」。那是错的:`host-dimension` 的 Windows +行实测建了 `x86_64-windows-gnu`,并解析到 `openkal-musl@0.19.0`。**E1 的 Windows 那条腿 +确实被验到了**,结论成立。 + +**真正的缺口比那小,但仍然是缺口:`openkal-musl` 自己没有任何 Windows 格子。** 而本轮 +它改的恰恰是 `bits/setjmp.h`——一个**只在 Windows 目标上有分支**的已安装头。它今天被验到 +是**传递的**:runtime 的 CI 把 musl 当 path dep 拉进去,顺带编了它。一个包的关键改动 +由另一个仓库的 CI 代为验证,是[[a-passing-criterion-that-measured-nothing]] 的邻居—— +它今天对,是因为恰好有人在别处建了那个目标。 **唯一建那个目标的是 mcpp 自己的 `openkal-cross`**(3 宿主 × 3 目标)。而它把要验的 生态分支写死成 `OPENKAL_BRANCH: main`——于是一个**需要生态协同提交**的引擎改动,在那个 From 0e10e35d9b0ef3af5b1bdb2821b3758c95592a16 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 04:42:23 +0800 Subject: [PATCH 06/11] the second layer of __cxa_thread_atexit is located, and the first hypothesis was right Following the criterion the finding itself wrote down --- instrument `run_dtors` --- with `&dtors` printed at both sites: registered dtor, dtors=0x7ffffe994680, &dtors=0x7ffffe9946a8 run_dtors called, dtors=0, &dtors=0x7ffffe9946c8 `run_dtors` IS called, which retires one branch. `&dtors` DIFFERS three times in one thread, which gives the other: `__thread` is emutls here, emutls keeps its per-thread blocks behind a pthread key of its own, that key's destructor had already released this thread's block, and every read afterwards allocates a fresh zeroed one. HYPOTHESIS ONE IN SECTION 4 WAS CORRECT AND WAS RECORDED AS REFUTED. What was wrong was the probe: musl runs key destructors in creation order, and that probe created its own key BEFORE first touching a thread-local, so emutls outlived it. The real ordering is the reverse. A probe cannot report an ordering it was constructed to avoid --- the predicate was right and the object's construction excluded the condition under test. Fixed in openkal-llvm-runtime 0.15.0 by keeping the list in the TLS key's own value, which no other key's teardown can reach. Two conditions asserted, in examples/cxx, on both targets: the thread_local is constructed, and its destructor runs. --- .../2026-09-20-cxa-thread-atexit-finding.md | 66 ++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/.agents/docs/2026-09-20-cxa-thread-atexit-finding.md b/.agents/docs/2026-09-20-cxa-thread-atexit-finding.md index 328cb416..0518ccb7 100644 --- a/.agents/docs/2026-09-20-cxa-thread-atexit-finding.md +++ b/.agents/docs/2026-09-20-cxa-thread-atexit-finding.md @@ -1,9 +1,14 @@ --- subject: review -status: active +status: resolved --- -# `__cxa_thread_atexit` 在 openkal-Windows 上:定位到一层,第二层未定位 +# `__cxa_thread_atexit` 在 openkal-Windows 上:两层都已定位并修复 + +> **2026-09-21 收尾。** 第二层已定位,修法已实测,发在 +> `openkal-llvm-runtime@0.15.0`。下文 §1–§5 保留当时的记录(包括两个被否掉的假设), +> §7 是结论。**§4 的第一个假设当时被判为「否」,而它其实是对的——错的是那次探针的 +> 构造,见 §7。** - 日期:2026-09-20 - 来源:mcpp-index 的 30-member 重测,doctest 与 spdlog 两个成员停在 @@ -117,3 +122,60 @@ Windows : __cxa_thread_atexit_impl = 0 -> fallback branch - 被调用而链表为空 ⇒ 注册那一侧的问题 - 没被调用 ⇒ `__libcpp_tls_create` / key 注册那一侧的问题 2. 无论结论如何,修法必须让「析构会跑」与「链接会过」同时成立,或者两者都不成立。 + + +--- + +## 7. 第二层:已定位(2026-09-21) + +### 读数 + +按 §6 写下的第一条判据做——在 fallback 的 `run_dtors` 里打一行,并同时打印 +`&dtors`: + +``` +[probe] DtorsManager ctor: creating key +[probe] registered dtor, dtors=0x7ffffe994680, key=0x2, &dtors=0x7ffffe9946a8 +[prog] in thread, v=7 +[probe] run_dtors called, dtors=0, alive=0, &dtors=0x7ffffe9946c8 +[prog] after join, ran=0 (expect 7) +[probe] run_dtors called, dtors=0, alive=0, &dtors=0x7ffffe994708 +``` + +`run_dtors` **被调用了**——§6 的第二支排除。而 **`&dtors` 三次都不同**,在同一个线程里。 + +### 真因 + +`__thread DtorList* dtors` 在本包为 PE 采用的 `-femulated-tls` 下由 emutls 提供。emutls +把每线程的块挂在它**自己的**一个 pthread key 后面,而那个 key 的析构已经先释放了本线程 +的块;之后每次读都新分配一个**清零**的块,所以地址每次都不一样。`run_dtors` 走的是空链表。 + +### §4 假设一其实是对的,错的是那次探针的构造 + +当时写的是:「PE 上 `thread_local` 走 emutls,它自己的 pthread key 先于 libc++abi 的 key +被析构,于是 `run_dtors` 读到的链表已经空了」——**这就是真因**。 + +那次探针之所以读到 42,是因为 musl **按 key 的创建顺序**逐个调析构,而探针自己 +`pthread_key_create` 在第一次访问 `thread_local` **之前**,于是 emutls 的 key 排在它后面、 +析构也在它之后。真实情形里 libc++abi 的 `dtors_key`(实测 `key=0x2`)排在 emutls 之后。 + +**一个探针报不出它被构造成不会发生的那个顺序。** 判据落在了一个正确的谓词上,而对象的 +构造恰好排除了被测的那个条件——这与 [[a-check-that-picks-its-object-by-convention]] 同族。 + +### 修法 + +链表存进 **key 自己的值**。key 的析构函数本来就被交给这个值,而任何别的 key 的拆除都碰 +不到它。`dtors_alive` 随之不需要:值非空就是「链表在」。零新机制。 + +### 判据(两条,缺一不可) + +`examples/cxx`,两个目标: + +``` +ok: a thread_local is constructed in a spawned thread +ok: and its destructor runs when that thread ends +``` + +`x86_64-linux-gnu` 与 `x86_64-windows-gnu`(wine)均 `failures: 0`。只断言「链接通过」 +或只断言「构造发生」的判据会同时放过两层——这正是 §5 决定不发第一层补丁的那个理由, +现在它变成了判据本身的形状。 From 8638942f6f6601cbc3df83dab59d24754b3fd1ab Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 04:43:05 +0800 Subject: [PATCH 07/11] plan: C1 is closed --- the only true unknown in the list Both layers located and fixed, shipping in openkal-llvm-runtime 0.15.0. The second layer: `__thread` is emutls under `-femulated-tls`, emutls keeps its per-thread blocks behind a pthread key of its own, and that key's destructor releases this thread's block before libc++abi's runs. Criterion: `&dtors` differs three times in one thread. It was closed by the criterion the finding document itself wrote down. And the hypothesis that document recorded as refuted was correct --- the probe created its own key before first touching a thread-local, and musl runs key destructors in creation order, so emutls outlived it. The predicate was right; the object's construction excluded the condition under test. --- ...kal-ecosystem-completion-and-acceptance.md | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md b/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md index b71301b9..845b1329 100644 --- a/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md +++ b/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md @@ -273,17 +273,39 @@ musl 0.19.0 必须先登记进索引。 #### C1 — `__cxa_thread_atexit` -**状态**:第一层可修但**不能只修第一层**;第二层未定位。记录: -`.agents/docs/2026-09-20-cxa-thread-atexit-finding.md`。 +**状态**:**两层都已定位并修复**,发在 `openkal-llvm-runtime@0.15.0`。记录: +`.agents/docs/2026-09-20-cxa-thread-atexit-finding.md` §7。 + +**第二层的真因**:`__thread DtorList* dtors` 在 `-femulated-tls` 下由 emutls 提供,而 +emutls 把每线程的块挂在它自己的一个 pthread key 后面;那个 key 的析构先释放了本线程的块, +之后每次读都新分配一个**清零**的块。判据是同一线程里 `&dtors` 三次不同 +(`...6a8` / `...6c8` / `...708`)。 + +**本清单里「唯一的未知数」是被它自己写下的判据关掉的**——发现文档 §6 写的第一步就是 +「在 fallback 里打一行,看 `run_dtors` 到底有没有被调用」。它被调用了,而链表是空的。 + +**并且:§4 那条被判为「否」的假设其实是对的。** 那次探针的 `pthread_key_create` 排在第一次 +访问 `thread_local` **之前**,而 musl 按创建顺序调 key 析构,于是 emutls 反而活得更久, +读到了期望值。真实情形顺序相反。**一个探针报不出它被构造成不会发生的那个顺序**——谓词是 +对的,对象的构造把被测条件排除掉了。 + +**修法**:链表存进 key 自己的值(析构函数本来就被交给它),零新机制。 只补符号会把一个**构建期的响亮失败**换成一个**运行期的静默失败**:链接过了, `thread_local` 的析构不跑。补丁试过并**主动回退**,因为验证显示析构确实没执行。 -**判据**:最小复现(五行,文档里有)在 `x86_64-windows-gnu` 上**链接通过且析构函数 -真的执行**——两个条件缺一不可。只断言链接通过是错的判据。 +**判据(已通过)**:`examples/cxx` 两条断言,两个目标: + +``` +ok: a thread_local is constructed in a spawned thread +ok: and its destructor runs when that thread ends +``` + +`x86_64-linux-gnu` 与 `x86_64-windows-gnu`(wine)均 `failures: 0`。只断言链接通过、 +或只断言构造发生,都会同时放过两层——这正是当初决定不发第一层补丁的理由,现在它变成了 +判据本身的形状。 -**阻塞**:第二层未定位(emutls 在 PE 上的注册路径)。这是本清单里**唯一一个真正的 -未知数**。 +**阻塞**:无。本清单里唯一那个真正的未知数已关闭。 #### C2 — `linux/` uapi 头(curl, cmp-module) From 5b0849ba77e93f56cdb0bd958056be44622c92b1 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 04:45:06 +0800 Subject: [PATCH 08/11] the finding's status word is one the checker knows `status: resolved` is not in the vocabulary the structure check accepts (active | landed | superseded | abandoned), and the agents index had not been regenerated. Both caught by `check_docs_structure.sh`, which is the check that exists so a front-matter word nobody reads does not drift. --- .agents/docs/2026-09-20-cxa-thread-atexit-finding.md | 2 +- .agents/docs/README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.agents/docs/2026-09-20-cxa-thread-atexit-finding.md b/.agents/docs/2026-09-20-cxa-thread-atexit-finding.md index 0518ccb7..d291df70 100644 --- a/.agents/docs/2026-09-20-cxa-thread-atexit-finding.md +++ b/.agents/docs/2026-09-20-cxa-thread-atexit-finding.md @@ -1,6 +1,6 @@ --- subject: review -status: resolved +status: landed --- # `__cxa_thread_atexit` 在 openkal-Windows 上:两层都已定位并修复 diff --git a/.agents/docs/README.md b/.agents/docs/README.md index d8683f72..fcb19197 100644 --- a/.agents/docs/README.md +++ b/.agents/docs/README.md @@ -63,7 +63,7 @@ Records that declare one. Everything else is listed by date below. - [本轮生态级自审](2026-09-20-wave-self-review.md) — active - [#674 设计方案评审:`-include unistd.h` 在 Windows + `presents = "posix"` 上的可行性](2026-09-20-issue-674-design-review.md) — active -- [`__cxa_thread_atexit` 在 openkal-Windows 上:定位到一层,第二层未定位](2026-09-20-cxa-thread-atexit-finding.md) — active +- [`__cxa_thread_atexit` 在 openkal-Windows 上:两层都已定位并修复](2026-09-20-cxa-thread-atexit-finding.md) — landed ### targets @@ -99,7 +99,7 @@ Records that declare one. Everything else is listed by date below. - [openkal 生态:能力的时刻模型,以及 C 环境方案空间的划分](2026-09-20-openkal-c-environment-ecosystem-design.md) — active - [#674 设计方案评审:`-include unistd.h` 在 Windows + `presents = "posix"` 上的可行性](2026-09-20-issue-674-design-review.md) — active - [C 环境生态方案:执行计划](2026-09-20-ecosystem-execution-plan.md) — active -- [`__cxa_thread_atexit` 在 openkal-Windows 上:定位到一层,第二层未定位](2026-09-20-cxa-thread-atexit-finding.md) — active +- [`__cxa_thread_atexit` 在 openkal-Windows 上:两层都已定位并修复](2026-09-20-cxa-thread-atexit-finding.md) — landed - [#674:`presents = "posix"` 在 Windows 上兑现契约的下半段](2026-09-19-issue-674-cenv-posix-preinclude-design.md) — superseded - [运行时绑定方案 v3:让 mcpp 真正安装它所声明的运行时](2026-09-17-runtime-binding-multi-repo-plan.md) — landed - [#662:目标侧由依赖图提供时,编译器的隐式头文件搜索仍指向宿主](2026-09-17-issue-662-graph-target-header-isolation-plan.md) — active From 5baa58b9eee12e176fa9cee6e6cdad3af510e74e Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 04:47:06 +0800 Subject: [PATCH 09/11] plan: why the missing Windows cell is not a one-line matrix row Measured rather than assumed. A minimal criterion --- a setjmp/longjmp program with `_Static_assert(sizeof(jmp_buf) >= 32 * sizeof(unsigned long long))`, declaring `openkal-musl` alone, built for x86_64-windows-gnu: compiles which is the layer that matters, since a short jmp_buf is silent and a compile-time assertion is the only thing that makes it loud does not link cpow.o and others leave references unresolved; openkal-musl alone is not a complete link, because the openkal implementation and compiler-rt are assembled by openkal-llvm-runtime So a Windows cell there is either compile-only (which does catch this defect, and is cheap) or pulls the runtime in as a path dependency --- the mirror of what the runtime's own CI already does. Either is new work. --- ...1-openkal-ecosystem-completion-and-acceptance.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md b/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md index 845b1329..917b27f8 100644 --- a/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md +++ b/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md @@ -173,6 +173,19 @@ static assertion failed: UnwindCursor<> does not fit in unw_cursor_t 由另一个仓库的 CI 代为验证,是[[a-passing-criterion-that-measured-nothing]] 的邻居—— 它今天对,是因为恰好有人在别处建了那个目标。 +**为什么补这个格子不是顺手的事,是实测出来的。** 想写一个最小判据——一个 `setjmp` / +`longjmp` 程序,带 `_Static_assert(sizeof(jmp_buf) >= 32 * sizeof(unsigned long long))` +——只声明 `openkal-musl = "0.19.0"` 一个依赖,为 `x86_64-windows-gnu` 构建: + +- **编译过了**,而那正是判据要抓的那一层:短 `jmp_buf` 是静默的,编译期断言是唯一能让它 + 变响的地方。 +- **链接不过**:`cpow.o` 等一批目标文件的引用无人解析。openkal-musl 单独不是一条完整的 + 链接——openkal 实现与 compiler-rt 由 `openkal-llvm-runtime` 组装。 + +所以 musl 侧的 Windows 格子要么只做**编译期**断言(能抓这个缺陷,且便宜),要么把 runtime +当 path dep 拉进来(就是 runtime 的 CI 反过来做的那件事)。**两种都是新增工作量,不是 +把矩阵加一行。** 记在这里,下一个接手的人不必再量一遍。 + **唯一建那个目标的是 mcpp 自己的 `openkal-cross`**(3 宿主 × 3 目标)。而它把要验的 生态分支写死成 `OPENKAL_BRANCH: main`——于是一个**需要生态协同提交**的引擎改动,在那个 提交落地之前既无法验证、也无法合入(因为正是这个 job 会红)。 From ac45e1f311da866b011fddebfe1811ed935b5fee Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 04:59:57 +0800 Subject: [PATCH 10/11] a sandbox verification for this wave, with its control reading Six sections. B and C are CHANGE and must fail on the previous release; E and F are GUARD and must pass on both. Measured, on the host, against the genuine published archive of 2026.9.21.1: 2026.9.21.1 fails=2 B: no upper-case target macro C: an unanswered requirement produced no note 2026.9.21.2 fails=0 C'\''s second leg passes on both by design: it asserts the note is ABSENT when the provider does state its list. Without it, the first leg would pass against an engine that printed the line unconditionally --- a negative control is not a hole in a CHANGE section. B carries both directions in ONE translation unit, because either alone passes for the wrong reason: an engine defining NEITHER spelling satisfies "the lower-case one is gone", and one defining BOTH satisfies "the upper-case one is here". Its second leg asks a freestanding target for its own macro, which is what says the spelling is DERIVED from the triple rather than enumerated. D and F need openkal-llvm-runtime 0.14.0 from the index and reported NOT RUN rather than passing when it was not yet registered. --- ...2026-09-21-macros-and-withdrawal-verify.sh | 302 ++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100755 .agents/docs/2026-09-21-macros-and-withdrawal-verify.sh diff --git a/.agents/docs/2026-09-21-macros-and-withdrawal-verify.sh b/.agents/docs/2026-09-21-macros-and-withdrawal-verify.sh new file mode 100755 index 00000000..0e27ca78 --- /dev/null +++ b/.agents/docs/2026-09-21-macros-and-withdrawal-verify.sh @@ -0,0 +1,302 @@ +#!/usr/bin/env bash +# Ecosystem verification for the 2026.9.21.2 wave against the PUBLISHED mcpp +# and index, run inside a SubOS sandbox with CN mirrors for xlings and mcpp. +# +# B64=$(base64 -w0 .agents/docs/2026-09-21-macros-and-withdrawal-verify.sh) +# xlings subos use v920 --sandbox --cmd \ +# "echo $B64 | base64 -d > /tmp/v.sh && MCPP_VERIFY_VERSION=2026.9.21.2 bash /tmp/v.sh" +# +# RUN IT AGAINST THE PREVIOUS RELEASE FIRST (MCPP_VERIFY_VERSION=2026.9.21.1): +# every CHANGE section must FAIL there and pass here, and every GUARD section +# must pass on both. A CHANGE section green on both measured nothing. +# +# The sandbox's $HOME persists between runs of one SubOS, so each section +# clears its own directory. A section that cannot run says so and is listed +# again at the end: a run reporting only failures cannot be told from one that +# examined nothing. +# +# TWO RUNS AND THE READING FROM EACH (host dry run, 2026-09-21, the older one +# against the genuine published archive rather than a local build): +# +# mcpp 2026.9.21.1 (published) fails=2 +# B the engine does not define the upper-case target macro +# C an unanswered requirement produced no note +# mcpp 2026.9.21.2 fails=0 +# +# C's SECOND LEG PASSES ON BOTH, and that is the point of having it: it asserts +# the note is ABSENT when the provider does state its list, so without it the +# first leg would pass against an engine that printed the line unconditionally. +# A negative control is not a hole in a CHANGE section. +# +# D and F needed `openkal-llvm-runtime@0.14.0`, registered after that dry run, +# and both reported NOT RUN rather than passing. +set -u + +VER="${MCPP_VERIFY_VERSION:?set MCPP_VERIFY_VERSION}" +STORE="${MCPP_VERIFY_BIN:-$HOME/.xlings/data/xpkgs/xim-x-mcpp/$VER/bin/mcpp}" + +fails=0 +skipped="" +fail() { printf 'ASSERT-FAIL: %s\n' "$1"; fails=$((fails + 1)); } +ok() { printf 'ok: %s\n' "$1"; } +section() { printf '\n== %s ==\n' "$1"; } +skip() { printf 'NOT RUN: %s\n' "$1"; skipped="$skipped + - $1"; } +unset XLINGS_ACTIVE_SUBOS + +root="$HOME/verify-9212" +rm -rf "$root"; mkdir -p "$root" + +section "A. identity and mirror" +if [ ! -x "$STORE" ]; then + skip "mcpp $VER is not in the store at $STORE" + printf '\n-- summary --\nfails=%d\nnot run:%s\n' "$fails" "${skipped:- (none)}" + exit 1 +fi +got="$("$STORE" --version 2>&1 | head -1)" +case "$got" in + *"$VER"*) ok "mcpp $VER from $STORE" ;; + *) fail "the binary at $STORE reports '$got'" ;; +esac +"$STORE" self config --mirror CN >/dev/null 2>&1 \ + && ok "mcpp mirror set to CN" || fail "mcpp self config --mirror CN" + +# ── CHANGE 1. The owned macros are spelt in upper case ────────────────────── +# +# BOTH DIRECTIONS IN ONE TRANSLATION UNIT, because either alone passes for the +# wrong reason: an engine defining NEITHER spelling satisfies "the lower-case +# one is gone", and one defining BOTH satisfies "the upper-case one is here". +section "B. __MCPP_TARGET___ replaces __mcpp_target___ (CHANGE)" +b="$root/b"; rm -rf "$b"; mkdir -p "$b/src" +cat > "$b/src/main.c" <<'EOF' +#if !defined(__MCPP_TARGET_LINUX__) +#error "__MCPP_TARGET_LINUX__ is not defined" +#endif +#if defined(__mcpp_target_linux__) +#error "the lower-case spelling is still defined" +#endif +int main(void) { return 0; } +EOF +cat > "$b/mcpp.toml" <<'EOF' +[package] +name = "macro-probe" +version = "0.1.0" + +[targets.macro-probe] +kind = "bin" +main = "src/main.c" +EOF +if (cd "$b" && "$STORE" build >/dev/null 2>&1); then + ok "the upper-case target macro is defined and the lower-case one is not" +else + out=$(cd "$b" && "$STORE" build 2>&1) + case "$out" in + *"__MCPP_TARGET_LINUX__ is not defined"*) + fail "the engine does not define the upper-case target macro" ;; + *"lower-case spelling is still defined"*) + fail "the engine still defines the lower-case target macro" ;; + *) fail "the probe did not build, and for neither of the two reasons" ;; + esac +fi + +# THE SPELLING IS DERIVED, NOT ENUMERATED. A second target with a different +# `os` says the engine reads the triple rather than carrying a table of names. +b2="$root/b2"; rm -rf "$b2"; mkdir -p "$b2/src" +cat > "$b2/src/main.c" <<'EOF' +#if !defined(__MCPP_TARGET_NONE__) +#error "__MCPP_TARGET_NONE__ is not defined for a freestanding target" +#endif +void _start(void) {} +EOF +cat > "$b2/mcpp.toml" <<'EOF' +[package] +name = "macro-probe-bare" +version = "0.1.0" + +[targets.macro-probe-bare] +kind = "bin" +main = "src/main.c" + +[build] +ldflags = ["-nostdlib", "-nostartfiles", "-static"] +EOF +if (cd "$b2" && "$STORE" build --target riscv64-none-elf >/dev/null 2>&1); then + ok "a freestanding target spells its own macro from the triple" +else + skip "the freestanding toolchain did not resolve in this sandbox" +fi + +# ── CHANGE 2. A requirement nobody answered is named ──────────────────────── +# +# Three situations exist and two build. Without this line the first and the +# third produce identical output, so a consumer cannot tell "checked and +# agreed" from "never asked". Both legs, because a note printed +# unconditionally would satisfy the first one alone. +section "C. an unanswered requirement is named (CHANGE)" +c="$root/c"; rm -rf "$c"; mkdir -p "$c/impl/src" "$c/src" +printf 'int fake_kernel_marker(void){return 0;}\n' > "$c/impl/src/lib.c" +printf 'int main(void){return 0;}\n' > "$c/src/main.c" +mk_impl() { # $1 = provides-interfaces body, or empty + if [ -z "$1" ]; then + cat > "$c/impl/mcpp.toml" <<'EOF' +[package] +name = "fakekernel" +version = "0.1.0" +provides = ["mcpp:kernel-abi=openkal"] + +[targets.fakekernel] +kind = "lib" +sources = ["src/*.c"] +EOF + else + cat > "$c/impl/mcpp.toml" < "$c/mcpp.toml" <<'EOF' +[package] +name = "iface-probe" +version = "0.1.0" + +[dependencies] +fakekernel = { path = "impl" } + +[build] +allow_host_libs = true + +[kernel-abi] +requires-interfaces = ["openkal.fs", "openkal.net"] +EOF +mk_impl '' +out=$(cd "$c" && "$STORE" build 2>&1) +if [ $? -ne 0 ]; then + fail "a provider that states nothing must not be refused" +else + case "$out" in + *"kernel-abi interfaces"*"states none"*"2 requirements unchecked"*) + ok "the note names the implementation and how many went unchecked" ;; + *"kernel-abi interfaces"*) + fail "the note is printed but does not carry the count" ;; + *) fail "an unanswered requirement produced no note" ;; + esac +fi +rm -rf "$c/target" +mk_impl '"openkal.fs", "openkal.net", "openkal.abort"' +out=$(cd "$c" && "$STORE" build 2>&1) +if [ $? -ne 0 ]; then + fail "a graph whose provider states every requirement must build" +else + case "$out" in + *"kernel-abi interfaces"*) + fail "the note appeared for a graph in which everything WAS checked" ;; + *) ok "a provider that states its list draws no note" ;; + esac +fi + +# ── CHANGE 3. The borrowed name is withdrawn ──────────────────────────────── +# +# `__CYGWIN__` was left defined so that code needing "PE object format with a +# POSIX-presenting C environment" had a name. A 30-member measurement found +# four members reading it as "Win32 is available" and reaching windows.h. +# This needs a real openkal graph, so it skips rather than failing when the +# sandbox cannot reach one. +section "D. __CYGWIN__ is withdrawn on a Windows target presenting POSIX (CHANGE)" +d="$root/d"; rm -rf "$d"; mkdir -p "$d/src" +cat > "$d/src/main.cpp" <<'EOF' +#if defined(__CYGWIN__) || defined(__CYGWIN32__) +#error "the borrowed name is still defined" +#endif +#if !defined(__MCPP_TARGET_WINDOWS__) +#error "mcpp's own name for the target is missing" +#endif +#if defined(_WIN32) +#error "presents = posix must suppress _WIN32" +#endif +#if !defined(__OPENKAL__) +#error "__OPENKAL__ is not defined over a resolved openkal layer" +#endif +#if defined(__openkal__) +#error "the lower-case spelling is still defined" +#endif +int main() { return 0; } +EOF +cat > "$d/mcpp.toml" <<'EOF' +[package] +name = "withdrawal-probe" +version = "0.1.0" + +[dependencies] +openkal-llvm-runtime = "0.14.0" +EOF +out=$(cd "$d" && "$STORE" build --target x86_64-windows-gnu 2>&1) +rc=$? +case "$out" in + *"openkal-llvm-runtime"*"not found"*|*"did not resolve"*) + skip "openkal-llvm-runtime 0.14.0 did not resolve from the index" ;; + *) + if [ $rc -eq 0 ]; then + ok "the borrowed name is gone, mcpp's own name is there, over openkal" + else + case "$out" in + *"borrowed name is still defined"*) fail "__CYGWIN__ is still defined" ;; + *"own name for the target is missing"*) fail "__MCPP_TARGET_WINDOWS__ is missing" ;; + *"__OPENKAL__ is not defined"*) fail "__OPENKAL__ is missing over openkal" ;; + *"lower-case spelling is still defined"*) fail "__openkal__ is still defined" ;; + *"presents = posix"*) fail "_WIN32 survived the substitution" ;; + *) skip "the openkal Windows graph did not build in this sandbox" ;; + esac + fi ;; +esac + +# ── GUARD. A package declaring nothing is untouched ───────────────────────── +section "E. a package declaring neither key builds and runs (GUARD)" +e="$root/e"; rm -rf "$e"; mkdir -p "$e/src" +printf '#include \nint main(){std::puts("plain ok");return 0;}\n' > "$e/src/main.cpp" +cat > "$e/mcpp.toml" <<'EOF' +[package] +name = "plain" +version = "0.1.0" +EOF +if (cd "$e" && "$STORE" build >/dev/null 2>&1) \ + && "$e"/target/*/*/bin/plain 2>/dev/null | grep -q "plain ok"; then + ok "a package that declares nothing builds and runs" +else + fail "a package that declares nothing must be untouched by this release" +fi + +# ── GUARD. An openkal program from the published index ────────────────────── +section "F. an openkal program from the published index (GUARD)" +f="$root/f"; rm -rf "$f"; mkdir -p "$f/src" +printf '#include \nint main(){std::puts("openkal ok");return 0;}\n' > "$f/src/main.cpp" +cat > "$f/mcpp.toml" <<'EOF' +[package] +name = "openkal-hello" +version = "0.1.0" + +[dependencies] +openkal-llvm-runtime = "0.14.0" +EOF +if (cd "$f" && "$STORE" build >/dev/null 2>&1); then + if "$f"/target/*/*/bin/openkal-hello 2>/dev/null | grep -q "openkal ok"; then + ok "an openkal program builds and runs from the published index" + else + fail "the openkal program built and did not run" + fi +else + skip "openkal-llvm-runtime 0.14.0 did not resolve from the index" +fi + +printf '\n-- summary --\nfails=%d\nnot run:%s\n' "$fails" "${skipped:- + (none)}" +[ "$fails" -eq 0 ] From 7383b9abae03b2ae4d4fe015a241b8c0b522d6d4 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 05:02:03 +0800 Subject: [PATCH 11/11] plan: the criteria table carries status, and two rows split E1b and E1c did not exist when the table was written; C2 turned out to be two items with two different owners; E2 left this round with its blocker corrected. Each row now says what state it is in rather than only what would count as passing, because a table of criteria with no readings is a list of intentions. Recorded readings: the verification script's control run (2026.9.21.1 fails=2, 2026.9.21.2 fails=0), the nine green cells of openkal-cross, and the two directions the A3 check was measured in. --- ...kal-ecosystem-completion-and-acceptance.md | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md b/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md index 917b27f8..928497fe 100644 --- a/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md +++ b/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md @@ -652,20 +652,25 @@ openkal-musl 0.18.0,而 0.18.0 当时还没进索引,消费者自己的 CI ## 7. 判据总表 -| 编号 | 判据 | 怎么算通过 | 阻塞 | +状态截至 2026-09-21 收尾。 + +| 编号 | 判据 | 怎么算通过 | 状态 | | --- | --- | --- | --- | -| E1 | 30 成员重测 | `windows.h` 组 4→0 且总失败不增 | 无 | -| E2 | 引用未提供接口的符号 | 链接期被点名拒绝;声明正确时零开销 | 工作量 | -| E3 | 提供方什么都不声明 | 构建成功**且**报告里有 unchecked 一行 | 无 | -| C1 | 五行最小复现 | 链接通过**且析构真的执行** | **第二层未定位** | -| C2 | curl / cmp-module | 记为 `refused` 而非 `fails` | 无 | -| C3 | `arc4random_buf` | 「应当有的符号」CI 断言 | 无 | -| C4 | `aarch64-macos --profile release` | **已通过,本机实测** | 关闭;不复现 | -| I1 | 写死的节点集合 | conformance 逐条断言 | 无 | -| I2 | `presents = "windows"` 的 C 库 | 引擎改动数为 0 | 无 | -| **A1** | 三目标 `mcpp build` | 全绿 | E1, C4 | -| **A2** | 三目标 `mcpp test` + 真跑 | 全绿 | A1 | -| **A3** | 三份 `os.cppm` | **除六行外逐字节相同** | A1 | +| E1 | 30 成员重测 | `windows.h` 组 4→0 且总失败不增 | **待测**(由移 `pins.toml` 的那个 PR 触发);`openkal-cross` 九格已全绿 | +| E1b | 大写重命名 | 上一版红、本版绿 | **已通过**:验证脚本 B 段,`2026.9.21.1` fails=2 → `2026.9.21.2` fails=0 | +| E1c | 交叉验证协议的反向 | `openkal_ref` 留空时行为不变 | **已落地** | +| E2 | 引用未提供接口的符号 | 链接期被点名拒绝;声明正确时零开销 | **退出本轮**:阻塞是设计决定不是工作量,见 §2.1 | +| E3 | 提供方什么都不声明 | 构建成功**且**报告里有 unchecked 一行 | **已通过**:e2e 743 四条腿 + 验证脚本 C 段双向 | +| C1 | 最小复现 | 链接通过**且析构真的执行** | **已通过**:三层全部定位并修复,`openkal-llvm-runtime@0.15.0` | +| C2 | cmp-module | 从分母移除,理由可追溯,**且声明可被证伪** | **已通过**:`[not-portable]` + `compat.py check`,红/绿两向实测 | +| C2' | curl | 配方缺陷,不是不可移植 | **已定位未修**:两个目标两个不同真因,见 `docs/openkal-compat.md` | +| C3 | `arc4random_buf` | 「应当有的符号」CI 断言 | 已通过 | +| C4 | `aarch64-macos --profile release` | 本机实测 | 关闭;不复现 | +| I1 | 写死的节点集合 | conformance 逐条断言 | 未实现(第五批) | +| I2 | `presents = "windows"` 的 C 库 | 引擎改动数为 0 | 未实现(第五批) | +| **A1** | 三目标 `mcpp build` | 全绿 | **待跑**(须用已发布钉,见 §8 第二条) | +| **A2** | 三目标 `mcpp test` + 真跑 | 全绿 | 待跑 | +| **A3** | 三份 `os.cppm` | **除六行外逐字节相同** | **已机械化并进 CI**;四种失败形态逐个量红过 | ---