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 ] 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..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 @@ -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(用户要求:不分开,免得发布周期太长)。 @@ -441,6 +487,84 @@ 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 +都可能重复这次静默——**一个只在「它起作用」这一假设下成立的机制,需要一条断言它起作用的 +判据。** + +**这个符号属于哪一层,查过了,而答案把问题从「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-*) #### C4' — 「不复现」是在另一个版本上量的(2026-09-21 更正) @@ -646,6 +770,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. 执行顺序 @@ -756,7 +905,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 | @@ -766,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` + 真跑 | 全绿 | 待跑 | @@ -774,6 +925,67 @@ 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`。 + +### 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. 本方案自身的失败模式 写下来,因为它们在本轮各出现过一次。 diff --git a/.github/workflows/openkal-cross.yml b/.github/workflows/openkal-cross.yml index b2bc67d3..4dec8d29 100644 --- a/.github/workflows/openkal-cross.yml +++ b/.github/workflows/openkal-cross.yml @@ -264,6 +264,129 @@ 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. + 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 + # 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 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" + "$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 e3de8ab3..1f7bb482 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,113 @@ > 本文件追踪 `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`:为一个跑不了的目标构建测试,并把这当成答案 + +一个本机既不能执行、也没有 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 目标上 +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` +都写着「借来的 `__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/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/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..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 @@ -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/50-machine-output.md b/docs/50-machine-output.md index d27aaec2..48a3e61a 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 @@ -551,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/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/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/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/docs/zh/50-machine-output.md b/docs/zh/50-machine-output.md index 41711fb5..dc42ac4f 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`。这是关于整次调用的 @@ -479,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/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 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..c9589c05 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. @@ -585,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( @@ -612,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) { @@ -630,9 +645,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)); } } @@ -661,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; @@ -693,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/src/toolchain/cenv.cppm b/src/toolchain/cenv.cppm index 7a279389..f3552a92 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__`. @@ -534,9 +558,59 @@ 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. + // + // `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-memset_pattern16"); + r.builtinsTokens.push_back("-fno-builtin"); } return r; 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 ────────────────────────────────────────────────────────────── // 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..5a1866e9 --- /dev/null +++ b/tests/e2e/745_no_run_builds_the_tests_and_says_so.sh @@ -0,0 +1,184 @@ +#!/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. +# +# 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`. +# 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. +# 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}" +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 + +# --- 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" 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) ──────────