Skip to content

Commit 620f9c1

Browse files
committed
fix(scanner, manifest, build): one answer per question, and no key that reaches no decision (2026.9.9.1)
Four defects with two shapes behind them: a question answered twice whose answers are read by different sites, and a key that is parsed and then reaches no decision. Design record: .agents/docs/2026-09-09-two-answers-and-two-silences.md `module : private;` was read as an implementation partition. The name tokeniser admits `:` so that `M:part` scans as one token, and the partition test is "the name contains a colon" -- but the colon in the private module fragment comes first and belongs to no name. A valid file was refused at scan time with a sentence that is false about it. A regression from #433, first released in v2026.8.18.1: on clang the same source built and ran under 2026.8.17.1. The scanner also no longer records an identity no source could have declared; a BOM combined with a fragment previously produced a BMI named after a colon. A UTF-8 byte-order mark is consumed where bytes become lines, and by every TOML document mcpp reads. MSVC writes one by default, so this is ordinary input. The failure it caused appeared at the consumer (`module 'x' not found`) rather than at the file that was wrong. UTF-16/32 is refused by name. `module_lang` and `module_output` become per-edge, bound from what the scan found rather than from the file's extension. A module-extension file need not provide a module, and where the classifier and the scanner disagreed the edge carried an interface's flags with no BMI: clang accepted `-fmodule-output=` with no value and wrote no BMI at all, and compiled implementation units as interfaces. GCC's interface spelling is the plain language, so the identical project built there and not on clang. `ConditionalConfig::empty()` replaces two hand-written gates that omitted `libraries` and `linkLibraryDirs`, so a predicate carrying only a `[target.<pred>.runtime]` table was parsed and discarded. It composes `BuildInputs::empty()` and the existing `XlingsConfig::empty()`, closing a fourth instance in the xpkg gate. All 228 descriptors in mcpp-index parse identically before and after. `[runtime]` and `[target.<pred>.runtime]` now report an unsupported key, the rule `[build]` and `[target.<triple>]` have followed since #418 and #249. Deliberately not done: no detector for GNU-spelled `ldflags` reaching MSVC. `ldflags` is a documented raw pass-through and `[runtime] libraries` is the supported neutral form; inspecting the former would be a second answer to a question the author already answered.
1 parent ead8dfe commit 620f9c1

23 files changed

Lines changed: 1802 additions & 30 deletions

.agents/docs/2026-09-09-two-answers-and-two-silences.md

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

