Skip to content

Commit 3d59901

Browse files
committed
2026.8.26.2 — the answer was already resolved, and nothing consulted it
Two defects the previous release's family does not cover. There the predicate asked a narrower question than the one it was given; here the predicate asked the right one, got the right answer, and the answer never reached a decision. Both are a recorded field with no decision-side reader. `requires` was checked, never applied. `prepare.cppm` has a block titled "the toolchain, resolved now that the graph exists"; it scans `provides` to decide the compiler and does not scan `requires_`, which is collected a thousand lines later and used only to reject the outcome. Measured with llvm@22.1.8 already installed: `mcpp build` refused and advised `mcpp toolchain default llvm` — a global change, for one project's dependency — while `MCPP_TOOLCHAIN=llvm@22.1.8 mcpp build` finished in 1.02s. Selecting it writes nothing, and that is where the decision sits rather than a rule to remember: `resolve_target_toolchain` has two call sites, both after the graph, so the first-run install-and-persist branch and all three `write_default_toolchain` calls are downstream. On a machine with no toolchain the branch is not even entered — its condition is `!tcSpec.has_value()`. Refusal now happens only where the project stated its own compiler, and the advice points at that statement; two packages requiring different families is an error naming both. The tier gate asked about the identity, not the request. `parse` fills a missing env segment lexically so the identity stays total, and `envExplicit` exists to record that it was a fill — the gate did not read it. `--target aarch64-linux` refused as `aarch64-linux-gnu` (planned) while `aarch64-linux-musl` built, and `riscv64-linux` was reported `unknown` although the family is registered. A request that declined the segment is now completed against the vocabulary, with the lexical default tried first so the rule retires itself. `parse()` is unchanged: identity must stay lexical, total and host-independent. Two diagnostics stopped lying. `unknown target` no longer fires when the (arch, os) group is non-empty, refusals quote the spelling the user wrote, and the unknown path finally records a code instead of reporting `other`. `why toolchain --format json` gave two answers for the C library — `cLibrary` said glibc/payload while `layers[].c-abi` said musl/graph, and the artifact (static, no interpreter, no DT_NEEDED, 11 openkal symbols) settled it. Adds `cLibrary.suppliesTarget` rather than renaming a field, per docs/11 §6. Criteria: e2e 299-303 classify through `--format json` and run on all four build hosts via the target-matrix invariants layer. 301's criterion is the sha256 of config.toml, not a successful build — those can both be true, which is the behaviour being removed. 299's second half is the control that `x86_64-linux` is still gnu.
1 parent 4cc7e6a commit 3d59901

24 files changed

Lines changed: 2092 additions & 22 deletions

.agents/docs/2026-08-26-resolved-but-not-consulted.md

Lines changed: 545 additions & 0 deletions
Large diffs are not rendered by default.

