From aa21320a6bf2675d2aebedf331a917ac061481e3 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 01:53:57 +0800 Subject: [PATCH 1/5] 2026.9.21.1: the borrowed __CYGWIN__ is withdrawn, by measurement `presents = "posix"` on Windows realises as a Cygwin-flavoured target. The previous release LEFT `__CYGWIN__`/`__CYGWIN32__` defined, so that portable third-party code needing to know the OBJECT FORMAT would keep a name for "PE format with a POSIX-presenting C environment", and it wrote its own condition for reversal: a trade-off for the 30-member measurement to settle, flipping if defining them produced more failures than it fixed. IT PRODUCED FOUR AND FIXED NONE. Across 60 member-target combinations, archive, sqlite3, mimalloc and c-ares each stopped at `#include `, reached through a guard of the shape `#if defined(_WIN32) || defined(__CYGWIN__)`. Nothing in the same run failed for want of the macro. Upstream says what it means by the name. mimalloc puts it in the guard's own comment --- `we use windows locks on cygwin, but otherwise treat it at unix` --- and sqlite3 lists it in the `SQLITE_OS_WIN` detection set before including `windows.h`. A BORROWED NAME MEANS WHAT THE LENDER'S HISTORY MADE IT MEAN, not what the borrower intended by it. The object-format question keeps no macro at all: a package asks `cfg(os = "windows")`, which needs none. If a third party is ever found that can only ask in the preprocessor, mcpp defines a name of its own. WHAT CARRIES THE CHANGE IS NOT THE TWO `-U` TOKENS. It is the two entries added to `expectUndefined`: `cenv_probe::verify` compares the realised configuration's predefines against those lists and refuses on a mismatch, so a `-U` that failed to take effect is a verification failure rather than a silent one. Measured directly with the pinned clang, in the order mcpp emits them: echo | clang -dM -E -x c - -U__CYGWIN__ -U__CYGWIN32__ \ --target=x86_64-pc-cygwin -U__CYGWIN__ -U__CYGWIN32__ -> __unix__ defined, __CYGWIN__ absent, _WIN32 absent The unit test and e2e 741 now assert the opposite side, and both record that this module has held both answers and what flipped it. The pair appears twice on a `.S` command line and once on `.c`/`.cpp`, because `cEnvTokens` reaches asmflags directly and `-D`/`-U`/`-I`-shaped tokens also arrive through the channel that carries defines into assembly; `-U X` twice is `-U X`, and the probe is the judge, so no count is asserted. Also in this release, as documents rather than engine changes: * `.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md` --- the nine items this wave did not implement, each with its criterion, its owner and what blocks it; why `lsp-mcpp-private` is the acceptance vehicle (its platform surface is six constants, none of them a POSIX facility, and it makes no direct POSIX call); and a cross-repository verification protocol: an engine PR is built by the ecosystem through `MCPP_SOURCE_REF` and both sides must be green BEFORE it merges. That protocol exists because this very change needed a second release --- a reading the thirty member graphs could have produced before the first one. --- ...kal-ecosystem-completion-and-acceptance.md | 470 ++++++++++++++++++ .agents/docs/README.md | 4 +- CHANGELOG.md | 39 +- docs/22-target-side.md | 41 +- docs/zh/22-target-side.md | 30 +- mcpp.toml | 2 +- modules/versioning/src/version.cppm | 2 +- src/toolchain/cenv.cppm | 81 +-- ...i_environment_declared_by_the_c_library.sh | 48 +- tests/unit/test_cenv.cpp | 40 +- 10 files changed, 669 insertions(+), 88 deletions(-) create mode 100644 .agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md 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 new file mode 100644 index 00000000..c53b92d4 --- /dev/null +++ b/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md @@ -0,0 +1,470 @@ +--- +subject: plan +status: active +--- + +# openkal 生态:完整性收尾与验收方案 + +- 日期:2026-09-21 +- 依据:`.agents/docs/2026-09-20-openkal-c-environment-ecosystem-design.md`(下称「设计」)。 + 本文不重述它的论证,只处理**它没有实现的部分**与**怎么验收**。 +- 验收载体:`lsp-mcpp-private`(`mcpp-language-server`) + +--- + +## 0. 本文回答三个问题 + +1. 2026.9.20.1 这一轮之后,生态还差什么。 +2. 这些差项的**判据**分别是什么,谁负责,卡在什么上。 +3. 怎么用一个**真实程序**验收整条链,而不是靠合成夹具。 + +第三点是本文与设计的主要差别。设计的验证是引擎侧的单测、e2e 与 30 成员测量;这些都 +只回答「引擎与包各自对不对」。**没有任何一处回答「一个真实的、非玩具的 C++23 程序, +能不能在三个目标上只写一份源码」**,而那才是 openkal 存在的理由。 + +--- + +## 1. 分母:本轮已关闭的 + +给未实现项一个对照,不展开。 + +| 项 | 形态 | +| --- | --- | +| P0 探针 | 探针量它要核对的目标,而不是构建宿主 | +| P1 | `presents` 冻结;`[c-abi]` 的词汇不再增长 | +| P2 | `refused` 与 `fails` 在测量里分开计 | +| P5 | `[kernel-abi] provides/requires-interfaces`,解析期集合差 | +| P7-L1/L2 | 声明由产物派生(三个实现均已落地);解析期比对 | +| `[c-abi-absent]` | C 库一级枚举例外而不枚举规则,24 行,CI 逐条断言 | +| 宿主面 | 五处 `fs::which` 穷举,两行补齐,扫描方式写进文档 | + +**测量基线(旧图,runtime 0.12.0 / mcpp 2026.9.18.3)**:60 个 `成员 × 目标` 组合, +50 通过(其中 47 个 `runs (posix)`),10 失败。失败按根因: + +| 数量 | 根因 | 成员 | 归属 | +| --- | --- | --- | --- | +| 4 | `windows.h`,经**我们自己定义的** `__CYGWIN__` | archive, sqlite3, mimalloc, c-ares | **P3** | +| 2 | `__cxa_thread_atexit` | spdlog, doctest | **C1** | +| 2 | `linux/` uapi 头 | curl, cmp-module | C2 | +| 1 | `arc4random_buf` | expat | C3 | +| 1 | `curl_off_t` | curl | C4 | + +这张表是本文所有优先级的来源:**P3 一条覆盖 40% 的失败,P3 + C1 覆盖 60%。** + +--- + +## 2. 未实现清单 + +按**谁负责**分组。每项给出:动机、判据、阻塞。 + +### 2.1 引擎侧(mcpp) + +#### E1 — P3:撤掉 `__CYGWIN__` 借用 + +**状态**:**已实现**(mcpp 2026.9.21.1)。下文保留论证;落地形态见本节末。 + +保留 `__CYGWIN__` 的本意是给「PE 格式 + POSIX C 环境」一个名字。实证否定了这个用法: + +```c +/* mimalloc/atomic.h */ +#if defined(_WIN32) || defined(__CYGWIN__) // we use windows locks on cygwin, but otherwise treat it at unix +/* xz/tuklib_physmem.c */ +#if defined(_WIN32) || defined(__CYGWIN__) +/* sqlite3.c:15456 —— SQLITE_OS_WIN 的判定列表 */ +# if defined(_WIN32) || defined(WIN32) || defined(__CYGWIN__) || \ +``` + +上游用这个名字表达的是「**Win32 API 可用**」。**一个借来的名字,语义由借出方的历史 +决定,不由我们的意图决定。** + +**2026-09-21 新增的源码级验证**:`tuklib_physmem.c` 是一条 `#if/#elif` 链,两个宏都 +不成立时**不包含任何平台头**,而函数体同样的链落到 `#endif` 后 `return ret;`(`ret = 0`, +「物理内存未知」,xz 自己处理)。**所以撤掉 `__CYGWIN__` 后该文件编译通过。** + +> 这一条推翻了一个流传中的结论。`lsp-mcpp-private` 线上的分析把这个失败归给 +> 「xz 不适配、上游不打算修」,引的是 openkal 0.13 记录第 5 节「xz 去掉 `_WIN32` 后 +> 在 musl-Windows 上**解码失败**」。那是**运行期**的说法;现在撞上的是**编译期** +> 失败,两者被并成了一条。**编译这条是我们自己造成的,解码那条 P3 不自动回答。** + +**形状**:`cenv.cppm` 的 Windows+Posix 分支加 `-U__CYGWIN__ -U__CYGWIN32__`, +`expectUndefined` 加两项。 + +**「对象格式是 PE」这一维**:默认**不给宏**。包问 `cfg(os = "windows")`,那不需要任何 +宏。若重测证明确有第三方代码只能在预处理期问,回落是 mcpp 定义**自己的**名字 +(`__mcpp_format_pe__`),不继续借别人的。 + +**判据**:30 成员重测,`windows.h` 组从 4 降到 0,且**总失败数不增加**。 + +**阻塞**:无技术阻塞。**风险点已点名**:libarchive 的生成配置头含 +`#if defined(_WIN32) && !defined(__CYGWIN__)`,撤销会**翻转**它的分支。基线已有,重测 +即可读出净值。 + +**落地形态(2026.9.21.1)**:`cenv.cppm` 的 Windows+Posix 分支加两个 `-U` token, +`expectUndefined` 加两项——**后者才是承重的**:探针会把实现出的配置的预定义与这两张表 +比对,一个没生效的 `-U` 是一次校验失败,不是一次沉默。 + +直接量过一次,用钉住的 clang、按 mcpp 实际发出的 token 顺序: + +``` +echo | clang -dM -E -x c - -U__CYGWIN__ -U__CYGWIN32__ \ + --target=x86_64-pc-cygwin -U__CYGWIN__ -U__CYGWIN32__ + -> __unix__ 定义;__CYGWIN__ 消失;_WIN32 仍不存在 +``` + +**`.S` 行上这对 token 出现两次**,`.c`/`.cpp` 各一次。`cEnvTokens` 会被加进包的 +asmflags,而形如 `-D`/`-U`/`-I` 的 token 同时经由「把 define 带进汇编」的通道到达; +`--target=` 与 `-fno-short-wchar` 不是那个形状,所以只出现一次。`-U X` 两次等于一次, +且真正的判据是探针,所以 e2e 不对次数作断言——那会把 flag 管线的实现细节钉死,而不是 +钉住被测性质。 + +#### E2 — P7-L3:链接期集合差 + +**状态**:未实现。设计称其为「本轮与设计最大的偏差」。 + +`requires-interfaces` 今天只在**解析期**被比对,而解析期比的是**声明**。声明由产物 +派生(L1 已保证),但**没有任何东西在链接期检查程序实际引用的符号是否落在声明的接口 +集合内**。 + +**判据**:一个程序引用了实现未提供的接口里的符号,链接期被点名拒绝,且**声明正确时 +零额外链接开销**。 + +**阻塞**:需要把 `SURFACE.txt` 的「接口 → 符号」映射带到链接期。材料齐(`SURFACE.txt`、 +`kal_interfaces()`),是工作量不是未知数。 + +#### E3 — 未被回答的 requirement 没有提示 + +**状态**:未实现。记录:记忆 `an-unanswered-requirement-looks-like-a-confirmed-one`。 + +三种情形两种绿:提供方声明且包含 → 构建(**已确认**);声明但不包含 → 拒绝; +**什么都没声明 → 构建(从没被检查过)**。第三种是有意的(`provides-interfaces` 晚于 +那些包出现),但它让「是」与「没问成」同读数。 + +生态侧本轮已补齐(三个实现都声明了),所以这条现在只对**第三方实现**成立。 + +**形状**:target-side 报告里加一行,形如 +`kernel-abi interfaces: states none, N requirement(s) unchecked`。 + +**判据**:一个声明了 `requires-interfaces` 的包,配一个什么都没声明的提供方,构建 +**成功**且输出里**有**这一行;提供方声明了则**没有**这一行。 + +### 2.2 C 库侧(openkal-musl) + +#### C1 — `__cxa_thread_atexit` + +**状态**:第一层可修但**不能只修第一层**;第二层未定位。记录: +`.agents/docs/2026-09-20-cxa-thread-atexit-finding.md`。 + +只补符号会把一个**构建期的响亮失败**换成一个**运行期的静默失败**:链接过了, +`thread_local` 的析构不跑。补丁试过并**主动回退**,因为验证显示析构确实没执行。 + +**判据**:最小复现(五行,文档里有)在 `x86_64-windows-gnu` 上**链接通过且析构函数 +真的执行**——两个条件缺一不可。只断言链接通过是错的判据。 + +**阻塞**:第二层未定位(emutls 在 PE 上的注册路径)。这是本清单里**唯一一个真正的 +未知数**。 + +#### C2 — `linux/` uapi 头(curl, cmp-module) + +**形态**:程序 `#include ` 一类。openkal 不是 Linux,没有 uapi 头, +**这是正确的**。 + +**本文初稿把它写成「P5 应当转成 `refused`」,那是错的。** P5 比对的是 **openkal +接口**,而 Linux uapi 头不是任何 openkal 接口;引擎对它只能给出一条普通编译错误,与 +其他编译错误不可区分。`compat.py` 的 `kind_of` 对 `fails` 刻意返回 `None`,并在注释里 +写明理由:「没有声明或测量支持的标签,比没有标签更坏」——所以**没有现成机制可用**。 + +**正确形态**:这是成员**按构造不可移植**(无条件包含平台头),应当由 `members.toml` +上的一条**声明**表达,而不是由引擎猜。判据随之变成:该声明存在时,成员从兼容率的 +**分母**里移除,且移除理由可追溯到那条声明。 + +**归属**:mcpp-index(测量的成员表),不是 C 库。 + +#### C3 — `arc4random_buf`(expat)——**已修复,且归属与初稿不同** + +**初稿写的是**「musl 有 `arc4random_buf`,openkal-musl 的移植未导出」。**这是错的。** +musl 1.2.5 **根本没有 arc4random**(`src/prng/` 是 rand48 一族),头文件里也没有这个名字。 + +**真因在索引配方**:`compat.expat.lua` 的 `generated_files["lib/expat_config.h"]` 是 +**一份对所有目标通用的静态头**,里面从 glibc 的 configure 结果抄来了 +`#define HAVE_ARC4RANDOM_BUF 1`。而这个宏**短路整条链**: + +```c +#if defined(HAVE_ARC4RANDOM_BUF) + arc4random_buf(&entropy, sizeof(entropy)); /* 无条件走这条,下面的回落永不到达 */ +#elif defined(HAVE_ARC4RANDOM) +#else +# elif defined(HAVE_GETRANDOM) || defined(HAVE_SYSCALL_GETRANDOM) +``` + +取消定义后落到 `HAVE_GETRANDOM`(同一份头里已定义,musl 与 glibc 都提供),再落到 +`XML_DEV_URANDOM`。**没有任何目标因此失去高质量熵源。** + +**状态**:已实现并验证(mcpp-index PR)。对着已发布栈(openkal-linux 0.15.0 / +openkal-musl 0.18.0 / openkal-llvm-runtime 0.13.0)构建,expat 2.7.1 编译通过、程序 +链接并运行,产物 `nm -u` 里没有任何 arc4random 符号。 + +**归属**:mcpp-index 配方,不是 C 库。 + +#### C4 — `memset_pattern16`(macOS release) + +Darwin libc 扩展。`-O2` 下编译器把填充循环换成它,openkal-musl 没有。 + +**注意**:这不是「程序调用了它」,是**编译器合成的调用**。所以它必须由 C 库提供, +程序侧无法规避。 + +**判据**:`aarch64-macos` 的 release 构建链接通过。这条是 `lsp-mcpp-private` macOS +验收的**硬阻塞**。 + +### 2.3 实现侧(openkal-*) + +#### I1 — P4:合成节点层 + +**状态**:未实现。设计 §5.4。 + +写得完全正确的 POSIX 代码——`open("/dev/urandom")`、`fopen("/dev/null","w")`、 +`/dev/fd/N`、`/tmp`——在 openkal-Windows 上会坏,**而没有任何一层负责**。这是唯一一类 +「代码本来就对」的失败。 + +**它不是新层**:`openkal-musl` 的 `port/src/okm_fd.c` 已经造了两样同源的东西(描述符表、 +单一全局根的名字解析)。合成节点是第三样。 + +**判据**:节点集合**写死**,不迭代扩张;conformance 逐条断言;集合外的路径行为不变。 + +#### I2 — P6:`openkal-win-ucrt` 形态 + +**状态**:未实现。设计 §5.6。已验证**引擎侧改动数为 0**。 + +**判据**:一个 `presents = "windows"` 的 C 库在同一引擎下构建,引擎不认识任何新名字。 + +### 2.4 生态数据侧 + +#### D1 — libarchive 这条路(`lsp-mcpp-private` 的打包) + +`compat.libarchive` 无条件依赖 zlib/bzip2/lz4/zstd/xz,配置头写死 `HAVE_LIBLZMA 1`, +没有可关的 feature。后果:Windows 编不过(xz → `windows.h`,**E1 覆盖**),macOS +release 链接不过(`memset_pattern16`,**C4 覆盖**)。 + +**所以这条路的两个阻塞分别由 E1 与 C4 关闭,不需要单独决策。** 只有在 E1+C4 落地后 +重测仍红时,才需要在「上游推一个不带编码器的 feature / 只保留解包 / 分平台走两条路」 +之间拍板。**先测再拍板,不要先拍板。** + +--- + +## 3. 为什么用 `lsp-mcpp-private` 验收 + +三条性质使它成为**比任何合成夹具都强**的载体。 + +**(一)它是真实的、非玩具的。** `mcpp-language-server`,C++23 模块,二十余个模块, +六个可执行目标(服务器、conformance、lspgen、devtools、两个 mock)。合成夹具只能证明 +「引擎在我构造的那一小格里对」;它证明「一个有人用的程序能不能过」。 + +**(二)它的平台面已经被压到可数。** `modules/os/{linux,macos,windows}/src/os.cppm` +各 20 行,导出同一个 `mcppls.os` 接口,只有六个常量: + +```cpp +Family FAMILY; string_view FAMILY_NAME; string_view EXECUTABLE_SUFFIX; +char PATH_LIST_SEPARATOR; string_view VSCODE_TARGET; bool CASE_INSENSITIVE_PATHS; +``` + +**这六个都不是 POSIX 设施**,是打包与命名事实(可执行后缀、PATH 分隔符、VS Code 目标 +三元组、路径大小写)。它们**本来就该由目标回答**,不归 openkal。 + +由此得到一个**极其锋利的判据**:**除这六个常量外,任何新出现的平台分支都是一处被点名 +的 openkal 缺口。** 分母固定,信号无噪声。 + +**(三)它几乎不直接碰 C。** 实测:`src/` 与 `modules/` 下**没有任何直接 POSIX 调用** +(`select(` / `pipe(` 的命中全是它自己的同名函数),唯一的 C 头是 +`archive.h` / `archive_entry.h`。 + +于是它的失败面**只有两处**:C++ 运行时(libc++/libc++abi/libunwind over musl over +openkal),与 libarchive 的传递 C 依赖。**一个失败落在哪一侧,一眼可辨。** + +> 反面:正因为它不直接碰 C,它**测不到** C 库的大部分表面。所以它是 +> **整合验收**,不替代 30 成员测量(那个才覆盖 C 表面)。两者分工,不重叠。 + +--- + +## 4. 验收判据 + +### 4.1 三级,逐级收紧 + +| 级 | 判据 | 目标 | +| --- | --- | --- | +| **A1 构建** | `mcpp build` 通过 | `x86_64-linux-gnu`、`x86_64-windows-gnu`、`aarch64-macos` | +| **A2 运行** | `mcpp test` 全部通过;`mcppls --version` 在目标上真的跑 | 同上(非本机经 runner) | +| **A3 形状** | `modules/os/*/src/os.cppm` 仍**只有那六个常量**,且三份文件的 `diff` 仅这六行 | 全目标 | + +**A3 是本方案的核心判据。** A1/A2 回答「能不能跑」,A3 回答「**为了能跑,付出了几处 +平台分支**」——那才是 openkal 的命题。一次 A1 绿而 A3 红的验收,等于用分支换通过, +结论与不做无异。 + +### 4.2 机械化 + +`modules/os` 的三份文件逐行比对可以写成脚本,进 `lsp-mcpp-private` 的 CI: + +``` +三份 os.cppm 除 FAMILY / FAMILY_NAME / EXECUTABLE_SUFFIX / +PATH_LIST_SEPARATOR / VSCODE_TARGET / CASE_INSENSITIVE_PATHS +六行外必须逐字节相同;出现第七处差异即红,并打印那一行。 +``` + +**这条断言的价值在于它会因为「有人加了一处分支」而红,而不是因为「构建坏了」而红。** +前者是缓慢的、无人察觉的退化,正是它抓的。 + +### 4.3 与 30 成员测量的分工 + +| | 覆盖 | 不覆盖 | +| --- | --- | --- | +| 30 成员测量 | C 库表面(uapi、libc 扩展、`__CYGWIN__` 类分支) | 真实程序的整合、C++ 运行时深处 | +| `lsp-mcpp-private` | C++ 运行时、模块图、三目标一份源码 | 大部分 C 表面 | + +**两者都绿才叫生态闭环。** 任一单独绿都不足以下结论。 + +--- + +## 5. 执行顺序 + +依赖关系是真实的,不可交换。 + +``` +E1 (P3, 撤 __CYGWIN__) + ├─ 解锁 30 成员测量的 4 个失败 + └─ 解锁 lsp-mcpp-private 的 Windows 目标(经 xz) + │ +C4 (memset_pattern16) + └─ 解锁 lsp-mcpp-private 的 macOS release + │ + └──> A1/A2/A3 三目标验收 <── 本方案的终点 + │ +C1 (__cxa_thread_atexit) ──┘ (不阻塞 lsp-mcpp-private:它不用 thread_local 析构; + 阻塞的是 spdlog/doctest 那类消费者) + +E2 (P7-L3)、E3、I1 (P4)、I2 (P6)、C2、C3 —— 与上面无依赖,可并行,不阻塞验收 +``` + +**建议批次**: + +| 批 | 内容 | 为什么在一起 | +| --- | --- | --- | +| **第一批** | E1 + 30 成员重测 | 代价最小、收益最大(4/10),且是后续的前置读数 | +| **第二批** | C4 + C3 + C2 归类 | 都是 C 库侧的符号/归类问题,一次 openkal-musl 发布带走 | +| **第三批** | `lsp-mcpp-private` A1/A2/A3 三目标验收 + A3 断言进 CI | 前两批的收敛点 | +| **第四批** | E2 (P7-L3)、E3 | 引擎侧新能力,与验收无依赖 | +| **第五批** | I1 (P4)、I2 (P6)、C1 第二层 | 独立工作量;C1 含唯一的未知数 | + +--- + +## 6. 发布前的交叉验证协议 + +引擎是通用件,而生态是它唯一的真实负载。**mcpp 的 PR 在合入之前,应当由生态仓库拉着 +那个 PR 分支跑一遍;两侧 CI 同时绿,才构成合入的依据。** + +### 6.1 这条协议要防的事已经发生过 + +2026-09-20 发了 2026.9.20.1;不到一天,E1(撤 `__CYGWIN__`)又要一次 +2026.9.21.1。E1 的依据——四个成员因 `__CYGWIN__` 停在 `windows.h`——**在 2026.9.20.1 +合入之前就已经可以测出来**:那份 30 成员测量用的是已发布引擎,但同一批成员完全可以 +用 PR 分支构建的引擎先跑一遍。 + +**一次本可以合并的发布,变成了两次。** 每一次发布都要走完 tag、四平台构建、两端镜像、 +逐资产 GET 核验、xim-pkgindex bump、索引 artifact 发布、消费方确认——协议省下的是 +这整条链,不是一次构建。 + +### 6.2 机制已经在那里 + +生态各仓的 CI 都认 `MCPP_SOURCE_REF`:非空时从 mcpp 的那个 ref 现场构建引擎,并断言 +PATH 上的 `mcpp` 就是构建出来的那一个("the engine on PATH is the one under review")。 + +| 仓库 | `MCPP_SOURCE_REF` | +| --- | --- | +| openkal, openkal-linux, openkal-windows, openkal-macos, openkal-musl, openkal-llvm-runtime | 已有 | +| **mcpp-index** | **没有** | +| **lsp-mcpp-private** | **没有** | + +后两个恰是最该有的:mcpp-index 承载 30 成员测量(引擎改动的最大负载面), +lsp-mcpp-private 是本方案的验收载体。**补这两处,是本协议唯一的一次性工作量。** + +### 6.3 顺序 + +``` +1. mcpp 开 PR,自身 CI 绿 +2. 在每个相关生态仓库开一个临时 PR(或 workflow_dispatch), + MCPP_SOURCE_REF = mcpp 的那个 PR 分支 +3. 两侧 CI 同时绿 +4. 生态级全局 review(见 6.4) +5. 合入 mcpp PR,发布 +6. 撤掉临时 PR 里的 MCPP_SOURCE_REF 覆盖,换成已发布的版本钉 +``` + +第 6 步不可省。`MCPP_SOURCE_REF` 是**开发形态**;留在合入的分支里,生态仓库就永远在 +验证一个未发布的引擎,而它自己的版本钉从此不被任何东西检查。 + +### 6.4 合入前的生态级 review + +跨仓库 review 要回答的不是"这段代码对不对",而是: + +| 问 | 为什么 | +| --- | --- | +| 这次改动改变了哪些包的**命令行**? | 引擎改动的影响面按构造是全图的 | +| 有没有包的**清单**因此需要改? | 需要改,就意味着这不是一次纯引擎发布 | +| 索引 `min_mcpp` 要不要动? | 动它会让停在下限的客户端**整个索引打不开** | +| 有没有**消费者**被钉在旧版本上而看不到这次改动? | 精确钉是常态,改动不会自动到达 | +| 生态 CI 的绿,是**替换过工作树**的绿吗? | 是的话,它对"已发布形态"零信息量 | + +### 6.5 这条协议**不覆盖**什么 + +生态仓库的 CI 大多把整张图换成兄弟仓库的**工作树**(`tools/branch-graph.sh`、 +`tools/working-trees.sh`)。所以它**按构造看不见**"某个版本还没注册进索引"这类缺口—— +2026-09-20 就是这样红了一次 mcpp 的 `openkal-cross`:openkal-llvm-runtime 0.13.0 钉了 +openkal-musl 0.18.0,而 0.18.0 当时还没进索引,消费者自己的 CI 全绿。 + +**交叉验证回答"引擎与生态的代码合不合得上",不回答"发布物到不到得了"。** 后者只有 +两个判据:一个不做工作树替换的第三方(mcpp 的 `openkal-cross` 就是),与沙箱里对已 +发布物的验证。两者都不能被本协议替代。 + +--- + +## 7. 判据总表 + +| 编号 | 判据 | 怎么算通过 | 阻塞 | +| --- | --- | --- | --- | +| 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 | + +--- + +## 8. 本方案自身的失败模式 + +写下来,因为它们在本轮各出现过一次。 + +**(一)「绿了但布置没成立」。** A1 可以因为有人加了一处 `#ifdef` 而绿。**A3 存在就是 +为了这个**,它必须与 A1 同时断言,不能事后补。 + +**(二)「判据取自会替换工作树的 CI」。** `lsp-mcpp-private` 若用 path 覆盖或分支克隆 +解析 openkal,它的绿对「已发布形态」零信息量。**验收必须走已发布的版本钉**, +且先 `mcpp index update` 再构建。 + +**(三)「注册与动 pin 同一个 PR」。** `lsp-mcpp-private` 的 +`openkal-llvm-runtime = "0.12.0"` 要移到 `0.13.0`;0.13.0 已注册并发布(2026-09-20), +所以这一步现在安全。**以后每次都要先确认。** + +**(四)「拿一句记录当判据」。** E1 那条就是:「xz 不适配」是一句关于**解码**的记录, +被当成了关于**编译**的结论。**读到「上游不打算修」时,先把那段源码打开。** + +--- + +## 9. 一句话 + +本轮把「能力何时被回答」这件事从预处理期移到了解析期,并让三个实现都说出自己提供什么。 +**剩下的工作里,代价最小、收益最大的一条是撤掉一个我们自己借来的名字**;而验收该由一个 +真实程序给出,判据不是「它能不能构建」,是「为了构建,它还剩几处平台分支」。 diff --git a/.agents/docs/README.md b/.agents/docs/README.md index c981faa5..d8683f72 100644 --- a/.agents/docs/README.md +++ b/.agents/docs/README.md @@ -18,7 +18,7 @@ superseded_by: 2026-09-07-....md # when status is superseded --- ``` -302 records. +303 records. ## By subject @@ -52,6 +52,7 @@ Records that declare one. Everything else is listed by date below. ### plan +- [openkal 生态:完整性收尾与验收方案](2026-09-21-openkal-ecosystem-completion-and-acceptance.md) — active - [C 环境生态方案:执行计划](2026-09-20-ecosystem-execution-plan.md) — active ### plugins @@ -93,6 +94,7 @@ Records that declare one. Everything else is listed by date below. ### 2026-09 +- [openkal 生态:完整性收尾与验收方案](2026-09-21-openkal-ecosystem-completion-and-acceptance.md) — active - [本轮生态级自审](2026-09-20-wave-self-review.md) — active - [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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 216a8a01..e8d51d82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,43 @@ ## [Unreleased] +## [2026.9.21.1] - 2026-09-21 + +### 撤掉借来的 `__CYGWIN__`,由一次测量裁决 + +`presents = "posix"` 在 Windows 上实现成 Cygwin 形状的目标。上一版**保留** +`__CYGWIN__`/`__CYGWIN32__` 定义,理由是:需要知道**目标文件格式**的第三方可移植代码, +没有别的名字能指代「PE 格式 + 呈现 POSIX 的 C 环境」。那一版同时写下了自己的翻转条件 +——**留给 30 成员测量裁决;若定义它带来的失败比修好的多,结论就翻过来。** + +测量在 2026-09-20 给出读数,60 个「成员 × 目标」组合:**保留它代价是四个成员**—— +`archive`、`sqlite3`、`mimalloc`、`c-ares`,各自停在 `#include `,都经由形如 +`#if defined(_WIN32) || defined(__CYGWIN__)` 的守卫到达。**同一轮里没有任何一项因为缺少 +这个宏而失败。** 四比零。 + +上游自己说明了这个名字的含义。mimalloc 把话写在守卫的注释里: +`we use windows locks on cygwin, but otherwise treat it at unix`;sqlite3 把它列进 +`SQLITE_OS_WIN` 的检测集合,随后 `#include "windows.h"`。**它们用这个名字表达的是 +「Win32 可用」,不是「目标文件格式是 PE」。一个借来的名字,语义由借出方的历史决定, +不由借用方的意图决定。** + +于是「目标文件格式」这一维**不保留任何宏**:包问 `cfg(os = "windows")`,那不需要宏。 +若某天确实发现只能在预处理期问这件事的第三方代码,答案是 mcpp 定义一个自己的名字。 + +承重的不是那两个 `-U` token,是 `expectUndefined` 里新增的两项:探针把实现出的配置的 +预定义与它比对,**一个没生效的 `-U` 是一次校验失败,不是一次沉默**。 + +实测(钉住的 clang,按 mcpp 实际发出的 token 顺序): + +``` +echo | clang -dM -E -x c - -U__CYGWIN__ -U__CYGWIN32__ \ + --target=x86_64-pc-cygwin -U__CYGWIN__ -U__CYGWIN32__ + -> __unix__ 定义;__CYGWIN__ 消失;_WIN32 仍不存在 +``` + +单测与 e2e 741 都改成断言相反的一侧,注释里记着这个模块两种答案都持有过、以及是什么 +把它翻过来的。 + ## [2026.9.20.1] - 2026-09-20 ### 校验探针量的是构建宿主,而不是它要核对的那个目标 @@ -100,7 +137,7 @@ requires-interfaces = ["openkal.fs", "openkal.net"] `refusal.cppm` 能发出的令牌与表里的行,并要求简体中文镜像携带同一个集合。 两个方向各去掉一条都会红。 -⚠️ 这张表的**列头**是 `| \`reason\` | |`——第一格里一个反引号名字,形状与下面每一行 +这张表的**列头**是 `| \`reason\` | |`——第一格里一个反引号名字,形状与下面每一行 完全相同。按「行首反引号名字」匹配会把 `reason` 当成一个令牌。行与列头的区别在**第二格 非空**,所以判据按性质挑对象,不按语法挑。 diff --git a/docs/22-target-side.md b/docs/22-target-side.md index ff90536e..56dc1c73 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; `__CYGWIN__`/`__CYGWIN32__` are left defined (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; `__CYGWIN__`/`__CYGWIN32__` are undefined on top of it (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,20 +418,31 @@ 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. -**`__CYGWIN__`/`__CYGWIN32__` are left defined — a revision from the -openkal-musl spike, not the design's original claim.** Undefining them was -tried first, on the reasoning that a real Cygwin userland is not in the -graph. Portable third-party code that needs to know the **object format** — -not the C environment, not the platform API — has no name for "PE format -with a POSIX-presenting C environment" other than `__CYGWIN__`, and such code -cannot be patched the way this ecosystem's own packages can. `presents = -"posix"` answers one question, which environment-identity macros source -sees; it does not get to answer a different one, what object format this is, -by deleting the only macro that names it. This is a **trade-off for the -30-member measurement to settle, not a settled fact**: a library reaching for -`__CYGWIN__` may also reach for a real Cygwin interface (`sys/cygwin.h`, -`cygwin_conv_path`) that does not exist here, and if defining it produces -more new failures than it fixes, the answer flips. +**`__CYGWIN__`/`__CYGWIN32__` are undefined, and a measurement decided it +(2026.9.21.1).** This page has carried both answers. A middle revision left +them defined, so that portable third-party code needing to know the **object +format** — not the C environment, not the platform API — would keep a name +for "PE format with a POSIX-presenting C environment". That revision wrote +its own condition for reversal: a trade-off for the 30-member measurement to +settle, flipping if defining them produced more failures than it fixed. + +It produced four and fixed none. Across 60 member-target combinations, +`archive`, `sqlite3`, `mimalloc` and `c-ares` each stopped at +`#include `, reached through a guard of the shape +`#if defined(_WIN32) || defined(__CYGWIN__)`. Nothing in the same run failed +for want of the macro. + +**A borrowed name means what the lender's history made it mean.** Upstream +uses this one to say *Win32 is available*, not *the object format is PE* — +mimalloc states it in the guard itself (`we use windows locks on cygwin, but +otherwise treat it at unix`), and sqlite3 lists it in the `SQLITE_OS_WIN` +detection set before including `windows.h`. The intent behind borrowing it +does not travel with it. + +The object-format question therefore keeps **no macro at all**. A package +asks `cfg(os = "windows")`, which needs none. Should a third party ever be +found that can only ask in the preprocessor, the answer is a name mcpp +defines itself, not one it borrows. **A `kernel-abi` provider's own units are INFERRED onto the platform boundary — it never has to say so (mcpp 2026.9.18+, a mid-PR revision from the diff --git a/docs/zh/22-target-side.md b/docs/zh/22-target-side.md index d6392f4b..a6cf8816 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`;`__CYGWIN__`/`__CYGWIN32__` 保持定义(见下方说明);`data-model` 变为 LP64 是三元组切换的结果,不是另一个开关 | +| Windows | `posix` / `arch-default` | 采用 Cygwin 式语义:仅在编译行加 `--target=x86_64-pc-cygwin`;在其之上再加 `-U__CYGWIN__ -U__CYGWIN32__`(见下方说明);`data-model` 变为 LP64 是三元组切换的结果,不是另一个开关 | | 任意目标 | `builtins = "iso"` | 关闭代码生成阶段假定平台 C 库在场的惯用法识别——本轮实测到的唯一一例是 Apple 目标上的 `-fno-builtin-memset_pattern16`;`src/toolchain/cenv.cppm` 记录了还核实过哪些、结论是不适用 | | 其余情况 | | 明确拒绝,点名目标、请求与缺什么——不静默降级 | @@ -337,15 +337,25 @@ Windows 一行是旗舰情形:`x86_64-w64-windows-gnu` 与 `x86_64-pc-cygwin` 同样的 PE 格式、同样的 Win64 调用约定、同样的 SEH——差别只在预处理器看到什么、`long` 有多宽。 因此实现只触及**编译**行;**链接**行保持图解析出的三元组,因为目标文件格式没有变化。 -**`__CYGWIN__`/`__CYGWIN32__` 保持定义——这是 openkal-musl 尖峰实验带来的修订,不是设计 -最初的陈述。** 最初试过取消定义它们,理由是图里没有真正的 Cygwin 用户态。第三方可移植 -代码里,需要知道**目标文件格式**——不是 C 环境,也不是平台 API——的那部分,没有别的名字 -能指代「PE 格式加呈现 POSIX 的 C 环境」这个组合,只有 `__CYGWIN__`;这样的代码不像本生态 -自己的包那样可以打补丁。`presents = "posix"` 回答的是一个问题——源码看到哪些环境身份宏; -它不能顺带删掉唯一能回答另一个问题——这是什么目标文件格式——的宏。这是一项**留给 30 个 -成员那轮实测去判定的权衡,不是已经定论的事实**:一个库伸手去够 `__CYGWIN__`,也可能伸手 -去够一个这里并不存在的真正 Cygwin 接口(`sys/cygwin.h`、`cygwin_conv_path`)——如果定义它 -带来的新失败比修好的还多,结论就会翻过来。 +**`__CYGWIN__`/`__CYGWIN32__` 取消定义,而这是一次实测裁决的(2026.9.21.1)。** 本页 +两种答案都写过。中间一版保持它们定义,为的是让需要知道**目标文件格式**——不是 C 环境, +也不是平台 API——的第三方可移植代码,留住「PE 格式加呈现 POSIX 的 C 环境」这个组合的名字。 +那一版自己写下了翻转条件:留给 30 个成员那轮实测判定的权衡,若定义它带来的失败比修好的多, +结论就翻过来。 + +它带来四个,修好零个。60 个「成员 × 目标」组合里,`archive`、`sqlite3`、`mimalloc`、 +`c-ares` 各自停在 `#include `,经由形如 +`#if defined(_WIN32) || defined(__CYGWIN__)` 的守卫到达。同一轮里没有任何一项因为**缺少** +这个宏而失败。 + +**一个借来的名字,它的语义由借出方的历史决定。** 上游用它表达的是「**Win32 可用**」, +不是「目标文件格式是 PE」——mimalloc 把这句话写在守卫自己的注释里(`we use windows locks +on cygwin, but otherwise treat it at unix`),sqlite3 把它列进 `SQLITE_OS_WIN` 的检测集合 +随后 `#include "windows.h"`。借用时的意图不会跟着名字一起走。 + +于是「目标文件格式」这个问题**不保留任何宏**。包问 `cfg(os = "windows")`,那不需要宏。 +若某天确实发现只能在预处理期问这件事的第三方代码,答案是 mcpp 定义一个**自己的**名字, +而不是继续借别人的。 **`kernel-abi` 提供者的自身单元被推导落到平台边界上——它不必自己说出来** (mcpp 2026.9.18+,PR 进行中根据 openkal-musl 尖峰实验做的修订)。提供 diff --git a/mcpp.toml b/mcpp.toml index cf90fc4c..a842076b 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.9.20.1" +version = "2026.9.21.1" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index 11c5c6aa..f1afeecb 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.20.1"; +inline constexpr std::string_view MCPP_VERSION = "2026.9.21.1"; } // namespace mcpp diff --git a/src/toolchain/cenv.cppm b/src/toolchain/cenv.cppm index e60e5709..6f56316a 100644 --- a/src/toolchain/cenv.cppm +++ b/src/toolchain/cenv.cppm @@ -28,9 +28,9 @@ // 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`, -// `__CYGWIN__`/`__CYGWIN32__` STAY DEFINED (see -// the note below the table — this is a design -// revision, not the original §3.3 text) +// plus `-U__CYGWIN__ -U__CYGWIN32__` (see +// the note below the table — measurement +// returned this to the original §3.3 text) // * 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,24 +70,27 @@ // 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 NOT REMOVED, AND THE FIRST VERSION OF THIS -// MODULE GOT THAT WRONG. §3.3's original text called for `-U__CYGWIN__ -// -U__CYGWIN32__` on the reasoning that a real Cygwin userland is not in the -// graph. A first reading of the openkal-musl spike's libunwind build failure -// blamed a missing `__CYGWIN__` branch in libunwind itself; reading the -// vendored source shows that is wrong — upstream libunwind has no such -// branch, so defining it there would have changed nothing (the actual break -// was a downstream package selecting on `_WIN32` and is being fixed there). -// The reason to keep them defined is narrower and still real: third-party -// portable code that has to know the OBJECT FORMAT — as opposed to which C -// environment or which platform API — has no name for "PE format with a -// POSIX-presenting C environment" other than `__CYGWIN__`, and such code -// cannot be patched the way this ecosystem's own packages can. The cost is -// symmetric: a library that reaches for `__CYGWIN__` may also reach for a -// real Cygwin interface (`sys/cygwin.h`, `cygwin_conv_path`) that does not -// exist here. This is a TRADE-OFF for the 30-member measurement to settle — -// if defining it produces more new failures than it fixes, the answer flips -// — not a fact this module is asserting as closed. +// `__CYGWIN__`/`__CYGWIN32__` ARE REMOVED, AND THIS MODULE HAS HELD BOTH +// ANSWERS. The first version removed them; a second kept them, on the +// argument that third-party code needing to know the OBJECT FORMAT has no +// other name for "PE format with a POSIX-presenting C environment". That +// version wrote the condition for changing its mind: 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 it (2026-09-20, 60 member-target combinations). +// Keeping them cost four members, each stopping at `#include ` +// reached through `#if defined(_WIN32) || defined(__CYGWIN__)`; none failed +// for want of the macro. Four against zero, so the answer flipped. +// +// What the second version got wrong is worth keeping, because it is not +// specific to this macro. The name was borrowed to mean "the object format +// is PE". Upstream uses it to mean "Win32 is available" — mimalloc says so in +// the guard itself, and sqlite3 puts it in the `SQLITE_OS_WIN` detection set. +// A BORROWED NAME MEANS WHAT THE LENDER'S HISTORY MADE IT MEAN, not what the +// borrower intended. The object-format question therefore keeps no macro at +// all: a package asks `cfg(os = "windows")`. If some day a third party can +// only ask it in the preprocessor, mcpp defines a name of its own. export module mcpp.toolchain.cenv; import std; @@ -286,16 +289,38 @@ inline std::expected realise( "on x86_64 only; this arch has no verified " "substitute triple"); // `--target=x86_64-pc-cygwin`, on the COMPILE line only (module - // header above). `__CYGWIN__`/`__CYGWIN32__` are LEFT AS THE - // TRIPLE SUBSTITUTION DEFINES THEM — not undefined (see the - // module header's note: portable third-party code that needs to - // know the object format has no other name for "PE format, - // POSIX-presenting environment", and this is a trade-off for the - // 30-member measurement, not a settled fact). + // header above), AND `__CYGWIN__`/`__CYGWIN32__` UNDEFINED ON TOP + // OF IT. The triple is what suppresses `_WIN32`, gives `__unix__` + // and gives LP64; the two `-U` tokens remove the one thing the + // triple carries that this environment cannot honour. + // + // THE MEASUREMENT THIS WAS LEFT TO HAS SETTLED IT (2026-09-20, 60 + // member-target combinations on the published graph). Defining + // them cost four members --- archive, sqlite3, mimalloc, c-ares + // --- each stopping at `#include ` reached through a + // guard of the shape `#if defined(_WIN32) || defined(__CYGWIN__)`. + // Nothing in the same run failed for want of the macro. The + // module header's condition was "if defining it produces more new + // failures than it fixes, the answer flips": four against zero. + // + // Upstream says plainly what it means by the name. mimalloc: + // `// we use windows locks on cygwin, but otherwise treat it at + // unix`. sqlite3 lists it in the `SQLITE_OS_WIN` detection set and + // then includes `windows.h`. The name asserts WIN32 IS AVAILABLE, + // not "the object format is PE" --- a borrowed name means what the + // lender's history made it mean, whatever we intended by it. + // + // The object-format question keeps no macro. A package asks + // `cfg(os = "windows")`, which needs none. Should measurement ever + // show third-party code that can only ask it in the preprocessor, + // the answer is a name mcpp defines itself, not one it borrows. 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/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 1c85f7ca..22758d3d 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 @@ -154,24 +154,24 @@ def joined(argv_iter): # The ordinary consumer (main.cpp, package "cabi-probe") is target-side and # does NOT declare c-environment: it must carry the Cygwin-flavoured tokens. # -# `__CYGWIN__`/`__CYGWIN32__` are NOT undefined (design revision from the -# openkal-musl spike: third-party portable code that needs to know the -# object format -- not the C environment, not the platform API -- has no -# other name for "PE format, POSIX-presenting environment", and such code -# cannot be patched the way this ecosystem's own packages can; a trade-off -# for the 30-member measurement to settle, not a settled fact) -- so this -# asserts `-U__CYGWIN__` is ABSENT from the command line, the opposite of an -# earlier version of this test. +# `__CYGWIN__`/`__CYGWIN32__` ARE undefined, and this assertion has held both +# answers. A middle revision kept them defined so third-party code needing to +# know the OBJECT FORMAT would have a name for "PE format, POSIX-presenting +# environment", and wrote its own condition for reversal: a trade-off for the +# 30-member measurement to settle. The measurement settled it (2026-09-20, 60 +# member-target combinations): keeping them cost four members -- archive, +# sqlite3, mimalloc, c-ares -- each stopping at `#include ` reached +# through `#if defined(_WIN32) || defined(__CYGWIN__)`, and nothing failed for +# want of the macro. Upstream uses the name to mean "Win32 is available", not +# "the object format is PE"; a borrowed name means what the lender's history +# 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") +missing = [tok for tok in ("--target=x86_64-pc-cygwin", "-fno-short-wchar", + "-U__CYGWIN__", "-U__CYGWIN32__") if tok not in consumer] if missing: print(f"FAIL: ordinary package is missing realised tokens {missing}\n args: {consumer}") sys.exit(1) -present = [tok for tok in ("-U__CYGWIN__", "-U__CYGWIN32__") if tok in consumer] -if present: - print(f"FAIL: __CYGWIN__/__CYGWIN32__ must stay defined, but found {present}\n args: {consumer}") - sys.exit(1) # fakemusl's OWN units get them too -- the environment applies to the C # library itself, not only to its consumers. @@ -200,18 +200,28 @@ if not asm_args: print("FAIL: could not find the .S unit's compile command at all") sys.exit(1) asm_joined = joined(iter(asm_args)) -missing = [tok for tok in ("--target=x86_64-pc-cygwin", "-fno-short-wchar") +missing = [tok for tok in ("--target=x86_64-pc-cygwin", "-fno-short-wchar", + "-U__CYGWIN__", "-U__CYGWIN32__") if tok not in asm_joined] if missing: print(f"FAIL: the assembly unit is missing realised tokens {missing} " f"-- the [c-abi] substitution must reach .S the same as .c/.cpp\n" f" args: {asm_joined}") sys.exit(1) -present = [tok for tok in ("-U__CYGWIN__", "-U__CYGWIN32__") if tok in asm_joined] -if present: - print(f"FAIL: __CYGWIN__/__CYGWIN32__ must stay defined on assembly too, " - f"but found {present}\n args: {asm_joined}") - sys.exit(1) + +# THE `-U` PAIR APPEARS TWICE ON THIS LINE, AND THAT IS NOT ASSERTED AGAINST. +# `cEnvTokens` is appended to a package's asmflags, and the tokens shaped like +# `-D`/`-U`/`-I` also reach assembly through the channel that carries defines +# there; `--target=` and `-fno-short-wchar` are not that shape and so appear +# once. Measured with the pinned clang, in the order mcpp emits them: +# +# echo | clang -dM -E -x c - -U__CYGWIN__ -U__CYGWIN32__ \ +# --target=x86_64-pc-cygwin -U__CYGWIN__ -U__CYGWIN32__ +# -> __unix__ defined, __CYGWIN__ absent, _WIN32 absent +# +# `-U X` twice is `-U X`, and the probe (`expectUndefined`) is what actually +# judges the result, so a count assertion here would pin an implementation +# detail of the flag plumbing rather than the property under test. # openkalwin provides `mcpp:kernel-abi=openkal` and declares NO # `c-environment` of its own -- the platform boundary is INFERRED from that diff --git a/tests/unit/test_cenv.cpp b/tests/unit/test_cenv.cpp index 3c11aa21..828f214e 100644 --- a/tests/unit/test_cenv.cpp +++ b/tests/unit/test_cenv.cpp @@ -64,29 +64,45 @@ TEST(CEnv, MacosPosixArchDefaultDefinesUnix) { // The flagship case: Cygwin-flavoured Windows. // -// `__CYGWIN__`/`__CYGWIN32__` STAY DEFINED — a design revision from the -// openkal-musl spike, not the original §3.3 text. Third-party portable code -// that needs to know the OBJECT FORMAT (as opposed to the C environment or -// the platform API) has no name for "PE format, POSIX-presenting -// environment" other than `__CYGWIN__`, and such code cannot be patched the -// way this ecosystem's own packages can. This is a trade-off for the -// 30-member measurement to settle, not a settled fact: the cost is that a -// library reaching for `__CYGWIN__` may also reach for a real Cygwin -// interface that does not exist here. +// `__CYGWIN__`/`__CYGWIN32__` ARE UNDEFINED, and this test has asserted both +// answers. The triple is what suppresses `_WIN32`, supplies `__unix__` and +// supplies LP64; the two `-U` tokens remove the one thing it carries that +// this environment cannot honour. +// +// A middle revision kept them defined, so that third-party code needing to +// know the OBJECT FORMAT would have a name for "PE format, POSIX-presenting +// environment", and wrote its own condition for reversal: a trade-off for the +// 30-member measurement to settle. The measurement settled it on 2026-09-20 +// across 60 member-target combinations. Keeping them cost four members --- +// archive, sqlite3, mimalloc, c-ares --- each stopping at +// `#include ` reached through `#if defined(_WIN32) || +// defined(__CYGWIN__)`. Nothing failed for want of the macro. Four to zero. +// +// The general lesson, which outlives this macro: upstream uses the name to +// mean "Win32 is available" (mimalloc says so in the guard; sqlite3 lists it +// under `SQLITE_OS_WIN`), not "the object format is PE". A BORROWED NAME +// MEANS WHAT THE LENDER'S HISTORY MADE IT MEAN. The object-format question +// keeps no macro: a package asks `cfg(os = "windows")`. 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")); - EXPECT_FALSE(has(r->tokens, "-U__CYGWIN__")); - EXPECT_FALSE(has(r->tokens, "-U__CYGWIN32__")); + 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); EXPECT_EQ(r->expectWcharBits, 32); ASSERT_TRUE(has(r->expectDefined, "__unix__")); - ASSERT_TRUE(has(r->expectDefined, "__CYGWIN__")); ASSERT_TRUE(has(r->expectUndefined, "_WIN32")); + // THE PROBE CHECKS THEM, which is what keeps this from being a token the + // compiler could ignore: `cenv_probe::verify` compares the realised + // configuration's predefines against these lists and refuses on a + // mismatch. A `-U` that did not take effect is a verification failure, + // not a silent one. + ASSERT_TRUE(has(r->expectUndefined, "__CYGWIN__")); + ASSERT_TRUE(has(r->expectUndefined, "__CYGWIN32__")); } // wchar = 16 on the Cygwin substitution matches Cygwin's own default, so no From e6a3a0a6808ff170a1a80a6d362f519c616cf0a4 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 02:03:28 +0800 Subject: [PATCH 2/5] C4's premise does not hold on this stack, so nothing is implemented for it The plan listed `memset_pattern16` as an openkal-musl gap and the hard blocker for macOS acceptance, on the strength of a report that the link still fails on the new stack. The engine already does the thing that should prevent it: `cenv.cppm` emits `-fno-builtin-memset_pattern16` for `builtins = "iso"` on macOS and iOS, and openkal-musl declares exactly that. AND THE FLAG REACHES THE PACKAGES NAMED. Measured by emitting a build database for `aarch64-macos` with 2026.9.21.1: all 29 of zstd's translation units carry it, including the one on the chain the report named. Three explanations survive, none of them decidable here --- a Linux host cannot compile for macOS, there being no SDK: the report predates the flag; the synthesised idiom is `memset_pattern4`/`8`, for which the engine emits no flag (clang's `-fno-builtin-` family is one flag per idiom) though the report names `16`; or the flag does not reach LLVM's loop-idiom pass. So the next step is a reading, not an implementation. `lsp-mcpp-private`'s `aarch64-macos --profile release` leg answers it directly, and each surviving explanation has its own follow-up. Adding the symbol to openkal-musl now would be repairing a gap that may not exist. --- ...kal-ecosystem-completion-and-acceptance.md | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) 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 c53b92d4..e5b59bb7 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 @@ -205,15 +205,33 @@ openkal-musl 0.18.0 / openkal-llvm-runtime 0.13.0)构建,expat 2.7.1 编译 **归属**:mcpp-index 配方,不是 C 库。 -#### C4 — `memset_pattern16`(macOS release) +#### C4 — `memset_pattern16`(macOS release)——**前提待重测,不要先动手** -Darwin libc 扩展。`-O2` 下编译器把填充循环换成它,openkal-musl 没有。 +Darwin libc 扩展。`-O2` 下编译器把填充循环换成它,openkal-musl 没有这个符号。这不是 +「程序调用了它」,是**编译器合成的调用**。 -**注意**:这不是「程序调用了它」,是**编译器合成的调用**。所以它必须由 C 库提供, -程序侧无法规避。 +**本文初稿据此把它列为 openkal-musl 的缺口,并当成 macOS 验收的硬阻塞。查下来前提 +站不住。** -**判据**:`aarch64-macos` 的 release 构建链接通过。这条是 `lsp-mcpp-private` macOS -验收的**硬阻塞**。 +引擎**已经**在做本该阻止它的事:`cenv.cppm` 对 `builtins = "iso"` 且目标为 +macOS/iOS 时发 `-fno-builtin-memset_pattern16`,而 openkal-musl 正是 +`builtins = "iso"`。并且这个 flag **确实到达了那些包**——用 2026.9.21.1 对 +`aarch64-macos` 发 build-database,`zstd` 的 29 个翻译单元(含报告点名的那条链上的 +`zstd_decompress_block.c`)命令行上都带着它。 + +于是报告里「新栈照样失败」只剩三种可能,**没有一种在本机可判**(Linux 宿主编不了 +macOS,缺 SDK): + +1. 那次测量跑在**加入该 flag 之前**的引擎上; +2. 合成出的是 `memset_pattern4`/`8`——引擎只发 `16` 那一个 flag,而 clang 的 + `-fno-builtin-` 家族是一个 idiom 一个 flag;但报告点名的确实是 `16`; +3. `-fno-builtin-memset_pattern16` 不足以关掉 LLVM 的 loop-idiom pass。 + +**下一步不是实现符号,是取一次当前栈上的读数。** `lsp-mcpp-private` 的 +`aarch64-macos --profile release` 会直接给出答案;若仍红,第 2、3 种可能各有明确的 +后续动作(补发另外两个 flag / 由 C 库提供该符号)。 + +**归属**:待定。**在读数出来之前给 openkal-musl 加符号,是在修一个可能不存在的缺口。** ### 2.3 实现侧(openkal-*) @@ -331,8 +349,8 @@ E1 (P3, 撤 __CYGWIN__) ├─ 解锁 30 成员测量的 4 个失败 └─ 解锁 lsp-mcpp-private 的 Windows 目标(经 xz) │ -C4 (memset_pattern16) - └─ 解锁 lsp-mcpp-private 的 macOS release +C4 (memset_pattern16) —— 前提待重测;引擎已发 -fno-builtin-memset_pattern16 + └─ 可能根本不是阻塞。读数由 A1 的 macOS 腿给出 │ └──> A1/A2/A3 三目标验收 <── 本方案的终点 │ @@ -434,7 +452,7 @@ openkal-musl 0.18.0,而 0.18.0 当时还没进索引,消费者自己的 CI | C1 | 五行最小复现 | 链接通过**且析构真的执行** | **第二层未定位** | | C2 | curl / cmp-module | 记为 `refused` 而非 `fails` | 无 | | C3 | `arc4random_buf` | 「应当有的符号」CI 断言 | 无 | -| C4 | `aarch64-macos --profile release` | 链接通过 | 无 | +| C4 | `aarch64-macos --profile release` | 链接通过 | **前提待重测**(引擎已发抑制 flag,且实测到达了 zstd 的 29 个单元) | | I1 | 写死的节点集合 | conformance 逐条断言 | 无 | | I2 | `presents = "windows"` 的 C 库 | 引擎改动数为 0 | 无 | | **A1** | 三目标 `mcpp build` | 全绿 | E1, C4 | From 358f4e2b6398b098c06d2e59453b0b2634edd245 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 02:20:06 +0800 Subject: [PATCH 3/5] cross-verification stopped the withdrawal, so mcpp names the target itself The first shape of this change simply withdrew `__CYGWIN__`. It was green here and green in four of five ecosystem repositories built against this branch; openkal-llvm-runtime failed, libunwind's `static_assert` reporting that `Registers_x86_64` does not fit `unw_context_t`. TWO INSTALLED HEADERS IN THIS ECOSYSTEM READ THAT MACRO ON PURPOSE, each saying so in its own source: `openkal-musl`'s `bits/setjmp.h` sizes `jmp_buf` by it, and `openkal-llvm-runtime`'s `__libunwind_config.h` sizes `unw_context_t`. Both are INSTALLED --- an application's own compile reads them --- so neither can use the package-private define its sibling `.S` files use, and `__CYGWIN__` was the only name mcpp kept defined target-wide. The 30-member measurement that justified withdrawal had counted THIRD-PARTY readers of the macro and not ours. Ours are load-bearing, and wrong is silent where it matters most: `setjmp.h`'s own comment says "a mismatch nothing reports until the record overruns". libunwind having a `static_assert` is what made this loud, not anything the measurement did. So mcpp states the fact itself. `-D__mcpp_target_windows__=1` answers the question those headers ask --- is this target Windows, whatever C environment is presented above it --- and being mcpp's own name, its meaning is not decided by anyone else's history. It is emitted only under this substitution; an ordinary Windows build still has `_WIN64`. `expectDefined` carries it, so a `-D` that failed to take effect is a verification failure rather than a silent one. The name differs from the design's proposed `__mcpp_format_pe__` because the two consumers do not want the object format: they size Win64 register save areas, which is the calling convention. The two co-vary on this target, and naming it for the question actually asked is the honest form. `__CYGWIN__` REMAINS DEFINED, as step one of three: this release adds the new name, those packages move onto it while still accepting the old one, and only then does a release stop defining the borrowed one. Step three taken first would leave every published copy of those headers falling to its `#else` --- the wrong record size, reported by nothing. Four loudly failing third-party members is the better state to hold for one release. --- ...kal-ecosystem-completion-and-acceptance.md | 46 +++++-- CHANGELOG.md | 58 ++++---- docs/22-target-side.md | 69 ++++++---- docs/zh/22-target-side.md | 52 ++++--- src/toolchain/cenv.cppm | 128 +++++++++++------- ...i_environment_declared_by_the_c_library.sh | 4 +- tests/unit/test_cenv.cpp | 51 +++---- 7 files changed, 238 insertions(+), 170 deletions(-) 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 e5b59bb7..18f647e8 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,7 +61,8 @@ status: active #### E1 — P3:撤掉 `__CYGWIN__` 借用 -**状态**:**已实现**(mcpp 2026.9.21.1)。下文保留论证;落地形态见本节末。 +**状态**:**分三步,本版落地第一步**(mcpp 2026.9.21.1)。下文保留论证;落地形态与 +被交叉验证挡下的那一版见本节末。 保留 `__CYGWIN__` 的本意是给「PE 格式 + POSIX C 环境」一个名字。实证否定了这个用法: @@ -99,23 +100,38 @@ status: active `#if defined(_WIN32) && !defined(__CYGWIN__)`,撤销会**翻转**它的分支。基线已有,重测 即可读出净值。 -**落地形态(2026.9.21.1)**:`cenv.cppm` 的 Windows+Posix 分支加两个 `-U` token, -`expectUndefined` 加两项——**后者才是承重的**:探针会把实现出的配置的预定义与这两张表 -比对,一个没生效的 `-U` 是一次校验失败,不是一次沉默。 +**先做的那一版被跨仓库交叉验证挡下了,这条记录比结论更值钱。** -直接量过一次,用钉住的 clang、按 mcpp 实际发出的 token 顺序: +第一次实现就是直接撤:加 `-U__CYGWIN__ -U__CYGWIN32__`,`expectUndefined` 加两项。本机 +全绿,五个生态仓库里四个也绿——**openkal-llvm-runtime 红了**,libunwind 的 +`static_assert` 失败:`Registers_x86_64` 装不进 `unw_context_t`。 -``` -echo | clang -dM -E -x c - -U__CYGWIN__ -U__CYGWIN32__ \ - --target=x86_64-pc-cygwin -U__CYGWIN__ -U__CYGWIN32__ - -> __unix__ 定义;__CYGWIN__ 消失;_WIN32 仍不存在 -``` +根因是生态里有**两个已安装的公开头**有意读 `__CYGWIN__`,理由写在各自源码里: + +| 包 | 头文件 | 它定尺寸的记录 | +| --- | --- | --- | +| openkal-musl | `port/include/bits/setjmp.h` | `jmp_buf` | +| openkal-llvm-runtime | `__libunwind_config.h` | `unw_context_t`、`unw_cursor_t` | + +两者都写明:这是**已安装**的头,会被**应用程序自己的编译**读到,所以用不了包私有的 +`OKM_TARGET_WINDOWS` / `OPENKAL_TARGET_WINDOWS`;而 `__CYGWIN__` 是 mcpp **target-wide** +保持定义的唯一名字。 + +**那个「四比零」数的是第三方读者,没数我们自己的。** 而自己这两个是承重的,且错了是 +**静默**的——`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` 仍不存在。 + +**为什么名字是 `__mcpp_target_windows__` 而不是设计里写的 `__mcpp_format_pe__`**:两个 +消费者要的不是「目标文件格式」,是**调用约定**(Win64 的寄存器保存区大小)。在这个目标上 +两者同变,但按消费者实际问的那个问题命名更诚实。 -**`.S` 行上这对 token 出现两次**,`.c`/`.cpp` 各一次。`cEnvTokens` 会被加进包的 -asmflags,而形如 `-D`/`-U`/`-I` 的 token 同时经由「把 define 带进汇编」的通道到达; -`--target=` 与 `-fno-short-wchar` 不是那个形状,所以只出现一次。`-U X` 两次等于一次, -且真正的判据是探针,所以 e2e 不对次数作断言——那会把 flag 管线的实现细节钉死,而不是 -钉住被测性质。 +**第二、三步**:两个包改读新名字并保留 `|| defined(__CYGWIN__)`(在两种引擎上都能构建); +之后的版本再停止定义借来的那个。**先做第三步会让已发布的那两个头静默落进 `#else`。** #### E2 — P7-L3:链接期集合差 diff --git a/CHANGELOG.md b/CHANGELOG.md index e8d51d82..399df486 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,40 +7,30 @@ ## [2026.9.21.1] - 2026-09-21 -### 撤掉借来的 `__CYGWIN__`,由一次测量裁决 - -`presents = "posix"` 在 Windows 上实现成 Cygwin 形状的目标。上一版**保留** -`__CYGWIN__`/`__CYGWIN32__` 定义,理由是:需要知道**目标文件格式**的第三方可移植代码, -没有别的名字能指代「PE 格式 + 呈现 POSIX 的 C 环境」。那一版同时写下了自己的翻转条件 -——**留给 30 成员测量裁决;若定义它带来的失败比修好的多,结论就翻过来。** - -测量在 2026-09-20 给出读数,60 个「成员 × 目标」组合:**保留它代价是四个成员**—— -`archive`、`sqlite3`、`mimalloc`、`c-ares`,各自停在 `#include `,都经由形如 -`#if defined(_WIN32) || defined(__CYGWIN__)` 的守卫到达。**同一轮里没有任何一项因为缺少 -这个宏而失败。** 四比零。 - -上游自己说明了这个名字的含义。mimalloc 把话写在守卫的注释里: -`we use windows locks on cygwin, but otherwise treat it at unix`;sqlite3 把它列进 -`SQLITE_OS_WIN` 的检测集合,随后 `#include "windows.h"`。**它们用这个名字表达的是 -「Win32 可用」,不是「目标文件格式是 PE」。一个借来的名字,语义由借出方的历史决定, -不由借用方的意图决定。** - -于是「目标文件格式」这一维**不保留任何宏**:包问 `cfg(os = "windows")`,那不需要宏。 -若某天确实发现只能在预处理期问这件事的第三方代码,答案是 mcpp 定义一个自己的名字。 - -承重的不是那两个 `-U` token,是 `expectUndefined` 里新增的两项:探针把实现出的配置的 -预定义与它比对,**一个没生效的 `-U` 是一次校验失败,不是一次沉默**。 - -实测(钉住的 clang,按 mcpp 实际发出的 token 顺序): - -``` -echo | clang -dM -E -x c - -U__CYGWIN__ -U__CYGWIN32__ \ - --target=x86_64-pc-cygwin -U__CYGWIN__ -U__CYGWIN32__ - -> __unix__ 定义;__CYGWIN__ 消失;_WIN32 仍不存在 -``` - -单测与 e2e 741 都改成断言相反的一侧,注释里记着这个模块两种答案都持有过、以及是什么 -把它翻过来的。 +### mcpp 为「这个目标是 Windows」给出自己的名字 + +`presents = "posix"` 在 Windows 上实现成 Cygwin 形状的目标,有意压掉 `_WIN32`——那正是 +「呈现 POSIX」的含义。但 **ABI 并没有跟着环境一起变**:调用约定仍是 Win64,寄存器保存区 +仍按它的大小。生态里有两个**已安装的**头按这个事实定尺寸——openkal-musl 的 +`bits/setjmp.h` 定 `jmp_buf`,openkal-llvm-runtime 的 `__libunwind_config.h` 定 +`unw_context_t`。已安装的头会被**应用程序自己的编译**读到,所以两者都用不了包私有的 define。 + +本版发 `-D__mcpp_target_windows__`。它是 mcpp 自己的名字,语义由 mcpp 自己定;只在这次 +替换下发出,普通 Windows 构建仍有 `_WIN64`。 + +**`__CYGWIN__` 仍然定义着,这是次序不是结论。** 30 成员测量已判定这个借来的名字代价是 +四个成员(`archive`、`sqlite3`、`mimalloc`、`c-ares` 各自停在 `#include `, +经由 `#if defined(_WIN32) || defined(__CYGWIN__)`);上游用它表达「Win32 可用」, +mimalloc 把这句话写在守卫自己的注释里。**一个借来的名字,语义由借出方的历史决定。** + +**撤掉它试过了,被跨仓库交叉验证挡下。** 上面那两个头正是因为没有别的 target-wide 名字 +才读它;撤掉后 libunwind 的 `static_assert` 响亮地红了,而 `setjmp.h` 那一处**不会**响—— +它自己的注释写着「a mismatch nothing reports until the record overruns」。那次测量数的是 +**第三方**读者,没数我们自己的。 + +于是撤销分三步,每个中间状态都能构建:本版**增加**新名字;两个包改读新名字并保留 +`|| defined(__CYGWIN__)`;之后的版本再停止定义借来的那个。先做第三步会让**已发布**的那 +两个头全部落进 `#else`——错误的记录尺寸,没有任何东西报告。 ## [2026.9.20.1] - 2026-09-20 diff --git a/docs/22-target-side.md b/docs/22-target-side.md index 56dc1c73..3f9972fa 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; `__CYGWIN__`/`__CYGWIN32__` are undefined on top of it (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,31 +418,48 @@ 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. -**`__CYGWIN__`/`__CYGWIN32__` are undefined, and a measurement decided it -(2026.9.21.1).** This page has carried both answers. A middle revision left -them defined, so that portable third-party code needing to know the **object -format** — not the C environment, not the platform API — would keep a name -for "PE format with a POSIX-presenting C environment". That revision wrote -its own condition for reversal: a trade-off for the 30-member measurement to -settle, flipping if defining them produced more failures than it fixed. - -It produced four and fixed none. Across 60 member-target combinations, -`archive`, `sqlite3`, `mimalloc` and `c-ares` each stopped at -`#include `, reached through a guard of the shape -`#if defined(_WIN32) || defined(__CYGWIN__)`. Nothing in the same run failed -for want of the macro. - -**A borrowed name means what the lender's history made it mean.** Upstream -uses this one to say *Win32 is available*, not *the object format is PE* — -mimalloc states it in the guard itself (`we use windows locks on cygwin, but -otherwise treat it at unix`), and sqlite3 lists it in the `SQLITE_OS_WIN` -detection set before including `windows.h`. The intent behind borrowing it -does not travel with it. - -The object-format question therefore keeps **no macro at all**. A package -asks `cfg(os = "windows")`, which needs none. Should a third party ever be -found that can only ask in the preprocessor, the answer is a name mcpp -defines itself, not one it borrows. +**`__mcpp_target_windows__` is defined here, and `__CYGWIN__` still is too +(2026.9.21.1).** + +The substitution suppresses `_WIN32` on purpose — that is what presenting +POSIX means. But the **ABI did not change with the environment**: the calling +convention is still Win64 and the register save areas are still its. Two +INSTALLED headers in this ecosystem size records by that fact: + +| package | header | sizes | +|---|---|---| +| openkal-musl | `port/include/bits/setjmp.h` | `jmp_buf` | +| 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 +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; +an ordinary Windows build still has `_WIN64`. + +**`__CYGWIN__` is still defined, and that is a sequence rather than a +decision to keep it.** The 30-member measurement settled that the borrowed +name costs four members: `archive`, `sqlite3`, `mimalloc` and `c-ares` each +stop at `#include `, reached through +`#if defined(_WIN32) || defined(__CYGWIN__)`. Upstream means *Win32 is +available* by it — mimalloc states so in the guard itself, sqlite3 lists it +under `SQLITE_OS_WIN`. **A borrowed name means what the lender's history made +it mean**, not what the borrower intended. + +Withdrawing it was tried and broke the two headers above, which read it for +want of any other target-wide name. libunwind's `static_assert` failed +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; +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. + +Taking step three first would leave every **published** copy of those headers +falling to its `#else`: the wrong record size, reported by nothing. **A `kernel-abi` provider's own units are INFERRED onto the platform boundary — it never has to say so (mcpp 2026.9.18+, a mid-PR revision from the diff --git a/docs/zh/22-target-side.md b/docs/zh/22-target-side.md index a6cf8816..6f9c9758 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`;在其之上再加 `-U__CYGWIN__ -U__CYGWIN32__`(见下方说明);`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,25 +337,37 @@ Windows 一行是旗舰情形:`x86_64-w64-windows-gnu` 与 `x86_64-pc-cygwin` 同样的 PE 格式、同样的 Win64 调用约定、同样的 SEH——差别只在预处理器看到什么、`long` 有多宽。 因此实现只触及**编译**行;**链接**行保持图解析出的三元组,因为目标文件格式没有变化。 -**`__CYGWIN__`/`__CYGWIN32__` 取消定义,而这是一次实测裁决的(2026.9.21.1)。** 本页 -两种答案都写过。中间一版保持它们定义,为的是让需要知道**目标文件格式**——不是 C 环境, -也不是平台 API——的第三方可移植代码,留住「PE 格式加呈现 POSIX 的 C 环境」这个组合的名字。 -那一版自己写下了翻转条件:留给 30 个成员那轮实测判定的权衡,若定义它带来的失败比修好的多, -结论就翻过来。 - -它带来四个,修好零个。60 个「成员 × 目标」组合里,`archive`、`sqlite3`、`mimalloc`、 -`c-ares` 各自停在 `#include `,经由形如 -`#if defined(_WIN32) || defined(__CYGWIN__)` 的守卫到达。同一轮里没有任何一项因为**缺少** -这个宏而失败。 - -**一个借来的名字,它的语义由借出方的历史决定。** 上游用它表达的是「**Win32 可用**」, -不是「目标文件格式是 PE」——mimalloc 把这句话写在守卫自己的注释里(`we use windows locks -on cygwin, but otherwise treat it at unix`),sqlite3 把它列进 `SQLITE_OS_WIN` 的检测集合 -随后 `#include "windows.h"`。借用时的意图不会跟着名字一起走。 - -于是「目标文件格式」这个问题**不保留任何宏**。包问 `cfg(os = "windows")`,那不需要宏。 -若某天确实发现只能在预处理期问这件事的第三方代码,答案是 mcpp 定义一个**自己的**名字, -而不是继续借别人的。 +**这里定义 `__mcpp_target_windows__`,而 `__CYGWIN__` 仍然定义着(2026.9.21.1)。** + +这次替换有意压掉 `_WIN32`——那正是「呈现 POSIX」的含义。但**ABI 并没有跟着环境一起变**: +调用约定仍是 Win64,寄存器保存区仍按它的大小。生态里有两个**已安装的**头按这个事实定尺寸: + +| 包 | 头文件 | 它定尺寸的记录 | +|---|---|---| +| openkal-musl | `port/include/bits/setjmp.h` | `jmp_buf` | +| openkal-llvm-runtime | `__libunwind_config.h` | `unw_context_t`、`unw_cursor_t` | + +已安装的头会被**应用程序自己的编译**读到,所以两者都用不了包私有的 define。 +`__mcpp_target_windows__` 是 mcpp 为它们所问的那个问题给出的**自己的**名字——这个目标是不是 +Windows,无论上面呈现的是什么 C 环境——既然是自己的名字,它的语义就不由别人的历史决定。 +它只在这次替换下发出;普通的 Windows 构建仍然有 `_WIN64`。 + +**`__CYGWIN__` 仍然定义着,这是一个次序问题,不是决定留下它。** 30 成员测量已经判定这个 +借来的名字代价是四个成员:`archive`、`sqlite3`、`mimalloc`、`c-ares` 各自停在 +`#include `,经由 `#if defined(_WIN32) || defined(__CYGWIN__)` 到达。上游用它 +表达的是「**Win32 可用**」——mimalloc 把话写在守卫自己的注释里,sqlite3 把它列进 +`SQLITE_OS_WIN`。**一个借来的名字,语义由借出方的历史决定**,不由借用方的意图决定。 + +撤掉它试过了,结果打断了上面那两个头——它们读它,正是因为没有别的 target-wide 名字可用。 +libunwind 的 `static_assert` 响亮地红了;`setjmp.h` 那一处不会——它自己的注释写着 +「a mismatch nothing reports until the record overruns」。所以撤销分三步,每个中间状态都能构建: + +1. 本版**增加** `__mcpp_target_windows__`——纯增量; +2. 那两个包改读新名字,保留 `|| defined(__CYGWIN__)` 以便在两种引擎上都能构建; +3. 之后的版本再停止定义借来的那个。 + +先做第三步,会让**已发布**的那两个头全部落进各自的 `#else`:错误的记录尺寸,而没有任何 +东西报告。 **`kernel-abi` 提供者的自身单元被推导落到平台边界上——它不必自己说出来** (mcpp 2026.9.18+,PR 进行中根据 openkal-musl 尖峰实验做的修订)。提供 diff --git a/src/toolchain/cenv.cppm b/src/toolchain/cenv.cppm index 6f56316a..6ef15170 100644 --- a/src/toolchain/cenv.cppm +++ b/src/toolchain/cenv.cppm @@ -28,9 +28,9 @@ // 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 `-U__CYGWIN__ -U__CYGWIN32__` (see -// the note below the table — measurement -// returned this to the original §3.3 text) +// plus `-D__mcpp_target_windows__`; +// `__CYGWIN__` stays defined for now (see +// the note below the table — a sequence) // * 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,27 +70,37 @@ // 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 REMOVED, AND THIS MODULE HAS HELD BOTH -// ANSWERS. The first version removed them; a second kept them, on the -// argument that third-party code needing to know the OBJECT FORMAT has no -// other name for "PE format with a POSIX-presenting C environment". That -// version wrote the condition for changing its mind: a trade-off "for the -// 30-member measurement to settle — if defining it produces more new -// failures than it fixes, the answer flips". +// `__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. // -// The measurement settled it (2026-09-20, 60 member-target combinations). -// Keeping them cost four members, each stopping at `#include ` -// reached through `#if defined(_WIN32) || defined(__CYGWIN__)`; none failed -// for want of the macro. Four against zero, so the answer flipped. +// 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 +// defining it produces more new failures than it fixes, the answer flips". // -// What the second version got wrong is worth keeping, because it is not -// specific to this macro. The name was borrowed to mean "the object format -// is PE". Upstream uses it to mean "Win32 is available" — mimalloc says so in -// the guard itself, and sqlite3 puts it in the `SQLITE_OS_WIN` detection set. -// A BORROWED NAME MEANS WHAT THE LENDER'S HISTORY MADE IT MEAN, not what the -// borrower intended. The object-format question therefore keeps no macro at -// all: a package asks `cfg(os = "windows")`. If some day a third party can -// only ask it in the preprocessor, mcpp defines a name of its own. +// 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 — +// 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 +// 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". +// +// 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. export module mcpp.toolchain.cenv; import std; @@ -289,38 +299,60 @@ inline std::expected realise( "on x86_64 only; this arch has no verified " "substitute triple"); // `--target=x86_64-pc-cygwin`, on the COMPILE line only (module - // header above), AND `__CYGWIN__`/`__CYGWIN32__` UNDEFINED ON TOP - // OF IT. The triple is what suppresses `_WIN32`, gives `__unix__` - // and gives LP64; the two `-U` tokens remove the one thing the - // triple carries that this environment cannot honour. + // header above), PLUS `-D__mcpp_target_windows__`. + // + // THE SUBSTITUTION HIDES THE TARGET FROM THE PREPROCESSOR, AND + // SOMETHING HAS TO SAY IT ANYWAY. `_WIN32`/`_WIN64` are what + // ordinary code reads to learn that the calling convention is + // Win64 and the object format PE, and this realisation suppresses + // them on purpose --- that is the whole point of presenting + // POSIX. But the ABI did not change with the environment: the + // register save areas are still Win64's. + // + // Two INSTALLED headers in this ecosystem size records by that + // fact and cannot read a package-private define, because an + // application's own compile reads them: + // + // openkal-musl `port/include/bits/setjmp.h` -> jmp_buf + // openkal-llvm-runtime `__libunwind_config.h` -> unw_context_t + // + // Both currently read `__CYGWIN__`, for want of any other name + // mcpp keeps defined target-wide. Getting it wrong is silent: + // setjmp.h's own comment says "a mismatch nothing reports until + // the record overruns", and libunwind's case was a write through + // a register read from the wrong save slot on the first `throw`. + // + // So mcpp states it itself. `__mcpp_target_windows__` answers the + // question those headers actually ask --- is this target Windows, + // whatever C environment is presented on top --- and it is mcpp's + // own name, so its meaning is not decided by anyone else's + // history. It is emitted only here, where the target's own macros + // are suppressed; an ordinary Windows build still has `_WIN64`. // - // THE MEASUREMENT THIS WAS LEFT TO HAS SETTLED IT (2026-09-20, 60 - // member-target combinations on the published graph). Defining - // them cost four members --- archive, sqlite3, mimalloc, c-ares - // --- each stopping at `#include ` reached through a - // guard of the shape `#if defined(_WIN32) || defined(__CYGWIN__)`. - // Nothing in the same run failed for want of the macro. The - // module header's condition was "if defining it produces more new - // failures than it fixes, the answer flips": four against zero. + // `__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: // - // Upstream says plainly what it means by the name. mimalloc: - // `// we use windows locks on cygwin, but otherwise treat it at - // unix`. sqlite3 lists it in the `SQLITE_OS_WIN` detection set and - // then includes `windows.h`. The name asserts WIN32 IS AVAILABLE, - // not "the object format is PE" --- a borrowed name means what the - // lender's history made it mean, whatever we intended by it. + // 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 object-format question keeps no macro. A package asks - // `cfg(os = "windows")`, which needs none. Should measurement ever - // show third-party code that can only ask it in the preprocessor, - // the answer is a name mcpp defines itself, not one it borrows. + // 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. r.tokens.push_back("--target=x86_64-pc-cygwin"); - r.tokens.push_back("-U__CYGWIN__"); - r.tokens.push_back("-U__CYGWIN32__"); + r.tokens.push_back("-D__mcpp_target_windows__=1"); r.expectDefined.push_back("__unix__"); + r.expectDefined.push_back("__CYGWIN__"); + r.expectDefined.push_back("__mcpp_target_windows__"); 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/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 22758d3d..b7b25351 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", - "-U__CYGWIN__", "-U__CYGWIN32__") + "-D__mcpp_target_windows__=1") if tok not in consumer] if missing: print(f"FAIL: ordinary package is missing realised tokens {missing}\n args: {consumer}") @@ -201,7 +201,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", - "-U__CYGWIN__", "-U__CYGWIN32__") + "-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/unit/test_cenv.cpp b/tests/unit/test_cenv.cpp index 828f214e..16aaffd8 100644 --- a/tests/unit/test_cenv.cpp +++ b/tests/unit/test_cenv.cpp @@ -64,45 +64,46 @@ TEST(CEnv, MacosPosixArchDefaultDefinesUnix) { // The flagship case: Cygwin-flavoured Windows. // -// `__CYGWIN__`/`__CYGWIN32__` ARE UNDEFINED, and this test has asserted both -// answers. The triple is what suppresses `_WIN32`, supplies `__unix__` and -// supplies LP64; the two `-U` tokens remove the one thing it carries that -// this environment cannot honour. +// THE SUBSTITUTION HIDES THE TARGET, SO mcpp STATES IT. 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 +// (`openkal-musl`'s `bits/setjmp.h` for `jmp_buf`, +// `openkal-llvm-runtime`'s `__libunwind_config.h` for `unw_context_t`). +// Installed headers are read by an application's own compile, so a +// package-private define cannot reach them. `__mcpp_target_windows__` is +// mcpp's own name for the question they ask. // -// A middle revision kept them defined, so that third-party code needing to -// know the OBJECT FORMAT would have a name for "PE format, POSIX-presenting -// environment", and wrote its own condition for reversal: a trade-off for the -// 30-member measurement to settle. The measurement settled it on 2026-09-20 -// across 60 member-target combinations. Keeping them cost four members --- -// archive, sqlite3, mimalloc, c-ares --- each stopping at -// `#include ` reached through `#if defined(_WIN32) || -// defined(__CYGWIN__)`. Nothing failed for want of the macro. Four to zero. -// -// The general lesson, which outlives this macro: upstream uses the name to -// mean "Win32 is available" (mimalloc says so in the guard; sqlite3 lists it -// under `SQLITE_OS_WIN`), not "the object format is PE". A BORROWED NAME -// MEANS WHAT THE LENDER'S HISTORY MADE IT MEAN. The object-format question -// keeps no macro: a package asks `cfg(os = "windows")`. +// `__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. 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")); - EXPECT_TRUE(has(r->tokens, "-U__CYGWIN__")); - EXPECT_TRUE(has(r->tokens, "-U__CYGWIN32__")); + EXPECT_TRUE(has(r->tokens, "-D__mcpp_target_windows__=1")); + // Not withdrawn yet, and asserted so, 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__")); // 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); EXPECT_EQ(r->expectWcharBits, 32); ASSERT_TRUE(has(r->expectDefined, "__unix__")); ASSERT_TRUE(has(r->expectUndefined, "_WIN32")); - // THE PROBE CHECKS THEM, which is what keeps this from being a token the - // compiler could ignore: `cenv_probe::verify` compares the realised + // 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 `-U` that did not take effect is a verification failure, + // mismatch. A `-D` that did not take effect is a verification failure, // not a silent one. - ASSERT_TRUE(has(r->expectUndefined, "__CYGWIN__")); - ASSERT_TRUE(has(r->expectUndefined, "__CYGWIN32__")); + ASSERT_TRUE(has(r->expectDefined, "__CYGWIN__")); + ASSERT_TRUE(has(r->expectDefined, "__mcpp_target_windows__")); } // wchar = 16 on the Cygwin substitution matches Cygwin's own default, so no From 1bd80e85bdcb5136ff5ac8ea82a2300447c18869 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 02:30:51 +0800 Subject: [PATCH 4/5] the macros this engine defines are a module, so the rule cannot drift `src/toolchain/predefines.cppm` is the specification and the implementation of one thing. The contract is data in that module (`kContract`), the emission is a function beside it (`define_tokens`), and `tests/unit/test_predefines.cpp` asserts the two agree in BOTH directions: a macro emitted and unlisted is a promise nobody can rely on, a row listing a macro nothing emits is one a reader waits for forever. This wave already paid once for a rule kept in a document while the code moved --- the reason-token table was "the four it was missing" twice over. GENERALISED PAST WINDOWS. `__mcpp_target___` is spelt from the triple's own `os` field, so the ENGINE LEARNS NO OPERATING-SYSTEM NAME and a target added to the triple parser gets its macro with no change here --- the same discipline `[kernel-abi]` interface names follow. Measured: `x86_64-linux-gnu` gives `__mcpp_target_linux__`, `x86_64-windows-gnu` gives `__mcpp_target_windows__`, `riscv64-none-elf` gives `__mcpp_target_none__`. DEFINED ALWAYS, not only where a realisation suppressed something. Conditional emission would make its absence ambiguous: "not Windows" and "Windows, but nothing hid its macros" would read the same, which is the shape of every defect where a "no" and a "never asked" share a reading. Lowercase, `__mcpp_`-prefixed. Two conventions exist --- vendor and product names upper (`__APPLE__`, `_WIN32`), kind-of-system names lower (`__linux__`, `__unix__`) --- and these name kinds of target, sitting beside the second family in real guards. The prefix is load-bearing: a name mcpp owns means what mcpp says it means, which is exactly what `__CYGWIN__` could not offer. `__openkal__` joins the same contract; `__unix__` is listed as SUPPLIED rather than owned, so it keeps the standard spelling and mcpp may not redefine it. THE SEPARATION IS ITSELF A TESTED PROPERTY. The macro was first realised in `mcpp.toolchain.cenv`, which made it derived from a declaration --- yet whether a target is Windows does not depend on any `[c-abi]` block existing. Moving it out turned the cenv test red, correctly; that test now asserts the realisation does NOT carry it, because a token with two owners is a token that will disagree with itself. docs/21 renders the contract in both languages, and docs/24's note that the `__CYGWIN__` trade-off "may flip" is updated: it has, and the replacement has a name. --- CHANGELOG.md | 20 +++ docs/21-the-target-triple.md | 75 +++++++++ docs/24-openkal-cross.md | 13 +- docs/zh/21-the-target-triple.md | 56 +++++++ docs/zh/24-openkal-cross.md | 10 +- src/build/prepare.cppm | 33 +++- src/toolchain/cenv.cppm | 41 ++--- src/toolchain/predefines.cppm | 142 ++++++++++++++++++ ...i_environment_declared_by_the_c_library.sh | 5 + tests/unit/test_cenv.cpp | 28 ++-- tests/unit/test_predefines.cpp | 133 ++++++++++++++++ 11 files changed, 501 insertions(+), 55 deletions(-) create mode 100644 src/toolchain/predefines.cppm create mode 100644 tests/unit/test_predefines.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 399df486..d8ad1587 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,26 @@ ## [2026.9.21.1] - 2026-09-21 +### 引擎定义的宏成为一份规范,而规范就是那个模块 + +`src/toolchain/predefines.cppm` 同时是契约与实现:契约是模块里的数据(`kContract`), +发出是它旁边的函数(`define_tokens`),`tests/unit/test_predefines.cpp` **双向**断言两者 +一致——发出去却不在表里、或在表里却没人发,都让构建变红。规范与实现分处两地就会漂移, +这一点本轮已经在理由令牌表上付过一次学费。 + +**`__mcpp_target___`,覆盖所有平台而不只是 Windows。** 拼法取自三元组自己的 `os` +字段,**引擎不认识任何操作系统名**——三元组解析器新增一个目标,它的宏随之存在。实测: +`x86_64-linux-gnu` → `__mcpp_target_linux__`;`x86_64-windows-gnu` → +`__mcpp_target_windows__`;`riscv64-none-elf` → `__mcpp_target_none__`。 + +**总是定义,不只在有东西被压掉时。** 条件式发出会让缺席含义不唯一:「不是 Windows」与 +「是 Windows 但没有东西压掉它的宏」会读成同一件事。 + +**命名:小写,`__mcpp_` 前缀。** 业界并存两套约定——厂商与产品名大写(`__APPLE__`、 +`_WIN32`),系统种类名小写(`__linux__`、`__unix__`)——这些命名的是目标种类,在真实守卫里 +与第二族并排。`__mcpp_` 前缀承重:**一个 mcpp 拥有的名字,语义由 mcpp 自己定。** +`__openkal__` 收进同一份契约;`__unix__` 列在表里但标注「供给而非拥有」,保持标准拼法。 + ### mcpp 为「这个目标是 Windows」给出自己的名字 `presents = "posix"` 在 Windows 上实现成 Cygwin 形状的目标,有意压掉 `_WIN32`——那正是 diff --git a/docs/21-the-target-triple.md b/docs/21-the-target-triple.md index 1d8bd264..6305a0c6 100644 --- a/docs/21-the-target-triple.md +++ b/docs/21-the-target-triple.md @@ -611,6 +611,81 @@ produced rows* and *every row the expected table names was reached*, rather than a total: a cell that vanishes because a payload was not restored is otherwise indistinguishable from a cell that passed. +## The macros mcpp defines + +`src/toolchain/predefines.cppm` is the specification and the implementation of +this table at once: the contract is data in that module, the emission is a +function beside it, and `tests/unit/test_predefines.cpp` asserts the two agree +in both directions. A macro emitted and unlisted, or listed and never emitted, +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 | +| `__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 +package states what it needs in its manifest and the engine answers by +RESOLUTION — `cfg(os = "windows")`, `cfg(c-abi = "musl")`, a capability, a +feature. That path is testable, reportable, and visible to a reader of the +manifest; a macro is none of those. Two justifications have survived: + +1. **The source is not ours to edit** and asks in the preprocessor. Upstream C + selects platform behaviour with `#if`, and no manifest key reaches inside a + third-party `.c`. +2. **The reader is an installed header.** A package's own build defines can be + 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 +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. + +*Allowed:* learning the target's operating system where the C environment +presented above it has suppressed the platform's own macros, and sizing a +record by the target's ABI. A Windows target presenting POSIX has no `_WIN32` +on purpose — while the calling convention is still Win64, which is what +`openkal-musl`'s `bits/setjmp.h` and `openkal-llvm-runtime`'s +`__libunwind_config.h` each size a record by. + +*Forbidden:* selecting a header the manifest could select, or standing in for +`cfg(os = …)` in a package this project controls. + +**It is defined always, not only where something is suppressed.** Conditional +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. + +`__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. + ## Custom Targets diff --git a/docs/24-openkal-cross.md b/docs/24-openkal-cross.md index 247f8da5..a2b69875 100644 --- a/docs/24-openkal-cross.md +++ b/docs/24-openkal-cross.md @@ -57,10 +57,15 @@ combination, "PE format with a POSIX-presenting C environment," is `__CYGWIN__`/`__CYGWIN32__`, which the Cygwin-flavoured realisation 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 (it is not a settled fact, and may flip). Reading it -as a fourth C-environment macro, rather than as what it actually is — an -object-format fact `__CYGWIN__` happens to be the only name for — is -exactly the shape of confusion this whole section exists to head off. +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 +(`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 +is then withdrawn. Reading either as a fourth C-environment macro, rather +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 unit, whenever the resolved `kernel-abi` layer's interface name is diff --git a/docs/zh/21-the-target-triple.md b/docs/zh/21-the-target-triple.md index 686f7d79..d1a92825 100644 --- a/docs/zh/21-the-target-triple.md +++ b/docs/zh/21-the-target-triple.md @@ -545,6 +545,62 @@ x86_64 宿主行得通的路线,而平台模拟器会直接拒绝异构 guest 行都被跑到**,而不是一个总数:一格因为载荷没被恢复而消失,与一格通过了,在退出码 上没有区别。 +## mcpp 定义的宏 + +`src/toolchain/predefines.cppm` 同时是这张表的规范与实现:契约是那个模块里的数据, +发出是它旁边的函数,而 `tests/unit/test_predefines.cpp` 双向断言两者一致。发出去却不在 +表里、或在表里却没人发,都会让构建变红,而不是漂进一次发布。 + +| 宏 | 何时定义 | 是否 mcpp 拥有 | +|---|---|---| +| `__mcpp_target___` | 每次构建一个,拼法取自三元组的 `os` 字段 | 是 | +| `__openkal__` | 解析出的 `kernel-abi` 层接口名是 `openkal` | 是 | +| `__unix__` | `[c-abi]` 实现供给它,在工具链不会定义它的地方 | 否 | + +**引擎原则上不该定义任何宏,每一行都要为自己辩护。** 包在清单里陈述需要什么,引擎以 +**解析**回答——`cfg(os = "windows")`、`cfg(c-abi = "musl")`、能力、feature。那条路可测、 +可报告、读清单就能看见;宏三样都不是。只有两条理由通过了: + +1. **源码不归我们改**,而它在预处理期发问。上游 C 用 `#if` 选平台行为,任何清单键都伸不进 + 第三方的 `.c` 里。 +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__`。引擎不认识任何操作系统名:三元组解析器新增一个目标,它的宏随之 +存在,引擎无需改动。 + +*允许:* 当上面呈现的 C 环境压掉了平台自己的宏时,用它得知目标的操作系统;以及按目标的 +ABI 给记录定尺寸。呈现 POSIX 的 Windows 目标**有意**没有 `_WIN32`,而调用约定仍是 Win64 +——那正是 `openkal-musl` 的 `bits/setjmp.h` 与 `openkal-llvm-runtime` 的 +`__libunwind_config.h` 各自据以定尺寸的事实。 + +*禁止:* 选一个清单本可以选的头;或在本项目控制得了的包里用它代替 `cfg(os = …)`。 + +**它总是被定义,不只在有东西被压掉时。** 条件式发出会让它的**缺席**含义不唯一: +「不是 Windows」与「是 Windows,但没有东西压掉它的宏」会读成同一件事。一个缺席只有一种 +含义的宏,值一个 `-D`。 + +**命名。** `__mcpp_` 前缀、`__` 后缀、小写、单词用 `_` 分隔。业界并存两套约定——厂商与 +产品名用大写(`__APPLE__`、`_WIN32`、`__MINGW32__`),系统种类名用小写(`__linux__`、 +`__unix__`)——而这些命名的是目标的种类,在真实守卫里与第二族并排出现: +`#if defined(__linux__) || defined(__mcpp_target_windows__)`。 + +`__mcpp_` 这个前缀是承重的。**一个 mcpp 拥有的名字,语义由 mcpp 自己定。** 另一条路试过: +`__CYGWIN__` 曾被保留定义,为的是让需要「PE 目标文件格式 + POSIX C 环境」的代码有个名字, +而 30 成员测量发现四个成员把它读成「**Win32 可用**」并走向 `#include `——那正是 +上游用它表达的意思。**一个借来的名字,语义由借出方的历史决定**,不由借用方的意图决定。 + +`__unix__` 是印证这条规则的例外:mcpp **供给**它而不拥有它,所以它保持标准拼法,mcpp 也 +不得改变它的含义。读它就按在任何别的 POSIX 系统上那样读。 + +**稳定性。** 这张表里的一条是**已发布的接口**。删掉一条、或收窄它被定义的条件,对本项目 +控制不了的源码就是破坏性变更,而失败通常是**静默**的——一个 `#if` 选了另一条分支,照常 +编译。所以撤销永远是一个**序列**,不是一次编辑:先加替代品,让消费者迁移过去并仍接受旧 +名字,之后才停止定义它。 + ## 自定义目标 diff --git a/docs/zh/24-openkal-cross.md b/docs/zh/24-openkal-cross.md index 30b8970e..87c16442 100644 --- a/docs/zh/24-openkal-cross.md +++ b/docs/zh/24-openkal-cross.md @@ -45,9 +45,13 @@ C++ 运行时 —— 成为一组由依赖图解析、并由当前运行的编 代码里,给这个特定组合——「PE 格式 + 呈现 POSIX 的 C 环境」——起名字的唯一办法,是 `__CYGWIN__`/`__CYGWIN32__`,所以 Cygwin 式的实现让它们保持**定义**,而不是把它折进上面 三行里的任何一行——完整的权衡说明见 -[22 自己的说明](22-target-side.md#c-abi-包陈述它呈现的-c-环境mcpp-2026918)(不是已经定论 -的事实,结论可能翻过来)。把它当成第四个 C 环境宏来读,而不是它实际是的样子——一个 -`__CYGWIN__` 恰好是唯一名字的目标文件格式事实——正是这一整节想要提前避免的那种混淆。 +[22 自己的说明](22-target-side.md#c-abi-包陈述它呈现的-c-环境mcpp-2026918)。自 +2026.9.21.1 起,`__CYGWIN__` 不再是那个事实的唯一名字:mcpp 为每个目标定义 +`__mcpp_target___`(见 `docs/21`「mcpp 定义的宏」),于是需要在被呈现的环境之下得知 +目标的源码,有了一个 mcpp 自己拥有的名字。`__CYGWIN__` 在生态的已安装头迁移到新名字期间 +仍然定义着,之后才撤销。把两者中的任何一个当成第四个 C 环境宏来读,而不是它们实际是的 +样子——一个关于**目标**的事实,而不是关于其上呈现的环境——正是这一整节想要提前避免的 +那种混淆。 **`__openkal__` 的规则。** 只要解析出的 `kernel-abi` 层的接口名是 `openkal`,引擎就为目标侧的 每一个编译单元定义它——取自层的取值,绝不取自包名,因此第二个 `mcpp:kernel-abi=openkal` 的 diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index b5269941..285557da 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -35,6 +35,7 @@ import mcpp.toolchain.clang; import mcpp.toolchain.hostflags; // the compile-token producer the package std module reuses import mcpp.toolchain.cenv; // [c-abi] declaration → compiler configuration (design 2026-09-18) import mcpp.toolchain.cenv_probe; // [c-abi] declaration is checked, not trusted (design §3.2) +import mcpp.toolchain.predefines; // the macros this engine defines: contract and emission in one module import mcpp.toolchain.cppfly; import mcpp.toolchain.detect; import mcpp.toolchain.dialect; @@ -11146,14 +11147,36 @@ prepare_build(bool print_fingerprint, // `-fno-builtin-memset_pattern16`) and none is rejected — so nothing // here is filtered a second time; if a future token IS GAS-hostile, // `cenv::realise` is where to split it, not this broadcast. + // THE MACROS THIS ENGINE DEFINES --- the contract, the rules for + // reading them and the reason each one exists rather than a manifest + // key, are `mcpp.toolchain.predefines`. That module is the + // specification and the implementation of the same thing, so this + // site decides only WHERE the tokens go, never WHICH they are. + if (tc) { + std::string targetOs; + if (auto ttOs = mcpp::toolchain::triple::parse(tc->targetTriple)) + targetOs = ttOs->os; + const auto engineDefines = + mcpp::toolchain::predefines::define_tokens( + targetOs, tc->kernelAbiIsOpenkal); + // Into `cflags`/`cxxflags` only: these are all `-D`, and the + // channel that builds an assembly unit's flags keeps the -D/-U/-I + // words of those two (`compile_commands::unit_asm_flags`), so a + // second copy here would put each one on a `.S` line twice. + for (auto& p : packages) { + appendUniqueFlags(p.privateBuild.cflags, engineDefines); + appendUniqueFlags(p.privateBuild.cxxflags, engineDefines); + } + } + if (tc && (tc->kernelAbiIsOpenkal || !tc->cEnvTokens.empty() || !tc->cEnvBuiltinsTokens.empty())) { - static const std::vector kOpenkalDefine = {"-D__openkal__"}; for (auto& p : packages) { - if (tc->kernelAbiIsOpenkal) { - appendUniqueFlags(p.privateBuild.cflags, kOpenkalDefine); - appendUniqueFlags(p.privateBuild.cxxflags, kOpenkalDefine); - } + // `__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 + // see, not about whether its code may call `kal_*`. if (p.manifest.cEnvironment == "platform") continue; appendUniqueFlags(p.privateBuild.cflags, tc->cEnvTokens); appendUniqueFlags(p.privateBuild.cxxflags, tc->cEnvTokens); diff --git a/src/toolchain/cenv.cppm b/src/toolchain/cenv.cppm index 6ef15170..3872e752 100644 --- a/src/toolchain/cenv.cppm +++ b/src/toolchain/cenv.cppm @@ -299,35 +299,18 @@ inline std::expected realise( "on x86_64 only; this arch has no verified " "substitute triple"); // `--target=x86_64-pc-cygwin`, on the COMPILE line only (module - // header above), PLUS `-D__mcpp_target_windows__`. + // header above). // - // THE SUBSTITUTION HIDES THE TARGET FROM THE PREPROCESSOR, AND - // SOMETHING HAS TO SAY IT ANYWAY. `_WIN32`/`_WIN64` are what - // ordinary code reads to learn that the calling convention is - // Win64 and the object format PE, and this realisation suppresses - // them on purpose --- that is the whole point of presenting - // POSIX. But the ABI did not change with the environment: the - // register save areas are still Win64's. - // - // Two INSTALLED headers in this ecosystem size records by that - // fact and cannot read a package-private define, because an - // application's own compile reads them: - // - // openkal-musl `port/include/bits/setjmp.h` -> jmp_buf - // openkal-llvm-runtime `__libunwind_config.h` -> unw_context_t - // - // Both currently read `__CYGWIN__`, for want of any other name - // mcpp keeps defined target-wide. Getting it wrong is silent: - // setjmp.h's own comment says "a mismatch nothing reports until - // the record overruns", and libunwind's case was a write through - // a register read from the wrong save slot on the first `throw`. - // - // So mcpp states it itself. `__mcpp_target_windows__` answers the - // question those headers actually ask --- is this target Windows, - // whatever C environment is presented on top --- and it is mcpp's - // own name, so its meaning is not decided by anyone else's - // history. It is emitted only here, where the target's own macros - // are suppressed; an ordinary Windows build still has `_WIN64`. + // THE SUBSTITUTION HIDES THE TARGET FROM THE PREPROCESSOR, and + // 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, + // 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 @@ -348,10 +331,8 @@ inline std::expected realise( // reported by nothing. A loud failure in four third-party members // is the better state to hold for one release. r.tokens.push_back("--target=x86_64-pc-cygwin"); - r.tokens.push_back("-D__mcpp_target_windows__=1"); r.expectDefined.push_back("__unix__"); r.expectDefined.push_back("__CYGWIN__"); - r.expectDefined.push_back("__mcpp_target_windows__"); r.expectUndefined.push_back("_WIN32"); cygwinIdentity = true; } else if (decl.presents == CAbiPresents::Windows) { diff --git a/src/toolchain/predefines.cppm b/src/toolchain/predefines.cppm new file mode 100644 index 00000000..e44e1db3 --- /dev/null +++ b/src/toolchain/predefines.cppm @@ -0,0 +1,142 @@ +// THE MACROS mcpp ITSELF DEFINES, AND THE RULES FOR READING THEM. +// +// This module is the specification and the implementation of the same thing. +// The contract is `kContract` below --- data, enumerable, checked --- and the +// emission is `define_tokens`. `docs/21` renders the table for a reader; +// `tests/unit/test_predefines.cpp` asserts the two halves agree, so a macro +// added to one and not the other fails a build rather than drifting into a +// release. +// +// ── WHY AN ENGINE DEFINES ANY MACRO AT ALL ───────────────────────────────── +// +// Normally it should not. A package states what it needs in its manifest and +// the engine answers by RESOLUTION: `cfg(os = "windows")`, `cfg(c-abi = +// "musl")`, a capability, a feature. That path is testable, reportable, and +// visible to a reader of the manifest. A macro is none of those things, so +// every entry here has to justify itself against the manifest alternative. +// +// Exactly two justifications have survived: +// +// 1. THE SOURCE IS NOT OURS TO EDIT, and asks in the preprocessor. Upstream +// C code selects platform behaviour with `#if`, and no manifest key can +// reach inside a third-party `.c` file. +// +// 2. THE READER IS AN INSTALLED HEADER. A package's own build defines can +// be spelled in its manifest, but a header it INSTALLS is read by an +// application's own compile, which those defines never reach. Two such +// headers exist in this ecosystem and both size records by the target's +// ABI: `openkal-musl`'s `bits/setjmp.h` (`jmp_buf`) and +// `openkal-llvm-runtime`'s `__libunwind_config.h` (`unw_context_t`). +// +// ── 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 +// 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 (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. +// +// ── 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. +export module mcpp.toolchain.predefines; + +import std; + +export namespace mcpp::toolchain::predefines { + +// What decides whether an entry is defined for a given build. +enum class When { + EveryTarget, // always, for every target-side translation unit + TargetOs, // one per build, spelled from the triple's `os` field + ResolvedLayer, // the resolved layer's interface name selects it + Realisation, // emitted by the `[c-abi]` realisation, not by this module +}; + +struct Entry { + 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; + std::string_view forbidden; +}; + +// THE COMPLETE SET. A macro mcpp defines and this table omits is a promise +// nobody can rely on; a row naming a macro nothing emits is one a reader will +// wait for forever. `test_predefines.cpp` compares the two directions. +inline constexpr std::array kContract {{ + { + "__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", + "selecting a header that the manifest could select, or standing in " + "for `cfg(os = ...)` in a package this project controls", + }, + { + "__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 " + "around a missing SDK, or telling linux/windows/macos apart", + }, + { + "__unix__", When::Realisation, /*owned=*/false, + "reading it exactly as on any other POSIX system; mcpp SUPPLIES it " + "where the target's own toolchain would not, because `[c-abi] " + "presents = \"posix\"` says this environment is one", + "treating its presence as an mcpp-specific signal --- it is the " + "standard name, and mcpp neither owns it nor may change its meaning", + }, +}}; + +// THE TOKENS FOR ONE BUILD, for the entries this module emits. +// +// `When::Realisation` entries are absent by construction: they belong to +// `mcpp.toolchain.cenv`, which emits them as part of realising a declared +// `[c-abi]` block. They are listed in `kContract` because the contract is +// about what a READER may encounter, not about which module wrote it. +// +// NO OPERATING-SYSTEM NAME APPEARS IN THIS FUNCTION. The spelling comes from +// the triple's own `os` field, so a target added to the triple parser gets +// its macro with no change here --- the same discipline `[kernel-abi]` +// interface names follow. +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)); + if (kernelAbiIsOpenkal) + out.push_back("-D__openkal__"); + return out; +} + +// The spelling `kContract` uses, for one concrete target. Exists so a test +// 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); + return s; +} + +} // namespace mcpp::toolchain::predefines 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 b7b25351..94e6997d 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 @@ -169,6 +169,11 @@ consumer = joined(args_for("main.cpp")) missing = [tok for tok in ("--target=x86_64-pc-cygwin", "-fno-short-wchar", "-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 +# environment presented above it. It is asserted HERE, on a real compile +# line, because that is where the two channels meet and where a reader of +# this graph actually encounters it. if missing: print(f"FAIL: ordinary package is missing realised tokens {missing}\n args: {consumer}") sys.exit(1) diff --git a/tests/unit/test_cenv.cpp b/tests/unit/test_cenv.cpp index 16aaffd8..98a0ae4d 100644 --- a/tests/unit/test_cenv.cpp +++ b/tests/unit/test_cenv.cpp @@ -64,15 +64,16 @@ TEST(CEnv, MacosPosixArchDefaultDefinesUnix) { // The flagship case: Cygwin-flavoured Windows. // -// THE SUBSTITUTION HIDES THE TARGET, SO mcpp STATES IT. 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 -// (`openkal-musl`'s `bits/setjmp.h` for `jmp_buf`, -// `openkal-llvm-runtime`'s `__libunwind_config.h` for `unw_context_t`). -// Installed headers are read by an application's own compile, so a -// package-private define cannot reach them. `__mcpp_target_windows__` is -// mcpp's own name for the question they ask. +// THE SUBSTITUTION HIDES THE TARGET, AND WHAT STATES IT ANYWAY IS NOT HERE. +// 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 +// 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, @@ -87,9 +88,11 @@ TEST(CEnv, WindowsPosixArchDefaultSubstitutesTheCygwinTriple) { 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")); - EXPECT_TRUE(has(r->tokens, "-D__mcpp_target_windows__=1")); - // Not withdrawn yet, and asserted so, because the step that withdraws it - // must be a deliberate edit to this line rather than a silent drift. + // 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__")); // wchar 32 differs from Cygwin's own default (16) — the flag is added. EXPECT_TRUE(has(r->tokens, "-fno-short-wchar")); @@ -103,7 +106,6 @@ TEST(CEnv, WindowsPosixArchDefaultSubstitutesTheCygwinTriple) { // mismatch. A `-D` that did not take effect is a verification failure, // not a silent one. ASSERT_TRUE(has(r->expectDefined, "__CYGWIN__")); - ASSERT_TRUE(has(r->expectDefined, "__mcpp_target_windows__")); } // 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 new file mode 100644 index 00000000..e07bfa75 --- /dev/null +++ b/tests/unit/test_predefines.cpp @@ -0,0 +1,133 @@ +// The contract and the emission are two halves of one module, and this is +// what keeps them from drifting apart. +// +// `mcpp.toolchain.predefines` states the complete set of macros this engine +// defines (`kContract`) and produces the tokens for one build +// (`define_tokens`). A macro emitted and not listed is a promise nobody can +// rely on; a row listing a macro nothing emits is one a reader waits for +// forever. Both directions are asserted here rather than reviewed by eye, +// because a set compared by reading is a set compared by sampling --- this +// project has already shipped two tables that were "the ones it was missing" +// and were not. +#include + +#include +#include +#include + +import mcpp.toolchain.predefines; + +namespace pd = mcpp::toolchain::predefines; + +namespace { + +// `-D__name__=1` or `-D__name__` -> `__name__` +std::string macro_of(std::string_view token) { + std::string s(token); + if (s.starts_with("-D")) s.erase(0, 2); + if (const auto eq = s.find('='); eq != std::string::npos) s.erase(eq); + return s; +} + +std::vector emitted(std::string_view os, bool openkal) { + std::vector out; + for (auto const& t : pd::define_tokens(os, openkal)) out.push_back(macro_of(t)); + std::ranges::sort(out); + return out; +} + +bool listed(std::string_view name, std::string_view os) { + return std::ranges::any_of(pd::kContract, [&](pd::Entry const& e) { + return pd::spelling_for(e, os) == name; + }); +} + +} // namespace + +TEST(Predefines, EveryEmittedMacroIsInTheContract) { + for (auto const& os : {"linux", "windows", "macos", "ios", "android", "none"}) + for (bool openkal : {false, true}) + for (auto const& m : emitted(os, openkal)) + EXPECT_TRUE(listed(m, os)) + << m << " is emitted for os=" << os + << " openkal=" << openkal << " and kContract does not list it"; +} + +TEST(Predefines, EveryContractRowThisModuleEmitsIsReachable) { + // `Realisation` rows are emitted by `mcpp.toolchain.cenv`, deliberately, + // and are listed here because the contract is about what a READER may + // encounter rather than about which module wrote it. + for (auto const& e : pd::kContract) { + if (e.when == pd::When::Realisation) continue; + bool reached = false; + for (auto const& os : {"linux", "windows", "macos", "ios", "android", "none"}) + for (bool openkal : {false, true}) { + auto ms = emitted(os, openkal); + if (std::ranges::find(ms, pd::spelling_for(e, os)) != ms.end()) + reached = true; + } + EXPECT_TRUE(reached) + << e.spelling << " is in kContract and no configuration emits it"; + } +} + +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"); + EXPECT_EQ(pd::define_tokens("none", false).front(), + "-D__mcpp_target_none__=1"); +} + +TEST(Predefines, OneMacroPerTargetAndItIsAlwaysPresent) { + // Conditional emission would make absence ambiguous: "not Windows" and + // "Windows, but nothing suppressed its macros" would read the same. + 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_"); + }); + EXPECT_EQ(n, 1) << "os=" << os; + } +} + +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()); + } +} + +TEST(Predefines, AnEmptyTargetOsEmitsNoTargetMacro) { + // 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 + // written, and the rest follow it. + for (auto const& e : pd::kContract) { + EXPECT_FALSE(e.allowed.empty()) << e.spelling; + EXPECT_FALSE(e.forbidden.empty()) << e.spelling; + } +} + +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. + 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_")) + << e.spelling << " is supplied rather than owned, so it keeps " + "the standard spelling"; + } +} From ac073140cb798d0054fa69ba7549de5353fd8bcf Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Mon, 21 Sep 2026 02:44:18 +0800 Subject: [PATCH 5/5] C4 does not reproduce, and the belief that blocked measuring it was wrong `aarch64-macos --profile release` over the openkal stack links, and the artefact references no `memset_pattern` symbol at all. Measured here, on this Linux host, with mcpp 2026.9.21.1 and llvm@22.1.8 through openkal-macos 0.12.0 / openkal-musl 0.18.0 / openkal-llvm-runtime 0.13.0: zstd 1.5.7 compiles, links; Mach-O 64-bit arm64, NOUNDEFS; 0 refs xz 5.8.3 compiles, links Both are on the chain the report named. The engine has emitted `-fno-builtin-memset_pattern16` for `builtins = "iso"` on macOS since before this branch, and that report was taken on an engine that did not. Nothing is implemented for C4, and nothing should be: adding the symbol to openkal-musl would have repaired a gap that does not exist. THE BELIEF THAT KEPT THIS UNMEASURED IS THE MORE USEFUL FINDING. Both this plan and the previous wave's self-review stated that macOS facts need a macOS runner, because a probe targeting `aarch64-macos` failed here. That probe declared no openkal dependency, so it took the PLATFORM path, which does need an Apple SDK. The openkal path does not --- universal cross-building is the premise of the whole system, and a Linux host reaches `arm64-apple-macos14.0` through it with every layer resolved from the graph. The cost of that belief compounded: it filed C4 as unmeasurable, and it filed openkal-macos's `provides-interfaces` as needing a runner when 0.12.0 derived it mechanically. Both corrected, with the rule beside them --- a claim that some platform cannot be measured locally has to be tested with a probe that goes THROUGH the stack under test, not one that bypasses it. --- .agents/docs/2026-09-20-wave-self-review.md | 7 ++++- ...kal-ecosystem-completion-and-acceptance.md | 31 ++++++++++++++----- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/.agents/docs/2026-09-20-wave-self-review.md b/.agents/docs/2026-09-20-wave-self-review.md index d3ebd9a9..0e55a95d 100644 --- a/.agents/docs/2026-09-20-wave-self-review.md +++ b/.agents/docs/2026-09-20-wave-self-review.md @@ -107,5 +107,10 @@ status: active 最大的偏差。 2. **P3 撤 `__CYGWIN__`。** 判据已由 mimalloc 与 sqlite3 给出,代价需一次重测。 3. **`__cxa_thread_atexit`。** 本轮测量新发现的 C++ 运行时缺口。 -4. **openkal-macos 的 `provides-interfaces`。** 无法在 Linux 宿主派生,需 macOS runner。 +4. **openkal-macos 的 `provides-interfaces`。** 已在 0.12.0 关闭。 + **「无法在 Linux 宿主派生,需 macOS runner」这句判断是错的**——openkal 的前提就是通用 + 交叉构建,Linux 宿主经 openkal 栈可以构建 `aarch64-macos`(实测:`kernel-abi` + openkal-macos、`c-abi` musl、`c++-abi` libc++ 全部解析,产出 Mach-O arm64)。需要 + Apple SDK 的是**平台路径**,不是 openkal 路径。这条错误认知一度把 C4 也挡在"本机不可测" + 之外。 5. **P4 / P6。** 独立工作量。 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 18f647e8..0b20cc7d 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 @@ -243,11 +243,28 @@ macOS,缺 SDK): `-fno-builtin-` 家族是一个 idiom 一个 flag;但报告点名的确实是 `16`; 3. `-fno-builtin-memset_pattern16` 不足以关掉 LLVM 的 loop-idiom pass。 -**下一步不是实现符号,是取一次当前栈上的读数。** `lsp-mcpp-private` 的 -`aarch64-macos --profile release` 会直接给出答案;若仍红,第 2、3 种可能各有明确的 -后续动作(补发另外两个 flag / 由 C 库提供该符号)。 +**读数已取到,C4 关闭:在当前栈上不复现。** -**归属**:待定。**在读数出来之前给 openkal-musl 加符号,是在修一个可能不存在的缺口。** +而且**是在本机取到的**——这推翻了本文另一处判断。openkal 的前提就是通用交叉构建,Linux +宿主经 openkal 栈可以构建 `aarch64-macos`:需要 Apple SDK 的是**平台路径**,不是 openkal +路径。我先前那个失败的探针没有依赖 openkal,于是走了平台路径,我把它读成了"macOS 本机 +测不了"。 + +实测(mcpp 2026.9.21.1、llvm@22.1.8、`--profile release`、`--target aarch64-macos`, +经 openkal-macos 0.12.0 / openkal-musl 0.18.0 / openkal-llvm-runtime 0.13.0): + +| 成员 | 读数 | +| --- | --- | +| zstd 1.5.7 | 编译、链接通过;产物 `Mach-O 64-bit arm64, NOUNDEFS`;`memset_pattern` 引用 **0** | +| xz 5.8.3 | 编译、链接通过 | + +两个都是报告点名的那条链上的。所以那份报告测的是**加入 `-fno-builtin-memset_pattern16` +之前**的引擎——三种可能里的第一种。 + +**归属**:无。不需要给 openkal-musl 加任何符号。 + +**留下的方法论**:一条"某平台本机测不了"的判断,要用**走 openkal 栈**的探针去验,不能用 +走平台路径的。两者对 SDK 的要求完全不同。 ### 2.3 实现侧(openkal-*) @@ -365,8 +382,8 @@ E1 (P3, 撤 __CYGWIN__) ├─ 解锁 30 成员测量的 4 个失败 └─ 解锁 lsp-mcpp-private 的 Windows 目标(经 xz) │ -C4 (memset_pattern16) —— 前提待重测;引擎已发 -fno-builtin-memset_pattern16 - └─ 可能根本不是阻塞。读数由 A1 的 macOS 腿给出 +C4 (memset_pattern16) —— 已关闭,本机实测不复现 + └─ 不是阻塞 │ └──> A1/A2/A3 三目标验收 <── 本方案的终点 │ @@ -468,7 +485,7 @@ openkal-musl 0.18.0,而 0.18.0 当时还没进索引,消费者自己的 CI | C1 | 五行最小复现 | 链接通过**且析构真的执行** | **第二层未定位** | | C2 | curl / cmp-module | 记为 `refused` 而非 `fails` | 无 | | C3 | `arc4random_buf` | 「应当有的符号」CI 断言 | 无 | -| C4 | `aarch64-macos --profile release` | 链接通过 | **前提待重测**(引擎已发抑制 flag,且实测到达了 zstd 的 29 个单元) | +| C4 | `aarch64-macos --profile release` | **已通过,本机实测** | 关闭;不复现 | | I1 | 写死的节点集合 | conformance 逐条断言 | 无 | | I2 | `presents = "windows"` 的 C 库 | 引擎改动数为 0 | 无 | | **A1** | 三目标 `mcpp build` | 全绿 | E1, C4 |