Skip to content

Commit 476c366

Browse files
2026.9.21.3 — a token that did nothing, a count that was wrong, and a build that measured the dependencies (#682)
* the count was four and it is two, and the grouping used the wrong key THE RE-MEASUREMENT AFTER THE WITHDRAWAL REFUTED A NUMBER THIS REPOSITORY HAD WRITTEN IN FIVE PLACES. `cenv.cppm`, `predefines.cppm`, `docs/21`, `docs/22` and the CHANGELOG all said the borrowed `__CYGWIN__` cost four members: archive, sqlite3, mimalloc, c-ares. The 30-member run on 2026.9.21.2 says otherwise. total failures 10 -> 5 newly failing none the windows.h group 4 -> 2, not 4 -> 0 And the half that did not hold is the useful half, because it says the DENOMINATOR was wrong: archive (xz) __CYGWIN__ correct cleared sqlite3 __CYGWIN__ correct cleared c-ares __CYGWIN__ WRONG still fails mimalloc __CYGWIN__ WRONG still fails c-ares reaches `windows.h` through `#ifdef HAVE_WINDOWS_H`, and that macro is defined by mcpp-index's own recipe in its Windows branch --- a recipe defect, the same shape as curl's `HAVE_LINUX_TCP_H`. mimalloc no longer reaches a header at all. ALL FOUR WERE GROUPED BY THEIR DIAGNOSTIC. Every one of them stopped at `windows.h`, so they were recorded as one cause. GROUPING BY DIAGNOSTIC IS NOT GROUPING BY CAUSE, and a count collected that way overstates what withdrawing a name can fix. This is the mirror of a lesson already in this repository: a conclusion gets re-checked, its REASONS do not. The conclusion --- withdraw the borrowed name --- was right and was re-verified. The number inside the reason was inherited from an earlier record three times over, into engine comments, a changelog and a pull request body, and nothing checked it until the measurement did. AND mimalloc IS A FINDING OF ITS OWN: fatal error: error in backend: Target OS doesn't support __builtin_thread_pointer() yet. `presents = "posix"` on Windows realises as `--target=x86_64-pc-cygwin`, and LLVM does not implement that builtin for that OS. Every previous note about the substitution discussed what the PREPROCESSOR sees --- the ABI unchanged, the link unchanged, only macro visibility different. This is the first measured cost that the CODE GENERATOR sees, and it is recorded rather than fixed: the repair belongs either in mimalloc's recipe or in the choice of substitute triple, and that is a measurement not yet taken. A tooling defect surfaced with it. The measurement recorded mimalloc as `fails: error: build failed`, because `compat.py`'s `first_diagnostic` matched none of its three patterns --- a backend error carries no `file:line:` and no `FAIL` --- and its fallback returns the LAST line, which is mcpp's own summary. A fallback that returns something diagnostic-shaped when the match failed is worse than returning nothing. * plan: the two measurements do not overlap in TARGETS either, and that was unwritten The division of labour said one measures the C surface and the other measures integration. It did not say they ask about different targets: 30-member measurement x86_64-linux-gnu, x86_64-windows-gnu --- no macOS lsp-mcpp-private three targets, including aarch64-macos Measured this round: `archive` is `runs (posix)` on both columns of the 30-member measurement, and the same libarchive fails to link on `lsp-mcpp-private`'s macOS with `memset_pattern16`. Both readings are true. That symbol is an Apple libc function clang emits only for Apple targets; on linux and windows-gnu it cannot appear. So nothing in this ecosystem sweeps macOS at 30-member scale, and E1's criterion --- 10 failures to 5, none newly failing --- holds ON THOSE TWO COLUMNS. A defect of this class is invisible in that number by construction. Adding the column needs a decision first: a Linux host can cross-build aarch64-macos but cannot run it, so that column tops out at `builds`, which RANK places below `runs`. A column that can only reach `builds` makes the regression check permanently looser there than on the other two. * C4 was closed on the wrong objects, and the possibility it dismissed was the right one The closure measured zstd and xz --- the two the OLD report named --- and found them clean. The member that fails is libarchive, and it was never measured. `lsp-mcpp-private`'s `test_archive` does not link for aarch64-macos: ld64.lld: error: undefined symbol: memset_pattern16 referenced by `archive_read_support_format_7zip.o`. zstd and xz happen not to trigger the idiom, so measuring them answered a different question. THE RECORD LISTED THREE POSSIBILITIES AND PICKED THE FIRST. The third --- "`-fno-builtin-memset_pattern16` is not enough to turn off LLVM's loop-idiom pass" --- is the one that holds. A/B on the real compile command from build.ninja, varying only that flag: as built (flag present) 1 reference 38200 bytes flag REMOVED 1 reference -fno-builtin 0 38888 (+1.8%) -ffreestanding 0 -mllvm -disable-loop-idiom-memset 0 38152 The flag changes nothing. And it cannot report that it changed nothing: clang accepts `-fno-builtin-totally_not_a_function` silently, because `memset_pattern16` is an LLVM TargetLibraryInfo libfunc rather than a clang builtin. The preprocessed source contains the symbol zero times, which confirms the call is generated rather than written. TWO ENGINE-SIDE GAPS, AND THE SECOND IS WHY THE FIRST SURVIVED: 1. the token `builtins = "iso"` emits is ineffective for the one case its own comment names; 2. that token is not verified. In the same file, `-D` and `-U` are checked by the probe against `expectDefined`/`expectUndefined`, under a comment saying a `-D` that did not take effect is a verification failure rather than a silent one. `builtinsTokens` has no such list. The criterion that follows: `builtinsTokens` needs the same probe check. A mechanism that holds only under the assumption that it works needs an assertion that it works. The cost of each candidate repair is measured above rather than argued. * the symbol belongs to no layer that carries it, which makes this an architecture choice `memset_pattern16` is not a function programs call; it is a helper the code generator emits, in the same family as `memcpy` and `__udivti3`. That family belongs to the compiler runtime. Measured: grep -rln memset_pattern16 openkal-llvm-runtime/llvm/ -> 0 hits LLVM carries no implementation anywhere, so compiler-rt has no fallback: the call upstream emits is one only Apple's libSystem supplies. `builtins = "iso"` therefore meets a case it can DECLARE and cannot ENFORCE. The flag that would enforce it does not work, and LLVM offers no second one. Four candidates, costs measured: -fno-builtin works, +1.8% here, disables ISO functions' optimisation too -mllvm -disable-loop-idiom-memset works, -0.1%, not a stable interface supply it in openkal-musl's Apple ~6 lines; port/src/mach/ exists; port contrary to the declaration's wording declare the member unbuildable discards a combination that works The third asks the better question. The declaration is about what the C library PRESENTS, and a compiler-emitted helper is not an interface the program requested --- it is closer to ABI. A C library presenting only ISO C may still owe the code generator the helpers it assumes for that target, the same way it owes `memcpy`. This is a decision, not an implementation: it settles whether `builtins = "iso"` means "turn off what the generator assumes" or "declare the surface the program can see". Those diverge in other cases too. * the token was accepted in silence and changed nothing `[c-abi] builtins = "iso"` emitted `-fno-builtin-memset_pattern16` from this mechanism's first revision. A/B on a real compile command, varying only that flag, reads as built (flag present) 1 reference to memset_pattern16 flag REMOVED 1 reference -fno-builtin 0 -mllvm -disable-loop-idiom-memset 0 and clang accepts `-fno-builtin-totally_not_a_function` just as quietly: the `-fno-builtin-<fn>` family is matched against clang's builtin table, while `memset_pattern16` is an LLVM TargetLibraryInfo libfunc. The call is emitted by LoopIdiomRecognize, which consults TLI, and the per-function attribute does not reach it. `-mllvm` is not chosen because it passes an internal LLVM option, which can be renamed or removed between releases; when it is, the mechanism returns to failing silently, which is the defect being repaired. The cost of the blunt flag is measured rather than argued: on the translation unit that surfaced this the object grows 38200 to 38888 bytes, 1.8 per cent. THE NO-OP SURVIVED BECAUSE IT HAD NO CRITERION. `cenv` verifies its tokens against a `-dM` dump, and a code-generation property is not visible there. The criterion now lives in `openkal-cross.yml` and has three legs, on all three hosts: no flag (the symbol MUST appear, or the probe measures nothing), the per-function flag (it must still appear, pinning the defect), and an `aarch64-macos` build over the openkal stack by the mcpp under test (zero references). Run against the previous binary the step fails, at the link: ld64.lld: error: undefined symbol: memset_pattern16 * a target that cannot be run could only be measured by not building it `mcpp test` for a target this host can neither execute nor reach through a runner leaves every test `not run` and exits 2. That is the correct answer to "do these tests pass" — mcpp did not find out — but 2 is also what a broken runner returns, so a caller that wanted only the build cannot tell the two apart and falls back to `mcpp build`. AND `mcpp build` BUILDS THE PACKAGE. For a package whose only sources are under `tests/` it compiles nothing of it at all. Measured on mcpp-index's `archive` member, whose sources are two files under `tests/`: $ mcpp build --target aarch64-macos # exits 0 Compiling compat.lz4 / compat.xz / compat.zlib / compat.zstd … $ find target -name '*compression*' -o -name '*versions*' (only musl's versionsort.o) The member's dependencies compiled; not one line of the member did. A compatibility sweep reading that exit code records the member as building on macOS, which is a reading about the dependencies with the member's name on it. `--no-run` gives the narrower claim its own answer: every selected test is compiled and linked for the target, none is executed, and the result says so. `built` is counted apart from `not_run`, in the human summary and in the machine interface, because `not_run` means mcpp tried and could not — the question is open, the exit code is 2 — while `built` means it was told not to, so the build was the whole question and the exit code is 0. The criterion is `745_no_run_builds_the_tests_and_says_so.sh`, four legs. Its runner is a name that is not a program: an unexecutable target would make the test need a cross toolchain and a host that cannot run it, while a runner that cannot be found produces the same situation on every host, for the native target, with nothing installed. Run against a binary without the flag it fails at leg B. * the reader was a byte match and the hosts do not agree about binaries Leg 1 of the builtins criterion failed on the macOS host with all three readings 0, which is what leg 1 exists to report: the two legs behind it were measuring nothing. The cause is the reader, not the compiler. `grep -ac memset_pattern16` reads 1/0 correctly with GNU grep, and the macOS runner's grep is BSD, where what `-a` promises about a binary file differs. The name being present in an object's string table made a byte match look like it needed no tool; it needed agreement about binary input instead. `llvm-nm -u` is in the payload beside the clang already being used, costs the same lookup, and asks the question the step is actually asking. Measured unchanged on Linux: 1 / 1 / 0, engine 0, and the control against the previous binary still fails at the link. * the acceptance record gains the two criteria this round's closing found C5 is the builtins token, whose first leg went red on macOS and reported that the two behind it were measuring nothing. C6 is what `builds` meant on a target with no runner: the member's dependencies, not the member. * a count that stops at the member level leaves the same reading one level up `--workspace --no-run` reported "ok. N member(s); 0 passed; 0 failed", which is what a workspace with no tests at all reports — the false reading `totalNotRun` was added to the same line to prevent, one level down. The fan-out now carries `built` through to the workspace total and to `workspace_summary` as `tests_built`, kept apart from `tests_not_run` for the reason the per-member fields are: one is a question left open, the other is a question that was not asked. Leg E of 745 covers it: two members, one test each, `--workspace --no-run`, exit 0 and "2 built, not run" in the total. * 2026.9.21.3 Both version sites in one commit, which `01_help_and_version.sh` cross-checks. The bootstrap pin in .xlings.json stays at 2026.9.21.1: it must name a version that is published, mirrored and in the index, and this one is none of those yet. * the Apple list has one definition and three spellings `os == "macos" || os == "ios"` is `Triple::is_apple()`. This module takes `os` as a string rather than a `Triple` deliberately — it is pure, and importing the toolchain model to reach one predicate would couple what that choice decoupled — so the spelling stays and the comment names where the canonical list lives and which other sites carry a copy. * the sandbox verification gains the two sections this release adds G reads an object file, because a code-generation property is in no `-dM` dump — which is why the token it asserts was a silent no-op for the whole of its first life. On 2026.9.21.2 the aarch64-macos link fails outright, so a failed build there is the negative reading rather than an absent one. H asserts `--no-run`; before this release the flag does not exist. Dry-run on the host against both engines: both sections pass on this one and fail on the previous, which is the property a CHANGE section has to have. * the next batch's first item, with a number attached aarch64-macos was measurable for the first time once a target with no runner compiled the member's own tests: 20 of 30 build, 10 do not, and nine of the ten are `#ifdef __APPLE__` reaching for the Apple C environment on a target where `__APPLE__` is true and libSystem is not there. It is the Windows problem mirrored, minus the lever: presenting POSIX on Windows is realised as a cygwin triple and `_WIN32` goes away, while on macOS the realisation adds `-D__unix__` and leaves `__APPLE__` standing because it is correct. Nothing in the identity a source file sees there answers which C library is underneath. --------- Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
1 parent 9fa3e53 commit 476c366

22 files changed

Lines changed: 1003 additions & 48 deletions

.agents/docs/2026-09-21-macros-and-withdrawal-verify.sh

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,99 @@ else
297297
skip "openkal-llvm-runtime 0.14.0 did not resolve from the index"
298298
fi
299299

300+
# ── CHANGE. `builtins = "iso"` withdraws the Apple pattern fill ─────────────
301+
section "G. builtins = \"iso\" emits -fno-builtin (CHANGE)"
302+
# A CODE-GENERATION PROPERTY, so the reading is an object file rather than a
303+
# `-dM` dump — which is exactly why the token this asserts was a silent no-op
304+
# for the whole of its first life. `2026.9.21.2` and earlier emit
305+
# `-fno-builtin-memset_pattern16`; clang matches `-fno-builtin-<fn>` against
306+
# its builtin table, `memset_pattern16` is an LLVM TargetLibraryInfo libfunc
307+
# and not in it, so the flag is accepted in silence and the call survives. On
308+
# those versions the aarch64-macos link fails with
309+
#
310+
# ld64.lld: error: undefined symbol: memset_pattern16
311+
#
312+
# so a failed build here IS the negative reading rather than an absent one.
313+
g="$root/g"; rm -rf "$g"; mkdir -p "$g/src"
314+
cat > "$g/src/main.cpp" <<'EOF'
315+
extern "C" void fill(int* a, long n) {
316+
for (long i = 0; i < n; ++i) a[i] = 0x01020304;
317+
}
318+
int main() { static int buf[64]; fill(buf, 64); return buf[0] == 0x01020304 ? 0 : 1; }
319+
EOF
320+
# `-O2` per package rather than --release: the idiom pass does not run at the
321+
# dev profile's -O0, and a release build would compile the runtime a second
322+
# time in a second profile for no reading.
323+
cat > "$g/mcpp.toml" <<'EOF'
324+
[package]
325+
name = "builtins-probe"
326+
version = "0.1.0"
327+
328+
[build]
329+
cxxflags = ["-O2"]
330+
331+
[dependencies]
332+
openkal-llvm-runtime = "0.15.0"
333+
334+
[toolchain]
335+
default = "llvm@22.1.8"
336+
EOF
337+
gnm=$(ls "$HOME"/.xlings/data/xpkgs/xim-x-llvm/22.1.8/bin/llvm-nm 2>/dev/null | head -1)
338+
if [ -z "$gnm" ]; then
339+
skip "G: no llvm-nm in the payload to read the object with"
340+
elif (cd "$g" && "$STORE" build --target aarch64-macos >/dev/null 2>&1); then
341+
gobj=$(find "$g/target" -name 'main.o' 2>/dev/null | head -1)
342+
if [ -z "$gobj" ]; then
343+
fail "G: the build reported success and produced no object"
344+
elif [ "$("$gnm" -u "$gobj" 2>/dev/null | grep -c memset_pattern16)" = 0 ]; then
345+
ok "builtins = \"iso\" leaves no memset_pattern16 in the object"
346+
else
347+
fail "builtins = \"iso\" did not withdraw memset_pattern16"
348+
fi
349+
else
350+
fail "G: the aarch64-macos build did not complete (the old token's signature)"
351+
fi
352+
353+
# ── CHANGE. `mcpp test --no-run` ────────────────────────────────────────────
354+
section "H. mcpp test --no-run builds the tests and says so (CHANGE)"
355+
# Before this release the flag does not exist and the command exits non-zero
356+
# with "unknown option: --no-run". The runner named here is a program that
357+
# does not exist, which produces "tests built, nothing run" on every host for
358+
# the native target with nothing installed.
359+
h="$root/h"; rm -rf "$h"; mkdir -p "$h/tests"
360+
printf 'int main() { return 0; }\n' > "$h/tests/alpha.cpp"
361+
printf 'int main() { return 0; }\n' > "$h/tests/beta.cpp"
362+
printf '[package]\nname = "norun"\nversion = "0.1.0"\n' > "$h/mcpp.toml"
363+
hhost=$("$STORE" --print-target 2>/dev/null || true)
364+
if [ -z "$hhost" ]; then
365+
(cd "$h" && "$STORE" build >/dev/null 2>&1) || true
366+
hhost=$(ls "$h/target" 2>/dev/null | grep -v '^\.' | head -1)
367+
fi
368+
if [ -z "$hhost" ]; then
369+
skip "H: could not determine the host triple"
370+
else
371+
cat > "$h/mcpp.toml" <<EOF
372+
[package]
373+
name = "norun"
374+
version = "0.1.0"
375+
376+
[target.$hhost]
377+
runner = ["mcpp-no-such-runner-exists"]
378+
EOF
379+
hout=$( (cd "$h" && "$STORE" test --target "$hhost" --no-run 2>&1) )
380+
hrc=$?
381+
case "$hout" in
382+
*"2 built, not run"*)
383+
if [ "$hrc" = 0 ]; then
384+
ok "--no-run builds the tests and exits 0"
385+
else
386+
fail "--no-run reported the built tests and exited $hrc"
387+
fi ;;
388+
*) fail "--no-run did not report two tests as built" ;;
389+
esac
390+
fi
391+
392+
300393
printf '\n-- summary --\nfails=%d\nnot run:%s\n' "$fails" "${skipped:-
301394
(none)}"
302395
[ "$fails" -eq 0 ]

