Repository navigation
Expand file tree
/
Copy pathdialect.cppm
More file actions
388 lines (353 loc) · 18.4 KB
/
Copy pathdialect.cppm
File metadata and controls
388 lines (353 loc) · 18.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
// mcpp.toolchain.dialect — command-line *spelling* per compiler family.
//
// The single data point for how a flag is spelt: GNU driver style
// (GCC / Clang / MinGW share one instance) vs MSVC cl.exe style. Consumers
// (flags.cppm, ninja_backend.cppm, plan.cppm) concatenate these fragments
// instead of hardcoding "-I"/"-o"/"ar rcs" — adding a compiler family is a
// second row of data, not another if/else at every call site.
//
// Deliberately a value-type aggregate, not a class hierarchy — same
// rationale as BmiTraits (2026-05-15-clang-parity-and-toolchain-abstraction
// §2.3): the differences are strings and booleans, not behavior.
//
// See .agents/docs/2026-07-13-toolchain-backend-abstraction-msvc-mingw-design.md §2.1.
export module mcpp.toolchain.dialect;
import std;
import mcpp.toolchain.model;
export namespace mcpp::toolchain {
struct CommandDialect {
std::string_view id; // "gnu" | "msvc"
// Flag spellings. Prefixes are concatenated with their (already
// ninja-escaped) argument by the caller.
std::string_view includePrefix; // "-I" | "/I"
std::string_view definePrefix; // "-D" | "/D"
std::string_view stdPrefix; // "-std=" | "/std:"
std::string_view compileOnly; // "-c" | "/c"
std::string_view outputObjPrefix; // "-o " | "/Fo:"
std::string_view optPrefix; // "-O" | "/O"
std::string_view debugFlags; // "-g" | "/Zi /FS"
std::string_view alwaysFlags; // "" | "/nologo /EHsc /utf-8"
// Same rationale as forceCxxLangArgv: the argv consumer gets tokens, not
// a string it has to split. Both spans point at static arrays below.
std::span<const std::string_view> alwaysFlagsArgv;
// Link and language-selection spellings.
//
// `libFlag` is a FORMAT, not a prefix: GNU names a library by prefixing
// (`-lz`) while MSVC names it by suffixing (`z.lib`), and no single
// prefix string can express both. Use lib_flag_for().
std::string_view libFlag; // "-l{}" | "{}.lib"
std::string_view libSearchPrefix; // "-L" | "/LIBPATH:"
// The `.mcpp` extension is unknown to every compiler driver, so the
// language has to be forced or the driver hands the file to the linker.
//
// Two forms of the same thing, because the two consumers need different
// shapes and neither should re-derive the other's: a ninja command line
// wants one string, an argv vector wants tokens. Storing both keeps the
// spelling in one row — splitting the string at the call site would put
// the token boundary in a second place, and cost a helper this file is
// better off without (see the note on `alwaysFlagsArgv`).
std::string_view forceCxxLang; // "-x c++" | "/TP"
std::span<const std::string_view> forceCxxLangArgv;
// Per-FILE language force, for a command line that also carries object
// files. The two drivers differ structurally, not just in spelling: GNU's
// `-x c++` is positional and stays in effect until `-x none`, while
// cl.exe's `/TP` applies to EVERY input — so an object listed after it is
// fed to the C++ frontend and dies with C2018. cl's per-file form is
// `/Tp<file>`; GNU has none, and uses the positional pair plus a reset.
// Empty means "no per-file form — use forceCxxLangArgv and reset after".
std::string_view perFileCxxPrefix; // "" | "/Tp"
// Static CRT / runtime. On MSVC this is a compile-time CRT model, not a
// link mode — there is no /MT equivalent of `-static` for the whole image.
std::string_view staticRuntime; // "-static"| "/MT"
// The MSVC CRT model for the OTHER linkage. Only meaningful on the MSVC
// dialect; GNU has no counterpart (the empty string).
std::string_view dynamicRuntime; // "" | "/MD"
// Output an executable (linking driver step).
std::string_view outputExePrefix; // "-o " | "/Fe:"
// Artifact naming.
std::string_view objExt; // ".o" | ".obj"
// Ninja integration.
// deps mode for header dependencies ("" = none today for gnu — module
// deps go through the P1689/dyndep pipeline instead).
std::string_view ninjaDepsMode; // "" | "msvc"
// Whether link/archive command lines must go through a response file
// (Windows 8191-char command-line limit; cl/link/lib accept @file).
bool rspfileLink = false;
// Link step shape: the compiler driver links (g++/clang++ $in -o $out)
// or a separate linker is invoked (link.exe /OUT:).
enum class LinkStyle { Driver, SeparateLinker };
LinkStyle linkStyle = LinkStyle::Driver;
// Full ninja command template for static archives.
std::string_view archiveCmd; // "$ar rcs $out $in" | "$ar /nologo /OUT:$out $in"
// How this archiver DELETES a member, as argv words before the member
// names. `mcpp pack` needs it: a library package publishes its interface
// units as source, so their objects have to come OUT of the archive, or the
// consumer links two definitions of each published module's initialiser.
//
// It lives here rather than in the packer because "how do you speak to the
// archiver" is exactly what this table is for. Hard-coding `ar`-style `d`
// there would be right for GNU and llvm-ar and wrong for LIB.EXE, which
// spells it `/REMOVE:<member>` and takes one flag per member — a difference
// in ARITY as well as in spelling, which is why this is a template with a
// placeholder rather than a prefix string.
//
// `{}` is substituted with the member name; the words are joined with
// spaces after the archive path.
std::string_view archiveRemoveArg; // "d" needs no {} | "/REMOVE:{}"
bool archiveRemoveTakesArchiveFirst = true;
// THE IMPORT-LIBRARY AND `.def` SPELLINGS ARE NOT HERE, on purpose.
//
// They were, keyed on the dialect, and that is wrong in a way Windows CI
// demonstrated: clang targeting the MSVC ABI speaks the GNU DIALECT while
// driving LLD-LINK, so it was handed `-Wl,--out-implib,` and answered
// `lld-link: warning: ignoring unknown argument '--out-implib'`. The
// spelling follows the target ABI, which this table does not know — see
// ninja_backend's pe_link_flag.
};
// Dialect lookup. GCC / Clang / MinGW → gnu; MSVC → msvc.
const CommandDialect& dialect_for(const Toolchain& tc);
// The MSVC CRT model for a given linkage, in ONE place.
//
// cl bakes `_MSVC_MT` / `_MSVC_MD` into every module it produces, so the std
// module and the TUs importing it must agree. They were derived in two places:
// the project's TUs from `flags.cppm` and the std module from cl's own default
// (`/MT`, because the std build passed no flag at all). A project on the default
// dynamic linkage therefore imported a `/MT` std, which cl accepts with a C5050
// warning and then fails on for real inside the ucrt headers — #422.
//
// Same shape as `macos_deployment_target`, which `stdmod::ensure_built` already
// takes for exactly this reason on the other platform.
constexpr std::string_view msvc_crt_flag(const CommandDialect& d, bool staticCrt) {
return staticCrt ? d.staticRuntime : d.dynamicRuntime;
}
// Does this build want MSVC's STATIC CRT? TWO manifest keys answer it:
//
// [build] linkage = "static" the libc axis
// [build] cxx_runtime = "self-contained" the C++ runtime axis
//
// That is not a redundancy to be picked between — on the MSVC ABI they are
// physically ONE switch. `/MT` links the C runtime and the C++ runtime out of
// the same library, and there is no way to have one static and the other
// dynamic. distribution.cppm's PE/MinGW branch already says exactly this
// about `-static`; MSVC is the same statement, and used to disagree with
// itself about it (`linkage` emitted /MT while `cxx_runtime` reported "not
// implemented" — for the same physical outcome).
//
// `cxxRuntime` is the RAW manifest scalar, and must stay that way: the
// resolved Contract defaults to SelfContained for most roles, so keying off
// it would flip every Windows build from /MD to /MT. Empty here means nobody
// wrote it down, which is the only reading under which "explicit" is honest.
//
// Asking the question once is also what keeps the project's TUs and the std
// module from disagreeing (#422): both callers ask THIS, rather than each
// spelling out `linkage == "static"` and drifting the next time a key is
// added — which is precisely how the second key came to be ignored.
constexpr bool msvc_wants_static_crt(std::string_view linkage,
std::string_view cxxRuntime) {
return linkage == "static" || cxxRuntime == "self-contained";
}
// THE CRT MODEL WORD, SPELLED FOR WHICHEVER DRIVER `tc` IS.
//
// Every MSVC-ABI row now receives the model — cl and clang++ targeting
// `*-windows-msvc` alike (#649 E10, #718) — because the CRT is a property of
// the target ABI, not of the compiler. `dialect_for` still gives clang the
// GNU dialect (its `-I`/`-D`/... spellings are unchanged), so this is NOT
// `msvc_crt_flag(dialect_for(tc), ...)`: the GNU dialect's own
// `staticRuntime` is `-static`, full static linking, a different axis this
// function must not be confused with.
//
// cl.exe /MT | /MD
// clang++ *-windows-msvc -fms-runtime-lib=static | -fms-runtime-lib=dll
// every other row "" (MinGW links the MSVC CRT to no row at all)
//
// ONE HELPER, so the translation units (flags.cppm), the std and std.compat
// BMIs (stdmod.cppm via clang.cppm/msvc.cppm) and the link command
// (flags.cppm's `LinkShape::PeLld` branch) cannot spell three different
// answers to the same question — which is exactly how E10 arose: a flag
// given only at compile time does not reach clang's link step, which chooses
// `-defaultlib:` on its own (measured, `2026-09-16-646-649-four-issues-by-
// home.md` §4.5).
std::string msvc_abi_crt_word(const Toolchain& tc, bool staticCrt);
// A FREE-FORM CRT WORD IS ALWAYS A SECOND STATEMENT (D3, #718).
//
// Every MSVC-ABI build now states its own CRT model, so a literal spelling of
// it in `[build] cxxflags` or `dialect_cxxflags` — cl's `/MT`/`/MD`(`d`) or
// clang's `-fms-runtime-lib=*` (either dash) — can never be the only voice:
// either it repeats what mcpp already resolved, or it contradicts it, and the
// engine must never let the last word on the command line decide silently.
//
// `word` is one token already read out of a flags list (SPEC-004 §8's
// per-word reading). `key` names where the caller found it, so the message
// can point back at it. Returns nullopt for a spelling this function does not
// recognise as a CRT word — it says nothing about flags outside this axis.
struct CrtWordVerdict {
bool contradicts;
std::string message;
};
std::optional<CrtWordVerdict> check_crt_word(std::string_view word,
bool staticCrt,
std::string_view key);
// The two dialect rows, reachable without a Toolchain. Exposed so the MSVC
// row — which no build reaches until the cl.exe backend lands — can still be
// unit-tested, and so callers that already know the shape they want (the
// build.mcpp host compile) need not synthesize a Toolchain to ask.
const CommandDialect& gnu_dialect();
const CommandDialect& msvc_dialect();
// Name a library the way this dialect does: `-lz` vs `z.lib`.
std::string lib_flag_for(const CommandDialect& d, std::string_view name);
// The full -std=/-/std: flag for a normalized standard (canonical like
// "c++26"/"gnu++23", numeric level). MSVC: /std:c++20 exists; everything
// newer maps to /std:c++latest (required for import std); gnu dialects have
// no MSVC equivalent and take the same mapping.
std::string std_flag_for(const CommandDialect& d,
std::string_view canonical, int level);
} // namespace mcpp::toolchain
namespace mcpp::toolchain {
namespace {
// Token forms of the multi-token rows. Static arrays so the spans above are
// constexpr-initializable and no consumer has to split a string at runtime.
constexpr std::string_view kGnuForceCxxArgv[] = {"-x", "c++"};
constexpr std::string_view kMsvcForceCxxArgv[] = {"/TP"};
constexpr std::string_view kMsvcAlwaysArgv[] = {"/nologo", "/EHsc", "/utf-8"};
constexpr CommandDialect kGnuDialect{
.id = "gnu",
.includePrefix = "-I",
.definePrefix = "-D",
.stdPrefix = "-std=",
.compileOnly = "-c",
.outputObjPrefix = "-o ",
.optPrefix = "-O",
.debugFlags = "-g",
.alwaysFlags = "",
.alwaysFlagsArgv = {},
.libFlag = "-l{}",
.libSearchPrefix = "-L",
.forceCxxLang = "-x c++",
.forceCxxLangArgv = kGnuForceCxxArgv,
.perFileCxxPrefix = "",
.staticRuntime = "-static",
.outputExePrefix = "-o ",
.objExt = ".o",
.ninjaDepsMode = "",
.rspfileLink = false,
.linkStyle = CommandDialect::LinkStyle::Driver,
.archiveCmd = "$ar rcs $out $in",
// `ar d <archive> <member>...` — one verb, then every member.
.archiveRemoveArg = "d",
.archiveRemoveTakesArchiveFirst = true,
};
// Native cl.exe. Unreachable in builds until the MSVC backend lands
// (prepare.cppm gates CompilerId::MSVC) — the row exists so the data model
// is complete and unit-tested ahead of that work.
constexpr CommandDialect kMsvcDialect{
.id = "msvc",
.includePrefix = "/I",
.definePrefix = "/D",
.stdPrefix = "/std:",
.compileOnly = "/c",
.outputObjPrefix = "/Fo:",
.optPrefix = "/O",
.debugFlags = "/Zi /FS",
.alwaysFlags = "/nologo /EHsc /utf-8",
.alwaysFlagsArgv = kMsvcAlwaysArgv,
.libFlag = "{}.lib",
.libSearchPrefix = "/LIBPATH:",
.forceCxxLang = "/TP",
.forceCxxLangArgv = kMsvcForceCxxArgv,
.perFileCxxPrefix = "/Tp",
.staticRuntime = "/MT",
.dynamicRuntime = "/MD",
.outputExePrefix = "/Fe:",
.objExt = ".obj",
.ninjaDepsMode = "msvc",
.rspfileLink = true,
.linkStyle = CommandDialect::LinkStyle::SeparateLinker,
.archiveCmd = "$ar /nologo /OUT:$out $in",
// `LIB /REMOVE:<member> … <archive>` — one flag PER member, and the
// archive last. Untested against a real LIB.EXE (mcpp's Windows CI packs
// with clang, whose llvm-ar takes the GNU form), so a failure here is
// reported with the command that produced it rather than swallowed.
.archiveRemoveArg = "/REMOVE:{}",
.archiveRemoveTakesArchiveFirst = false,
};
} // namespace
const CommandDialect& dialect_for(const Toolchain& tc) {
if (tc.compiler == CompilerId::MSVC) return kMsvcDialect;
return kGnuDialect;
}
const CommandDialect& gnu_dialect() { return kGnuDialect; }
const CommandDialect& msvc_dialect() { return kMsvcDialect; }
std::string msvc_abi_crt_word(const Toolchain& tc, bool staticCrt) {
if (tc.compiler == CompilerId::MSVC)
return std::string(msvc_crt_flag(msvc_dialect(), staticCrt));
if (is_msvc_target(tc))
return staticCrt ? "-fms-runtime-lib=static" : "-fms-runtime-lib=dll";
return {};
}
std::string lib_flag_for(const CommandDialect& d, std::string_view name) {
// Two shapes, one table entry: `{}` marks where the name goes, which is
// a prefix position for GNU and a suffix position for MSVC.
std::string out(d.libFlag);
if (auto p = out.find("{}"); p != std::string::npos)
out.replace(p, 2, name);
return out;
}
std::string std_flag_for(const CommandDialect& d,
std::string_view canonical, int level) {
if (d.id == "msvc") {
if (level <= 20) return "/std:c++20";
return "/std:c++latest";
}
return std::format("{}{}", d.stdPrefix, canonical);
}
std::optional<CrtWordVerdict> check_crt_word(std::string_view word,
bool staticCrt,
std::string_view key) {
// Both dash conventions: cl.exe accepts `-MD` exactly as it does `/MD`,
// and a manifest that targets more than one driver from one list (SPEC-
// 004 §8) should not have to spell the word twice.
std::optional<bool> wantsStatic;
if (word == "/MT" || word == "-MT" || word == "/MTd" || word == "-MTd"
|| word == "-fms-runtime-lib=static"
|| word == "-fms-runtime-lib=static_dbg") {
wantsStatic = true;
} else if (word == "/MD" || word == "-MD" || word == "/MDd" || word == "-MDd"
|| word == "-fms-runtime-lib=dll"
|| word == "-fms-runtime-lib=dll_dbg") {
wantsStatic = false;
} else {
return std::nullopt;
}
// A DEBUG CRT WORD IS NEVER REDUNDANT. The model states static or dynamic
// and nothing about debug (that axis is deferred, docs/20), while the std
// module and the link are built against the release CRT it resolved. A
// debug word therefore compiles its units against a CRT nothing else uses.
const bool debugWord = word.ends_with("d") || word.ends_with("_dbg");
if (debugWord) {
return CrtWordVerdict{true, std::format(
"`{}` in {} asks for a debug CRT, which the CRT model does not "
"express; the standard library module and the link use {}. Remove "
"the flag",
word, key, staticCrt ? "the static CRT, /MT" : "the dynamic CRT, /MD")};
}
std::string_view wordValue =
*wantsStatic ? "the static CRT (/MT)" : "the dynamic CRT (/MD)";
if (*wantsStatic == staticCrt) {
return CrtWordVerdict{false, std::format(
"`{}` in {} agrees with the CRT model this build already "
"resolved and says nothing new. Write {} instead if {} should "
"stay an explicit statement, and drop the flag",
word, key,
staticCrt ? "`cxx_runtime = \"self-contained\"` (or `linkage = \"static\"`)"
: "`cxx_runtime = \"toolchain-coupled\"` (or `\"host-coupled\"`)",
wordValue)};
}
return CrtWordVerdict{true, std::format(
"`{}` in {} asks for {}, which contradicts the CRT model this build "
"already resolved ({}). Every MSVC-ABI row now states its own CRT, "
"so the word can never be the only voice: remove it, or change "
"`cxx_runtime`/`linkage` to match",
word, key, wordValue,
staticCrt ? "the static CRT, /MT" : "the dynamic CRT, /MD")};
}
} // namespace mcpp::toolchain