.github/workflows/ci-target-matrix.yml

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,10 @@ jobs:
8080
command -v jq >/dev/null || { echo "::error::jq is missing on ${{ matrix.host }}"; exit 1; }
8181
fail=0
8282
for t in tests/e2e/295_*.sh tests/e2e/296_*.sh \
83-
tests/e2e/297_*.sh tests/e2e/298_*.sh; do
83+
tests/e2e/297_*.sh tests/e2e/298_*.sh \
84+
tests/e2e/299_*.sh tests/e2e/300_*.sh \
85+
tests/e2e/301_*.sh tests/e2e/302_*.sh \
86+
tests/e2e/303_*.sh; do
8487
echo "=== $t ==="
8588
bash "$t" 2>&1 | tee "$(basename "$t").log" || true
8689
rc=${PIPESTATUS[0]}
@@ -173,6 +176,44 @@ jobs:
173176
"OK: a convention may be overridden, but not merely removed" \
174177
"llvm is not installed here" || fail=1
175178
fi
179+
180+
# ── 2026.8.26.2: an answer mcpp already had, now used ─────────────
181+
#
182+
# ⭐ 299/300/303 CARRY NO SKIP AT ALL, so they use `check` on every
183+
# host. They read the vocabulary and the query's own document —
184+
# neither depends on which payloads this machine happens to hold, and
185+
# a version of them that skipped anywhere would be a version that
186+
# could skip everywhere.
187+
check 299_a_request_that_named_no_c_library_resolves_to_a_row_that_exists.sh \
188+
"OK: a request that named no C library resolves to a row that exists" || fail=1
189+
check 300_a_registered_family_is_not_reported_unknown.sh \
190+
"OK: a registered family is not reported unknown" || fail=1
191+
check 303_the_query_gives_one_answer_for_the_c_library.sh \
192+
"OK: the query gives one answer for the C library" || fail=1
193+
194+
# ⚠️ 301/302 NEED TWO COMPILER FAMILIES, AND THAT IS A PROPERTY OF THE
195+
# MACHINE RATHER THAN OF THE CLAIM. "A requirement that DIFFERS from
196+
# mcpp's own answer is applied" cannot be stated where only one family
197+
# exists — macOS installs llvm only, and aarch64 Linux has no llvm
198+
# payload at all (see 298's note).
199+
#
200+
# ⭐ AND THE DENOMINATOR IS linux-x86_64, WHICH HAS BOTH. Without a
201+
# host required to actually run these, a reason accepted everywhere is
202+
# a test that runs nowhere.
203+
if [ "${{ matrix.host }}" = linux-x86_64 ]; then
204+
check 301_the_graphs_compiler_is_taken_and_nothing_is_written.sh \
205+
"OK: the graph's compiler is taken and nothing is written" || fail=1
206+
check 302_a_stated_compiler_outranks_the_graph_and_two_requirements_do_not_stack.sh \
207+
"OK: a stated compiler outranks the graph and two requirements do not stack" || fail=1
208+
else
209+
check_or_declared_skip 301_the_graphs_compiler_is_taken_and_nothing_is_written.sh \
210+
"OK: the graph's compiler is taken and nothing is written" \
211+
"no other family is" || fail=1
212+
# 302's half two needs no second family and always runs; only half
213+
# one is skipped, so the file still reaches its conclusion.
214+
check 302_a_stated_compiler_outranks_the_graph_and_two_requirements_do_not_stack.sh \
215+
"OK: a stated compiler outranks the graph and two requirements do not stack" || fail=1
216+
fi
176217
[ "$fail" = 0 ] || exit 1
177218
178219
scan:

CHANGELOG.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,98 @@
33
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
44
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
55
6+
## [2026.8.26.2] — 2026-08-26
7+
8+
已经解析出的答案,没有被用来做决定。完整分析见
9+
[`.agents/docs/2026-08-26-resolved-but-not-consulted.md`](.agents/docs/2026-08-26-resolved-but-not-consulted.md)
10+
11+
**这不是 2026.8.25.x 那个「谓词回答了比自己更窄的问题」的家族。** 那一族是判据
12+
问错了;这一族里谓词问对了、答案也算对了,只是那个答案**没有接到决定上**。两条都
13+
是「多存了一个字段而没有多接一根线」,因此读判据时看不出来 —— 只在用户问「你既然
14+
已经知道了,为什么还要我说一遍」时暴露。
15+
16+
### 修复
17+
18+
- **⭐⭐ 依赖声明的编译器被检查,但从未被采纳。**
19+
20+
```
21+
$ cat mcpp.toml
22+
[dependencies]
23+
openkal-llvm-runtime = "0.1.3" # requires = ["mcpp:compiler=llvm"]
24+
25+
$ mcpp build # 全局默认 gcc@16.1.0,而 llvm@22.1.8 已装
26+
error: `openkal-llvm-runtime@0.1.3` requires the compiler to be `llvm`.
27+
Select that compiler … mcpp toolchain default llvm
28+
```
29+
30+
这次拒绝没有换来任何信息:它要求的东西已经在本机,版本也已确定
31+
(`MCPP_TOOLCHAIN=llvm@22.1.8 mcpp build` 用时 1.02s)。付出的代价是让用户改一
32+
**全局**状态去满足**一个**工程的一条依赖。
33+
34+
真因是位置:`prepare.cppm` 有一个标题写着「the toolchain, resolved now that
35+
the graph exists」的接缝,它扫 `pkg.manifest.provides` 来决定编译器,**却不扫
36+
`requires_`** —— 后者在一千行之后才被收集,只用来否决这个决定。
37+
38+
⭐⭐ **修好之后不写任何东西,而这是位置带来的,不是额外加的开关。**
39+
`resolve_target_toolchain` 只有两个调用点,整个函数体(含首次运行的
40+
安装并持久化分支与全部三处 `write_default_toolchain`)都在图之后。把图的要求
41+
写进 `tcSpec` 的时机早于首次运行分支被求值,于是:
42+
43+
| | 之前 | 现在 |
44+
|---|---|---|
45+
| 已有 gcc 默认的机器 | 拒绝,要求改全局默认 | 装/用 llvm,`config.toml` 不动 |
46+
| 什么都没装的机器 | 装 gcc → 持久化 gcc → 再拒绝 | 首次运行分支根本不进,直接装 llvm |
47+
48+
拒绝只剩一种局面:工程自己在 `[toolchain]``[target.X]` 写下了相反的编译器。
49+
⚠️ 那种局面里全局默认与本次构建无关,因此原来那条 `mcpp toolchain default llvm`
50+
的建议**连问题都解决不了**,已改为指向那条陈述本身。两个包要求不同的族则是错误
51+
而不是一次挑选,并同时点名两个包。
52+
53+
- **⭐⭐ tier 闸问的是补全后的身份,而不是请求。**
54+
55+
```
56+
$ mcpp build --target aarch64-linux
57+
error: target 'aarch64-linux-gnu' is registered but not yet supported (planned)
58+
$ mcpp build --target aarch64-linux-musl
59+
Finished dev [unoptimized + debuginfo] in 0.99s
60+
```
61+
62+
`parse` 把缺失的 env 段按词法填成 `gnu`,那是为了让**身份**完整(输出目录、缓存
63+
键),`envExplicit` 就是为记住这个区别而存在的 —— 而 tier 闸不看它。被问的是
64+
「aarch64 的 Linux」,被回答的是「aarch64-linux-**gnu**」,报错还引用了一个用户
65+
从没打过的字符串。
66+
67+
省略了 env 段的请求现在对着词表补全,规则 1(词法默认受支持就用它)排在最前,
68+
因此 `x86_64-linux` 一动不动,而这件事能自己退休。⚠️ **`parse()` 未改**:身份
69+
必须保持词法、全量、与宿主无关。
70+
71+
- **⚠️ `unknown target 'riscv64-linux'` 说的是假话。**
72+
73+
`riscv64-linux-musl` 就在词表里(`planned`)。词法填充产生了一个**完全不存在**
74+
的行,于是一个已登记的目标族被报成未知。而且这条路径没有 `refusal::record`,
75+
`--format json` 把它报成 `reason: "other"`。现在它给出 planned 的诊断并点名
76+
`riscv64-linux-musl`;真正的拼写错误仍报 unknown,但带上了新的
77+
`unknown-target` 记号。
78+
79+
- **⚠️ `why toolchain --format json` 的两个字段互相矛盾。**
80+
81+
`cLibrary` 说 glibc/payload,`layers[].c-abi` 说 musl/graph。产物给出裁决 ——
82+
静态、无解释器、无 `DT_NEEDED`、11 个 openkal 符号 —— glibc 不在里面。两者各自
83+
准确,回答的却是不同的问题,而消费方无从判断该信哪个。新增
84+
`cLibrary.suppliesTarget`;⭐ 是**增字段**而非改名或给 `mode` 加取值,因为
85+
docs/11 §6 承诺字段只增不删、含义永不改变。
86+
87+
### 判据
88+
89+
- e2e `299``303`,全部走 `--format json` 分类而非字符串搜索,并接入
90+
`target matrix` 的第一层 —— **四台构建机各跑一遍**
91+
-`301` 的判据是 `~/.mcpp/config.toml`**sha256**,不是「构建成功」:构建成功
92+
与配置被改写可以同时为真,而那正是这次要消除的行为。
93+
-`299` 的第二半是对照 —— `x86_64-linux` 必须仍是 gnu。只测 aarch64 会让「把
94+
linux 的默认整个换成 musl」这种过头实现看起来是对的。
95+
- 单测 `TripleRequest.*` 七条,含一条遍历整张词表的「每个受支持的行都能从它自己的
96+
拼写到达」。
97+
698
## [2026.8.26.1] — 2026-08-26
799

