From 72b7c5d1b355d5313633e112d00069d222ccc17f Mon Sep 17 00:00:00 2001 From: speak-agent Date: Sat, 5 Sep 2026 21:49:03 +0800 Subject: [PATCH 1/6] chore: remove the remaining decorative markers from comments and documentation The sweep that accompanied 2026.9.5.3 covered docs, README.md, CHANGELOG, the engine sources, tests, examples and the workflow files. It did not cover bench/, tools/, scripts/, mcpp.toml or README.zh-CN.md, which still carried the warning and star markers in comments and prose. Program output keeps its markers: a line a user reads on a terminal is neither documentation nor a comment. README.zh-CN.md also gains the Cortex-M row its English counterpart has, and its status column now reads the same words that one does. --- ...heterogeneous-build-ecosystem-design-v2.md | 18 +++- .../docs/2026-09-05-heterogeneous-verify.sh | 50 ++++++++++- README.zh-CN.md | 25 +++--- bench/README.md | 86 +++++++++---------- bench/README.zh-CN.md | 22 ++--- bench/SPEC.md | 16 ++-- bench/projects/xlings/README.md | 12 +-- bench/results/README.md | 2 +- .../pinned-workloads-20260813/report.md | 4 +- .../standard-20260814-linux-x86_64/README.md | 2 +- bench/run-standard.sh | 12 +-- bench/src/engines/bazel.cpp | 2 +- bench/src/engines/engine.cpp | 2 +- bench/src/engines/xmake.cpp | 4 +- bench/src/engines/xmake.cppm | 2 +- bench/src/fixture/buildfiles.cpp | 2 +- bench/src/journal.cppm | 2 +- bench/src/main.cpp | 4 +- bench/src/platform.cpp | 2 +- bench/src/platform.cppm | 2 +- bench/src/platform/windows.cppm | 4 +- bench/src/protocol.cppm | 4 +- bench/src/runner.cpp | 6 +- bench/src/runner.cppm | 2 +- bench/src/toolchain.cpp | 8 +- bench/src/toolchain.cppm | 4 +- bench/tests/harness.sh | 6 +- bench/tools/report.py | 33 ++++--- mcpp.toml | 2 +- scripts/aur/reconcile_mcpp_bin.py | 2 +- tools/dev-mcpp-path.sh | 2 +- tools/lint-ci-assertions.sh | 12 +-- 32 files changed, 205 insertions(+), 151 deletions(-) diff --git a/.agents/docs/2026-09-05-heterogeneous-build-ecosystem-design-v2.md b/.agents/docs/2026-09-05-heterogeneous-build-ecosystem-design-v2.md index 2525d8ad..e1db7c4a 100644 --- a/.agents/docs/2026-09-05-heterogeneous-build-ecosystem-design-v2.md +++ b/.agents/docs/2026-09-05-heterogeneous-build-ecosystem-design-v2.md @@ -332,6 +332,8 @@ Status is one of `done`, `open`, `deferred (reason)`. | I3 | `compat.opencl-headers`, `compat.opencl` verified with a probe (`tests/examples/opencl`, a workspace member); `compat.opencl-runtime` 2026.09.05 farms the libraries the host manifests name, their closure and the vendor family, prefers payloads, records the surface; payload entries of `OCL_ICD_FILENAMES` are left to the payload | the probe enumerates the NVIDIA platform on this machine and zero platforms on a runner; the pocl platform once X2 is installed | X2 for the pocl half | done for the host half: `tests/examples/opencl` lists `NVIDIA CUDA / RTX 4080` here and zero platforms on the Linux runner; with `OCL_ICD_FILENAMES=/lib/libpocl.so` the same loader lists `Portable Computing Language` and `NVIDIA CUDA` in one process | | I4 | CI green; merge; index artifact published | `Publish Index Artifact` green on the merge commit | I1–I3 | done: PR #349 green (10 pass; the `full sweep` job skips by design), merged as 754d775, `Publish Index Artifact` green on it (run 33968256961) | +| I5 | `compat.vulkan-runtime` 2026.09.06: the payload set is declared as `xpm.linux.deps.runtime` rather than discovered in the store; `PAYLOAD_PACKAGES` maps each soname to the package declared for it and reports a declaration that did not take effect; both adapters refuse a payload built for another machine (ELF `e_machine`); the Vulkan adapter gains the aarch64 multiarch directories | a fresh environment substitutes the same set a developer machine does; the report contains no line saying a declaration did not take effect | I4, X5 | see section 6.4 | + #### xim-pkgindex (PR #762) | # | task | criterion | depends on | status | @@ -347,5 +349,17 @@ Status is one of `done`, `open`, `deferred (reason)`. | # | task | criterion | depends on | status | |---|---|---|---|---| -| V1 | Fresh sandbox (`xlings subos --sandbox --cmd`), CN mirror configured for both mcpp and xlings: install mcpp 2026.9.5.3, build examples 09 and 10 against `mcpp:plugins`, run 10 on the lavapipe payload, run the OpenCL probe on pocl | `12 24 36 48` from example 10 with no GPU; the pocl platform enumerated; `HOST-SURFACE.txt` empty in the sandbox | M6, P3, I4, X5 | open | -| V2 | The same on this host with the GPU: example 10 on the host ICD, example 09 on both routes | `12 24 36 48` in every case; `HOST-SURFACE.txt` lists proprietary userspace only | V1 | open | +| V1 | Fresh sandbox (`xlings subos --sandbox --cmd`), CN mirror configured for both mcpp and xlings: install mcpp 2026.9.5.3, build examples 09 and 10 against `mcpp:plugins`, run 10 on the lavapipe payload, run the OpenCL probe on pocl | `12 24 36 48` from example 10 with no GPU; the pocl platform enumerated; every declared payload substitution took effect (see 6.4 for why an empty host surface is not the criterion) | M6, P3, I4, X5, I5 | open | +| V2 | The same on this host with the GPU: example 10 on the host ICD, example 09 on both routes | `12 24 36 48` in every case; the host entries of `HOST-SURFACE.txt` are proprietary userspace, host Mesa drivers, and the recorded symbol-set and soname gaps only | V1 | open | + +### 6.4 What the first verification run measured, and the two criteria it retired + +The first sandbox run failed six assertions. One was a defect in the ecosystem, two were criteria that could not hold by construction, and three were the machine's disk filling up mid-run. They are separated here because only the first is a change to a package. + +**The defect: a substitution that the environment decided.** `compat.vulkan-runtime` 2026.09.05 re-points a farmed soname at an installed payload when the payload's symbol set covers the host copy's, and *installed* was left to chance -- the pass looked in the store and took what an earlier, unrelated install had put there. The same package therefore produced twenty payload substitutions on the developer machine and one in a fresh subos, and the report read `no installed payload provides this soname` for sonames this index publishes. A published package whose behaviour is that much better on the machine that wrote it is the shape recorded in `.agents/docs` as a development overlay verifying a world that does not exist. 2026.09.06 declares the set (row I5). Measured against a project-local index on this host: 24 substitutions against 20, seven host entries against eleven, and a Vulkan probe still enumerating two devices. + +**The first retired criterion: an empty host surface in a sandbox.** A subos shares the host's `/usr`, which the run confirmed: 5354 entries in `/usr/lib/x86_64-linux-gnu` inside the sandbox, `libnvidia-*` among them. The farm therefore sees the proprietary driver there exactly as it does on the host, and `HOST-SURFACE.txt empty in the sandbox` was unreachable by construction rather than a statement about the ecosystem. What a sandbox does measure is the substitution invariant: every soname the adapter declares a payload for is taken from that payload, and a declaration that did not take effect says so in the report. + +**The second retired criterion: the exit code of a mirror write.** Both tools print their configuration on stderr, so reading it through `2>/dev/null` returned an empty string and reported a mirror that was in fact set to CN. + +**Two properties of the sandbox that the run established.** A subos root has no `/run`, so the inherited `XDG_RUNTIME_DIR` names a directory that does not exist; Mesa falls back to it when `memfd` is unavailable and lavapipe fails with `Failed to create anonymous file for memory allocations` before it reports a device. And the registry's index snapshot is shared with whatever wrote it last: the run resolved against a snapshot four commits behind `main`, where `compat.opencl-headers` reported `download artifact missing` because the recipe was not in it. The verification script now redirects the first and refreshes the second before it measures anything. diff --git a/.agents/docs/2026-09-05-heterogeneous-verify.sh b/.agents/docs/2026-09-05-heterogeneous-verify.sh index 4ddb5331..568881ff 100644 --- a/.agents/docs/2026-09-05-heterogeneous-verify.sh +++ b/.agents/docs/2026-09-05-heterogeneous-verify.sh @@ -31,11 +31,33 @@ got=$("$STORE" --version 2>&1 | head -1) # value was written, so both tools are asked what they hold afterwards. "$STORE" self config --mirror "${MCPP_VERIFY_MIRROR:-CN}" >/dev/null 2>&1 || true "$XL" config --mirror "${MCPP_VERIFY_MIRROR:-CN}" >/dev/null 2>&1 || true -xm=$("$XL" config 2>/dev/null | grep -i 'mirror' | head -1 | awk '{print $NF}') +# Both tools print their configuration banner on stderr, so a `2>/dev/null` +# here reads an empty string and reports a mirror that is in fact set. +xm=$("$XL" config 2>&1 | sed 's/\x1b\[[0-9;]*m//g' | grep -i 'mirror' | head -1 | awk '{print $NF}') [ "$xm" = "${MCPP_VERIFY_MIRROR:-CN}" ] && ok "xlings mirror is $xm" || fail "xlings mirror is '$xm', not ${MCPP_VERIFY_MIRROR:-CN}" -mm=$("$STORE" self config 2>/dev/null | grep -i 'mirror' | head -1 | awk '{print $NF}') +mm=$("$STORE" self config 2>&1 | sed 's/\x1b\[[0-9;]*m//g' | grep -i 'mirror' | head -1 | awk '{print $NF}') [ -n "$mm" ] && ok "mcpp mirror is $mm" || printf 'note: mcpp self config does not print its mirror (%s)\n' "$("$STORE" self config 2>&1 | head -1)" +# THE REGISTRY IS SHARED WITH WHATEVER WROTE IT LAST, AND ITS INDEX SNAPSHOT +# IS PART OF THAT. A fresh subos inherits the snapshot the registry holds, and +# the refresh is resolution-driven with a freshness window, so a package merged +# minutes ago resolves as `download artifact missing` until the snapshot moves. +# Measured 2026-09-05: section E failed against a snapshot four commits behind. +"$STORE" index update >/dev/null 2>&1 || true +snap=$(cat "$HOME/.mcpp/registry/data/mcpplibs/.xlings-index-version" 2>/dev/null || echo "unknown") +ok "index snapshot $snap" + +# Mesa creates its allocations through an anonymous file and falls back to +# XDG_RUNTIME_DIR when memfd is unavailable. A subos root has no /run, so the +# inherited value names a directory that does not exist and lavapipe fails with +# `Failed to create anonymous file for memory allocations` before it reports a +# device. A writable directory inside the sandbox is the whole fix. +if [ ! -d "${XDG_RUNTIME_DIR:-/nonexistent}" ]; then + XDG_RUNTIME_DIR="$HOME/.cache/xdg-runtime"; mkdir -p "$XDG_RUNTIME_DIR" + chmod 700 "$XDG_RUNTIME_DIR"; export XDG_RUNTIME_DIR + ok "XDG_RUNTIME_DIR redirected to $XDG_RUNTIME_DIR" +fi + # -- B. the plugin collection resolves from the index ------------------------- # # A consumer that names mcpp:plugins with one feature and imports the member @@ -103,6 +125,13 @@ if [ -n "$SRC" ] && [ -d "$SRC/examples/10-vulkan-compute/app" ]; then icd=$(ls "$HOME"/.mcpp/registry/data/xpkgs/xim-x-mesa-lavapipe/*/share/vulkan/icd.d/*.json \ "$ex10"/.mcpp/.xlings/data/xpkgs/xim-x-mesa-lavapipe/*/share/vulkan/icd.d/*.json \ "$HOME"/.xlings/data/xpkgs/xim-x-mesa-lavapipe/*/share/vulkan/icd.d/*.json 2>/dev/null | head -1) + if [ -z "$icd" ]; then + # The example declares the payload (mcpp#569); an older checkout does + # not, in which case the payload is installed here so the criterion is + # still the driver and not the manifest's history. + "$XL" install mesa-lavapipe -y >/dev/null 2>&1 || true + icd=$(ls "$HOME"/.xlings/data/xpkgs/xim-x-mesa-lavapipe/*/share/vulkan/icd.d/*.json 2>/dev/null | head -1) + fi [ -n "$icd" ] && ok "lavapipe manifest at $icd" || fail "no lavapipe ICD manifest in any store" out=$(cd "$ex10" && VK_DRIVER_FILES="$icd" "$STORE" run 2>&1) printf '%s\n' "$out" | grep -q '12 24 36 48' && ok "example 10 answered 12 24 36 48 on the payload driver" \ @@ -115,9 +144,22 @@ if [ -n "$SRC" ] && [ -d "$SRC/examples/10-vulkan-compute/app" ]; then "$ex10"/.mcpp/.xlings/data/xpkgs/compat-x-vulkan-runtime/*/mcpp_generated/vulkan_runtime/HOST-SURFACE.txt 2>/dev/null | head -1) if [ -n "$hs" ]; then printf -- '--- %s\n' "$hs"; sed -n '/^## farmed/,$p' "$hs" | head -60 + # WHAT A SANDBOX CAN AND CANNOT ASSERT. A subos shares the host's + # /usr, so the farm sees the host's proprietary driver there and a + # count of zero host entries is not reachable by construction -- the + # earlier form of this check asserted it and failed on a correct farm. + # What the sandbox does measure is the substitution invariant: every + # soname compat.vulkan-runtime declares a payload for is taken from + # that payload, and a declaration that did not take effect says so in + # the report. hostlines=$(grep -c -- '-- host;' "$hs" || true) - if [ -n "${MCPP_VERIFY_HOST:-}" ]; then ok "host surface recorded ($hostlines host entries, see above)"; else - [ "$hostlines" -eq 0 ] && ok "no host library is on the runtime path in the sandbox" || fail "$hostlines host entries in a sandbox"; fi + payloads=$(grep -c -- '-- payload;' "$hs" || true) + undeclared=$(grep -c 'did not take effect' "$hs" || true) + printf 'note: %s payload substitutions, %s host entries\n' "$payloads" "$hostlines" + [ "$undeclared" -eq 0 ] && ok "every declared payload took effect" \ + || fail "$undeclared declared payload(s) did not take effect" + [ "$payloads" -ge 20 ] && ok "$payloads farmed libraries come from payloads" \ + || fail "only $payloads payload substitutions; the declared set did not install" else fail "no HOST-SURFACE.txt written by compat.vulkan-runtime"; fi else printf 'skip: MCPP_VERIFY_SRC not set or has no examples/10-vulkan-compute\n' diff --git a/README.zh-CN.md b/README.zh-CN.md index de55f3f8..9ed95b31 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -345,7 +345,7 @@ cmake 4.4.2 / xmake 3.1.0 · `-` 表示未测,本表没有 · 29.83s;而在 mcpp 本已跳过级联的两行上没有收益。调度错误的表现是静默失效而非 报错,因此不以单台机器的证据变更默认值。 -📊 **[方法、锁定的版本、完整数据 → `bench/README.zh-CN.md`](bench/README.zh-CN.md)** · +**[方法、锁定的版本、完整数据 → `bench/README.zh-CN.md`](bench/README.zh-CN.md)** · [English](bench/README.md) ## 平台支持 @@ -360,17 +360,18 @@ mcpp 的身份模型是两条正交轴:**工具链** = `family@version`(family | Target | 约定工具链 | 状态 | |---|---|:---:| -| `x86_64-linux-gnu` | gcc(*Linux 默认*)或 llvm | ✅ | -| `x86_64-linux-musl` | gcc 16,全静态 | ✅ | -| `aarch64-linux-musl` | gcc 16,全静态——x86_64 交叉(qemu 实测)或原生 | ✅ | -| `x86_64-windows-gnu` | gcc 16 MinGW-w64——Windows 原生,Linux 交叉(wine 实测)(*无 Visual Studio 时的 Windows 默认*) | ✅ | -| `x86_64-windows-msvc` | `msvc@system`(探测 VS/BuildTools)或 llvm ¹(*有 Visual Studio 时的 Windows 默认*) | ✅ | -| `aarch64-macos` | llvm(*macOS 默认*) | ✅ | -| `riscv64-linux-musl` | — | 🔄 | -| `aarch64-linux-gnu` | — | 🔄 | -| `x86_64-macos` | — | 🔄 | - -✅ 已验证——CI 端到端构建**并真实执行**产物(含 qemu/wine)| 🔄 计划中 +| `x86_64-linux-gnu` | gcc(*Linux 默认*)或 llvm | 是 | +| `x86_64-linux-musl` | gcc 16,全静态 | 是 | +| `aarch64-linux-musl` | gcc 16,全静态——x86_64 交叉(qemu 实测)或原生 | 是 | +| `x86_64-windows-gnu` | gcc 16 MinGW-w64——Windows 原生,Linux 交叉(wine 实测)(*无 Visual Studio 时的 Windows 默认*) | 是 | +| `x86_64-windows-msvc` | `msvc@system`(探测 VS/BuildTools)或 llvm ¹(*有 Visual Studio 时的 Windows 默认*) | 是 | +| `aarch64-macos` | llvm(*macOS 默认*) | 是 | +| `thumbv7em-none-eabi` · `thumbv8m.base-none-eabi` · `thumbv8m.main-none-eabihf` | llvm 22——可构建可链接;未记录模拟器运行 | 计划中 | +| `riscv64-linux-musl` | — | 计划中 | +| `aarch64-linux-gnu` | — | 计划中 | +| `x86_64-macos` | — | 计划中 | + +是——CI 端到端构建**并真实执行**产物(含 qemu/wine)| 计划中——尚未验证 > Linux release 二进制为 x86_64 与 aarch64 的 musl 全静态构建 > (`x86_64-linux-musl` 与 `aarch64-linux-musl`)。 diff --git a/bench/README.md b/bench/README.md index e9e379b0..f816165d 100644 --- a/bench/README.md +++ b/bench/README.md @@ -69,7 +69,7 @@ run id : 4e58a816 (resuming, 73 unit(s) recorded) run id : a0840b10 (fresh) ``` -⚠️ **Resume is where a benchmark quietly splices two runs together**, so two +**Resume is where a benchmark quietly splices two runs together**, so two things are deliberate: * The fingerprint **excludes the mcpp binary**. Including it would restart from @@ -114,7 +114,7 @@ BENCH=$(ls -t bench/target/*/*/bin/mbench | head -1) --scenarios noop,touch-hub --hub src/platform/platform.cppm --runs 3 ``` -⚠️ **Always pass mcpp as a PATH, never as the bare name `mcpp`.** The bare name +**Always pass mcpp as a PATH, never as the bare name `mcpp`.** The bare name resolves through PATH to the xlings shim, which re-picks its version from the working directory — and for `--project` that directory is the measured tree, which carries its own pin. A whole run once reported `mcpp@2026.8.11.3` in every @@ -228,25 +228,25 @@ record and the reader cannot see. Four things this bought, each of which had already gone wrong: * **cmake 3.31.6** is what the GitHub runner images ship. It does not have the - CMake 4.0 experimental key for `import std`, so *every module cell failed to + CMake 4.0 experimental key for `import std`, so *every module cell failed to configure*. With 4.0.2 they pass. * **`command -v g++`** on those images is gcc 13.3.0. cmake cannot configure - C++23 modules with it and xmake crashes it with an internal compiler error — + C++23 modules with it and xmake crashes it with an internal compiler error — while mcpp quietly used its own registry's gcc 16.1 regardless. The table read - `48 failed / 6 ok` and was still called a comparison of build engines. The + `48 failed / 6 ok` and was still called a comparison of build engines. The suite now hands **every** engine the driver out of mcpp's own payload (`--compiler payload:gcc`), which is its fairness rule finally enforced rather than merely written down. * **The measured workloads moved.** xlings was `git clone --depth 1` of its default branch at run time, so the target changed with every upstream push — - `--hub src/xlings.cppm` had been naming a file that no longer existed for + `--hub src/xlings.cppm` had been naming a file that no longer existed for months, every xlings cell reported `skipped`, and every xlings job reported success. mcpp's own sources had the same defect in a form that is harder to see: `--project $GITHUB_WORKSPACE` made the checkout the workload, so every commit on a branch silently changed the thing being measured. **The engine under test is the binary and is supposed to move; the workload is not.** - All three are git submodules now, and the guard checks that each `hub` and - `body` exists in the pinned tree. + All three are git submodules now, and the guard checks that each `hub` and + `body` exists in the pinned tree. * **Only one mcpp was measured.** A report that says how fast this branch is, without saying whether it got faster, is not what a benchmark on a pull request is for. @@ -275,7 +275,7 @@ Four things this bought, each of which had already gone wrong: ### The headline numbers, and where they come from -> ⚠️ **`bmi_schedule` has a known correctness bug — do not quote these numbers.** +> **`bmi_schedule` has a known correctness bug — do not quote these numbers.** > On the generated fixture's `modules` variant, four scenarios fail with > `failed to read compiled module: No such file or directory` in an importer. > It reproduces at `-j1`, so it is not a race between compilers: phase 1 parks @@ -317,18 +317,18 @@ Four things this says, and the fixture can say none of them: that column is a *setting*. **And the schedule column is not a free upgrade.** On `touch-hub` and - `edit-comment` it is 0.44s against the default's 0.40s and 0.38s — slightly - WORSE, because those are exactly the rows where mcpp already skips the + `edit-comment` it is 0.44s against the default's 0.40s and 0.38s — slightly + WORSE, because those are exactly the rows where mcpp already skips the cascade, so the split graph adds edges and buys nothing. It pays where a cascade is genuinely owed (`cold`, `edit-body`) and nowhere else. 3. **The daily loop is where the engines differ**, by ~190x on this project: touching a hub interface costs cmake and xmake a full 83-second rebuild because they decide by timestamp, and 0.40s for an engine that compares the - BMI it just produced against the previous one. + BMI it just produced against the previous one. 4. **`edit-body` is the control.** mcpp is deliberately *not* fast there (0.89x): the perturbation inserts a statement into an interface unit, which moves the source position GCC records for every declaration after it, so the - BMI changes and the cascade is owed. An engine that were fast on that row + BMI changes and the cascade is owed. An engine that were fast on that row would have skipped work it owed. A body edit that does NOT move lines, or one in a separate `.cpp`, owes no cascade and mcpp skips it — see §7. @@ -397,12 +397,12 @@ split schedule, and leaving it out understated mcpp badly — `edit-body` reads and skips 45 importers. cmake and xmake decide by timestamp and rebuild all of them — to within 0.00s of each other, which is what two timestamp-driven engines should look like. -* **⚠️ These `bmi_schedule=on` cells were taken BEFORE the §8b fix, and are +* ** These `bmi_schedule=on` cells were taken BEFORE the §8b fix, and are therefore suspect in the same way the mcpp table's were.** All ten reported - `ok` — status cannot see a build that stopped early. The two mcpp-workload + `ok` — status cannot see a build that stopped early. The two mcpp-workload cells that were affected there (`touch-hub`, `edit-comment`) doubled once the object edge stopped being cleaned by the cascade's own restat; `cold` and - `edit-body` did not move. This table's `cold` and `edit-body` are the two + `edit-body` did not move. This table's `cold` and `edit-body` are the two quoted above, so the headline holds, but it has not been re-run. When it is, the file to compare against is `bench/results/xlings-3way-20260814/`. @@ -440,7 +440,7 @@ report: `bench/results/xlings-3way-20260814/xlings-split-3way.json`. (2.38x). Moving implementations out of interface units buys more than switching build tool does. * **`edit-body` is 0.75x — mcpp is SLOWER than cmake here**, 1.79s against 1.35s. - With the body in a `.cpp`, one object recompiles and nothing cascades, so the + With the body in a `.cpp`, one object recompiles and nothing cascades, so the scenario measures per-invocation overhead rather than graph reasoning — the same fixed cost `noop` shows. On this axis mcpp has no advantage to offer and the number says so. @@ -485,7 +485,7 @@ codebase that has not made that change. whole suite, and it is a *code style*, not an engine feature. * **`touch-hub` reproduces the engine result on a codebase nobody tuned for it** — 50.6x, against 190x on mcpp's own tree. Different magnitude, same mechanism. -* ⚠️ **The `cold` row was nearly published as a 23% REGRESSION.** At n=1 the +* **The `cold` row was nearly published as a 23% REGRESSION.** At n=1 the split tree read `29.13s → 35.88s`, i.e. the new mcpp slower. Re-measured at n=3 it is `30.33s → 29.78s` — marginally *faster*. The single pair had simply caught the new arm near the old arm's maximum: the old arm's spread is @@ -540,7 +540,7 @@ whole argument for the toolchain being an axis: **the answer is not the same multiple on both**, so a suite that pinned one compiler would publish one of these two numbers as if it were the answer. -> ⚠️ Those numbers were taken with **cmake 4.0.2 / xmake 3.0.7**, before the +> Those numbers were taken with **cmake 4.0.2 / xmake 3.0.7**, before the > pins in the table above. They are quoted here because they are a real, > reproducible, in-repo result file; CI now runs the pinned versions and the > tables are refreshed from its artifacts. Do not mix rows from the two. @@ -704,9 +704,9 @@ Two details that are easy to get wrong and change the answer: * **`edit-body` and `edit-comment` are separate on purpose.** They were one scenario, named `edit-body`, that inserted a comment — so every "N times faster on edits" number it produced was really a statement about comments. - Splitting them costs one extra column and makes each number mean its name. + Splitting them costs one extra column and makes each number mean its name. - On GCC 16.1 both happen to be cheap for the same underlying reason, and it is + On GCC 16.1 both happen to be cheap for the same underlying reason, and it is worth stating because it is easy to misread as a bug: **GCC does not encode the body of an exported non-template function into the BMI.** Editing such a body changes the object file and leaves the BMI byte-identical apart from its @@ -738,9 +738,9 @@ Two details that are easy to get wrong and change the answer: situation developers live in, and dropping caches adds variance unrelated to the engine. * The harness never lets build output reach its own stdout; child streams go to - `/logs/-.log`. A mixed stream cannot be parsed — and + `/logs/-.log`. A mixed stream cannot be parsed — and the log lives under the WORK root, never inside the measured tree, so a - `--project` run cannot drop scratch into someone's repository. + `--project` run cannot drop scratch into someone's repository. ### 4a. Validity rules — when a cell must NOT be compared @@ -818,9 +818,9 @@ These cannot be removed, so they are stated rather than hidden. here because it stood as a "declared asymmetry" for a while, and a thing you can fix should not stay on this list. * **The `+schedule=on` arm is the same binary, not a different engine.** mcpp's - BMI schedule is a key in the MEASURED PROJECT's manifest and the workloads are + BMI schedule is a key in the MEASURED PROJECT's manifest and the workloads are pinned (one belongs to someone else), so the harness reaches it through - `MCPP_BMI_SCHEDULE` and labels the arm `mcpp@+schedule=on`. It is an + `MCPP_BMI_SCHEDULE` and labels the arm `mcpp@+schedule=on`. It is an option under test, and it is on the same row set as the default so the two are read together rather than across runs. * **No fixture says `import std;`.** Engines differ wildly in how — and whether — @@ -830,14 +830,14 @@ These cannot be removed, so they are stated rather than hidden. every engine handles identically. **This suite measures module machinery, not std-module support.** * **The three arms do not obtain their dependencies the same way, and their - `cold` columns are therefore not the same quantity.** xlings links ftxui, + `cold` columns are therefore not the same quantity.** xlings links ftxui, libarchive, lua and mbedtls, which mcpp's registry ships as SOURCE. The cmake and bazel arms compile those sources themselves — from each package's own - `.xpkg.lua`, so the file list matches mcpp's exactly — which means their - `cold` includes ~470 dependency translation units. The xmake arm declares them + `.xpkg.lua`, so the file list matches mcpp's exactly — which means their + `cold` includes ~470 dependency translation units. The xmake arm declares them through xrepo instead, the way xlings' own `xmake.lua` does, so it links libraries xrepo built earlier and its `cold` does not include them. - `xmake clean` does not evict the xrepo package cache, so this is stable across + `xmake clean` does not evict the xrepo package cache, so this is stable across runs rather than a first-run artefact — but it is a real difference in workload, not a difference in engine speed. Compare `cold` across engines on the FIXTURE, which has no third-party dependencies at all; on xlings, compare @@ -858,7 +858,7 @@ These cannot be removed, so they are stated rather than hidden. | bazel 9.2 + rules_cc 0.2.22 | **yes** | no — `aggregate-ddi failed … Invalid JSON string`, i.e. its ddi aggregator cannot parse GCC's P1689 output | | meson 1.10.2 | no — `fatal error: module 'fx.a' not found`; no attribute declares an interface unit | no | - So a gcc run and a clang run legitimately have **different sets of populated + So a gcc run and a clang run legitimately have **different sets of populated cells**, and a table must say which compiler it used before its `unavailable` rows mean anything. * **bazel module builds are forced to one object flavour.** `cc_binary` registers @@ -866,15 +866,15 @@ These cannot be removed, so they are stated rather than hidden. names the output `.CXXModules.json` for both, so analysis aborts before any compilation: - ``` - Attempted action contains artifacts not in previous action: _objs/fx/unit_0.pic.ddi - Previous action contains artifacts not in attempted action: _objs/fx/unit_0.ddi - Outputs: are equal - ``` + ``` + Attempted action contains artifacts not in previous action: _objs/fx/unit_0.pic.ddi + Previous action contains artifacts not in attempted action: _objs/fx/unit_0.ddi + Outputs: are equal + ``` - The adapter passes `--force_pic` — to **every** variant, so bazel's own + The adapter passes `--force_pic` — to **every** variant, so bazel's own headers-vs-modules rows stay comparable, and PIC rather than - `--features=-supports_pic` because it yields a PIE executable, which is what + `--features=-supports_pic` because it yields a PIE executable, which is what the other engines produce by default. --- @@ -939,7 +939,7 @@ Five parsing traps it exists to get right — each one changed a conclusion duri the original analysis: 1. A multi-output edge (`build a.o | a.gcm : cxx_module`) writes **one - `.ninja_log` line per output**, sharing start/end. Summing lines double-counts + `.ninja_log` line per output**, sharing start/end. Summing lines double-counts compile time (302 s reads as 604 s). 2. For a modules build the real edges live in the **dyndep files** (`obj/*.ddi.dd`), not in `build.ninja`. Ignoring them made mcpp's critical path @@ -986,7 +986,7 @@ export module repro.leaf; export int leaf_value() { return 1; } // change to 42, rebuild ``` - Finished dev in 0.02s <- reported success, 3 of 8 edges run + Finished dev in 0.02s <- reported success, 3 of 8 edges run ./repro8b -> 1 <- the source says 42 No link error and no diagnostic. The detached compiler wrote the correct object @@ -1041,7 +1041,7 @@ missing is a check that sees it from inside the harness. | [`common/`](projects/common/) | the per-engine payload logic both projects share — one branch per compiler family | | [`xlings/`](projects/xlings/) | an **independent** codebase (110 modules / 46k lines, different authors) — the control that separates "a faster build engine" from "a faster benchmark target" | -⚠️ **An engine change that only helps the project it was developed on is not an +**An engine change that only helps the project it was developed on is not an engine change.** The split module schedule was developed against mcpp (2.30x) and reproduces on xlings at **3.38x**; that second number is the one that makes it a general result. Conversely, restructuring a target's modules speeds up that @@ -1087,15 +1087,15 @@ repository, which this harness refuses to do. ### The cmake description has two traps worth knowing * **`FILE_SET CXX_MODULES` requires every file under a base directory.** The - `mcpplibs.cmdline` dependency lives in the registry, outside the tree, so it + `mcpplibs.cmdline` dependency lives in the registry, outside the tree, so it needs its own file set with an explicit `BASE_DIRS`. * **`add_compile_options()` does not reach the `std` module.** CMake generates that target itself, so directory-scope options miss it: the std module then compiles against the compiler's default libc headers while every mcpp unit compiles against `--sysroot`, and the build dies on a type that exists in both (`conflicting type for imported declaration 'char _IO_FILE::_unused2 [20]'`). - The error names neither the flag nor the target that is wrong. Use - `CMAKE_CXX_FLAGS`. + The error names neither the flag nor the target that is wrong. Use + `CMAKE_CXX_FLAGS`. The xmake description pins the compiler by reading `[toolchain] default` out of `mcpp.toml`, because the registry holds several GCCs and "newest directory wins" only *happens* to diff --git a/bench/README.zh-CN.md b/bench/README.zh-CN.md index a559c730..6cc91fa9 100644 --- a/bench/README.zh-CN.md +++ b/bench/README.zh-CN.md @@ -60,7 +60,7 @@ run id : 4e58a816 (resuming, 73 unit(s) recorded) run id : a0840b10 (fresh) ``` -⚠️ **断点续跑正是「把两次跑无声拼在一起」最容易发生的地方**,所以有两条是刻意的: +**断点续跑正是「把两次跑无声拼在一起」最容易发生的地方**,所以有两条是刻意的: * 指纹**不含 mcpp 二进制**。含了的话每次重编都从零开始 —— 而那恰恰是续跑最有用 的时候。每条记录改为携带当时的版本,认领到版本不同的记录时**会报出来**,不是 @@ -96,7 +96,7 @@ BENCH=$(ls -t bench/target/*/*/bin/mbench | head -1) --scenarios noop,touch-hub --hub src/platform/platform.cppm --runs 3 ``` -⚠️ **mcpp 必须以路径指定,不得使用裸名 `mcpp`。** 裸名经 PATH 解析到 xlings shim, +**mcpp 必须以路径指定,不得使用裸名 `mcpp`。** 裸名经 PATH 解析到 xlings shim, 而 **shim 会按当前工作目录重新解析版本** —— `--project` 模式下该目录即被测的树, 而每个工作负载各自锁定了不同版本。曾因此有一整轮将七个格子**全部**测成 `mcpp@2026.8.11.3`(已发布的旧版本,而非被测分支),且**没有任何失败提示这一点**。 @@ -215,7 +215,7 @@ __format/format_functions.h:99:30: error: call to implicitly-deleted default 这解决了四件已经真实发生过的事: * **cmake 3.31.6** 是 GitHub runner 镜像自带的版本。它没有 CMake 4.0 的 - `import std` 实验开关键,所以*每一个 module 格子都 configure 失败*。换成 + `import std` 实验开关键,所以*每一个 module 格子都 configure 失败*。换成 4.0.2 之后全过。 * **`command -v g++`** 在那些镜像上是 gcc 13.3.0。cmake 用它配不出 C++23 modules,xmake 直接把它编崩(internal compiler error)—— 而 mcpp 一直悄悄用 @@ -259,8 +259,8 @@ __format/format_functions.h:99:30: error: call to implicitly-deleted default ## 2. 两种模式 * **fixture 模式** —— 生成一棵合成树,参数化(`--preset` / `--units` / - `--fanin` / `--weight`)。唯一能同时给出 `headers` / `modules` / - `modules-impl` 三种形态的地方,所以也是 *variant* 轴真正成为受控变量的地方。 + `--fanin` / `--weight`)。唯一能同时给出 `headers` / `modules` / + `modules-impl` 三种形态的地方,所以也是 *variant* 轴真正成为受控变量的地方。 * **`--project` 模式** —— 原地测量一棵真实的树。**永远不写入被测仓库**: 编辑类场景会先存下文件字节、无论函数怎么退出都还原(`SourceGuard`),子进程 日志一律落在 `--work` 目录里。 @@ -338,14 +338,14 @@ job 一个测量都没有,这个状态持续了好几周。 进了 xlings 保留在接口单元里的内联函数体,BMI 真的变了,级联是**欠的**。格子的 note 会记录当次是哪种形态(见 §3),不要把它读成优化失效。 * **`touch-hub` 才是级联抑制的真结果:54.96x。** 内容没变,mcpp 拿编译器刚产出的 - BMI 和上一份比,跳过 45 个导入者;cmake 与 xmake 按时间戳判断,把它们全部重建 + BMI 和上一份比,跳过 45 个导入者;cmake 与 xmake 按时间戳判断,把它们全部重建 —— 两者相差 0.00s,这正是两个时间戳驱动的引擎该有的样子。 -* **⚠️ 这里的 `bmi_schedule=on` 格子取自 §8b 修复之前,因此和 mcpp 那张表一样可疑。** +* ** 这里的 `bmi_schedule=on` 格子取自 §8b 修复之前,因此和 mcpp 那张表一样可疑。** 十个格子全报 `ok` —— 状态列看不见「构建提前收工」。在 mcpp 工作负载上受影响的两 个格子(`touch-hub`、`edit-comment`)在 object 边不再被级联自己的 restat 清掉之后 翻了一倍;`cold` 与 `edit-body` 没动。上面引用的正是 `cold` 与 `edit-body`,所以 结论成立,但这张表**尚未重跑**。重跑时的对照文件是 - `bench/results/xlings-3way-20260814/`。 + `bench/results/xlings-3way-20260814/`。 xlings `2026.8.11.2`,gcc 16.1.0 载荷,Linux x86_64 · i9-13900K · n=1 · `--baseline cmake`。原始报告:`bench/results/xlings-3way-20260814/`。 @@ -378,7 +378,7 @@ job 一个测量都没有,这个状态持续了好几周。 降到 27.59s(3.35x),而 cmake 自己的 cold 也从 119.46s 降到 50.13s(2.38x)。 把实现搬出接口单元,比换构建工具更值。 * **`edit-body` 是 0.75x —— 这里 mcpp 比 cmake 慢**(1.79s 对 1.35s)。函数体在 - `.cpp` 里,只重编一个 object、没有级联,于是这个场景量的是**每次调用的固定 + `.cpp` 里,只重编一个 object、没有级联,于是这个场景量的是**每次调用的固定 开销**而不是图推理 —— 和 `noop` 反映的是同一笔成本。在这条轴上 mcpp 没有优势 可言,数字也就这么写。 * **`touch-hub` 仍然值:20.08x。** 比 combined 树的 54.96x 小,因为下游本来就 @@ -428,7 +428,7 @@ export module repro.leaf; export int leaf_value() { return 1; } // 改成 42,重建 ``` - Finished dev in 0.02s <- 报告成功,8 条边只跑了 3 条 + Finished dev in 0.02s <- 报告成功,8 条边只跑了 3 条 ./repro8b -> 1 <- 源码写的是 42 没有链接错误,没有任何诊断。分离出去的编译器在 0.2 秒后写出了正确的 object,而 @@ -483,7 +483,7 @@ ninja 早已决定不链接它。在生成的 fixture 上,同一个跳过表现 请比增量场景,那里没有任何一条臂会重建依赖。 * **`+schedule=on` 那条臂是同一个二进制,不是另一个引擎。** 它是被测工程 manifest 里的一个键,由 harness 经 `MCPP_BMI_SCHEDULE` 打开,标签写作 - `mcpp@+schedule=on`,和默认配置放在同一组行里一起读。 + `mcpp@+schedule=on`,和默认配置放在同一组行里一起读。 * **没有任何 fixture 写 `import std;`。** 各引擎在「能否、以及如何」构建 std 模块 上差异极大(CMake 需要一把随版本变化的实验性 UUID,meson 根本没有说法),这个 差异会淹没一切测量。fixture 一律经全局模块片段取标准库。**本套件测的是模块 diff --git a/bench/SPEC.md b/bench/SPEC.md index d27a17aa..d36b4039 100644 --- a/bench/SPEC.md +++ b/bench/SPEC.md @@ -119,11 +119,11 @@ Real projects are the control that stops an engine change from being an artefact of one graph shape: * **`fixture`** — synthetic, parameterised (`--preset`, `--units`, `--fanin`, - `--weight`). The only project where `headers` / `modules` / `modules-impl` + `--weight`). The only project where `headers` / `modules` / `modules-impl` are all generated, so it is where the *variant* axis is a controlled variable. * **`mcpp-2026.8.11.3`** (`a749e9f`) — 137 modules / 57k lines, one source dependency, build descriptions for every engine under `projects/mcpp/`. - Pinned like everything else: the engine under test is the binary, and a + Pinned like everything else: the engine under test is the binary, and a workload that moves with the branch makes two runs incomparable. * **`xlings-2026.8.11.2`** and **`xlings-2026.8.13.1`** — 110 modules / 46k lines, **different authors**. This is what separates "a faster build engine" @@ -245,7 +245,7 @@ job: the cell still runs, and its note says what to distrust. | `edit-body` | a real semantic edit inside a function body | the everyday loop — and whether a cascade is owed depends on **where the body lives and whether the edit moves lines**, not on what the body now does. See below. | | `touch-leaf` | mtime bump on a unit nobody imports | recompile 1 + link | -#### ⚠️ `edit-body` perturbs a DIFFERENT FILE in each variant, and the two ask +#### `edit-body` perturbs a DIFFERENT FILE in each variant, and the two ask #### opposite questions | variant | file perturbed | what a correct engine does | @@ -265,7 +265,7 @@ same edit expressed as a same-line substitution does **not** cascade on GCC. Under clang the interface cascades either way, because clang serialises definitions into the BMI regardless of where in the file they appear. -⚠️ **The generated fixture does not reproduce this**: its perturbed function is +**The generated fixture does not reproduce this**: its perturbed function is the LAST declaration in `unit_0.cppm`, so nothing shifts and the BMI is unchanged (measured: 0.94s, against 80.87s for the same scenario on the mcpp tree). Any conclusion about a real project drawn from the fixture's `edit-body` is invalid @@ -319,7 +319,7 @@ body *does* is invisible to importers. What it does serialise is the source position of each declaration — so inserting a line moves every declaration after it and the BMI changes for that reason alone. -⚠️ **An earlier version of this section said the deciding factor was whether the +**An earlier version of this section said the deciding factor was whether the body belonged to an exported class.** That was reasoning, and the measurement refuted it: editing `Version::str()` — a member of an exported class — in place rebuilt its object and left the BMI byte-identical, so no importer was touched. @@ -328,11 +328,11 @@ The deciding factor is line movement, not class membership. Two consequences: * "editing one function rebuilt forty modules" is not inherent to named modules. - It follows from the edit moving lines in an interface unit. + It follows from the edit moving lines in an interface unit. * the third row is the sturdiest, because it holds for **every** compiler and for every edit: a `.cpp` implementation unit produces no BMI, so nothing downstream can depend on its contents. Clang, whose BMI carries more than - GCC's, cascades on an in-place body edit in a `.cppm` but not on a `.cpp`. + GCC's, cascades on an in-place body edit in a `.cppm` but not on a `.cpp`. **This is what the two xlings pins measure.** Moving the implementations out of the interface units takes `edit-body` from 88.33s to **1.77s** on the same @@ -380,7 +380,7 @@ two *descriptions* as an engine result. `os` matches the machine it is on, and runs **every engine that cell lists** at **3 samples** each. -⚠️ **`allow_failed` is NOT consulted by the runner.** Those waivers were recorded +**`allow_failed` is NOT consulted by the runner.** Those waivers were recorded against failures on a shared CI runner, and at least two of them describe arms that configure and generate perfectly well on a developer machine. Filtering by them would carry a runner's limitation into local data and publish a smaller diff --git a/bench/projects/xlings/README.md b/bench/projects/xlings/README.md index 9b406fb6..42ecacdb 100644 --- a/bench/projects/xlings/README.md +++ b/bench/projects/xlings/README.md @@ -118,26 +118,26 @@ Three things looked like boundaries and were not: include directories of the dependency targets rather than as a hand-written walk of registry subdirectories. * **A generated module.** `mcpplibs.xpkg.lua_stdlib` is not checked in; libxpkg's - `build.mcpp` produces it. But all it does is embed eleven `.lua` files as + `build.mcpp` produces it. But all it does is embed eleven `.lua` files as strings, so [`embed_lua_stdlib.cmake`](embed_lua_stdlib.cmake) reproduces it. *"mcpp runs a build program"* is not by itself a boundary. * **The source dependencies.** `ftxui`, `libarchive`, `lua` and `mbedtls` arrive as **source** and mcpp compiles them, so the link asked for symbols nobody had built here. Neither obvious answer works: `add_subdirectory` on the vendored - CMakeLists drags in test suites libarchive cannot even configure without, and + CMakeLists drags in test suites libarchive cannot even configure without, and mbedtls 3.6.1 `FATAL_ERROR`s **unconditionally** on a `framework/` submodule the registry tarball does not carry — no option disables it. A glob of the unpacked tree compiles the wrong set (`libarchive/*.c` is 132 files where mcpp compiles 127; `lua/src/*.c` is 34 where it compiles 32, the two extra being - `lua.c` and `luac.c`, each with its own `main()`). + `lua.c` and `luac.c`, each with its own `main()`). - What does work is that **every package in mcpp's registry ships a `.xpkg.lua` + What does work is that **every package in mcpp's registry ships a `.xpkg.lua` naming exactly the sources, include dirs and cflags mcpp compiles it with**. [`xpkg_source_library.cmake`](xpkg_source_library.cmake) reads that, so both engines compile the same 127 files with the same defines, and a pattern that resolves to nothing is a configure error rather than a quietly smaller build. -⚠️ The copied module list in the generator **already drifted once**: a first +The copied module list in the generator **already drifted once**: a first regex caught ten of eleven entries, and the failure surfaced three files away as `error: 'base64_lua' is not a member of ...detail`. The generator now fails on a missing `.lua` rather than trusting the list. @@ -149,7 +149,7 @@ a second engine. And because the cmake description now links, the same tree also carries a cross-engine comparison on a project with six dependencies, four of them compiled from source by both engines. -⚠️ **One asymmetry is real and is declared rather than smoothed over**: on a cold +**One asymmetry is real and is declared rather than smoothed over**: on a cold build cmake compiles all nine source packages (467 C/C++ translation units) while mcpp may serve them from its global dependency cache. See `../../README.md` §5. diff --git a/bench/results/README.md b/bench/results/README.md index 386abf71..fe2cd0d7 100644 --- a/bench/results/README.md +++ b/bench/results/README.md @@ -16,7 +16,7 @@ per cell, and the only one the README tables quote. Everything else is history kept because a claim that cannot be checked against the run that produced it is not a measurement, and deleted history cannot be checked at all. -⚠️ **Every directory below the standard one was taken under conditions that no +**Every directory below the standard one was taken under conditions that no longer hold.** Read them as a record of what was measured then, not as data about mcpp now: diff --git a/bench/results/pinned-workloads-20260813/report.md b/bench/results/pinned-workloads-20260813/report.md index 94edeee5..6262e078 100644 --- a/bench/results/pinned-workloads-20260813/report.md +++ b/bench/results/pinned-workloads-20260813/report.md @@ -45,7 +45,7 @@ BMI schedule enabled via `MCPP_BMI_SCHEDULE`. * **`edit-comment` here is the `end-of-file` form**: mcpp's hub has no function body, so nothing shifts. See §3. -⚠️ **The `xmake` column is from a separate run** (`…-xmake-refixed.json`). In the +**The `xmake` column is from a separate run** (`…-xmake-refixed.json`). In the five-arm file its `cold` reads **0.60s** — invalid. xmake normalises `--buildir` to a path relative to `-P` and resolves it against the process cwd, so `clean()` had been removing a directory nothing ever wrote to. Fixed, re-measured, and the @@ -71,7 +71,7 @@ mcpp. * `touch-hub` reproduces the engine result on a codebase nobody tuned for it. -⚠️ **The `cold` row was nearly published as a 23% regression.** At n=1 it read +**The `cold` row was nearly published as a 23% regression.** At n=1 it read `29.13s → 35.88s`. At n=3 it is `30.33s → 29.78s`, marginally faster: the single pair had caught the new arm near the old arm's max. The old arm's spread is **19.1%** — a hair under the 20% that §4a R2 calls noisy. diff --git a/bench/results/standard-20260814-linux-x86_64/README.md b/bench/results/standard-20260814-linux-x86_64/README.md index 2a264ba7..d2a1ea8a 100644 --- a/bench/results/standard-20260814-linux-x86_64/README.md +++ b/bench/results/standard-20260814-linux-x86_64/README.md @@ -12,7 +12,7 @@ The Linux standard data set. **696 measured samples**, 3 per cell, produced by | **mcpp under test** | **built from `8b579fa`** — see the note below | | started | 2026-08-15 (UTC date stamp `20260814`) | -## ⚠️ The commit is recorded HERE and not in the JSON +## The commit is recorded HERE and not in the JSON Every engine labels itself from `--version`, and mcpp's version is a **date**: every commit on a branch reports `2026.8.13.1`. So these reports can say which diff --git a/bench/run-standard.sh b/bench/run-standard.sh index ccf6b447..e3cff9d6 100755 --- a/bench/run-standard.sh +++ b/bench/run-standard.sh @@ -20,7 +20,7 @@ # script does not repeat it; it selects the cells for THIS os and runs every # engine they list. # -# ⚠️ `allow_failed` IS DELIBERATELY IGNORED HERE, and the first version of this +# `allow_failed` IS DELIBERATELY IGNORED HERE, and the first version of this # script got that wrong. Those waivers were recorded against failures on the CI # RUNNER — cmake's `__CMAKE::CXX23` on the mcpp tree, cmake's `manifest has no # sources` on the xlings tree — and both of those arms configure and generate @@ -53,7 +53,7 @@ # resume that is not actually the same run lands in a different cache and starts # from zero on its own. # -# ⚠️ SEED BUILDS ARE NOT UNITS AND ARE REDONE. An incremental scenario needs a +# SEED BUILDS ARE NOT UNITS AND ARE REDONE. An incremental scenario needs a # tree that is already up to date, and that state was built by a seed build that # no journal can hold. A resumed cell therefore pays its seed again before it # can skip anything — resume is cheap, not free. @@ -107,7 +107,7 @@ BENCH="$(ls -t "$ROOT"/bench/target/*/*/bin/mbench 2>/dev/null | head -1)" MCPP_BIN="$(ls -t "$ROOT"/target/*/*/bin/mcpp 2>/dev/null | head -1)" [ -n "$MCPP_BIN" ] || say_missing "the mcpp under test — run: mcpp build --release" -# ⚠️ THE ENGINE MUST BE A BINARY, NEVER THE BARE NAME `mcpp`. +# THE ENGINE MUST BE A BINARY, NEVER THE BARE NAME `mcpp`. # # A bare `mcpp` resolves through PATH to the xlings shim, and that shim RE-PICKS # its version from the working directory — which, for a `--project` run, is the @@ -179,7 +179,7 @@ for c in m["cells"]: engines = [e.strip() for e in c["engines"].split(",") if e.strip()] if not engines: continue - # ⚠️ NOT A TAB. `IFS=$'\t' read` treats tab as IFS WHITESPACE, so consecutive + # NOT A TAB. `IFS=$'\t' read` treats tab as IFS WHITESPACE, so consecutive # tabs COLLAPSE — a cell with no `leaf` shifted every later field one place # left and cmake was handed the BASELINE as its source directory: # CMake Error: The source directory ".../bench/projects/2026.8.11.3" @@ -200,13 +200,13 @@ PY STAMP="$(date -u +%Y%m%d)" OUT="$ROOT/bench/results/standard-$STAMP-$OS-$ARCH" -# ⚠️ SCRATCH DOES NOT LIVE IN results/. Putting the work tree under $OUT +# SCRATCH DOES NOT LIVE IN results/. Putting the work tree under $OUT # meant engine scratch — including JSON files whose top level is an ARRAY — # landed in the directory the README guard scans for published medians, and # it died with `AttributeError: 'list' object has no attribute 'get'`. # It would also have been committed along with the data. WORK="${TMPDIR:-/tmp}/bench-standard-$STAMP-$$" -# ⚠️ REFUSE TO MIX RUNS. A previous invocation's reports must not sit beside +# REFUSE TO MIX RUNS. A previous invocation's reports must not sit beside # this one's under the same names. # # It happened: an earlier run was stopped with SIGTERM, did not die immediately, diff --git a/bench/src/engines/bazel.cpp b/bench/src/engines/bazel.cpp index 6d8478ab..ee7c6632 100644 --- a/bench/src/engines/bazel.cpp +++ b/bench/src/engines/bazel.cpp @@ -86,7 +86,7 @@ platform::RunResult BazelEngine::build(const Job& job) const{ // it yields a PIE executable, which is what gcc/clang produce by default // for everyone else in the table. // - // ⚠️ POSIX ONLY. Windows has no PIC — code there is relocatable by + // POSIX ONLY. Windows has no PIC — code there is relocatable by // construction — so its toolchain does not enable `supports_pic`, and // asking for it is not ignored, it is fatal at ANALYSIS: // diff --git a/bench/src/engines/engine.cpp b/bench/src/engines/engine.cpp index cb532482..b0f1480b 100644 --- a/bench/src/engines/engine.cpp +++ b/bench/src/engines/engine.cpp @@ -63,7 +63,7 @@ Availability probe_program(std::string_view program, version_argv.size() > 1 ? version_argv[1] : "--version", r.exit_code)}; auto banner = first_line(*captured); - // ⚠️ A SHIM THAT ANSWERS FOR A PROGRAM IT DOES NOT HAVE. xlings installs + // A SHIM THAT ANSWERS FOR A PROGRAM IT DOES NOT HAVE. xlings installs // `bazel`, `mcpp` and friends as shims on PATH; ask one for its version // when the package is not installed and it prints // diff --git a/bench/src/engines/xmake.cpp b/bench/src/engines/xmake.cpp index 06146bd0..73716a64 100644 --- a/bench/src/engines/xmake.cpp +++ b/bench/src/engines/xmake.cpp @@ -34,7 +34,7 @@ platform::RunResult XmakeEngine::configure(const Job& job) const { "-P", job.buildfile_dir.string(), "-m", job.profile == "debug" ? "debug" : "release", "-o", job.build_dir.string(), - // ⚠️ xmake's compiler cache is ON BY DEFAULT (`--ccache=y`) and it + // xmake's compiler cache is ON BY DEFAULT (`--ccache=y`) and it // lives OUTSIDE the build directory, so `clean()` cannot reach it. // A `cold` build then restores every object from it: // seed build 105.059s @@ -124,7 +124,7 @@ platform::RunResult XmakeEngine::configure(const Job& job) const { // warning: std and std.compat modules not found! // maybe try to add --sdk= or install libc++ // - // ⚠️ THAT SUGGESTION IS A DEAD END, and following it cost three rounds. + // THAT SUGGESTION IS A DEAD END, and following it cost three rounds. // `--sdk` is only read on the `c++` branch, which was never reached. The // payload has carried `lib//libc++.modules.json` — precisely // what that branch looks for — the entire time. diff --git a/bench/src/engines/xmake.cppm b/bench/src/engines/xmake.cppm index 13e4e08c..603ce47d 100644 --- a/bench/src/engines/xmake.cppm +++ b/bench/src/engines/xmake.cppm @@ -54,7 +54,7 @@ public: platform::RunResult build(const Job& job) const override; - // ⚠️ EVERY COMMAND RUNS FROM `buildfile_dir`, i.e. the `-P` directory, and + // EVERY COMMAND RUNS FROM `buildfile_dir`, i.e. the `-P` directory, and // that is load-bearing rather than tidiness. // // xmake normalises `--builddir` (`-o`) to a path RELATIVE TO THE PROJECT diff --git a/bench/src/fixture/buildfiles.cpp b/bench/src/fixture/buildfiles.cpp index b48542fc..2d1c020b 100644 --- a/bench/src/fixture/buildfiles.cpp +++ b/bench/src/fixture/buildfiles.cpp @@ -147,7 +147,7 @@ void emit_xmake(const std::filesystem::path& root, Variant variant, const Shape& // cost none of the others do. lua += " set_policy(\"build.c++.modules.std\", false)\n"; } - // ⚠️ PIE MUST BE EXPLICIT ON BOTH SIDES. The payload gcc defaults to a PIE + // PIE MUST BE EXPLICIT ON BOTH SIDES. The payload gcc defaults to a PIE // link on the CI runners and to a non-PIE link on some developer boxes, so // compiling without `-fPIE` produced objects the linker then refused: // relocation R_X86_64_32 against `.rodata.str1.1' can not be used diff --git a/bench/src/journal.cppm b/bench/src/journal.cppm index 23c60626..f9bb533c 100644 --- a/bench/src/journal.cppm +++ b/bench/src/journal.cppm @@ -3,7 +3,7 @@ // INTERFACE ONLY. Definitions live in journal.cpp, the same split the xlings // tree this suite measures uses for every module. // -// ⚠️ THAT SPLIT IS NOT STYLE HERE, IT IS THE FIX. This started as one file with +// THAT SPLIT IS NOT STYLE HERE, IT IS THE FIX. This started as one file with // the class defined inline in the interface, and the whole suite then failed to // compile with // diff --git a/bench/src/main.cpp b/bench/src/main.cpp index dd796aa9..746d1d39 100644 --- a/bench/src/main.cpp +++ b/bench/src/main.cpp @@ -509,7 +509,7 @@ int main(int argc, char** argv) { const bench::Journal journal(cache_dir / "journal.jsonl"); auto loaded = journal.load(id.str()); - // ⚠️ A RESUME MUST NOT SILENTLY SPAN TWO BUILDS OF WHAT IS UNDER TEST. + // A RESUME MUST NOT SILENTLY SPAN TWO BUILDS OF WHAT IS UNDER TEST. // // The fingerprint deliberately excludes the binary, so that rebuilding does // not throw the cache away — and that is exactly what makes this possible: @@ -527,7 +527,7 @@ int main(int argc, char** argv) { if (std::ifstream in(stamp); in) std::getline(in, previous); if (!previous.empty() && !opts->under_test.empty() && previous != opts->under_test) std::println(std::cerr, - "bench: ⚠️ this cache holds samples measured with '{}' but this run is " + "bench: this cache holds samples measured with '{}' but this run is " "'{}'. The report will mix them. Use --id to fork a fresh cache, or " "delete {}.", previous, opts->under_test, cache_dir.string()); diff --git a/bench/src/platform.cpp b/bench/src/platform.cpp index e82dcd1d..19005781 100644 --- a/bench/src/platform.cpp +++ b/bench/src/platform.cpp @@ -68,7 +68,7 @@ std::string log_grep(const std::filesystem::path& p, if (!in) return {}; std::string out, line; std::size_t kept = 0; - // ⚠️ THE MESSAGE IS OFTEN ON THE NEXT LINE. cmake writes + // THE MESSAGE IS OFTEN ON THE NEXT LINE. cmake writes // CMake Error at xpkg_source_library.cmake:231 (message): // bench: 's manifest has no `sources` list // and a grep that returns only matching lines keeps the location and throws diff --git a/bench/src/platform.cppm b/bench/src/platform.cppm index 412bc2ca..1528f239 100644 --- a/bench/src/platform.cppm +++ b/bench/src/platform.cppm @@ -66,7 +66,7 @@ struct RunResult { // Distinguishes "could not start" from "started and failed" — the whole // basis for reporting an engine as unavailable rather than broken. // - // ⚠️ NOT `exit_code >= 0`. That was the test, and on Windows it is wrong in + // NOT `exit_code >= 0`. That was the test, and on Windows it is wrong in // the one case that matters: a child that CRASHES exits with a status like // 0xC0000135 (a DLL it needs is missing) or 0xC0000005 (access violation), // and `GetExitCodeProcess` hands back a DWORD that becomes a NEGATIVE int. diff --git a/bench/src/platform/windows.cppm b/bench/src/platform/windows.cppm index 96a2f8f9..9b87d8a1 100644 --- a/bench/src/platform/windows.cppm +++ b/bench/src/platform/windows.cppm @@ -133,7 +133,7 @@ export int run_process(const std::vector& argv, // the job; KILL_ON_JOB_CLOSE so the tree also dies if the harness itself is // killed, which is the case a timeout handler cannot cover. // - // ⚠️ NOT VERIFIED ON WINDOWS — this repository's author has no Windows + // NOT VERIFIED ON WINDOWS — this repository's author has no Windows // machine, and CI does not time out, so no job here exercises it. It is // written to be non-regressive rather than to be trusted: every step is // checked, and any failure falls through to exactly the previous behaviour @@ -155,7 +155,7 @@ export int run_process(const std::vector& argv, job ? CREATE_SUSPENDED : 0, nullptr, cwd.empty() ? nullptr : cwd_s.c_str(), &si, &pi); if (!ok) { - // ⚠️ GetLastError FIRST — CloseHandle overwrites it, and this is the + // GetLastError FIRST — CloseHandle overwrites it, and this is the // whole reason the message exists. "could not start the process" covers // a program not on PATH (2), a cwd that does not exist (267), a bad // handle and an ACL; those are four different fixes and the harness diff --git a/bench/src/protocol.cppm b/bench/src/protocol.cppm index 05ca38fe..4cbb68b2 100644 --- a/bench/src/protocol.cppm +++ b/bench/src/protocol.cppm @@ -151,13 +151,13 @@ struct Report { // Hashed: engines, variants, scenarios, run count, compiler, profile, fixture // shape, project, buildfiles, and `--id`. // -// ⚠️ NOT hashed: the mcpp binary, the installed cmake, anything that can change +// NOT hashed: the mcpp binary, the installed cmake, anything that can change // underneath while the command line stays the same. Folding those in would // restart from zero on every rebuild — the normal case while developing, and // exactly when resume is worth having. They are recorded per entry as observed // facts, and adopting a record measured with a different one is REPORTED. // -// ⚠️ WHY AN IDENTITY AT ALL: resuming means treating an old record as this +// WHY AN IDENTITY AT ALL: resuming means treating an old record as this // run's result, so without one a resumable benchmark is a machine for silently // splicing runs together. Not hypothetical — a killed run did not die on // SIGTERM, finished its cell and wrote its report AFTER the `rm -rf` meant to diff --git a/bench/src/runner.cpp b/bench/src/runner.cpp index fd3af4c7..57635907 100644 --- a/bench/src/runner.cpp +++ b/bench/src/runner.cpp @@ -30,7 +30,7 @@ std::optional insert_into_first_body( // that follows a ')'. Anchoring on the brace rather than a name keeps this // working for all three variants, whose function text differs. // - // ⚠️ AFTER THE BRACE, not after the newline that follows it. Those are the + // AFTER THE BRACE, not after the newline that follows it. Those are the // same position only when the body spans several lines. Given a one-line // body — `export int f() { return 1; }` — the newline is past the CLOSING // brace, so the statement landed at namespace scope and the build died with @@ -204,7 +204,7 @@ CellResult Runner::measure(engines::Engine& engine, const Instance& inst, Varian const auto crashed = platform::log_mentions( job.log_path, {"PLEASE submit a bug report", "Stack dump"}); - // ⚠️ A TAIL IS THE WRONG SHAPE WHEN THE TOOL IS CHATTY. Every build + // A TAIL IS THE WRONG SHAPE WHEN THE TOOL IS CHATTY. Every build // engine here prints a progress line per translation unit, so 20 // lines of tail is 20 lines of `generating.module.deps ...` and the // error that actually stopped it — printed once, hundreds of lines @@ -218,7 +218,7 @@ CellResult Runner::measure(engines::Engine& engine, const Instance& inst, Varian // it is the difference between "exited 255" and a cause. if (const auto why = platform::log_grep( job.log_path, - // ⚠️ EVERY ENGINE SPELLS IT DIFFERENTLY, and the first + // EVERY ENGINE SPELLS IT DIFFERENTLY, and the first // version of this list only knew the compiler's spelling. // cmake writes `CMake Error in CMakeLists.txt:` — no colon // after "Error", capital E — so the very next failure it diff --git a/bench/src/runner.cppm b/bench/src/runner.cppm index e9749f9e..5bea5741 100644 --- a/bench/src/runner.cppm +++ b/bench/src/runner.cppm @@ -56,7 +56,7 @@ struct RunOptions { // Hooks rather than a Journal member so the runner keeps knowing nothing // about files — the same reason `on_progress` is a callback. // - // ⚠️ THE SEED BUILD IS NOT A UNIT and is redone on resume. An incremental + // THE SEED BUILD IS NOT A UNIT and is redone on resume. An incremental // scenario only means anything against an up-to-date tree, and that state // is what the seed establishes; it lives on disk, not in the journal. So // resuming a partially-measured cell costs one seed build. Stated because diff --git a/bench/src/toolchain.cpp b/bench/src/toolchain.cpp index d44643f6..bdb55dcc 100644 --- a/bench/src/toolchain.cpp +++ b/bench/src/toolchain.cpp @@ -90,7 +90,7 @@ PayloadFlags payload_flags(std::string_view compiler) { return f; } - // ⚠️ macOS GETS NOTHING, AND THAT IS THE CORRECT ANSWER. + // macOS GETS NOTHING, AND THAT IS THE CORRECT ANSWER. // // Pointing `-L`/`-rpath` at the registry's lib directory puts a second // libc++ where the platform toolchain can find it, and Apple's own linker @@ -116,7 +116,7 @@ PayloadFlags payload_flags(std::string_view compiler) { if (std::filesystem::is_directory(root / "include" / "c++" / "v1", ec)) { f.compile += " --no-default-config -nostdinc++" " -isystem" + (root / "include" / "c++" / "v1").string(); - // ⚠️ AND THE PER-TRIPLE DIRECTORY, which is where `__config_site` lives. + // AND THE PER-TRIPLE DIRECTORY, which is where `__config_site` lives. // libc++'s `__config` includes it, so without this every TU dies with // // __config:13:10: fatal error: '__config_site' file not found @@ -128,7 +128,7 @@ PayloadFlags payload_flags(std::string_view compiler) { if (std::filesystem::is_directory(cand, ec)) f.compile += " -isystem" + cand.string(); } - // ⚠️ AND THE PER-TRIPLE lib DIRECTORY. In this payload libc++ lives in + // AND THE PER-TRIPLE lib DIRECTORY. In this payload libc++ lives in // `lib/x86_64-unknown-linux-gnu/`, not `lib/`. A clang DRIVER finds it // by itself, which is why the cmake arm worked with `-L…/lib` alone — // but an engine that links through a different driver does not, and @@ -137,7 +137,7 @@ PayloadFlags payload_flags(std::string_view compiler) { // ld: cannot find -lc++: No such file or directory // // Naming both directories makes the flags independent of who links. - // ⚠️ -L TELLS THE LINKER; -rpath TELLS THE LOADER. They are different + // -L TELLS THE LINKER; -rpath TELLS THE LOADER. They are different // questions and this payload needs both answered: libc++ lives inside // the registry, nowhere the dynamic loader looks by default. With only // -L, every engine on macOS produced a binary that linked cleanly and diff --git a/bench/src/toolchain.cppm b/bench/src/toolchain.cppm index 6fa2289e..afaf8739 100644 --- a/bench/src/toolchain.cppm +++ b/bench/src/toolchain.cppm @@ -45,7 +45,7 @@ bool is_clang_request(std::string_view compiler); // decision `mcpp_pin` and `payload_cxx` both read, so the toolchain mcpp is told // to use and the driver every other engine is handed cannot disagree. // -// ⚠️ THE HOST IS PART OF THE ANSWER. There is no gcc payload for macOS in mcpp's +// THE HOST IS PART OF THE ANSWER. There is no gcc payload for macOS in mcpp's // registry (bench/matrix.json excludes the macos/gcc cell for exactly that // reason), and the Windows payload is llvm. A pin that reads `gcc@16.1.0` // everywhere fails on those hosts with @@ -81,7 +81,7 @@ Resolved payload_cxx(std::string_view compiler); // build and link — the generated fixture's counterpart of // bench/projects/common/cmake/hermetic_payload.cmake. // -// ⚠️ WHY THIS EXISTS AT ALL, given that file exists. The checked-in project +// WHY THIS EXISTS AT ALL, given that file exists. The checked-in project // descriptions `include()` it; the fixture is GENERATED into a scratch // directory by a binary that may live anywhere, so it has no path to include. // The two are the same decision in two places and must be kept in step — the diff --git a/bench/tests/harness.sh b/bench/tests/harness.sh index 8c2e4d38..f6c86e5c 100755 --- a/bench/tests/harness.sh +++ b/bench/tests/harness.sh @@ -55,7 +55,7 @@ echo "harness built with: $("$MCPP" --version 2>&1 | head -1) ($MCPP)" # stale one. That is a test exercising code that has already been replaced, with # no symptom at all. See .github/tools/newest_artifact.sh. # -# ⚠️ THE NAME COMES FROM THE MANIFEST, NOT FROM THIS LINE. The harness binary was +# THE NAME COMES FROM THE MANIFEST, NOT FROM THIS LINE. The harness binary was # renamed `bench` -> `mbench` and this kept asking for `bench`, so the test died # with `newest_artifact: no 'bench' under target/*/*/bin/` — a rename caught only # by running it, because nothing ties the two together. Reading the name out of @@ -64,7 +64,7 @@ BENCH_NAME="$(sed -n 's/^ *name *= *"\([^"]*\)".*/\1/p' "$REPO/bench/mcpp.toml" [ -n "$BENCH_NAME" ] || { echo "FAIL: no package name in bench/mcpp.toml"; exit 1; } BENCH="$REPO/bench/$(bash "$REPO/.github/tools/newest_artifact.sh" target "$BENCH_NAME")" -# ⚠️ A TEST MUST MEASURE, NOT RESUME. +# A TEST MUST MEASURE, NOT RESUME. # # mbench caches every measured sample under `--cache-root` (default `.mbench` in # the CURRENT DIRECTORY) and replays it when the configuration matches. Run from @@ -118,7 +118,7 @@ bench --engines "mcpp=$MCPP" --variants modules --scenarios cold,noop \ || { echo "harness exited non-zero"; dump_child_logs; exit 1; } # 3. The report must be a protocol-shaped document, not merely non-empty. -# ⚠️ THE EXPECTED VERSION COMES FROM THE PROTOCOL, NOT FROM THIS LINE. +# THE EXPECTED VERSION COMES FROM THE PROTOCOL, NOT FROM THIS LINE. # # This asserted `"protocol_version": 1` literally. The protocol says to bump on # any field addition — which is exactly what adding `under_test` did — and the diff --git a/bench/tools/report.py b/bench/tools/report.py index 63b0b75e..6c7dd69a 100755 --- a/bench/tools/report.py +++ b/bench/tools/report.py @@ -39,14 +39,14 @@ def load_journal(path): """Reduce a `.mbench//journal.jsonl` to report cells. - ⚠️ WHY THIS EXISTS. The journal records one line per measured SAMPLE, but a + WHY THIS EXISTS. The journal records one line per measured SAMPLE, but a report JSON is only written when a whole CELL finishes — so an interrupted run left its samples on disk with no way to look at them. 42 measured points of an in-flight cell were invisible to every table in the repository while sitting in a file. The design says the journal is the source of truth and the report is derived from it; until this function, that was only half true. - The reduction is deliberately the same one the harness does: group by + The reduction is deliberately the same one the harness does: group by (project, variant, scenario, engine), median/min/max over the samples present. A partial group is reported with the count it actually has, never padded — `runs` in the output is how many samples exist, not how many were @@ -158,7 +158,7 @@ def render(cells, baseline): # `233_bench_matrix.sh` can check it against ONE file instead of a hard-coded # list of three. # -# ⚠️ The wording is deliberately not translated field-by-field. A benchmark table +# The wording is deliberately not translated field-by-field. A benchmark table # that says different things in two languages is two claims, and only one of them # can be checked against the data. ALLOW_SUSPECT = [False] @@ -186,8 +186,8 @@ def render(cells, baseline): def engine_order(engines): """mcpp arms first, newest version first, each default before its opt-in arm. - Reading order matters more than it looks: the build under test and its - `+schedule=on` arm answer one question ("what does the key buy?") and the + Reading order matters more than it looks: the build under test and its + `+schedule=on` arm answer one question ("what does the key buy?") and the released reference answers a different one ("did this get faster?"). Sorting the raw strings interleaved them — reference, opt-in, default — so neither pair sat together and every comparison was two columns apart. @@ -214,14 +214,14 @@ def key(e): # horizontal scrollbar and the reader sees two columns of a five-column # comparison. The identity moves to the footnote, where it is read once. # -# ⚠️ THE MAPPING IS EMITTED, NOT REPEATED. A short name in the table and a long +# THE MAPPING IS EMITTED, NOT REPEATED. A short name in the table and a long # one in the data is exactly the drift this tool exists to remove, so the header # carries a machine-readable `` line and the guard reads the # mapping from there instead of keeping a second copy. def short_name(engine, newest, lang="en"): """The column header. - ⚠️ NOT `mcpp +schedule`. "schedule" is the name of the MECHANISM, and a + NOT `mcpp +schedule`. "schedule" is the name of the MECHANISM, and a reader meeting this table for the first time has no idea whether a build scheduler makes things faster, slower or merely different. The column is there to say "this is the opt-in speed-up"; what it actually turns on is one @@ -234,7 +234,7 @@ def short_name(engine, newest, lang="en"): return "mcpp +优化" if lang == "zh" else "mcpp +opt" if base == newest: return "mcpp" - # ⚠️ NAME THE RELEASE, do not write "old". The version is already in the + # NAME THE RELEASE, do not write "old". The version is already in the # engine key — it is the version that binary reported about ITSELF — so a # header that hides it is throwing away the one fact that makes the column # checkable. A guard used to enforce this from outside, by requiring @@ -248,7 +248,7 @@ def short_name(engine, newest, lang="en"): def columns_legend(short, engines, lang): """The sentence that has to accompany short column names. - Shortening a header is only safe if the identity it dropped is still stated + Shortening a header is only safe if the identity it dropped is still stated somewhere the reader will see. This is that somewhere, generated from the same mapping the table uses so the two cannot disagree.""" parts = [] @@ -281,8 +281,7 @@ def headline(cells, baseline, lang): w = HEADLINE_WHAT[lang] # The real workload only: a headline table that silently mixed a generated # fixture into it would be comparing two different questions. - # - # ⚠️ THE DISCRIMINATOR IS THE FIXTURE NAME, NOT THE VARIANT. `variant` is + # # THE DISCRIMINATOR IS THE FIXTURE NAME, NOT THE VARIANT. `variant` is # whatever the cell declared, and the same real project is labelled `modules` # in one published run and `native` in another — so filtering on `native` # silently dropped every cmake and xmake column and the table rendered with @@ -300,16 +299,14 @@ def headline(cells, baseline, lang): newest = next((e.split("+", 1)[0] for e in engines if e.startswith("mcpp@")), "") short = {e: short_name(e, newest, lang) for e in engines} - # ⚠️ A COLD BUILD THAT IS NOT SLOWER THAN A NO-OP DID NOT BUILD ANYTHING. - # - # Not a hypothetical: rendering the previously published files produced - # xmake cold 0.60s · 153.1x + # A COLD BUILD THAT IS NOT SLOWER THAN A NO-OP DID NOT BUILD ANYTHING. + # # Not a hypothetical: rendering the previously published files produced + # xmake cold 0.60s · 153.1x # next to cmake's 92s — xmake was resolving `--buildir` relative to `-P` and # configuring into a directory that was already populated, so it exited # having compiled nothing. The number was real, the measurement was not, and # in a headline table it reads as xmake being 153x faster than cmake. - # - # This is the whole failure mode of the suite in one cell, so it BLOCKS + # # This is the whole failure mode of the suite in one cell, so it BLOCKS # rather than warns: a table nobody can publish is better than one that is # wrong in the reader's favour. suspect = [] @@ -373,7 +370,7 @@ def main(argv): elif a == "--headline": mode = "headline" elif a == "--allow-suspect": - ALLOW_SUSPECT[0] = True + ALLOW_SUSPECT[0] = True elif a == "--lang": lang = next(it, "en") else: diff --git a/mcpp.toml b/mcpp.toml index 40ce7513..d5760314 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -22,7 +22,7 @@ default-profile = "release" # and the full e2e suite, on Linux (detach-codegen, gcc) and on macOS and Windows # (two-phase, clang) — three platforms, every PR. # -# ⚠️ THAT SAFETY NET IS NOT DECORATION — it has already caught one defect. The +# THAT SAFETY NET IS NOT DECORATION — it has already caught one defect. The # windows-host cross build to x86_64-linux-musl failed on the BMI edge with # # failed: gcm.cache/mcpp.libs.json.gcm diff --git a/scripts/aur/reconcile_mcpp_bin.py b/scripts/aur/reconcile_mcpp_bin.py index 99cbd401..869a160d 100755 --- a/scripts/aur/reconcile_mcpp_bin.py +++ b/scripts/aur/reconcile_mcpp_bin.py @@ -1050,7 +1050,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: parser.add_argument("--arch-image", default=ARCH_IMAGE) parser.add_argument("--push-attempts", type=int, default=4) parser.add_argument("--push-base-delay", type=float, default=5.0) - # ⚠️ 12 x 5s = 60s WAS TOO SHORT, AND THE FAILURE IT PRODUCED WAS A LIE + # 12 x 5s = 60s WAS TOO SHORT, AND THE FAILURE IT PRODUCED WAS A LIE # ABOUT WHOSE FAULT IT WAS. # # This poll runs AFTER the git push has succeeded, so by the time it starts diff --git a/tools/dev-mcpp-path.sh b/tools/dev-mcpp-path.sh index 8ff02199..a05db41d 100755 --- a/tools/dev-mcpp-path.sh +++ b/tools/dev-mcpp-path.sh @@ -4,7 +4,7 @@ # `find … | head -1` answers a build you did not make — this repository has # been bitten by that shape repeatedly. The marker makes "which build" a fact. # -# ⚠️ RUN THE E2E SUITE AGAINST THIS PATH, NOT AGAINST A COPY. mcpp resolves +# RUN THE E2E SUITE AGAINST THIS PATH, NOT AGAINST A COPY. mcpp resolves # MCPP_HOME from the binary's own location, so a binary copied elsewhere is a # different binary as far as several tests are concerned — # `30_dev_binary_home.sh` fails on a copy and passes in place, which reads as a diff --git a/tools/lint-ci-assertions.sh b/tools/lint-ci-assertions.sh index efb200a0..120d9df8 100755 --- a/tools/lint-ci-assertions.sh +++ b/tools/lint-ci-assertions.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # lint-ci-assertions — where an assertion is written, checked mechanically. # -# ⚠️ THIS EXISTS BECAUSE THE SAME MISTAKE WAS MADE THREE TIMES, AND EACH TIME +# THIS EXISTS BECAUSE THE SAME MISTAKE WAS MADE THREE TIMES, AND EACH TIME # THE TEST WAS GREEN. # # 1. (openarch 0.4.0) A template check sat in a job that installs no @@ -24,7 +24,7 @@ # One shape: an assertion must live where the phenomenon it claims can be # observed, and a matrix ROW is a separate observation environment from its job. # -# ⚠️⚠️ WHAT THIS CANNOT DO, STATED FIRST SO IT IS NOT DISCOVERED LATER. +# WHAT THIS CANNOT DO, STATED FIRST SO IT IS NOT DISCOVERED LATER. # # Case 1 above is NOT detectable here, and the attempt to detect it is what # taught the limit. Its defect was an assertion that was ABSENT — the step @@ -34,7 +34,7 @@ # same SHAPE going forward (a run assertion placed where nothing can run), which # is the most this mechanism can offer. # -# ⚠️ WARNINGS, NOT FAILURES. These rules have real false positives — a control +# WARNINGS, NOT FAILURES. These rules have real false positives — a control # step deliberately pins one target; some greps legitimately expect empty output # from a tool that cannot fail. The value here is being READ. A hard gate would # be routed around with a suppression within a month. @@ -84,7 +84,7 @@ for f in "${FILES[@]}"; do # "nothing was wrong". Discarding stderr is what makes the two # indistinguishable. # - # ⚠️ NARROWED TO VERDICTS-BY-ABSENCE, and the narrowing came from running it. + # NARROWED TO VERDICTS-BY-ABSENCE, and the narrowing came from running it. # `cmd 2>/dev/null | grep -q .` looks identical to a machine and is the # OPPOSITE thing — it asserts the output is non-empty, which is what this rule # asks for. Only `-v` (everything except the expected), `-c` (a count that is @@ -102,7 +102,7 @@ for f in "${FILES[@]}"; do # artifact (`mcpp run --target <...>-none-<...>`) or an emulator binary # directly, some step in the SAME job must install one. # - # ⚠️ Restricted to freestanding targets, and the restriction came from running + # Restricted to freestanding targets, and the restriction came from running # it: a plain `mcpp run` on a hosted target needs no emulator and fired on six # jobs that were entirely correct. A rule with that hit rate is not read. awk -v file="$f" ' @@ -151,7 +151,7 @@ if [ -f "$PAIRS" ]; then sed -n "/$(printf '%s' "$marker" | sed 's/[]\/$*.^[]/\\&/g')/,\$p" "$a" > "$ta" sed -n "/$(printf '%s' "$marker" | sed 's/[]\/$*.^[]/\\&/g')/,\$p" "$b" > "$tb" if [ ! -s "$ta" ] || [ ! -s "$tb" ]; then - # ⚠️ An absent marker would make both sides empty and the comparison + # An absent marker would make both sides empty and the comparison # would pass — the emptiness-as-success shape this file exists to name. note "$PAIRS" 0 R2 "marker '$marker' not found in '$a' or '$b'; the comparison would have passed on two empty texts" elif ! diff -q "$ta" "$tb" > /dev/null 2>&1; then From 3306ac67bc281801a88cad9b5d444591d91a3e2c Mon Sep 17 00:00:00 2001 From: speak-agent Date: Sun, 6 Sep 2026 00:08:55 +0800 Subject: [PATCH 2/6] fix(build): the fast path compares every declared build-program input `rerun_if_changed("data/table.csv")` states that the program must run again when that file's content changes. The project-level fast path skips prepare_build when no source is newer than build.ninja, and prepare_build is where the program's cache is read; the check the fast path did run asked only about glob path sets. A data file is neither under src/ nor named with a C++ extension, so the mtime sweep cannot see it either. Editing it therefore left the previous run's generated header in place: `Finished dev in 0.00s`, and the program compiled the previous bytes. The fast path now compares the three kinds of input the cache records: a glob's path set, a declared file's content hash, and a declared environment variable's value. `glob_inputs_stale` is renamed `program_inputs_stale` because the name was the reason the other two were never asked about. e2e 612 uses the program's output as its criterion -- a stale header and a fresh one make the binary print different strings -- and runs the other half of the control: with nothing touched, the next build still takes the fast path, which "always rebuild" would also pass the first assertion with. Verified failing on 2026.9.5.3 and passing here. Found by mcpp.tools.embed, the first non-rule member of mcpp:plugins: it writes a data file into a header while the build program runs, so it submits no action and falls entirely on this path. Documentation for both languages, CHANGELOG, and version 2026.9.5.4. --- CHANGELOG.md | 23 +++++ docs/07-build-mcpp.md | 11 +++ docs/zh/07-build-mcpp.md | 7 ++ mcpp.toml | 2 +- modules/versioning/src/version.cppm | 2 +- src/build/build_program.cppm | 32 +++++-- src/build/execute.cppm | 7 +- .../612_build_program_file_input_fast_path.sh | 96 +++++++++++++++++++ tests/unit/test_source_kind.cpp | 4 +- 9 files changed, 172 insertions(+), 12 deletions(-) create mode 100755 tests/e2e/612_build_program_file_input_fast_path.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index ede4e262..ca14d7cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,29 @@ ## [Unreleased] +## [2026.9.5.4] — 2026-09-06 + +### 构建程序声明的文件输入,快路径此前不比较 + +`rerun_if_changed("data/table.csv")` 声明的是「这个文件的内容变了就重跑」。工程级快路径 +在没有任何**源文件**比 `build.ninja` 新时跳过 `prepare_build`,而构建程序缓存正是在 +`prepare_build` 里被读取的;快路径自己只问过 glob 输入的**路径集合**(#359),没问过声明 +文件的内容。数据文件既不在 `src/` 下也没有 C++ 扩展名,mtime 扫描看不见它,于是改了数据、 +`mcpp build` 打印 `Finished dev in 0.00s`、程序里编进去的还是上一次的字节。 + +快路径现在按缓存里记录的方式比较三类构建程序输入:glob 的路径集合、声明文件的内容哈希、 +声明环境变量的值。e2e 612 用程序的**输出**做判据(陈旧的头文件与新的头文件让二进制打印 +不同的字符串),并跑对照的另一半:输入没变时第二次构建仍然走快路径,否则「总是重建」也能 +让第一条断言通过。发现它的是 `mcpp.tools.embed`——mcpp-plugins 0.1.1 里第一个非规则成员, +它把数据文件写成头文件,没有 action 可提交,所以完全落在这条路径上。 + +### 注释与文档里不再有装饰性符号 + +2026.9.5.3 的清理覆盖了 `docs/`、`README.md`、CHANGELOG、引擎源码、测试、示例与工作流 +文件,没有覆盖 `bench/`、`tools/`、`scripts/`、`mcpp.toml` 与 `README.zh-CN.md`。程序 +输出保留原样:用户在终端上读到的一行既不是文档也不是注释。`README.zh-CN.md` 同时补上英文 +版已有的 Cortex-M 行,状态列改用与英文版相同的词。 + ## [2026.9.5.3] — 2026-09-05 ### 官方构建插件集中为一个包:`mcpp:plugins` diff --git a/docs/07-build-mcpp.md b/docs/07-build-mcpp.md index a552d875..9cdf7392 100644 --- a/docs/07-build-mcpp.md +++ b/docs/07-build-mcpp.md @@ -348,6 +348,17 @@ paths** and nothing else: The build output tree and `.git` are never part of the set, so a wide pattern cannot make the program re-run forever against its own outputs. +**Every declared input is compared on the fast path too** (2026.9.5.4+). A +project whose sources are all older than `build.ninja` takes a fast path that +skips the phase where the program's cache is normally consulted, and until +2026.9.5.4 that path asked only about glob path sets. A data file a program +reads is neither under `src/` nor named with a C++ extension, so the mtime +sweep cannot see it either: editing it left the previous run's output in place +and the build reported `Finished dev in 0.00s`. The fast path now compares what +the cache records — a glob's path set, a declared file's content hash, and a +declared environment variable's value — so `rerun_if_changed` means the same +thing under both paths. + ### Declaring work instead of doing it: `mcpp::action` (2026.8.5.1+) Generating a source by writing it *here* is the easy path and the wrong one diff --git a/docs/zh/07-build-mcpp.md b/docs/zh/07-build-mcpp.md index 61ec3d72..f2ffb988 100644 --- a/docs/zh/07-build-mcpp.md +++ b/docs/zh/07-build-mcpp.md @@ -305,6 +305,13 @@ int main() { 构建输出目录与 `.git` 永远不进入集合,因此再宽的模式也不会让程序对着自己的产物 无限重跑。 +**声明过的输入在快路径上同样被比较**(2026.9.5.4+)。当所有源文件都不比 +`build.ninja` 新时,工程走快路径,跳过读取构建程序缓存的那个阶段;在 2026.9.5.4 +之前,快路径只问 glob 的路径集合。数据文件既不在 `src/` 下也没有 C++ 扩展名,mtime +扫描同样看不见它:改了它,上一次的产出原样留着,构建打印 `Finished dev in 0.00s`。 +现在快路径按缓存记录的方式比较三类输入 —— glob 的路径集合、声明文件的内容哈希、 +声明环境变量的值 —— 因此 `rerun_if_changed` 在两条路径上含义相同。 + ### 声明产出而非执行动作:`mcpp::action`(2026.8.5.1+) 在**这里**直接把源码写出来是省事的路,超过一定规模就是错的:它每次 prepare 跑 diff --git a/mcpp.toml b/mcpp.toml index d5760314..01abd145 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.9.5.3" +version = "2026.9.5.4" 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 a09cf03c..bf3c90f8 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.5.3"; +inline constexpr std::string_view MCPP_VERSION = "2026.9.5.4"; } // namespace mcpp diff --git a/src/build/build_program.cppm b/src/build/build_program.cppm index 5c8ead7d..512c42e4 100644 --- a/src/build/build_program.cppm +++ b/src/build/build_program.cppm @@ -255,8 +255,9 @@ std::expected run_build_program( const mcpp::manifest::CppStandardConfig& cppStandard, const BuildProgramEnv& env); -// #359: has any recorded glob input's path SET changed since its build.mcpp -// cache was written? +// Has any recorded build-program input changed since its build.mcpp cache was +// written: a glob's path SET (#359), a declared file's CONTENT, or a declared +// environment variable's value? // // The project-level fast path skips prepare_build entirely when no source is // newer than build.ninja, and prepare is where the build.mcpp cache is @@ -267,10 +268,19 @@ std::expected run_build_program( // the build.mcpp source itself; a glob is one more kind of build-program input, // so it belongs to the same question. // +// A DECLARED FILE IS THE SAME QUESTION AND WAS NOT ASKED (2026.9.5.4). The +// mtime sweep that guards the fast path walks SOURCES, so a data file a build +// program reads -- `rerun_if_changed("data/table.csv")` -- is invisible to it, +// and this function used to skip a cache record that carried no glob at all. +// The result was that editing such a file left the generated header from the +// previous build in place: `Finished dev in 0.00s`, and the program compiled +// the previous bytes. `mcpp.tools.embed` is the case that found it. Contents, +// not mtime, exactly as the cache records them. +// // Scans the caches under `/target/.build-mcpp` (the root's own and -// each dependency's). Each cache records the root its globs were relative to, -// so a dependency's glob is evaluated against the dependency's tree. -bool glob_inputs_stale(const std::filesystem::path& projectRoot); +// each dependency's). Each cache records the root its entries were relative to, +// so a dependency's input is evaluated against the dependency's tree. +bool program_inputs_stale(const std::filesystem::path& projectRoot); } // namespace mcpp::build @@ -1199,7 +1209,7 @@ std::expected run_build_program( return {}; } -bool glob_inputs_stale(const fs::path& projectRoot) { +bool program_inputs_stale(const fs::path& projectRoot) { std::error_code ec; const fs::path base = projectRoot / "target" / ".build-mcpp"; if (!fs::exists(base, ec)) return false; @@ -1216,11 +1226,19 @@ bool glob_inputs_stale(const fs::path& projectRoot) { if (it.depth() >= 3) { it.disable_recursion_pending(); continue; } if (it->path().filename() != "build.mcpp.cache") continue; auto rec = read_cache(it->path().parent_path()); - if (!rec.loaded || rec.globs.empty() || rec.rootPath.empty()) continue; + if (!rec.loaded || rec.rootPath.empty()) continue; fs::path recRoot{rec.rootPath}; auto outName = output_dir_name(recRoot, it->path().parent_path()); for (auto const& [h, pattern] : rec.globs) if (dirs::glob_fingerprint(recRoot, pattern, outName) != h) return true; + // The same comparison `cache_fresh` makes when prepare_build runs. It + // is repeated here rather than shared because the fast path has no + // manifest, no toolchain and no context hash -- only the recorded + // entries and the tree they were measured against. + for (auto const& [h, rel] : rec.inputs) + if (mcpp::toolchain::hash_file(abs_against_root(recRoot, rel)) != h) return true; + for (auto const& [h, name] : rec.envs) + if (mcpp::toolchain::hash_string(env_value(name)) != h) return true; } return false; } diff --git a/src/build/execute.cppm b/src/build/execute.cppm index 89853d12..b8b49561 100644 --- a/src/build/execute.cppm +++ b/src/build/execute.cppm @@ -918,7 +918,12 @@ bool sources_newer_than(const std::filesystem::path& projectRoot, // sweep therefore cannot see it, and the fast path would report // "Finished dev in 0.00s" while the new file is never generated. Same // question as the build.mcpp check above, different kind of input. - if (mcpp::build::glob_inputs_stale(projectRoot)) return true; + // + // 2026.9.5.4: the same call now also compares a declared FILE's content and + // a declared environment variable's value. The sweep below walks sources, + // so a data file a build program reads is invisible to it for the same + // reason a new .proto is. + if (mcpp::build::program_inputs_stale(projectRoot)) return true; // mcpp#365: an author-written `.rc` is a third input of the same kind. It // is not under src/ and has no C++ extension, so the sweep below cannot see // it — and unlike the icon or a header the script includes, editing it can diff --git a/tests/e2e/612_build_program_file_input_fast_path.sh b/tests/e2e/612_build_program_file_input_fast_path.sh new file mode 100755 index 00000000..c94eb208 --- /dev/null +++ b/tests/e2e/612_build_program_file_input_fast_path.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# requires: gcc +# 612_build_program_file_input_fast_path.sh — editing a file a build program +# declared with `rerun_if_changed` rebuilds, even on the fast path +# (mcpp 2026.9.5.4+). +# +# The fast path skips prepare_build when no SOURCE is newer than build.ninja, +# and prepare_build is where the build.mcpp cache is consulted. A data file is +# not a source: it has no C++ extension and does not live under src/, so the +# mtime sweep cannot see it, and the staleness check the fast path did run +# asked only about glob path SETS. Editing the file therefore left the header +# generated by the previous run in place, `mcpp build` reported +# "Finished dev in 0.00s", and the program kept compiling the previous bytes. +# +# The criterion is the program's OUTPUT, not a line of build log: a stale +# header and a fresh one differ in what the binary prints. +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT +cd "$TMP" + +"$MCPP" new probe > /dev/null; cd probe +rm -f src/*.cppm +mkdir -p data +printf 'first\n' > data/value.txt + +cat > mcpp.toml <<'EOT' +[package] +name = "probe" +version = "0.1.0" +[language] +standard = "c++23" +modules = true +import_std = true + +[targets.probe] +kind = "bin" +main = "src/main.cpp" +EOT + +# A build program that reads a data file and writes a header from it. No +# action is submitted: the work happens while the program runs, which is the +# shape `mcpp.tools.embed` has and the shape the fast path used to skip. +cat > build.mcpp <<'EOT' +import std; +import mcpp; +int main() { + const std::filesystem::path root(mcpp::manifest_dir()); + const std::filesystem::path gen(std::string(mcpp::out_dir()) + "/include"); + std::error_code ec; + std::filesystem::create_directories(gen, ec); + std::ifstream in(root / "data" / "value.txt"); + std::string value; + std::getline(in, value); + std::ofstream out(gen / "value.h", std::ios::trunc); + out << "#pragma once\n#define PROBE_VALUE \"" << value << "\"\n"; + mcpp::rerun_if_changed((root / "data" / "value.txt").string().c_str()); + mcpp::include_dir(gen.string().c_str()); + return 0; +} +EOT + +cat > src/main.cpp <<'EOT' +#include +#include "value.h" +int main() { std::printf("VALUE=%s\n", PROBE_VALUE); } +EOT + +value() { "$MCPP" run 2>&1 | grep '^VALUE=' | tail -1; } + +"$MCPP" build > b1.log 2>&1 || { cat b1.log; echo "FAIL: the first build failed"; exit 1; } +[[ "$(value)" == "VALUE=first" ]] || { echo "FAIL: the first build did not embed the first value"; exit 1; } + +# Content only. The path set is unchanged, so a glob fingerprint cannot see +# this, and no source's mtime moves. +printf 'second\n' > data/value.txt + +"$MCPP" build > b2.log 2>&1 || { cat b2.log; echo "FAIL: the second build failed"; exit 1; } +out="$(value)" +[[ "$out" == "VALUE=second" ]] || { + echo "FAIL: a declared file input changed and the build replayed the old header ($out)" + cat b2.log + exit 1 +} +echo "PASS: a declared file input is compared on the fast path" + +# The mirror half: an unchanged input must NOT force work. Two builds in a row +# with nothing touched, and the second one has to take the fast path -- without +# this the fix could be "always rebuild", which passes the assertion above and +# is not the behaviour asked for. +"$MCPP" build > b3.log 2>&1 +grep -qE 'Finished .* in ' b3.log || { cat b3.log; echo "FAIL: the third build did not finish"; exit 1; } +"$MCPP" build > b4.log 2>&1 +grep -q 'Compiling probe' b4.log && { cat b4.log; echo "FAIL: an unchanged input recompiled the project"; exit 1; } +echo "PASS: an unchanged input still takes the fast path" diff --git a/tests/unit/test_source_kind.cpp b/tests/unit/test_source_kind.cpp index 30ef3f89..914e90d8 100644 --- a/tests/unit/test_source_kind.cpp +++ b/tests/unit/test_source_kind.cpp @@ -116,7 +116,7 @@ TEST(SourceKind, PredicatesAgreeWithTheKind) { // Assembly is absent on purpose — it has no import and no scanned include // graph, so editing one changes its object (ninja tracks that) and nothing // else. A NEW assembly file is a different question, answered by - // glob_inputs_stale. + // program_inputs_stale. EXPECT_TRUE(mcpp::affects_graph_shape(SourceKind::ModuleInterface)); EXPECT_TRUE(mcpp::affects_graph_shape(SourceKind::Cxx)); EXPECT_TRUE(mcpp::affects_graph_shape(SourceKind::C)); @@ -305,7 +305,7 @@ TEST(SourceKind, DevicePredicates) { EXPECT_FALSE(mcpp::is_cxx_like(SourceKind::Device)); EXPECT_FALSE(mcpp::links_unconditionally(SourceKind::Device)); // Absent for the same reason assembly is absent: the content change is - // tracked by ninja, and a NEW file is `glob_inputs_stale`'s question. + // tracked by ninja, and a NEW file is `program_inputs_stale`'s question. EXPECT_FALSE(mcpp::affects_graph_shape(SourceKind::Device)); EXPECT_EQ(mcpp::to_string(SourceKind::Device), "device"); } From fe6cb32049d402da8ce312072bd8568e2b921461 Mon Sep 17 00:00:00 2001 From: speak-agent Date: Sun, 6 Sep 2026 00:14:24 +0800 Subject: [PATCH 3/6] fix(prepare): a version conflict on a package with no C++ module says so Two versions of one package are reconciled by mangling the secondary copy's module names. The two branches that refuse name both versions and who asked for them; the third reported only that the package declares no named C++ module to rewrite, which is a true statement about a package the reader never asked to be staged and says nothing about the conflict that brought it there. A C package reaches it: compat.vulkan-runtime compiles one C file, so a manifest that pins one version of it while a dependency asks for another got a message about modules. It now names both versions, both requesters, and what to do. examples/10 moves its pin to compat.vulkan-runtime 2026.09.06, the version compat.vulkan asks for since mcpp-index#350. The example is the consumer that found this. --- .../docs/2026-09-05-heterogeneous-verify.sh | 15 ++++++++++--- examples/10-vulkan-compute/app/mcpp.toml | 2 +- src/build/prepare.cppm | 22 ++++++++++++++++++- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/.agents/docs/2026-09-05-heterogeneous-verify.sh b/.agents/docs/2026-09-05-heterogeneous-verify.sh index 568881ff..0f7b835e 100644 --- a/.agents/docs/2026-09-05-heterogeneous-verify.sh +++ b/.agents/docs/2026-09-05-heterogeneous-verify.sh @@ -31,9 +31,11 @@ got=$("$STORE" --version 2>&1 | head -1) # value was written, so both tools are asked what they hold afterwards. "$STORE" self config --mirror "${MCPP_VERIFY_MIRROR:-CN}" >/dev/null 2>&1 || true "$XL" config --mirror "${MCPP_VERIFY_MIRROR:-CN}" >/dev/null 2>&1 || true -# Both tools print their configuration banner on stderr, so a `2>/dev/null` -# here reads an empty string and reports a mirror that is in fact set. -xm=$("$XL" config 2>&1 | sed 's/\x1b\[[0-9;]*m//g' | grep -i 'mirror' | head -1 | awk '{print $NF}') +# THE SETTING, NOT THE TOOL'S OUTPUT. `xlings config` renders a banner through +# its ui layer, which prints nothing when its stdout is a pipe -- so a command +# substitution reads an empty string and reports a mirror that is in fact set. +# The file the tool writes answers the same question and cannot be suppressed. +xm=$(python3 -c "import json,os;print(json.load(open(os.path.expanduser('~/.xlings/.xlings.json'))).get('mirror',''))" 2>/dev/null) [ "$xm" = "${MCPP_VERIFY_MIRROR:-CN}" ] && ok "xlings mirror is $xm" || fail "xlings mirror is '$xm', not ${MCPP_VERIFY_MIRROR:-CN}" mm=$("$STORE" self config 2>&1 | sed 's/\x1b\[[0-9;]*m//g' | grep -i 'mirror' | head -1 | awk '{print $NF}') [ -n "$mm" ] && ok "mcpp mirror is $mm" || printf 'note: mcpp self config does not print its mirror (%s)\n' "$("$STORE" self config 2>&1 | head -1)" @@ -142,6 +144,13 @@ if [ -n "$SRC" ] && [ -d "$SRC/examples/10-vulkan-compute/app" ]; then || fail "example 10 --no-accel: $(printf '%s' "$out" | tail -3 | tr '\n' ' ')" hs=$(ls "$HOME"/.mcpp/registry/data/xpkgs/compat-x-vulkan-runtime/*/mcpp_generated/vulkan_runtime/HOST-SURFACE.txt \ "$ex10"/.mcpp/.xlings/data/xpkgs/compat-x-vulkan-runtime/*/mcpp_generated/vulkan_runtime/HOST-SURFACE.txt 2>/dev/null | head -1) + # WHICH ADAPTER WROTE IT. The substitution criteria below are properties of + # 2026.09.06, the version that declares its payload set; an older report + # carries neither the declarations nor the class they produce, so both + # would pass on an empty finding. The version is a path segment. + adapter=$(printf '%s' "$hs" | sed -n 's#.*/compat-x-vulkan-runtime/\([^/]*\)/.*#\1#p') + [ "$adapter" = "${MCPP_VERIFY_ADAPTER:-2026.09.06}" ] && ok "the farm was written by compat.vulkan-runtime $adapter" \ + || fail "the farm was written by compat.vulkan-runtime '$adapter', not ${MCPP_VERIFY_ADAPTER:-2026.09.06}" if [ -n "$hs" ]; then printf -- '--- %s\n' "$hs"; sed -n '/^## farmed/,$p' "$hs" | head -60 # WHAT A SANDBOX CAN AND CANNOT ASSERT. A subos shares the host's diff --git a/examples/10-vulkan-compute/app/mcpp.toml b/examples/10-vulkan-compute/app/mcpp.toml index e67ec3da..9c0d43bd 100644 --- a/examples/10-vulkan-compute/app/mcpp.toml +++ b/examples/10-vulkan-compute/app/mcpp.toml @@ -22,7 +22,7 @@ plugins = { version = "0.1.0", features = ["rules-spirv"], host-module = true } # below is a payload and the hardware ones are the host's. [dependencies.compat] vulkan = "1.4.357.0" -vulkan-runtime = "2026.09.05" +vulkan-runtime = "2026.09.06" # The two payloads this build names rather than discovers: the shader compiler, # and a Vulkan driver that is always present because it is the CPU. With diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 76613465..982ec03d 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -5554,7 +5554,27 @@ prepare_build(bool print_fingerprint, // same map in both the secondary and its consumer. auto moduleNames = declared_modules_for( secondaryRoot, secondaryManifest); - if (!moduleNames) return std::unexpected(moduleNames.error()); + // The two branches above name both versions and who asked + // for them; this one used to report only that the package + // declares no named C++ module, which is a true statement + // about a package the reader never asked to be staged. A + // C package -- compat.vulkan-runtime is one -- reaches + // here whenever a manifest pins one version of it and + // another dependency asks for a second, and the message + // has to say that before it says anything about modules. + if (!moduleNames) return std::unexpected(std::format( + "dependency '{}{}{}' has irreconcilable versions:\n" + " '{}' requested by '{}'\n" + " '{}' requested by '{}'\n" + "Multi-version mangling cannot separate them: {}.\n" + "A package with no named C++ module has nothing to " + "rewrite, so the two requests must agree. Align the " + "pin in your mcpp.toml with the version the other " + "dependency asks for.", + key.ns, key.ns.empty() ? "" : ".", key.shortName, + it->second.version, it->second.requestedBy, + spec.version, item.requestedBy, + moduleNames.error())); std::map rename; for (auto const& module : *moduleNames) { rename.emplace(module, From 1ce5be17aa1e2bf8a19f4b7ff2bd7d98d4a7ca4a Mon Sep 17 00:00:00 2001 From: speak-agent Date: Sun, 6 Sep 2026 00:18:28 +0800 Subject: [PATCH 4/6] fix(examples): 10 pins compat.vulkan-runtime 2026.09.07 The version compat.vulkan asks for since mcpp-index#351, where a soname carried by two installed payloads is decided by symbol coverage rather than by which store path sorts last. --- examples/10-vulkan-compute/app/mcpp.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/10-vulkan-compute/app/mcpp.toml b/examples/10-vulkan-compute/app/mcpp.toml index 9c0d43bd..9049704a 100644 --- a/examples/10-vulkan-compute/app/mcpp.toml +++ b/examples/10-vulkan-compute/app/mcpp.toml @@ -22,7 +22,7 @@ plugins = { version = "0.1.0", features = ["rules-spirv"], host-module = true } # below is a payload and the hardware ones are the host's. [dependencies.compat] vulkan = "1.4.357.0" -vulkan-runtime = "2026.09.06" +vulkan-runtime = "2026.09.07" # The two payloads this build names rather than discovers: the shader compiler, # and a Vulkan driver that is always present because it is the CPU. With From 77549224793b453b1f3d7a72a3eab2cd38fc0520 Mon Sep 17 00:00:00 2001 From: speak-agent Date: Sun, 6 Sep 2026 00:24:50 +0800 Subject: [PATCH 5/6] docs(plan): the second round's rows, and the two items the first document had left open The heterogeneous-build plan gains the rows for this round: the declared payload set and the candidate walk in the two runtime adapters, the fast path comparing every declared build-program input, the diagnostic for a version conflict on a package with no C++ module, and the first member of the tools half of mcpp:plugins. The multi-device document's section 12.4 listed pocl/lavapipe and the rule package in the index as not done. Both are done, and neither took more hours: conda-forge publishes Linux binaries for the two drivers, so the work was repacking and closure verification rather than building mesa, and the rules moved to their own repository and are published as one package whose members features select. --- ...heterogeneous-build-ecosystem-design-v2.md | 19 +++++++++++++- ...026-09-05-multi-device-ecosystem-design.md | 26 +++++++++++++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/.agents/docs/2026-09-05-heterogeneous-build-ecosystem-design-v2.md b/.agents/docs/2026-09-05-heterogeneous-build-ecosystem-design-v2.md index e1db7c4a..d966fafc 100644 --- a/.agents/docs/2026-09-05-heterogeneous-build-ecosystem-design-v2.md +++ b/.agents/docs/2026-09-05-heterogeneous-build-ecosystem-design-v2.md @@ -332,7 +332,8 @@ Status is one of `done`, `open`, `deferred (reason)`. | I3 | `compat.opencl-headers`, `compat.opencl` verified with a probe (`tests/examples/opencl`, a workspace member); `compat.opencl-runtime` 2026.09.05 farms the libraries the host manifests name, their closure and the vendor family, prefers payloads, records the surface; payload entries of `OCL_ICD_FILENAMES` are left to the payload | the probe enumerates the NVIDIA platform on this machine and zero platforms on a runner; the pocl platform once X2 is installed | X2 for the pocl half | done for the host half: `tests/examples/opencl` lists `NVIDIA CUDA / RTX 4080` here and zero platforms on the Linux runner; with `OCL_ICD_FILENAMES=/lib/libpocl.so` the same loader lists `Portable Computing Language` and `NVIDIA CUDA` in one process | | I4 | CI green; merge; index artifact published | `Publish Index Artifact` green on the merge commit | I1–I3 | done: PR #349 green (10 pass; the `full sweep` job skips by design), merged as 754d775, `Publish Index Artifact` green on it (run 33968256961) | -| I5 | `compat.vulkan-runtime` 2026.09.06: the payload set is declared as `xpm.linux.deps.runtime` rather than discovered in the store; `PAYLOAD_PACKAGES` maps each soname to the package declared for it and reports a declaration that did not take effect; both adapters refuse a payload built for another machine (ELF `e_machine`); the Vulkan adapter gains the aarch64 multiarch directories | a fresh environment substitutes the same set a developer machine does; the report contains no line saying a declaration did not take effect | I4, X5 | see section 6.4 | +| I5 | `compat.vulkan-runtime` 2026.09.06: the payload set is declared as `xpm.linux.deps.runtime` rather than discovered in the store; `PAYLOAD_PACKAGES` maps each soname to the package declared for it and reports a declaration that did not take effect; both adapters refuse a payload built for another machine (ELF `e_machine`); the Vulkan adapter gains the aarch64 multiarch directories | a fresh environment substitutes the same set a developer machine does; the report contains no line saying a declaration did not take effect | I4, X5 | done: merged as 0bbf3ad, index artifact published; measured in a fresh subos, 24 payload substitutions against 1, and four host sonames without a payload against thirty | +| I6 | Both adapters 2026.09.07: a soname carried by more than one installed payload is decided by symbol coverage rather than by which store path sorts last; `compat.vulkan` and `compat.opencl` move their pins | in a fresh subos, `libX11.so.6` comes from `xim:libX11` rather than staying on the host with `xim:mesa-lavapipe`'s bundled copy named as the reason | I5 | PR #351 | #### xim-pkgindex (PR #762) @@ -345,6 +346,22 @@ Status is one of `done`, `open`, `deferred (reason)`. | X4 | `xim:mesa` gains the Intel Vulkan driver | on an Intel machine `HOST-SURFACE.txt` has no driver entry | libclc and SPIRV-LLVM-Translator payloads | deferred: anv requires `intel_clc`, which needs a libclc and clang chain this index does not publish yet; the chain is a separate packaging round and is recorded here rather than approximated | | X5 | CI green; merge; index artifact published | `Publish Index Artifact` green on the merge commit | X1–X3 | done: PR #762 green on its pull_request-event runs (the push-event `linux-install-test` sees only the last push and fails on libbsd by construction, as the workflow header states), merged as 2d2a2ee; `Publish Index Artifact` green on it (run 33967729241) | +#### mcpp, second round (PR #570, version 2026.9.5.4) + +| # | task | criterion | depends on | status | +|---|---|---|---|---| +| M7 | The fast path compares every declared build-program input, not only glob path sets | e2e 612: editing a file declared with `rerun_if_changed` changes what the binary prints, and an unchanged input still takes the fast path | — | done: verified failing on 2026.9.5.3 and passing on this branch | +| M8 | A version conflict on a package with no named C++ module names both versions and both requesters | the message example 10 produced now says which two versions are in the graph | — | done | +| M9 | The markers removed from `bench/`, `tools/`, `scripts/`, `mcpp.toml` and `README.zh-CN.md`; the Chinese target table gains the row and the words its English counterpart has | a sweep over the emoji ranges returns nothing outside program output | — | done | +| M10 | Release 2026.9.5.4, mirror, index bump, bootstrap pin | both mirrors return 200 and identical bytes; xim-pkgindex names it as `latest` | M7-M9 | open | + +#### mcpp-plugins, second round (PR #2, version 0.1.1) + +| # | task | criterion | depends on | status | +|---|---|---|---|---| +| P4 | `mcpp.tools.embed`, the first member of the tools half, selected by `tools-embed` | the fixture activating only that feature embeds a data file, and its second run proves an edit reaches the binary | M7 | done, CI pending on the 2026.9.5.4 release | +| P5 | The markers removed from the rule sources; version 0.1.1; release and mirror; index descriptor; the two examples move their pin | the same two URLs return 200 with one sha256 | P4, M10 | open | + #### Verification | # | task | criterion | depends on | status | diff --git a/.agents/docs/2026-09-05-multi-device-ecosystem-design.md b/.agents/docs/2026-09-05-multi-device-ecosystem-design.md index 402cf062..05168185 100644 --- a/.agents/docs/2026-09-05-multi-device-ecosystem-design.md +++ b/.agents/docs/2026-09-05-multi-device-ecosystem-design.md @@ -1238,9 +1238,9 @@ GPU 索引包不自己探测宿主(委托 xim sentinel);链宿主 `libcudart` | 项 | 理由 | |---|---| | T1.2 `llvm-offload` | `dpcpp@7.1.0` 载荷自带全套 offload 工具,需要 RDC 的工程可用它;独立包仍待做 | -| T1.4 / T1.5(pocl / lavapipe)与 T4.2 | 需要**重打 mesa 载荷**(见 12.1 末行),或新建 pocl 源码构建配方。两者都是多小时的载荷工程 | +| ~~T1.4 / T1.5(pocl / lavapipe)~~ | **已做**(2026-09-06,见 12.6)。理由被一条更短的路推翻:两者在 conda-forge 上都有现成的 Linux 二进制,重打载荷因此不是「重新构建 mesa」而是「重打包并核验闭包」 | | T1.8/T1.9/T1.10(chipstar / adaptivecpp / hip) | 依赖 T1.2/T1.4 | -| T4.3 规则包进索引 | 依赖 ③ —— 描述符指向 mcpp 的**源码 tarball**(`grpcgen` 同形),tag 不存在则算不出 sha256。规则包已改名到 `mcpplibs` 命名空间,就是为了让它可被引用而不是被复制 | +| ~~T4.3 规则包进索引~~ | **已做,但形态换了**(2026-09-05/06,见 12.6)。不再指向 mcpp 源码 tarball 的子路径:规则集中到 `mcpp-community/mcpp-plugins`,以一个包 `mcpp:plugins` 发布,成员由 feature 选择。命名空间也随之从 `mcpplibs` 改为 `mcpp` | | ⑤ 九个框架 | 依赖 ④ 的规则包条目。`ggml-org.llamacpp` 与 `opencv.opencv` 已在索引里,多后端是改**它们各自的 `-m` 仓库**而不是索引条目。T5.1 已做到「链路全通、卡在载荷矩阵」—— 见 12.5 | | T2.6 的端到端判据 | `accel` 已是 `pack::AbiTag` 第四维并进指纹;「`.a` 随包传播」还缺一条跨包的判据 | @@ -1274,3 +1274,25 @@ action **只**挂到可执行/共享库/测试上,而 llama.cpp 的 CUDA 后端 `cuda_runtime.h` 同一形状,第三次); **clang 路线要带 `-D_ALLOW_UNSUPPORTED_LIBCPP`**,因为 NVIDIA 那条 `libc++ is not supported` 的守卫看的是 `__CUDACC__`,而 clang 编 CUDA 时自己就定义它。 + +## 12.6 2026-09-06 补记:12.4 里两行被后一轮关掉 + +本文 12.4 记的是 2026-09-05 当天的读数。次日那一轮(设计见 +`2026-09-05-heterogeneous-build-ecosystem-design-v2.md` §6)关掉了其中两行,都不是 +靠更多工时,而是靠换一条更短的路。 + +**T1.4 / T1.5 —— pocl 与 lavapipe 都成了载荷。** 当时写的理由是「需要重打 mesa 载荷 +或新建 pocl 源码构建配方,都是多小时的载荷工程」。这个理由把「载荷」等同于「从源码 +构建」;而 conda-forge 对这两者都发布了 Linux 二进制,于是工作变成重打包并核验闭包。 +`xim:mesa-lavapipe@26.2.1`(x86_64 与 aarch64)与 `xim:pocl@7.1` 已发布, +`xlings install pocl` 之后 Khronos loader 在一个进程里同时列出 +`Portable Computing Language` 与 `NVIDIA CUDA`。 + +**T4.3 —— 规则包进了索引,但不是当时设想的形态。** 当时的方案是让描述符指向 mcpp +源码 tarball 的子路径(`grpcgen` 同形),受阻于「tag 不存在则算不出 sha256」。实际 +落地的是另一种:规则不再住在 mcpp 的 `examples/` 下,而集中到 +`mcpp-community/mcpp-plugins`,以一个包 `mcpp:plugins` 发布,成员由 feature 选择, +命名空间从 `mcpplibs` 改为 `mcpp`。索引里的 `pkgs/m/mcpp.plugins.lua` 指向那个仓库 +自己的 release 资产,sha256 因此是一个已经存在的文件的哈希。 + +其余各行的理由未变。 From c0d52d567f5c559989cf724ed070a78510eb25e9 Mon Sep 17 00:00:00 2001 From: speak-agent Date: Sun, 6 Sep 2026 00:34:54 +0800 Subject: [PATCH 6/6] docs(plan): the C1 row names the module the round settled on --- .../docs/2026-09-05-heterogeneous-build-ecosystem-design-v2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.agents/docs/2026-09-05-heterogeneous-build-ecosystem-design-v2.md b/.agents/docs/2026-09-05-heterogeneous-build-ecosystem-design-v2.md index d966fafc..db6a699f 100644 --- a/.agents/docs/2026-09-05-heterogeneous-build-ecosystem-design-v2.md +++ b/.agents/docs/2026-09-05-heterogeneous-build-ecosystem-design-v2.md @@ -187,7 +187,7 @@ Dependencies run downward; each task states the criterion that decides it. | # | task | criterion | depends on | |---|---|---|---| -| C1 | `mcpp.build.spirv` + `examples/10-vulkan-compute` | three devices, one artifact — **done** | — | +| C1 | `mcpp.build.spirv` + `examples/10-vulkan-compute` (the module is `mcpp.rules.spirv` since round 3; see 6.2) | three devices, one artifact — **done** | — | | C2 | `mcpp.build.sycl` driving the `dpcpp` payload | a SYCL kernel runs on the CUDA backend | — | | C3 | `mcpp.build.hip` | `HIP_PLATFORM=nvidia` kernel runs | `hip-runtime` payload | | C4 | llama.cpp Vulkan lane | tokens on lavapipe with no GPU | C1, and a `glslc` payload or a flag translator |