diff --git a/.agents/docs/2026-09-20-cxa-thread-atexit-finding.md b/.agents/docs/2026-09-20-cxa-thread-atexit-finding.md index 328cb416..d291df70 100644 --- a/.agents/docs/2026-09-20-cxa-thread-atexit-finding.md +++ b/.agents/docs/2026-09-20-cxa-thread-atexit-finding.md @@ -1,9 +1,14 @@ --- subject: review -status: active +status: landed --- -# `__cxa_thread_atexit` 在 openkal-Windows 上:定位到一层,第二层未定位 +# `__cxa_thread_atexit` 在 openkal-Windows 上:两层都已定位并修复 + +> **2026-09-21 收尾。** 第二层已定位,修法已实测,发在 +> `openkal-llvm-runtime@0.15.0`。下文 §1–§5 保留当时的记录(包括两个被否掉的假设), +> §7 是结论。**§4 的第一个假设当时被判为「否」,而它其实是对的——错的是那次探针的 +> 构造,见 §7。** - 日期:2026-09-20 - 来源:mcpp-index 的 30-member 重测,doctest 与 spdlog 两个成员停在 @@ -117,3 +122,60 @@ Windows : __cxa_thread_atexit_impl = 0 -> fallback branch - 被调用而链表为空 ⇒ 注册那一侧的问题 - 没被调用 ⇒ `__libcpp_tls_create` / key 注册那一侧的问题 2. 无论结论如何,修法必须让「析构会跑」与「链接会过」同时成立,或者两者都不成立。 + + +--- + +## 7. 第二层:已定位(2026-09-21) + +### 读数 + +按 §6 写下的第一条判据做——在 fallback 的 `run_dtors` 里打一行,并同时打印 +`&dtors`: + +``` +[probe] DtorsManager ctor: creating key +[probe] registered dtor, dtors=0x7ffffe994680, key=0x2, &dtors=0x7ffffe9946a8 +[prog] in thread, v=7 +[probe] run_dtors called, dtors=0, alive=0, &dtors=0x7ffffe9946c8 +[prog] after join, ran=0 (expect 7) +[probe] run_dtors called, dtors=0, alive=0, &dtors=0x7ffffe994708 +``` + +`run_dtors` **被调用了**——§6 的第二支排除。而 **`&dtors` 三次都不同**,在同一个线程里。 + +### 真因 + +`__thread DtorList* dtors` 在本包为 PE 采用的 `-femulated-tls` 下由 emutls 提供。emutls +把每线程的块挂在它**自己的**一个 pthread key 后面,而那个 key 的析构已经先释放了本线程 +的块;之后每次读都新分配一个**清零**的块,所以地址每次都不一样。`run_dtors` 走的是空链表。 + +### §4 假设一其实是对的,错的是那次探针的构造 + +当时写的是:「PE 上 `thread_local` 走 emutls,它自己的 pthread key 先于 libc++abi 的 key +被析构,于是 `run_dtors` 读到的链表已经空了」——**这就是真因**。 + +那次探针之所以读到 42,是因为 musl **按 key 的创建顺序**逐个调析构,而探针自己 +`pthread_key_create` 在第一次访问 `thread_local` **之前**,于是 emutls 的 key 排在它后面、 +析构也在它之后。真实情形里 libc++abi 的 `dtors_key`(实测 `key=0x2`)排在 emutls 之后。 + +**一个探针报不出它被构造成不会发生的那个顺序。** 判据落在了一个正确的谓词上,而对象的 +构造恰好排除了被测的那个条件——这与 [[a-check-that-picks-its-object-by-convention]] 同族。 + +### 修法 + +链表存进 **key 自己的值**。key 的析构函数本来就被交给这个值,而任何别的 key 的拆除都碰 +不到它。`dtors_alive` 随之不需要:值非空就是「链表在」。零新机制。 + +### 判据(两条,缺一不可) + +`examples/cxx`,两个目标: + +``` +ok: a thread_local is constructed in a spawned thread +ok: and its destructor runs when that thread ends +``` + +`x86_64-linux-gnu` 与 `x86_64-windows-gnu`(wine)均 `failures: 0`。只断言「链接通过」 +或只断言「构造发生」的判据会同时放过两层——这正是 §5 决定不发第一层补丁的那个理由, +现在它变成了判据本身的形状。 diff --git a/.agents/docs/2026-09-21-macros-and-withdrawal-verify.sh b/.agents/docs/2026-09-21-macros-and-withdrawal-verify.sh new file mode 100755 index 00000000..0e27ca78 --- /dev/null +++ b/.agents/docs/2026-09-21-macros-and-withdrawal-verify.sh @@ -0,0 +1,302 @@ +#!/usr/bin/env bash +# Ecosystem verification for the 2026.9.21.2 wave against the PUBLISHED mcpp +# and index, run inside a SubOS sandbox with CN mirrors for xlings and mcpp. +# +# B64=$(base64 -w0 .agents/docs/2026-09-21-macros-and-withdrawal-verify.sh) +# xlings subos use v920 --sandbox --cmd \ +# "echo $B64 | base64 -d > /tmp/v.sh && MCPP_VERIFY_VERSION=2026.9.21.2 bash /tmp/v.sh" +# +# RUN IT AGAINST THE PREVIOUS RELEASE FIRST (MCPP_VERIFY_VERSION=2026.9.21.1): +# every CHANGE section must FAIL there and pass here, and every GUARD section +# must pass on both. A CHANGE section green on both measured nothing. +# +# The sandbox's $HOME persists between runs of one SubOS, so each section +# clears its own directory. A section that cannot run says so and is listed +# again at the end: a run reporting only failures cannot be told from one that +# examined nothing. +# +# TWO RUNS AND THE READING FROM EACH (host dry run, 2026-09-21, the older one +# against the genuine published archive rather than a local build): +# +# mcpp 2026.9.21.1 (published) fails=2 +# B the engine does not define the upper-case target macro +# C an unanswered requirement produced no note +# mcpp 2026.9.21.2 fails=0 +# +# C's SECOND LEG PASSES ON BOTH, and that is the point of having it: it asserts +# the note is ABSENT when the provider does state its list, so 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. +# +# D and F needed `openkal-llvm-runtime@0.14.0`, registered after that dry run, +# and both reported NOT RUN rather than passing. +set -u + +VER="${MCPP_VERIFY_VERSION:?set MCPP_VERIFY_VERSION}" +STORE="${MCPP_VERIFY_BIN:-$HOME/.xlings/data/xpkgs/xim-x-mcpp/$VER/bin/mcpp}" + +fails=0 +skipped="" +fail() { printf 'ASSERT-FAIL: %s\n' "$1"; fails=$((fails + 1)); } +ok() { printf 'ok: %s\n' "$1"; } +section() { printf '\n== %s ==\n' "$1"; } +skip() { printf 'NOT RUN: %s\n' "$1"; skipped="$skipped + - $1"; } +unset XLINGS_ACTIVE_SUBOS + +root="$HOME/verify-9212" +rm -rf "$root"; mkdir -p "$root" + +section "A. identity and mirror" +if [ ! -x "$STORE" ]; then + skip "mcpp $VER is not in the store at $STORE" + printf '\n-- summary --\nfails=%d\nnot run:%s\n' "$fails" "${skipped:- (none)}" + exit 1 +fi +got="$("$STORE" --version 2>&1 | head -1)" +case "$got" in + *"$VER"*) ok "mcpp $VER from $STORE" ;; + *) fail "the binary at $STORE reports '$got'" ;; +esac +"$STORE" self config --mirror CN >/dev/null 2>&1 \ + && ok "mcpp mirror set to CN" || fail "mcpp self config --mirror CN" + +# ── CHANGE 1. The owned macros are spelt in upper case ────────────────────── +# +# 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". +section "B. __MCPP_TARGET___ replaces __mcpp_target___ (CHANGE)" +b="$root/b"; rm -rf "$b"; mkdir -p "$b/src" +cat > "$b/src/main.c" <<'EOF' +#if !defined(__MCPP_TARGET_LINUX__) +#error "__MCPP_TARGET_LINUX__ is not defined" +#endif +#if defined(__mcpp_target_linux__) +#error "the lower-case spelling is still defined" +#endif +int main(void) { return 0; } +EOF +cat > "$b/mcpp.toml" <<'EOF' +[package] +name = "macro-probe" +version = "0.1.0" + +[targets.macro-probe] +kind = "bin" +main = "src/main.c" +EOF +if (cd "$b" && "$STORE" build >/dev/null 2>&1); then + ok "the upper-case target macro is defined and the lower-case one is not" +else + out=$(cd "$b" && "$STORE" build 2>&1) + case "$out" in + *"__MCPP_TARGET_LINUX__ is not defined"*) + fail "the engine does not define the upper-case target macro" ;; + *"lower-case spelling is still defined"*) + fail "the engine still defines the lower-case target macro" ;; + *) fail "the probe did not build, and for neither of the two reasons" ;; + esac +fi + +# THE SPELLING IS DERIVED, NOT ENUMERATED. A second target with a different +# `os` says the engine reads the triple rather than carrying a table of names. +b2="$root/b2"; rm -rf "$b2"; mkdir -p "$b2/src" +cat > "$b2/src/main.c" <<'EOF' +#if !defined(__MCPP_TARGET_NONE__) +#error "__MCPP_TARGET_NONE__ is not defined for a freestanding target" +#endif +void _start(void) {} +EOF +cat > "$b2/mcpp.toml" <<'EOF' +[package] +name = "macro-probe-bare" +version = "0.1.0" + +[targets.macro-probe-bare] +kind = "bin" +main = "src/main.c" + +[build] +ldflags = ["-nostdlib", "-nostartfiles", "-static"] +EOF +if (cd "$b2" && "$STORE" build --target riscv64-none-elf >/dev/null 2>&1); then + ok "a freestanding target spells its own macro from the triple" +else + skip "the freestanding toolchain did not resolve in this sandbox" +fi + +# ── CHANGE 2. A requirement nobody answered is named ──────────────────────── +# +# Three situations exist and two build. Without this line the first and the +# third produce identical output, so a consumer cannot tell "checked and +# agreed" from "never asked". Both legs, because a note printed +# unconditionally would satisfy the first one alone. +section "C. an unanswered requirement is named (CHANGE)" +c="$root/c"; rm -rf "$c"; mkdir -p "$c/impl/src" "$c/src" +printf 'int fake_kernel_marker(void){return 0;}\n' > "$c/impl/src/lib.c" +printf 'int main(void){return 0;}\n' > "$c/src/main.c" +mk_impl() { # $1 = provides-interfaces body, or empty + if [ -z "$1" ]; then + cat > "$c/impl/mcpp.toml" <<'EOF' +[package] +name = "fakekernel" +version = "0.1.0" +provides = ["mcpp:kernel-abi=openkal"] + +[targets.fakekernel] +kind = "lib" +sources = ["src/*.c"] +EOF + else + cat > "$c/impl/mcpp.toml" < "$c/mcpp.toml" <<'EOF' +[package] +name = "iface-probe" +version = "0.1.0" + +[dependencies] +fakekernel = { path = "impl" } + +[build] +allow_host_libs = true + +[kernel-abi] +requires-interfaces = ["openkal.fs", "openkal.net"] +EOF +mk_impl '' +out=$(cd "$c" && "$STORE" build 2>&1) +if [ $? -ne 0 ]; then + fail "a provider that states nothing must not be refused" +else + case "$out" in + *"kernel-abi interfaces"*"states none"*"2 requirements unchecked"*) + ok "the note names the implementation and how many went unchecked" ;; + *"kernel-abi interfaces"*) + fail "the note is printed but does not carry the count" ;; + *) fail "an unanswered requirement produced no note" ;; + esac +fi +rm -rf "$c/target" +mk_impl '"openkal.fs", "openkal.net", "openkal.abort"' +out=$(cd "$c" && "$STORE" build 2>&1) +if [ $? -ne 0 ]; then + fail "a graph whose provider states every requirement must build" +else + case "$out" in + *"kernel-abi interfaces"*) + fail "the note appeared for a graph in which everything WAS checked" ;; + *) ok "a provider that states its list draws no note" ;; + esac +fi + +# ── CHANGE 3. The borrowed name is withdrawn ──────────────────────────────── +# +# `__CYGWIN__` was left defined so that code needing "PE object format with a +# POSIX-presenting C environment" had a name. A 30-member measurement found +# four members reading it as "Win32 is available" and reaching windows.h. +# This needs a real openkal graph, so it skips rather than failing when the +# sandbox cannot reach one. +section "D. __CYGWIN__ is withdrawn on a Windows target presenting POSIX (CHANGE)" +d="$root/d"; rm -rf "$d"; mkdir -p "$d/src" +cat > "$d/src/main.cpp" <<'EOF' +#if defined(__CYGWIN__) || defined(__CYGWIN32__) +#error "the borrowed name is still defined" +#endif +#if !defined(__MCPP_TARGET_WINDOWS__) +#error "mcpp's own name for the target is missing" +#endif +#if defined(_WIN32) +#error "presents = posix must suppress _WIN32" +#endif +#if !defined(__OPENKAL__) +#error "__OPENKAL__ is not defined over a resolved openkal layer" +#endif +#if defined(__openkal__) +#error "the lower-case spelling is still defined" +#endif +int main() { return 0; } +EOF +cat > "$d/mcpp.toml" <<'EOF' +[package] +name = "withdrawal-probe" +version = "0.1.0" + +[dependencies] +openkal-llvm-runtime = "0.14.0" +EOF +out=$(cd "$d" && "$STORE" build --target x86_64-windows-gnu 2>&1) +rc=$? +case "$out" in + *"openkal-llvm-runtime"*"not found"*|*"did not resolve"*) + skip "openkal-llvm-runtime 0.14.0 did not resolve from the index" ;; + *) + if [ $rc -eq 0 ]; then + ok "the borrowed name is gone, mcpp's own name is there, over openkal" + else + case "$out" in + *"borrowed name is still defined"*) fail "__CYGWIN__ is still defined" ;; + *"own name for the target is missing"*) fail "__MCPP_TARGET_WINDOWS__ is missing" ;; + *"__OPENKAL__ is not defined"*) fail "__OPENKAL__ is missing over openkal" ;; + *"lower-case spelling is still defined"*) fail "__openkal__ is still defined" ;; + *"presents = posix"*) fail "_WIN32 survived the substitution" ;; + *) skip "the openkal Windows graph did not build in this sandbox" ;; + esac + fi ;; +esac + +# ── GUARD. A package declaring nothing is untouched ───────────────────────── +section "E. a package declaring neither key builds and runs (GUARD)" +e="$root/e"; rm -rf "$e"; mkdir -p "$e/src" +printf '#include \nint main(){std::puts("plain ok");return 0;}\n' > "$e/src/main.cpp" +cat > "$e/mcpp.toml" <<'EOF' +[package] +name = "plain" +version = "0.1.0" +EOF +if (cd "$e" && "$STORE" build >/dev/null 2>&1) \ + && "$e"/target/*/*/bin/plain 2>/dev/null | grep -q "plain ok"; then + ok "a package that declares nothing builds and runs" +else + fail "a package that declares nothing must be untouched by this release" +fi + +# ── GUARD. An openkal program from the published index ────────────────────── +section "F. an openkal program from the published index (GUARD)" +f="$root/f"; rm -rf "$f"; mkdir -p "$f/src" +printf '#include \nint main(){std::puts("openkal ok");return 0;}\n' > "$f/src/main.cpp" +cat > "$f/mcpp.toml" <<'EOF' +[package] +name = "openkal-hello" +version = "0.1.0" + +[dependencies] +openkal-llvm-runtime = "0.14.0" +EOF +if (cd "$f" && "$STORE" build >/dev/null 2>&1); then + if "$f"/target/*/*/bin/openkal-hello 2>/dev/null | grep -q "openkal ok"; then + ok "an openkal program builds and runs from the published index" + else + fail "the openkal program built and did not run" + fi +else + skip "openkal-llvm-runtime 0.14.0 did not resolve from the index" +fi + +printf '\n-- summary --\nfails=%d\nnot run:%s\n' "$fails" "${skipped:- + (none)}" +[ "$fails" -eq 0 ] diff --git a/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md b/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md index 0b20cc7d..928497fe 100644 --- a/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md +++ b/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md @@ -61,8 +61,9 @@ status: active #### E1 — P3:撤掉 `__CYGWIN__` 借用 -**状态**:**分三步,本版落地第一步**(mcpp 2026.9.21.1)。下文保留论证;落地形态与 -被交叉验证挡下的那一版见本节末。 +**状态**:**三步全部落地**。第一步 mcpp 2026.9.21.1(加名字)+ 2026.9.21.2(改大写); +第二步 `openkal-musl@0.19.0` 与 `openkal-llvm-runtime@0.14.0`;第三步 mcpp 2026.9.21.2 +停止定义借来的名字。下文保留论证;落地形态与被交叉验证挡下的那一版见本节末。 保留 `__CYGWIN__` 的本意是给「PE 格式 + POSIX C 环境」一个名字。实证否定了这个用法: @@ -121,17 +122,106 @@ status: active **静默**的——`setjmp.h` 自己的注释写着「a mismatch nothing reports until the record overruns」。libunwind 那处有 `static_assert` 才响,属于运气。 -**落地形态(第一步)**:`cenv.cppm` 的 Windows+Posix 分支加 `-D__mcpp_target_windows__=1`, -`expectDefined` 加该项与 `__CYGWIN__`——探针核对,一个没生效的 `-D` 是校验失败不是沉默。 -实测:三个通道(`.c`/`.cpp`/`.S`)全部到达;预定义为 `__unix__` + `__CYGWIN__` + -`__mcpp_target_windows__`,`_WIN32` 仍不存在。 +**落地形态(第一步)**:`cenv.cppm` 的 Windows+Posix 分支加 `-D__MCPP_TARGET_WINDOWS__=1` +(2026.9.21.1 发的是小写拼法,2026.9.21.2 在它还没有消费者时改成大写——命名约定见 +`src/toolchain/predefines.cppm` 与 `docs/21`),`expectDefined` 加该项——探针核对,一个 +没生效的 `-D` 是校验失败不是沉默。实测:三个通道(`.c`/`.cpp`/`.S`)全部到达;预定义为 +`__unix__` + `__MCPP_TARGET_WINDOWS__`,`_WIN32` 仍不存在。 -**为什么名字是 `__mcpp_target_windows__` 而不是设计里写的 `__mcpp_format_pe__`**:两个 +**为什么名字是 `__MCPP_TARGET_WINDOWS__` 而不是设计里写的 `__mcpp_format_pe__`**:两个 消费者要的不是「目标文件格式」,是**调用约定**(Win64 的寄存器保存区大小)。在这个目标上 两者同变,但按消费者实际问的那个问题命名更诚实。 -**第二、三步**:两个包改读新名字并保留 `|| defined(__CYGWIN__)`(在两种引擎上都能构建); -之后的版本再停止定义借来的那个。**先做第三步会让已发布的那两个头静默落进 `#else`。** +**第二、三步(已落地)**:两个包改读新名字并保留 `|| defined(__CYGWIN__)`,于是在本次改动 +两侧的引擎上都能构建;2026.9.21.2 的 `cenv.cppm` 在编译行加 `-U__CYGWIN__` / +`-U__CYGWIN32__`,`expectUndefined` 同步。 + +**次序是被测出来的,不是被断言的。** 它成立于**仓库之间**,所以任何一个仓库里的测试都 +检查不到它。在 2026.9.21.2 的引擎上,拿**已发布的** `openkal-llvm-runtime@0.13.0` 为 +`x86_64-windows-gnu` 构建一个 openkal 程序: + +``` +static assertion failed: x86_64 registers do not fit into unw_context_t +static assertion failed: UnwindCursor<> does not fit in unw_cursor_t +``` + +那就是「先做第三步」的读数,也就是第二步必须先发布的理由。`setjmp.h` 是沉默的那一半—— +它自己的注释写着「a mismatch nothing reports until the record overruns」。 + +**残余窗口被点名而不是被说没有**:把 `openkal-musl` **精确**钉在 0.18.0 或更早、同时把 +引擎升过 2026.9.21.2 的工程,会拿到那个静默的 `#else`。在引擎发布之前把索引的 `latest` +移到 0.19.0,是把窗口压到「精确钉」的办法;这里没有任何机制能把它关掉,因为引擎无从知道 +一个包安装出去的头读了哪些宏。 + +#### E1c — 交叉验证协议的一个结构性漏洞:没有人建 Windows-over-openkal + +**2026-09-21 实测发现,在用协议验 E1 的过程中。** 逐 job 核对两个包的交叉验证运行: + +| 仓库 | 它的 CI 建的目标 | 有没有 Windows 目标 | +|---|---|---| +| `openkal-llvm-runtime` | `runtime`(linux + riscv 裸机)、`host-dimension` 矩阵(macOS 宿主、**Windows 宿主 → 每个目标**)、两个「产物在那个系统上跑」 | **有** | +| `openkal-musl` | linux/gcc、linux/llvm、macos/llvm、cross-link(Linux↔macOS)、「跨建的产物在那边启动」 | **没有** | + +**先写下更正,因为我第一次读错了。** 只看 runtime 运行里的第一个 job,我下过结论说 +「整个生态没有一个包建 Windows-over-openkal」。那是错的:`host-dimension` 的 Windows +行实测建了 `x86_64-windows-gnu`,并解析到 `openkal-musl@0.19.0`。**E1 的 Windows 那条腿 +确实被验到了**,结论成立。 + +**真正的缺口比那小,但仍然是缺口:`openkal-musl` 自己没有任何 Windows 格子。** 而本轮 +它改的恰恰是 `bits/setjmp.h`——一个**只在 Windows 目标上有分支**的已安装头。它今天被验到 +是**传递的**:runtime 的 CI 把 musl 当 path dep 拉进去,顺带编了它。一个包的关键改动 +由另一个仓库的 CI 代为验证,是[[a-passing-criterion-that-measured-nothing]] 的邻居—— +它今天对,是因为恰好有人在别处建了那个目标。 + +**为什么补这个格子不是顺手的事,是实测出来的。** 想写一个最小判据——一个 `setjmp` / +`longjmp` 程序,带 `_Static_assert(sizeof(jmp_buf) >= 32 * sizeof(unsigned long long))` +——只声明 `openkal-musl = "0.19.0"` 一个依赖,为 `x86_64-windows-gnu` 构建: + +- **编译过了**,而那正是判据要抓的那一层:短 `jmp_buf` 是静默的,编译期断言是唯一能让它 + 变响的地方。 +- **链接不过**:`cpow.o` 等一批目标文件的引用无人解析。openkal-musl 单独不是一条完整的 + 链接——openkal 实现与 compiler-rt 由 `openkal-llvm-runtime` 组装。 + +所以 musl 侧的 Windows 格子要么只做**编译期**断言(能抓这个缺陷,且便宜),要么把 runtime +当 path dep 拉进来(就是 runtime 的 CI 反过来做的那件事)。**两种都是新增工作量,不是 +把矩阵加一行。** 记在这里,下一个接手的人不必再量一遍。 + +**唯一建那个目标的是 mcpp 自己的 `openkal-cross`**(3 宿主 × 3 目标)。而它把要验的 +生态分支写死成 `OPENKAL_BRANCH: main`——于是一个**需要生态协同提交**的引擎改动,在那个 +提交落地之前既无法验证、也无法合入(因为正是这个 job 会红)。 + +**修法**:`openkal-cross.yml` 增加 `workflow_dispatch` 输入 `openkal_ref`,留空时行为 +不变。这是协议的另一半:生态仓库早就能用 `MCPP_SOURCE_REF` 指到 mcpp 的 PR 分支,反向 +一直没有。 + +**次序上的后果**:`openkal-cross` 里那个示例通过 `path = "../.."` 依赖 runtime,而 +runtime 的清单从**索引**钉 `openkal-musl = "0.19.0"`。所以用 `openkal_ref` 验之前, +musl 0.19.0 必须先登记进索引。 + +#### E1b — 引擎拥有的宏改为全大写(2026.9.21.2) + +**状态**:已落地,与 E1 第三步同一个 PR(用户要求:不分开,免得发布周期太长)。 + +`__mcpp_target___` → `__MCPP_TARGET___`,`__openkal__` → `__OPENKAL__`。 + +**约定按「名字是什么」分,不按谁写的分。** 厂商名与产品名大写(`__APPLE__`、`_WIN32`、 +`__MINGW32__`、`__GNUC__`),系统种类名小写(`__linux__`、`__unix__`)。mcpp **拥有**的 +每一行都属于第一类。小写那一版的推理是「它们在守卫里与 `__linux__` 并排,所以跟它一致」 +——**那是把「相邻」当成了「同类」**,而 `__APPLE__` 在同样那些守卫里却是大写。 + +**撤销小写拼法的代价是量出来的。** 分母是全生态每一个仓库,逐文件类型扫过: + +| 撤销的名字 | 安装头里的读者 | 包源码/清单里的读者 | 第三方读者 | 暴露 | 步数 | +|---|---|---|---|---|---| +| `__CYGWIN__` | **6 处**(两个头) | — | **4 个成员** | 上游二十年 | 3 | +| `__mcpp_target___` | 0 | 0 | 0(按构造) | 1 个发布 | 1 | +| `__openkal__` | 0 | 0 | 0(按构造) | 3 天 | 1 | + +「按构造」是指:两个名字都是在这里发明的,不可能有上游代码握着它们。**两次撤销之间规则 +没变,变的是数目**——而一次不由数目支撑的撤销,正是本项目已经犯错过一次的那种论证 +(E1 的第一个形态)。 + +用户先要求「两个拼法并存、文档推荐大写」,在看到代价读数后改为「代价不大就取消」。取消。 #### E2 — P7-L3:链接期集合差 @@ -144,12 +234,41 @@ overruns」。libunwind 那处有 `static_assert` 才响,属于运气。 **判据**:一个程序引用了实现未提供的接口里的符号,链接期被点名拒绝,且**声明正确时 零额外链接开销**。 -**阻塞**:需要把 `SURFACE.txt` 的「接口 → 符号」映射带到链接期。材料齐(`SURFACE.txt`、 -`kal_interfaces()`),是工作量不是未知数。 +**阻塞(2026-09-21 更正)**:**不是工作量,是一个设计决定。** 原文写「材料齐,是工作量 +不是未知数」,实查否掉了这个前提: + +1. **`SURFACE.txt` 不被安装。** `mcpplibs/openkal/mcpp.toml` 里没有任何一条把它装出去, + 所以链接期的引擎拿不到它。 +2. **`docs/22` 有一条明写的原则挡在前面**:「**引擎不认识两个集合里的任何一个成员**。对 + 它们做的唯一操作是集合差,所以规范可以新增一个接口而不需要发布 mcpp。」要在链接期 + 点名**接口**,引擎就必须持有「接口 → 符号」的映射;映射放进引擎会直接违反这条。 + +所以真正要先回答的是:**映射住在哪,而引擎能不能读它却不学会这个生态的词汇。** 两条候选: + +| 形态 | 代价 | +|---|---| +| 规范包安装 `SURFACE.txt`,清单新增一个键指向它 | 引擎要学会一种文件格式;新键 → 索引 floor 一轮 | +| 只报「解析出的实现声明了这 N 个接口,缺的符号不在它的导出里」 | 零新键、零新格式;**但点不出接口名**,弱于本项判据 | + +第二条是今天就能做的,第一条才满足判据。**在做出这个决定之前,把 E2 排进任何一个 PR 都是 +在实现一个还没选定的形状**——这正是「凭印象写下的『为什么不行』会否掉正确修法」的镜像: +凭印象写下的「为什么可以」同样会带来一个错的实现。 + +**后果**:E2 退出本轮(2026.9.21.2)。它不阻塞验收(A1/A2/A3 不依赖它),批次表已把它放在 +第四批。 #### E3 — 未被回答的 requirement 没有提示 -**状态**:未实现。记录:记忆 `an-unanswered-requirement-looks-like-a-confirmed-one`。 +**状态**:**已落地**(mcpp 2026.9.21.2,与 E1/E1b 同一个 PR)。实际输出: + +``` + note kernel-abi interfaces: fakekernel@0.1.0 states none, 2 requirements unchecked +``` + +判据落在 `tests/e2e/743` 的第三、四条腿上。**第四条腿是必需的**:没有它,第三条腿在一个 +无条件打印这行的引擎上同样会绿——两条腿互为对照,这正是「判据通过了但什么都没测到」 +那一类缺陷的形状。断言里包含**数目**(该夹具声明两条),因为数目是夹具唯一决定的那部分, +一个报「1」或「0」的提示仍会匹配所有只认标识符的断言。 三种情形两种绿:提供方声明且包含 → 构建(**已确认**);声明但不包含 → 拒绝; **什么都没声明 → 构建(从没被检查过)**。第三种是有意的(`provides-interfaces` 晚于 @@ -167,20 +286,69 @@ overruns」。libunwind 那处有 `static_assert` 才响,属于运气。 #### C1 — `__cxa_thread_atexit` -**状态**:第一层可修但**不能只修第一层**;第二层未定位。记录: -`.agents/docs/2026-09-20-cxa-thread-atexit-finding.md`。 +**状态**:**两层都已定位并修复**,发在 `openkal-llvm-runtime@0.15.0`。记录: +`.agents/docs/2026-09-20-cxa-thread-atexit-finding.md` §7。 + +**第二层的真因**:`__thread DtorList* dtors` 在 `-femulated-tls` 下由 emutls 提供,而 +emutls 把每线程的块挂在它自己的一个 pthread key 后面;那个 key 的析构先释放了本线程的块, +之后每次读都新分配一个**清零**的块。判据是同一线程里 `&dtors` 三次不同 +(`...6a8` / `...6c8` / `...708`)。 + +**本清单里「唯一的未知数」是被它自己写下的判据关掉的**——发现文档 §6 写的第一步就是 +「在 fallback 里打一行,看 `run_dtors` 到底有没有被调用」。它被调用了,而链表是空的。 + +**并且:§4 那条被判为「否」的假设其实是对的。** 那次探针的 `pthread_key_create` 排在第一次 +访问 `thread_local` **之前**,而 musl 按创建顺序调 key 析构,于是 emutls 反而活得更久, +读到了期望值。真实情形顺序相反。**一个探针报不出它被构造成不会发生的那个顺序**——谓词是 +对的,对象的构造把被测条件排除掉了。 + +**修法**:链表存进 key 自己的值(析构函数本来就被交给它),零新机制。 只补符号会把一个**构建期的响亮失败**换成一个**运行期的静默失败**:链接过了, `thread_local` 的析构不跑。补丁试过并**主动回退**,因为验证显示析构确实没执行。 -**判据**:最小复现(五行,文档里有)在 `x86_64-windows-gnu` 上**链接通过且析构函数 -真的执行**——两个条件缺一不可。只断言链接通过是错的判据。 +**判据(已通过)**:`examples/cxx` 两条断言,两个目标: + +``` +ok: a thread_local is constructed in a spawned thread +ok: and its destructor runs when that thread ends +``` + +`x86_64-linux-gnu` 与 `x86_64-windows-gnu`(wine)均 `failures: 0`。只断言链接通过、 +或只断言构造发生,都会同时放过两层——这正是当初决定不发第一层补丁的理由,现在它变成了 +判据本身的形状。 -**阻塞**:第二层未定位(emutls 在 PE 上的注册路径)。这是本清单里**唯一一个真正的 -未知数**。 +**阻塞**:无。本清单里唯一那个真正的未知数已关闭。 #### C2 — `linux/` uapi 头(curl, cmp-module) +**2026-09-21 更正:这是两件不同的事,本文初稿把它们并成了一类。** 逐条读了实测诊断与 +配方之后: + +| 成员 | 诊断 | 真正的归属 | +|---|---|---| +| `curl` | `lib/setopt.c:31: 'linux/tcp.h' file not found` | **配方缺陷**,与 C3/expat 同形状 | +| `cmp-module` | `asio/detail/config.hpp:899: 'linux/version.h' file not found` | **真的 C2** | + +**curl 是配方缺陷。** `pkgs/c/compat.curl.lua` 生成的 `curl_config.h` 里有 +`#define HAVE_LINUX_TCP_H 1`,位于 `#if defined(__linux__)` 之内。openkal 跑在 Linux +内核上,`__linux__` **是对的**;错的是配方把它读成了「glibc 的整套 Linux userspace 头 +都装好了」。同一个块里还有 `HAVE_GLIBC_STRERROR_R`(openkal-musl 是 musl,不是 glibc, +这一条**主动是错的**)、`HAVE_SYS_EVENTFD_H`、`HAVE_FSETXATTR`、以及一条写死的宿主路径 +`CURL_CA_BUNDLE "/etc/ssl/certs/..."`。**诚实的判据是 `__has_include()`** +——它是 C 标准的、问的正是要问的那件事,而不是从「哪个内核」推断「哪些头存在」。 + +**cmp-module 才是 C2。** asio 的 `detail/config.hpp` 写的是: + +```c +#if defined(__linux__) +# include /* 在所有 ASIO_DISABLE_* 守卫之外 */ +``` + +那个 `#include` **不受任何配置宏控制**——`-DASIO_DISABLE_EPOLL` 挡不住它。**源码不归 +我们改,而没有任何清单键伸得进第三方的 `.c`/`.hpp` 里**(这正是 `predefines.cppm` 记的 +第一条理由)。所以这个成员在面向 Linux 的 openkal 图里按构造建不起来。 + **形态**:程序 `#include ` 一类。openkal 不是 Linux,没有 uapi 头, **这是正确的**。 @@ -195,6 +363,13 @@ overruns」。libunwind 那处有 `static_assert` 才响,属于运气。 **归属**:mcpp-index(测量的成员表),不是 C 库。 +**已有机制比初稿以为的多一半。** `members.toml` 里已经有 `[excluded]` 表,它的语义正是 +「在任何 openkal 图里都建不起来」——`cmp-module` 恰好符合。缺的不是表,是**第二个理由 +类别**:现有六条都是「宿主程序 / 厂商二进制」,而 asio 这条是「上游源码无条件包含平台 +头」。判据不变:声明存在时该成员从分母里移除,且理由可追溯到那条声明。 + +**curl 不进这张表**,它要修配方。两者分开之后,九条失败里这一类只剩一条。 + #### C3 — `arc4random_buf`(expat)——**已修复,且归属与初稿不同** **初稿写的是**「musl 有 `arc4random_buf`,openkal-musl 的移植未导出」。**这是错的。** @@ -477,20 +652,25 @@ openkal-musl 0.18.0,而 0.18.0 当时还没进索引,消费者自己的 CI ## 7. 判据总表 -| 编号 | 判据 | 怎么算通过 | 阻塞 | +状态截至 2026-09-21 收尾。 + +| 编号 | 判据 | 怎么算通过 | 状态 | | --- | --- | --- | --- | -| E1 | 30 成员重测 | `windows.h` 组 4→0 且总失败不增 | 无 | -| E2 | 引用未提供接口的符号 | 链接期被点名拒绝;声明正确时零开销 | 工作量 | -| E3 | 提供方什么都不声明 | 构建成功**且**报告里有 unchecked 一行 | 无 | -| C1 | 五行最小复现 | 链接通过**且析构真的执行** | **第二层未定位** | -| C2 | curl / cmp-module | 记为 `refused` 而非 `fails` | 无 | -| C3 | `arc4random_buf` | 「应当有的符号」CI 断言 | 无 | -| C4 | `aarch64-macos --profile release` | **已通过,本机实测** | 关闭;不复现 | -| I1 | 写死的节点集合 | conformance 逐条断言 | 无 | -| I2 | `presents = "windows"` 的 C 库 | 引擎改动数为 0 | 无 | -| **A1** | 三目标 `mcpp build` | 全绿 | E1, C4 | -| **A2** | 三目标 `mcpp test` + 真跑 | 全绿 | A1 | -| **A3** | 三份 `os.cppm` | **除六行外逐字节相同** | A1 | +| E1 | 30 成员重测 | `windows.h` 组 4→0 且总失败不增 | **待测**(由移 `pins.toml` 的那个 PR 触发);`openkal-cross` 九格已全绿 | +| E1b | 大写重命名 | 上一版红、本版绿 | **已通过**:验证脚本 B 段,`2026.9.21.1` fails=2 → `2026.9.21.2` fails=0 | +| E1c | 交叉验证协议的反向 | `openkal_ref` 留空时行为不变 | **已落地** | +| E2 | 引用未提供接口的符号 | 链接期被点名拒绝;声明正确时零开销 | **退出本轮**:阻塞是设计决定不是工作量,见 §2.1 | +| E3 | 提供方什么都不声明 | 构建成功**且**报告里有 unchecked 一行 | **已通过**:e2e 743 四条腿 + 验证脚本 C 段双向 | +| C1 | 最小复现 | 链接通过**且析构真的执行** | **已通过**:三层全部定位并修复,`openkal-llvm-runtime@0.15.0` | +| C2 | cmp-module | 从分母移除,理由可追溯,**且声明可被证伪** | **已通过**:`[not-portable]` + `compat.py check`,红/绿两向实测 | +| C2' | curl | 配方缺陷,不是不可移植 | **已定位未修**:两个目标两个不同真因,见 `docs/openkal-compat.md` | +| C3 | `arc4random_buf` | 「应当有的符号」CI 断言 | 已通过 | +| C4 | `aarch64-macos --profile release` | 本机实测 | 关闭;不复现 | +| I1 | 写死的节点集合 | conformance 逐条断言 | 未实现(第五批) | +| I2 | `presents = "windows"` 的 C 库 | 引擎改动数为 0 | 未实现(第五批) | +| **A1** | 三目标 `mcpp build` | 全绿 | **待跑**(须用已发布钉,见 §8 第二条) | +| **A2** | 三目标 `mcpp test` + 真跑 | 全绿 | 待跑 | +| **A3** | 三份 `os.cppm` | **除六行外逐字节相同** | **已机械化并进 CI**;四种失败形态逐个量红过 | --- diff --git a/.agents/docs/README.md b/.agents/docs/README.md index d8683f72..fcb19197 100644 --- a/.agents/docs/README.md +++ b/.agents/docs/README.md @@ -63,7 +63,7 @@ Records that declare one. Everything else is listed by date below. - [本轮生态级自审](2026-09-20-wave-self-review.md) — active - [#674 设计方案评审:`-include unistd.h` 在 Windows + `presents = "posix"` 上的可行性](2026-09-20-issue-674-design-review.md) — active -- [`__cxa_thread_atexit` 在 openkal-Windows 上:定位到一层,第二层未定位](2026-09-20-cxa-thread-atexit-finding.md) — active +- [`__cxa_thread_atexit` 在 openkal-Windows 上:两层都已定位并修复](2026-09-20-cxa-thread-atexit-finding.md) — landed ### targets @@ -99,7 +99,7 @@ Records that declare one. Everything else is listed by date below. - [openkal 生态:能力的时刻模型,以及 C 环境方案空间的划分](2026-09-20-openkal-c-environment-ecosystem-design.md) — active - [#674 设计方案评审:`-include unistd.h` 在 Windows + `presents = "posix"` 上的可行性](2026-09-20-issue-674-design-review.md) — active - [C 环境生态方案:执行计划](2026-09-20-ecosystem-execution-plan.md) — active -- [`__cxa_thread_atexit` 在 openkal-Windows 上:定位到一层,第二层未定位](2026-09-20-cxa-thread-atexit-finding.md) — active +- [`__cxa_thread_atexit` 在 openkal-Windows 上:两层都已定位并修复](2026-09-20-cxa-thread-atexit-finding.md) — landed - [#674:`presents = "posix"` 在 Windows 上兑现契约的下半段](2026-09-19-issue-674-cenv-posix-preinclude-design.md) — superseded - [运行时绑定方案 v3:让 mcpp 真正安装它所声明的运行时](2026-09-17-runtime-binding-multi-repo-plan.md) — landed - [#662:目标侧由依赖图提供时,编译器的隐式头文件搜索仍指向宿主](2026-09-17-issue-662-graph-target-header-isolation-plan.md) — active diff --git a/.github/workflows/openkal-cross.yml b/.github/workflows/openkal-cross.yml index 21d7cefa..b2bc67d3 100644 --- a/.github/workflows/openkal-cross.yml +++ b/.github/workflows/openkal-cross.yml @@ -57,6 +57,11 @@ name: openkal cross-build (3 hosts × 3 targets) on: pull_request: workflow_dispatch: + inputs: + openkal_ref: + description: "Branch of the openkal packages to build against (empty = main)" + required: false + default: "" concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} @@ -77,7 +82,22 @@ env: # engine against a tree the ecosystem had left behind — and the two # regressions found today both hid behind exactly this shape, a pin that # keeps a check green by keeping it out of date. - OPENKAL_BRANCH: main + # + # IT IS OVERRIDABLE FOR ONE RUN, AND THAT IS THE OTHER HALF OF A PROTOCOL + # THIS REPOSITORY ALREADY HAS ONE HALF OF. 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 this job is the one that fails. + # + # `__CYGWIN__`'s withdrawal is the case that showed it: this job builds + # `openkal-llvm-runtime@main`, whose installed header read only the borrowed + # name, so the engine's own CI reproduced the ordering constraint as a red + # cell. The constraint is real and the cell is correct --- the packages must + # publish first --- but verifying the engine BEFORE that publish needs this + # input. Left empty, nothing changes. + OPENKAL_BRANCH: ${{ github.event.inputs.openkal_ref || 'main' }} jobs: build: diff --git a/.xlings.json b/.xlings.json index 27cf18b7..e05750f1 100644 --- a/.xlings.json +++ b/.xlings.json @@ -1,5 +1,5 @@ { "workspace": { - "mcpp": "2026.9.20.1" + "mcpp": "2026.9.21.1" } } diff --git a/CHANGELOG.md b/CHANGELOG.md index d8ad1587..e3de8ab3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,94 @@ ## [Unreleased] +## [2026.9.21.2] - 2026-09-21 + +### 引擎拥有的宏改为全大写 + +`__mcpp_target___` 成为 `__MCPP_TARGET___`,`__openkal__` 成为 `__OPENKAL__`。 +拼法仍取自三元组自己的 `os` 字段,只是转为大写,引擎依旧不认识任何操作系统名。 + +**业界的约定是按「名字是什么」分的,不是按谁写的分。** 厂商名与产品名用大写 +(`__APPLE__`、`_WIN32`、`__MINGW32__`、`__GNUC__`),系统种类名用小写(`__linux__`、 +`__unix__`、`__gnu_linux__`)。mcpp **拥有**的每一行都属于第一类:它命名的是 mcpp,或者 +是 openkal。「是哪一种系统」由 `__linux__` 那一族回答,而那一族是 mcpp **供给**而非拥有 +的,所以它们保持小写。 + +小写拼法在 2026.9.21.1 发布过一版,产生它的推理是:这些名字在真实守卫里与 `__linux__` +并排出现,跟它一致看起来就像一致性。**那是把「相邻」当成了「同类」。** `__APPLE__` 出现 +在同样那些守卫里却是大写,因为它属于某个人。 + +**`__MCPP_` 是前缀而不是规则的全部。** `__OPENKAL__` 同样是 mcpp 拥有的,但它命名的是 +*openkal* 而不是 mcpp。`test_predefines.cpp` 里那条断言因此改为检查**拼法约定**(大写、 +`__` 包裹),而不再检查前缀——按前缀断言会把 `__OPENKAL__` 判成违规,而它不是。 + +### 撤销一个宏的判据是一次数读者的测量 + +这张表里的一条是**已发布的接口**,而撤销一条是**静默**的:一个 `#if` 选了另一条分支, +照常编译。构建工具手里没有任何机制能让它变响。所以撤销不由**读**来决定,而由**一次枚举 +读者的测量**来决定,数出多少就决定要走几步。 + +| 撤销的名字 | 数出的读者 | 步数 | +|---|---|---| +| `__CYGWIN__` | 四个第三方成员,外加本生态**安装出去**的两个头里的六处 | 三步 | +| `__mcpp_target___`、`__openkal__` | 零 | 一步 | + +第二行的分母是全生态每一个仓库,逐文件类型扫过:没有一个源文件、没有一份清单读它们, +出现的地方只有引擎自己的发出处、它的测试与散文。两个名字都是在这里发明的,所以不可能有 +上游代码握着它们;暴露窗口 `__openkal__` 是三天,目标宏是一个发布。**两次之间规则没变, +变的是数目。** + +新增判据 `EveryTargetInTheRegistryYieldsAValidIdentifier`:分母取自目标注册表本身而不是 +测试旁边写死的一张名单,逐行解析并断言发出的宏是合法标识符——拼法既然取自 `os` 字段,一个 +带点或带版本后缀的 `os` 就会产出编不过的宏,而失败会落在用户的构建里而不是这里。 + +### `__CYGWIN__` 撤销完成(三仓序列的最后一步) + +Windows 上 `presents = "posix"` 的实现现在在编译行加 `-U__CYGWIN__` / `-U__CYGWIN32__`。 +`--target=x86_64-pc-cygwin` 本身保留——它供给 `__unix__` 并压掉 `_WIN32`,那正是「呈现 +POSIX」的含义;不需要的只是那个**借来的名字**。 + +30 成员测量判定这个名字的代价是四个成员(`archive`、`sqlite3`、`mimalloc`、`c-ares` 各自 +停在 `#include `,经由 `#if defined(_WIN32) || defined(__CYGWIN__)`)。上游用 +它表达「Win32 可用」,mimalloc 把这句话写在守卫自己的注释里。 + +**序列按仓库排序,而这个次序就是安全性论证本身:** + +1. 2026.9.21.1 在借来的名字旁边加上 mcpp 自己的名字;2026.9.21.2 趁它还没有消费者时改成 + 大写。每一步都是纯增量。 +2. `openkal-musl@0.19.0` 与 `openkal-llvm-runtime@0.14.0` 改读新名字,并保留 + `|| defined(__CYGWIN__)`,于是在本次改动两侧的引擎上都能构建。**先于第三步发布。** +3. 本版停止定义借来的名字。 + +先做第三步——本分支的一个更早修订就这么做过——会让**已发布**的那两个安装头全部落进各自的 +`#else`。libunwind 的 `static_assert` 会响亮地红;`setjmp.h` 那一处不会,它自己的注释写着 +「a mismatch nothing reports until the record overruns」。 + +**残余窗口被点名而不是被说没有。** 把 `openkal-musl` **精确**钉在 0.18.0 或更早、同时把 +引擎升过本版的工程,会拿到那个静默的 `#else`。在本版之前把索引的 `latest` 移到 0.19.0, +是把窗口压到「精确钉」的办法;这里没有任何机制能把它关掉,因为引擎无从知道一个包安装出去 +的头读了哪些宏。 + +### 一条没人回答的 requirement 会说出来 + +一共三种情形、其中两种能构建:提供方陈述了 `provides-interfaces` 且包含该需求(构建); +陈述了但不包含(拒绝);**什么都没陈述(构建,而在这行提示之前是静默的)**。第三种是有意的 +——`provides-interfaces` 晚于那些实现出现,一个还没采纳它的图必须照常构建——但它让 +**「是」与「没问成」同读数**。 + +``` + note kernel-abi interfaces: fakekernel@0.1.0 states none, 2 requirements unchecked +``` + +提示**点名解析出的那个实现**:一个只被告知「有东西没检查」的读者无法据此行动。提供方确实 +陈述了列表时这行不出现——`tests/e2e/743` 双向断言,新增的第四条腿正是为此:没有它,第三条腿 +在一个无条件打印这行的引擎上同样会绿,那样就什么都没测到。 + +### 自举钉 + +`.xlings.json` 的 workspace pin 从 2026.9.20.1 走到 2026.9.21.1。 + + ## [2026.9.21.1] - 2026-09-21 ### 引擎定义的宏成为一份规范,而规范就是那个模块 diff --git a/docs/21-the-target-triple.md b/docs/21-the-target-triple.md index 6305a0c6..b2ec66d6 100644 --- a/docs/21-the-target-triple.md +++ b/docs/21-the-target-triple.md @@ -621,8 +621,8 @@ fails a build rather than drifting into a release. | macro | when | owned | |---|---|---| -| `__mcpp_target___` | one per build, spelt from the triple's `os` field | yes | -| `__openkal__` | the resolved `kernel-abi` layer's interface name is `openkal` | yes | +| `__MCPP_TARGET___` | one per build, spelt from the triple's `os` field | yes | +| `__OPENKAL__` | the resolved `kernel-abi` layer's interface name is `openkal` | yes | | `__unix__` | the `[c-abi]` realisation supplies it where the toolchain would not | no | **An engine should not define macros, and each row has to justify itself.** A @@ -638,12 +638,12 @@ manifest; a macro is none of those. Two justifications have survived: spelled in its manifest, but a header it INSTALLS is read by an application's own compile, which those defines never reach. -**`__mcpp_target___` — the rule.** Defined for every target-side +**`__MCPP_TARGET___` — the rule.** Defined for every target-side translation unit, always, one per build. The spelling is the triple's own `os` -field, so `x86_64-linux-gnu` gives `__mcpp_target_linux__`, `x86_64-windows-gnu` -gives `__mcpp_target_windows__`, and `riscv64-none-elf` gives -`__mcpp_target_none__`. The engine learns no operating-system name: a target -added to the triple parser gets its macro with no engine change. +field, upper-cased, so `x86_64-linux-gnu` gives `__MCPP_TARGET_LINUX__`, +`x86_64-windows-gnu` gives `__MCPP_TARGET_WINDOWS__`, and `riscv64-none-elf` +gives `__MCPP_TARGET_NONE__`. The engine learns no operating-system name: a +target added to the triple parser gets its macro with no engine change. *Allowed:* learning the target's operating system where the C environment presented above it has suppressed the platform's own macros, and sizing a @@ -660,31 +660,58 @@ emission would make its absence ambiguous: "not Windows" and "Windows, but nothing hid its macros" would read the same. A macro whose absence means one thing is worth one `-D`. -**Naming.** `__mcpp_`-prefixed, `__`-suffixed, lowercase, words separated by -`_`. Two conventions exist in the wild — vendor and product names are upper -(`__APPLE__`, `_WIN32`, `__MINGW32__`), kind-of-system names are lower -(`__linux__`, `__unix__`) — and these name kinds of target, sitting beside -that second family in real guards: `#if defined(__linux__) || -defined(__mcpp_target_windows__)`. - -The `__mcpp_` prefix is load-bearing. A name mcpp owns means what mcpp says it -means. The alternative was tried: `__CYGWIN__` was left defined so that code -needing "PE object format, POSIX C environment" would have a name, and a -30-member measurement found four members reading it as *Win32 is available* -and reaching `#include ` — which is what upstream means by it. **A -borrowed name means what the lender's history made it mean**, not what the -borrower intended. +**Naming.** UPPER CASE, `__`-wrapped, words separated by `_`; the names mcpp +owns carry `__MCPP_`. + +The convention in the wild 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 mcpp 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 first, and the reasoning that produced it is +worth keeping because the mistake is easy to repeat: these names sit beside +`__linux__` in real guards, so matching it looked like consistency. **That +confuses adjacency with kind.** `__APPLE__` sits in those same guards and is +upper, because it belongs to somebody — as does every other name in them that +belongs to somebody. + +The `__MCPP_` prefix is load-bearing, and it is a prefix rather than the whole +rule: a name mcpp owns means what mcpp says it means, while `__OPENKAL__` is +owned and names *openkal* rather than mcpp. The alternative — borrowing — +was tried. `__CYGWIN__` was left defined so that code needing "PE object +format, POSIX C environment" would have a name, and a 30-member measurement +found four members reading it as *Win32 is available* and reaching `#include +`, which is what upstream means by it. **A borrowed name means what +the lender's history made it mean**, not what the borrower intended. `__unix__` is the exception that proves the rule: mcpp SUPPLIES it rather than owning it, so it keeps the standard spelling and mcpp may not change its meaning. Read it exactly as on any other POSIX system. -**Stability.** An entry here is a published interface. Removing one, or -narrowing when it is defined, is a breaking change for source this project -does not control, and the failure is usually SILENT — a `#if` selects the -other branch and compiles. Withdrawal is therefore a sequence, never an edit: -add the replacement, let consumers move onto it while still accepting the old -name, and only then stop defining it. +**Stability.** An entry here is a published interface, and withdrawing one is +SILENT: a `#if` selects the other branch and compiles, on a machine nobody is +watching. No mechanism available to a build tool makes that loud — a +preprocessor cannot be told to complain about a name it does not find. + +So a withdrawal is never decided by reading. It is decided by **a measurement +that enumerates readers**, and what the measurement finds sets how many steps +the withdrawal takes. Both withdrawals this project has performed are on the +record, and they came out differently: + +| withdrawn | readers found | steps | +|---|---|---| +| `__CYGWIN__` (2026.9.21.1) | four third-party members, plus six sites in the two headers this ecosystem installs | three: publish the replacement, move consumers, stop defining | +| `__mcpp_target___`, `__openkal__` (2026.9.21.2) | none — no source file and no manifest in any repository of this ecosystem | one | + +The rule did not change between those two; the count did. The second row is +cheap for a reason worth stating: both names were invented here, so no +upstream code can be holding one, and the exposure was three days for +`__openkal__` and a single release for the target macro. A withdrawal argued +from anything other than a count is an argument this project has already got +wrong once. ## Custom Targets diff --git a/docs/22-target-side.md b/docs/22-target-side.md index 3f9972fa..f7c661e9 100644 --- a/docs/22-target-side.md +++ b/docs/22-target-side.md @@ -377,7 +377,7 @@ that names no C library: | Linux | `posix` / `arch-default` | the default triple already satisfies it | | macOS | `posix` / `arch-default` | one token, `-D__unix__` — Apple's clang predefines `__APPLE__`/`__MACH__` on its default triple, never `__unix__` | | freestanding | `posix` / `arch-default` | the same one token, `-D__unix__`, for the same reason: nothing here defines it either | -| Windows | `posix` / `arch-default` | Cygwin-flavoured: `--target=x86_64-pc-cygwin` on the compile line only; plus `-D__mcpp_target_windows__` (see the note below); `data-model` becomes LP64 as a consequence of the triple, not a separate flag | +| Windows | `posix` / `arch-default` | Cygwin-flavoured: `--target=x86_64-pc-cygwin` on the compile line only; plus `-D__MCPP_TARGET_WINDOWS__` (see the note below); `data-model` becomes LP64 as a consequence of the triple, not a separate flag | | any | `builtins = "iso"` | turns off code-generation idioms that assume a platform C library — `-fno-builtin-memset_pattern16` on Apple targets is the one this survey measured; see `src/toolchain/cenv.cppm` for what else was checked and found not to apply | | anything else | | refused, naming the target, the request and what is missing — never a silent downgrade | @@ -418,8 +418,9 @@ preprocessor sees and how wide `long` is. Realisation therefore touches only the **compile** line; the **link** line keeps the triple the graph resolved, because nothing about the object format changed. -**`__mcpp_target_windows__` is defined here, and `__CYGWIN__` still is too -(2026.9.21.1).** +**`__MCPP_TARGET_WINDOWS__` is defined here, and `__CYGWIN__` still is too +(2026.9.21.1; the name was spelt in lower case in that one release and +re-spelt in 2026.9.21.2, before it had a consumer).** The substitution suppresses `_WIN32` on purpose — that is what presenting POSIX means. But the **ABI did not change with the environment**: the calling @@ -432,7 +433,7 @@ INSTALLED headers in this ecosystem size records by that fact: | openkal-llvm-runtime | `__libunwind_config.h` | `unw_context_t`, `unw_cursor_t` | An installed header is read by an **application's own compile**, so neither -can use a package-private define. `__mcpp_target_windows__` is mcpp's own +can use a package-private define. `__MCPP_TARGET_WINDOWS__` is mcpp's own name for the question they ask — is this target Windows, whatever C environment is presented above it — and being mcpp's own, its meaning is not decided by anyone else's history. It is emitted only under this substitution; @@ -453,7 +454,7 @@ loudly; `setjmp.h`'s equivalent would not have — its own comment says *a mismatch nothing reports until the record overruns*. So the withdrawal is three steps, and each intermediate state builds: -1. this release defines `__mcpp_target_windows__` as well — purely additive; +1. 2026.9.21.1 defines `__MCPP_TARGET_WINDOWS__` as well — purely additive; 2. those packages read the new name, keeping `|| defined(__CYGWIN__)` so they build on both engines; 3. a later release stops defining the borrowed one. @@ -607,6 +608,23 @@ A graph whose implementation carries no `provides-interfaces` builds unchanged; the key postdates the packages, and the link still reports an absence in the vocabulary it always did. +**It says so, though (mcpp 2026.9.21.2).** Three situations exist and two of +them build — the provider states a list containing the requirement, states a +list without it (refused), or states nothing at all — and until this note the +first and the third produced identical output: + +``` + note kernel-abi interfaces: fakekernel@0.1.0 states none, 2 requirements unchecked +``` + +**A requirement nobody answered reads exactly like a confirmed one**, which is +the failure this closes: a consumer inspecting a green build could not tell +"checked and agreed" from "never asked". The note names the resolved +implementation, because a reader told only that something went unchecked +cannot act on it. It is absent whenever the provider does state its list, and +`tests/e2e/743` asserts both directions — without the second leg the first +would pass against an engine that printed the line unconditionally. + **What a C library does not supply (`[c-abi-absent]`, mcpp 2026.9.20.1).** The set of names a C library supplies is not enumerable in a manifest — POSIX has about twelve hundred — and enumerating it is the mistake §3.3 records @@ -664,7 +682,7 @@ cache an ordinary dependency compile reuses across projects and across an keyed per package from exactly the axes that reach that package's own compile command line (`mcpp.build.cache_key`). The realised environment reaches a package's command line entirely through an engine BROADCAST (the -same channel `targetSideUsage` and `-D__openkal__` use, never the package's +same channel `targetSideUsage` and `-D__OPENKAL__` use, never the package's own declared `[build] cflags`/`cxxflags`), so the key's own derivation had to be told to read the broadcast, not only the declaration — found exactly that way (coordinator report, openkal-musl spike): upgrading `mcpp` in diff --git a/docs/24-openkal-cross.md b/docs/24-openkal-cross.md index a2b69875..1afaf489 100644 --- a/docs/24-openkal-cross.md +++ b/docs/24-openkal-cross.md @@ -44,7 +44,7 @@ meant "is the Windows CRT present." | Family | States | Defined by | Example | |---|---|---|---| -| kernel ABI | `kal_*` is callable, and behaves the same on every platform | the layer providing `mcpp:kernel-abi=openkal` | `__openkal__` | +| kernel ABI | `kal_*` is callable, and behaves the same on every platform | the layer providing `mcpp:kernel-abi=openkal` | `__OPENKAL__` | | C environment | the shape of the C environment source sees | the layer providing `mcpp:c-abi=`, via [`[c-abi]`](22-target-side.md#the-c-environment-a-c-abi-package-presents-mcpp-2026918) | `__unix__`, `_WIN32`, `__MINGW32__` | | system & architecture | the underlying OS and processor | the target triple | `__linux__`, `__APPLE__`, `__x86_64__` | @@ -58,7 +58,7 @@ combination, "PE format with a POSIX-presenting C environment," is therefore leaves DEFINED rather than folding into the three rows above — see [22's own note](22-target-side.md#the-c-environment-a-c-abi-package-presents-mcpp-2026918) for the full trade-off. Since 2026.9.21.1 `__CYGWIN__` is no longer the only -name for that fact: mcpp defines `__mcpp_target___` for every target +name for that fact: mcpp defines `__MCPP_TARGET___` for every target (`docs/21`, "The macros mcpp defines"), so source needing to know the target under a presented environment has a name mcpp owns. `__CYGWIN__` remains defined while the ecosystem's installed headers move onto the new name, and @@ -67,7 +67,7 @@ than as what they are — a fact about the TARGET, not about the environment presented above it — is exactly the shape of confusion this whole section exists to head off. -**`__openkal__` — the rule.** The engine defines it, for every target-side +**`__OPENKAL__` — the rule.** The engine defines it, for every target-side unit, whenever the resolved `kernel-abi` layer's interface name is `openkal` — read from the LAYER's value, never from a package name, so a second implementation (`openkal-macos`, `openkal-opensbi`, …) needs no @@ -85,7 +85,7 @@ write `cfg(c-abi = "…")` or `cfg(kernel-abi = "…")` in the manifest instead for the predicate grammar). **Platform units.** A package that itself needs the platform's own -environment never reads `__openkal__` or any other macro to work that out — +environment never reads `__OPENKAL__` or any other macro to work that out — the boundary is stated in the manifest, not inferred from source, and everything crossing it is still fixed-width (SPEC §5.4). Two different packages reach `[package] c-environment = "platform"` (docs/22) by two diff --git a/docs/zh/21-the-target-triple.md b/docs/zh/21-the-target-triple.md index d1a92825..e7fabc2c 100644 --- a/docs/zh/21-the-target-triple.md +++ b/docs/zh/21-the-target-triple.md @@ -553,8 +553,8 @@ x86_64 宿主行得通的路线,而平台模拟器会直接拒绝异构 guest | 宏 | 何时定义 | 是否 mcpp 拥有 | |---|---|---| -| `__mcpp_target___` | 每次构建一个,拼法取自三元组的 `os` 字段 | 是 | -| `__openkal__` | 解析出的 `kernel-abi` 层接口名是 `openkal` | 是 | +| `__MCPP_TARGET___` | 每次构建一个,拼法取自三元组的 `os` 字段 | 是 | +| `__OPENKAL__` | 解析出的 `kernel-abi` 层接口名是 `openkal` | 是 | | `__unix__` | `[c-abi]` 实现供给它,在工具链不会定义它的地方 | 否 | **引擎原则上不该定义任何宏,每一行都要为自己辩护。** 包在清单里陈述需要什么,引擎以 @@ -566,11 +566,11 @@ x86_64 宿主行得通的路线,而平台模拟器会直接拒绝异构 guest 2. **读者是已安装的头。** 包自己的构建 define 可以写在清单里,但它**安装出去**的头会被 **应用程序自己的编译**读到,那些 define 永远到不了那里。 -**`__mcpp_target___` 的规则。** 为每一个目标侧翻译单元定义,总是定义,每次构建一个。 -拼法取自三元组自己的 `os` 字段:`x86_64-linux-gnu` 给出 `__mcpp_target_linux__`, -`x86_64-windows-gnu` 给出 `__mcpp_target_windows__`,`riscv64-none-elf` 给出 -`__mcpp_target_none__`。引擎不认识任何操作系统名:三元组解析器新增一个目标,它的宏随之 -存在,引擎无需改动。 +**`__MCPP_TARGET___` 的规则。** 为每一个目标侧翻译单元定义,总是定义,每次构建一个。 +拼法取自三元组自己的 `os` 字段并转为大写:`x86_64-linux-gnu` 给出 +`__MCPP_TARGET_LINUX__`,`x86_64-windows-gnu` 给出 `__MCPP_TARGET_WINDOWS__`, +`riscv64-none-elf` 给出 `__MCPP_TARGET_NONE__`。引擎不认识任何操作系统名:三元组解析器新增 +一个目标,它的宏随之存在,引擎无需改动。 *允许:* 当上面呈现的 C 环境压掉了平台自己的宏时,用它得知目标的操作系统;以及按目标的 ABI 给记录定尺寸。呈现 POSIX 的 Windows 目标**有意**没有 `_WIN32`,而调用约定仍是 Win64 @@ -583,23 +583,44 @@ ABI 给记录定尺寸。呈现 POSIX 的 Windows 目标**有意**没有 `_WIN32 「不是 Windows」与「是 Windows,但没有东西压掉它的宏」会读成同一件事。一个缺席只有一种 含义的宏,值一个 `-D`。 -**命名。** `__mcpp_` 前缀、`__` 后缀、小写、单词用 `_` 分隔。业界并存两套约定——厂商与 -产品名用大写(`__APPLE__`、`_WIN32`、`__MINGW32__`),系统种类名用小写(`__linux__`、 -`__unix__`)——而这些命名的是目标的种类,在真实守卫里与第二族并排出现: -`#if defined(__linux__) || defined(__mcpp_target_windows__)`。 +**命名。** **全大写**、`__` 包裹、单词用 `_` 分隔;mcpp 拥有的名字带 `__MCPP_`。 -`__mcpp_` 这个前缀是承重的。**一个 mcpp 拥有的名字,语义由 mcpp 自己定。** 另一条路试过: -`__CYGWIN__` 曾被保留定义,为的是让需要「PE 目标文件格式 + POSIX C 环境」的代码有个名字, -而 30 成员测量发现四个成员把它读成「**Win32 可用**」并走向 `#include `——那正是 -上游用它表达的意思。**一个借来的名字,语义由借出方的历史决定**,不由借用方的意图决定。 +业界的约定是按**名字是什么**分的,不是按谁写的分。厂商名与产品名用大写——`__APPLE__`、 +`_WIN32`、`__MINGW32__`、`__GNUC__`;系统种类名用小写——`__linux__`、`__unix__`、 +`__gnu_linux__`。mcpp **拥有**的每一行都属于第一类:它命名的是 mcpp,或者是 openkal。 +「是哪一种系统」这个问题由 `__linux__` 那一族回答,而那一族是 mcpp **供给**而非拥有的, +正因如此它们保持小写拼法。 + +小写拼法先发布过,而产生它的那段推理值得留着,因为这个错很容易再犯一次:这些名字在真实 +守卫里与 `__linux__` 并排出现,于是「跟它一致」看起来就像一致性。**那是把「相邻」当成了 +「同类」。** `__APPLE__` 出现在同样那些守卫里,却是大写,因为它属于某个人——那些守卫里 +凡是属于某个人的名字都是大写。 + +`__MCPP_` 这个前缀是承重的,但它是**前缀**而不是规则的全部:**一个 mcpp 拥有的名字,语义 +由 mcpp 自己定**,而 `__OPENKAL__` 同样是拥有的,它命名的是 *openkal* 而不是 mcpp。另一条 +路——**借用**——试过:`__CYGWIN__` 曾被保留定义,为的是让需要「PE 目标文件格式 + POSIX C +环境」的代码有个名字,而 30 成员测量发现四个成员把它读成「**Win32 可用**」并走向 +`#include `——那正是上游用它表达的意思。**一个借来的名字,语义由借出方的历史 +决定**,不由借用方的意图决定。 `__unix__` 是印证这条规则的例外:mcpp **供给**它而不拥有它,所以它保持标准拼法,mcpp 也 不得改变它的含义。读它就按在任何别的 POSIX 系统上那样读。 -**稳定性。** 这张表里的一条是**已发布的接口**。删掉一条、或收窄它被定义的条件,对本项目 -控制不了的源码就是破坏性变更,而失败通常是**静默**的——一个 `#if` 选了另一条分支,照常 -编译。所以撤销永远是一个**序列**,不是一次编辑:先加替代品,让消费者迁移过去并仍接受旧 -名字,之后才停止定义它。 +**稳定性。** 这张表里的一条是**已发布的接口**,而撤销一条是**静默**的:一个 `#if` 选了 +另一条分支,照常编译,发生在没人看着的机器上。构建工具手里没有任何机制能让它变响—— +预处理器无法被告知「为一个它找不到的名字报错」。 + +所以撤销从来不由**读**来决定,而由**一次枚举读者的测量**来决定,测量数出多少,就决定这次 +撤销要走几步。本项目做过的两次撤销都在记录里,而它们的结果不同: + +| 撤销的名字 | 数出的读者 | 步数 | +|---|---|---| +| `__CYGWIN__`(2026.9.21.1) | 四个第三方成员,外加本生态**安装出去**的两个头里的六处 | 三步:发布替代品、迁移消费者、最后停止定义 | +| `__mcpp_target___`、`__openkal__`(2026.9.21.2) | 零——本生态任何仓库里没有一个源文件、没有一份清单读它们 | 一步 | + +两次之间规则没变,变的是数目。第二行便宜是有原因的,而这个原因值得写出来:两个名字都是 +在这里发明的,所以不可能有上游代码握着它们;暴露窗口 `__openkal__` 是三天,目标宏是一个 +发布。**一次不由数目支撑的撤销,正是本项目已经犯错过一次的那种论证。** ## 自定义目标 diff --git a/docs/zh/22-target-side.md b/docs/zh/22-target-side.md index 6f9c9758..5405f82b 100644 --- a/docs/zh/22-target-side.md +++ b/docs/zh/22-target-side.md @@ -309,7 +309,7 @@ libunwind 的 `assembly.h`,正是按这个宏来选寄存器保存集的)。mcpp | Linux | `posix` / `arch-default` | 默认三元组已经满足 | | macOS | `posix` / `arch-default` | 一个令牌,`-D__unix__`——Apple 的 clang 默认三元组预定义的是 `__APPLE__`/`__MACH__`,从来不是 `__unix__` | | 裸机(freestanding) | `posix` / `arch-default` | 同样一个令牌,`-D__unix__`,原因相同:这里同样没有任何东西定义它 | -| Windows | `posix` / `arch-default` | 采用 Cygwin 式语义:仅在编译行加 `--target=x86_64-pc-cygwin`;并加 `-D__mcpp_target_windows__`(见下方说明);`data-model` 变为 LP64 是三元组切换的结果,不是另一个开关 | +| Windows | `posix` / `arch-default` | 采用 Cygwin 式语义:仅在编译行加 `--target=x86_64-pc-cygwin`;并加 `-D__MCPP_TARGET_WINDOWS__`(见下方说明);`data-model` 变为 LP64 是三元组切换的结果,不是另一个开关 | | 任意目标 | `builtins = "iso"` | 关闭代码生成阶段假定平台 C 库在场的惯用法识别——本轮实测到的唯一一例是 Apple 目标上的 `-fno-builtin-memset_pattern16`;`src/toolchain/cenv.cppm` 记录了还核实过哪些、结论是不适用 | | 其余情况 | | 明确拒绝,点名目标、请求与缺什么——不静默降级 | @@ -337,7 +337,8 @@ Windows 一行是旗舰情形:`x86_64-w64-windows-gnu` 与 `x86_64-pc-cygwin` 同样的 PE 格式、同样的 Win64 调用约定、同样的 SEH——差别只在预处理器看到什么、`long` 有多宽。 因此实现只触及**编译**行;**链接**行保持图解析出的三元组,因为目标文件格式没有变化。 -**这里定义 `__mcpp_target_windows__`,而 `__CYGWIN__` 仍然定义着(2026.9.21.1)。** +**这里定义 `__MCPP_TARGET_WINDOWS__`,而 `__CYGWIN__` 仍然定义着(2026.9.21.1;这个名字在 +那一版是小写拼法,2026.9.21.2 在它还没有消费者时改成了大写)。** 这次替换有意压掉 `_WIN32`——那正是「呈现 POSIX」的含义。但**ABI 并没有跟着环境一起变**: 调用约定仍是 Win64,寄存器保存区仍按它的大小。生态里有两个**已安装的**头按这个事实定尺寸: @@ -348,7 +349,7 @@ Windows 一行是旗舰情形:`x86_64-w64-windows-gnu` 与 `x86_64-pc-cygwin` | openkal-llvm-runtime | `__libunwind_config.h` | `unw_context_t`、`unw_cursor_t` | 已安装的头会被**应用程序自己的编译**读到,所以两者都用不了包私有的 define。 -`__mcpp_target_windows__` 是 mcpp 为它们所问的那个问题给出的**自己的**名字——这个目标是不是 +`__MCPP_TARGET_WINDOWS__` 是 mcpp 为它们所问的那个问题给出的**自己的**名字——这个目标是不是 Windows,无论上面呈现的是什么 C 环境——既然是自己的名字,它的语义就不由别人的历史决定。 它只在这次替换下发出;普通的 Windows 构建仍然有 `_WIN64`。 @@ -362,7 +363,7 @@ Windows,无论上面呈现的是什么 C 环境——既然是自己的名字, libunwind 的 `static_assert` 响亮地红了;`setjmp.h` 那一处不会——它自己的注释写着 「a mismatch nothing reports until the record overruns」。所以撤销分三步,每个中间状态都能构建: -1. 本版**增加** `__mcpp_target_windows__`——纯增量; +1. 2026.9.21.1 **增加** `__MCPP_TARGET_WINDOWS__`——纯增量; 2. 那两个包改读新名字,保留 `|| defined(__CYGWIN__)` 以便在两种引擎上都能构建; 3. 之后的版本再停止定义借来的那个。 @@ -476,6 +477,19 @@ requires-interfaces = ["openkal.fs", "openkal.net"] **一个什么都没陈述的提供者,不是一个什么都不提供的提供者。** 实现方没有写 `provides-interfaces` 的图照常构建;这个键晚于那些包出现,而链接仍以它一贯的词汇报告缺席。 +**但它会说出来(mcpp 2026.9.21.2)。** 一共三种情形、其中两种能构建——提供方陈述了列表且 +包含该需求;陈述了列表但不包含(拒绝);什么都没陈述——而在这行提示之前,第一种与第三种 +产生一模一样的输出: + +``` + note kernel-abi interfaces: fakekernel@0.1.0 states none, 2 requirements unchecked +``` + +**一条没人回答的需求,读起来和一条被确认的需求一模一样**,这正是它要关掉的缺口:一个看着 +绿色构建的消费者,分不清「问过并且一致」与「从来没问成」。提示点名解析出的那个实现,因为 +一个只被告知「有东西没检查」的读者无法据此行动。提供方确实陈述了列表时这行不出现, +`tests/e2e/743` 双向断言——没有第二条腿的话,第一条腿在一个无条件打印这行的引擎上同样会绿。 + **一个 C 库不供给什么(`[c-abi-absent]`,mcpp 2026.9.20.1)。** 一个 C 库供给的名字集合 在清单里不可枚举——POSIX 约有一千二百个——而枚举它正是 §3.3 记录下撤回的那个错误。 例外是可枚举的: @@ -517,7 +531,7 @@ tcsetattr = { form = "accepted-no-effect", note = "openkal 不命名的那些字 `~/.mcpp/build-cache/v1`——一次普通依赖编译跨项目、也跨 `mcpp` 升级复用的缓存——是与上面 构建指纹分开的另一套机制,按包逐一取键,只取真正到达该包自身编译命令行的那些轴 (`mcpp.build.cache_key`)。解析出的环境到达一个包的命令行,完全是通过引擎的**广播** -(与 `targetSideUsage`、`-D__openkal__` 同一条通道),从来不经过包自己声明的 +(与 `targetSideUsage`、`-D__OPENKAL__` 同一条通道),从来不经过包自己声明的 `[build] cflags`/`cxxflags`——所以键的推导本身也得被告知去读广播后的值,而不只是声明的值。 这个缺口正是这样被发现的(协调者反馈,openkal-musl 尖峰实验):原地升级 `mcpp`、缓存目录 未清理时,给按新环境构建的镜像喂了按**旧**解析环境编译出的目标文件——一个镜像里混了两种 diff --git a/docs/zh/24-openkal-cross.md b/docs/zh/24-openkal-cross.md index 87c16442..01bc5973 100644 --- a/docs/zh/24-openkal-cross.md +++ b/docs/zh/24-openkal-cross.md @@ -35,7 +35,7 @@ C++ 运行时 —— 成为一组由依赖图解析、并由当前运行的编 | 宏族 | 陈述 | 定义者 | 例子 | |---|---|---|---| -| 内核 ABI | `kal_*` 可调用,且在每个平台上行为一致 | 提供 `mcpp:kernel-abi=openkal` 的层 | `__openkal__` | +| 内核 ABI | `kal_*` 可调用,且在每个平台上行为一致 | 提供 `mcpp:kernel-abi=openkal` 的层 | `__OPENKAL__` | | C 环境 | 源码看到的 C 环境形状 | 提供 `mcpp:c-abi=` 的层,通过 [`[c-abi]`](22-target-side.md#c-abi-包陈述它呈现的-c-环境mcpp-2026918) | `__unix__`、`_WIN32`、`__MINGW32__` | | 系统与架构 | 底层操作系统与处理器 | 目标三元组 | `__linux__`、`__APPLE__`、`__x86_64__` | @@ -47,13 +47,13 @@ C++ 运行时 —— 成为一组由依赖图解析、并由当前运行的编 三行里的任何一行——完整的权衡说明见 [22 自己的说明](22-target-side.md#c-abi-包陈述它呈现的-c-环境mcpp-2026918)。自 2026.9.21.1 起,`__CYGWIN__` 不再是那个事实的唯一名字:mcpp 为每个目标定义 -`__mcpp_target___`(见 `docs/21`「mcpp 定义的宏」),于是需要在被呈现的环境之下得知 +`__MCPP_TARGET___`(见 `docs/21`「mcpp 定义的宏」),于是需要在被呈现的环境之下得知 目标的源码,有了一个 mcpp 自己拥有的名字。`__CYGWIN__` 在生态的已安装头迁移到新名字期间 仍然定义着,之后才撤销。把两者中的任何一个当成第四个 C 环境宏来读,而不是它们实际是的 样子——一个关于**目标**的事实,而不是关于其上呈现的环境——正是这一整节想要提前避免的 那种混淆。 -**`__openkal__` 的规则。** 只要解析出的 `kernel-abi` 层的接口名是 `openkal`,引擎就为目标侧的 +**`__OPENKAL__` 的规则。** 只要解析出的 `kernel-abi` 层的接口名是 `openkal`,引擎就为目标侧的 每一个编译单元定义它——取自层的取值,绝不取自包名,因此第二个 `mcpp:kernel-abi=openkal` 的 实现不需要引擎改动。 @@ -65,7 +65,7 @@ C++ 运行时 —— 成为一组由依赖图解析、并由当前运行的编 `cfg(c-abi = "…")` 或 `cfg(kernel-abi = "…")`(谓词语法见 [22 —— 对已解析目标侧的适配](22-target-side.md#对已解析目标侧的适配))。 -**平台单元。** 自己需要平台原生环境的包,从不读 `__openkal__` 或别的宏去推断这件事——边界 +**平台单元。** 自己需要平台原生环境的包,从不读 `__OPENKAL__` 或别的宏去推断这件事——边界 是清单里写明的,不是从源码推断出来的,跨越它的一切仍然只能是定宽类型(SPEC §5.4)。两类 不同的包,通过两条不同的路径拿到 `[package] c-environment = "platform"`(docs/22): diff --git a/mcpp.toml b/mcpp.toml index a842076b..1fa98a77 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.9.21.1" +version = "2026.9.21.2" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/modules/toolchain-model/src/model.cppm b/modules/toolchain-model/src/model.cppm index 252247b3..80ff7a9c 100644 --- a/modules/toolchain-model/src/model.cppm +++ b/modules/toolchain-model/src/model.cppm @@ -184,7 +184,7 @@ struct Toolchain { std::vector cEnvExpectDefined; std::vector cEnvExpectUndefined; // The resolved `kernel-abi` is `openkal` — the engine defines - // `__openkal__` for every target-side unit (design §2.1, §3.4). Read from + // `__OPENKAL__` for every target-side unit (design §2.1, §3.4). Read from // the LAYER's value, never from a package name, so a second `openkal` // implementation needs no engine change. bool kernelAbiIsOpenkal = false; diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index f1afeecb..aea504b0 100644 --- a/modules/versioning/src/version.cppm +++ b/modules/versioning/src/version.cppm @@ -31,6 +31,6 @@ import std; export namespace mcpp { -inline constexpr std::string_view MCPP_VERSION = "2026.9.21.1"; +inline constexpr std::string_view MCPP_VERSION = "2026.9.21.2"; } // namespace mcpp diff --git a/src/build/cache_key.cppm b/src/build/cache_key.cppm index 5fcaadde..2374869f 100644 --- a/src/build/cache_key.cppm +++ b/src/build/cache_key.cppm @@ -76,7 +76,7 @@ export namespace mcpp::build::cache_key { // 3 (design 2026-09-18, coordinator review): `fill_package_config` used to // read only a package's OWN declared cflags/cxxflags, never the engine // broadcast channel (`privateBuild`) the realised [c-abi] environment and -// `-D__openkal__` actually travel through — so an entry written before this +// `-D__OPENKAL__` actually travel through — so an entry written before this // fix can be WRONG in a way probe_cached's normal "did the inputs change" // check cannot see: the compiled object and the recorded key silently // disagreed about what was compiled from the moment they were written, not @@ -533,7 +533,7 @@ void fill_package_config(PackageAxes& out, // are what its compile command line actually carries, because several // engine broadcasts append to `privateBuild` and never touch `bc` — // `targetSideUsage` (the resolved target side's own include dirs/flags), - // `-D__openkal__`, and the realised [c-abi] environment (design + // `-D__OPENKAL__`, and the realised [c-abi] environment (design // 2026-09-18) chief among them. A cache key built from `bc` alone cannot // tell two builds of the SAME package apart when only one of them // realised, say, LP64 instead of LLP64 — found exactly that way diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index 285557da..d99f9fc4 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -10778,7 +10778,7 @@ prepare_build(bool print_fingerprint, resolvedTargetSide = tsd::resolve(in); targetSideResolved = true; - // `__openkal__` — design 2026-09-18 §2.1, §3.4. Read from the + // `__OPENKAL__` — design 2026-09-18 §2.1, §3.4. Read from the // resolved LAYER's interface name, never from a package name, so a // second implementation of `mcpp:kernel-abi=openkal` needs no engine // change. Applies to every target-side unit unconditionally — even @@ -11111,7 +11111,7 @@ prepare_build(bool print_fingerprint, } } - // `__openkal__` AND THE REALISED [c-abi] ENVIRONMENT — design + // `__OPENKAL__` AND THE REALISED [c-abi] ENVIRONMENT — design // 2026-09-18 §2.1, §3.2-§3.4. Broadcast into every package's OWN // `privateBuild`, the same channel `targetSideUsage` just used above: // it reaches that package's C/C++ compiles AND its dependency scan @@ -11120,7 +11120,7 @@ prepare_build(bool print_fingerprint, // itself and the driver's own defaults still come last. // // `c-environment = "platform"` (§3.4) opts a package OUT of the - // [c-abi] REALISATION ONLY — `__openkal__` still reaches it, because + // [c-abi] REALISATION ONLY — `__OPENKAL__` still reaches it, because // the exception is about the C environment a package's headers see, // not about whether its own code may call `kal_*`. The base command // line these tokens are appended to is untouched either way, which is @@ -11137,7 +11137,7 @@ prepare_build(bool print_fingerprint, // only their -D/-U/-I words, on purpose — a -std= or -O token meant // for the C compiler has no meaning for GAS), so the object-format // and wchar-width tokens have to be named again here, into the - // channel `unit_asm_flags` passes through UNFILTERED. `__openkal__` + // channel `unit_asm_flags` passes through UNFILTERED. `__OPENKAL__` // needs no second copy: it is a -D, and the -D/-U/-I filter already // carries it from `cflags` into every assembly unit. // @@ -11172,7 +11172,7 @@ prepare_build(bool print_fingerprint, if (tc && (tc->kernelAbiIsOpenkal || !tc->cEnvTokens.empty() || !tc->cEnvBuiltinsTokens.empty())) { for (auto& p : packages) { - // `__openkal__` is emitted above, with the rest of the + // `__OPENKAL__` is emitted above, with the rest of the // engine's own defines; it is NOT subject to the // `c-environment = "platform"` exception below, because that // exception is about which C environment a package's headers @@ -11237,15 +11237,26 @@ prepare_build(bool print_fingerprint, providerId = pkg.manifest.package.name; break; } + // A REQUIREMENT NOBODY ANSWERED IS SAID SO, because otherwise + // "yes" and "never asked" are the same reading. + // + // Three situations exist and two of them build: the provider + // states a list and it contains the requirement (build); it + // states a list and does not (refuse, below); it states nothing + // at all (build, and until this note, in silence). The third is + // deliberate --- `provides-interfaces` is younger than the + // implementations that exist, and a graph that has not adopted it + // must keep building --- but a consumer reading a green build + // cannot tell it from the first. One line closes that, and it + // costs nothing to a graph where the provider does declare. + std::size_t uncheckedRequirements = 0; for (auto& pkg : packages) { const auto& need = pkg.manifest.kernelAbiRequiresInterfaces; if (need.empty()) continue; - // A consumer that names interfaces while no package in the - // graph states what it provides is not refused: the provider - // predates this key, and a graph that has not yet adopted it - // must keep building. The link still reports the absence, in - // the vocabulary it always did. - if (providerId.empty()) continue; + if (providerId.empty()) { + uncheckedRequirements += need.size(); + continue; + } auto missing = mcpp::targetside::interfaces_not_provided( need, providedInterfaces); if (missing.empty()) continue; @@ -11288,6 +11299,20 @@ prepare_build(bool print_fingerprint, providedInterfaces.size() == 1 ? "" : "s", pkg.manifest.package.name)); } + + if (uncheckedRequirements > 0) { + // THE IMPLEMENTATION IS NAMED FROM THE RESOLVED LAYER, not + // from whichever package happened to be first: the note has + // to say WHOSE silence this is, or a reader cannot act on it. + const auto& impl = resolvedTargetSide.kernelAbi.impl; + mcpp::ui::info("note", std::format( + "kernel-abi interfaces: {} states none, {} requirement{} " + "unchecked", + impl.empty() ? std::string("the resolved implementation") + : impl, + uncheckedRequirements, + uncheckedRequirements == 1 ? "" : "s")); + } } if (auto why = tsd::check_layering(resolvedTargetSide)) { @@ -12489,14 +12514,14 @@ prepare_build(bool print_fingerprint, for (auto& f : mcpp::toolchain::graph_runtime_compile_flags(*tc)) flags += " " + f; } - // `__openkal__` AND THE REALISED [c-abi] ENVIRONMENT REACH THE STD + // `__OPENKAL__` AND THE REALISED [c-abi] ENVIRONMENT REACH THE STD // MODULE TOO (design §3.4: "环境作用于目标侧的全部编译单元... 以及图中 // 所有普通包"). The std module's own command is assembled here rather // than through `mcpp.toolchain.hostflags`'s shared string (see the // comment above), so it needs the same broadcast the ordinary // per-package loop gives every other unit — this is that same rule, // stated once more at the one site it cannot reach on its own. - if (tc->kernelAbiIsOpenkal) flags += " -D__openkal__"; + if (tc->kernelAbiIsOpenkal) flags += " -D__OPENKAL__"; if (pkg.manifest.cEnvironment != "platform") { for (auto& t : tc->cEnvTokens) flags += " " + t; for (auto& t : tc->cEnvBuiltinsTokens) flags += " " + t; @@ -12749,7 +12774,7 @@ prepare_build(bool print_fingerprint, fpi.cppStandard = m->package.standard; fpi.compileFlags = canonical_compile_flags(*m) + canonical_package_build_metadata(packages); - // [c-abi] REALISATION AND `__openkal__` PARTICIPATE IN THE FINGERPRINT + // [c-abi] REALISATION AND `__OPENKAL__` PARTICIPATE IN THE FINGERPRINT // (design 2026-09-18 §3.4, gap #4 of the design's own self-review). Two // builds whose C library declares `data-model = "lp64"` and `"llp64"` // compile the SAME source, against the SAME manifest, into objects whose diff --git a/src/toolchain/cenv.cppm b/src/toolchain/cenv.cppm index 3872e752..7a279389 100644 --- a/src/toolchain/cenv.cppm +++ b/src/toolchain/cenv.cppm @@ -28,9 +28,11 @@ // freestanding posix / arch-default the same one token, `-D__unix__`, for the // same reason: nothing here defines it either // Windows posix / arch-default Cygwin-flavoured: `--target=x86_64-pc-cygwin`, -// plus `-D__mcpp_target_windows__`; -// `__CYGWIN__` stays defined for now (see -// the note below the table — a sequence) +// plus `-U__CYGWIN__`/`-U__CYGWIN32__` +// (see the note below the table — the last +// step of a three-repository sequence); +// `__MCPP_TARGET_WINDOWS__` is the name that +// replaces them, broadcast by `mcpp.build.prepare` // * builtins = iso turn off the platform-C-library idioms the // code generator assumes (§3.2.1) — Apple's // `memset_pattern16` is the one measured case @@ -70,37 +72,48 @@ // identity links exactly like one compiled under the MinGW one. Only the // preprocessor saw a different environment; the linker never has to know. // -// `__CYGWIN__`/`__CYGWIN32__` ARE STILL DEFINED, AND `__mcpp_target_windows__` -// IS DEFINED BESIDE THEM. This module has held both answers about the -// borrowed name, and the sequence between them is the point. +// `__CYGWIN__`/`__CYGWIN32__` ARE WITHDRAWN, AND `__MCPP_TARGET_WINDOWS__` +// STANDS IN THEIR PLACE. This module has held both answers about the borrowed +// name, and the sequence between them is the point. // // A middle revision kept `__CYGWIN__` defined so that code needing to know // the object format would have a name, and wrote its own condition for -// reversal: a trade-off "for the 30-member measurement to settle — if +// reversal: a trade-off "for the 30-member measurement to settle --- if // defining it produces more new failures than it fixes, the answer flips". // // The measurement settled the THIRD-PARTY half (2026-09-20, 60 member-target // combinations): keeping it costs four members, each stopping at // `#include ` reached through `#if defined(_WIN32) || -// defined(__CYGWIN__)`. Upstream means "Win32 is available" by the name — +// defined(__CYGWIN__)`. Upstream means "Win32 is available" by the name --- // mimalloc says so in the guard's own comment, sqlite3 lists it under // `SQLITE_OS_WIN`. A BORROWED NAME MEANS WHAT THE LENDER'S HISTORY MADE IT // MEAN, not what the borrower intended by it. // -// WITHDRAWING IT WAS TRIED IN THIS BRANCH AND BROKE THIS ECOSYSTEM'S OWN +// WITHDRAWING IT WAS TRIED TOO EARLY ONCE, AND BROKE THIS ECOSYSTEM'S OWN // INSTALLED HEADERS. `openkal-musl`'s `bits/setjmp.h` and // `openkal-llvm-runtime`'s `__libunwind_config.h` both read `__CYGWIN__` to // size a Win64 register save area, deliberately: they are INSTALLED, an // application's own compile reads them, and a package-private define cannot // reach that compile. libunwind's `static_assert` failed loudly under -// cross-verification; setjmp.h's equivalent would not have — its own comment -// says "a mismatch nothing reports until the record overruns". +// cross-verification; setjmp.h's equivalent would not have --- its own +// comment says "a mismatch nothing reports until the record overruns". // // The measurement had counted third-party readers of the macro and not ours. -// So the withdrawal is a sequence: this release adds `__mcpp_target_windows__` -// (mcpp's own name, meaning what mcpp says it means), the two packages move -// onto it while still accepting `__CYGWIN__`, and only then does a release -// stop defining the borrowed one. Each intermediate state builds. +// So the withdrawal became a sequence, and it ran to completion across three +// repositories: +// +// 1. 2026.9.21.1 defined mcpp's own name beside the borrowed one, and +// 2026.9.21.2 re-spelt it `__MCPP_TARGET_WINDOWS__` while it still had +// no consumer. Purely additive at every point. +// 2. `openkal-musl@0.19.0` and `openkal-llvm-runtime@0.14.0` read the new +// name and keep `|| defined(__CYGWIN__)`, so they build on an engine +// from either side of this change. Published BEFORE step three. +// 3. this release stops defining the borrowed name. +// +// Each intermediate state builds, and the ordering between repositories --- +// packages first, engine last --- is the safety argument; see the withdrawal +// note at the emission site for the residual window it does not close. + export module mcpp.toolchain.cenv; import std; @@ -305,35 +318,59 @@ inline std::expected realise( // what says it anyway is NOT emitted here. `_WIN32`/`_WIN64` are // suppressed on purpose --- that is what presenting POSIX means // --- while the ABI is unchanged, and source that sizes a Win64 - // record still has to know. `__mcpp_target___` answers that, + // record still has to know. `__MCPP_TARGET___` answers that, // for every target and not only this one, and it is broadcast in // `mcpp.build.prepare` rather than realised here: it is a fact // about the TARGET, true whether or not any `[c-abi]` block // exists, so deriving it from a declaration would make its // absence ambiguous. See docs/21, "The macros mcpp defines". // - // `__CYGWIN__` IS STILL DEFINED, AND THAT IS A SEQUENCE, NOT A - // DECISION TO KEEP IT. The 30-member measurement settled that the - // borrowed name costs four members (archive, sqlite3, mimalloc, - // c-ares stop at `#include ` through - // `#if defined(_WIN32) || defined(__CYGWIN__)`; upstream means - // "Win32 is available" by it, as mimalloc's own guard comment - // says). Withdrawing it is right and is step three of three: + // `__CYGWIN__` IS WITHDRAWN HERE, AND THAT IS THE LAST STEP OF + // A SEQUENCE RATHER THAN A DECISION TAKEN ON ITS OWN. The + // 30-member measurement settled that the borrowed name costs four + // members (archive, sqlite3, mimalloc, c-ares stop at + // `#include ` through `#if defined(_WIN32) || + // defined(__CYGWIN__)`; upstream means "Win32 is available" by + // it, as mimalloc's own guard comment says). The three steps, in + // the order they were taken: + // + // 1. 2026.9.21.1 defined mcpp's own name beside the borrowed + // one; 2026.9.21.2 re-spelt it `__MCPP_TARGET_WINDOWS__`. + // Purely additive: every published package kept working. + // 2. `openkal-musl@0.19.0` and `openkal-llvm-runtime@0.14.0` + // read the new name, keeping `|| defined(__CYGWIN__)` so + // they build on either engine. PUBLISHED BEFORE THIS STEP. + // 3. this release stops defining it. + // + // STEP TWO SHIPPING FIRST IS THE WHOLE SAFETY ARGUMENT, and it is + // an ordering between REPOSITORIES that no test in this one can + // check. Taking step three first --- which an earlier revision of + // this branch did --- leaves every published copy of those two + // installed headers falling to its `#else`: the wrong record + // size. libunwind's `static_assert` catches its half loudly; + // `setjmp.h` says in its own comment that nothing reports the + // other until the record overruns. // - // 1. this release: define `__mcpp_target_windows__` as well. - // Purely additive; every published package keeps working. - // 2. the two packages above read the new name, keeping - // `|| defined(__CYGWIN__)` so they build on both engines. - // 3. a later release stops defining `__CYGWIN__`. + // THE RESIDUAL WINDOW IS NAMED RATHER THAN CLAIMED AWAY. A + // project pinning `openkal-musl` at 0.18.0 or earlier EXACTLY, + // and upgrading the engine past this release, gets that silent + // `#else`. Moving the index's `latest` onto 0.19.0 before this + // release is what keeps the window to exact pins; no mechanism + // available here closes it, because the engine cannot know which + // macros a package's installed headers read. // - // Step three taken first would leave every PUBLISHED copy of - // those headers falling to its `#else` --- the wrong record size, - // reported by nothing. A loud failure in four third-party members - // is the better state to hold for one release. + // `-U` RATHER THAN A DIFFERENT TRIPLE. The Cygwin identity is + // what supplies `__unix__` and suppresses `_WIN32`, which is the + // whole point of `presents = "posix"`; only the borrowed NAME is + // unwanted. Undefining it on the compile line keeps every other + // property of the substitution intact. r.tokens.push_back("--target=x86_64-pc-cygwin"); + r.tokens.push_back("-U__CYGWIN__"); + r.tokens.push_back("-U__CYGWIN32__"); r.expectDefined.push_back("__unix__"); - r.expectDefined.push_back("__CYGWIN__"); r.expectUndefined.push_back("_WIN32"); + r.expectUndefined.push_back("__CYGWIN__"); + r.expectUndefined.push_back("__CYGWIN32__"); cygwinIdentity = true; } else if (decl.presents == CAbiPresents::Windows) { // Already the base triple's own identity — nothing to add. diff --git a/src/toolchain/predefines.cppm b/src/toolchain/predefines.cppm index e44e1db3..76966c3c 100644 --- a/src/toolchain/predefines.cppm +++ b/src/toolchain/predefines.cppm @@ -30,18 +30,25 @@ // // ── NAMING ───────────────────────────────────────────────────────────────── // -// `__mcpp_`-prefixed, `__`-suffixed, lowercase, words separated by `_`. -// -// Lowercase rather than upper, and the reason is consistency rather than -// taste. Two conventions exist in the wild: VENDOR AND PRODUCT names are -// upper (`__APPLE__`, `_WIN32`, `__MINGW32__`), KIND-OF-SYSTEM names are -// lower (`__linux__`, `__unix__`, `__gnu_linux__`). These name kinds of -// target, they sit beside that second family in real guards --- `#if -// defined(__linux__) || defined(__mcpp_target_windows__)` --- and this -// project already published `__openkal__` in that spelling. One rule, stated -// and checked, is worth more than either convention argued in isolation. -// -// THE `__mcpp_` PREFIX IS LOAD-BEARING. A name mcpp owns means what mcpp says +// UPPER CASE, `__`-wrapped, words separated by `_`; the names mcpp owns carry +// `__MCPP_`. +// +// The convention in the wild 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 table 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 first, and the reasoning that produced it +// is worth keeping because the mistake is easy to repeat: these names sit +// beside `__linux__` in real guards, so matching it looked like consistency. +// THAT CONFUSES ADJACENCY WITH KIND. `__APPLE__` sits in those same guards +// and is upper, because it belongs to somebody; so does every other name in +// them that belongs to somebody. Adjacency is not a naming rule. +// +// THE `__MCPP_` PREFIX IS LOAD-BEARING. A name mcpp owns means what mcpp says // it means. The alternative was tried: `__CYGWIN__` was left defined so that // code needing "PE object format, POSIX C environment" would have a name, and // a 30-member measurement found four members reading it as "Win32 is @@ -52,12 +59,33 @@ // // ── STABILITY ────────────────────────────────────────────────────────────── // -// An entry here is a published interface. Removing one, or narrowing when it -// is defined, is a breaking change for source this project does not control, -// and the failure is usually SILENT --- a `#if` selects the other branch and -// compiles. Withdrawal is therefore a sequence, never an edit: add the -// replacement, let consumers move onto it while still accepting the old name, -// and only then stop defining it. +// An entry here is a published interface, and withdrawing one is SILENT: a +// `#if` selects the other branch and compiles, on a machine nobody is +// watching. No mechanism available to a build tool makes that loud --- a +// preprocessor cannot be told to complain about a name it does not find. +// +// So a withdrawal is never decided by reading. It is decided by A MEASUREMENT +// THAT ENUMERATES READERS, and what the measurement finds sets how many steps +// the withdrawal takes. Both withdrawals this project has performed are on +// the record, and they came out differently: +// +// `__CYGWIN__` (2026.9.21.1). The enumeration found four third-party +// members reading it, and --- the half a first, sloppier count missed --- +// six sites in the two headers this ecosystem INSTALLS. Readers exist, so +// the withdrawal was a sequence: publish the replacement, move the +// consumers onto it while the old name is still defined, stop last. +// +// `__mcpp_target___` and `__openkal__` (2026.9.21.2). The enumeration +// found NONE. Across every repository of this ecosystem, not one source +// file and not one manifest read either name; the only occurrences were +// this engine's own emitter, its tests, and prose. Exposure was three days +// for `__openkal__` and a single release for the target macro, and both are +// names invented here, so no upstream code can be holding one. Zero readers +// collapses the sequence to one step. +// +// The rule did not change between those two; the count did. A withdrawal +// argued from anything other than a count is an argument this project has +// already got wrong once. export module mcpp.toolchain.predefines; import std; @@ -73,7 +101,7 @@ enum class When { }; struct Entry { - std::string_view spelling; // `` stands for the triple's `os` field + std::string_view spelling; // `` stands for the triple's `os` field When when; bool owned; // false: a standard name mcpp SUPPLIES, not owns std::string_view allowed; @@ -85,7 +113,7 @@ struct Entry { // wait for forever. `test_predefines.cpp` compares the two directions. inline constexpr std::array kContract {{ { - "__mcpp_target___", When::TargetOs, /*owned=*/true, + "__MCPP_TARGET___", When::TargetOs, /*owned=*/true, "learning the target's operating system when the C environment " "presented above it has suppressed the platform's own macros, and " "sizing a record by the target's ABI", @@ -93,7 +121,7 @@ inline constexpr std::array kContract {{ "for `cfg(os = ...)` in a package this project controls", }, { - "__openkal__", When::ResolvedLayer, /*owned=*/true, + "__OPENKAL__", When::ResolvedLayer, /*owned=*/true, "gating whether a call site invokes `kal_*` at all; its meaning is " "identical on every target", "selecting a header, inferring whether `_WIN32` is real, working " @@ -109,6 +137,19 @@ inline constexpr std::array kContract {{ }, }}; +// ASCII upper case. The triple's `os` field is a closed vocabulary of +// lower-case identifier tokens --- `linux`, `windows`, `macos`, `ios`, +// `emscripten`, `none` --- every one of them assigned from a literal in the +// parser, so this always yields a valid identifier. That is a property of +// another module, so `test_predefines.cpp` asserts it over the TARGET +// REGISTRY rather than over this sentence. +inline std::string upper(std::string_view s) { + std::string out(s); + for (char& c : out) + if (c >= 'a' && c <= 'z') c = static_cast(c - 'a' + 'A'); + return out; +} + // THE TOKENS FOR ONE BUILD, for the entries this module emits. // // `When::Realisation` entries are absent by construction: they belong to @@ -124,9 +165,9 @@ inline std::vector define_tokens(std::string_view targetOs, bool kernelAbiIsOpenkal) { std::vector out; if (!targetOs.empty()) - out.push_back(std::format("-D__mcpp_target_{}__=1", targetOs)); + out.push_back(std::format("-D__MCPP_TARGET_{}__=1", upper(targetOs))); if (kernelAbiIsOpenkal) - out.push_back("-D__openkal__"); + out.push_back("-D__OPENKAL__"); return out; } @@ -134,8 +175,8 @@ inline std::vector define_tokens(std::string_view targetOs, // can compare the table against `define_tokens` without knowing either. inline std::string spelling_for(const Entry& e, std::string_view targetOs) { std::string s(e.spelling); - if (const auto at = s.find(""); at != std::string::npos) - s.replace(at, 4, targetOs); + if (const auto at = s.find(""); at != std::string::npos) + s.replace(at, 4, upper(targetOs)); return s; } diff --git a/tests/e2e/741_c_abi_environment_declared_by_the_c_library.sh b/tests/e2e/741_c_abi_environment_declared_by_the_c_library.sh index 94e6997d..e2abb14a 100755 --- a/tests/e2e/741_c_abi_environment_declared_by_the_c_library.sh +++ b/tests/e2e/741_c_abi_environment_declared_by_the_c_library.sh @@ -167,7 +167,7 @@ def joined(argv_iter): # made it mean. The object-format question keeps no macro at all. consumer = joined(args_for("main.cpp")) missing = [tok for tok in ("--target=x86_64-pc-cygwin", "-fno-short-wchar", - "-D__mcpp_target_windows__=1") + "-D__MCPP_TARGET_WINDOWS__=1") if tok not in consumer] # The last of those comes from `mcpp.toolchain.predefines`, not from the # `[c-abi]` realisation -- a fact about the TARGET rather than about the @@ -206,7 +206,7 @@ if not asm_args: sys.exit(1) asm_joined = joined(iter(asm_args)) missing = [tok for tok in ("--target=x86_64-pc-cygwin", "-fno-short-wchar", - "-D__mcpp_target_windows__=1") + "-D__MCPP_TARGET_WINDOWS__=1") if tok not in asm_joined] if missing: print(f"FAIL: the assembly unit is missing realised tokens {missing} " diff --git a/tests/e2e/743_kernel_abi_interfaces_are_resolved_not_preprocessed.sh b/tests/e2e/743_kernel_abi_interfaces_are_resolved_not_preprocessed.sh index d0020318..d9a9c3fb 100755 --- a/tests/e2e/743_kernel_abi_interfaces_are_resolved_not_preprocessed.sh +++ b/tests/e2e/743_kernel_abi_interfaces_are_resolved_not_preprocessed.sh @@ -15,13 +15,17 @@ # that is why every macro-shaped answer to it has had to be replaced by the # next one. # -# Three legs: +# Four legs: # A a requirement the implementation provides: builds. # B a requirement it does not: refused, naming the interface and both # packages, with nothing compiled. -# C a graph whose implementation states nothing: builds. The key postdates -# the provider, and a graph that has not adopted it must keep building -- -# the link still reports the absence in the vocabulary it always did. +# C a graph whose implementation states nothing: builds, AND SAYS SO. The +# key postdates the provider, and a graph that has not adopted it must +# keep building -- but a green build in which nobody checked reads +# exactly like one in which everybody agreed, so the note names it. +# D the mirror of C: when the provider does state a list, that note is +# ABSENT. Without this leg C would pass against an engine that printed +# the note unconditionally, which measures nothing. set -e MCPP="${MCPP:-mcpp}" @@ -188,6 +192,50 @@ out=$("$MCPP" build 2>&1) || { echo "$out" >&2 exit 1 } -echo "OK: C (a provider that states nothing is not refused)" +# AND THE SILENCE IS NAMED. Three situations exist and two build: the +# provider lists the interface (A), it lists others (B, refused), it lists +# nothing (here). Without this line the first and the third produce the same +# output, so a reader cannot tell "checked and agreed" from "never asked". +echo "$out" | grep -q "kernel-abi interfaces" || { + echo "FAIL: a build in which no provider stated a list must say so --" \ + "otherwise an unanswered requirement reads as a confirmed one" >&2 + echo "$out" >&2 + exit 1 +} +echo "$out" | grep -q "fakekernel" || { + echo "FAIL: the note must name WHOSE silence this is; a reader told only" \ + "that something went unchecked cannot act on it" >&2 + echo "$out" | grep -m1 "kernel-abi interfaces" >&2 + exit 1 +} +# THE COUNT, BECAUSE IT IS THE ONE PART THE FIXTURE DETERMINES. The root +# declares two requirements; a note that said "1" or "0" would still match +# every assertion above while reporting something other than what was asked. +echo "$out" | grep -q "2 requirements unchecked" || { + echo "FAIL: the note must report how many requirements went unchecked;" \ + "this graph asked for two" >&2 + echo "$out" | grep -m1 "kernel-abi interfaces" >&2 + exit 1 +} +echo "OK: C (a provider that states nothing is not refused, and is named)" + +# ── D. the mirror: a provider that states a list produces no such note ──── +rm -rf target +write_impl '"openkal.abort", "openkal.stream", "openkal.memory", "openkal.fs"' +write_root '"openkal.fs", "openkal.stream"' +out=$("$MCPP" build 2>&1) || { + echo "FAIL: leg D's graph is leg A's and must build" >&2 + echo "$out" >&2 + exit 1 +} +echo "$out" | grep -q "kernel-abi interfaces" && { + echo "FAIL: the note belongs to the case where nothing was checked. A" \ + "graph whose provider DID state its list was checked, and printing" \ + "it here would make leg C pass against an engine that always" \ + "printed it" >&2 + echo "$out" | grep -m1 "kernel-abi interfaces" >&2 + exit 1 +} +echo "OK: D (a provider that states its list draws no note)" echo "OK" diff --git a/tests/unit/test_cache_key.cpp b/tests/unit/test_cache_key.cpp index d2740777..d6529059 100644 --- a/tests/unit/test_cache_key.cpp +++ b/tests/unit/test_cache_key.cpp @@ -389,7 +389,7 @@ TEST(CacheKey, AHostedTargetHasNoTargetImpliedFlagsEitherWay) { // `manifest.buildConfig.cflags`/`cxxflags` at all — it is an ENGINE // BROADCAST, appended into `PackageRoot::privateBuild.cflags`/`cxxflags`/ // `asmflags` by `prepare.cppm` once the target side resolves (the same -// channel `targetSideUsage` and `-D__openkal__` use). `fill_package_config` +// channel `targetSideUsage` and `-D__OPENKAL__` use). `fill_package_config` // used to read only `manifest.buildConfig.cflags`/`cxxflags` — the // package's OWN declaration — so two builds of the identical package, // realising two DIFFERENT C environments (LP64 vs LLP64, say), produced the @@ -430,7 +430,7 @@ TEST(CacheKey, TwoDifferentRealisedCEnvironmentsDoNotShareASlot) { // A DURABLE GUARD FOR THE WHOLE CLASS THE PREVIOUS TEST FOUND ONE INSTANCE // OF — this predates the c-abi wave (`targetSideUsage`'s own broadcast, and -// `-D__openkal__`, had the identical exposure before [c-abi] existed) and +// `-D__OPENKAL__`, had the identical exposure before [c-abi] existed) and // will recur: `PackageRoot::privateBuild` (`mcpp.modgraph.scanner // ::UsageRequirements`) is the engine's OWN channel for "this reaches a // package's compile command line even though the package wrote nothing" — diff --git a/tests/unit/test_cenv.cpp b/tests/unit/test_cenv.cpp index 98a0ae4d..d03e6069 100644 --- a/tests/unit/test_cenv.cpp +++ b/tests/unit/test_cenv.cpp @@ -68,32 +68,43 @@ TEST(CEnv, MacosPosixArchDefaultDefinesUnix) { // Suppressing `_WIN32` is the point of presenting POSIX, but the ABI did not // change with the environment --- the register save areas are still Win64's, // and two INSTALLED headers in this ecosystem size records by that fact. -// `__mcpp_target___` answers it, for every target rather than only this +// `__MCPP_TARGET___` answers it, for every target rather than only this // one, and it belongs to `mcpp.toolchain.predefines`: a fact about the // TARGET is true whether or not any `[c-abi]` block exists, so realising it // from a declaration would make its absence ambiguous. This test therefore // asserts that the realisation does NOT carry it --- the separation is the // property, and a token appearing in both places would mean two owners. // -// `__CYGWIN__` IS STILL DEFINED, AND THAT IS A SEQUENCE. Withdrawing it is -// right --- the 30-member measurement settled that it costs four members, -// which read it as "Win32 is available" and reach `#include ` --- -// and withdrawing it FIRST was tried in this branch and broke the two headers -// above, because they read it too and nothing else names the target for them. -// libunwind's `static_assert` failed loudly; setjmp.h's equivalent would not -// have. So: add the name here, move those packages onto it, and only then -// stop defining the borrowed one. This test pins the first step. +// `__CYGWIN__` IS WITHDRAWN, AND THAT WAS THE LAST STEP OF A SEQUENCE. The +// 30-member measurement settled that the borrowed name costs four members, +// which read it as "Win32 is available" and reach `#include `. +// Withdrawing it FIRST was tried in this branch and broke the two headers +// above, because they read it too and nothing else named the target for them; +// libunwind's `static_assert` failed loudly, setjmp.h's equivalent would not +// have. So the order was: name it here, publish packages that read the new +// name, and only then stop defining the borrowed one. +// +// WHAT THIS TEST CAN AND CANNOT PIN. It pins the engine's half --- the +// tokens, and that the probe will verify them. It cannot pin the ORDERING, +// which holds between repositories: that `openkal-musl@0.19.0` and +// `openkal-llvm-runtime@0.14.0` were published BEFORE this release. That was +// measured instead, by building an openkal program for `x86_64-windows-gnu` +// against the PUBLISHED 0.13.0 on this engine, which fails on libunwind's +// two `static_assert`s exactly as predicted. An ordering between +// repositories has no assertion in either of them. TEST(CEnv, WindowsPosixArchDefaultSubstitutesTheCygwinTriple) { auto d = decl(ts::CAbiPresents::Posix, ts::CAbiDataModel::ArchDefault, 32); auto r = cenv::realise(d, "windows", "x86_64", false); ASSERT_TRUE(r.has_value()) << r.error(); EXPECT_TRUE(has(r->tokens, "--target=x86_64-pc-cygwin")); // The target's identity is not this module's to state: see above. - EXPECT_FALSE(has(r->tokens, "-D__mcpp_target_windows__=1")); - // `__CYGWIN__` is not withdrawn yet, and that is asserted, because the - // step that withdraws it must be a deliberate edit to this line rather - // than a silent drift. - EXPECT_FALSE(has(r->tokens, "-U__CYGWIN__")); + EXPECT_FALSE(has(r->tokens, "-D__MCPP_TARGET_WINDOWS__=1")); + // The borrowed name is undefined on the compile line. `-U` rather than a + // different triple: the Cygwin identity is what supplies `__unix__` and + // suppresses `_WIN32`, which is the point of `presents = "posix"`; only + // the NAME was unwanted. + EXPECT_TRUE(has(r->tokens, "-U__CYGWIN__")); + EXPECT_TRUE(has(r->tokens, "-U__CYGWIN32__")); // wchar 32 differs from Cygwin's own default (16) — the flag is added. EXPECT_TRUE(has(r->tokens, "-fno-short-wchar")); EXPECT_EQ(r->expectLongBytes, 8); @@ -103,9 +114,12 @@ TEST(CEnv, WindowsPosixArchDefaultSubstitutesTheCygwinTriple) { // THE PROBE CHECKS THESE, which is what keeps the tokens from being ones // the compiler could ignore: `cenv_probe::verify` compares the realised // configuration's predefines against these lists and refuses on a - // mismatch. A `-D` that did not take effect is a verification failure, - // not a silent one. - ASSERT_TRUE(has(r->expectDefined, "__CYGWIN__")); + // mismatch. A `-U` that did not take effect is a verification failure, + // not a silent one --- and silence is the whole risk here, since a `#if` + // that finds the name still defined simply compiles the other branch. + ASSERT_TRUE(has(r->expectUndefined, "__CYGWIN__")); + ASSERT_TRUE(has(r->expectUndefined, "__CYGWIN32__")); + EXPECT_FALSE(has(r->expectDefined, "__CYGWIN__")); } // wchar = 16 on the Cygwin substitution matches Cygwin's own default, so no diff --git a/tests/unit/test_predefines.cpp b/tests/unit/test_predefines.cpp index e07bfa75..32d7c7dc 100644 --- a/tests/unit/test_predefines.cpp +++ b/tests/unit/test_predefines.cpp @@ -12,12 +12,16 @@ #include #include +#include +#include #include #include import mcpp.toolchain.predefines; +import mcpp.toolchain.triple; namespace pd = mcpp::toolchain::predefines; +namespace triple = mcpp::toolchain::triple; namespace { @@ -75,9 +79,9 @@ TEST(Predefines, TheTargetMacroIsSpeltFromTheTripleField) { // The engine learns no operating-system name: a target added to the // triple parser gets its macro without an edit to this module. EXPECT_EQ(pd::define_tokens("freebsd", false).front(), - "-D__mcpp_target_freebsd__=1"); + "-D__MCPP_TARGET_FREEBSD__=1"); EXPECT_EQ(pd::define_tokens("none", false).front(), - "-D__mcpp_target_none__=1"); + "-D__MCPP_TARGET_NONE__=1"); } TEST(Predefines, OneMacroPerTargetAndItIsAlwaysPresent) { @@ -86,7 +90,7 @@ TEST(Predefines, OneMacroPerTargetAndItIsAlwaysPresent) { for (auto const& os : {"linux", "windows", "macos", "ios", "android", "none"}) { auto ms = emitted(os, false); const auto n = std::ranges::count_if(ms, [](std::string const& m) { - return m.starts_with("__mcpp_target_"); + return m.starts_with("__MCPP_TARGET_"); }); EXPECT_EQ(n, 1) << "os=" << os; } @@ -96,20 +100,20 @@ TEST(Predefines, OpenkalIsTiedToTheResolvedLayerAndNotToAnyTarget) { for (auto const& os : {"linux", "windows", "macos"}) { auto without = emitted(os, false); auto with = emitted(os, true); - EXPECT_EQ(std::ranges::find(without, "__openkal__"), without.end()); - EXPECT_NE(std::ranges::find(with, "__openkal__"), with.end()); + EXPECT_EQ(std::ranges::find(without, "__OPENKAL__"), without.end()); + EXPECT_NE(std::ranges::find(with, "__OPENKAL__"), with.end()); } } TEST(Predefines, AnEmptyTargetOsEmitsNoTargetMacro) { - // A triple that failed to parse must not produce `__mcpp_target___`. + // A triple that failed to parse must not produce `__MCPP_TARGET___`. auto ms = emitted("", false); EXPECT_TRUE(ms.empty()); } TEST(Predefines, EveryContractRowStatesBothWhatIsAllowedAndWhatIsNot) { // A rule with only a permission invites every use its author did not - // think of; the `__openkal__` entry has had a forbidden list since it was + // think of; the `__OPENKAL__` entry has had a forbidden list since it was // written, and the rest follow it. for (auto const& e : pd::kContract) { EXPECT_FALSE(e.allowed.empty()) << e.spelling; @@ -117,17 +121,61 @@ TEST(Predefines, EveryContractRowStatesBothWhatIsAllowedAndWhatIsNot) { } } -TEST(Predefines, EveryOwnedNameCarriesTheMcppPrefix) { - // A name mcpp owns means what mcpp says it means. A name it merely - // supplies -- `__unix__` -- is the standard one and must NOT be renamed - // into this project's namespace. +TEST(Predefines, EveryOwnedNameIsSpeltInThisProjectsConvention) { + // Upper case, `__`-wrapped. The convention splits by what a name IS: a + // vendor or product name is upper (`__APPLE__`, `_WIN32`), a + // kind-of-system name is lower (`__linux__`). Every owned row names a + // vendor or a product --- mcpp itself, or openkal --- so every one of + // them is upper. + // + // THE PREFIX IS NOT THE INVARIANT, and asserting it was would be wrong: + // `__OPENKAL__` is owned and names openkal, not mcpp. What every owned + // row shares is the spelling convention, which is what is checked here. + // A row mcpp merely SUPPLIES keeps the standard spelling, whatever it is, + // and must never be pulled into this project's namespace. for (auto const& e : pd::kContract) { - if (e.owned) - EXPECT_TRUE(e.spelling.starts_with("__mcpp_") || e.spelling == "__openkal__") - << e.spelling << " is owned and is spelt outside the namespace"; - else - EXPECT_FALSE(e.spelling.starts_with("__mcpp_")) + if (!e.owned) { + EXPECT_FALSE(e.spelling.starts_with("__MCPP_")) << e.spelling << " is supplied rather than owned, so it keeps " "the standard spelling"; + continue; + } + EXPECT_TRUE(e.spelling.starts_with("__") && e.spelling.ends_with("__")) + << e.spelling << " is owned and is not `__`-wrapped"; + for (char c : e.spelling) + EXPECT_FALSE(c >= 'a' && c <= 'z') + << e.spelling << " is owned and carries a lower-case letter"; } } + +TEST(Predefines, EveryTargetInTheRegistryYieldsAValidIdentifier) { + // The spelling is derived from the triple's own `os` field, which is why + // a target added to the parser needs no edit to `predefines.cppm`. The + // price of that is that an `os` carrying a character no identifier may + // hold --- a dot, a dash, a version suffix --- would produce a macro no + // compiler accepts, and the failure would land in the user's build rather + // than here. + // + // THE DENOMINATOR IS THE REGISTRY, not a list of names written beside + // this test. A list is a sample, and a sample cannot report the row + // somebody adds next year. + std::size_t rows = 0; + for (auto const& row : triple::known_targets()) { + const auto t = triple::parse(row.canonical); + ASSERT_TRUE(t.has_value()) << row.canonical << " does not parse"; + const auto toks = pd::define_tokens(t->os, false); + ASSERT_EQ(toks.size(), 1u) << row.canonical << " (os=" << t->os << ")"; + const std::string m = macro_of(toks.front()); + EXPECT_FALSE(m.empty()); + EXPECT_TRUE(std::isalpha(static_cast(m.front())) + || m.front() == '_') + << row.canonical << " -> " << m; + for (char c : m) + EXPECT_TRUE(std::isalnum(static_cast(c)) || c == '_') + << row.canonical << " -> " << m + << " is not a valid identifier"; + ++rows; + } + // A registry that enumerated nothing would pass every assertion above. + EXPECT_GT(rows, 10u) << "the target registry produced " << rows << " rows"; +}