8100
写出 `--target` 这个动作,曾被当成「这个构建的系统来自依赖图」。完整分析见

docs/03-toolchains.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,58 @@ The pair persists as `[toolchain] default = "gcc@16.1.0"` +
8181
configs with combined spellings like `default = "gcc@15.1.0-musl"` keep
8282
working unchanged.)
8383

84+
### What decides a build's compiler
85+
86+
Five things can name it. They are ranked, and the rank is what makes the two
87+
statements a project can write outrank everything mcpp keeps on its own:
88+
89+
| | source | may mcpp revise it |
90+
|---|---|---|
91+
| 1 | `[target.<triple>] toolchain` in `mcpp.toml` | no |
92+
| 2 | `[toolchain] default` in `mcpp.toml` (or `MCPP_TOOLCHAIN`) | no |
93+
| 3 | `requires = ["mcpp:compiler=<family>"]` from a dependency ||
94+
| 4 | the target row's pin, when the row's payload supplies the target side | yes |
95+
| 5 | `mcpp toolchain default`, then mcpp's first-run default | yes |
96+
97+
**A dependency may require a compiler family.** A C++ runtime is configured for
98+
one family and records that configuration in the headers it ships, so a package
99+
supplying one states which compiler it was built for. When that requirement
100+
differs from a value at rank 4 or 5 — answers mcpp derived itself — mcpp takes
101+
the required family for that build:
102+
103+
```
104+
$ mcpp build
105+
Resolving toolchain
106+
Resolved llvm@22.1.8 → …/xim-x-llvm/22.1.8/bin/clang++
107+
required by openkal-llvm-runtime@0.1.3 (`requires = ["mcpp:compiler=llvm"]`),
108+
not your gcc@16.1.0 — this project only
109+
```
110+
111+
**Nothing is written.** Not `~/.mcpp/config.toml`, not the project's
112+
`mcpp.toml`. The requirement is a property of this build, so it applies to this
113+
build; the machine's default stays whatever it was, for every other project.
114+
The version comes from what is already installed — the same resolution
115+
`mcpp toolchain default <family>` performs — and only from the ecosystem's own
116+
pin when nothing of that family is present.
117+
118+
**A compiler the project states is not revised.** At ranks 1–2 the project has
119+
said what it builds with, and a dependency disagreeing is a real contradiction:
120+
121+
```
122+
error: `openkal-llvm-runtime@0.1.3` requires the compiler to be `llvm`.
123+
compiler gcc (16.1.0, payload)
124+
required llvm (required by openkal-llvm-runtime@0.1.3)
125+
This build's compiler is stated in [toolchain] in mcpp.toml, and a compiler
126+
the project states outranks one its dependencies ask for.
127+
Change it to `llvm`, or remove it — with nothing stated, mcpp takes the
128+
compiler the graph requires and changes no configuration to do it.
129+
```
130+
131+
**Two dependencies requiring different families is an error, not a pick.** One
132+
build has one compiler; resolving by graph-traversal order would decide it by an
133+
order the author neither writes nor can predict, and would satisfy one package
134+
while failing the other inside a header.
135+
84136
## Inspecting Toolchain Status
85137

