Skip to content

the c-abi probe measures the target it verifies, and a capability is answered where the answer exists (2026.9.20.1) - #678

Merged
Sunrisepeak merged 32 commits into
mainfrom
cenv-probe-target-and-interface-enumeration
Sep 20, 2026
Merged

Sunrisepeak merged 32 commits into
mainfrom
cenv-probe-target-and-interface-enumeration

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

依据:.agents/docs/2026-09-20-openkal-c-environment-ecosystem-design.md(生态级设计)与 .agents/docs/2026-09-20-issue-674-design-review.md(实测核查)。

四处改动同一个主题:一项能力的信息在三个时刻出现,每个是它最早能存在的时刻(openkal SPEC 0.14 §6.2),而这个引擎此前把其中两个问题回答在了错误的时刻。

1. 校验探针量的是构建宿主

Toolchain::crossTargetFlag 只为 hosted 目标设置;freestanding 目标的 --target 与随行 ISA 标志在 mcpp.freestanding.linkline,探针从不问它;cenv::realise 对 freestanding 也不产 --target。于是探针命令行是

-D__unix__ -fno-short-wchar -ffreestanding -x c++ -E -dM -

没有任何目标选择,clang 回答的是它所在的那台机器。

宿主 探针读到 结果
Linux __unix__ 有、_WIN32 无、wchar 32 以错误的理由通过
Windows _WIN32 有、wchar 16 失败,被读成"--target= 替换没剥掉宿主预定义"

实测否掉该归因(预定义跟随目标不跟随宿主):

$ clang --target=riscv64-none-elf -dM | grep -c __linux__        → 0
$ clang --target=riscv64-none-elf -dM | grep __SIZEOF_WCHAR_T__  → 4
$ clang --target=x86_64-w64-windows-gnu -dM (Linux 上)          → _WIN32 1

若那个 --target 真在命令行上,clang 会答 4 而不是 2。观察到 2,正是它不在的证据。

修法:探针拿到 freestanding 编译前缀;hostStripMacros 删除(它抹掉的正是"量错了机器"的唯一证据);新增 cenv_probe::assemble_argv 在装配处拒绝这次遗漏——各部件在某些配置下都合法地为空,所以没有任何单独一个能承载这条不变量。

三个 strip 测试被替换:它们测的是"-U 有没有到命令行"这个机制,不是"探针量的是不是正确的机器"这条性质。五个新 CenvProbeArgv 测试不需要交叉工具链

2. builtins 的 Windows 行:结论不变,机制写错了

-nostdlibinc 不关 clang 的 resource dir(那是 -nobuiltininc),带着它仍复现 intrin.h:12:15——与 mcpp-index 为 fmtlib.fmt 记录的诊断逐字符相同。真正关掉它们的是 __MINGW32__ / _WIN32 的消失。

3. [kernel-abi] provides-interfaces / requires-interfaces

openkal SPEC §3.3 撤回过 hosted——"一个描述环境类别的名字会被没有人想到过的那个环境证伪",而它在自己的生态里一个发布周期内就被证伪了;替代做法是消费者逐条列举。

引擎不认识这两个集合的任何一个成员:唯一操作是集合差,所以规范新增接口不需要 mcpp 发版。不满足即在编译任何东西之前拒绝,同时点名缺的接口、要它的包、没提供它的实现。

一个什么都没陈述的提供者,不是一个什么都不提供的提供者:没写 provides-interfaces 的图照常构建。

4. [c-abi.absent]:枚举例外,不枚举规则

POSIX 约一千二百个名字,正向枚举正是 §3.3 撤回的那个错误;例外可枚举。form 必填且封闭:link 是 openkal 模型要求的形状(§6.1 把运行期报告不支持称为缺陷),另外两个是偏离,命名它们是为了让偏离成为可以被数出来的东西。

5. presents 取值集冻结

docs/22 写明它不回答任何能力是否存在,取值集不增长。

判据

  • mcpp test 全绿(123 个测试二进制)
  • 新增 e2e 743:三条腿(满足→构建 / 不满足→拒绝且未编译任何东西 / 提供者什么都没说→构建)
  • CenvProbeArgv 五个测试不 skip,无需交叉工具链
  • 未声明任何新键的包,命令行逐字节不变

macOS 27 的 job 红是已知的 lld 22.1.8 / arm64e.x1,上游发版前不可能绿。

speak-agent and others added 30 commits September 20, 2026 18:24
…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 <noreply@anthropic.com>
…s 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 <noreply@anthropic.com>
… 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 <noreply@anthropic.com>
… 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 <noreply@anthropic.com>
…ects found on the way

Co-authored-by: Claude Code <noreply@anthropic.com>
… 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 <noreply@anthropic.com>
…me 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 <noreply@anthropic.com>
…ons whose sets differ

Co-authored-by: Claude Code <noreply@anthropic.com>
`Layer::impl` is `name@version`. A substring test matched `openkal` against
`openkal-linux@0.15.0` and would have read one implementation's interface list
as another's. The name is what precedes the separator.

Co-authored-by: Claude Code <noreply@anthropic.com>
…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 <noreply@anthropic.com>
… 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 <noreply@anthropic.com>
…ts 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 <noreply@anthropic.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
`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 <noreply@anthropic.com>
…eplaced it

Co-authored-by: Claude Code <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…reamble

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 <noreply@anthropic.com>
…wers

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 <noreply@anthropic.com>
…n 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 <noreply@anthropic.com>
…oes 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 <noreply@anthropic.com>
…he 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.
…sing

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.
…ould 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.
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.
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 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.
…nores

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.
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.
@Sunrisepeak
Sunrisepeak merged commit cfe4696 into main Sep 20, 2026
40 of 42 checks passed
@Sunrisepeak
Sunrisepeak deleted the cenv-probe-target-and-interface-enumeration branch September 20, 2026 14:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants