Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
567 changes: 567 additions & 0 deletions .agents/docs/2026-09-09-two-answers-and-two-silences.md

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion .agents/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ superseded_by: 2026-09-07-....md # when status is superseded
---
```

272 records.
273 records.

## By subject

Expand All @@ -35,10 +35,15 @@ Records that declare one. Everything else is listed by date below.
- [The island boundary's names: one rule for both lanes, and the check that makes it true](2026-09-08-island-boundary-names.md) — active
- [Implementation plan: the island boundary's names](2026-09-08-island-boundary-names-implementation-plan.md) — active

### modules

- [Two answers and two silences: the scanner's second grammar, and the manifest keys nothing reads](2026-09-09-two-answers-and-two-silences.md) — active

## By date

### 2026-09

- [Two answers and two silences: the scanner's second grammar, and the manifest keys nothing reads](2026-09-09-two-answers-and-two-silences.md) — active
- [The documentation as a book: a chapter-by-chapter design](2026-09-08-the-documentation-as-a-book.md) — active
- [The island boundary's names: one rule for both lanes, and the check that makes it true](2026-09-08-island-boundary-names.md) — active
- [Implementation plan: the island boundary's names](2026-09-08-island-boundary-names-implementation-plan.md) — active
Expand Down
89 changes: 89 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,95 @@

## [Unreleased]

## [2026.9.9.1] - 2026-09-09

本次修复的四条缺陷,来源是同一类问题:一个问题被回答了两次,而读答案的地方各读各的;
或者一个键被解析了,却没有接到任何决定上。设计记录见
`.agents/docs/2026-09-09-two-answers-and-two-silences.md`。

### `module : private;` 不再被读成实现分区

私有模块片段([module.private.frag])是第三种产生式,不是「名字以冒号开头的分区」:

```cpp
module M; // 实现单元: 需要 M
module M:part; // 实现分区: 提供 M:part
module : private; // 私有模块片段:两者都不声明
```

扫描器的名字分词器为了让 `M:part` 成为一个 token 而接受 `:`,分区判据又是「名字里
含冒号」,于是这条合法的声明被拒绝在扫描期,消息是 `file already provides module 'M';
cannot also provide ':'` —— 一句关于用户源码的假话。这是 #433 引入的回归,首发于
v2026.8.18.1;在 clang 上,同一份源码在 2026.8.17.1 能构建并运行,在 2026.9.8.1 被拒绝。

编译器是否实现该特性由编译器回答:GCC 16.1 报 `sorry, unimplemented: private module
fragment`,mcpp 不在其上追加任何说法。

同时,扫描器不再记录任何源码不可能声明出来的模块身份。此前 BOM 与私有片段同时出现时,
构建图里会长出 `pcm.cache/-.pcm` —— 一个名字是标点的模块的 BMI,而没有任何东西报告它。

### 源码与 mcpp.toml 都按 UTF-8 读取,BOM 被消耗

MSVC 默认写出带 BOM 的 UTF-8。`trim` 用的 `std::isspace` 对那三个字节为假,于是标记
留在首个 token 上,模块声明不再被看见。失败因此不出现在出错的那个文件上,而出现在
消费者那里:`module 'foo' not found`。UTF-16/32 标记改为具名拒绝而不是被误读。

同一规则适用于 mcpp.toml:带 BOM 的清单此前报 `1:1: error: expected key`,一句关于
token 为真、关于文件无用的话。

### 模块扩展名的文件不必提供模块

实现单元(`module M;`)是 `.cppm` 的合法居民。`module_extensions` 说的是扫描哪些文件,
不是每个文件是什么 —— 后者只有内容能回答。规则由扩展名选出,而 BMI 绑定来自扫描结果,
两者在这类文件上不一致:

- clang:`-fmodule-output=` 空值被接受、退出 0、不写任何 BMI,失败转移到无关的消费者;
- clang:`-x c++-module` 把实现单元当接口编译,报 `missing 'export' specifier`;
- MSVC:`/ifcOutput ` 后无路径,会吃掉下一个 token。

GCC 的接口拼法本就是纯语言,因此同一个工程在 GCC 上能构建、在 clang 上不能 —— 这道
可移植性裂口只有一个 flag 宽。现在这两个 flag 按边绑定,取自扫描结果,空值状态因此
在构造上不可达。

### 只写 `[target.<谓词>.runtime]` 的谓词块不再被丢弃

`[target.<pred>.runtime]` 是链接行中与方言无关的那一半,自 2026.8.29.1 起可用。但决定
是否记录该条件块的闸门是一份手写的字段析取式,`libraries` 与 `link_library_dirs` 加入
结构体时没有同时加入它。于是该块被解析、被填充,然后丢弃;在同一谓词下随便再写一条
无关的 `defines` 就能让它生效。

闸门改为 `is_empty(ConditionalConfig)`,与既有的 `append` 并列为自由函数,并由 mcpp.toml
与 xpkg 两个读者共用;它进一步组合 `is_empty(BuildInputs)` 与既有的 `XlingsConfig::empty()`。
自由函数而非成员是被实测逼出来的:给这个模块导出的结构体加内联成员会改变导入方从它的 BMI
里实例化的东西,而 `Profile` 的 `std::optional<std::string>` 成员在 clang + MSVC 标准库下
本就会崩——初版写成成员,Windows CI 上 `test_modgraph.cpp` 因此编译失败,报错指向一个本次
改动从未碰过的结构体。同一形状的
第四处随之关闭:xpkg 的闸门漏掉了它自己会填的 `privateIncludeDirs`。索引里 228 份
descriptor 用新旧两个二进制解析,结果逐字节相同。

### `Profile.dependency_linkage` 不再是 `std::optional<std::string>` 成员

导出结构体的 `std::optional<std::string>` **数据成员**会迫使本模块的接口实例化该特化的
特殊成员机制,在 clang + MSVC 标准库下无法编译:

```
optional:262: error: no matching constructor for initialization of
'_SMF_control<_Optional_construct_base<basic_string<char,...>>, ...>'
```

路径是 `Manifest` -> `std::map<std::string, Profile>` -> `Profile`。同一文件里
`TargetEntry::sysroot` 的注释早已禁止这个形状并给出修法(两个普通成员),`Profile` 是
模块里最后一个仍是该形状的成员。现按同一修法改为
`dependencyLinkage` + `dependencyLinkageDeclared`,语义不变:未声明仍表示「沿用
`[build]` 的值」。

### `[runtime]` 与 `[target.<谓词>.runtime]` 的未知键会被报出

不受支持的键被报出而不是丢弃,是 `[build]`、`[target.<triple>]` 与
`[target.<pred>.build]` 自 #418 / #249 / #544 起各自遵守的规则;这两张表按同样的方式
被读取,却没有任何东西清扫它们。消息里列出它比对用的那份键表。
`[runtime.<capability>]` 子表是 provider 覆盖而不是键,不在清扫范围内。

## [2026.9.8.1] - 2026-09-08

### 一个包的 host module,按它们互相 import 的顺序编译
Expand Down
43 changes: 43 additions & 0 deletions docs/04-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,43 @@ mcpp = {
To extend the plan-vs-ddi audit to *every* module unit (not just overrides),
set `MCPP_VERIFY_MODGRAPH=1` when generating the build.

### 2.8.4 What The Default Scanner Reads

The scanner answers two questions per file — what does this unit provide, and
what does it require — and nothing else decides them.

**Three module declarations, and they are distinct productions.**

```cpp
module M; // implementation unit: requires M, provides nothing
module M:part; // implementation partition: provides M:part
module : private; // private module fragment: declares neither
```

The third is not a partition whose name begins with a colon. It contributes no
edge in either direction, and what follows it is still part of the same unit.
Whether the compiler implements it is the compiler's answer: GCC 16.1 reports
`sorry, unimplemented: private module fragment`, and mcpp adds nothing to that.

**A module-extension file need not provide a module.** An implementation unit
is a legal inhabitant of a `.cppm`, and `module_extensions` says which files to
*scan*, not what each one *is*. The compile mode follows the scan: a unit that
provides a module is compiled as an interface and given somewhere to write its
BMI; one that does not is compiled as an ordinary translation unit. Both
compilers therefore receive the same instruction for the same file, which they
did not before mcpp 2026.9.9.1 — Clang infers `c++-module` from the extension
and rejected the file, while GCC built it.

**Source is read as UTF-8.** A UTF-8 byte-order mark is consumed and is not part
of the text, which is what every compiler does with one and what MSVC writes by
default. A UTF-16 or UTF-32 mark is refused by name rather than misread. The
same rule applies to `mcpp.toml`.

**A name that no source could have declared is refused.** A module identity is a
dot-separated sequence of identifiers, optionally followed by `:` and one more
such sequence. Anything else fails the scan rather than entering the build graph,
where it would become a BMI path that nothing reports.

### 2.9 `[profile.<name>]` — Build Profiles

```toml
Expand Down Expand Up @@ -965,6 +1002,12 @@ deploy_files = ["bin/widget.dll"]
provider = "acme.widget-runtime@2.0.0"
```

An unsupported key in this table is **reported and ignored**, and the message
lists the keys it checked against. A `[runtime.<capability>]` sub-table is a
provider override rather than a key, so it is not swept. The same rule applies
to `[target.<predicate>.runtime]`, whose vocabulary is `libraries` and
`link_library_dirs` only ([22 — The Target Side](22-target-side.md)).

`requirements` records a non-empty `kind`/`value`, a `link` or `run` phase,
and whether the requirement is mandatory (`required` defaults to `true`).

Expand Down
21 changes: 20 additions & 1 deletion docs/22-target-side.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,26 @@ for arch/env conditions and combinators.
conditional source globs, e.g. gating `src/x86/**/*.asm` behind
`cfg(arch = "x86_64")`; `!`-exclusion globs work here too), plus `flags` and
`include_dirs` / `include_dirs_after` (mcpp 0.0.102+), plus
`private_include_dirs` and `std-module-flags` (mcpp 2026.9.1.1+).
`private_include_dirs` and `std-module-flags` (mcpp 2026.9.1.1+), and
`runtime` with `libraries` / `link_library_dirs` (mcpp 2026.8.29.1+).
- **`runtime` is the dialect-neutral half of a link line.** `build.ldflags` is
spelled the GNU way, and a native `cl.exe` rejects `-L`. These two keys say
the same thing without committing to a spelling: mcpp renders them as
`-L<dir>` + `-l<name>` or `/LIBPATH:<dir>` + `<name>.lib` according to the
target. They are the same two keys `[runtime]` (§2.11 of
[04 — mcpp.toml](04-mcpp-toml.md)) already has at the top level; this makes
them per-target and invents no vocabulary. Any other `[runtime]` key is
reported here and ignored, because the rest are not per-target.

```toml
# Linked only on Windows, and spelled correctly for whichever compiler builds it.
[target.windows.runtime]
libraries = ["user32", "gdi32"]
```

A predicate whose only content is this table is applied like any other. Until
mcpp 2026.9.9.1 it was not: the block was parsed and then discarded unless
something else appeared under the same predicate.
- **What `build` accepts is exactly the set of *additive build inputs*** — the
things that combine by appending and are consumed after the predicate is
evaluated, which is the member list of `BuildInputs`. `linkage`, `target`,
Expand Down
36 changes: 36 additions & 0 deletions docs/zh/04-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,37 @@ mcpp = {
要把 plan 与 ddi 的比对审计扩展到**每一个**模块单元(而不只是 override),
在生成构建时设置 `MCPP_VERIFY_MODGRAPH=1`。

### 2.8.4 默认扫描器读到的东西

扫描器对每个文件只回答两个问题 —— 这个单元提供什么、需要什么 —— 并且没有别的
东西决定它们。

**三种模块声明,它们是不同的产生式。**

```cpp
module M; // 实现单元: 需要 M,不提供任何东西
module M:part; // 实现分区: 提供 M:part
module : private; // 私有模块片段:两者都不声明
```

第三种不是一个名字以冒号开头的分区。它在两个方向上都不产生边,其后的内容仍属于
同一个单元。编译器是否实现它由编译器回答:GCC 16.1 报
`sorry, unimplemented: private module fragment`,mcpp 不在其上追加任何说法。

**模块扩展名的文件不必提供模块。** 实现单元是 `.cppm` 的合法居民,而
`module_extensions` 说的是**扫描**哪些文件,不是每个文件**是**什么。编译方式跟随
扫描结果:提供模块的单元按接口编译并获得写 BMI 的位置;不提供的按普通翻译单元
编译。因此两个编译器对同一个文件收到相同的指令 —— 在 mcpp 2026.9.9.1 之前并非
如此:Clang 从扩展名推断出 `c++-module` 并拒绝该文件,而 GCC 能构建它。

**源码按 UTF-8 读取。** UTF-8 字节序标记会被消耗,不属于正文 —— 这正是每个编译器
对它的处理,也是 MSVC 默认写出的东西。UTF-16 或 UTF-32 标记会被具名拒绝,而不是
被误读。同一规则适用于 `mcpp.toml`。

**任何源码都不可能声明出来的名字会被拒绝。** 模块身份是以点分隔的标识符序列,
其后可选地跟一个 `:` 和另一个这样的序列。此外的形式在扫描阶段失败,而不是进入
构建图 —— 在那里它会变成一条没有任何东西会报告的 BMI 路径。

### 2.9 `[profile.<name>]` — 构建档案

```toml
Expand Down Expand Up @@ -843,6 +874,11 @@ deploy_files = ["bin/widget.dll"]
provider = "acme.widget-runtime@2.0.0"
```

本表中不受支持的键会被**报出并忽略**,消息里列出它比对用的那份键表。
`[runtime.<capability>]` 子表是 provider 覆盖而不是键,因此不在清扫范围内。
同一规则适用于 `[target.<predicate>.runtime]`,其词汇表只有 `libraries` 与
`link_library_dirs`(见[22 —— 目标侧](22-target-side.md))。

`requirements` 记录非空 `kind`/`value`、`link` 或 `run` 阶段,以及是否强制
(`required` 默认 `true`)。`artifacts` 必须含 `role`、`path`、`provenance`;
可选 requirement 仍保留为 provenance,但不会进入硬 ABI/doctor 输入。
Expand Down
18 changes: 17 additions & 1 deletion docs/zh/22-target-side.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,23 @@ cxxflags = ["-march=x86-64-v2"]
的 `build`(mcpp 0.0.95+ —— 条件源码 glob,例如把 `src/x86/**/*.asm` 收在
`cfg(arch = "x86_64")` 之后;`!` 排除 glob 在此同样有效),再加 `flags` 与
`include_dirs` / `include_dirs_after`(mcpp 0.0.102+),
以及 `private_include_dirs` 与 `std-module-flags`(mcpp 2026.9.1.1+)。
以及 `private_include_dirs` 与 `std-module-flags`(mcpp 2026.9.1.1+),
还有带 `libraries` / `link_library_dirs` 的 `runtime`(mcpp 2026.8.29.1+)。
- **`runtime` 是链接行中与方言无关的那一半。** `build.ldflags` 按 GNU 拼法书写,
而原生 `cl.exe` 不接受 `-L`。这两个键表达同一件事而不承诺拼法:mcpp 按目标
渲染成 `-L<dir>` + `-l<name>` 或 `/LIBPATH:<dir>` + `<name>.lib`。它们就是
顶层 `[runtime]`(见 [04 —— mcpp.toml](04-mcpp-toml.md) §2.11)已有的同两个键,
此处只是让它们按目标生效,并未引入新词汇。`[runtime]` 的其余键在这里会被报出
并忽略,因为它们不是按目标区分的。

```toml
# 只在 Windows 上链接,并按实际编译器的拼法书写。
[target.windows.runtime]
libraries = ["user32", "gdi32"]
```

一个谓词下只写这一张表时,它与其他情形一样生效。在 mcpp 2026.9.9.1 之前并非
如此:除非同一谓词下还写了别的东西,该块会被解析后丢弃。
- **`build` 接受的恰好是*可叠加的构建输入*集合** —— 那些以追加方式合并、
并在谓词求值之后被消费的东西,也就是 `BuildInputs` 的成员表。`linkage`、`target`
与档案开关刻意不在其中:它们是**目标选择的输入**(用一个针对 `target` 求值的谓词
Expand Down
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mcpp"
version = "2026.9.8.1"
version = "2026.9.9.1"
description = "Modern C++ build & package management tool"
license = "Apache-2.0"
authors = ["mcpp-community"]
Expand Down
14 changes: 14 additions & 0 deletions modules/libs/src/toml.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,20 @@ const Table* Document::get_table(std::string_view path) const {

std::expected<Document, ParseError> parse(std::string_view src) {
using namespace detail;
// A UTF-8 byte-order mark is not part of the document.
//
// MSVC writes one by default, so an mcpp.toml authored on Windows commonly
// begins `EF BB BF [ p a c k a g e ]`. The lexer saw those bytes as the
// start of a bare key and reported `1:1: error: expected key`, which is a
// true statement about the token and tells the author nothing about the
// file. The same mark is skipped by the source scanner for the same reason
// and with the same sentence: the mark is an encoding annotation, not text,
// and every other reader of these files skips it.
//
// Consumed here rather than in the manifest reader so that the rule holds
// for every document this parser is given — mcpp.toml, mcpp.lock, and the
// configuration files — instead of for the one that happened to report it.
if (src.starts_with("\xEF\xBB\xBF")) src.remove_prefix(3);
Lexer L { src };
Table root;
std::set<std::string, std::less<>> explicitTables;
Expand Down
Loading
Loading