.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md

Lines changed: 214 additions & 2 deletions
Large diffs are not rendered by default.

.github/workflows/openkal-cross.yml

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,129 @@ jobs:
264264
echo "${{ matrix.host }} → $t : $(ls -l "$a" | awk '{print $5}') bytes"
265265
done
266266
267+
# THE ONLY CRITERION THE `builtins` TOKEN HAS, AND WHY IT IS HERE.
268+
#
269+
# `[c-abi] builtins = "iso"` states that the C library supplies the ISO
270+
# functions and no vendor extensions. On Darwin targets clang's loop
271+
# idiom recogniser rewrites a constant-pattern fill into a call to
272+
# `memset_pattern16`, an Apple libc extension no such library carries.
273+
# That call is produced by the code generator, so it appears in no `-D`
274+
# and in no preprocessor dump --- and `mcpp.toolchain.cenv` verifies its
275+
# tokens by comparing a `-dM` dump. The token this mechanism emitted was
276+
# therefore a silent no-op for the whole of its first life, and this
277+
# step exists because no other kind of check could have reported it.
278+
#
279+
# LEG 1 IS THE DENOMINATOR, AND IT IS NOT DECORATION. At `-O0` the pass
280+
# does not run, and `-ffreestanding` implies `-fno-builtin`; an
281+
# assertion that only reads "the symbol is absent" therefore passes in
282+
# several worlds where nothing was measured. Leg 1 compiles with no flag
283+
# at all and fails if the symbol does NOT appear.
284+
#
285+
# LEG 2 PINS THE DEFECT ITSELF. `-fno-builtin-memset_pattern16` is
286+
# accepted in silence and changes nothing: `-fno-builtin-<fn>` is matched
287+
# against clang's builtin table, and `memset_pattern16` is an LLVM
288+
# TargetLibraryInfo libfunc rather than a clang builtin. The toolchain is
289+
# pinned in this job, so the reading is stable. Should a later pin make
290+
# leg 2 fail, clang has gained the narrower behaviour, and `cenv` can
291+
# emit the narrower flag and recover the 1.8 per cent `-fno-builtin`
292+
# costs.
293+
#
294+
# LEG 3 IS THE ENGINE: the same idiom, over the openkal stack, for
295+
# `aarch64-macos`, built by the mcpp under test. Measured on the token
296+
# this step was written for, the two readings are
297+
#
298+
# -fno-builtin-memset_pattern16 1 reference, and the link fails
299+
# -fno-builtin 0 references, and it links
300+
#
301+
# so a regression here reports itself at the link before the assertion
302+
# is reached. The assertion covers the remaining case, in which some
303+
# layer happens to supply the symbol and the link succeeds anyway.
304+
#
305+
# `-O2` IS PER PACKAGE RATHER THAN `--release`. The idiom pass does not
306+
# run at the dev profile's `-O0`, and a release build would compile the
307+
# runtime a second time in a second profile for no reading.
308+
- name: builtins = "iso" withdraws the Apple pattern fill
309+
run: |
310+
set -euo pipefail
311+
BIN="${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs/xim-x-llvm/22.1.8/bin"
312+
# Not `ls ... | head -1`: with `pipefail` the absent candidate's
313+
# exit status ends the step before the guard below is reached.
314+
pick() { # $1..$n = candidate paths; prints the first executable one
315+
for c in "$@"; do
316+
if [ -x "$c" ]; then printf '%s' "$c"; return 0; fi
317+
done
318+
return 1
319+
}
320+
CLANG="$(pick "$BIN/clang" "$BIN/clang.exe")" \
321+
|| { echo "::error::no clang under $BIN"; exit 1; }
322+
NM="$(pick "$BIN/llvm-nm" "$BIN/llvm-nm.exe")" \
323+
|| { echo "::error::no llvm-nm under $BIN"; exit 1; }
324+
325+
# Inside the clone, so the dependency is named by a relative path.
326+
# `$RUNNER_TEMP` is a backslash path on the Windows host and a TOML
327+
# string would read its separators as escapes.
328+
W="$RUNNER_TEMP/okl/examples/builtins-probe"
329+
rm -rf "$W"; mkdir -p "$W/src"
330+
331+
# The one shape the idiom recogniser rewrites. The element type is
332+
# `int` and not `char` because a byte-repeating value becomes
333+
# `memset`, which every C library has.
334+
cat > "$W/src/main.cpp" <<'PROBE'
335+
extern "C" void fill(int* a, long n) {
336+
for (long i = 0; i < n; ++i) a[i] = 0x01020304;
337+
}
338+
339+
int main() {
340+
static int buf[64];
341+
fill(buf, 64);
342+
return buf[0] == 0x01020304 ? 0 : 1;
343+
}
344+
PROBE
345+
346+
cat > "$W/mcpp.toml" <<'PROJECT'
347+
[package]
348+
name = "openkal-builtins-probe"
349+
version = "0.1.0"
350+
351+
[build]
352+
cxxflags = ["-O2"]
353+
354+
[dependencies]
355+
openkal-llvm-runtime = { path = "../.." }
356+
357+
[toolchain]
358+
default = "llvm@22.1.8"
359+
PROJECT
360+
361+
# THE READER ASKS FOR UNDEFINED SYMBOLS, NOT FOR BYTES.
362+
#
363+
# This was `grep -ac memset_pattern16`, on the reasoning that the name
364+
# is in the object's string table and a byte match needs no tool. It
365+
# reads 1/0 correctly with GNU grep and read 0 for all three legs on
366+
# the macOS host, where grep is BSD: the object is binary, and what
367+
# `-a` promises about that differs between the two. Leg 1 is what
368+
# reported it. `llvm-nm` is in the payload beside the clang already
369+
# being used, and answers the question this step is actually asking.
370+
refs() { "$NM" -u "$1" 2>/dev/null | grep -c memset_pattern16 || true; }
371+
T=--target=arm64-apple-macos14.0
372+
373+
"$CLANG" $T -O2 -c "$W/src/main.cpp" -o "$W/bare.o"
374+
"$CLANG" $T -O2 -fno-builtin-memset_pattern16 -c "$W/src/main.cpp" -o "$W/narrow.o"
375+
"$CLANG" $T -O2 -fno-builtin -c "$W/src/main.cpp" -o "$W/blunt.o"
376+
echo "no flag : $(refs "$W/bare.o")"
377+
echo "-fno-builtin-memset_pattern16 : $(refs "$W/narrow.o")"
378+
echo "-fno-builtin : $(refs "$W/blunt.o")"
379+
380+
[ "$(refs "$W/bare.o")" = 1 ] || { echo "::error::leg 1: the probe no longer triggers the idiom, so legs 2 and 3 measure nothing"; exit 1; }
381+
[ "$(refs "$W/narrow.o")" = 1 ] || { echo "::error::leg 2: clang now honours -fno-builtin-memset_pattern16, and mcpp.toolchain.cenv can emit the narrower token"; exit 1; }
382+
[ "$(refs "$W/blunt.o")" = 0 ] || { echo "::error::leg 2: -fno-builtin no longer withdraws the pattern fill"; exit 1; }
383+
384+
(cd "$W" && "$MCPP_UNDER_TEST" build --target aarch64-macos)
385+
obj=$(find "$W/target" -name 'main.o' | head -1)
386+
[ -n "$obj" ] || { echo "::error::leg 3 produced no object to read"; exit 1; }
387+
echo "engine, aarch64-macos over openkal: $(refs "$obj")"
388+
[ "$(refs "$obj")" = 0 ] || { echo "::error::leg 3: builtins = \"iso\" did not withdraw memset_pattern16"; exit 1; }
389+
267390
- uses: actions/upload-artifact@v4
268391
with:
269392
name: openkal-built-on-${{ matrix.host }}

