Skip to content

Commit 94603a9

Browse files
committed
modgraph: resolve a module name in the importer's closure (#732)
A module name identifies one module within one program: GCC and clang name a module's entities and its initializer after it, so a program cannot link two modules of one name (measured: multiple definition of value@common()), and two programs may each have one. A build holds several programs, a package and the programs it ships through artifacts or the members of a workspace, and mcpp refused a name that two packages of one build provided whichever programs they belonged to. The prepare phase computes each compiled package's closure (code and member edges; not artifacts, tools or build-dependencies; no closure for a workspace's virtual root). One resolver, choose_provider and resolve_provider in mcpp.modgraph, answers which provider an import means, and the scanner, the interface check, the packer and the backend's std reachability all use it. Two providers of one name are refused only when one closure holds both, and one file reached twice is recognised by identity rather than spelling. When two packages provide a name, their BMIs lie below their packages' directories and every unit that may import the name is bound to its provider: a module map for GCC, whose mapper does not fall back for an unlisted name, -fmodule-file= for clang and /reference for MSVC; mcpp dyndep reads the same map through --module-map. With every name provided once nothing changes: build.ninja and compile_commands.json of the xlings workspace are byte-identical to 2026.9.30.1's. A package that provides a collided name is compiled rather than served from the global cache. The plan's unread module map is removed. e2e 847 (GCC and clang) and 848 (MSVC) cover an artifacts updater, two workspace members, the reported layout, and the two refusals; 847 fails on 2026.9.30.1 with the error of #732.
1 parent 2beeec9 commit 94603a9

19 files changed

Lines changed: 906 additions & 95 deletions

‎docs/05-dependencies.md‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,35 @@ updater = { path = "../updater", artifacts = ["updater"] }
525525
> by nothing that made a decision: writing it produced a manifest that loaded,
526526
> no diagnostic, and no effect.
527527
528+
### One module per name in each program (mcpp 2026.9.30.2+)
529+
530+
A module name identifies one module within one program. The compilers name a
531+
module's entities and its initializer after the module, so a program cannot
532+
link two modules of one name. A build can hold several programs (a package and
533+
the programs it ships through `artifacts`, the members of a workspace), and
534+
each of them may have its own module of one name.
535+
536+
- An import is resolved within the importing package's closure: the package
537+
and every package it reaches through its dependencies. The closure does not
538+
follow `artifacts`, `tools` or `[build-dependencies]` edges, whose programs
539+
are built separately.
540+
- Two packages that one program links may not provide the same name. The build
541+
is refused, and the message names the package whose closure holds both.
542+
- When two packages of one build provide a name, each BMI lies below its
543+
package's directory in the build directory, and every compile that may import
544+
the name is told which one it means: through a module map with GCC, through
545+
`-fmodule-file=` with Clang, and through `/reference` with MSVC. When every
546+
name has one provider, the build directory and every command are as they
547+
were before.
548+
- A package that provides such a name is compiled in the project, not served
549+
from the global dependency cache.
550+
- clangd finds a module by its name in the compilation database, so for a name
551+
two packages provide it may show the other program's module. The build is
552+
not affected.
553+
554+
A module that several programs share is best provided by one package that the
555+
others depend on; it is then compiled once.
556+
528557
## Current limitations
529558

530559
- **Two things need the network, and only two:** resolving a branch that has no

‎docs/07-workspace.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -451,9 +451,11 @@ member that several members use is compiled once.
451451
`compile_commands.json` once, as the union of their databases (2026.9.29.5+).
452452
- **No-op builds.** A command repeated with nothing changed is answered by one
453453
check per configuration, without planning.
454-
- **Module names.** Members built in one graph share one module namespace:
455-
two members that each provide a module of the same name cannot be built in
456-
one `--workspace` command; build each with `-p`.
454+
- **Module names.** A module name is unique within one program, not within one
455+
graph (2026.9.30.2+). Two members that share no program may each provide a
456+
module of the same name, and one `--workspace` command builds both; a member
457+
that links both is refused. See
458+
[05 — One module per name in each program](05-dependencies.md#one-module-per-name-in-each-program-mcpp-20269302).
457459

458460
## 6. Directory Layout
459461

‎docs/zh/05-dependencies.md‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,25 @@ updater = { path = "../updater", artifacts = ["updater"] }
478478
> 这个段很早就能被解析,而直到 2026.8.29.1,没有任何做决定的代码读过它:
479479
> 写下它得到的是一份能加载的 manifest、零诊断、零效果。
480480
481+
### 每个程序里一个名字只对应一个模块(mcpp 2026.9.30.2+)
482+
483+
模块名在一个程序之内标识一个模块。编译器以模块名命名模块的实体与初始化函数,因此一个程序
484+
不能链接两个同名模块。一次构建可以包含多个程序(一个包,以及它经 `artifacts` 发布的程序;
485+
工作区的各个成员),其中每个程序都可以有自己的同名模块。
486+
487+
- import 在导入方所在包的闭包内解析:该包,以及它经依赖到达的每个包。闭包不沿
488+
`artifacts`、`tools` 与 `[build-dependencies]` 边延伸,这些边对应的程序另行构建。
489+
- 被同一个程序链接的两个包不得提供同一个名字。此时构建被拒绝,消息点名其闭包同时包含两者的包。
490+
- 同一次构建中有两个包提供同一个名字时,各自的 BMI 位于构建目录中其所属包的子目录下,
491+
每一个可能导入该名字的编译都会被告知它指哪一个:GCC 经模块映射文件,Clang 经
492+
`-fmodule-file=`,MSVC 经 `/reference`。每个名字只有一个提供方时,构建目录与每条命令都与
493+
以前相同。
494+
- 提供这种名字的包在项目内编译,不从全局依赖缓存取用。
495+
- clangd 在编译数据库中按名字查找模块,因此对两个包提供的同一个名字,编辑器可能显示另一个
496+
程序的模块。构建不受影响。
497+
498+
几个程序共用的模块,最好由一个包提供、其余的包依赖它;这样它只编译一次。
499+
481500
## 当前边界
482501

483502
- **只有两件事需要网络,而且只有这两件:** 解析一个在 lock 里没有 commit 的

‎docs/zh/07-workspace.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -414,8 +414,9 @@ mcpp test --workspace --workspace-timeout 1800 # whole fan-out (default 0 = no
414414
多个配置的命令只写一次根目录的 `compile_commands.json`,内容为各配置数据库的并集
415415
(2026.9.29.5+)。
416416
- **无事可做的构建。** 在没有任何改动时重复执行的命令,每个配置只做一次检查,不重新规划。
417-
- **模块名。** 在同一张图中构建的成员共享一个模块命名空间:两个成员各自提供同名模块时,不能在
418-
同一条 `--workspace` 命令中构建;分别用 `-p` 构建。
417+
- **模块名。** 模块名在一个程序之内唯一,而不是在一张图之内唯一(2026.9.30.2+)。不共享任何
418+
程序的两个成员可以各自提供同名模块,一条 `--workspace` 命令会把两者都构建出来;同时链接两者的
419+
成员会被拒绝。见 [05 —— 每个程序里一个名字只对应一个模块](05-dependencies.md#每个程序里一个名字只对应一个模块mcpp-20269302)。
419420

420421
## 6. 目录布局
421422

‎modules/dyndep/src/dyndep.cppm‎

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,18 @@ struct DyndepOptions {
5454
// A unit that provides nothing (implementation unit, plain .cpp) is not
5555
// split, so it keeps its single record.
5656
bool splitModuleEdges = false;
57+
// mcpp#732: module name -> BMI path, for a unit whose package's closure
58+
// holds a name two packages provide (the plan's module map). A name it
59+
// lists takes that path; any other takes `<bmiDir>/<name><bmiExt>`.
60+
const std::map<std::string, std::string, std::less<>>* moduleMap = nullptr;
5761
};
5862

63+
// The BMI path `name` takes under `opts`: the module map's, or the flat one.
64+
std::string bmi_path_for(std::string_view name, const DyndepOptions& opts);
65+
66+
// Parse a module map: `<module name> <BMI path>` per line.
67+
std::map<std::string, std::string, std::less<>> parse_module_map(std::string_view body);
68+
5969
// Parse a single .ddi JSON body to a UnitInfo. Returns unexpected on JSON error.
6070
std::expected<UnitInfo, std::string> parse_ddi(std::string_view body);
6171

@@ -188,6 +198,26 @@ std::size_t find_key(std::string_view s, std::size_t start, std::string_view key
188198

189199
} // namespace
190200

201+
std::string bmi_path_for(std::string_view name, const DyndepOptions& opts) {
202+
if (opts.moduleMap)
203+
if (auto it = opts.moduleMap->find(name); it != opts.moduleMap->end()) return it->second;
204+
return std::string(opts.bmiDir) + "/" + bmi_basename(name, opts.bmiExt);
205+
}
206+
207+
std::map<std::string, std::string, std::less<>> parse_module_map(std::string_view body) {
208+
std::map<std::string, std::string, std::less<>> out;
209+
while (!body.empty()) {
210+
auto nl = body.find('\n');
211+
auto line = body.substr(0, nl);
212+
body.remove_prefix(nl == std::string_view::npos ? body.size() : nl + 1);
213+
while (!line.empty() && (line.back() == '\r' || line.back() == ' ')) line.remove_suffix(1);
214+
auto sp = line.find(' ');
215+
if (sp == std::string_view::npos || sp == 0) continue;
216+
out.emplace(std::string(line.substr(0, sp)), std::string(line.substr(sp + 1)));
217+
}
218+
return out;
219+
}
220+
191221
std::string bmi_basename(std::string_view logicalName,
192222
std::string_view ext) {
193223
std::string out;
@@ -207,8 +237,7 @@ namespace {
207237
std::vector<std::string> dyndep_targets(const UnitInfo& u, const DyndepOptions& opts) {
208238
std::vector<std::string> t;
209239
if (opts.splitModuleEdges && !u.provides.empty()) {
210-
t.push_back(std::string(opts.bmiDir) + "/"
211-
+ bmi_basename(u.provides.front(), opts.bmiExt));
240+
t.push_back(bmi_path_for(u.provides.front(), opts));
212241
}
213242
if (!u.primaryOutput.empty()) t.push_back(u.primaryOutput.string());
214243
return t;
@@ -314,8 +343,7 @@ std::string emit_dyndep(const std::vector<UnitInfo>& units,
314343
bool selfProvides = false;
315344
for (auto& p : u.provides) if (p == r) { selfProvides = true; break; }
316345
if (selfProvides) continue;
317-
std::string bmiDir(opts.bmiDir);
318-
add_implicit(bmiDir + "/" + bmi_basename(r, opts.bmiExt));
346+
add_implicit(bmi_path_for(r, opts));
319347
}
320348
line += "\n restat = 1\n";
321349
out += line;
@@ -367,8 +395,7 @@ emit_dyndep_single(const std::filesystem::path& ddiPath,
367395
for (auto& p : u->provides) if (p == r) { selfProvides = true; break; }
368396
if (selfProvides) continue;
369397
if (firstImplicit) { line += " |"; firstImplicit = false; }
370-
std::string bmiDir(opts.bmiDir);
371-
line += " " + bmiDir + "/" + bmi_basename(r, opts.bmiExt);
398+
line += " " + bmi_path_for(r, opts);
372399
}
373400
line += "\n restat = 1\n";
374401
out += line;

‎modules/toolchain-model/src/model.cppm‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,10 @@ struct BmiTraits {
441441
std::string_view compileModulesFlag; // " -fmodules" (GCC) | ""
442442
std::string_view stdBmiUsePrefix; // "" | " -fmodule-file=std=" | " /reference std="
443443
std::string_view stdCompatBmiUsePrefix; // "" | " -fmodule-file=std.compat=" | " /reference std.compat="
444+
// Binds one module name to one BMI file for one compile, as `<prefix><name>=<path>`
445+
// (mcpp#732: two modules of one name in one build directory). "" for GCC,
446+
// which binds names through a mapper file instead.
447+
std::string_view moduleFileUsePrefix; // "" | " -fmodule-file=" | " /reference "
444448
std::string_view moduleOutputPrefix; // "" | " -fmodule-output=" | " /ifcOutput "
445449
std::string_view bmiSearchPrefix; // "" | " -fprebuilt-module-path=" | " /ifcSearchDir "
446450
// How this compiler is TOLD that a translation unit is a module interface.
@@ -647,6 +651,7 @@ BmiTraits bmi_traits(const Toolchain& tc) {
647651
.compileModulesFlag = "",
648652
.stdBmiUsePrefix = " /reference std=",
649653
.stdCompatBmiUsePrefix = " /reference std.compat=",
654+
.moduleFileUsePrefix = " /reference ",
650655
.moduleOutputPrefix = " /ifcOutput ",
651656
.bmiSearchPrefix = " /ifcSearchDir ",
652657
// Pre-existing behaviour, unchanged: cl has always been told
@@ -670,6 +675,7 @@ BmiTraits bmi_traits(const Toolchain& tc) {
670675
.compileModulesFlag = "",
671676
.stdBmiUsePrefix = " -fmodule-file=std=",
672677
.stdCompatBmiUsePrefix = " -fmodule-file=std.compat=",
678+
.moduleFileUsePrefix = " -fmodule-file=",
673679
.moduleOutputPrefix = " -fmodule-output=",
674680
.bmiSearchPrefix = " -fprebuilt-module-path=",
675681
.moduleInterfaceLangFlag = " -x c++-module",
@@ -689,6 +695,7 @@ BmiTraits bmi_traits(const Toolchain& tc) {
689695
.compileModulesFlag = " -fmodules",
690696
.stdBmiUsePrefix = "",
691697
.stdCompatBmiUsePrefix = "",
698+
.moduleFileUsePrefix = "",
692699
.moduleOutputPrefix = "",
693700
.bmiSearchPrefix = "",
694701
// GCC decides interface-ness from the content (`export module`), so

‎src/build/configure.cppm‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,15 @@ stage_configure_prerequisites(const BuildPlan& plan) {
6262
if (!unit.servedFromCache || unit.providesModule.empty()
6363
|| unit.cachedBmi.empty()) continue;
6464

65-
std::string fileName;
66-
fileName.reserve(unit.providesModule.size() + traits.bmiExt.size());
67-
for (char ch : unit.providesModule)
68-
fileName.push_back(ch == ':' ? '-' : ch);
69-
fileName += traits.bmiExt;
65+
// Where the plan placed the unit's BMI: below its package's directory
66+
// when two packages provide its module name (mcpp#732).
67+
std::string fileName = unit.bmiFile;
68+
if (fileName.empty()) {
69+
fileName.reserve(unit.providesModule.size() + traits.bmiExt.size());
70+
for (char ch : unit.providesModule)
71+
fileName.push_back(ch == ':' ? '-' : ch);
72+
fileName += traits.bmiExt;
73+
}
7074

7175
auto result = stage_one(
7276
unit.cachedBmi, plan.outputDir / traits.bmiDir / fileName,

0 commit comments

Comments
 (0)