-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathdialect.cppm
More file actions
286 lines (255 loc) · 13.2 KB
/
Copy pathdialect.cppm
File metadata and controls
286 lines (255 loc) · 13.2 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
// 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 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 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);
}
} // namespace mcpp::toolchain