86138
```bash

docs/11-machine-output.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,15 +264,36 @@ It resolves and reports; it does not build. `data` is:
264264
| `reason` | a refusal token, or `none` |
265265
| `compiler` | `{family, version, driver}` — the driver that would run |
266266
| `triple` | `{requested, toolchain, llvm}` |
267-
| `cLibrary` | `{mode, path, origin}``mode` is `sysroot` / `payload-first` / `none`; `origin` is `payload` / `subos` / `host` / `none` |
267+
| `cLibrary` | `{mode, path, origin, suppliesTarget}``mode` is `sysroot` / `payload-first` / `none`; `origin` is `payload` / `subos` / `host` / `none` |
268268
| `layers[]` | the five target-side layers: `{layer, interface, impl, origin, subset}` |
269269

270+
⚠️ **`cLibrary` and `layers[].c-abi` answer two questions, and `suppliesTarget`
271+
says which one governs.** `cLibrary` describes the *payload's* link model — the
272+
search paths a payload-supplied C library would use. `layers[].c-abi` describes
273+
the *build*. When a dependency supplies the C library the two diverge, and
274+
before `suppliesTarget` existed the document reported both with no way to tell
275+
them apart:
276+
277+
```jsonc
278+
"cLibrary": { "origin": "payload", "path": "…/xim-x-glibc/2.44/lib64",
279+
"suppliesTarget": false }, // ← added; the payload is not in the artifact
280+
"layers": [ { "layer": "c-abi", "interface": "musl",
281+
"impl": "openkal-musl@0.3.5", "origin": "graph" } ]
282+
```
283+
284+
A field was added rather than `cLibrary` renamed or `mode` widened, because §6
285+
promises that fields are added and never removed and that a field's meaning
286+
never changes.
287+
270288
**`reason` is a token, not a sentence.** The refusal's message is still
271289
written for a person and still names the target, the rule and the way out — but
272290
a program classifying the outcome reads `reason`:
273291

274292
| `reason` | |
275293
|---|---|
294+
| `unknown-target` | the spelling names no row, and no `(arch, os)` group either |
295+
| `ambiguous-request` | several rows serve this `(arch, os)` and none is the default |
296+
| `compiler-requirement-conflict` | the graph's required compiler cannot be used here |
276297
| `tier-planned` | the row exists in the vocabulary; nothing is wired yet |
277298
| `host-cannot-serve` | no payload here, and no dependency supplied the system |
278299
| `capability-pin` | the row's toolchain is a capability, not a preference |

docs/16-the-target-triple.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ cannot rename it.
6767
mcpp build --target x86_64-linux # = x86_64-linux-gnu
6868
mcpp build --target x86_64-windows # = x86_64-windows-gnu
6969
mcpp build --target riscv64-none # = riscv64-none-elf
70+
mcpp build --target aarch64-linux # = aarch64-linux-musl
7071
mcpp build --target aarch64-macos # macOS has no segment to decline
7172
```
7273

@@ -80,6 +81,45 @@ identity, which must be total, and as a request, which must be able to say
8081
nothing; mcpp keeps both, filling the segment for the identity while recording
8182
that the fill was a fill.
8283