CHANGELOG.md

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,113 @@
33
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
44
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。
55

6-
## [Unreleased]
6+
## [2026.9.21.3] - 2026-09-21
7+
8+
### `mcpp test --no-run`:为一个跑不了的目标构建测试,并把这当成答案
9+
10+
一个本机既不能执行、也没有 runner 可达的目标,会让每个测试停在 `not run`,命令退出 2。
11+
这是对「这些测试通过吗」的正确回答——mcpp 没有查明。但 **2 同样是 runner 坏掉时的
12+
退出码**,于是一个只想要「构建」的调用方无法区分这两者,只能退回去用 `mcpp build`。
13+
14+
而 `mcpp build` 构建的是**包**。对一个源码只在 `tests/` 下的包,它一行都不编译。
15+
实测 mcpp-index 的 `archive` 成员(源码是 `tests/` 下两个文件):
16+
17+
```
18+
$ mcpp build --target aarch64-macos # 退出 0
19+
Compiling compat.lz4 / compat.xz / compat.zlib / compat.zstd ...
20+
$ find target -name '*compression*' -o -name '*versions*'
21+
(只有 musl 的 versionsort.o)
22+
```
23+
24+
退出 0,编译了这个成员的**依赖**,而**该成员自己的代码一行都没有编过**。一个读这个
25+
退出码的兼容性测量,会把它记成「这个成员在 macOS 上构建通过」。
26+
27+
`--no-run` 让那个更窄的断言有了自己的答案:被选中的每个测试都为该目标编译并链接,
28+
没有任何一个被执行,结果也这么写:
29+
30+
```
31+
test result ok. 0 passed; 0 failed; 2 built, not run
32+
```
33+
34+
`built` 与 `not_run` 分开计数,机器接口也一样(`docs/50`):`not_run` 是「试过而做不到,
35+
问题悬着,退出 2」,`built` 是「被要求不要执行,构建就是问题的全部,退出 0」。编译不过
36+
的测试仍然是失败。`--no-run` 与 `--no-runner` 同时给出会被拒绝——两个名字只差一个字符
37+
而含义相反,不存在应当优先的那一个读法。
38+
39+
判据是 `tests/e2e/745_no_run_builds_the_tests_and_says_so.sh`,四条腿。其中 runner 用的是
40+
一个**不存在的程序名**:用一个执行不了的目标会让这个测试需要交叉工具链和特定宿主,而
41+
一个找不到的 runner 在每台宿主上、对本机目标、不装任何东西,就能造出同一个局面。
42+
43+
### `builtins = "iso"` 发的那个 token 是静默空操作,已换成 `-fno-builtin`
44+
45+
`[c-abi] builtins = "iso"` 声明 C 库只提供 ISO 函数、没有厂商扩展。Apple 目标上
46+
clang 的循环惯用法识别会把常量模式填充改写成 `memset_pattern16` 调用——那是一个
47+
Apple libc 扩展,这样的库没有它。此前这里发的是 `-fno-builtin-memset_pattern16`。
48+
在一份真实的 `build.ninja` 编译命令上做 A/B,只改这一个 flag:
49+
50+
| flag | `memset_pattern16` 引用数 |
51+
|---|---|
52+
| 照原样(flag 在) | 1 |
53+
| flag **删掉** | 1 |
54+
| `-fno-builtin` | 0 |
55+
| `-mllvm -disable-loop-idiom-memset` | 0 |
56+
57+
**而它报不出自己什么都没做。** clang 静默接受 `-fno-builtin-totally_not_a_function`:
58+
`-fno-builtin-X` 这一族按 clang 的 builtin 表校验,而 `memset_pattern16` 是 LLVM
59+
TargetLibraryInfo 的 libfunc,不在那张表里;发出调用的是 LoopIdiomRecognize,它查
60+
TLI,按函数名的属性到不了它。
61+
62+
不选 `-mllvm` 的理由是它传的是 LLVM 内部选项,不是受支持的接口,改名或删除之后这套
63+
机制会再次静默失效——那正是这次要修的缺陷本身。代价是量出来的:在暴露此事的那个翻译
64+
单元(libarchive 的 7zip reader,`-O2`,aarch64-macos)上,目标文件从 38200 涨到
65+
38888 字节,1.8%,因为 `-fno-builtin` 同时撤走了 C 库确实提供的那些 ISO 函数。这比
66+
`builtins = "iso"` 声明的范围宽,而它宽在安全的方向:代码生成器不合成的调用不会变成
67+
链接错误。
68+
69+
**这个空操作能活下来,是因为它没有判据。** `cenv` 的探针用 `-dM` dump 校验自己发的
70+
token,而代码生成阶段的性质在预处理器 dump 里不可见。判据现在在
71+
`.github/workflows/openkal-cross.yml`,三条腿,跑在三台宿主上:
72+
73+
| 腿 | 内容 | 判据 |
74+
|---|---|---|
75+
| 1 | 不带任何 flag 编译探针 | 符号**必须出现**——否则探针已经触发不了惯用法,腿 2、3 什么都没测 |
76+
| 2 | `-fno-builtin-memset_pattern16` | 符号**仍必须出现**(钉住这个缺陷;若哪天红了,说明 clang 认了窄拼法,`cenv` 可以改回去把这 1.8% 拿回来) |
77+
| 3 | mcpp 为 `aarch64-macos` 走 openkal 栈构建同一份源码 | 目标文件里**零引用** |
78+
79+
腿 3 的对照:换回旧 token,同一个工程链接失败于
80+
`ld64.lld: error: undefined symbol: memset_pattern16`。
81+
82+
### 更正:那个「四个成员」是二,而分组用错了依据
83+
84+
2026.9.21.2 的条目、`cenv.cppm` 与 `predefines.cppm` 的注释、`docs/21` 与 `docs/22`
85+
都写着「借来的 `__CYGWIN__` 代价是四个成员:archive、sqlite3、mimalloc、c-ares」。
86+
**撤销之后的重测把这个数目否掉了。**
87+
88+
| 成员 | 记的真因 | 实测真因 | 撤销后 |
89+
|---|---|---|---|
90+
| `archive`(经 xz) | `__CYGWIN__` | **对** | **清了** |
91+
| `sqlite3` | `__CYGWIN__` | **对** | **清了** |
92+
| `c-ares` | `__CYGWIN__` | **错**:`#ifdef HAVE_WINDOWS_H`,而那个宏由 mcpp-index 自己的配方在 windows 分支 `#define` | 仍红 |
93+
| `mimalloc` | `__CYGWIN__` | **错**:已经不走到任何头文件——`error in backend: Target OS doesn't support __builtin_thread_pointer() yet` | 仍红 |
94+
95+
**四个是按「诊断」分的组,不是按「真因」。** 四个都停在 `windows.h`(mimalloc 当时如此),
96+
于是被记成同一类。**按诊断分组不是按真因分组**,这样数出来的数目会高估一次撤销能修掉多少。
97+
98+
这条更正本身来自判据:重测把总失败从 10 降到 5、**新增失败为零**,而「`windows.h` 组 4→0」
99+
这半条没有达成——是 4→2。**一个达成了一半的判据,比一个没写的判据更有价值:它指出了
100+
分母是错的。**
101+
102+
### `mimalloc` 暴露出替身三元组的一个代价,与宏无关
103+
104+
```
105+
fatal error: error in backend: Target OS doesn't support __builtin_thread_pointer() yet.
106+
```
107+
108+
`presents = "posix"` 在 Windows 上实现成 `--target=x86_64-pc-cygwin`。LLVM 没有为那个
109+
OS 实现 `__builtin_thread_pointer()`,而 mimalloc 用它取线程局部堆指针。**这是替身
110+
三元组的第一个被测量到的、超出宏名之外的代价**;先前关于这次替换的记录只讨论了预处理器
111+
看到什么。
112+
7113

