Tags: mcpp-community/mcpp
Tags
2026.9.26.1: paths in UTF-8, a C standard per package, a graph link w… …ithout the host, and the level that was declared (#698) * 2026.9.26.1: paths in UTF-8, a C standard per package, a graph link without the host, and the level that was declared #693. mcpp held paths in the Windows ANSI code page, and the JSON it writes holds UTF-8 only, so every build under a non-ASCII project directory or home failed with an internal JSON exception, including names the code page can spell; a Latin-1 directory name fails the same way on Linux. mcpp.exe now declares the UTF-8 code page (res/mcpp.rc), build programs are linked with the same manifest, host tools default to it, and the new target key `windows_code_page` states it for a project's own executables. A path with no UTF-8 spelling is refused (project directory, MCPP_HOME), skipped and reported (a name inside a project), or refused by key (a build.mcpp directive), never an internal exception. The response files of the msvc dialect begin with a byte order mark, which cl.exe, link.exe and lib.exe need to read UTF-8 (measured), and Ninja's own encoding is checked when build.ninja is not ASCII. #695. `[build] c_standard` reached every C unit of the graph through the file-level $cflags, and a dependency's own value was not applied. Each package's C units now compile at that package's standard; an undeclared package at c11. #696. A link over a graph-supplied C library searched the host's library directories, so `-lm` linked glibc objects into a musl image. Such a link on ELF now carries --sysroot naming an empty directory, the hermetic check holds every -L to the store, the build directory and the graph's packages, and an unanswered -l fails with a note naming openkal-musl 0.19.2. #694. The musl -Og workaround is removed; the compile and the Finished line read one realised optimization level. Plan and measurements: .agents/docs/2026-09-25-issues-693-696-triage-and-repair-plan.md * xlings 2026.9.26.2, the key rule a test still stated, and the graph link measured on four legs - kXlingsVersion and every xlings pin in .github move to 2026.9.26.2 (openxlings/xlings#613): xlings.exe declares the UTF-8 code page and its main has an exception boundary, so a working directory or an MCPP_HOME outside the ANSI code page no longer ends it with 0xC0000409 and no output. - test_cache_key stated the old rule, that a package's `c_standard = "c11"` is keyed; a package that spells the default and one that says nothing compile identically and now share a key. CI run 1 failed that test on every platform, and nothing else in the unit suites. - e2e 778 pins each leg to the openkal-musl and openkal-llvm-runtime pair that belongs together (the runtime pins openkal-musl exactly, and a root that pins another is refused as irreconcilable), and adds the report's aarch64 case under qemu-aarch64 and a host `-L` refused by the hermetic check. The openkal job asserts each leg ran. - docs: a macOS file name is UTF-8; `allow_host_libs` lifts the graph-link refusal rather than turning it into a warning. - The plan's implementation record carries CI run 1: every Windows row of the regression job passes, and the two xcode-27 legs fail as on main (#669). * SPEC-002 rule three: a link over a graph-supplied C library searches no host directory (v1.1) --------- Co-authored-by: speak-agent <[email protected]>
2026.9.25.1: workspace members compile the same way in every position… …, and a consumer's headers stay in the consumer (#690) (#691) * docs: #690 design record and implementation plan * fix(workspace): one inheritance pipeline for every member, keyed defines, one key table (#690) A member reached as a dependency now inherits [workspace.package], x.workspace = true entries and [workspace.build] at its load site, before the conditional merge and the defines fold, as the root does. The snapshot in makePackageRoot no longer inherits and refuses unfolded defines with an internal error. A member of a git-hosted workspace inherits from its own repository. [build] defines is a set keyed by macro name, and !NAME removes an inherited name. The inheritable [build] subset is one exported table, which accepts ios_deployment_target. The e2e harness links payloads per version, so a version installed by a test no longer lands in the developer's registry. * fix(build): a consumer's include directories stay in the consumer (#690 W6) The root's [build] include_dirs and include_dirs_after were written into the file-level $cxxflags/$cflags/$asmflags/$nasmflags of build.ninja, which every unit in the graph reads. A root header named like a system header shadowed it inside a dependency, private_include_dirs included, and because no cache-key axis contained the broadcast, a dependency object in the global cache could be compiled against another project's root headers (measured: cJSON_Compare answered 1 in an unrelated project). - flags.cppm: no include directory is broadcast; every unit keeps its own package's directories through $local_includes (C, C++, GAS and NASM). - cache_key.cppm: kCacheEpoch 3 -> 4, orphaning entries written while the broadcast existed. - ninja_backend.cppm / execute.cppm: consumer_include_scope_advice names the consumer directory that holds a header a dependency now fails to find, on the full path and, through a sidecar beside build.ninja, on the fast path. - tests: unit tests for the file-level channel, the unit channel's absolutisation, the advice and the sidecar; e2e 765. * docs: workspace inheritance in every position, keyed defines, include scope; SPEC-004 1.6 (#690) * docs: noun-phrase heading for the defines and include scope section * docs: #690 sandbox verification script * publish: the effective manifest, a normalised published form, and a reproducible archive (#690 W4, W5) * test: renumber the published-form e2e scripts to 772 and 773 * fix: every reader of a member manifest reads the effective manifest; 2026.9.25.1 (#690 W4) prepare_build loads through load_effective_manifest and no longer inherits a preloaded host-tool manifest a second time. sbom, index list/update, the index refresh of mcpp update, the fast-path identity and test discovery read the effective manifest. * chore: xlings pin 2026.9.16.1 -> 2026.9.20.1 * fix: a member inside an index archive inherits its workspace; publish reads the one key table (#690) inherit_as_workspace_member is the one function for a sibling path dependency, a git-hosted member and a member inside an index package's archive (Form A pointer), searched no higher than the install root. normalize.cppm writes [workspace.build] back through kWorkspaceBuildKeys instead of a second copy of the key set. e2e 774. * docs: publishing a workspace member, the index-archive member, CHANGELOG 2026.9.25.1, SPEC-004 criteria 13-14 * publish: the archive commit ignores commit.gpgSign and the manifest blob ignores .gitattributes * fix: a host-tool sub-build merges its dependency's conditional sections once (#690 F12); self-review record * test: host-spelled fixture path in 772, build-directory words excluded from 765 (c), cmd.exe quoting in 775 --------- Co-authored-by: speak-agent <[email protected]> Co-authored-by: speak-agent <[email protected]>
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 <[email protected]>
2026.9.21.2: the macros mcpp owns are spelt in upper case, and the bo… …rrowed name is withdrawn (#681) * 2026.9.21.2: the macros mcpp owns are spelt in upper case, and the borrowed name is withdrawn `__mcpp_target_<os>__` becomes `__MCPP_TARGET_<OS>__` and `__openkal__` becomes `__OPENKAL__`. The spelling is still the triple's own `os` field, now upper-cased; the engine still learns no operating-system name. THE CONVENTION SPLITS BY WHAT A NAME IS, NOT BY WHO WRITES IT. A vendor or product name is upper --- `__APPLE__`, `_WIN32`, `__MINGW32__`, `__GNUC__`. A kind-of-system name is lower --- `__linux__`, `__unix__`, `__gnu_linux__`. Every row this engine OWNS is of the first kind: it names mcpp, or it names openkal. The kind-of-system question is answered by `__linux__` and its family, which mcpp SUPPLIES rather than owns and which therefore keep their lower-case spelling, for that exact reason. The lower-case spelling shipped in 2026.9.21.1 on the reverse reading: that these names sit beside `__linux__` in real guards, so matching it was consistency. THAT CONFUSES ADJACENCY WITH KIND. `__APPLE__` sits in those same guards and is upper, because it belongs to somebody. `__MCPP_` is a prefix, not the whole rule: `__OPENKAL__` is owned and names *openkal* rather than mcpp. The assertion in `test_predefines.cpp` therefore checks the SPELLING CONVENTION --- upper case, `__`-wrapped --- instead of the prefix, which would have judged `__OPENKAL__` a violation when it is not. WITHDRAWING A MACRO IS DECIDED BY A COUNT, NOT BY READING An entry in this contract is a published interface, and withdrawing one is SILENT: a `#if` selects the other branch and compiles. No mechanism available to a build tool makes that loud. So a withdrawal is decided by a measurement that ENUMERATES READERS, and what it finds sets how many steps the withdrawal takes. The two this project has performed came out differently: withdrawn readers found steps __CYGWIN__ 4 third-party members, 3 + 6 sites in the two headers we INSTALL __mcpp_target_<os>__, __openkal__ none 1 The second row's denominator is every repository of this ecosystem, swept by file type: no source file and no manifest reads either name, and the only occurrences are this engine's emitter, its tests, and prose. Both names were invented here, so no upstream code can hold one; exposure was three days for `__openkal__` and a single release for the target macro. The rule did not change between the two rows. The count did. A new assertion takes its denominator from the target registry rather than from a list written beside the test: every canonical triple is parsed and its emitted macro checked to be a valid identifier. The spelling comes from the `os` field, so an `os` carrying a dot or a version suffix would produce a macro no compiler accepts, and the failure would land in a user's build. __CYGWIN__ IS WITHDRAWN --- THE LAST STEP OF A THREE-REPOSITORY SEQUENCE The Windows `presents = "posix"` realisation now adds `-U__CYGWIN__` and `-U__CYGWIN32__` to the compile line. `--target=x86_64-pc-cygwin` stays: it is what supplies `__unix__` and suppresses `_WIN32`, which is what presenting POSIX means. Only the borrowed NAME was unwanted. 1. 2026.9.21.1 defined mcpp's own name beside the borrowed one; this release re-spells it, while it still has no consumer. Purely additive. 2. `[email protected]` and `[email protected]` read the new name and keep `|| defined(__CYGWIN__)`, so they build on an engine from either side of this change. PUBLISHED BEFORE THIS RELEASE. 3. this release stops defining the borrowed name. THE ORDERING IS MEASURED, NOT ASSERTED. It holds between repositories, so no test in this one can check it. Building an openkal program for `x86_64-windows-gnu` against the PUBLISHED `[email protected]` on this engine fails on libunwind's two `static_assert`s --- `x86_64 registers do not fit into unw_context_t`, `UnwindCursor<> does not fit in unw_cursor_t`. That is step three taken first, and it is why step two ships first. `setjmp.h` is the silent half: its own comment says a mismatch is reported by nothing until the record overruns. THE RESIDUAL WINDOW IS NAMED RATHER THAN CLAIMED AWAY. A project pinning `openkal-musl` at 0.18.0 or earlier EXACTLY, and upgrading past this release, gets that silent `#else`. Moving the index's `latest` onto 0.19.0 first keeps the window to exact pins; nothing available here closes it, because the engine cannot know which macros a package's installed headers read. Bootstrap pin advances from 2026.9.20.1 to 2026.9.21.1. * an unanswered requirement says so, and the openkal branch this engine is verified against is overridable E3: A REQUIREMENT NOBODY ANSWERED READ EXACTLY LIKE A CONFIRMED ONE. Three situations exist and two of them build: the resolved implementation states a `provides-interfaces` list containing the requirement (build), states a list without it (refused), or states nothing at all (build). The third is deliberate --- the key postdates the implementations, and a graph that has not adopted it must keep building --- but until now it produced output identical to the first, so a consumer inspecting a green build could not tell "checked and agreed" from "never asked". note kernel-abi interfaces: [email protected] states none, 2 requirements unchecked The note names the implementation taken from the RESOLVED layer: a reader told only that something went unchecked cannot act on it. `tests/e2e/743` gains two legs rather than one. Leg C asserts the note is present, leg D that it is absent when the provider does state its list. WITHOUT LEG D, LEG C PASSES AGAINST AN ENGINE THAT PRINTS THE LINE UNCONDITIONALLY, which measures nothing. Leg C also asserts the COUNT, the one part of the message the fixture determines: a note reporting "1" or "0" would satisfy every assertion that matches only an identifier. THE OPENKAL BRANCH IS OVERRIDABLE FOR ONE RUN This repository already has one half of a cross-verification protocol: the ecosystem repositories build against an mcpp PR branch through `MCPP_SOURCE_REF`, so an engine change is measured against them before it merges. The reverse was hard-coded to `main`, which makes a change that REQUIRES a coordinated ecosystem commit unverifiable until after that commit lands --- and unmergeable until then, since `openkal-cross` is the job that fails. `__CYGWIN__`'s withdrawal is the case that showed it. `openkal-cross` builds `openkal-llvm-runtime@main`, whose installed header read only the borrowed name, so this engine's own CI reproduced the ordering constraint as a red cell. The cell is CORRECT --- the packages must publish first --- but verifying the engine before that publish needs the input. Left empty, nothing changes. E2 (the link-time set difference) is recorded in the plan as withdrawn from this release, with its blocker corrected: it is not the workload the plan claimed but a design decision. `SURFACE.txt` is not installed by any package, and docs/22 states that the engine knows no member of either set --- so where an interface-to-symbol map lives, and whether the engine can read one without learning this ecosystem's vocabulary, has to be answered before the feature has a shape. * plan: C2 is two different things, and only one of them is C2 Reading the measured diagnostics and the recipes apart: curl lib/setopt.c:31: 'linux/tcp.h' file not found cmp-module asio/detail/config.hpp:899: 'linux/version.h' file not found CURL IS A RECIPE DEFECT, the same shape as C3/expat. `compat.curl.lua` generates `#define HAVE_LINUX_TCP_H 1` inside `#if defined(__linux__)`. openkal runs on the Linux kernel, so `__linux__` is CORRECT; what is wrong is the recipe reading it as "glibc's whole Linux userspace is installed". The same block asserts `HAVE_GLIBC_STRERROR_R` --- openkal-musl is musl, so that one is actively false --- along with `HAVE_SYS_EVENTFD_H`, `HAVE_FSETXATTR` and a hard-coded host path in `CURL_CA_BUNDLE`. The honest test is `__has_include(<linux/tcp.h>)`: standard C, and it asks the question being asked rather than inferring which headers exist from which kernel. CMP-MODULE IS THE REAL C2. asio's `#include <linux/version.h>` sits outside every `ASIO_DISABLE_*` guard, so no configuration macro prevents it, and no manifest key reaches inside a third-party header --- which is justification one in `predefines.cppm`. `members.toml` already has an `[excluded]` table whose stated semantics are exactly "cannot be built in any openkal graph". What is missing is a second reason category, not a second table. E2's blocker is corrected in the same pass: not the workload the plan claimed but a design decision, since SURFACE.txt is installed by nothing and docs/22 states the engine knows no member of either set. * plan: the cross-verification protocol has a structural hole, found while using it Both ecosystem packages went green against the mcpp PR branch, AND NEITHER GREEN TOUCHED `__MCPP_TARGET_WINDOWS__`: openkal-musl linux/gcc, linux/llvm, macos/llvm; the cross-link row is Linux<->macOS openkal-llvm-runtime x86_64-linux-gnu and riscv64-none-elf No package's CI in this ecosystem builds `x86_64-windows-gnu`, and the whole subject of this change is a macro that exists only on Windows targets. The greens are real and they measure something else --- a criterion that ran, passed, and whose object was not present. The only job that builds that target is mcpp's own `openkal-cross`, and it had the ecosystem branch hard-coded to `main`, so an engine change requiring a coordinated ecosystem commit was unverifiable until after that commit landed. `openkal_ref` closes it; empty means today's behaviour. Consequence for ordering: the example in that job reaches the runtime through `path = "../.."` and the runtime's manifest pins `openkal-musl = "0.19.0"` from the INDEX, so musl 0.19.0 has to be registered before the input can be used. * plan: correct the Windows-coverage finding --- the runtime does have that leg Read job by job instead of from the first job of a run: openkal-llvm-runtime `host-dimension` has a Windows-host row that builds every target, including x86_64-windows-gnu, and it resolved [email protected] on the PR engine. The Windows leg of E1 WAS verified. openkal-musl linux/gcc, linux/llvm, macos/llvm, a Linux<->macOS cross-link, and a start-on-the-other-system job. No Windows cell at all. I concluded from the first job alone that no package in the ecosystem builds Windows-over-openkal. That was wrong and the conclusion it cast doubt on stands. The real gap is smaller and still a gap: the package whose change this round is `bits/setjmp.h` --- an installed header whose only branch is about Windows --- has no Windows cell of its own. It is verified today only transitively, because the runtime's CI pulls musl in as a path dependency and compiles it along the way. * the second layer of __cxa_thread_atexit is located, and the first hypothesis was right Following the criterion the finding itself wrote down --- instrument `run_dtors` --- with `&dtors` printed at both sites: registered dtor, dtors=0x7ffffe994680, &dtors=0x7ffffe9946a8 run_dtors called, dtors=0, &dtors=0x7ffffe9946c8 `run_dtors` IS called, which retires one branch. `&dtors` DIFFERS three times in one thread, which gives the other: `__thread` is emutls here, emutls keeps its per-thread blocks behind a pthread key of its own, that key's destructor had already released this thread's block, and every read afterwards allocates a fresh zeroed one. HYPOTHESIS ONE IN SECTION 4 WAS CORRECT AND WAS RECORDED AS REFUTED. What was wrong was the probe: musl runs key destructors in creation order, and that probe created its own key BEFORE first touching a thread-local, so emutls outlived it. The real ordering is the reverse. A probe cannot report an ordering it was constructed to avoid --- the predicate was right and the object's construction excluded the condition under test. Fixed in openkal-llvm-runtime 0.15.0 by keeping the list in the TLS key's own value, which no other key's teardown can reach. Two conditions asserted, in examples/cxx, on both targets: the thread_local is constructed, and its destructor runs. * plan: C1 is closed --- the only true unknown in the list Both layers located and fixed, shipping in openkal-llvm-runtime 0.15.0. The second layer: `__thread` is emutls under `-femulated-tls`, emutls keeps its per-thread blocks behind a pthread key of its own, and that key's destructor releases this thread's block before libc++abi's runs. Criterion: `&dtors` differs three times in one thread. It was closed by the criterion the finding document itself wrote down. And the hypothesis that document recorded as refuted was correct --- the probe created its own key before first touching a thread-local, and musl runs key destructors in creation order, so emutls outlived it. The predicate was right; the object's construction excluded the condition under test. * the finding's status word is one the checker knows `status: resolved` is not in the vocabulary the structure check accepts (active | landed | superseded | abandoned), and the agents index had not been regenerated. Both caught by `check_docs_structure.sh`, which is the check that exists so a front-matter word nobody reads does not drift. * plan: why the missing Windows cell is not a one-line matrix row Measured rather than assumed. A minimal criterion --- a setjmp/longjmp program with `_Static_assert(sizeof(jmp_buf) >= 32 * sizeof(unsigned long long))`, declaring `openkal-musl` alone, built for x86_64-windows-gnu: compiles which is the layer that matters, since a short jmp_buf is silent and a compile-time assertion is the only thing that makes it loud does not link cpow.o and others leave references unresolved; openkal-musl alone is not a complete link, because the openkal implementation and compiler-rt are assembled by openkal-llvm-runtime So a Windows cell there is either compile-only (which does catch this defect, and is cheap) or pulls the runtime in as a path dependency --- the mirror of what the runtime's own CI already does. Either is new work. * a sandbox verification for this wave, with its control reading Six sections. B and C are CHANGE and must fail on the previous release; E and F are GUARD and must pass on both. Measured, on the host, against the genuine published archive of 2026.9.21.1: 2026.9.21.1 fails=2 B: no upper-case target macro C: an unanswered requirement produced no note 2026.9.21.2 fails=0 C'\''s second leg passes on both by design: it asserts the note is ABSENT when the provider does state its list. Without it, the first leg would pass against an engine that printed the line unconditionally --- a negative control is not a hole in a CHANGE section. B carries both directions in ONE translation unit, because either alone passes for the wrong reason: an engine defining NEITHER spelling satisfies "the lower-case one is gone", and one defining BOTH satisfies "the upper-case one is here". Its second leg asks a freestanding target for its own macro, which is what says the spelling is DERIVED from the triple rather than enumerated. D and F need openkal-llvm-runtime 0.14.0 from the index and reported NOT RUN rather than passing when it was not yet registered. * plan: the criteria table carries status, and two rows split E1b and E1c did not exist when the table was written; C2 turned out to be two items with two different owners; E2 left this round with its blocker corrected. Each row now says what state it is in rather than only what would count as passing, because a table of criteria with no readings is a list of intentions. Recorded readings: the verification script's control run (2026.9.21.1 fails=2, 2026.9.21.2 fails=0), the nine green cells of openkal-cross, and the two directions the A3 check was measured in. --------- Co-authored-by: speak-agent <[email protected]>
2026.9.21.1: the borrowed __CYGWIN__ is withdrawn, by measurement (#680) * 2026.9.21.1: the borrowed __CYGWIN__ is withdrawn, by measurement `presents = "posix"` on Windows realises as a Cygwin-flavoured target. The previous release LEFT `__CYGWIN__`/`__CYGWIN32__` defined, so that portable third-party code needing to know the OBJECT FORMAT would keep a name for "PE format with a POSIX-presenting C environment", and it wrote its own condition for reversal: a trade-off for the 30-member measurement to settle, flipping if defining them produced more failures than it fixed. IT PRODUCED FOUR AND FIXED NONE. Across 60 member-target combinations, archive, sqlite3, mimalloc and c-ares each stopped at `#include <windows.h>`, reached through a guard of the shape `#if defined(_WIN32) || defined(__CYGWIN__)`. Nothing in the same run failed for want of the macro. Upstream says what it means by the name. mimalloc puts it in the guard's own comment --- `we use windows locks on cygwin, but otherwise treat it at unix` --- and sqlite3 lists it in the `SQLITE_OS_WIN` detection set before including `windows.h`. A BORROWED NAME MEANS WHAT THE LENDER'S HISTORY MADE IT MEAN, not what the borrower intended by it. The object-format question keeps no macro at all: a package asks `cfg(os = "windows")`, which needs none. If a third party is ever found that can only ask in the preprocessor, mcpp defines a name of its own. WHAT CARRIES THE CHANGE IS NOT THE TWO `-U` TOKENS. It is the two entries added to `expectUndefined`: `cenv_probe::verify` compares the realised configuration's predefines against those lists and refuses on a mismatch, so a `-U` that failed to take effect is a verification failure rather than a silent one. Measured directly with the pinned clang, in the order mcpp emits them: echo | clang -dM -E -x c - -U__CYGWIN__ -U__CYGWIN32__ \ --target=x86_64-pc-cygwin -U__CYGWIN__ -U__CYGWIN32__ -> __unix__ defined, __CYGWIN__ absent, _WIN32 absent The unit test and e2e 741 now assert the opposite side, and both record that this module has held both answers and what flipped it. The pair appears twice on a `.S` command line and once on `.c`/`.cpp`, because `cEnvTokens` reaches asmflags directly and `-D`/`-U`/`-I`-shaped tokens also arrive through the channel that carries defines into assembly; `-U X` twice is `-U X`, and the probe is the judge, so no count is asserted. Also in this release, as documents rather than engine changes: * `.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md` --- the nine items this wave did not implement, each with its criterion, its owner and what blocks it; why `lsp-mcpp-private` is the acceptance vehicle (its platform surface is six constants, none of them a POSIX facility, and it makes no direct POSIX call); and a cross-repository verification protocol: an engine PR is built by the ecosystem through `MCPP_SOURCE_REF` and both sides must be green BEFORE it merges. That protocol exists because this very change needed a second release --- a reading the thirty member graphs could have produced before the first one. * C4's premise does not hold on this stack, so nothing is implemented for it The plan listed `memset_pattern16` as an openkal-musl gap and the hard blocker for macOS acceptance, on the strength of a report that the link still fails on the new stack. The engine already does the thing that should prevent it: `cenv.cppm` emits `-fno-builtin-memset_pattern16` for `builtins = "iso"` on macOS and iOS, and openkal-musl declares exactly that. AND THE FLAG REACHES THE PACKAGES NAMED. Measured by emitting a build database for `aarch64-macos` with 2026.9.21.1: all 29 of zstd's translation units carry it, including the one on the chain the report named. Three explanations survive, none of them decidable here --- a Linux host cannot compile for macOS, there being no SDK: the report predates the flag; the synthesised idiom is `memset_pattern4`/`8`, for which the engine emits no flag (clang's `-fno-builtin-` family is one flag per idiom) though the report names `16`; or the flag does not reach LLVM's loop-idiom pass. So the next step is a reading, not an implementation. `lsp-mcpp-private`'s `aarch64-macos --profile release` leg answers it directly, and each surviving explanation has its own follow-up. Adding the symbol to openkal-musl now would be repairing a gap that may not exist. * cross-verification stopped the withdrawal, so mcpp names the target itself The first shape of this change simply withdrew `__CYGWIN__`. It was green here and green in four of five ecosystem repositories built against this branch; openkal-llvm-runtime failed, libunwind's `static_assert` reporting that `Registers_x86_64` does not fit `unw_context_t`. TWO INSTALLED HEADERS IN THIS ECOSYSTEM READ THAT MACRO ON PURPOSE, each saying so in its own source: `openkal-musl`'s `bits/setjmp.h` sizes `jmp_buf` by it, and `openkal-llvm-runtime`'s `__libunwind_config.h` sizes `unw_context_t`. Both are INSTALLED --- an application's own compile reads them --- so neither can use the package-private define its sibling `.S` files use, and `__CYGWIN__` was the only name mcpp kept defined target-wide. The 30-member measurement that justified withdrawal had counted THIRD-PARTY readers of the macro and not ours. Ours are load-bearing, and wrong is silent where it matters most: `setjmp.h`'s own comment says "a mismatch nothing reports until the record overruns". libunwind having a `static_assert` is what made this loud, not anything the measurement did. So mcpp states the fact itself. `-D__mcpp_target_windows__=1` answers the question those headers ask --- is this target Windows, whatever C environment is presented above it --- and being mcpp's own name, its meaning is not decided by anyone else's history. It is emitted only under this substitution; an ordinary Windows build still has `_WIN64`. `expectDefined` carries it, so a `-D` that failed to take effect is a verification failure rather than a silent one. The name differs from the design's proposed `__mcpp_format_pe__` because the two consumers do not want the object format: they size Win64 register save areas, which is the calling convention. The two co-vary on this target, and naming it for the question actually asked is the honest form. `__CYGWIN__` REMAINS DEFINED, as step one of three: this release adds the new name, those packages move onto it while still accepting the old one, and only then does a release stop defining the borrowed one. Step three taken first would leave every published copy of those headers falling to its `#else` --- the wrong record size, reported by nothing. Four loudly failing third-party members is the better state to hold for one release. * the macros this engine defines are a module, so the rule cannot drift `src/toolchain/predefines.cppm` is the specification and the implementation of one thing. The contract is data in that module (`kContract`), the emission is a function beside it (`define_tokens`), and `tests/unit/test_predefines.cpp` asserts the two agree in BOTH directions: a macro emitted and unlisted is a promise nobody can rely on, a row listing a macro nothing emits is one a reader waits for forever. This wave already paid once for a rule kept in a document while the code moved --- the reason-token table was "the four it was missing" twice over. GENERALISED PAST WINDOWS. `__mcpp_target_<os>__` is spelt from the triple's own `os` field, so the ENGINE LEARNS NO OPERATING-SYSTEM NAME and a target added to the triple parser gets its macro with no change here --- the same discipline `[kernel-abi]` interface names follow. Measured: `x86_64-linux-gnu` gives `__mcpp_target_linux__`, `x86_64-windows-gnu` gives `__mcpp_target_windows__`, `riscv64-none-elf` gives `__mcpp_target_none__`. DEFINED ALWAYS, not only where a realisation suppressed something. Conditional emission would make its absence ambiguous: "not Windows" and "Windows, but nothing hid its macros" would read the same, which is the shape of every defect where a "no" and a "never asked" share a reading. Lowercase, `__mcpp_`-prefixed. Two conventions exist --- vendor and product names upper (`__APPLE__`, `_WIN32`), kind-of-system names lower (`__linux__`, `__unix__`) --- and these name kinds of target, sitting beside the second family in real guards. The prefix is load-bearing: a name mcpp owns means what mcpp says it means, which is exactly what `__CYGWIN__` could not offer. `__openkal__` joins the same contract; `__unix__` is listed as SUPPLIED rather than owned, so it keeps the standard spelling and mcpp may not redefine it. THE SEPARATION IS ITSELF A TESTED PROPERTY. The macro was first realised in `mcpp.toolchain.cenv`, which made it derived from a declaration --- yet whether a target is Windows does not depend on any `[c-abi]` block existing. Moving it out turned the cenv test red, correctly; that test now asserts the realisation does NOT carry it, because a token with two owners is a token that will disagree with itself. docs/21 renders the contract in both languages, and docs/24's note that the `__CYGWIN__` trade-off "may flip" is updated: it has, and the replacement has a name. * C4 does not reproduce, and the belief that blocked measuring it was wrong `aarch64-macos --profile release` over the openkal stack links, and the artefact references no `memset_pattern` symbol at all. Measured here, on this Linux host, with mcpp 2026.9.21.1 and [email protected] through openkal-macos 0.12.0 / openkal-musl 0.18.0 / openkal-llvm-runtime 0.13.0: zstd 1.5.7 compiles, links; Mach-O 64-bit arm64, NOUNDEFS; 0 refs xz 5.8.3 compiles, links Both are on the chain the report named. The engine has emitted `-fno-builtin-memset_pattern16` for `builtins = "iso"` on macOS since before this branch, and that report was taken on an engine that did not. Nothing is implemented for C4, and nothing should be: adding the symbol to openkal-musl would have repaired a gap that does not exist. THE BELIEF THAT KEPT THIS UNMEASURED IS THE MORE USEFUL FINDING. Both this plan and the previous wave's self-review stated that macOS facts need a macOS runner, because a probe targeting `aarch64-macos` failed here. That probe declared no openkal dependency, so it took the PLATFORM path, which does need an Apple SDK. The openkal path does not --- universal cross-building is the premise of the whole system, and a Linux host reaches `arm64-apple-macos14.0` through it with every layer resolved from the graph. The cost of that belief compounded: it filed C4 as unmeasurable, and it filed openkal-macos's `provides-interfaces` as needing a runner when 0.12.0 derived it mechanically. Both corrected, with the rule beside them --- a claim that some platform cannot be measured locally has to be tested with a probe that goes THROUGH the stack under test, not one that bypasses it. --------- Co-authored-by: speak-agent <[email protected]>
the c-abi probe measures the target it verifies, and a capability is … …answered where the answer exists (2026.9.20.1) (#678) * the c-abi probe measures the target it verifies, and a capability is answered where the answer exists (2026.9.20.1) Four changes, one subject: information about a capability becomes available at three times, each being the earliest at which it exists (openkal SPEC 0.14 §6.2), and this engine had been answering two of those questions at the wrong one. ## The probe measured the build host The c-abi verification probe selected no target on every freestanding build. `Toolchain::crossTargetFlag` is set for hosted targets only --- its own comment states the reason --- and a freestanding target's `--target` travels with the ISA flags that must accompany it, in `mcpp.freestanding.linkline`, which the probe never asked. `cenv::realise` adds none either. The command line was therefore `-D__unix__ -fno-short-wchar -ffreestanding -x c++ -E -dM -`, and clang answered for the machine it was running on. On a Linux host that machine satisfies `__unix__` defined, `_WIN32` undefined, 32-bit `wchar_t`, so the check passed for the wrong reason. On a Windows host it reported `_WIN32` defined and a 16-bit `wchar_t`, and 2026.9.18.3 read those two as a `--target=` substitution failing to strip host predefines. Clang's predefines follow the target: `--target= riscv64-none-elf` on a Linux host reports `__riscv`, no `__linux__`, and `__SIZEOF_WCHAR_T__` 4. Had that flag been on the command line it would have answered 4 rather than 2; 2 is the evidence that it was absent. The probe now receives the freestanding compile prefix. `hostStripMacros` is removed, and its removal is the point rather than a tidy-up: it deleted the one piece of evidence that said the probe was measuring the wrong machine. `cenv_probe::assemble_argv` holds the invariant --- every piece is legitimately empty in some configuration, so none of them could carry it at the call site --- and refuses a freestanding argv that selects no target while accepting a native hosted one, where the host IS the target and the absence is the decision rather than its omission. The three `hostStripMacros` tests are replaced. They pinned whether `-U` reached the command line, which is the mechanism; what matters is whether the probe measures the right machine, which is the property. The five new `CenvProbeArgv` tests reach it with no cross toolchain at all. ## builtins: the conclusion stands, the mechanism beside it did not `cenv.cppm` said clang's bundled `intrin.h` / `mm_malloc.h` were already closed by `-nostdlibinc`. That flag removes the standard system include directories and leaves clang's own resource directory in place, which is what `-nobuiltininc` removes; with it present `intrin.h:12:15` still reproduces, character for character the diagnostic mcpp-index recorded for fmtlib.fmt. What closes both is the Cygwin-flavoured realisation: `mm_malloc.h:42` selects `__mingw_aligned_malloc` on `__MINGW32__` and falls to `posix_memalign` without it. No Windows loop-idiom builtin to disable, as before. ## A capability is enumerated by the consumer, at resolution openkal SPEC 0.14 §3.3 withdrew `hosted`, the one name it had given to a set of interfaces, because a name describing a class of environment is falsified by an environment nobody had in mind --- and that one was falsified inside its own ecosystem within a release. Enumeration by the consumer replaced it. mcpp now carries that enumeration for the `kernel-abi` layer: `provides-interfaces` on the package that supplies it, `requires-interfaces` on anyone. The engine learns no member of either set. The only operation is a set difference, so a specification may add an interface without a release of mcpp, and a misspelling produces a refusal naming the string rather than a silently disabled check. A graph whose provider states nothing keeps building: a package that states nothing is not a package that provides nothing. ## [c-abi.absent]: enumerate the exception, not the rule POSIX has about twelve hundred names and enumerating them is the mistake §3.3 records withdrawing. The exceptions are enumerable --- openkal-musl's README lists six, in prose that nothing executes and that was contradicted once already. `form` is required and closed: `link` is the shape openkal's own model requires of an implementation (§6.1 calls a run-time report of unsupportedness a defect), and the other two are departures from it, named so that a departure is something that can be counted. mcpp reads the list back when a link names a `link`-shaped entry. ## presents is frozen docs/22 now states that `presents` answers which environment-identity macros source sees and answers nothing about capability, and that its value set does not grow --- for the reason openkal gives for closing its core set. Design: .agents/docs/2026-09-20-openkal-c-environment-ecosystem-design.md Review: .agents/docs/2026-09-20-issue-674-design-review.md Co-authored-by: Claude Code <[email protected]> * the [c-abi.absent] parser gets its own function, and the docs index is regenerated Two CI reports, both from the first push. clang 20.1.7 on Windows crashed during LLVM IR generation of `parse_string` (exception 0xC0000005, the frame naming the `[c-abi.absent]` block's compound statement). Every other host compiled the same source. This codebase has met the shape before --- a construct that is fine in a function and not in a large one inside a module interface unit --- and the answer is the same: the block becomes `parse_c_abi_absent`, a free function, and the reason is written beside it so the next reader does not inline it back. `.agents/docs/README.md` regenerated, and the ecosystem design's front matter carries a `status` the structure check knows. Co-authored-by: Claude Code <[email protected]> * the [c-abi.absent] parser is an internal helper, in the plainest form that compiles everywhere clang 20.1.7 on Windows crashed generating code for this parser in three spellings: as a block inside `parse_string`, as a free function in the module's exported purview returning `std::expected<std::vector< CAbiAbsentEntry>, std::string>`, and with a pointer-to-member projection in its sort. Every other host and compiler took all three. What it takes is the plainest form available, and each part of that form is now measured rather than stylistic: an internal helper in the anonymous namespace rather than an inline function in the purview that every importer emits; an out parameter and `std::optional<std::string>` rather than `expected` over a vector of structs carrying strings; a comparator rather than a projection. The notes are beside it so a later tidy-up does not restore a shorter spelling and rediscover this on a Windows runner. Co-authored-by: Claude Code <[email protected]> * docs/22: which of the two new keys an older engine ignores, and which it refuses `[kernel-abi]` is an unknown top-level table to an engine that predates it and is ignored, so a package adopting it needs no floor move. `[c-abi.absent]` is a new key inside a table mcpp does know, where an unrecognised key is a parse error naming it --- the mechanism that keeps a misspelling from silently disabling a declaration --- so a package adopting that one does. Co-authored-by: Claude Code <[email protected]> * the execution record: what the measurement settled, and the three defects found on the way Co-authored-by: Claude Code <[email protected]> * the wave's sandbox verification, and the reading from each of its two runs Every CHANGE section is run against the published engine first and must fail there. Recorded in the script: 2026.9.17.1 reports fails=3 and 2026.9.20.1 reports fails=0, and the one leg that passes on both is named together with why that is the documented behaviour rather than a hole. Co-authored-by: Claude Code <[email protected]> * two defects found reviewing this branch: the wrong provider, and a name that is a prefix of another The interface list was read from the first package in the graph that stated one. A graph may carry more than one candidate for a layer --- a workspace member beside a dependency, a second implementation behind a feature that did not activate --- and only one of them is the provider this build resolved. Comparing a consumer's requirements against an implementation the build is not using is a wrong answer rather than a missing one. It now reads the resolved layer. `c_abi_absent_facility_advice` matched the symbol as a substring, and `undefined symbol: open` is a prefix of `undefined symbol: opendir`. A link failure would have been explained by a row with nothing to do with it, and an explanation that is confidently wrong is worse than the linker's own message. The name must now end where the diagnostic's name ends. Co-authored-by: Claude Code <[email protected]> * the record: what this wave did not do, and the first two implementations whose sets differ Co-authored-by: Claude Code <[email protected]> * the resolved provider is matched by name, not by substring `Layer::impl` is `name@version`. A substring test matched `openkal` against `[email protected]` and would have read one implementation's interface list as another's. The name is what precedes the separator. Co-authored-by: Claude Code <[email protected]> * the wave's self-review: five defects, and the one place this landing differs most from its design Three of the five were found by reading the branch's own diff and were green on every host: the interface list read from the first package that stated one rather than the resolved provider, a package name matched as a substring of `name@version`, and a symbol matched as a substring of a longer one. The other two were found by CI. The largest gap between the design and what landed is P7's L3, the link-time set difference. Without it a consumer's `requires-interfaces` is still only trusted: a package may declare fewer interfaces than it calls, pass resolution, and link on an implementation that provides them --- failing only on one that does not, on a user's target rather than in its author's build. Co-authored-by: Claude Code <[email protected]> * the design under-estimated L3, and the two sentences that cannot both hold §5.7.3 said the link-time check maps undefined symbols to interfaces through SURFACE.txt; §5.5 said the engine knows no member of either set. The mapping can only come from the graph, so L3's real shape is a second generated table in each implementation's manifest --- about a hundred lines, from the same script. The cost and the benefit are both different from what was written, so L3 is not in this landing, and the consequence of its absence is now written down rather than left implied: a consumer's requires-interfaces is still only trusted. Co-authored-by: Claude Code <[email protected]> * the absent-facility advice reaches both channels a failed build reports through A build reports failure on two paths: the one with a plan, and the fast path, which has none by construction. `ninja_backend.cppm` already records why that matters for `link_failure_advice` --- advice attached to only one of them "would appear or not depending on whether build.ninja happened to be up to date, which is exactly the kind of 'same decision in two places' this codebase keeps paying for" --- and the new `[c-abi.absent]` advice had been attached to one. The list travels between them in a file beside build.ninja, written when the plan emits it and opened only after a build has already failed. The fast path's purpose is to read as little as possible, so it reads nothing extra until there is a failure to explain. A graph that declares no absence removes the file rather than leaving it: a stale one would let the fast path explain a failure with a list the current graph never declared. Co-authored-by: Claude Code <[email protected]> * the design points at the record of what landed and what did not Co-authored-by: Claude Code <[email protected]> * the two probe tests that never ran anywhere now run `find_a_cxx_compiler` answers `/usr/bin/c++` first, which is GCC on every host this suite runs on, and GCC takes no `--target`. The two tests that check the probe measures the target rather than the host therefore skipped on every host including the CI shards with an LLVM toolchain --- coverage that looks like coverage and is not, which is the shape the rest of this branch exists to remove. They now look for a clang, and for one that actually has the back end they name: the host this was written on carries a vendor clang with neither RISC-V nor AArch64, so a PATH-only search would have skipped for a reason that has nothing to do with what is being tested. The search asks each candidate before accepting it, and falls back to the LLVM payload mcpp installs, which every shard resolving an `llvm@` toolchain has already downloaded. Measured here: 15 passed, 0 skipped, where it was 13 passed and 2 skipped. Co-authored-by: Claude Code <[email protected]> * the #674 Path C design is marked superseded, with a pointer to what replaced it Co-authored-by: Claude Code <[email protected]> * the probe fix, read from the probe's own cache rather than from a log After a freestanding build with a [c-abi] graph, the newest -dM dump in ~/.mcpp/build-cache/v1/cenv-probe reports __riscv, no __linux__, and a 4-byte wchar_t. The older dumps beside it report the opposite and are correct: those are hosted Linux targets, where the host is the target. What distinguishes them is the first line, and a freestanding probe used to look like the second. Co-authored-by: Claude Code <[email protected]> * the refusal carries its code, because something reads it The mcpp-index compatibility measurement distinguishes "this graph does not supply what the member asked for" from "the member did not build" --- a distinction that decides whether a member counts against a compatibility figure --- and it was written to do so on the refusal code `interface-not-provided`. That code reached no output: `refusal::record` is read by `mcpp doctor` and by one branch of the build-database path, and a `mcpp build` prints none of it. The consumer's judge could never fire. The code is now printed in the diagnostic, the way E0006 is, and e2e 743 asserts it. A refusal that only a person can recognise forces every machine consumer to match prose --- and prose that a package's own compile error could coincidentally contain. Found by running the consumer's own criterion against the engine rather than reading it: a mechanism with no way to trigger is the defect class this whole branch exists to remove, and it had one of its own. Co-authored-by: Claude Code <[email protected]> * the refusal-token table gains the four it was missing c-env-unrealisable, c-env-verification-mismatch and platform-dependency have been emitted since 2026.9.18.1 and were never listed, so a program classifying an outcome read them as `other` --- the token the table itself describes as "a refusal whose branch has not been given a token yet". interface-not-provided joins them, together with the note that it is the one token `mcpp build` also prints in its own message. Co-authored-by: Claude Code <[email protected]> * the zh note is placed beside the token table, not in the page's own preamble The previous commit inserted it at the first paragraph a regular expression matched, which was the page's "not here" line, and left that line reading as the note's opening. Repaired and placed where its English counterpart is: after the token table and before the exit-code paragraph. Co-authored-by: Claude Code <[email protected]> * the end-to-end reading: one requirement, two implementations, two answers Against the published implementations, a package stating `requires-interfaces = ["openkal.space"]` builds on Linux and is refused on Windows before anything is compiled --- because openkal-linux provides fifteen interfaces and openkal-windows fourteen, and the refusal names which, whose, and how many. This is the first instance of the reason SPEC clause 3.3 gives for withdrawing `hosted`: a name for a class of environment would have made these two implementations look alike. Also recorded: `mcpp index update` reports `index updated` while leaving the artifact sha where it was. Clearing the cache marker was not enough; the directory had to go. The criterion is the sha in `.xlings-index-version`, not the command's exit code and not the line it prints. Co-authored-by: Claude Code <[email protected]> * the release notes name the reason token, as this file's own convention does `interface-not-provided`, together with the three that have been emitted since 2026.9.18.1 and were never listed in the token table. A release note that describes a refusal without naming the token leaves the machine consumer to find it by reading the source. Co-authored-by: Claude Code <[email protected]> * the assembler asked PATH first, and every other tool in this engine does not `find_usable_nasm` called `which("nasm")` before looking in the sandbox. A machine with an assembler installed assembled with that one; a machine without downloaded the pinned `xim:nasm`. Three machines could produce three different objects from one source tree and no line of any of those builds said which assembler made them. Nothing else here is arranged that way: the compiler and the linker are a payload, the C library and the C++ runtime are packages, ninja and patchelf come from xlings, and ar/strip/objcopy are derived from the resolved toolchain's own directory and never as a bare name. The order is reversed. The host copy is kept --- an offline machine that already has a usable assembler should still build --- and when it is the one that served, the build names it rather than leaving it silent. docs/20 gains a section listing every host item mcpp reaches and the reason each is still there, in both languages. A host tool that reaches a build is not by itself the defect; a host tool that reaches a build silently is, which is why that section is a table rather than a prohibition. Co-authored-by: Claude Code <[email protected]> * the absence table is top-level, because the nested spelling refuses the manifest `[c-abi].absent` reads better and is where this was written. Measured against the genuine published 2026.9.18.3 archive -- the index floor at the time -- on the exact manifest openkal-musl 0.17.0 publishes: nested, every mcpp older than this release refuses THE WHOLE MANIFEST on every target with `[c-abi] has no member 'absent'`. The `[c-abi]` parser enumerates its members and rejects the rest, and that strictness is correct: a misspelled `presents` must not silently disable a declaration. In the same measurement an unknown TOP-LEVEL table is ignored and the build completes. Everything this table does is diagnostic. `c_abi_absent_facility_advice` annotates a link that has already failed; no flag, link line or artifact depends on it. So an engine that ignores it produces exactly the raw linker error it produces today, while an engine that refuses it takes the package away entirely and forces the index floor up to this release -- costing every client stopped below that floor the whole index, for a note they merely would not have received. Top-level, openkal-musl 0.17.0 asks nothing of `min_mcpp`. The two spellings are indistinguishable in the parsed result, so no test that reads only the outcome can tell them apart. Three tests state the shape directly: `absent` is not a member of `[c-abi]` and the refusal names the top-level spelling; absences may be stated without an environment declaration, which leaves `declared` false; and the provider gate `[c-abi]` applies is restated rather than inherited, since moving the table moved it out from behind that gate. `prepare` now tests `cAbiDecl->declared` rather than the optional, because a `[c-abi-absent]` table alone sets the optional and has nothing in it for `cenv::realise` to realise. * the host-surface table was written from one sweep, and three were missing The table in docs/20 says it is "the whole of what does not" come from the graph or from xlings. It was derived from a single sweep -- every `fs::which` call -- and three other shapes reach the host without passing through one. Two of them are rows the table owes a reason for, and now has: * A command interpreter. `[hooks]` runs through `run_shell_deadline`, the xlings CLI through `run_streaming_bounded`, and the detached codegen command through the same `/bin/sh` (`cmd.exe` on Windows). A hook is a line the USER wrote in shell syntax; shipping a shell would change the language that line is read in, so what mcpp depends on here is not a tool it could package. * The MSVC toolset and the Windows SDK, reached when a user names `msvc@system` or when a managed toolset has no SDK payload beside it. Not redistributable, the same category as the Apple SDK -- and already handled the way the assembler now is: a managed toolset BINDS its SDK and ignores `WindowsSdkDir` even when set, because a pin the environment can overwrite is not a pin, and the fallback to the machine's SDK carries a note the caller must surface. The third points the other way and is recorded for that reason: `src/runtime/ elf` writes `/usr/lib` and `/usr/lib64` only to MODEL a loader's run-time search, and `mcpp.toolchain.registry` REFUSES a payload descriptor whose `frontend` names `/usr/bin/g++`. The four sweeps are now written into the page, with the exact count each yields, so the next reader checks the list rather than trusting it. The count of `fs::which` call sites is five, and each is a row. * the self-review gains the two findings that neither the diff nor CI could reach Findings 6 and 7 of this wave, and the reason each needed something the machine doing the work did not have. Six: `[c-abi.absent]` nested in a table the engine already knows, so every older mcpp refused the whole manifest. Invisible to a local build (this tree's mcpp is the one that knows the key), invisible to a test that reads the parsed result (both spellings parse to the same `CAbiDecl`), and invisible to CI. The only criterion was the binary the index's `latest` points at, fed the manifest openkal-musl 0.17.0 would publish. Seven: the host-surface table in docs/20 called itself exhaustive on the strength of one sweep. Two rows were missing. Their shared shape is worth the entry: something was written, and green, while the premise it rested on had been measured by nothing. Six rested on "an older engine ignores a new key"; seven on "what I listed is all of it". The compatibility row of §2 and step 4 of the execution plan said the two keys differed and that one of them needed a floor raise. Both now say what is true: both keys are top-level, both are ignored, neither asks anything of `min_mcpp`, and openkal-musl 0.17.0 no longer waits on this release. * one predicate, named once, because both tables gate on it * the recorded readings are the ones the top-level table actually produces The verification script carried a reading taken while the absence table was still nested inside `[c-abi]`: three failures against 2026.9.17.1, one of them `[c-abi] has no member 'absent'`. That failure no longer exists, and recording it would have had the sandbox run confirm a shape the engine no longer has. Re-measured against the genuine published 2026.9.18.3 archive: fails=2, and fails=0 on this release. Each CHANGE section now has one leg that passes on BOTH, and the header says why that is the evidence rather than a hole -- both new tables are top-level, an older engine ignores them, so a graph that satisfies its requirements builds either way. What distinguishes the releases is the refusal in each, because an engine cannot refuse what it never read. The self-review's corresponding row moves with it. * the note about a declared absence was missing a closing parenthesis Every reader of it saw: note: the C library in this graph (musl declares that it does not supply The C library's name was interpolated from two separate conditionals --- an opening paren, then the name --- and the closing one was never emitted. It is one substitution now. NINE UNIT TESTS COVERED THIS FUNCTION AND NONE OF THEM SAW IT. They asserted `a.find("musl")`, which is true of the broken spelling as well: a criterion aimed at a substring of a sentence cannot see the sentence. The test now asserts the rendered clause, and a second one covers the other side of the same substitution --- an unnamed C library must leave no empty parentheses rather than `graph () declares`. WHAT ACTUALLY FOUND IT was running a real link. The nine unit tests covered the matching rules and the sidecar round-trip, and seven more covered parsing the table, but nothing ran the two together: that a real build writes the sidecar beside build.ninja, that a real link failure reads it back, and that what arrives is a sentence. e2e 744 does that, in four legs --- the linker's own message survives, the row's note reaches it, the sentence reads as one, and an absence is not reported as a defect in the build. Restoring the missing paren fails the third and only the third. The symbol the test uses is one nothing defines. `fork` is the real row in openkal-musl's manifest and every C library on a Linux host defines it, so a test written with it would link and assert nothing. * the label under a missing interface read as the claim the refusal denies The refusal listed what the implementation does not provide and then, directly beneath it, said: openkal.space provided by fakekernel (2 interfaces) which parses as "openkal.space is provided by fakekernel" --- the statement this refusal exists to deny. It now reads "the resolved implementation is fakekernel (2 interfaces), and none of those listed above is among them". EVERY ASSERTION ON THIS MESSAGE MATCHED AN IDENTIFIER, and an identifier sits in the right place under either wording: `grep -q fakekernel` is true of the sentence that says the opposite. e2e 743 now asserts the clause and rejects the old label explicitly; restoring it fails leg B and only leg B. This is the same shape as the missing parenthesis in the absence note, found the same way --- by rendering the message and reading it. The other five diagnostics this release adds were rendered too, and read correctly: `absent` written inside `[c-abi]` (which names the top-level spelling), `[c-abi-absent]` on a package that supplies no C library, an unknown `form`, a row that is not a table, and a table that is not a table. * e2e 744 depended on the host having a C runtime, and on a key mcpp ignores TWO DEFECTS IN ONE TEST, AND THE SHARD FOUND THE FIRST. The link it examines was an ordinary hosted one, so it needed startup files that the graph's C library --- a marker package supplying no C library at all --- does not provide. They came from the host: present on a developer's machine, absent in the container the Linux shard runs in, where the link died before it ever reached the symbol under examination: /usr/bin/ld: cannot find crt1.o: No such file or directory A test whose subject is a link diagnostic must not depend on anything else about the link succeeding up to that point. It is freestanding now (`-nostdlib -nostartfiles -static`, and an entry point of its own), so exactly one symbol is undefined and it is the one being examined. THE SECOND DEFECT WAS IN THE REPAIR. Those flags were first written as `[targets.<name>] ldflags`, which is not a key mcpp has: the manifest reports `unsupported key 'ldflags' (ignored)` and carries on. The link then still failed, still named the symbol, and still carried the note, so all four legs passed while the arrangement they rest on had not happened. `build.ldflags` is the spelling that exists, and the flags are now measured on the emitted link line rather than assumed. Leg E asserts the arrangement: no key was ignored, and the link never reached the host's C runtime. Putting the flags back in the target fails E and only E --- A through D go on passing, which is the whole reason E is there. A warning printed into a passing test is invisible. * the reason-token table and the engine no longer agree by being read docs/50's reason-token table is a machine interface: mcpp-index's compatibility measurement reads a token out of a refusal to tell "this graph does not supply what the member asked for" from "the member did not build", and that distinction decides a published figure. A token the engine emits and the table omits is a promise nobody can rely on. FOUR MORE WERE MISSING, AND THIS WAVE HAD ALREADY "ADDED THE MISSING ONES". `apple-sdk-absent`, `lld-required-absent`, `host-tool-toolchain` and `std-module-precompile` predate this branch and were absent the whole time; the earlier commit in this same branch added four others by reading the table against the code. A set compared by reading is a set compared by sampling. check_reason_tokens.sh compares both directions mechanically --- every token `refusal.cppm` can emit is a row, every row is a token some branch emits --- and requires the 简体中文 mirror to carry the same set, which a structural check counting headings cannot see. Removing a row from either page, or adding one no branch emits, fails it; all three were exercised. THE CHECK FIRST REPORTED `reason` AS A TOKEN. The table's COLUMN HEADER is `| `reason` | |`: a backticked name in the first cell, the same shape as every row under it. Selecting rows by "a backticked name at the start of a line" selects the header too. What distinguishes a row is a non-empty second cell, so the pattern requires one --- the object set is chosen by a property rather than by a syntax that the header also satisfies. Also corrects the assembler's degradation example in docs/20 and its mirror. It was written as `degraded:` with ten-space continuations; `diag::degraded` renders through `ui::warning`, so the real output is `warning:` with `impact:` and `hint:` labels, as every other example in these documents shows. --------- Co-authored-by: speak-agent <[email protected]> Co-authored-by: Claude Code <[email protected]>
Windows-host c-abi probe strips host predefines; freestanding wchar r… …ealisation always emits -fno-short-wchar (2026.9.18.3) (#673) * The c-abi probe on a Windows host strips the host's leaked macros; the wchar realisation on a freestanding target no longer assumes the toolchain default is 32 bits (2026.9.18.3) * fix cenv: the freestanding branch must emit -fno-short-wchar unconditionally bb0437d dropped the freestanding ? 32 : native_wchar_bits(os) special case that 7097acd had added, on the grounds that it was wrong on Windows hosts. That is right for the -U_<WIN32>-stripped probe, but for the REAL compile (the one whose cflags the engine broadcasts to the package's own translation units) the toolchain's freestanding default on Windows is host-contaminated: clang on Windows emits a 16-bit wchar_t by default for -target=riscv64-none-elf unless told otherwise. So a declaration of wchar=32 needs -fno-short-wchar on every freestanding target regardless of what native_wchar_bits(os) says, and the previous "if (decl.wcharBits != native)" gate lets a 32-bit declaration through to a 16-bit compile on Windows freestanding. Symmetric for wchar=16: a freestanding Linux/macOS host's default wchar is 32, and the declaration 16 needs -fshort-wchar. The "hosted" branch (the else-if) keeps the old behaviour; only the freestanding branch is restructured. Verified locally with openkal-musl#37 and openkal-llvm-runtime#24's matrix against mcpp built from this branch. * cenv_probe: cacheRoot before hostStripMacros; presents=none emits -fno-short-wchar The previous parameter order put hostStripMacros before cacheRoot. That meant callers passing a custom cache directory (the test suite does this in every TEST) had to also pass an empty hostStripMacros, even though they did not care about the strip. Reordering puts cacheRoot first so the strip's default value (`{}`) is what callers that override cache actually mean, and tests that want to assert the strip's behaviour pass both. prepare.cppm's call passes both, in the reordered order. The "presents = none" wchar branch used to assert tokens.empty(). That is wrong under the new freestanding-wchar rule: -fno-short-wchar is emitted regardless of presents, because the wchar realisation is about width (the toolchain's host-contaminated default), not about identity (what macros the preprocessor states). Updated to assert the flag's presence and the absence of any identity macros. * test_cenv_probe: pin the strip parameter end-to-end against a name no host predefines * test: re-trigger CI --------- Co-authored-by: sunrisepeak <[email protected]> Co-authored-by: speak-agent <[email protected]>
PreviousNext