84+
### The completion is chosen from the vocabulary, not from a fixed word
85+
86+
The fourth line above is why the two roles have to stay separate. Filling
87+
`aarch64-linux` lexically gives `aarch64-linux-gnu`, and that row is `planned`
88+
— while `aarch64-linux-musl` is `verified`. Before 2026.8.26.2 the tier gate
89+
asked about the filled value, so:
90+
91+
```
92+
$ mcpp build --target aarch64-linux
93+
error: target 'aarch64-linux-gnu' is registered but not yet supported (planned)
94+
$ mcpp build --target aarch64-linux-musl
95+
Finished dev [unoptimized + debuginfo] in 0.99s
96+
```
97+
98+
The question asked was *aarch64, Linux*. The question answered was
99+
*aarch64-linux-**gnu***, and the message quotes a triple that appears nowhere in
100+
the command. `riscv64-linux` was worse: the fill named a row outside the
101+
vocabulary entirely, so a registered family was reported as `unknown target`.
102+
103+
A request that declined the segment is completed against the known-target table,
104+
in this order:
105+
106+
1. the lexical default names a supported row — take it (`x86_64-linux``gnu`);
107+
2. exactly one row for this `(arch, os)` is supported — take it
108+
(`aarch64-linux``musl`);
109+
3. nothing is supported — keep the lexical form, and diagnose against the rows
110+
that *do* exist (`riscv64-linux` → "planned; registered rows for this system:
111+
`riscv64-linux-musl`");
112+
4. several are supported and the lexical default is none of them — refuse and
113+
list them. No `(arch, os)` has this shape today.
114+
115+
Rule 1 comes first so this retires itself: the day `aarch64-linux-gnu` graduates
116+
from `planned`, the lexical answer wins again with nothing to edit.
117+
118+
**Writing the segment opts out.** A written segment is a request, not a gap, so
119+
`--target aarch64-linux-gnu` still reaches the `planned` row's refusal — which
120+
is the escape hatch for opting into a row early with an explicit
121+
`[target.<triple>] toolchain`.
122+
83123
### Which Spelling To Use
84124

85125
**Under the build-time system, decline it.** The graph supplies the C library

docs/zh/03-toolchains.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,54 @@ mcpp toolchain default gcc@16 --target x86_64-linux-musl # "默认就要全静
7979
`[toolchain] default = "gcc@16.1.0"` + `default_target = "x86_64-linux-musl"`
8080
(存量 config 里 `default = "gcc@15.1.0-musl"` 这类合并拼写原样可用。)
8181

82+
### 谁决定一次构建的编译器
83+
84+
有五种来源会给它命名。它们是分级的,而这套分级正是让工程能写下的那两条压过
85+
mcpp 自己保管的一切的原因:
86+
87+
| | 来源 | mcpp 可否改写 |
88+
|---|---|---|
89+
| 1 | `mcpp.toml``[target.<triple>] toolchain` ||
90+
| 2 | `mcpp.toml``[toolchain] default`(或 `MCPP_TOOLCHAIN`) ||
91+
| 3 | 依赖的 `requires = ["mcpp:compiler=<族>"]` ||
92+
| 4 | 目标行的 pin(当该行的载荷供给目标侧时) ||
93+
| 5 | `mcpp toolchain default`,以及 mcpp 的首次运行默认值 ||
94+
95+
**依赖可以要求一个编译器族。** 一份 C++ 运行时是为某一个族 configure 过的,
96+
并把这份配置记在它所发布的头文件里,因此供给它的包会说明自己是为哪个编译器构建
97+
的。当这条要求与第 4、5 级 —— mcpp 自己推导出来的答案 —— 不同时,mcpp 就为这次
98+
构建取用被要求的那个族:
99+
100+
```
101+
$ mcpp build
102+
Resolving toolchain
103+
Resolved llvm@22.1.8 → …/xim-x-llvm/22.1.8/bin/clang++
104+
required by openkal-llvm-runtime@0.1.3 (`requires = ["mcpp:compiler=llvm"]`),
105+
not your gcc@16.1.0 — this project only
106+
```
107+
108+
**不写任何东西。** 不写 `~/.mcpp/config.toml`,也不写工程的 `mcpp.toml`
109+
这条要求是**这次构建**的性质,就只作用于这次构建;这台机器的默认值保持原样,对
110+
其他每一个工程都是。版本取自已经装好的那些 —— 与 `mcpp toolchain default <族>`
111+
走的是同一条解析 —— 只有该族一个都没装时,才取生态自己的 pin。
112+
113+
**工程写下的编译器不会被改写。** 第 1、2 级上工程已经说明了它用什么构建,依赖
114+
与之不一致就是一次真实的矛盾:
115+
116+
```
117+
error: `openkal-llvm-runtime@0.1.3` requires the compiler to be `llvm`.
118+
compiler gcc (16.1.0, payload)
119+
required llvm (required by openkal-llvm-runtime@0.1.3)
120+
This build's compiler is stated in [toolchain] in mcpp.toml, and a compiler
121+
the project states outranks one its dependencies ask for.
122+
Change it to `llvm`, or remove it — with nothing stated, mcpp takes the
123+
compiler the graph requires and changes no configuration to do it.
124+
```
125+
126+
**两个依赖要求不同的族是错误,不是一次挑选。** 一次构建只有一个编译器;按图的
127+
遍历顺序来定,等于让作者既不书写也无法预测的顺序做决定,并且会满足其中一个包而
128+
让另一个在它自己的头文件里失败。
129+
82130
## 查看工具链状态
83131

84132
```bash

0 commit comments

Comments
 (0)