8114
## [2026.9.21.2] - 2026-09-21
9115

docs/08-testing.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ mcpp performs with the test binary appended.
105105
```bash
106106
mcpp test --target thumbv7em-none-eabihf # built for the board, run through its runner
107107
mcpp test --no-runner # ignore the runner and execute directly
108+
mcpp test --target aarch64-macos --no-run # build the tests for the target and stop
108109
```
109110

110111
Nothing about the test changes. The same `tests/**/*.cpp` compiles for the
@@ -114,6 +115,23 @@ or a QEMU exit code is what a bare-metal runner is chosen to produce.
114115
`--no-runner` exists for a host that can execute the binaries natively and
115116
should not pay for an emulator.
116117

118+
`--no-run` makes the narrower claim, and it has to be asked for. Without it, a
119+
target this host can neither execute nor reach through a runner leaves every
120+
test not run and the command exits 2: mcpp did not establish whether the tests
121+
pass, and reporting that as success is the false reading this repository has
122+
recorded most often. But 2 is also what a broken runner returns, so a caller
123+
that wanted only the build could not tell the two apart. Under `--no-run` every
124+
selected test is compiled and linked for the target, none is executed, and the
125+
result says so:
126+
127+
```
128+
test result ok. 0 passed; 0 failed; 2 built, not run
129+
```
130+
131+
A test that does not compile is still a failure, and `--no-run` with
132+
`--no-runner` is refused rather than resolved: one says to run the binaries
133+
without the declared runner, the other says not to run them.
134+
117135
A test program carries the files it reads beside it: the runner receives
118136
`MCPP_RUNTIME_FILES`, the list of its deployed files and the shared libraries it
119137
loads, and a runner that moves the program to a device copies them with it. A

0 commit comments

Comments
 (0)