.agents/docs/README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ superseded_by: 2026-09-07-....md # when status is superseded
1818
---
1919
```
2020

21-
272 records.
21+
273 records.
2222

2323
## By subject
2424

@@ -35,10 +35,15 @@ Records that declare one. Everything else is listed by date below.
3535
- [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
3636
- [Implementation plan: the island boundary's names](2026-09-08-island-boundary-names-implementation-plan.md) — active
3737

38+
### modules
39+
40+
- [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
41+
3842
## By date
3943

4044
### 2026-09
4145

46+
- [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
4247
- [The documentation as a book: a chapter-by-chapter design](2026-09-08-the-documentation-as-a-book.md) — active
4348
- [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
4449
- [Implementation plan: the island boundary's names](2026-09-08-island-boundary-names-implementation-plan.md) — active

CHANGELOG.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,75 @@
55
66
## [Unreleased]
77

8+
## [2026.9.9.1] - 2026-09-09
9+
10+
本次修复的四条缺陷,来源是同一类问题:一个问题被回答了两次,而读答案的地方各读各的;
11+
或者一个键被解析了,却没有接到任何决定上。设计记录见
12+
`.agents/docs/2026-09-09-two-answers-and-two-silences.md`
13+
14+
### `module : private;` 不再被读成实现分区
15+
16+
私有模块片段([module.private.frag])是第三种产生式,不是「名字以冒号开头的分区」:
17+
18+
```cpp
19+
module M; // 实现单元: 需要 M
20+
module M:part; // 实现分区: 提供 M:part
21+
module : private; // 私有模块片段:两者都不声明
22+
```
23+
24+
扫描器的名字分词器为了让 `M:part` 成为一个 token 而接受 `:`,分区判据又是「名字里
25+
含冒号」,于是这条合法的声明被拒绝在扫描期,消息是 `file already provides module 'M';
26+
cannot also provide ':'` —— 一句关于用户源码的假话。这是 #433 引入的回归,首发于
27+
v2026.8.18.1;在 clang 上,同一份源码在 2026.8.17.1 能构建并运行,在 2026.9.8.1 被拒绝。
28+
29+
编译器是否实现该特性由编译器回答:GCC 16.1 报 `sorry, unimplemented: private module
30+
fragment`,mcpp 不在其上追加任何说法。
31+
32+
同时,扫描器不再记录任何源码不可能声明出来的模块身份。此前 BOM 与私有片段同时出现时,
33+
构建图里会长出 `pcm.cache/-.pcm` —— 一个名字是标点的模块的 BMI,而没有任何东西报告它。
34+
35+
### 源码与 mcpp.toml 都按 UTF-8 读取,BOM 被消耗
36+
37+
MSVC 默认写出带 BOM 的 UTF-8。`trim` 用的 `std::isspace` 对那三个字节为假,于是标记
38+
留在首个 token 上,模块声明不再被看见。失败因此不出现在出错的那个文件上,而出现在
39+
消费者那里:`module 'foo' not found`。UTF-16/32 标记改为具名拒绝而不是被误读。
40+
41+
同一规则适用于 mcpp.toml:带 BOM 的清单此前报 `1:1: error: expected key`,一句关于
42+
token 为真、关于文件无用的话。
43+
44+
### 模块扩展名的文件不必提供模块
45+
46+
实现单元(`module M;`)是 `.cppm` 的合法居民。`module_extensions` 说的是扫描哪些文件,
47+
不是每个文件是什么 —— 后者只有内容能回答。规则由扩展名选出,而 BMI 绑定来自扫描结果,
48+
两者在这类文件上不一致:
49+
50+
- clang:`-fmodule-output=` 空值被接受、退出 0、不写任何 BMI,失败转移到无关的消费者;
51+
- clang:`-x c++-module` 把实现单元当接口编译,报 `missing 'export' specifier`;
52+
- MSVC:`/ifcOutput ` 后无路径,会吃掉下一个 token。
53+
54+
GCC 的接口拼法本就是纯语言,因此同一个工程在 GCC 上能构建、在 clang 上不能 —— 这道
55+
可移植性裂口只有一个 flag 宽。现在这两个 flag 按边绑定,取自扫描结果,空值状态因此
56+
在构造上不可达。
57+
58+
### 只写 `[target.<谓词>.runtime]` 的谓词块不再被丢弃
59+
60+
`[target.<pred>.runtime]` 是链接行中与方言无关的那一半,自 2026.8.29.1 起可用。但决定
61+
是否记录该条件块的闸门是一份手写的字段析取式,`libraries``link_library_dirs` 加入
62+
结构体时没有同时加入它。于是该块被解析、被填充,然后丢弃;在同一谓词下随便再写一条
63+
无关的 `defines` 就能让它生效。
64+
65+
闸门改为 `ConditionalConfig::empty()`,定义在字段旁边,并由 mcpp.toml 与 xpkg 两个读者
66+
共用;它进一步组合 `BuildInputs::empty()` 与既有的 `XlingsConfig::empty()`。同一形状的
67+
第四处随之关闭:xpkg 的闸门漏掉了它自己会填的 `privateIncludeDirs`。索引里 228 份
68+
descriptor 用新旧两个二进制解析,结果逐字节相同。
69+
70+
### `[runtime]``[target.<谓词>.runtime]` 的未知键会被报出
71+
72+
不受支持的键被报出而不是丢弃,是 `[build]``[target.<triple>]`
73+
`[target.<pred>.build]`#418 / #249 / #544 起各自遵守的规则;这两张表按同样的方式
74+
被读取,却没有任何东西清扫它们。消息里列出它比对用的那份键表。
75+
`[runtime.<capability>]` 子表是 provider 覆盖而不是键,不在清扫范围内。
76+
877
## [2026.9.8.1] - 2026-09-08
978

1079
### 一个包的 host module,按它们互相 import 的顺序编译

docs/04-mcpp-toml.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -854,6 +854,43 @@ mcpp = {
854854
To extend the plan-vs-ddi audit to *every* module unit (not just overrides),
855855
set `MCPP_VERIFY_MODGRAPH=1` when generating the build.
856856

857+
### 2.8.4 What The Default Scanner Reads
858+
859+
The scanner answers two questions per file — what does this unit provide, and
860+
what does it require — and nothing else decides them.
861+
862+
**Three module declarations, and they are distinct productions.**
863+
864+
```cpp
865+
module M; // implementation unit: requires M, provides nothing
866+
module M:part; // implementation partition: provides M:part
867+
module : private; // private module fragment: declares neither
868+
```
869+
870+
The third is not a partition whose name begins with a colon. It contributes no
871+
edge in either direction, and what follows it is still part of the same unit.
872+
Whether the compiler implements it is the compiler's answer: GCC 16.1 reports
873+
`sorry, unimplemented: private module fragment`, and mcpp adds nothing to that.
874+
875+
**A module-extension file need not provide a module.** An implementation unit
876+
is a legal inhabitant of a `.cppm`, and `module_extensions` says which files to
877+
*scan*, not what each one *is*. The compile mode follows the scan: a unit that
878+
provides a module is compiled as an interface and given somewhere to write its
879+
BMI; one that does not is compiled as an ordinary translation unit. Both
880+
compilers therefore receive the same instruction for the same file, which they
881+
did not before mcpp 2026.9.9.1 — Clang infers `c++-module` from the extension
882+
and rejected the file, while GCC built it.
883+
884+
**Source is read as UTF-8.** A UTF-8 byte-order mark is consumed and is not part
885+
of the text, which is what every compiler does with one and what MSVC writes by
886+
default. A UTF-16 or UTF-32 mark is refused by name rather than misread. The
887+
same rule applies to `mcpp.toml`.
888+
889+
**A name that no source could have declared is refused.** A module identity is a
890+
dot-separated sequence of identifiers, optionally followed by `:` and one more
891+
such sequence. Anything else fails the scan rather than entering the build graph,
892+
where it would become a BMI path that nothing reports.
893+
857894
### 2.9 `[profile.<name>]` — Build Profiles
858895

859896
```toml
@@ -965,6 +1002,12 @@ deploy_files = ["bin/widget.dll"]
9651002
provider = "acme.widget-runtime@2.0.0"
9661003
```
9671004

1005+
An unsupported key in this table is **reported and ignored**, and the message
1006+
lists the keys it checked against. A `[runtime.<capability>]` sub-table is a
1007+
provider override rather than a key, so it is not swept. The same rule applies
1008+
to `[target.<predicate>.runtime]`, whose vocabulary is `libraries` and
1009+
`link_library_dirs` only ([22 — The Target Side](22-target-side.md)).
1010+
9681011
`requirements` records a non-empty `kind`/`value`, a `link` or `run` phase,
9691012
and whether the requirement is mandatory (`required` defaults to `true`).
9701013

docs/22-target-side.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,26 @@ for arch/env conditions and combinators.
381381
conditional source globs, e.g. gating `src/x86/**/*.asm` behind
382382
`cfg(arch = "x86_64")`; `!`-exclusion globs work here too), plus `flags` and
383383
`include_dirs` / `include_dirs_after` (mcpp 0.0.102+), plus
384-
`private_include_dirs` and `std-module-flags` (mcpp 2026.9.1.1+).
384+
`private_include_dirs` and `std-module-flags` (mcpp 2026.9.1.1+), and
385+
`runtime` with `libraries` / `link_library_dirs` (mcpp 2026.8.29.1+).
386+
- **`runtime` is the dialect-neutral half of a link line.** `build.ldflags` is
387+
spelled the GNU way, and a native `cl.exe` rejects `-L`. These two keys say
388+
the same thing without committing to a spelling: mcpp renders them as
389+
`-L<dir>` + `-l<name>` or `/LIBPATH:<dir>` + `<name>.lib` according to the
390+
target. They are the same two keys `[runtime]` (§2.11 of
391+
[04 — mcpp.toml](04-mcpp-toml.md)) already has at the top level; this makes
392+
them per-target and invents no vocabulary. Any other `[runtime]` key is
393+
reported here and ignored, because the rest are not per-target.
394+
395+
```toml
396+
# Linked only on Windows, and spelled correctly for whichever compiler builds it.
397+
[target.windows.runtime]
398+
libraries = ["user32", "gdi32"]
399+
```
400+
401+
A predicate whose only content is this table is applied like any other. Until
402+
mcpp 2026.9.9.1 it was not: the block was parsed and then discarded unless
403+
something else appeared under the same predicate.
385404
- **What `build` accepts is exactly the set of *additive build inputs*** — the
386405
things that combine by appending and are consumed after the predicate is
387406
evaluated, which is the member list of `BuildInputs`. `linkage`, `target`,

docs/zh/04-mcpp-toml.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,37 @@ mcpp = {
744744
要把 plan 与 ddi 的比对审计扩展到**每一个**模块单元(而不只是 override),
745745
在生成构建时设置 `MCPP_VERIFY_MODGRAPH=1`
746746

747+
### 2.8.4 默认扫描器读到的东西
748+
749+
扫描器对每个文件只回答两个问题 —— 这个单元提供什么、需要什么 —— 并且没有别的
750+
东西决定它们。
751+
752+
**三种模块声明,它们是不同的产生式。**
753+
754+
```cpp
755+
module M; // 实现单元: 需要 M,不提供任何东西
756+
module M:part; // 实现分区: 提供 M:part
757+
module : private; // 私有模块片段:两者都不声明
758+
```
759+
760+
第三种不是一个名字以冒号开头的分区。它在两个方向上都不产生边,其后的内容仍属于
761+
同一个单元。编译器是否实现它由编译器回答:GCC 16.1 报
762+
`sorry, unimplemented: private module fragment`,mcpp 不在其上追加任何说法。
763+
764+
**模块扩展名的文件不必提供模块。** 实现单元是 `.cppm` 的合法居民,而
765+
`module_extensions` 说的是**扫描**哪些文件,不是每个文件****什么。编译方式跟随
766+
扫描结果:提供模块的单元按接口编译并获得写 BMI 的位置;不提供的按普通翻译单元
767+
编译。因此两个编译器对同一个文件收到相同的指令 —— 在 mcpp 2026.9.9.1 之前并非
768+
如此:Clang 从扩展名推断出 `c++-module` 并拒绝该文件,而 GCC 能构建它。
769+
770+
**源码按 UTF-8 读取。** UTF-8 字节序标记会被消耗,不属于正文 —— 这正是每个编译器
771+
对它的处理,也是 MSVC 默认写出的东西。UTF-16 或 UTF-32 标记会被具名拒绝,而不是
772+
被误读。同一规则适用于 `mcpp.toml`
773+
774+
**任何源码都不可能声明出来的名字会被拒绝。** 模块身份是以点分隔的标识符序列,
775+
其后可选地跟一个 `:` 和另一个这样的序列。此外的形式在扫描阶段失败,而不是进入
776+
构建图 —— 在那里它会变成一条没有任何东西会报告的 BMI 路径。
777+
747778
### 2.9 `[profile.<name>]` — 构建档案
748779

749780
```toml
@@ -843,6 +874,11 @@ deploy_files = ["bin/widget.dll"]
843874
provider = "acme.widget-runtime@2.0.0"
844875
```
845876

877+
本表中不受支持的键会被**报出并忽略**,消息里列出它比对用的那份键表。
878+
`[runtime.<capability>]` 子表是 provider 覆盖而不是键,因此不在清扫范围内。
879+
同一规则适用于 `[target.<predicate>.runtime]`,其词汇表只有 `libraries`
880+
`link_library_dirs`(见[22 —— 目标侧](22-target-side.md))。
881+
846882
`requirements` 记录非空 `kind`/`value``link``run` 阶段,以及是否强制
847883
(`required` 默认 `true`)。`artifacts` 必须含 `role``path``provenance`;
848884
可选 requirement 仍保留为 provenance,但不会进入硬 ABI/doctor 输入。

docs/zh/22-target-side.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,23 @@ cxxflags = ["-march=x86-64-v2"]
326326
`build`(mcpp 0.0.95+ —— 条件源码 glob,例如把 `src/x86/**/*.asm` 收在
327327
`cfg(arch = "x86_64")` 之后;`!` 排除 glob 在此同样有效),再加 `flags`
328328
`include_dirs` / `include_dirs_after`(mcpp 0.0.102+),
329-
以及 `private_include_dirs``std-module-flags`(mcpp 2026.9.1.1+)。
329+
以及 `private_include_dirs``std-module-flags`(mcpp 2026.9.1.1+),
330+
还有带 `libraries` / `link_library_dirs``runtime`(mcpp 2026.8.29.1+)。
331+
- **`runtime` 是链接行中与方言无关的那一半。** `build.ldflags` 按 GNU 拼法书写,
332+
而原生 `cl.exe` 不接受 `-L`。这两个键表达同一件事而不承诺拼法:mcpp 按目标
333+
渲染成 `-L<dir>` + `-l<name>``/LIBPATH:<dir>` + `<name>.lib`。它们就是
334+
顶层 `[runtime]`(见 [04 —— mcpp.toml](04-mcpp-toml.md) §2.11)已有的同两个键,
335+
此处只是让它们按目标生效,并未引入新词汇。`[runtime]` 的其余键在这里会被报出
336+
并忽略,因为它们不是按目标区分的。
337+
338+
```toml
339+
# 只在 Windows 上链接,并按实际编译器的拼法书写。
340+
[target.windows.runtime]
341+
libraries = ["user32", "gdi32"]
342+
```
343+
344+
一个谓词下只写这一张表时,它与其他情形一样生效。在 mcpp 2026.9.9.1 之前并非
345+
如此:除非同一谓词下还写了别的东西,该块会被解析后丢弃。
330346
- **`build` 接受的恰好是*可叠加的构建输入*集合** —— 那些以追加方式合并、
331347
并在谓词求值之后被消费的东西,也就是 `BuildInputs` 的成员表。`linkage``target`
332348
与档案开关刻意不在其中:它们是**目标选择的输入**(用一个针对 `target` 求值的谓词

mcpp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mcpp"
3-
version = "2026.9.8.1"
3+
version = "2026.9.9.1"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

modules/libs/src/toml.cppm

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,20 @@ const Table* Document::get_table(std::string_view path) const {
496496

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

0 commit comments

Comments
 (0)