Skip to content

Commit 47e45d6

Browse files
committed
refactor(toolchain): CommandDialect + backend surface normalization (Part A)
New mcpp.toolchain.dialect: command-line *spellings* (gnu vs msvc) as a value-type traits table — flags.cppm and ninja rule templates now spell -I/-D/-std=/-c/-o/-O/-g/ar-invocations through it instead of hardcoding GNU syntax. BmiTraits gains the module-flag spellings (compileModulesFlag, stdBmiUsePrefix, moduleOutputPrefix, bmiSearchPrefix), replacing the is_clang()/stdlib_id branches in flags.cppm. ProviderCapabilities gains has_builtin_p1689_scan (drops ninja_backend's is_gcc gate). Toolchain gains envOverrides (EnvVar list — std::pair member trips GCC-16 module pendings) merged into ninja's child env; the MSVC backend will carry INCLUDE/LIB/PATH through it. gcc::std_module_build_commands normalizes the provider surface to the command-sequence shape clang already has. resolve_link_model returns the empty PE model explicitly on Windows. Zero-diff verified: build.ninja byte-identical (modulo the mcpp self-path var) for GCC and LLVM toolchains on a multi-module import-std project; mcpp test 31/31.
1 parent 1d646ac commit 47e45d6

8 files changed

Lines changed: 268 additions & 47 deletions

File tree

‎src/build/flags.cppm‎

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import mcpp.build.plan;
1616
import mcpp.platform;
1717
import mcpp.toolchain.clang;
1818
import mcpp.toolchain.detect;
19+
import mcpp.toolchain.dialect;
1920
import mcpp.toolchain.linkmodel;
2021
import mcpp.toolchain.provider;
2122
import mcpp.toolchain.registry;
@@ -122,10 +123,13 @@ std::string atomic_link_flag(const std::vector<std::filesystem::path>& linkDirs,
122123
CompileFlags compute_flags(const BuildPlan& plan) {
123124
CompileFlags f;
124125

125-
// ProviderCapabilities: centralised query point for per-toolchain decisions.
126-
// Prefer caps.* checks over ad-hoc is_clang()/is_musl_target() calls for
127-
// any new branching added to this function.
126+
// Central query points for per-toolchain decisions — prefer these over
127+
// ad-hoc is_clang()/is_gcc() calls:
128+
// caps — what the toolchain can do (scan-deps, stdlib id, …)
129+
// d — how a flag is SPELT (GNU "-I" vs MSVC "/I")
130+
// traits — BMI mechanics + module-flag spellings
128131
auto caps = mcpp::toolchain::capabilities_for(plan.toolchain);
132+
const auto& d = mcpp::toolchain::dialect_for(plan.toolchain);
129133

130134
// macOS minimum supported OS version for produced binaries.
131135
// Precedence: MACOSX_DEPLOYMENT_TARGET env (explicit per-invocation
@@ -152,7 +156,7 @@ CompileFlags compute_flags(const BuildPlan& plan) {
152156
std::string include_flags;
153157
for (auto& inc : plan.manifest.buildConfig.includeDirs) {
154158
auto abs = inc.is_absolute() ? inc : (plan.projectRoot / inc);
155-
include_flags += " -I" + escape_path(abs);
159+
include_flags += std::format(" {}{}", d.includePrefix, escape_path(abs));
156160
}
157161

158162
// Sysroot / payload paths — resolved ONCE by the toolchain link model
@@ -208,11 +212,11 @@ CompileFlags compute_flags(const BuildPlan& plan) {
208212
f.sysroot = link_toolchain_flags;
209213
}
210214

211-
// Binutils -B flag
215+
// Binutils -B flag — a GCC/libstdc++ payload concern (musl bundles its
216+
// own as/ld; Clang and MSVC never take an external binutils).
212217
bool isMuslTc = mcpp::toolchain::is_musl_target(plan.toolchain);
213-
bool isClang = mcpp::toolchain::is_clang(plan.toolchain);
214218
std::filesystem::path binutilsBin;
215-
if (!isMuslTc && !isClang) {
219+
if (!isMuslTc && caps.stdlib_id == "libstdc++") {
216220
auto ar = mcpp::toolchain::archive_tool(plan.toolchain);
217221
if (!ar.empty())
218222
binutilsBin = ar.parent_path();
@@ -231,8 +235,8 @@ CompileFlags compute_flags(const BuildPlan& plan) {
231235
// unless the profile pins -O0.
232236
auto& prof = plan.manifest.buildConfig;
233237
std::string opt_flag = isMuslTc && prof.optLevel != "0"
234-
? " -Og" : (" -O" + prof.optLevel);
235-
if (prof.debug) opt_flag += " -g";
238+
? " -Og" : std::format(" {}{}", d.optPrefix, prof.optLevel);
239+
if (prof.debug) opt_flag += std::format(" {}", d.debugFlags);
236240
if (prof.lto) opt_flag += " -flto";
237241

238242
// User link flags
@@ -247,20 +251,25 @@ CompileFlags compute_flags(const BuildPlan& plan) {
247251
plan.manifest.buildConfig.cStandard.empty() ? "c11" : plan.manifest.buildConfig.cStandard;
248252

249253
// Assemble
250-
// -fmodules is a GCC-only flag; Clang uses a different module ABI and does
251-
// not need it. caps.stdlib_id distinguishes GCC (libstdc++) from Clang
252-
// (libc++ / msvc-stl) without an extra is_clang() call.
253-
std::string module_flag = (caps.stdlib_id == "libstdc++") ? " -fmodules" : "";
254+
// Module-flag spellings come from BmiTraits: GCC needs -fmodules on every
255+
// TU (BMIs implicit); Clang/MSVC reference the staged std BMI and a BMI
256+
// search dir explicitly (spelled -fmodule-file=/-fprebuilt-module-path vs
257+
// /reference//ifcSearchDir).
258+
auto traits = mcpp::toolchain::bmi_traits(plan.toolchain);
259+
std::string module_flag{traits.compileModulesFlag};
254260
std::string std_module_flag;
255-
if (isClang && !plan.stdBmiPath.empty()) {
256-
std_module_flag = " -fmodule-file=std=" + escape_path(staged_std_bmi_path(plan));
261+
if (!traits.stdBmiUsePrefix.empty() && !plan.stdBmiPath.empty()) {
262+
std_module_flag = std::string(traits.stdBmiUsePrefix)
263+
+ escape_path(staged_std_bmi_path(plan));
257264
}
258265
std::string std_compat_module_flag;
259-
if (isClang && !plan.stdCompatBmiPath.empty()) {
266+
if (!traits.stdCompatBmiUsePrefix.empty() && !plan.stdCompatBmiPath.empty()) {
267+
// NOTE: staging path is Clang's today; registry-dispatch when the
268+
// MSVC backend lands (std.compat.ixx staging).
260269
auto compatDst = mcpp::toolchain::clang::staged_std_compat_bmi_path(plan.outputDir);
261-
std_compat_module_flag = " -fmodule-file=std.compat=" + escape_path(compatDst);
270+
std_compat_module_flag = std::string(traits.stdCompatBmiUsePrefix)
271+
+ escape_path(compatDst);
262272
}
263-
auto traits = mcpp::toolchain::bmi_traits(plan.toolchain);
264273
std::string prebuilt_module_flag;
265274
if (traits.needsPrebuiltModulePath) {
266275
// Absolute path: a bare `pcm.cache` / `gcm.cache` works at ninja
@@ -272,22 +281,32 @@ CompileFlags compute_flags(const BuildPlan& plan) {
272281
// resolution fails with `module 'X' not found`. The other
273282
// `-fmodule-file=` flags in this block are already escape_path'd
274283
// (absolute) for the same reason — this one was a leftover.
275-
prebuilt_module_flag = std::format(" -fprebuilt-module-path={}",
276-
escape_path(plan.outputDir / traits.bmiDir));
284+
prebuilt_module_flag = std::string(traits.bmiSearchPrefix)
285+
+ escape_path(plan.outputDir / traits.bmiDir);
277286
}
278287
std::string cxx_std_flag =
279-
plan.cppStandardFlag.empty() ? std::string("-std=c++23") : plan.cppStandardFlag;
288+
plan.cppStandardFlag.empty()
289+
? std::format("{}c++23", d.stdPrefix) : plan.cppStandardFlag;
280290
f.cxx = std::format("{}{}{}{}{}{}{}{}{}{}", cxx_std_flag, module_flag, std_module_flag,
281291
std_compat_module_flag, prebuilt_module_flag,
282292
opt_flag, pic_flag, compile_toolchain_flags, b_flag, include_flags);
283-
f.cc = std::format("-std={}{}{}{}{}{}", c_std, opt_flag, pic_flag, compile_toolchain_flags,
284-
b_flag, include_flags);
293+
f.cc = std::format("{}{}{}{}{}{}{}", d.stdPrefix, c_std, opt_flag, pic_flag,
294+
compile_toolchain_flags, b_flag, include_flags);
285295

286296
// Link flags
287297
f.staticStdlib = plan.manifest.buildConfig.staticStdlib;
288298
f.linkage = plan.manifest.buildConfig.linkage;
289299
std::string full_static = (mcpp::platform::supports_full_static && f.linkage == "static") ? " -static" : "";
290-
std::string static_stdlib = (f.staticStdlib && !isClang && !mcpp::platform::is_windows) ? " -static-libstdc++" : "";
300+
// Static C++ stdlib: a libstdc++ (GCC/MinGW) concern. On Windows a MinGW
301+
// toolchain also statically links libgcc, so produced binaries don't
302+
// depend on libstdc++-6.dll / libgcc_s DLLs from the toolchain dir
303+
// (portable-by-default, same spirit as the macOS static-libc++ path).
304+
std::string static_stdlib;
305+
if (f.staticStdlib && caps.stdlib_id == "libstdc++") {
306+
static_stdlib = " -static-libstdc++";
307+
if constexpr (mcpp::platform::is_windows)
308+
static_stdlib += " -static-libgcc";
309+
}
291310
std::string runtime_dirs;
292311
if constexpr (mcpp::platform::supports_rpath) {
293312
// Toolchain runtime dirs (glibc/gcc) as before...
@@ -320,7 +339,16 @@ CompileFlags compute_flags(const BuildPlan& plan) {
320339
if (prof.strip) link_extra += " -s";
321340

322341
if constexpr (mcpp::platform::is_windows) {
323-
f.ld = user_ldflags + link_extra;
342+
// PE link: no rpath/loader/payload model. MSVC-ABI Clang needs
343+
// nothing extra (MSVC STL/SDK via the driver); MinGW adds the static
344+
// libstdc++/libgcc pair (static_stdlib above) and -B so its own
345+
// binutils resolve, plus `-static` for full static when requested
346+
// (MinGW supports it, unlike MSVC-ABI links).
347+
std::string mingw_static =
348+
(caps.stdlib_id == "libstdc++" && f.linkage == "static")
349+
? " -static" : "";
350+
f.ld = std::format("{}{}{}{}{}", mingw_static, static_stdlib, b_flag,
351+
user_ldflags, link_extra);
324352
} else if constexpr (mcpp::platform::needs_explicit_libcxx) {
325353
// macOS. Two min-version concerns (see xlings
326354
// .agents/docs/2026-06-05-macos-min-version-support.md):

‎src/build/ninja_backend.cppm‎

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import mcpp.build.hermetic;
2525
import mcpp.build.compile_commands;
2626
import mcpp.dyndep;
2727
import mcpp.toolchain.detect;
28+
import mcpp.toolchain.dialect;
29+
import mcpp.toolchain.provider;
2830
import mcpp.toolchain.registry;
2931
import mcpp.xlings;
3032
import mcpp.platform;
@@ -258,10 +260,12 @@ std::string emit_ninja_string(const BuildPlan& plan) {
258260
// dyndep requires P1689 scanning capability:
259261
// GCC: built-in -fdeps-format=p1689r5
260262
// Clang: external clang-scan-deps tool (same P1689 output format)
261-
bool has_scanner = mcpp::toolchain::is_gcc(plan.toolchain)
262-
|| !plan.scanDepsPath.empty();
263+
// (MSVC /scanDependencies is the future third driver — scanner design §3a)
264+
auto caps = mcpp::toolchain::capabilities_for(plan.toolchain);
265+
bool has_scanner = caps.has_builtin_p1689_scan || !plan.scanDepsPath.empty();
263266
bool dyndep = dyndep_mode_enabled() && has_scanner;
264267
auto traits = mcpp::toolchain::bmi_traits(plan.toolchain);
268+
const auto& dial = mcpp::toolchain::dialect_for(plan.toolchain);
265269
std::string out;
266270
auto append = [&](std::string s) { out += std::move(s); };
267271

@@ -331,53 +335,65 @@ std::string emit_ninja_string(const BuildPlan& plan) {
331335
// Runtime library paths for private toolchain executables are scoped onto
332336
// the ninja subprocess instead of being emitted into each visible rule.
333337

338+
// Command spellings come from the toolchain's CommandDialect (gnu vs
339+
// msvc); the rule *structure* is shared across compilers.
334340
std::string module_output_flag = traits.needsExplicitModuleOutput
335-
? " -fmodule-output=$bmi_out" : "";
341+
? std::string(traits.moduleOutputPrefix) + "$bmi_out" : "";
342+
const std::string compile_tail = std::format(
343+
"{} $in {}$out", dial.compileOnly, dial.outputObjPrefix);
336344
append("rule cxx_module\n");
337345
if constexpr (mcpp::platform::is_windows) {
338346
// Windows: skip BMI restat optimization (requires POSIX shell).
339347
append(std::format(" command = "
340-
"$cxx $local_includes $cxxflags $unit_cxxflags{} -c $in -o $out\n", module_output_flag));
348+
"$cxx $local_includes $cxxflags $unit_cxxflags{} {}\n",
349+
module_output_flag, compile_tail));
341350
} else {
342351
append(std::format(" command = "
343352
"if [ -n \"$bmi_out\" ] && [ -f \"$bmi_out\" ]; then "
344353
"cp -p \"$bmi_out\" \"$bmi_out.bak\"; "
345354
"fi && "
346-
"$cxx $local_includes $cxxflags $unit_cxxflags{} -c $in -o $out && "
355+
"$cxx $local_includes $cxxflags $unit_cxxflags{} {} && "
347356
"if [ -n \"$bmi_out\" ] && [ -f \"$bmi_out.bak\" ] && "
348357
"cmp -s \"$bmi_out\" \"$bmi_out.bak\"; then "
349358
"mv \"$bmi_out.bak\" \"$bmi_out\"; "
350359
"else "
351360
"rm -f \"$bmi_out.bak\"; "
352-
"fi\n", module_output_flag));
361+
"fi\n", module_output_flag, compile_tail));
353362
}
354363
append(" description = MOD $out\n");
355364
if (dyndep)
356365
append(" restat = 1\n");
357366
append("\n");
358367

359368
append("rule cxx_object\n");
360-
append(" command = $cxx $local_includes $cxxflags $unit_cxxflags -c $in -o $out\n");
369+
append(std::format(
370+
" command = $cxx $local_includes $cxxflags $unit_cxxflags {}\n",
371+
compile_tail));
361372
append(" description = OBJ $out\n");
362373
if (dyndep)
363374
append(" restat = 1\n");
364375
append("\n");
365376

366377
if (need_c_rule) {
367378
append("rule c_object\n");
368-
append(" command = $cc $local_includes $cflags $unit_cflags -c $in -o $out\n");
379+
append(std::format(
380+
" command = $cc $local_includes $cflags $unit_cflags {}\n",
381+
compile_tail));
369382
append(" description = CC $out\n");
370383
if (dyndep)
371384
append(" restat = 1\n");
372385
append("\n");
373386
}
374387

388+
// Link rule: driver-style today (g++/clang++ act as the linker). The
389+
// dialect's LinkStyle::SeparateLinker (link.exe /OUT: + rspfile) is the
390+
// MSVC backend's insertion point — unreachable until that lands.
375391
append("rule cxx_link\n");
376392
append(" command = $cxx $in -o $out $ldflags $unit_ldflags\n");
377393
append(" description = LINK $out\n\n");
378394

379395
append("rule cxx_archive\n");
380-
append(" command = $ar rcs $out $in\n");
396+
append(std::format(" command = {}\n", dial.archiveCmd));
381397
append(" description = AR $out\n\n");
382398

383399
append("rule cxx_shared\n");
@@ -811,6 +827,11 @@ std::expected<BuildResult, BuildError> NinjaBackend::build(const BuildPlan& plan
811827
std::vector<std::pair<std::string, std::string>> nenv;
812828
if (r.runtimeEnvKey != "-" && !r.runtimeEnvValue.empty())
813829
nenv.emplace_back(r.runtimeEnvKey, r.runtimeEnvValue);
830+
// Toolchain-declared env (empty for GCC/Clang; MSVC's INCLUDE/LIB/PATH).
831+
// NOTE: not persisted in the fast-path cache yet — revisit when the MSVC
832+
// backend lands (its fast path must re-derive these from detection).
833+
for (auto& ev : plan.toolchain.envOverrides)
834+
nenv.emplace_back(ev.key, ev.value);
814835

815836
auto cap = mcpp::platform::process::capture_exec(nargv, nenv);
816837
std::string out = cap.output;

‎src/toolchain/dialect.cppm‎

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// mcpp.toolchain.dialect — command-line *spelling* per compiler family.
2+
//
3+
// The single data point for how a flag is spelt: GNU driver style
4+
// (GCC / Clang / MinGW share one instance) vs MSVC cl.exe style. Consumers
5+
// (flags.cppm, ninja_backend.cppm, plan.cppm) concatenate these fragments
6+
// instead of hardcoding "-I"/"-o"/"ar rcs" — adding a compiler family is a
7+
// second row of data, not another if/else at every call site.
8+
//
9+
// Deliberately a value-type aggregate, not a class hierarchy — same
10+
// rationale as BmiTraits (2026-05-15-clang-parity-and-toolchain-abstraction
11+
// §2.3): the differences are strings and booleans, not behavior.
12+
//
13+
// See .agents/docs/2026-07-13-toolchain-backend-abstraction-msvc-mingw-design.md §2.1.
14+
15+
export module mcpp.toolchain.dialect;
16+
17+
import std;
18+
import mcpp.toolchain.model;
19+
20+
export namespace mcpp::toolchain {
21+
22+
struct CommandDialect {
23+
std::string_view id; // "gnu" | "msvc"
24+
25+
// Flag spellings. Prefixes are concatenated with their (already
26+
// ninja-escaped) argument by the caller.
27+
std::string_view includePrefix; // "-I" | "/I"
28+
std::string_view definePrefix; // "-D" | "/D"
29+
std::string_view stdPrefix; // "-std=" | "/std:"
30+
std::string_view compileOnly; // "-c" | "/c"
31+
std::string_view outputObjPrefix; // "-o " | "/Fo:"
32+
std::string_view optPrefix; // "-O" | "/O"
33+
std::string_view debugFlags; // "-g" | "/Zi /FS"
34+
std::string_view alwaysFlags; // "" | "/nologo /EHsc /utf-8"
35+
36+
// Artifact naming.
37+
std::string_view objExt; // ".o" | ".obj"
38+
39+
// Ninja integration.
40+
// deps mode for header dependencies ("" = none today for gnu — module
41+
// deps go through the P1689/dyndep pipeline instead).
42+
std::string_view ninjaDepsMode; // "" | "msvc"
43+
// Whether link/archive command lines must go through a response file
44+
// (Windows 8191-char command-line limit; cl/link/lib accept @file).
45+
bool rspfileLink = false;
46+
47+
// Link step shape: the compiler driver links (g++/clang++ $in -o $out)
48+
// or a separate linker is invoked (link.exe /OUT:).
49+
enum class LinkStyle { Driver, SeparateLinker };
50+
LinkStyle linkStyle = LinkStyle::Driver;
51+
52+
// Full ninja command template for static archives.
53+
std::string_view archiveCmd; // "$ar rcs $out $in" | "$ar /nologo /OUT:$out $in"
54+
};
55+
56+
// Dialect lookup. GCC / Clang / MinGW → gnu; MSVC → msvc.
57+
const CommandDialect& dialect_for(const Toolchain& tc);
58+
59+
} // namespace mcpp::toolchain
60+
61+
namespace mcpp::toolchain {
62+
63+
namespace {
64+
65+
constexpr CommandDialect kGnuDialect{
66+
.id = "gnu",
67+
.includePrefix = "-I",
68+
.definePrefix = "-D",
69+
.stdPrefix = "-std=",
70+
.compileOnly = "-c",
71+
.outputObjPrefix = "-o ",
72+
.optPrefix = "-O",
73+
.debugFlags = "-g",
74+
.alwaysFlags = "",
75+
.objExt = ".o",
76+
.ninjaDepsMode = "",
77+
.rspfileLink = false,
78+
.linkStyle = CommandDialect::LinkStyle::Driver,
79+
.archiveCmd = "$ar rcs $out $in",
80+
};
81+
82+
// Native cl.exe. Unreachable in builds until the MSVC backend lands
83+
// (prepare.cppm gates CompilerId::MSVC) — the row exists so the data model
84+
// is complete and unit-tested ahead of that work.
85+
constexpr CommandDialect kMsvcDialect{
86+
.id = "msvc",
87+
.includePrefix = "/I",
88+
.definePrefix = "/D",
89+
.stdPrefix = "/std:",
90+
.compileOnly = "/c",
91+
.outputObjPrefix = "/Fo:",
92+
.optPrefix = "/O",
93+
.debugFlags = "/Zi /FS",
94+
.alwaysFlags = "/nologo /EHsc /utf-8",
95+
.objExt = ".obj",
96+
.ninjaDepsMode = "msvc",
97+
.rspfileLink = true,
98+
.linkStyle = CommandDialect::LinkStyle::SeparateLinker,
99+
.archiveCmd = "$ar /nologo /OUT:$out $in",
100+
};
101+
102+
} // namespace
103+
104+
const CommandDialect& dialect_for(const Toolchain& tc) {
105+
if (tc.compiler == CompilerId::MSVC) return kMsvcDialect;
106+
return kGnuDialect;
107+
}
108+
109+
} // namespace mcpp::toolchain

0 commit comments

Comments
 (0)