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 new file mode 100644 index 00000000..0b20cc7d --- /dev/null +++ b/.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md @@ -0,0 +1,521 @@ +--- +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__)`,撤销会**翻转**它的分支。基线已有,重测 +即可读出净值。 + +**先做的那一版被跨仓库交叉验证挡下了,这条记录比结论更值钱。** + +第一次实现就是直接撤:加 `-U__CYGWIN__ -U__CYGWIN32__`,`expectUndefined` 加两项。本机 +全绿,五个生态仓库里四个也绿——**openkal-llvm-runtime 红了**,libunwind 的 +`static_assert` 失败:`Registers_x86_64` 装不进 `unw_context_t`。 + +根因是生态里有**两个已安装的公开头**有意读 `__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 的寄存器保存区大小)。在这个目标上 +两者同变,但按消费者实际问的那个问题命名更诚实。 + +**第二、三步**:两个包改读新名字并保留 `|| defined(__CYGWIN__)`(在两种引擎上都能构建); +之后的版本再停止定义借来的那个。**先做第三步会让已发布的那两个头静默落进 `#else`。** + +#### 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 没有这个符号。这不是 +「程序调用了它」,是**编译器合成的调用**。 + +**本文初稿据此把它列为 openkal-musl 的缺口,并当成 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。 + +**读数已取到,C4 关闭:在当前栈上不复现。** + +而且**是在本机取到的**——这推翻了本文另一处判断。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-*) + +#### 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) —— 已关闭,本机实测不复现 + └─ 不是阻塞 + │ + └──> 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..d8ad1587 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,53 @@ ## [Unreleased] +## [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`——那正是 +「呈现 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 ### 校验探针量的是构建宿主,而不是它要核对的那个目标 @@ -100,7 +147,7 @@ requires-interfaces = ["openkal.fs", "openkal.net"] `refusal.cppm` 能发出的令牌与表里的行,并要求简体中文镜像携带同一个集合。 两个方向各去掉一条都会红。 -⚠️ 这张表的**列头**是 `| \`reason\` | |`——第一格里一个反引号名字,形状与下面每一行 +这张表的**列头**是 `| \`reason\` | |`——第一格里一个反引号名字,形状与下面每一行 完全相同。按「行首反引号名字」匹配会把 `reason` 当成一个令牌。行与列头的区别在**第二格 非空**,所以判据按性质挑对象,不按语法挑。 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/22-target-side.md b/docs/22-target-side.md index ff90536e..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 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; 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,20 +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 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. +**`__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/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/22-target-side.md b/docs/zh/22-target-side.md index d6392f4b..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`;`__CYGWIN__`/`__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,15 +337,37 @@ 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`)——如果定义它 -带来的新失败比修好的还多,结论就会翻过来。 +**这里定义 `__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/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/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/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 e60e5709..3872e752 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 `-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,24 +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 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 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. +// +// 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". +// +// 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; @@ -286,12 +299,37 @@ 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). + // + // 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 + // 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: + // + // 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__`. + // + // 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.expectDefined.push_back("__unix__"); r.expectDefined.push_back("__CYGWIN__"); 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 1c85f7ca..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 @@ -154,24 +154,29 @@ 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", + "-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) -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 +205,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", + "-D__mcpp_target_windows__=1") 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..98a0ae4d 100644 --- a/tests/unit/test_cenv.cpp +++ b/tests/unit/test_cenv.cpp @@ -64,29 +64,48 @@ 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. +// 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, +// 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")); + // The target's identity is not this module's to state: see above. + EXPECT_FALSE(has(r->tokens, "-D__mcpp_target_windows__=1")); + // `__CYGWIN__` is not withdrawn yet, and that is asserted, because the + // step that withdraws it must be a deliberate edit to this line rather + // than a silent drift. EXPECT_FALSE(has(r->tokens, "-U__CYGWIN__")); - EXPECT_FALSE(has(r->tokens, "-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 THESE, which is what keeps the tokens from being ones + // the compiler could ignore: `cenv_probe::verify` compares the realised + // configuration's predefines against these lists and refuses on a + // mismatch. A `-D` that did not take effect is a verification failure, + // not a silent one. + ASSERT_TRUE(has(r->expectDefined, "__CYGWIN__")); } // 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"; + } +}