From b0c33f9c8fb0a2633936f63d3740044bba2ad7d2 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 06:27:01 +0800 Subject: [PATCH 01/13] the count was four and it is two, and the grouping used the wrong key THE RE-MEASUREMENT AFTER THE WITHDRAWAL REFUTED A NUMBER THIS REPOSITORY HAD WRITTEN IN FIVE PLACES. `cenv.cppm`, `predefines.cppm`, `docs/21`, `docs/22` and the CHANGELOG all said the borrowed `__CYGWIN__` cost four members: archive, sqlite3, mimalloc, c-ares. The 30-member run on 2026.9.21.2 says otherwise. total failures 10 -> 5 newly failing none the windows.h group 4 -> 2, not 4 -> 0 And the half that did not hold is the useful half, because it says the DENOMINATOR was wrong: archive (xz) __CYGWIN__ correct cleared sqlite3 __CYGWIN__ correct cleared c-ares __CYGWIN__ WRONG still fails mimalloc __CYGWIN__ WRONG still fails c-ares reaches `windows.h` through `#ifdef HAVE_WINDOWS_H`, and that macro is defined by mcpp-index's own recipe in its Windows branch --- a recipe defect, the same shape as curl's `HAVE_LINUX_TCP_H`. mimalloc no longer reaches a header at all. ALL FOUR WERE GROUPED BY THEIR DIAGNOSTIC. Every one of them stopped at `windows.h`, so they were recorded as one cause. GROUPING BY DIAGNOSTIC IS NOT GROUPING BY CAUSE, and a count collected that way overstates what withdrawing a name can fix. This is the mirror of a lesson already in this repository: a conclusion gets re-checked, its REASONS do not. The conclusion --- withdraw the borrowed name --- was right and was re-verified. The number inside the reason was inherited from an earlier record three times over, into engine comments, a changelog and a pull request body, and nothing checked it until the measurement did. AND mimalloc IS A FINDING OF ITS OWN: fatal error: error in backend: Target OS doesn't support __builtin_thread_pointer() yet. `presents = "posix"` on Windows realises as `--target=x86_64-pc-cygwin`, and LLVM does not implement that builtin for that OS. Every previous note about the substitution discussed what the PREPROCESSOR sees --- the ABI unchanged, the link unchanged, only macro visibility different. This is the first measured cost that the CODE GENERATOR sees, and it is recorded rather than fixed: the repair belongs either in mimalloc's recipe or in the choice of substitute triple, and that is a measurement not yet taken. A tooling defect surfaced with it. The measurement recorded mimalloc as `fails: error: build failed`, because `compat.py`'s `first_diagnostic` matched none of its three patterns --- a backend error carries no `file:line:` and no `FAIL` --- and its fallback returns the LAST line, which is mcpp's own summary. A fallback that returns something diagnostic-shaped when the match failed is worse than returning nothing. --- ...kal-ecosystem-completion-and-acceptance.md | 48 ++++++++++++++++++- CHANGELOG.md | 32 +++++++++++++ docs/21-the-target-triple.md | 13 ++++- docs/22-target-side.md | 20 ++++++-- docs/zh/21-the-target-triple.md | 9 +++- src/toolchain/cenv.cppm | 42 ++++++++++++---- src/toolchain/predefines.cppm | 20 ++++++-- 7 files changed, 162 insertions(+), 22 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 b4d095b4..c2660c34 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 @@ -198,6 +198,52 @@ static assertion failed: UnwindCursor<> does not fit in unw_cursor_t runtime 的清单从**索引**钉 `openkal-musl = "0.19.0"`。所以用 `openkal_ref` 验之前, musl 0.19.0 必须先登记进索引。 +#### E1d — 「四个成员」是二,而那个分组按诊断而不是按真因(2026-09-21 重测) + +撤销之后的 30 成员重测:**总失败 10→5,新增失败零**。但判据的另一半「`windows.h` 组 +4→0」没有达成——是 **4→2**,而未达成的原因不是撤销无效,是**这个组从一开始就数错了**。 + +| 成员 | 记的真因 | 实测真因 | 撤销后 | +|---|---|---|---| +| `archive`(经 xz) | `__CYGWIN__` | **对** | **清了** | +| `sqlite3` | `__CYGWIN__` | **对** | **清了** | +| `c-ares` | `__CYGWIN__` | **错**:`ares_setup.h:81` 是 `#ifdef HAVE_WINDOWS_H`,而那个宏由 `mcpp-index` 自己的配方在 `windows = {` 块里 `#define`(`compat.c-ares.lua:1283`) | 仍红 | +| `mimalloc` | `__CYGWIN__` | **错**:已经不走到任何头文件 | 仍红 | + +**四个是按「诊断」分的组。** 四个当时都停在 `windows.h`,于是被记成同一类。 +**按诊断分组不是按真因分组**,这样数出来的数目会高估一次撤销能修掉多少。 + +这条是[[reasons-written-from-memory-kill-good-fixes]] 的镜像:结论(「撤销是对的」)被 +复查并成立,而**理由里的那个数目从未被复查**,它被从上一份记录继承了三次——写进引擎注释、 +CHANGELOG 和 PR 正文,直到重测把它否掉。 + +**判据达成一半比没写判据更有价值**:没达成的那一半正是它指出分母错了的地方。 + +#### E1e — `mimalloc` 暴露替身三元组的一个代价,与宏无关 + +``` +fatal error: error in backend: Target OS doesn't support __builtin_thread_pointer() yet. +``` + +`presents = "posix"` 在 Windows 上实现成 `--target=x86_64-pc-cygwin`。LLVM 没有为那个 OS +实现 `__builtin_thread_pointer()`,而 mimalloc 用它取线程局部堆指针。 + +**这是这次替换的第一个被测量到的、超出宏名之外的代价。** 此前 `cenv.cppm` 关于替身三元组 +的全部记录都只讨论「预处理器看到什么」——ABI 不变、链接不变、只有宏可见性变。这条说明 +**代码生成器也看得见那个 OS**。 + +**可能的修法(都未验证)**:mimalloc 的配方关掉那条路径(它有 `MI_*` 配置);或替身三元组 +换一个 LLVM 实现了该 builtin 的 OS。**先量再选**——这条记在这里正是为了下一个人不必 +从三十个成员的诊断里重新走一遍。 + +#### 一条测量工具的缺陷:`mimalloc` 的真诊断被兜底吃掉了 + +重测里 `mimalloc` 记的是 `fails: error: build failed`——`compat.py` 的 `first_diagnostic` +三条模式都没匹配(后端错误不带 `file:line:` 也不含 `FAIL`),于是兜底取了**最后一行**, +而最后一行是 mcpp 自己的总结。真因要本机复现才拿得到。 + +**判据的兜底不能是「最后一行」**:它在匹配失败时给出一个看起来像诊断的东西,比留空更坏。 + #### E1b — 引擎拥有的宏改为全大写(2026.9.21.2) **状态**:已落地,与 E1 第三步同一个 PR(用户要求:不分开,免得发布周期太长)。 @@ -756,7 +802,7 @@ openkal-musl 0.18.0,而 0.18.0 当时还没进索引,消费者自己的 CI | 编号 | 判据 | 怎么算通过 | 状态 | | --- | --- | --- | --- | -| E1 | 30 成员重测 | `windows.h` 组 4→0 且总失败不增 | **待测**(由移 `pins.toml` 的那个 PR 触发);`openkal-cross` 九格已全绿 | +| E1 | 30 成员重测 | `windows.h` 组 4→0 且总失败不增 | **一半达成,而未达成的那半否掉了判据自己的分母**:总失败 10→5、新增失败为零;但该组实际只有两个成员读 `__CYGWIN__`,见 §2.1 E1d | | E1b | 大写重命名 | 上一版红、本版绿 | **已通过**:验证脚本 B 段,`2026.9.21.1` fails=2 → `2026.9.21.2` fails=0 | | E1c | 交叉验证协议的反向 | `openkal_ref` 留空时行为不变 | **已落地** | | E2 | 引用未提供接口的符号 | 链接期被点名拒绝;声明正确时零开销 | **退出本轮**:阻塞是设计决定不是工作量,见 §2.1 | diff --git a/CHANGELOG.md b/CHANGELOG.md index e3de8ab3..aa498978 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,38 @@ ## [Unreleased] +### 更正:那个「四个成员」是二,而分组用错了依据 + +2026.9.21.2 的条目、`cenv.cppm` 与 `predefines.cppm` 的注释、`docs/21` 与 `docs/22` +都写着「借来的 `__CYGWIN__` 代价是四个成员:archive、sqlite3、mimalloc、c-ares」。 +**撤销之后的重测把这个数目否掉了。** + +| 成员 | 记的真因 | 实测真因 | 撤销后 | +|---|---|---|---| +| `archive`(经 xz) | `__CYGWIN__` | **对** | **清了** | +| `sqlite3` | `__CYGWIN__` | **对** | **清了** | +| `c-ares` | `__CYGWIN__` | **错**:`#ifdef HAVE_WINDOWS_H`,而那个宏由 mcpp-index 自己的配方在 windows 分支 `#define` | 仍红 | +| `mimalloc` | `__CYGWIN__` | **错**:已经不走到任何头文件——`error in backend: Target OS doesn't support __builtin_thread_pointer() yet` | 仍红 | + +**四个是按「诊断」分的组,不是按「真因」。** 四个都停在 `windows.h`(mimalloc 当时如此), +于是被记成同一类。**按诊断分组不是按真因分组**,这样数出来的数目会高估一次撤销能修掉多少。 + +这条更正本身来自判据:重测把总失败从 10 降到 5、**新增失败为零**,而「`windows.h` 组 4→0」 +这半条没有达成——是 4→2。**一个达成了一半的判据,比一个没写的判据更有价值:它指出了 +分母是错的。** + +### `mimalloc` 暴露出替身三元组的一个代价,与宏无关 + +``` +fatal error: error in backend: Target OS doesn't support __builtin_thread_pointer() yet. +``` + +`presents = "posix"` 在 Windows 上实现成 `--target=x86_64-pc-cygwin`。LLVM 没有为那个 +OS 实现 `__builtin_thread_pointer()`,而 mimalloc 用它取线程局部堆指针。**这是替身 +三元组的第一个被测量到的、超出宏名之外的代价**;先前关于这次替换的记录只讨论了预处理器 +看到什么。 + + ## [2026.9.21.2] - 2026-09-21 ### 引擎拥有的宏改为全大写 diff --git a/docs/21-the-target-triple.md b/docs/21-the-target-triple.md index b2ec66d6..5dac8f4c 100644 --- a/docs/21-the-target-triple.md +++ b/docs/21-the-target-triple.md @@ -683,10 +683,21 @@ 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 +found 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. +**That count was four and it is two** (corrected 2026-09-21 against the +re-measurement on the release that withdrew the name). `archive` (through xz) +and `sqlite3` read it; both cleared. The other two were grouped with them +because all four stopped at `windows.h`, and their guards differ: `c-ares` +reaches the header through `#ifdef HAVE_WINDOWS_H`, which this ecosystem's own +recipe defines in its Windows branch, and `mimalloc` no longer reaches a +header at all — it fails in the code generator on a builtin LLVM does not +implement for the substitute triple's OS. **Grouping by diagnostic is not +grouping by cause**, and a count collected that way overstates what a +withdrawal can fix. + `__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. diff --git a/docs/22-target-side.md b/docs/22-target-side.md index f7c661e9..1956cdfb 100644 --- a/docs/22-target-side.md +++ b/docs/22-target-side.md @@ -441,12 +441,22 @@ an ordinary Windows build still has `_WIN64`. **`__CYGWIN__` is still defined, and that is a sequence rather than a decision to keep it.** The 30-member measurement settled that the borrowed -name costs four members: `archive`, `sqlite3`, `mimalloc` and `c-ares` each -stop at `#include `, reached through +name costs members that stop at `#include `, reached through `#if defined(_WIN32) || defined(__CYGWIN__)`. Upstream means *Win32 is -available* by it — mimalloc states so in the guard itself, sqlite3 lists it -under `SQLITE_OS_WIN`. **A borrowed name means what the lender's history made -it mean**, not what the borrower intended. +available* by it — sqlite3 lists it under `SQLITE_OS_WIN`. **A borrowed name +means what the lender's history made it mean**, not what the borrower +intended. + +**The count was four and it is two** (corrected 2026-09-21). `archive` +(through xz) and `sqlite3` read the name; both cleared on the release that +withdrew it. `c-ares` and `mimalloc` were grouped with them because all four +stopped at `windows.h`, and neither was this name's doing: `c-ares` reaches +the header through `#ifdef HAVE_WINDOWS_H`, a macro mcpp-index's own recipe +defines in its Windows branch, and `mimalloc` no longer reaches a header — +it fails in the code generator with *Target OS doesn't support +`__builtin_thread_pointer()` yet*, which is a property of the substitute +triple rather than of any macro. **Grouping by diagnostic is not grouping by +cause.** Withdrawing it was tried and broke the two headers above, which read it for want of any other target-wide name. libunwind's `static_assert` failed diff --git a/docs/zh/21-the-target-triple.md b/docs/zh/21-the-target-triple.md index e7fabc2c..5db94efb 100644 --- a/docs/zh/21-the-target-triple.md +++ b/docs/zh/21-the-target-triple.md @@ -599,10 +599,17 @@ ABI 给记录定尺寸。呈现 POSIX 的 Windows 目标**有意**没有 `_WIN32 `__MCPP_` 这个前缀是承重的,但它是**前缀**而不是规则的全部:**一个 mcpp 拥有的名字,语义 由 mcpp 自己定**,而 `__OPENKAL__` 同样是拥有的,它命名的是 *openkal* 而不是 mcpp。另一条 路——**借用**——试过:`__CYGWIN__` 曾被保留定义,为的是让需要「PE 目标文件格式 + POSIX C -环境」的代码有个名字,而 30 成员测量发现四个成员把它读成「**Win32 可用**」并走向 +环境」的代码有个名字,而 30 成员测量发现有成员把它读成「**Win32 可用**」并走向 `#include `——那正是上游用它表达的意思。**一个借来的名字,语义由借出方的历史 决定**,不由借用方的意图决定。 +**那个数目写的是四,实际是二**(2026-09-21 按撤销该名字那一版的重测更正)。`archive` +(经由 xz)与 `sqlite3` 确实读它,两个都清了。另外两个是被归进来的——四个都停在 +`windows.h`——而它们的守卫并不相同:`c-ares` 走的是 `#ifdef HAVE_WINDOWS_H`,那个宏由 +本生态自己的配方在 windows 分支里 `#define`;`mimalloc` 已经不再走到任何头文件,它死在 +代码生成器里,用的那个 builtin 在替身三元组的 OS 上 LLVM 没有实现。**按诊断分组不是按 +真因分组**,这样数出来的数目会高估一次撤销能修掉多少。 + `__unix__` 是印证这条规则的例外:mcpp **供给**它而不拥有它,所以它保持标准拼法,mcpp 也 不得改变它的含义。读它就按在任何别的 POSIX 系统上那样读。 diff --git a/src/toolchain/cenv.cppm b/src/toolchain/cenv.cppm index 7a279389..bbab8ff0 100644 --- a/src/toolchain/cenv.cppm +++ b/src/toolchain/cenv.cppm @@ -82,12 +82,17 @@ // 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 +// combinations): keeping it costs members that stop at // `#include ` reached through `#if defined(_WIN32) || // 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. +// 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. +// +// THE COUNT SAID FOUR AND IT WAS TWO. Corrected against the re-measurement on +// 2026.9.21.2: `archive` and `sqlite3` read the name and both cleared. +// `c-ares` and `mimalloc` were grouped with them because all four stopped at +// `windows.h`, and a shared diagnostic is not a shared cause --- see the +// emission site below. // // WITHDRAWING IT WAS TRIED TOO EARLY ONCE, AND BROKE THIS ECOSYSTEM'S OWN // INSTALLED HEADERS. `openkal-musl`'s `bits/setjmp.h` and @@ -328,11 +333,30 @@ inline std::expected realise( // `__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: + // members that stop at `#include ` through + // `#if defined(_WIN32) || defined(__CYGWIN__)`; upstream means + // "Win32 is available" by it. The three steps, in the order they + // were taken: + // + // THE COUNT WAS FOUR AND IT IS TWO, and recording the correction + // matters more than the number. The re-measurement on this + // release cleared `archive` and `sqlite3`; `c-ares` and + // `mimalloc` remain, and neither was ever this name's doing: + // + // c-ares `#ifdef HAVE_WINDOWS_H`, and that macro is defined + // by mcpp-index's own recipe in its Windows branch. + // A recipe defect, the same shape as curl's + // `HAVE_LINUX_TCP_H`. + // mimalloc no longer reaches a header at all. It fails in the + // code generator: "Target OS doesn't support + // __builtin_thread_pointer() yet" --- LLVM does not + // implement that builtin for the substitute triple's + // OS. THE SUBSTITUTION HAS A COST BEYOND MACRO NAMES, + // and this is the first measurement of one. + // + // All four were grouped by their DIAGNOSTIC. Grouping by + // diagnostic is not grouping by cause, and a count collected that + // way overstates what withdrawing the name can fix. // // 1. 2026.9.21.1 defined mcpp's own name beside the borrowed // one; 2026.9.21.2 re-spelt it `__MCPP_TARGET_WINDOWS__`. diff --git a/src/toolchain/predefines.cppm b/src/toolchain/predefines.cppm index 76966c3c..3e6a1fb7 100644 --- a/src/toolchain/predefines.cppm +++ b/src/toolchain/predefines.cppm @@ -51,11 +51,21 @@ // 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 (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. +// a 30-member measurement found members reading it as "Win32 is available" +// and reaching `#include ` --- which is what upstream means by it +// (sqlite3 lists it under `SQLITE_OS_WIN`). A BORROWED NAME MEANS WHAT THE +// LENDER'S HISTORY MADE IT MEAN, not what the borrower intended. +// +// THE COUNT WAS FOUR AND IT IS TWO, corrected 2026-09-21 against the +// re-measurement on the release that withdrew the name. `archive` (through +// xz) and `sqlite3` read it and both cleared. The other two were grouped by +// their diagnostic --- all four stopped at `windows.h` --- rather than by +// their guard, and the guards differ: `c-ares` reaches it through +// `#ifdef HAVE_WINDOWS_H`, a macro this ecosystem's own recipe defines in its +// Windows branch, and `mimalloc` does not reach it at all any more; it fails +// in the code generator on `__builtin_thread_pointer()`, which LLVM does not +// implement for the substitute triple's OS. GROUPING BY DIAGNOSTIC IS NOT +// GROUPING BY CAUSE. // // ── STABILITY ────────────────────────────────────────────────────────────── // From b23cc82f76dadd61152ee04746c334b8944952b4 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 06:39:07 +0800 Subject: [PATCH 02/13] plan: the two measurements do not overlap in TARGETS either, and that was unwritten The division of labour said one measures the C surface and the other measures integration. It did not say they ask about different targets: 30-member measurement x86_64-linux-gnu, x86_64-windows-gnu --- no macOS lsp-mcpp-private three targets, including aarch64-macos Measured this round: `archive` is `runs (posix)` on both columns of the 30-member measurement, and the same libarchive fails to link on `lsp-mcpp-private`'s macOS with `memset_pattern16`. Both readings are true. That symbol is an Apple libc function clang emits only for Apple targets; on linux and windows-gnu it cannot appear. So nothing in this ecosystem sweeps macOS at 30-member scale, and E1's criterion --- 10 failures to 5, none newly failing --- holds ON THOSE TWO COLUMNS. A defect of this class is invisible in that number by construction. Adding the column needs a decision first: a Linux host can cross-build aarch64-macos but cannot run it, so that column tops out at `builds`, which RANK places below `runs`. A column that can only reach `builds` makes the regression check permanently looser there than on the other two. --- ...kal-ecosystem-completion-and-acceptance.md | 25 +++++++++++++++++++ 1 file changed, 25 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 c2660c34..280ec03b 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 @@ -692,6 +692,31 @@ PATH_LIST_SEPARATOR / VSCODE_TARGET / CASE_INSENSITIVE_PATHS **两者都绿才叫生态闭环。** 任一单独绿都不足以下结论。 +### 4.4 两者的**目标集合**也不重叠,而这一条本文原先没写(2026-09-21 补) + +分工写成了「30 成员测 C 表面、lsp 测整合」,**没有写它们问的目标不同**: + +| | 目标 | +|---|---| +| 30 成员测量(`pins.toml`) | `x86_64-linux-gnu`、`x86_64-windows-gnu` —— **没有 macOS** | +| `lsp-mcpp-private` | 三个目标,**含 `aarch64-macos`** | + +**后果是这一轮实测到的**:`archive` 在 30 成员测量里两列都是 `runs (posix)`,而同一个 libarchive +在 `lsp-mcpp-private` 的 macOS 上链接失败于 `memset_pattern16`。两个读数都是真的—— +`memset_pattern16` 是 Apple 的 libc 函数,clang 只在 Apple 目标上生成它,linux 与 +windows-gnu 上**按构造不会出现**。 + +**所以「openkal 生态里按 30 个成员规模扫 macOS 的东西,不存在」。** macOS 只被 +`lsp-mcpp-private` 一个程序、以及各实现包自己的 CI 覆盖。 + +**这也限定了 E1 判据的适用范围**:「总失败 10→5、新增为零」是**在那两列上**成立的; +macOS 那一列从来没进过这个统计,所以这一类缺陷在那个数字里按构造不可见。 + +**要补这一列,先有一个决定要拍:** Linux 宿主能为 `aarch64-macos` **交叉构建**但**跑不了**, +所以那一列最多到 `builds`,不是 `runs`。而 `RANK` 里 `builds` 低于 `runs`—— +**一个只能到 `builds` 的目标列,会让「有没有退步」这个判据在那一列上永远比另外两列松。** +是接受这个不对称、还是给那一列配 runner,是拍板项,不是实现项。 + --- ## 5. 执行顺序 From ac85c6d4fa54cc8e7d2a3842be437397f7c44c4d Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 06:50:44 +0800 Subject: [PATCH 03/13] C4 was closed on the wrong objects, and the possibility it dismissed was the right one The closure measured zstd and xz --- the two the OLD report named --- and found them clean. The member that fails is libarchive, and it was never measured. `lsp-mcpp-private`'s `test_archive` does not link for aarch64-macos: ld64.lld: error: undefined symbol: memset_pattern16 referenced by `archive_read_support_format_7zip.o`. zstd and xz happen not to trigger the idiom, so measuring them answered a different question. THE RECORD LISTED THREE POSSIBILITIES AND PICKED THE FIRST. The third --- "`-fno-builtin-memset_pattern16` is not enough to turn off LLVM's loop-idiom pass" --- is the one that holds. A/B on the real compile command from build.ninja, varying only that flag: as built (flag present) 1 reference 38200 bytes flag REMOVED 1 reference -fno-builtin 0 38888 (+1.8%) -ffreestanding 0 -mllvm -disable-loop-idiom-memset 0 38152 The flag changes nothing. And it cannot report that it changed nothing: clang accepts `-fno-builtin-totally_not_a_function` silently, because `memset_pattern16` is an LLVM TargetLibraryInfo libfunc rather than a clang builtin. The preprocessed source contains the symbol zero times, which confirms the call is generated rather than written. TWO ENGINE-SIDE GAPS, AND THE SECOND IS WHY THE FIRST SURVIVED: 1. the token `builtins = "iso"` emits is ineffective for the one case its own comment names; 2. that token is not verified. In the same file, `-D` and `-U` are checked by the probe against `expectDefined`/`expectUndefined`, under a comment saying a `-D` that did not take effect is a verification failure rather than a silent one. `builtinsTokens` has no such list. The criterion that follows: `builtinsTokens` needs the same probe check. A mechanism that holds only under the assumption that it works needs an assertion that it works. The cost of each candidate repair is measured above rather than argued. --- ...kal-ecosystem-completion-and-acceptance.md | 53 +++++++++++++++++++ 1 file changed, 53 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 280ec03b..048b0052 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 @@ -487,6 +487,59 @@ macOS,缺 SDK): **留下的方法论**:一条"某平台本机测不了"的判断,要用**走 openkal 栈**的探针去验,不能用 走平台路径的。两者对 SDK 的要求完全不同。 +#### C4' — 「关闭」是错的,而上面列出的第三种可能才是对的(2026-09-21 重测) + +上面的关闭依据是 **zstd 与 xz** 链接通过、`memset_pattern` 引用为 0。**那两个是从旧报告 +里抄来的对象,不是当前失败的那个。** `lsp-mcpp-private` 的 `test_archive` 在 +`aarch64-macos` 上链接失败: + +``` +ld64.lld: error: undefined symbol: memset_pattern16 +``` + +引用它的是 **libarchive 自己**:`archive_read_support_format_7zip.o`。zstd 与 xz 恰好 +不触发那个 idiom,所以"量它们"回答的是另一个问题—— +[[a-check-that-picks-its-object-by-convention]]。 + +**三种可能里,第 3 种是对的,而它当时被排除了。** A/B,用 build.ninja 里那条**真实的 +编译命令**,只改 flag 这一维: + +| flag | `memset_pattern16` 引用 | 目标文件 | +|---|---|---| +| 如实际构建(带 `-fno-builtin-memset_pattern16`) | **1** | 38200 | +| **把那个 flag 去掉** | **1** | — | +| `-fno-builtin` | 0 | 38888(+1.8%) | +| `-ffreestanding` | 0 | — | +| `-mllvm -disable-loop-idiom-memset` | 0 | 38152 | + +**加与不加,引用数都是 1——那个 flag 什么都没做。** + +**而且它不会报。** clang 对 `-fno-builtin-` 不给任何反馈: + +``` +-fno-builtin-totally_not_a_function accepted silently +``` + +`memset_pattern16` 是 LLVM TLI 的 libfunc 而不是 clang 的 builtin,所以这个 token +**看起来在做事,实际是个静默的空操作**。预处理后的源码里该符号出现 **0 次**,确认它确实 +是优化器生成的。 + +**缺口是引擎侧的,而且是成对的:** + +1. `builtins = "iso"` 发的 token 在它唯一点名的那个 case 上无效; +2. **那个 token 没有任何核对。** 同一个文件里 `-D`/`-U` 有探针核对 + `expectDefined` / `expectUndefined`,注释明写「一个没生效的 `-D` 是校验失败,不是 + 沉默」——而 `builtinsTokens` 走的是另一条路,没有对应的 `expect*`。 + +**判据**:`builtinsTokens` 要像 `-D` 一样被探针核对。没有这一条,换成任何一个新 token +都可能重复这次静默——**一个只在「它起作用」这一假设下成立的机制,需要一条断言它起作用的 +判据。** + +**三个候选修法的代价已量**(表见上):`-fno-builtin` 正确但连 ISO 函数的优化一起关掉, +在这个翻译单元上 +1.8%;`-mllvm` 更省但不是稳定的用户面;第三条是给 openkal-macos 提供 +`memset_pattern16` 本身,那与 `builtins = "iso"` 的意图相反。**先量再选,不在这里替人 +拍板。** + ### 2.3 实现侧(openkal-*) #### C4' — 「不复现」是在另一个版本上量的(2026-09-21 更正) From 93f5bfce7460d4d7792c8946e2b177375e89d673 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 06:53:02 +0800 Subject: [PATCH 04/13] the symbol belongs to no layer that carries it, which makes this an architecture choice `memset_pattern16` is not a function programs call; it is a helper the code generator emits, in the same family as `memcpy` and `__udivti3`. That family belongs to the compiler runtime. Measured: grep -rln memset_pattern16 openkal-llvm-runtime/llvm/ -> 0 hits LLVM carries no implementation anywhere, so compiler-rt has no fallback: the call upstream emits is one only Apple's libSystem supplies. `builtins = "iso"` therefore meets a case it can DECLARE and cannot ENFORCE. The flag that would enforce it does not work, and LLVM offers no second one. Four candidates, costs measured: -fno-builtin works, +1.8% here, disables ISO functions' optimisation too -mllvm -disable-loop-idiom-memset works, -0.1%, not a stable interface supply it in openkal-musl's Apple ~6 lines; port/src/mach/ exists; port contrary to the declaration's wording declare the member unbuildable discards a combination that works The third asks the better question. The declaration is about what the C library PRESENTS, and a compiler-emitted helper is not an interface the program requested --- it is closer to ABI. A C library presenting only ISO C may still owe the code generator the helpers it assumes for that target, the same way it owes `memcpy`. This is a decision, not an implementation: it settles whether `builtins = "iso"` means "turn off what the generator assumes" or "declare the surface the program can see". Those diverge in other cases too. --- ...kal-ecosystem-completion-and-acceptance.md | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 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 048b0052..896ba13f 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 @@ -535,10 +535,35 @@ ld64.lld: error: undefined symbol: memset_pattern16 都可能重复这次静默——**一个只在「它起作用」这一假设下成立的机制,需要一条断言它起作用的 判据。** -**三个候选修法的代价已量**(表见上):`-fno-builtin` 正确但连 ISO 函数的优化一起关掉, -在这个翻译单元上 +1.8%;`-mllvm` 更省但不是稳定的用户面;第三条是给 openkal-macos 提供 -`memset_pattern16` 本身,那与 `builtins = "iso"` 的意图相反。**先量再选,不在这里替人 -拍板。** +**这个符号属于哪一层,查过了,而答案把问题从「bug 修法」变成「架构选择」。** + +它不是程序调用的函数,是**编译器发出的辅助调用**——和 `memcpy`、`__udivti3` 同类。那一 +类归**编译器运行时**。但实测: + +``` +grep -rln memset_pattern16 openkal-llvm-runtime/llvm/ → 0 处 +``` + +**整个 LLVM 树里没有它的实现,compiler-rt 不带兜底。** 上游 LLVM 发出的是一个只有 +Apple 的 libSystem 提供的调用。 + +于是 `builtins = "iso"` 遇到一个它**声明得了、强制不了**的情形:能强制它的那个 flag 不 +工作,而 LLVM 没有提供第二个。四个候选,代价都已量: + +| 候选 | 有效 | 代价 | 性质 | +|---|---|---|---| +| `-fno-builtin` | 是 | +1.8%(本翻译单元);**连 ISO 函数的优化一起关掉** | 标准用户面,正确但过宽 | +| `-mllvm -disable-loop-idiom-memset` | 是 | −0.1% | **不是稳定接口**,LLVM 升级可能失效 | +| 在 openkal-musl 的 Apple 端口提供该符号 | 未验 | 约六行(`port/src/mach/` 已存在) | **与 `builtins = "iso"` 的字面意图相反** | +| 不动,把该成员声明为按构造失败 | — | 丢掉一个真实可用的组合 | 最弱 | + +**第三个候选值得认真看**,因为它把问题问对了:`builtins = "iso"` 说的是「这个 C 库呈现 +什么」,而编译器发出的辅助调用**不是程序请求的接口**,它更接近 ABI。一个声明自己只呈现 +ISO C 的 C 库,仍然可能需要供给代码生成器为那个目标假定的辅助函数——正如它供给 +`memcpy` 一样。 + +**这是拍板项,不是实现项。** 它决定 `builtins = "iso"` 到底是「关掉生成器的假定」还是 +「声明程序可见的接口面」——两者在别的情形下也会分叉。 ### 2.3 实现侧(openkal-*) From 65bf6758b740aa48d9db30fa7cc79fde04b58447 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 07:06:55 +0800 Subject: [PATCH 05/13] the token was accepted in silence and changed nothing `[c-abi] builtins = "iso"` emitted `-fno-builtin-memset_pattern16` from this mechanism's first revision. A/B on a real compile command, varying only that flag, reads as built (flag present) 1 reference to memset_pattern16 flag REMOVED 1 reference -fno-builtin 0 -mllvm -disable-loop-idiom-memset 0 and clang accepts `-fno-builtin-totally_not_a_function` just as quietly: the `-fno-builtin-` family is matched against clang's builtin table, while `memset_pattern16` is an LLVM TargetLibraryInfo libfunc. The call is emitted by LoopIdiomRecognize, which consults TLI, and the per-function attribute does not reach it. `-mllvm` is not chosen because it passes an internal LLVM option, which can be renamed or removed between releases; when it is, the mechanism returns to failing silently, which is the defect being repaired. The cost of the blunt flag is measured rather than argued: on the translation unit that surfaced this the object grows 38200 to 38888 bytes, 1.8 per cent. THE NO-OP SURVIVED BECAUSE IT HAD NO CRITERION. `cenv` verifies its tokens against a `-dM` dump, and a code-generation property is not visible there. The criterion now lives in `openkal-cross.yml` and has three legs, on all three hosts: no flag (the symbol MUST appear, or the probe measures nothing), the per-function flag (it must still appear, pinning the defect), and an `aarch64-macos` build over the openkal stack by the mcpp under test (zero references). Run against the previous binary the step fails, at the link: ld64.lld: error: undefined symbol: memset_pattern16 --- .github/workflows/openkal-cross.yml | 111 ++++++++++++++++++++++++++++ CHANGELOG.md | 39 ++++++++++ docs/22-target-side.md | 2 +- docs/zh/22-target-side.md | 2 +- src/toolchain/cenv.cppm | 44 ++++++++++- tests/unit/test_cenv.cpp | 9 ++- tests/unit/test_cenv_probe.cpp | 4 +- 7 files changed, 205 insertions(+), 6 deletions(-) diff --git a/.github/workflows/openkal-cross.yml b/.github/workflows/openkal-cross.yml index b2bc67d3..7ccd6a63 100644 --- a/.github/workflows/openkal-cross.yml +++ b/.github/workflows/openkal-cross.yml @@ -264,6 +264,117 @@ jobs: echo "${{ matrix.host }} → $t : $(ls -l "$a" | awk '{print $5}') bytes" done + # THE ONLY CRITERION THE `builtins` TOKEN HAS, AND WHY IT IS HERE. + # + # `[c-abi] builtins = "iso"` states that the C library supplies the ISO + # functions and no vendor extensions. On Darwin targets clang's loop + # idiom recogniser rewrites a constant-pattern fill into a call to + # `memset_pattern16`, an Apple libc extension no such library carries. + # That call is produced by the code generator, so it appears in no `-D` + # and in no preprocessor dump --- and `mcpp.toolchain.cenv` verifies its + # tokens by comparing a `-dM` dump. The token this mechanism emitted was + # therefore a silent no-op for the whole of its first life, and this + # step exists because no other kind of check could have reported it. + # + # LEG 1 IS THE DENOMINATOR, AND IT IS NOT DECORATION. At `-O0` the pass + # does not run, and `-ffreestanding` implies `-fno-builtin`; an + # assertion that only reads "the symbol is absent" therefore passes in + # several worlds where nothing was measured. Leg 1 compiles with no flag + # at all and fails if the symbol does NOT appear. + # + # LEG 2 PINS THE DEFECT ITSELF. `-fno-builtin-memset_pattern16` is + # accepted in silence and changes nothing: `-fno-builtin-` is matched + # against clang's builtin table, and `memset_pattern16` is an LLVM + # TargetLibraryInfo libfunc rather than a clang builtin. The toolchain is + # pinned in this job, so the reading is stable. Should a later pin make + # leg 2 fail, clang has gained the narrower behaviour, and `cenv` can + # emit the narrower flag and recover the 1.8 per cent `-fno-builtin` + # costs. + # + # LEG 3 IS THE ENGINE: the same idiom, over the openkal stack, for + # `aarch64-macos`, built by the mcpp under test. Measured on the token + # this step was written for, the two readings are + # + # -fno-builtin-memset_pattern16 1 reference, and the link fails + # -fno-builtin 0 references, and it links + # + # so a regression here reports itself at the link before the assertion + # is reached. The assertion covers the remaining case, in which some + # layer happens to supply the symbol and the link succeeds anyway. + # + # `-O2` IS PER PACKAGE RATHER THAN `--release`. The idiom pass does not + # run at the dev profile's `-O0`, and a release build would compile the + # runtime a second time in a second profile for no reading. + - name: builtins = "iso" withdraws the Apple pattern fill + run: | + set -euo pipefail + BIN="${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs/xim-x-llvm/22.1.8/bin" + # Not `ls ... | head -1`: with `pipefail` the absent candidate's + # exit status ends the step before the guard below is reached. + CLANG="" + for c in "$BIN/clang" "$BIN/clang.exe"; do + if [ -x "$c" ]; then CLANG="$c"; break; fi + done + [ -n "$CLANG" ] || { echo "::error::no clang under $BIN"; exit 1; } + + # Inside the clone, so the dependency is named by a relative path. + # `$RUNNER_TEMP` is a backslash path on the Windows host and a TOML + # string would read its separators as escapes. + W="$RUNNER_TEMP/okl/examples/builtins-probe" + rm -rf "$W"; mkdir -p "$W/src" + + # The one shape the idiom recogniser rewrites. The element type is + # `int` and not `char` because a byte-repeating value becomes + # `memset`, which every C library has. + cat > "$W/src/main.cpp" <<'PROBE' + extern "C" void fill(int* a, long n) { + for (long i = 0; i < n; ++i) a[i] = 0x01020304; + } + + int main() { + static int buf[64]; + fill(buf, 64); + return buf[0] == 0x01020304 ? 0 : 1; + } + PROBE + + cat > "$W/mcpp.toml" <<'PROJECT' + [package] + name = "openkal-builtins-probe" + version = "0.1.0" + + [build] + cxxflags = ["-O2"] + + [dependencies] + openkal-llvm-runtime = { path = "../.." } + + [toolchain] + default = "llvm@22.1.8" + PROJECT + + # The symbol name is in the object's string table, so a byte match + # reads it without naming an `llvm-nm` whose path differs per host. + refs() { grep -ac memset_pattern16 "$1" 2>/dev/null || true; } + T=--target=arm64-apple-macos14.0 + + "$CLANG" $T -O2 -c "$W/src/main.cpp" -o "$W/bare.o" + "$CLANG" $T -O2 -fno-builtin-memset_pattern16 -c "$W/src/main.cpp" -o "$W/narrow.o" + "$CLANG" $T -O2 -fno-builtin -c "$W/src/main.cpp" -o "$W/blunt.o" + echo "no flag : $(refs "$W/bare.o")" + echo "-fno-builtin-memset_pattern16 : $(refs "$W/narrow.o")" + echo "-fno-builtin : $(refs "$W/blunt.o")" + + [ "$(refs "$W/bare.o")" = 1 ] || { echo "::error::leg 1: the probe no longer triggers the idiom, so legs 2 and 3 measure nothing"; exit 1; } + [ "$(refs "$W/narrow.o")" = 1 ] || { echo "::error::leg 2: clang now honours -fno-builtin-memset_pattern16, and mcpp.toolchain.cenv can emit the narrower token"; exit 1; } + [ "$(refs "$W/blunt.o")" = 0 ] || { echo "::error::leg 2: -fno-builtin no longer withdraws the pattern fill"; exit 1; } + + (cd "$W" && "$MCPP_UNDER_TEST" build --target aarch64-macos) + obj=$(find "$W/target" -name 'main.o' | head -1) + [ -n "$obj" ] || { echo "::error::leg 3 produced no object to read"; exit 1; } + echo "engine, aarch64-macos over openkal: $(refs "$obj")" + [ "$(refs "$obj")" = 0 ] || { echo "::error::leg 3: builtins = \"iso\" did not withdraw memset_pattern16"; exit 1; } + - uses: actions/upload-artifact@v4 with: name: openkal-built-on-${{ matrix.host }} diff --git a/CHANGELOG.md b/CHANGELOG.md index aa498978..0c8ee801 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,45 @@ ## [Unreleased] +### `builtins = "iso"` 发的那个 token 是静默空操作,已换成 `-fno-builtin` + +`[c-abi] builtins = "iso"` 声明 C 库只提供 ISO 函数、没有厂商扩展。Apple 目标上 +clang 的循环惯用法识别会把常量模式填充改写成 `memset_pattern16` 调用——那是一个 +Apple libc 扩展,这样的库没有它。此前这里发的是 `-fno-builtin-memset_pattern16`。 +在一份真实的 `build.ninja` 编译命令上做 A/B,只改这一个 flag: + +| flag | `memset_pattern16` 引用数 | +|---|---| +| 照原样(flag 在) | 1 | +| flag **删掉** | 1 | +| `-fno-builtin` | 0 | +| `-mllvm -disable-loop-idiom-memset` | 0 | + +**而它报不出自己什么都没做。** clang 静默接受 `-fno-builtin-totally_not_a_function`: +`-fno-builtin-X` 这一族按 clang 的 builtin 表校验,而 `memset_pattern16` 是 LLVM +TargetLibraryInfo 的 libfunc,不在那张表里;发出调用的是 LoopIdiomRecognize,它查 +TLI,按函数名的属性到不了它。 + +不选 `-mllvm` 的理由是它传的是 LLVM 内部选项,不是受支持的接口,改名或删除之后这套 +机制会再次静默失效——那正是这次要修的缺陷本身。代价是量出来的:在暴露此事的那个翻译 +单元(libarchive 的 7zip reader,`-O2`,aarch64-macos)上,目标文件从 38200 涨到 +38888 字节,1.8%,因为 `-fno-builtin` 同时撤走了 C 库确实提供的那些 ISO 函数。这比 +`builtins = "iso"` 声明的范围宽,而它宽在安全的方向:代码生成器不合成的调用不会变成 +链接错误。 + +**这个空操作能活下来,是因为它没有判据。** `cenv` 的探针用 `-dM` dump 校验自己发的 +token,而代码生成阶段的性质在预处理器 dump 里不可见。判据现在在 +`.github/workflows/openkal-cross.yml`,三条腿,跑在三台宿主上: + +| 腿 | 内容 | 判据 | +|---|---|---| +| 1 | 不带任何 flag 编译探针 | 符号**必须出现**——否则探针已经触发不了惯用法,腿 2、3 什么都没测 | +| 2 | `-fno-builtin-memset_pattern16` | 符号**仍必须出现**(钉住这个缺陷;若哪天红了,说明 clang 认了窄拼法,`cenv` 可以改回去把这 1.8% 拿回来) | +| 3 | mcpp 为 `aarch64-macos` 走 openkal 栈构建同一份源码 | 目标文件里**零引用** | + +腿 3 的对照:换回旧 token,同一个工程链接失败于 +`ld64.lld: error: undefined symbol: memset_pattern16`。 + ### 更正:那个「四个成员」是二,而分组用错了依据 2026.9.21.2 的条目、`cenv.cppm` 与 `predefines.cppm` 的注释、`docs/21` 与 `docs/22` diff --git a/docs/22-target-side.md b/docs/22-target-side.md index 1956cdfb..3039ef48 100644 --- a/docs/22-target-side.md +++ b/docs/22-target-side.md @@ -378,7 +378,7 @@ that names no C library: | 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 | -| 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 | +| any | `builtins = "iso"` | turns off code-generation idioms that assume a platform C library — `-fno-builtin` on Apple targets, because the per-function spelling was measured to be a silent no-op for the one idiom that matters (`memset_pattern16` is an LLVM TargetLibraryInfo libfunc, not a clang builtin); see `src/toolchain/cenv.cppm` for the A/B and the measured cost | | anything else | | refused, naming the target, the request and what is missing — never a silent downgrade | **The macOS and freestanding rows are a correction, not the design's original diff --git a/docs/zh/22-target-side.md b/docs/zh/22-target-side.md index 5405f82b..de834566 100644 --- a/docs/zh/22-target-side.md +++ b/docs/zh/22-target-side.md @@ -310,7 +310,7 @@ libunwind 的 `assembly.h`,正是按这个宏来选寄存器保存集的)。mcpp | 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 是三元组切换的结果,不是另一个开关 | -| 任意目标 | `builtins = "iso"` | 关闭代码生成阶段假定平台 C 库在场的惯用法识别——本轮实测到的唯一一例是 Apple 目标上的 `-fno-builtin-memset_pattern16`;`src/toolchain/cenv.cppm` 记录了还核实过哪些、结论是不适用 | +| 任意目标 | `builtins = "iso"` | 关闭代码生成阶段假定平台 C 库在场的惯用法识别——Apple 目标上发 `-fno-builtin`,因为按函数名的那个拼法对唯一重要的那个惯用法实测是**静默空操作**(`memset_pattern16` 是 LLVM TargetLibraryInfo 的 libfunc,不是 clang 的 builtin);A/B 与实测代价见 `src/toolchain/cenv.cppm` | | 其余情况 | | 明确拒绝,点名目标、请求与缺什么——不静默降级 | **macOS 与裸机这两行是一次修正,不是设计原文(协调者修订,2026.9.18.1 发布不到一天就被真实 diff --git a/src/toolchain/cenv.cppm b/src/toolchain/cenv.cppm index bbab8ff0..ed20f2bd 100644 --- a/src/toolchain/cenv.cppm +++ b/src/toolchain/cenv.cppm @@ -558,9 +558,51 @@ inline std::expected realise( // So `iso` realises to one flag, on Apple targets only, and does nothing // measurable elsewhere today. That is reported rather than silently // accepted: a caller that wants to know what changed reads `builtinsTokens`. + // `-fno-builtin`, NOT `-fno-builtin-memset_pattern16`, AND THE NARROWER + // SPELLING WAS A SILENT NO-OP. + // + // The per-function form was emitted here from this mechanism's first + // revision, because `memset_pattern16` is the one platform idiom measured + // to matter and a targeted flag looks like the smaller instrument. A/B on + // the real compile command from a build.ninja, varying only this flag, + // says it does nothing: + // + // as built (flag present) 1 reference to memset_pattern16 + // flag REMOVED 1 reference + // -fno-builtin 0 + // -mllvm -disable-loop-idiom-memset 0 + // + // AND IT CANNOT REPORT THAT IT DOES NOTHING. clang accepts + // `-fno-builtin-totally_not_a_function` in silence: the `-fno-builtin-X` + // family is checked against clang's builtin table, and + // `memset_pattern16` is an LLVM TargetLibraryInfo libfunc rather than a + // clang builtin. The call is produced by LoopIdiomRecognize, which + // consults TLI, and the per-function attribute does not reach it. + // + // WHY THE BLUNT ONE AND NOT `-mllvm`. `-mllvm` passes an internal LLVM + // option; it is not a supported interface and can be renamed or removed + // between releases, and when it is, this mechanism goes back to failing + // silently --- which is exactly the defect being repaired. + // + // THE COST IS MEASURED RATHER THAN ARGUED: on the translation unit that + // surfaced this (libarchive's 7zip reader, `-O2`, aarch64-macos) the + // object grows 38200 to 38888 bytes, 1.8 per cent, because `-fno-builtin` + // also withdraws the ISO functions the C library does supply. That is + // broader than `builtins = "iso"` declares, and it errs in the safe + // direction: a call the generator does not synthesise is never a link + // error. + // + // NOTHING HERE IS VERIFIED BY THE PROBE, WHICH IS HOW THE NO-OP SURVIVED. + // `expectDefined`/`expectUndefined` are compared against the probe's `-dM` + // dump, under this module's own rule that a `-D` which did not take effect + // is a verification failure rather than a silent one. A code-generation + // property is not visible in a preprocessor dump, so the criterion for + // this token lives in `openkal-cross.yml`, which compiles an + // idiom-triggering unit for `aarch64-macos` over the openkal stack and + // asserts the symbol is absent from the object. if (decl.builtins == mcpp::targetside::CAbiBuiltins::Iso) { if (!freestanding && (os == "macos" || os == "ios")) - r.builtinsTokens.push_back("-fno-builtin-memset_pattern16"); + r.builtinsTokens.push_back("-fno-builtin"); } return r; diff --git a/tests/unit/test_cenv.cpp b/tests/unit/test_cenv.cpp index d03e6069..3cffcf73 100644 --- a/tests/unit/test_cenv.cpp +++ b/tests/unit/test_cenv.cpp @@ -147,7 +147,14 @@ TEST(CEnv, BuiltinsIsoOnMacosDisablesMemsetPattern16) { ts::CAbiBuiltins::Iso); auto r = cenv::realise(d, "macos", "x86_64", false); ASSERT_TRUE(r.has_value()) << r.error(); - EXPECT_TRUE(has(r->builtinsTokens, "-fno-builtin-memset_pattern16")); + // `-fno-builtin`, NOT the per-function spelling. The narrower form was + // emitted here and measured to do nothing: clang checks `-fno-builtin-X` + // against its builtin table, `memset_pattern16` is an LLVM TLI libfunc, + // and the call is produced by a pass that consults TLI. clang accepts an + // unknown name in that family in silence, so the flag looked right and + // was not. + EXPECT_TRUE(has(r->builtinsTokens, "-fno-builtin")); + EXPECT_FALSE(has(r->builtinsTokens, "-fno-builtin-memset_pattern16")); } TEST(CEnv, BuiltinsIsoOnLinuxAddsNothingMeasurable) { diff --git a/tests/unit/test_cenv_probe.cpp b/tests/unit/test_cenv_probe.cpp index 68b85636..946b50c5 100644 --- a/tests/unit/test_cenv_probe.cpp +++ b/tests/unit/test_cenv_probe.cpp @@ -352,11 +352,11 @@ TEST(CenvProbeArgv, AHostedCrossTargetCarriesItsCrossFlag) { TEST(CenvProbeArgv, BuiltinsTokensAreCarriedLast) { auto r = cp::assemble_argv("", {}, {"-D__unix__"}, - {"-fno-builtin-memset_pattern16"}, + {"-fno-builtin"}, false, "x86_64-apple-macos"); ASSERT_TRUE(r.has_value()) << r.error(); ASSERT_EQ(r->size(), 2u); - EXPECT_EQ((*r)[1], "-fno-builtin-memset_pattern16"); + EXPECT_EQ((*r)[1], "-fno-builtin"); } // ── The argv must select the target (mcpp#674 review, 2026-09-20) ────────── From e3a6aec80d4f0a4785dee7355c51907cc5ba7696 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 07:23:43 +0800 Subject: [PATCH 06/13] a target that cannot be run could only be measured by not building it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `mcpp test` for a target this host can neither execute nor reach through a runner leaves every test `not run` and exits 2. That is the correct answer to "do these tests pass" — mcpp did not find out — but 2 is also what a broken runner returns, so a caller that wanted only the build cannot tell the two apart and falls back to `mcpp build`. AND `mcpp build` BUILDS THE PACKAGE. For a package whose only sources are under `tests/` it compiles nothing of it at all. Measured on mcpp-index's `archive` member, whose sources are two files under `tests/`: $ mcpp build --target aarch64-macos # exits 0 Compiling compat.lz4 / compat.xz / compat.zlib / compat.zstd … $ find target -name '*compression*' -o -name '*versions*' (only musl's versionsort.o) The member's dependencies compiled; not one line of the member did. A compatibility sweep reading that exit code records the member as building on macOS, which is a reading about the dependencies with the member's name on it. `--no-run` gives the narrower claim its own answer: every selected test is compiled and linked for the target, none is executed, and the result says so. `built` is counted apart from `not_run`, in the human summary and in the machine interface, because `not_run` means mcpp tried and could not — the question is open, the exit code is 2 — while `built` means it was told not to, so the build was the whole question and the exit code is 0. The criterion is `745_no_run_builds_the_tests_and_says_so.sh`, four legs. Its runner is a name that is not a program: an unexecutable target would make the test need a cross toolchain and a host that cannot run it, while a runner that cannot be found produces the same situation on every host, for the native target, with nothing installed. Run against a binary without the flag it fails at leg B. --- CHANGELOG.md | 35 +++++ docs/08-testing.md | 18 +++ docs/50-machine-output.md | 13 +- docs/zh/08-testing.md | 14 ++ docs/zh/50-machine-output.md | 10 +- src/build/execute.cppm | 46 +++++- src/cli.cppm | 4 +- src/cli/cmd_build.cppm | 20 ++- ...745_no_run_builds_the_tests_and_says_so.sh | 144 ++++++++++++++++++ 9 files changed, 294 insertions(+), 10 deletions(-) create mode 100755 tests/e2e/745_no_run_builds_the_tests_and_says_so.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c8ee801..ac0e0e23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,41 @@ ## [Unreleased] +### `mcpp test --no-run`:为一个跑不了的目标构建测试,并把这当成答案 + +一个本机既不能执行、也没有 runner 可达的目标,会让每个测试停在 `not run`,命令退出 2。 +这是对「这些测试通过吗」的正确回答——mcpp 没有查明。但 **2 同样是 runner 坏掉时的 +退出码**,于是一个只想要「构建」的调用方无法区分这两者,只能退回去用 `mcpp build`。 + +而 `mcpp build` 构建的是**包**。对一个源码只在 `tests/` 下的包,它一行都不编译。 +实测 mcpp-index 的 `archive` 成员(源码是 `tests/` 下两个文件): + +``` +$ mcpp build --target aarch64-macos # 退出 0 + Compiling compat.lz4 / compat.xz / compat.zlib / compat.zstd ... +$ find target -name '*compression*' -o -name '*versions*' + (只有 musl 的 versionsort.o) +``` + +退出 0,编译了这个成员的**依赖**,而**该成员自己的代码一行都没有编过**。一个读这个 +退出码的兼容性测量,会把它记成「这个成员在 macOS 上构建通过」。 + +`--no-run` 让那个更窄的断言有了自己的答案:被选中的每个测试都为该目标编译并链接, +没有任何一个被执行,结果也这么写: + +``` +test result ok. 0 passed; 0 failed; 2 built, not run +``` + +`built` 与 `not_run` 分开计数,机器接口也一样(`docs/50`):`not_run` 是「试过而做不到, +问题悬着,退出 2」,`built` 是「被要求不要执行,构建就是问题的全部,退出 0」。编译不过 +的测试仍然是失败。`--no-run` 与 `--no-runner` 同时给出会被拒绝——两个名字只差一个字符 +而含义相反,不存在应当优先的那一个读法。 + +判据是 `tests/e2e/745_no_run_builds_the_tests_and_says_so.sh`,四条腿。其中 runner 用的是 +一个**不存在的程序名**:用一个执行不了的目标会让这个测试需要交叉工具链和特定宿主,而 +一个找不到的 runner 在每台宿主上、对本机目标、不装任何东西,就能造出同一个局面。 + ### `builtins = "iso"` 发的那个 token 是静默空操作,已换成 `-fno-builtin` `[c-abi] builtins = "iso"` 声明 C 库只提供 ISO 函数、没有厂商扩展。Apple 目标上 diff --git a/docs/08-testing.md b/docs/08-testing.md index 1ce9abe6..04fe4849 100644 --- a/docs/08-testing.md +++ b/docs/08-testing.md @@ -105,6 +105,7 @@ mcpp performs with the test binary appended. ```bash mcpp test --target thumbv7em-none-eabihf # built for the board, run through its runner mcpp test --no-runner # ignore the runner and execute directly +mcpp test --target aarch64-macos --no-run # build the tests for the target and stop ``` Nothing about the test changes. The same `tests/**/*.cpp` compiles for the @@ -114,6 +115,23 @@ or a QEMU exit code is what a bare-metal runner is chosen to produce. `--no-runner` exists for a host that can execute the binaries natively and should not pay for an emulator. +`--no-run` makes the narrower claim, and it has to be asked for. Without it, a +target this host can neither execute nor reach through a runner leaves every +test not run and the command exits 2: mcpp did not establish whether the tests +pass, and reporting that as success is the false reading this repository has +recorded most often. But 2 is also what a broken runner returns, so a caller +that wanted only the build could not tell the two apart. Under `--no-run` every +selected test is compiled and linked for the target, none is executed, and the +result says so: + +``` +test result ok. 0 passed; 0 failed; 2 built, not run +``` + +A test that does not compile is still a failure, and `--no-run` with +`--no-runner` is refused rather than resolved: one says to run the binaries +without the declared runner, the other says not to run them. + A test program carries the files it reads beside it: the runner receives `MCPP_RUNTIME_FILES`, the list of its deployed files and the shared libraries it loads, and a runner that moves the program to a device copies them with it. A diff --git a/docs/50-machine-output.md b/docs/50-machine-output.md index d27aaec2..22237387 100644 --- a/docs/50-machine-output.md +++ b/docs/50-machine-output.md @@ -523,8 +523,8 @@ Per test: |---|---| | `member` | the workspace member, or `""` outside a workspace | | `test` | the path-based test name (`tests/00-a/0.cpp` → `00-a/0`) | -| `status` | `pass`, `compile_fail`, `run_fail`, or `not_run` | -| `exit_code` | the test's exit status; `0` for `not_run` | +| `status` | `pass`, `compile_fail`, `run_fail`, `not_run`, or `built` | +| `exit_code` | the test's exit status; `0` for `not_run` and `built` | | `signal` | the signal number when the status encodes one, else `null` | | `duration_ms` | build+run wall time of this test | | `timed_out` | `true` when `--timeout` killed it (`run_fail`) | @@ -538,8 +538,17 @@ Summary record, `{"summary": {...}}`: | `member`, `passed`, `failed` | counts | | `not_run` | tests that were built and not executed | | `not_run_reason` | the reason shared by all of them, or `""` | +| `built` | tests built under `--no-run`, which were not to be executed | | `elapsed_ms`, `build_ms`, `run_ms` | wall time, split | +**`built` and `not_run` are different answers and are counted apart.** Both +describe a test that was compiled and not executed, and that is where the +resemblance ends: `not_run` means mcpp tried and could not, so the question +is open and the exit code is 2; `built` means `--no-run` said not to, so the +build was the whole question and the exit code is 0. A consumer that added +the two together would report a run it never asked for as one that could not +be performed. + **`not_run` is neither `pass` nor `run_fail`, and the exit code says so (2026.9.2.1).** A test is `not_run` when this host cannot load its artifact (`Exec format error` on a cross target with no runner declared), or when the diff --git a/docs/zh/08-testing.md b/docs/zh/08-testing.md index 520edd95..9070d027 100644 --- a/docs/zh/08-testing.md +++ b/docs/zh/08-testing.md @@ -96,6 +96,7 @@ runner 是板级支持包提供的一串 argv,mcpp 把测试二进制附加在 ```bash mcpp test --target thumbv7em-none-eabihf # 为板子构建,经它的 runner 运行 mcpp test --no-runner # 忽略 runner,直接执行 +mcpp test --target aarch64-macos --no-run # 只为目标构建测试,不执行 ``` 测试本身一个字都不用改。同样的 `tests/**/*.cpp` 为设备编译,判据仍然是退出码 —— @@ -103,6 +104,19 @@ mcpp test --no-runner # 忽略 runner,直接执行 `--no-runner` 是给「本机就能原生执行这些二进制、不该为模拟器付代价」的宿主准备的。 +`--no-run` 做的是更窄的那个断言,而它必须被显式要求。没有它时,一个本机既不能执行、 +也没有 runner 可达的目标,会让每个测试都停在 not run,命令退出 2:mcpp 没有查明这些 +测试是否通过,而把它报成成功是本仓记录得最多的一种假读数。但 2 同样是 runner 坏掉时 +的退出码,于是一个只想要「构建」的调用方无法区分这两者。在 `--no-run` 下,每个被选中 +的测试都为该目标编译并链接,没有任何一个被执行,结果也这么写: + +``` +test result ok. 0 passed; 0 failed; 2 built, not run +``` + +编译不过的测试仍然是失败;`--no-run` 与 `--no-runner` 同时给出会被拒绝,而不是在两者 +之间挑一个:一个说的是「不经声明的 runner 直接执行」,另一个说的是「不要执行」。 + 测试程序把它读取的文件带在身边:runner 收到 `MCPP_RUNTIME_FILES`,即它部署的文件与 它加载的共享库的清单,把程序移到设备上的 runner 连同这些文件一起复制。测试按相对于 自身所在目录的路径定位这类文件。在 Android 行上,除非 `cxx_runtime` 另有声明,测试 diff --git a/docs/zh/50-machine-output.md b/docs/zh/50-machine-output.md index 41711fb5..3e18c6cf 100644 --- a/docs/zh/50-machine-output.md +++ b/docs/zh/50-machine-output.md @@ -455,8 +455,8 @@ mcpp test [pattern] [--workspace] --message-format json |---|---| | `member` | workspace 成员;workspace 之外为 `""` | | `test` | 按路径命名的测试名(`tests/00-a/0.cpp` → `00-a/0`) | -| `status` | `pass`、`compile_fail`、`run_fail` 或 `not_run` | -| `exit_code` | 测试的退出状态;`not_run` 时为 `0` | +| `status` | `pass`、`compile_fail`、`run_fail`、`not_run` 或 `built` | +| `exit_code` | 测试的退出状态;`not_run` 与 `built` 时为 `0` | | `signal` | 状态编码了信号时是信号号,否则 `null` | | `duration_ms` | 这个测试构建+运行的墙钟时间 | | `timed_out` | 被 `--timeout` 杀掉时为 `true`(`run_fail`) | @@ -470,8 +470,14 @@ mcpp test [pattern] [--workspace] --message-format json | `member`、`passed`、`failed` | 计数 | | `not_run` | 已构建但没有执行的测试数 | | `not_run_reason` | 它们共同的原因,或 `""` | +| `built` | 在 `--no-run` 下构建、本就不打算执行的测试数 | | `elapsed_ms`、`build_ms`、`run_ms` | 墙钟时间,分段 | +**`built` 与 `not_run` 是两个不同的答案,分开计数。** 两者描述的都是「编译了但没有 +执行」的测试,相似之处到此为止:`not_run` 意味着 mcpp 试过而做不到,因此问题仍然悬着, +退出码是 2;`built` 意味着 `--no-run` 要求不要执行,因此构建就是问题的全部,退出码是 0。 +把两者相加的消费方,会把一次它从未要求过的运行,报成一次无法完成的运行。 + **`not_run` 既不是 `pass` 也不是 `run_fail`,退出码也这么说(2026.9.2.1)。** 本机无法加载测试产物(交叉目标未声明 runner 时的 `Exec format error`),或声明的 `[target.].runner` 找不到、启动不了时,测试为 `not_run`。这是关于整次调用的 diff --git a/src/build/execute.cppm b/src/build/execute.cppm index d5d0a7c6..e03bf692 100644 --- a/src/build/execute.cppm +++ b/src/build/execute.cppm @@ -2179,6 +2179,22 @@ export struct TestOptions { // runner — the operator on this host stating that the triple is native // here, a fact the manifest has no axis for (#544, D3). bool noRunner = false; + // `--no-run`: build the tests for `--target` and stop. THE CLAIM IT MAKES + // IS NARROWER THAN A PASS, AND IT IS STATED RATHER THAN INFERRED. + // + // Without it, a target this host cannot execute leaves every test `NotRun` + // and the command exits 2, which is correct: mcpp did not establish + // whether the tests pass, and a zero there is the false reading #544 + // records. But `2` is also what a broken runner returns, so a caller that + // wanted only the build --- a compatibility sweep measuring a target no + // runner exists for --- cannot tell "the tests built" from "the tests + // built and the runner is missing" and must settle for `mcpp build`, which + // builds the package and, for a package whose only sources are under + // `tests/`, compiles NOTHING of it at all. + // + // `--no-run` makes the narrower claim available as its own answer: every + // selected test compiled and linked for the target, and none was executed. + bool noRun = false; // Per-test RUN deadline. The default is deliberately non-zero: `mcpp test` // is something CI runs unattended, and an unbounded default makes a single // hung test able to consume the whole job with nothing to show for it. @@ -2211,6 +2227,10 @@ export struct TestRunSummary { // test did not run — and not a pass either: the exit code is 2. int notRun = 0; std::string notRunReason; + // Built and deliberately not executed (`--no-run`). Counted apart from + // `notRun` so the workspace total cannot add a stated build-only result to + // a run that mcpp could not perform. + int built = 0; long long buildMs = 0; // Phase A + bulk pass + per-test drives long long runMs = 0; // the test binaries' own execution long long elapsedMs = 0; // wall clock for the whole member @@ -2404,7 +2424,10 @@ export int run_tests(std::span passthrough, // Reporting that as `RunFail (exit 127)` states that the test ran and // returned 127, which is false and indistinguishable from a missing // program; reporting it as a pass would be read as one. - enum class St { Pass, CompileFail, RunFail, NotRun } status; + // `Built` (`--no-run`): compiled and linked, and deliberately not + // executed. Distinct from `NotRun`, which means mcpp tried and could + // not --- the difference is whether anything was left unanswered. + enum class St { Pass, CompileFail, RunFail, NotRun, Built } status; int exitCode = 0; std::string compileOutput; std::string runOutput; @@ -2422,6 +2445,7 @@ export int run_tests(std::span passthrough, const char* st = r.status == TestResult::St::Pass ? "pass" : r.status == TestResult::St::CompileFail ? "compile_fail" : r.status == TestResult::St::NotRun ? "not_run" + : r.status == TestResult::St::Built ? "built" : "run_fail"; std::string signal = (r.exitCode > 128 && r.exitCode < 128 + 65) ? std::to_string(r.exitCode - 128) : "null"; @@ -2891,7 +2915,18 @@ export int run_tests(std::span passthrough, // Pass 2: run them. Concurrently unless there is exactly one — see // `runJobs` for why the single-test case is deliberately different. - run_tests_now(runnable); + // + // UNDER `--no-run` THE LIST IS THE ANSWER. Everything that reaches + // `runnable` compiled and linked; a test that did not is already a + // `CompileFail` in `results` and keeps that status, so this path reports + // what was built without also reporting anything about what it does. + if (testOpts.noRun) { + for (auto& r : runnable) + results.push_back({r.name, TestResult::St::Built, 0, {}, {}, 0, + false, {}}); + } else { + run_tests_now(runnable); + } summary.elapsedMs = member_ms(); // 7. Summary. @@ -2900,8 +2935,10 @@ export int run_tests(std::span passthrough, int notRun = 0; std::string notRunReason; std::vector failures; + int built = 0; for (auto& r : results) { if (r.status == TestResult::St::Pass) ++passed; + else if (r.status == TestResult::St::Built) ++built; else if (r.status == TestResult::St::NotRun) { ++notRun; if (notRunReason.empty()) notRunReason = r.reason; @@ -2913,6 +2950,7 @@ export int run_tests(std::span passthrough, summary.failed = failed; summary.notRun = notRun; summary.notRunReason = notRunReason; + summary.built = built; // "build X + run Y" rather than one merged number: on a member whose tests // are cheap but whose link is not, those two are three orders of magnitude @@ -2932,9 +2970,10 @@ export int run_tests(std::span passthrough, if (json) { std::println("{{\"summary\":{{\"member\":\"{}\",\"passed\":{},\"failed\":{}," "\"not_run\":{},\"not_run_reason\":\"{}\"," + "\"built\":{}," "\"elapsed_ms\":{},\"build_ms\":{},\"run_ms\":{}}}}}", test_json_escape(memberName), passed, failed, - notRun, test_json_escape(notRunReason), + notRun, test_json_escape(notRunReason), built, summary.elapsedMs, summary.buildMs, summary.runMs); std::fflush(stdout); return rc; @@ -2944,6 +2983,7 @@ export int run_tests(std::span passthrough, // its reason: a quiet skip is read as a pass. First line of the reason // only — the full text was printed when it was established. auto counts = std::format("{} passed; {} failed", passed, failed); + if (built) counts += std::format("; {} built, not run", built); if (notRun) { auto firstLine = notRunReason.substr(0, notRunReason.find('\n')); counts += std::format("; {} not run ({})", notRun, firstLine); diff --git a/src/cli.cppm b/src/cli.cppm index fd3456c8..a84982a6 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -61,7 +61,7 @@ void print_usage() { std::println(" mcpp new Create a new package skeleton"); std::println(" mcpp build [options] Build the current package"); std::println(" mcpp run [target] [-- args...] Build + run a binary target"); - std::println(" mcpp test [pattern] [-- args...] Build + run tests/**/*.cpp (--list, --timeout, --build-timeout, --message-format json, --no-runner)"); + std::println(" mcpp test [pattern] [-- args...] Build + run tests/**/*.cpp (--list, --no-run, --timeout, --build-timeout, --message-format json, --no-runner)"); std::println(" mcpp clean [--stale] [--bmi-cache] Remove target/ (or, with --stale, only its non-current fingerprint dirs)"); std::println(" mcpp add [ns.]pkg@ver Add an exact dependency to mcpp.toml"); std::println(" mcpp remove [ns.]pkg Remove an exact dependency from mcpp.toml"); @@ -502,6 +502,8 @@ int run(int argc, char** argv) { .help("List (filtered) tests without building or running them")) .option(cl::Option("no-runner") .help("Run test binaries directly, ignoring any [target.].runner (a host that runs them natively)")) + .option(cl::Option("no-run") + .help("Build the tests for --target and stop; a build that succeeds is the result (for a target this host cannot execute)")) .option(cl::Option("timeout").takes_value().value_name("SECS") .help("Kill a test still RUNNING after SECS seconds (default 300; 0 = no limit)")) .option(cl::Option("build-timeout").takes_value().value_name("SECS") diff --git a/src/cli/cmd_build.cppm b/src/cli/cmd_build.cppm index ca7a9b2e..dc0a814d 100644 --- a/src/cli/cmd_build.cppm +++ b/src/cli/cmd_build.cppm @@ -539,6 +539,16 @@ export int cmd_test(const mcpplibs::cmdline::ParsedArgs& parsed, if (parsed.positional_count() > 0) to.filter = parsed.positional(0); to.list = parsed.is_flag_set("list"); to.noRunner = parsed.is_flag_set("no-runner"); // see cmd_run + to.noRun = parsed.is_flag_set("no-run"); + // The two read alike and mean opposite things: `--no-runner` says to run + // the binaries WITHOUT the declared runner, `--no-run` says not to run + // them at all. Asking for both is not a preference to resolve. + if (to.noRun && to.noRunner) { + mcpp::ui::error("--no-run and --no-runner cannot be combined: " + "--no-runner runs the test binaries directly, " + "--no-run does not run them."); + return 2; + } // The three deadlines share one parser: they differ only in what they // bound, not in how they are spelled. 0 always means "no limit" — for // --timeout that now has to be asked for rather than being the default. @@ -630,9 +640,15 @@ export int cmd_test(const mcpplibs::cmdline::ParsedArgs& parsed, std::format("member '{}' ({}/{}) FAILED — {} passed, {} failed in {:.2f}s", mp, idx, members->size(), sum.passed, sum.failed, secs)); } else { + // Under `--no-run` nothing passed and nothing was meant to: + // reporting "0 passed" for a member whose tests all built is + // the same sentence a member with no tests would produce. mcpp::ui::status("Workspace", - std::format("member '{}' ({}/{}) ok — {} passed in {:.2f}s", - mp, idx, members->size(), sum.passed, secs)); + sum.built + ? std::format("member '{}' ({}/{}) ok — {} built, not run in {:.2f}s", + mp, idx, members->size(), sum.built, secs) + : std::format("member '{}' ({}/{}) ok — {} passed in {:.2f}s", + mp, idx, members->size(), sum.passed, secs)); } } diff --git a/tests/e2e/745_no_run_builds_the_tests_and_says_so.sh b/tests/e2e/745_no_run_builds_the_tests_and_says_so.sh new file mode 100755 index 00000000..7a8f3dfb --- /dev/null +++ b/tests/e2e/745_no_run_builds_the_tests_and_says_so.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# requires: gcc +# 745 -- `mcpp test --no-run` builds the tests and reports that, and the +# report is distinguishable from the one mcpp gives when it tried to run them +# and could not. +# +# WHY THE TWO HAVE TO DIFFER. A target this host cannot execute leaves every +# test `not run` and the command exits 2, which is the correct answer to "do +# these tests pass": mcpp did not find out. But 2 is also what a missing or +# broken runner returns, so a caller that wanted only the build could not tell +# the two apart and had to use `mcpp build` instead -- which builds the +# PACKAGE, and for a package whose only sources are under `tests/` compiles +# nothing of it whatsoever. Measured on mcpp-index's `archive` member, whose +# sources are two files under `tests/`: `mcpp build --target aarch64-macos` +# exits 0 having compiled the member's dependencies and not one line of the +# member, and a compatibility sweep reading that exit code records the member +# as building on macOS. +# +# THE RUNNER HERE IS A NAME THAT IS NOT A PROGRAM, AND THAT IS THE POINT. +# Using an unexecutable target would make this test need a cross toolchain and +# a host that cannot run it; a declared runner that does not exist produces the +# same situation -- tests built, nothing run -- on every host, for the native +# target, with nothing installed. +# +# Four legs: +# A without `--no-run`: exit 2, and the tests are reported `not run`. +# B with `--no-run`: exit 0, and the count is reported as built. +# C a test that does not COMPILE is still a failure under `--no-run`. +# Without C, an implementation that reported everything as built whether +# or not it built would pass A and B. +# D `--no-run` with `--no-runner` is refused. The two names differ by one +# character and mean opposite things, and neither is a weaker form of the +# other, so there is no reading of the pair to prefer. +set -e + +MCPP="${MCPP:-mcpp}" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +cd "$work" + +# NO `src/`: the package's only sources are its tests, which is the shape the +# motivating case has and the shape `mcpp build` compiles nothing of. +mkdir -p tests +cat > mcpp.toml <<'EOF' +[package] +name = "norun" +version = "0.1.0" + +# A name, not a path, and not a program: the runner lookup fails, which is the +# situation this test is about. +[target.HOST_TRIPLE] +runner = ["mcpp-no-such-runner-exists"] +EOF +cat > tests/alpha.cpp <<'EOF' +int main() { return 0; } +EOF +cat > tests/beta.cpp <<'EOF' +int main() { return 0; } +EOF + +host="$("$MCPP" --print-target 2>/dev/null || true)" +if [ -z "$host" ]; then + # `--print-target` postdates some clients; take the triple from a build + # directory instead, which every version writes. + "$MCPP" build >/dev/null 2>&1 || true + host="$(ls target 2>/dev/null | grep -v '^\.' | head -1)" +fi +[ -n "$host" ] || { echo "FAIL: could not determine the host triple"; exit 1; } +sed -i.bak "s/HOST_TRIPLE/$host/" mcpp.toml && rm -f mcpp.toml.bak + +# --- A: it tried to run them and could not ------------------------------- +set +e +out_a="$("$MCPP" test --target "$host" 2>&1)" +rc_a=$? +set -e +printf '%s\n' "$out_a" | tail -3 +if [ "$rc_a" != 2 ]; then + echo "FAIL: A expected exit 2 from a runner that does not exist, got $rc_a" + exit 1 +fi +case "$out_a" in + *"not run"*) ;; + *) echo "FAIL: A did not report the tests as not run"; exit 1 ;; +esac + +# --- B: it was asked not to ---------------------------------------------- +set +e +out_b="$("$MCPP" test --target "$host" --no-run 2>&1)" +rc_b=$? +set -e +printf '%s\n' "$out_b" | tail -3 +if [ "$rc_b" != 0 ]; then + echo "FAIL: B expected exit 0 under --no-run, got $rc_b" + exit 1 +fi +case "$out_b" in + *"2 built, not run"*) ;; + *) echo "FAIL: B did not report two tests as built"; exit 1 ;; +esac +# The word that distinguishes B from A must not appear in B: "N not run" is +# the reading B exists to replace, and a summary carrying both says neither. +case "$out_b" in + *"2 not run"*) echo "FAIL: B reported the tests as not run as well as built"; exit 1 ;; +esac + +# --- C: --no-run does not make a broken test pass ------------------------- +cat > tests/gamma.cpp <<'EOF' +int main() { this_function_does_not_exist(); } +EOF +set +e +out_c="$("$MCPP" test --target "$host" --no-run 2>&1)" +rc_c=$? +set -e +if [ "$rc_c" = 0 ]; then + echo "FAIL: C a test that does not compile was reported as built" + printf '%s\n' "$out_c" | tail -5 + exit 1 +fi +# Non-zero is not enough: it must be non-zero BECAUSE `gamma` did not build, +# and the other two must still be reported as built. +case "$out_c" in + *gamma*) ;; + *) echo "FAIL: C failed without naming the test that did not compile"; exit 1 ;; +esac +case "$out_c" in + *"2 built, not run"*) ;; + *) echo "FAIL: C stopped reporting the tests that did build"; printf '%s\n' "$out_c" | tail -4; exit 1 ;; +esac + +# --- D: the two spellings are not a preference ---------------------------- +set +e +out_d="$("$MCPP" test --target "$host" --no-run --no-runner 2>&1)" +rc_d=$? +set -e +if [ "$rc_d" = 0 ]; then + echo "FAIL: D --no-run with --no-runner was accepted" + exit 1 +fi +case "$out_d" in + *"cannot be combined"*) ;; + *) echo "FAIL: D did not explain why the pair is refused"; printf '%s\n' "$out_d" | tail -3; exit 1 ;; +esac + +echo "PASS: --no-run builds the tests and says so, and says nothing else" From e82fe11113962734159e2f1d50284e1d7ab6251a Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 07:35:48 +0800 Subject: [PATCH 07/13] the reader was a byte match and the hosts do not agree about binaries Leg 1 of the builtins criterion failed on the macOS host with all three readings 0, which is what leg 1 exists to report: the two legs behind it were measuring nothing. The cause is the reader, not the compiler. `grep -ac memset_pattern16` reads 1/0 correctly with GNU grep, and the macOS runner's grep is BSD, where what `-a` promises about a binary file differs. The name being present in an object's string table made a byte match look like it needed no tool; it needed agreement about binary input instead. `llvm-nm -u` is in the payload beside the clang already being used, costs the same lookup, and asks the question the step is actually asking. Measured unchanged on Linux: 1 / 1 / 0, engine 0, and the control against the previous binary still fails at the link. --- .github/workflows/openkal-cross.yml | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/.github/workflows/openkal-cross.yml b/.github/workflows/openkal-cross.yml index 7ccd6a63..4dec8d29 100644 --- a/.github/workflows/openkal-cross.yml +++ b/.github/workflows/openkal-cross.yml @@ -311,11 +311,16 @@ jobs: BIN="${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs/xim-x-llvm/22.1.8/bin" # Not `ls ... | head -1`: with `pipefail` the absent candidate's # exit status ends the step before the guard below is reached. - CLANG="" - for c in "$BIN/clang" "$BIN/clang.exe"; do - if [ -x "$c" ]; then CLANG="$c"; break; fi - done - [ -n "$CLANG" ] || { echo "::error::no clang under $BIN"; exit 1; } + pick() { # $1..$n = candidate paths; prints the first executable one + for c in "$@"; do + if [ -x "$c" ]; then printf '%s' "$c"; return 0; fi + done + return 1 + } + CLANG="$(pick "$BIN/clang" "$BIN/clang.exe")" \ + || { echo "::error::no clang under $BIN"; exit 1; } + NM="$(pick "$BIN/llvm-nm" "$BIN/llvm-nm.exe")" \ + || { echo "::error::no llvm-nm under $BIN"; exit 1; } # Inside the clone, so the dependency is named by a relative path. # `$RUNNER_TEMP` is a backslash path on the Windows host and a TOML @@ -353,9 +358,16 @@ jobs: default = "llvm@22.1.8" PROJECT - # The symbol name is in the object's string table, so a byte match - # reads it without naming an `llvm-nm` whose path differs per host. - refs() { grep -ac memset_pattern16 "$1" 2>/dev/null || true; } + # THE READER ASKS FOR UNDEFINED SYMBOLS, NOT FOR BYTES. + # + # This was `grep -ac memset_pattern16`, on the reasoning that the name + # is in the object's string table and a byte match needs no tool. It + # reads 1/0 correctly with GNU grep and read 0 for all three legs on + # the macOS host, where grep is BSD: the object is binary, and what + # `-a` promises about that differs between the two. Leg 1 is what + # reported it. `llvm-nm` is in the payload beside the clang already + # being used, and answers the question this step is actually asking. + refs() { "$NM" -u "$1" 2>/dev/null | grep -c memset_pattern16 || true; } T=--target=arm64-apple-macos14.0 "$CLANG" $T -O2 -c "$W/src/main.cpp" -o "$W/bare.o" From e5dc170245e100e702f9205f98f5cea79504e81b Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 07:46:38 +0800 Subject: [PATCH 08/13] the acceptance record gains the two criteria this round's closing found C5 is the builtins token, whose first leg went red on macOS and reported that the two behind it were measuring nothing. C6 is what `builds` meant on a target with no runner: the member's dependencies, not the member. --- ...kal-ecosystem-completion-and-acceptance.md | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) 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 896ba13f..4b8a030e 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 @@ -915,7 +915,9 @@ openkal-musl 0.18.0,而 0.18.0 当时还没进索引,消费者自己的 CI | C2' | curl | 配方缺陷,不是不可移植 | **已定位未修**:两个目标两个不同真因,见 `docs/openkal-compat.md` | | C3 | `arc4random_buf` | 「应当有的符号」CI 断言 | 已通过 | | C4 | `aarch64-macos --profile release` | 本机实测 | **更正**:不是「不复现」,是被某个 runtime 版本修掉了——旧钉上仍在,见下 | -| I1 | 写死的节点集合 | conformance 逐条断言 | 未实现(第五批) | +| C5 | `builtins = "iso"` 发的 token | 目标文件里零引用,**且不带 flag 时必须有引用** | **已通过**:`openkal-cross.yml` 三条腿跑在三台宿主,1/1/0 + engine 0 | +| C6 | 跑不了的目标上 `builds` 的含义 | 编的是成员自己的测试,不是它的依赖 | **已通过**:`mcpp test --no-run`(e2e 745 四条腿)+ `compat.py` 的 `command_for` 三条 selftest | +| I1 | 写死的节点集合 | conformance 逐条断言 | 未实现(第五批) | | I2 | `presents = "windows"` 的 C 库 | 引擎改动数为 0 | 未实现(第五批) | | **A1** | 三目标 `mcpp build` | 全绿 | **待跑**(须用已发布钉,见 §8 第二条) | | **A2** | 三目标 `mcpp test` + 真跑 | 全绿 | 待跑 | @@ -923,6 +925,29 @@ openkal-musl 0.18.0,而 0.18.0 当时还没进索引,消费者自己的 CI --- +### 7.1 本轮收尾时新发现的两条,以及它们各自的判据 + +**C5 —— `builtins = "iso"` 发的那个 token 是静默空操作。** `-fno-builtin-memset_pattern16` +从这套机制第一版起就在发,而 `-fno-builtin-` 按 clang 的 builtin 表校验, +`memset_pattern16` 是 LLVM TargetLibraryInfo 的 libfunc、不在那张表里。A/B:不带 flag 1 次 +引用,带这个 flag 仍是 1 次,`-fno-builtin` 是 0。**它能活下来是因为它没有判据**——`cenv` +用 `-dM` dump 校验自己发的 token,而代码生成阶段的性质在 dump 里不可见。 + +判据写完之后,**第一条腿就在 macOS 宿主上红了**:三个读数全是 0。原因不在编译器在读数器—— +`grep -ac` 在 GNU grep 上读 1/0 正确,macOS 的 BSD grep 对二进制输入的 `-a` 语义不同。 +换成载荷里的 `llvm-nm -u` 之后三台宿主一致。**这正是第一条腿存在的理由:它报的是 +「后面两条什么都没测」。** + +**C6 —— 跑不了的目标上,`builds` 说的是它的依赖。** 本套件每个成员的源码都只在 `tests/` 下, +而没有 runner 的目标用 `mcpp build` 测量,`mcpp build` 构建的是**包**——对这种形状的包 +它一行都不编译。实测 `archive` 的 aarch64-macos:1990 个目标文件,没有一个来自 +`tests/compression.cpp` 或 `tests/versions.cpp`,退出 0,记成 `builds`。 + +CI 里这个读数从未错过,因为两个钉住的目标都能跑(linux 原生、windows 经 Wine)。**它在 +考虑第三个目标的那一刻才错**,而那正是本轮要做的事。引擎侧补 `mcpp test --no-run`, +测量侧把命令选择提到 `command_for` 并给它三条 selftest ——其中要紧的那条断言的是 +`--no-run` 而不是 `build`。 + ## 8. 本方案自身的失败模式 写下来,因为它们在本轮各出现过一次。 From fa99bd917232a0c7d82cfb288b5fe694174aaf24 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 07:51:43 +0800 Subject: [PATCH 09/13] a count that stops at the member level leaves the same reading one level up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `--workspace --no-run` reported "ok. N member(s); 0 passed; 0 failed", which is what a workspace with no tests at all reports — the false reading `totalNotRun` was added to the same line to prevent, one level down. The fan-out now carries `built` through to the workspace total and to `workspace_summary` as `tests_built`, kept apart from `tests_not_run` for the reason the per-member fields are: one is a question left open, the other is a question that was not asked. Leg E of 745 covers it: two members, one test each, `--workspace --no-run`, exit 0 and "2 built, not run" in the total. --- docs/50-machine-output.md | 7 +++- docs/zh/50-machine-output.md | 8 ++-- src/cli/cmd_build.cppm | 10 ++++- ...745_no_run_builds_the_tests_and_says_so.sh | 42 ++++++++++++++++++- 4 files changed, 60 insertions(+), 7 deletions(-) diff --git a/docs/50-machine-output.md b/docs/50-machine-output.md index 22237387..48a3e61a 100644 --- a/docs/50-machine-output.md +++ b/docs/50-machine-output.md @@ -560,10 +560,13 @@ test ran and passed. A client that read the exit code alone as pass/fail must handle 2, and a client that inferred "everything passed" from `failed == 0` must also read `not_run`. -`workspace_summary` adds `tests_not_run` (the sum over members) and +`workspace_summary` adds `tests_not_run` (the sum over members), +`tests_built` (the sum of tests built under `--no-run`) and `unrunnable_members` (members all of whose tests were `not_run`), alongside the existing `not_run` list, which continues to name members the -`--workspace-timeout` stopped before they started. +`--workspace-timeout` stopped before they started. `tests_built` is separate +from `tests_not_run` for the reason the per-member fields are: one is a +question left open, the other is a question that was not asked. ### The stage manifest diff --git a/docs/zh/50-machine-output.md b/docs/zh/50-machine-output.md index 3e18c6cf..dc42ac4f 100644 --- a/docs/zh/50-machine-output.md +++ b/docs/zh/50-machine-output.md @@ -485,9 +485,11 @@ mcpp test [pattern] [--workspace] --message-format json 含义不变 —— 有测试运行并失败;0 表示每个测试都运行并通过。只读退出码判 pass/fail 的 客户端必须处理 2;由 `failed == 0` 推断「全部通过」的客户端还必须读 `not_run`。 -`workspace_summary` 增加 `tests_not_run`(各成员之和)与 `unrunnable_members`(所有 -测试都 `not_run` 的成员),与既有的 `not_run` 列表并列;后者仍然指 -`--workspace-timeout` 到达时尚未开始的成员。 +`workspace_summary` 增加 `tests_not_run`(各成员之和)、`tests_built`(`--no-run` 下 +构建的测试数之和)与 `unrunnable_members`(所有测试都 `not_run` 的成员),与既有的 +`not_run` 列表并列;后者仍然指 `--workspace-timeout` 到达时尚未开始的成员。 +`tests_built` 与 `tests_not_run` 分开,理由和逐成员的那两个字段一样:一个是被悬着的 +问题,另一个是压根没问的问题。 ### 暂存清单 diff --git a/src/cli/cmd_build.cppm b/src/cli/cmd_build.cppm index dc0a814d..c9589c05 100644 --- a/src/cli/cmd_build.cppm +++ b/src/cli/cmd_build.cppm @@ -595,6 +595,10 @@ export int cmd_test(const mcpplibs::cmdline::ParsedArgs& parsed, std::vector unrunnable; // tests built, none executed (#544) std::vector> memberTimes; int totalPassed = 0, totalFailed = 0, totalNotRun = 0; + // Carried for the same reason `totalNotRun` is: without it a + // `--no-run` workspace reports "0 passed; 0 failed", which is + // what a workspace with no tests at all reports. + int totalBuilt = 0; auto tWs = std::chrono::steady_clock::now(); auto ws_ms = [&tWs] { return std::chrono::duration_cast( @@ -622,6 +626,7 @@ export int cmd_test(const mcpplibs::cmdline::ParsedArgs& parsed, totalPassed += sum.passed; totalFailed += sum.failed; totalNotRun += sum.notRun; + totalBuilt += sum.built; memberTimes.emplace_back(mp, sum.elapsedMs); auto secs = static_cast(sum.elapsedMs) / 1000.0; if (r == 2 && sum.failed == 0 && sum.notRun > 0) { @@ -677,10 +682,11 @@ export int cmd_test(const mcpplibs::cmdline::ParsedArgs& parsed, // `unrunnable_members` are #544's — tests that were built and not // executed, and the members all of whose tests were. std::println("{{\"workspace_summary\":{{\"members\":{},\"passed\":{},\"failed\":{}," - "\"tests_not_run\":{}," + "\"tests_not_run\":{},\"tests_built\":{}," "\"failed_members\":[{}],\"unrunnable_members\":[{}]," "\"not_run\":[{}],\"elapsed_ms\":{}}}}}", members->size(), totalPassed, totalFailed, totalNotRun, + totalBuilt, join(failed), join(unrunnable), join(notRun), wsElapsed); std::fflush(stdout); return rc; @@ -709,6 +715,8 @@ export int cmd_test(const mcpplibs::cmdline::ParsedArgs& parsed, // built and not executed must not read as a passing member. std::string notRunCounts = totalNotRun ? std::format("; {} not run", totalNotRun) : std::string{}; + if (totalBuilt) + notRunCounts += std::format("; {} built, not run", totalBuilt); if (failed.empty() && notRun.empty() && unrunnable.empty()) mcpp::ui::status("workspace result", std::format("ok. {} member(s); {} passed; 0 failed{}; finished in {:.2f}s", diff --git a/tests/e2e/745_no_run_builds_the_tests_and_says_so.sh b/tests/e2e/745_no_run_builds_the_tests_and_says_so.sh index 7a8f3dfb..5a1866e9 100755 --- a/tests/e2e/745_no_run_builds_the_tests_and_says_so.sh +++ b/tests/e2e/745_no_run_builds_the_tests_and_says_so.sh @@ -22,7 +22,7 @@ # same situation -- tests built, nothing run -- on every host, for the native # target, with nothing installed. # -# Four legs: +# Five legs: # A without `--no-run`: exit 2, and the tests are reported `not run`. # B with `--no-run`: exit 0, and the count is reported as built. # C a test that does not COMPILE is still a failure under `--no-run`. @@ -31,6 +31,8 @@ # D `--no-run` with `--no-runner` is refused. The two names differ by one # character and mean opposite things, and neither is a weaker form of the # other, so there is no reading of the pair to prefer. +# E `--workspace` totals the built count. A count that stops at the member +# level leaves the same false reading one level up. set -e MCPP="${MCPP:-mcpp}" @@ -141,4 +143,42 @@ case "$out_d" in *) echo "FAIL: D did not explain why the pair is refused"; printf '%s\n' "$out_d" | tail -3; exit 1 ;; esac +# --- E: the workspace total says it too ---------------------------------- +# A count that stops at the member level is the same defect one level up: a +# workspace summary reading "0 passed; 0 failed" is what a workspace with no +# tests reports. +rm -f tests/gamma.cpp +mkdir -p members/one/tests members/two/tests +cat > mcpp.toml < "members/$m/mcpp.toml" < "members/$m/tests/t.cpp" <<'EOF' +int main() { return 0; } +EOF +done +set +e +out_e="$("$MCPP" test --workspace --target "$host" --no-run 2>&1)" +rc_e=$? +set -e +printf '%s +' "$out_e" | tail -3 +if [ "$rc_e" != 0 ]; then + echo "FAIL: E expected exit 0 from a --workspace --no-run run, got $rc_e" + exit 1 +fi +case "$out_e" in + *"2 built, not run"*) ;; + *) echo "FAIL: E the workspace total did not report the built tests"; exit 1 ;; +esac + echo "PASS: --no-run builds the tests and says so, and says nothing else" From 2e288b5635882e766755a907e3f1abb9d694dfa7 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 07:53:52 +0800 Subject: [PATCH 10/13] 2026.9.21.3 Both version sites in one commit, which `01_help_and_version.sh` cross-checks. The bootstrap pin in .xlings.json stays at 2026.9.21.1: it must name a version that is published, mirrored and in the index, and this one is none of those yet. --- CHANGELOG.md | 2 +- mcpp.toml | 2 +- modules/versioning/src/version.cppm | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac0e0e23..1f7bb482 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ > 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。 > 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 -## [Unreleased] +## [2026.9.21.3] - 2026-09-21 ### `mcpp test --no-run`:为一个跑不了的目标构建测试,并把这当成答案 diff --git a/mcpp.toml b/mcpp.toml index 1fa98a77..860ea730 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.9.21.2" +version = "2026.9.21.3" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index aea504b0..3e42d572 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.2"; +inline constexpr std::string_view MCPP_VERSION = "2026.9.21.3"; } // namespace mcpp From 27864635a6decd2c81fa6896f2cb1df239e6dc75 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 07:58:56 +0800 Subject: [PATCH 11/13] the Apple list has one definition and three spellings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `os == "macos" || os == "ios"` is `Triple::is_apple()`. This module takes `os` as a string rather than a `Triple` deliberately — it is pure, and importing the toolchain model to reach one predicate would couple what that choice decoupled — so the spelling stays and the comment names where the canonical list lives and which other sites carry a copy. --- src/toolchain/cenv.cppm | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/toolchain/cenv.cppm b/src/toolchain/cenv.cppm index ed20f2bd..f3552a92 100644 --- a/src/toolchain/cenv.cppm +++ b/src/toolchain/cenv.cppm @@ -600,6 +600,14 @@ inline std::expected realise( // this token lives in `openkal-cross.yml`, which compiles an // idiom-triggering unit for `aarch64-macos` over the openkal stack and // asserts the symbol is absent from the object. + // + // `os == "macos" || os == "ios"` IS `Triple::is_apple()`, SPELLED OUT. + // This module takes `os` as a string rather than a `Triple` on purpose --- + // it is pure, and importing `mcpp.toolchain-model` to reach one predicate + // would couple what that choice decoupled. The canonical list is + // `modules/toolchain-model/src/triple.cppm`; `ninja_backend.cppm` spells + // it out twice for the same reason. A new Apple OS in the vocabulary has + // to visit all three, and this comment is the grep target that says so. if (decl.builtins == mcpp::targetside::CAbiBuiltins::Iso) { if (!freestanding && (os == "macos" || os == "ios")) r.builtinsTokens.push_back("-fno-builtin"); From 94a6b51ce02f89e2a415fcffe6f01c12afaf177f Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 08:01:14 +0800 Subject: [PATCH 12/13] the sandbox verification gains the two sections this release adds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit G reads an object file, because a code-generation property is in no `-dM` dump — which is why the token it asserts was a silent no-op for the whole of its first life. On 2026.9.21.2 the aarch64-macos link fails outright, so a failed build there is the negative reading rather than an absent one. H asserts `--no-run`; before this release the flag does not exist. Dry-run on the host against both engines: both sections pass on this one and fail on the previous, which is the property a CHANGE section has to have. --- ...2026-09-21-macros-and-withdrawal-verify.sh | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/.agents/docs/2026-09-21-macros-and-withdrawal-verify.sh b/.agents/docs/2026-09-21-macros-and-withdrawal-verify.sh index 0e27ca78..0e430e04 100755 --- a/.agents/docs/2026-09-21-macros-and-withdrawal-verify.sh +++ b/.agents/docs/2026-09-21-macros-and-withdrawal-verify.sh @@ -297,6 +297,99 @@ else skip "openkal-llvm-runtime 0.14.0 did not resolve from the index" fi +# ── CHANGE. `builtins = "iso"` withdraws the Apple pattern fill ───────────── +section "G. builtins = \"iso\" emits -fno-builtin (CHANGE)" +# A CODE-GENERATION PROPERTY, so the reading is an object file rather than a +# `-dM` dump — which is exactly why the token this asserts was a silent no-op +# for the whole of its first life. `2026.9.21.2` and earlier emit +# `-fno-builtin-memset_pattern16`; clang matches `-fno-builtin-` against +# its builtin table, `memset_pattern16` is an LLVM TargetLibraryInfo libfunc +# and not in it, so the flag is accepted in silence and the call survives. On +# those versions the aarch64-macos link fails with +# +# ld64.lld: error: undefined symbol: memset_pattern16 +# +# so a failed build here IS the negative reading rather than an absent one. +g="$root/g"; rm -rf "$g"; mkdir -p "$g/src" +cat > "$g/src/main.cpp" <<'EOF' +extern "C" void fill(int* a, long n) { + for (long i = 0; i < n; ++i) a[i] = 0x01020304; +} +int main() { static int buf[64]; fill(buf, 64); return buf[0] == 0x01020304 ? 0 : 1; } +EOF +# `-O2` per package rather than --release: the idiom pass does not run at the +# dev profile's -O0, and a release build would compile the runtime a second +# time in a second profile for no reading. +cat > "$g/mcpp.toml" <<'EOF' +[package] +name = "builtins-probe" +version = "0.1.0" + +[build] +cxxflags = ["-O2"] + +[dependencies] +openkal-llvm-runtime = "0.15.0" + +[toolchain] +default = "llvm@22.1.8" +EOF +gnm=$(ls "$HOME"/.xlings/data/xpkgs/xim-x-llvm/22.1.8/bin/llvm-nm 2>/dev/null | head -1) +if [ -z "$gnm" ]; then + skip "G: no llvm-nm in the payload to read the object with" +elif (cd "$g" && "$STORE" build --target aarch64-macos >/dev/null 2>&1); then + gobj=$(find "$g/target" -name 'main.o' 2>/dev/null | head -1) + if [ -z "$gobj" ]; then + fail "G: the build reported success and produced no object" + elif [ "$("$gnm" -u "$gobj" 2>/dev/null | grep -c memset_pattern16)" = 0 ]; then + ok "builtins = \"iso\" leaves no memset_pattern16 in the object" + else + fail "builtins = \"iso\" did not withdraw memset_pattern16" + fi +else + fail "G: the aarch64-macos build did not complete (the old token's signature)" +fi + +# ── CHANGE. `mcpp test --no-run` ──────────────────────────────────────────── +section "H. mcpp test --no-run builds the tests and says so (CHANGE)" +# Before this release the flag does not exist and the command exits non-zero +# with "unknown option: --no-run". The runner named here is a program that +# does not exist, which produces "tests built, nothing run" on every host for +# the native target with nothing installed. +h="$root/h"; rm -rf "$h"; mkdir -p "$h/tests" +printf 'int main() { return 0; }\n' > "$h/tests/alpha.cpp" +printf 'int main() { return 0; }\n' > "$h/tests/beta.cpp" +printf '[package]\nname = "norun"\nversion = "0.1.0"\n' > "$h/mcpp.toml" +hhost=$("$STORE" --print-target 2>/dev/null || true) +if [ -z "$hhost" ]; then + (cd "$h" && "$STORE" build >/dev/null 2>&1) || true + hhost=$(ls "$h/target" 2>/dev/null | grep -v '^\.' | head -1) +fi +if [ -z "$hhost" ]; then + skip "H: could not determine the host triple" +else + cat > "$h/mcpp.toml" <&1) ) + hrc=$? + case "$hout" in + *"2 built, not run"*) + if [ "$hrc" = 0 ]; then + ok "--no-run builds the tests and exits 0" + else + fail "--no-run reported the built tests and exited $hrc" + fi ;; + *) fail "--no-run did not report two tests as built" ;; + esac +fi + + printf '\n-- summary --\nfails=%d\nnot run:%s\n' "$fails" "${skipped:- (none)}" [ "$fails" -eq 0 ] From fb702cbb77da94a1e491c3f0e2197c9ed1496a5a Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 08:06:32 +0800 Subject: [PATCH 13/13] the next batch's first item, with a number attached aarch64-macos was measurable for the first time once a target with no runner compiled the member's own tests: 20 of 30 build, 10 do not, and nine of the ten are `#ifdef __APPLE__` reaching for the Apple C environment on a target where `__APPLE__` is true and libSystem is not there. It is the Windows problem mirrored, minus the lever: presenting POSIX on Windows is realised as a cygwin triple and `_WIN32` goes away, while on macOS the realisation adds `-D__unix__` and leaves `__APPLE__` standing because it is correct. Nothing in the identity a source file sees there answers which C library is underneath. --- ...kal-ecosystem-completion-and-acceptance.md | 38 +++++++++++++++++++ 1 file changed, 38 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 4b8a030e..b5ee2bf4 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 @@ -948,6 +948,44 @@ CI 里这个读数从未错过,因为两个钉住的目标都能跑(linux 原生 测量侧把命令选择提到 `command_for` 并给它三条 selftest ——其中要紧的那条断言的是 `--no-run` 而不是 `build`。 +### 7.2 下一批最该做的一条:`__APPLE__` 回答的是平台,不是 C 环境 + +`mcpp test --no-run` 让「宿主跑不了的目标」第一次可以被真正测量,于是 aarch64-macos +第一次被测了(2026-09-21,mcpp 2026.9.21.3 + runtime 0.15.0,30 个成员): +**20 个构建通过,10 个不通过。** + +十个里有九个是同一个原因: + +| 诊断 | 成员 | +| --- | --- | +| 找不到 `TargetConditionals.h` | catch2、curl、mimalloc、re2、sqlite3 | +| 经 Apple 的 `dnsinfo.h` 找不到 `sys/cdefs.h` | c-ares | +| 找不到 `sys/event.h`(kqueue) | cmp-module | +| 找不到 `xlocale.h` | fmtlib.fmt | +| `pthread_threadid_np` 未声明 | spdlog | +| `library not found for -lm` | brotli | + +每一条都走在 `#ifdef __APPLE__` 下面,而在这个目标上 **`__APPLE__` 是对的**——它确实是 +Apple 平台:Mach-O、arm64、macOS。**它没有说的是底下是哪个 C 库**,而上游用它同时表达 +这两件事,因为在真正的 macOS 上两者重合。 + +**这是 Windows 那个问题的 macOS 镜像,差别只有一处:那边引擎有杠杆。** 一个在 Windows 上 +presents POSIX 的 C 库被实现成 `--target=…-pc-cygwin`,`_WIN32` 因此消失,`#ifdef _WIN32` +不再选中 Win32 分支。macOS 上的实现只加了 `-D__unix__`,`__APPLE__` 与 `__MACH__` 原样留着 +——因为它们是真的。实测,源码在那里看到的整个身份是 + +``` +__APPLE__ __MACH__ __MCPP_TARGET_MACOS__ __OPENKAL__ __unix__ +``` + +**没有一个回答「哪个 C 库」**。musl 按其自身设计不提供任何标识宏,所以也没有一个可移植的 +问题可问。 + +⭐ 这一条的形状与 §2.1 的 E2 一样:**阻塞是一个设计决定,不是工作量**。要么引擎给出一个 +命名已解析 c-abi 的宏(那是往宏契约里加成员,`a-new-key-inside-a-known-table` 那条教训 +直接适用),要么由生态数据回答(每个配方按 `__has_include` 逐条问)。十个红格子的修法是 +**一个**问题,不是十个,而这条记录的作用是让那个问题带着数字被提出。 + ## 8. 本方案自身的失败模式 写下来,因为它们在本轮各出现过一次。