Skip to content

Commit 3470d67

Browse files
committed
fix(build): dialect-class flags reach the std BMI prebuild and the whole module graph (#210)
[build].cxxflags like -freflection change what libstdc++ headers DECLARE (<meta> is gated on __cpp_impl_reflection) — they are module-graph dialect, same nature as -std=. They now ride -std='s channels: the global $cxxflags (every TU incl. dependency modules — fixes the fmt.gcm-class secondary failure), the std/std.compat prebuild command, and scans. Known-list auto-promotion (reflection/contracts/char8_t/_GLIBCXX_USE_CXX11_ABI) + explicit [build] dialect_cxxflags escape hatch. The fingerprint already keyed these flags — only the command construction was missing them. Also: cppStandardFlag is now spelled per-dialect (std_flag_for — msvc /std:c++20 | /std:c++latest), groundwork for the native MSVC backend. Verified: issue #210's exact repro prints 'x 2 / y 3' via import std on gcc@16.1.0; e2e 98 covers both variants + std-module.json assertion.
1 parent b5609fe commit 3470d67

8 files changed

Lines changed: 230 additions & 3 deletions

File tree

src/build/flags.cppm

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,11 @@ CompileFlags compute_flags(const BuildPlan& plan) {
287287
std::string cxx_std_flag =
288288
plan.cppStandardFlag.empty()
289289
? std::format("{}c++23", d.stdPrefix) : plan.cppStandardFlag;
290-
f.cxx = std::format("{}{}{}{}{}{}{}{}{}{}", cxx_std_flag, module_flag, std_module_flag,
290+
// plan.dialectFlags rides right behind -std= (issue #210): module-graph-
291+
// global dialect flags reach every TU (deps included) via this global
292+
// cxxflags string, exactly like the standard flag itself.
293+
f.cxx = std::format("{}{}{}{}{}{}{}{}{}{}{}", cxx_std_flag, plan.dialectFlags,
294+
module_flag, std_module_flag,
291295
std_compat_module_flag, prebuilt_module_flag,
292296
opt_flag, pic_flag, compile_toolchain_flags, b_flag, include_flags);
293297
f.cc = std::format("{}{}{}{}{}{}{}", d.stdPrefix, c_std, opt_flag, pic_flag,

src/build/plan.cppm

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import mcpp.manifest;
1010
import mcpp.modgraph.graph;
1111
import mcpp.modgraph.scanner;
1212
import mcpp.toolchain.detect;
13+
import mcpp.toolchain.dialect;
1314
import mcpp.toolchain.fingerprint;
1415
import mcpp.platform;
1516

@@ -47,6 +48,10 @@ struct BuildPlan {
4748
mcpp::toolchain::Fingerprint fingerprint;
4849
std::string cppStandard = "c++23";
4950
std::string cppStandardFlag = "-std=c++23";
51+
// Module-graph-global dialect flags (issue #210), pre-joined with a
52+
// leading space per flag (e.g. " -freflection"). Rides -std='s channels:
53+
// global $cxxflags (all TUs incl. deps), std BMI prebuild, scans.
54+
std::string dialectFlags;
5055

5156
std::filesystem::path projectRoot; // where mcpp.toml lives
5257
std::filesystem::path outputDir; // target/<triple>/<fp>/
@@ -313,7 +318,13 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest,
313318
plan.fingerprint = fp;
314319
if (auto stdCfg = mcpp::manifest::normalize_cpp_standard(manifest.package.standard)) {
315320
plan.cppStandard = stdCfg->canonical;
316-
plan.cppStandardFlag = stdCfg->flag;
321+
// Spelled per-dialect: "-std=c++26" (gnu) vs "/std:c++latest" (msvc).
322+
plan.cppStandardFlag = mcpp::toolchain::std_flag_for(
323+
mcpp::toolchain::dialect_for(tc), stdCfg->canonical, stdCfg->level);
324+
}
325+
for (auto& f : mcpp::manifest::dialect_flags(manifest.buildConfig)) {
326+
plan.dialectFlags += ' ';
327+
plan.dialectFlags += f;
317328
}
318329
plan.projectRoot = projectRoot;
319330
plan.outputDir = outputDir;

src/build/prepare.cppm

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import mcpp.modgraph.scanner;
1717
import mcpp.modgraph.validate;
1818
import mcpp.toolchain.clang;
1919
import mcpp.toolchain.detect;
20+
import mcpp.toolchain.dialect;
2021
import mcpp.toolchain.fingerprint;
2122
import mcpp.toolchain.msvc;
2223
import mcpp.toolchain.registry;
@@ -202,6 +203,12 @@ std::string canonical_compile_flags(const mcpp::manifest::Manifest& m) {
202203
s += " cxxflag:";
203204
s += flag;
204205
}
206+
// Explicit [build] dialect_cxxflags (auto-promoted ones are already in
207+
// cxxflags above) — they change every BMI in the graph.
208+
for (auto const& flag : m.buildConfig.dialectCxxflags) {
209+
s += " dialect:";
210+
s += flag;
211+
}
205212
for (auto const& flag : m.buildConfig.ldflags) {
206213
s += " ldflag:";
207214
s += flag;
@@ -2633,8 +2640,21 @@ prepare_build(bool print_fingerprint,
26332640
std::filesystem::path stdCompatBmiPath;
26342641
std::filesystem::path stdCompatObjectPath;
26352642
if (needsStdModule) {
2643+
// The std BMI must be compiled with the SAME dialect set its
2644+
// importers use (issue #210: -freflection gates libstdc++'s <meta> —
2645+
// a std BMI built without it structurally lacks std::meta). The
2646+
// standard flag is spelled per-dialect and the graph-global dialect
2647+
// flags ride along; both were already in the fingerprint, so this
2648+
// only fixes the COMMAND construction the fingerprint promised.
2649+
std::string stdFlagAndDialect = mcpp::toolchain::std_flag_for(
2650+
mcpp::toolchain::dialect_for(*tc),
2651+
m->cppStandard.canonical, m->cppStandard.level);
2652+
for (auto& f : mcpp::manifest::dialect_flags(m->buildConfig)) {
2653+
stdFlagAndDialect += ' ';
2654+
stdFlagAndDialect += f;
2655+
}
26362656
auto sm = mcpp::toolchain::ensure_built(
2637-
*tc, fp.hex, m->package.standard, m->cppStandard.flag,
2657+
*tc, fp.hex, m->package.standard, stdFlagAndDialect,
26382658
mcpp::platform::macos::deployment_target(
26392659
m->buildConfig.macosDeploymentTarget));
26402660
if (!sm) return std::unexpected(sm.error().message);

src/manifest/toml.cppm

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,11 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
642642
if (auto v = doc->get_bool("build.allow_host_libs")) m.buildConfig.allowHostLibs = *v;
643643
if (auto v = doc->get_string_array("build.cflags")) m.buildConfig.cflags = *v;
644644
if (auto v = doc->get_string_array("build.cxxflags")) m.buildConfig.cxxflags = *v;
645+
// Module-graph-global dialect flags (issue #210) — see types.cppm
646+
// dialect_flags(); this key is the explicit escape hatch for flags the
647+
// known-list doesn't recognize yet.
648+
if (auto v = doc->get_string_array("build.dialect_cxxflags"))
649+
m.buildConfig.dialectCxxflags = *v;
645650
if (auto v = doc->get_string_array("build.ldflags")) m.buildConfig.ldflags = *v;
646651
if (auto v = doc->get_string("build.c_standard")) m.buildConfig.cStandard = *v;
647652
if (auto v = doc->get_string("build.default-profile")) m.buildConfig.defaultProfile = *v;

src/manifest/types.cppm

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,15 @@ struct BuildConfig {
149149
std::vector<std::string> cflags;
150150
std::vector<std::string> cxxflags;
151151
std::vector<std::string> ldflags;
152+
// Dialect-class C++ flags: flags that change what the standard library's
153+
// headers DECLARE or participate in module dialect checks (issue #210's
154+
// -freflection: libstdc++'s <meta> is gated on __cpp_impl_reflection).
155+
// These are module-graph-global — they ride -std='s channels (global
156+
// cxxflags for every TU incl. deps, the std/std.compat BMI prebuild,
157+
// scan commands). Populated from [build] dialect_cxxflags plus
158+
// auto-promotion of known flags found in [build] cxxflags
159+
// (see dialect_flags()).
160+
std::vector<std::string> dialectCxxflags;
152161
std::string cStandard;
153162
// Escape hatch for the hermetic link check: a sandbox toolchain whose
154163
// CRT/loader resolve OUTSIDE the sandbox is a hard error by default
@@ -393,6 +402,16 @@ struct ManifestError {
393402

394403
std::expected<CppStandardConfig, std::string> normalize_cpp_standard(std::string_view raw);
395404

405+
// The module-graph-global dialect flag set: explicit [build] dialect_cxxflags
406+
// plus KNOWN dialect-class flags auto-promoted out of [build] cxxflags
407+
// (they also stay per-unit there — duplication is harmless and keeps the
408+
// mechanism explainable). Deduplicated, declaration order preserved.
409+
std::vector<std::string> dialect_flags(const BuildConfig& bc);
410+
411+
// True when `flag` belongs to the known dialect-class list (changes what
412+
// libstdc++/libc++ headers declare, or participates in BMI dialect checks).
413+
bool is_dialect_flag(std::string_view flag);
414+
396415
std::filesystem::path resolve_lib_root_path(const Manifest& manifest);
397416

398417
// True if the manifest declares at least one `kind = "lib"` target.
@@ -431,6 +450,34 @@ std::optional<std::string> validate_target_soname(const Target& t,
431450
}
432451

433452

453+
bool is_dialect_flag(std::string_view flag) {
454+
// Deliberately conservative first list (design doc §1.3a):
455+
// -fno-exceptions / -fno-rtti stay per-unit until separately reviewed
456+
// (dependencies may assume exceptions are available).
457+
static constexpr std::string_view exact[] = {
458+
"-freflection", "-fno-reflection", // P2996 (GCC 16+)
459+
"-fcontracts", "-fno-contracts", // P2900
460+
"-fchar8_t", "-fno-char8_t",
461+
};
462+
for (auto e : exact)
463+
if (flag == e) return true;
464+
// libstdc++ dual-ABI switch changes declared symbols/types wholesale.
465+
if (flag.starts_with("-D_GLIBCXX_USE_CXX11_ABI=")) return true;
466+
return false;
467+
}
468+
469+
std::vector<std::string> dialect_flags(const BuildConfig& bc) {
470+
std::vector<std::string> out;
471+
auto add = [&](const std::string& f) {
472+
if (std::find(out.begin(), out.end(), f) == out.end())
473+
out.push_back(f);
474+
};
475+
for (auto& f : bc.dialectCxxflags) add(f);
476+
for (auto& f : bc.cxxflags)
477+
if (is_dialect_flag(f)) add(f);
478+
return out;
479+
}
480+
434481
std::expected<CppStandardConfig, std::string> normalize_cpp_standard(std::string_view raw) {
435482
auto trim_copy = [](std::string_view input) {
436483
std::size_t begin = 0;

src/toolchain/dialect.cppm

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ struct CommandDialect {
5656
// Dialect lookup. GCC / Clang / MinGW → gnu; MSVC → msvc.
5757
const CommandDialect& dialect_for(const Toolchain& tc);
5858

59+
// The full -std=/-/std: flag for a normalized standard (canonical like
60+
// "c++26"/"gnu++23", numeric level). MSVC: /std:c++20 exists; everything
61+
// newer maps to /std:c++latest (required for import std); gnu dialects have
62+
// no MSVC equivalent and take the same mapping.
63+
std::string std_flag_for(const CommandDialect& d,
64+
std::string_view canonical, int level);
65+
5966
} // namespace mcpp::toolchain
6067

6168
namespace mcpp::toolchain {
@@ -106,4 +113,13 @@ const CommandDialect& dialect_for(const Toolchain& tc) {
106113
return kGnuDialect;
107114
}
108115

116+
std::string std_flag_for(const CommandDialect& d,
117+
std::string_view canonical, int level) {
118+
if (d.id == "msvc") {
119+
if (level <= 20) return "/std:c++20";
120+
return "/std:c++latest";
121+
}
122+
return std::format("{}{}", d.stdPrefix, canonical);
123+
}
124+
109125
} // namespace mcpp::toolchain
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#!/usr/bin/env bash
2+
# requires: gcc
3+
# 98_reflection_import_std.sh — issue #210: dialect-class flags reach the std
4+
# module BMI prebuild AND every TU in the graph:
5+
# - [build] cxxflags = ["-freflection"] → `import std;` exposes std::meta
6+
# - a dependency module that imports std builds in the same graph (the
7+
# fmt.gcm-class secondary failure from the issue)
8+
# - std-module.json records the flag in the std build command
9+
set -e
10+
11+
TMP=$(mktemp -d)
12+
trap "rm -rf $TMP" EXIT
13+
14+
# The resolved toolchain must accept -freflection (GCC 16+). Probe via the
15+
# project's own toolchain resolution: build a trivial reflection TU.
16+
GXX=$(find "${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs/xim-x-gcc" -name "g++" -path "*/bin/*" 2>/dev/null | sort | tail -1)
17+
if [[ -z "$GXX" ]] || ! echo 'int main(){}' | "$GXX" -freflection -std=c++26 -x c++ - -fsyntax-only -o /dev/null 2>/dev/null; then
18+
echo "SKIP-INLINE: no -freflection-capable gcc payload available"
19+
exit 0
20+
fi
21+
22+
mkdir -p "$TMP/proj/src" "$TMP/proj/meta_dep/src"
23+
cd "$TMP/proj"
24+
25+
# Path dependency providing a module that ITSELF imports std — must be
26+
# compiled with the same dialect set or GCC rejects the BMI mix.
27+
cat > meta_dep/mcpp.toml <<'EOF'
28+
[package]
29+
name = "meta_dep"
30+
version = "0.1.0"
31+
standard = "c++26"
32+
33+
[targets.meta_dep]
34+
kind = "lib"
35+
EOF
36+
cat > meta_dep/src/meta_dep.cppm <<'EOF'
37+
export module meta_dep;
38+
import std;
39+
export namespace meta_dep {
40+
std::string tag() { return "dep-ok"; }
41+
}
42+
EOF
43+
44+
cat > mcpp.toml <<'EOF'
45+
[package]
46+
name = "refl"
47+
version = "0.1.0"
48+
standard = "c++26"
49+
50+
[build]
51+
cxxflags = ["-freflection"]
52+
53+
[dependencies]
54+
meta_dep = { path = "meta_dep" }
55+
EOF
56+
cat > src/main.cpp <<'EOF'
57+
import std;
58+
import meta_dep;
59+
60+
void print_struct(auto &&value) {
61+
constexpr auto info = std::meta::remove_cvref(^^decltype(value));
62+
constexpr auto no_check = std::meta::access_context::unchecked();
63+
static constexpr auto members = std::define_static_array(
64+
std::meta::nonstatic_data_members_of(info, no_check));
65+
template for (constexpr auto e : members) {
66+
auto &&member = value.[:e:];
67+
std::println("{} {}", identifier_of(e), member);
68+
}
69+
}
70+
71+
struct Point { double x{}; double y{}; };
72+
int main() {
73+
print_struct(Point{2, 3});
74+
std::println("{}", meta_dep::tag());
75+
}
76+
EOF
77+
78+
out=$("$MCPP" run 2>&1) || { echo "FAIL: build/run: $out"; exit 1; }
79+
[[ "$out" == *"x 2"* && "$out" == *"y 3"* ]] || { echo "FAIL: reflection output: $out"; exit 1; }
80+
[[ "$out" == *"dep-ok"* ]] || { echo "FAIL: dep module output: $out"; exit 1; }
81+
82+
# The std BMI build command must carry the dialect flag.
83+
grep -rl '"std_flag": "[^"]*-freflection' "${MCPP_HOME:-$HOME/.mcpp}/bmi/" >/dev/null \
84+
|| { echo "FAIL: std-module.json lacks -freflection in std_flag"; exit 1; }
85+
86+
echo "PASS: dialect flags reach std BMI + whole module graph (issue #210)"

tests/unit/test_toolchain_dialect.cpp

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include <gtest/gtest.h>
22

33
import std;
4+
import mcpp.manifest;
45
import mcpp.toolchain.dialect;
56
import mcpp.toolchain.model;
67
import mcpp.toolchain.registry;
@@ -81,6 +82,43 @@ TEST(MingwSpec, DisplayAndDefaultMatching) {
8182
EXPECT_FALSE(matches_default_toolchain("gcc@16.1.0", "mingw-gcc", "16.1.0"));
8283
}
8384

85+
TEST(StdFlagFor, PerDialectSpelling) {
86+
const auto& gnu = dialect_for(make_tc(CompilerId::GCC));
87+
const auto& msvc = dialect_for(make_tc(CompilerId::MSVC));
88+
EXPECT_EQ(std_flag_for(gnu, "c++26", 26), "-std=c++26");
89+
EXPECT_EQ(std_flag_for(gnu, "gnu++23", 23), "-std=gnu++23");
90+
EXPECT_EQ(std_flag_for(msvc, "c++20", 20), "/std:c++20");
91+
EXPECT_EQ(std_flag_for(msvc, "c++23", 23), "/std:c++latest");
92+
EXPECT_EQ(std_flag_for(msvc, "c++26", 26), "/std:c++latest");
93+
}
94+
95+
// ─── issue #210: dialect-class flag extraction ───────────────────────────
96+
97+
TEST(DialectFlags, KnownListAndEscapeHatch) {
98+
mcpp::manifest::BuildConfig bc;
99+
bc.cxxflags = {"-freflection", "-O3", "-Wall", "-D_GLIBCXX_USE_CXX11_ABI=0"};
100+
bc.dialectCxxflags = {"-fcustom-std-thing", "-freflection"}; // dup dedups
101+
auto flags = mcpp::manifest::dialect_flags(bc);
102+
ASSERT_EQ(flags.size(), 3u);
103+
EXPECT_EQ(flags[0], "-fcustom-std-thing"); // explicit first, order kept
104+
EXPECT_EQ(flags[1], "-freflection"); // deduped against cxxflags copy
105+
EXPECT_EQ(flags[2], "-D_GLIBCXX_USE_CXX11_ABI=0"); // auto-promoted
106+
}
107+
108+
TEST(DialectFlags, ExtractionDetails) {
109+
using mcpp::manifest::is_dialect_flag;
110+
EXPECT_TRUE(is_dialect_flag("-freflection"));
111+
EXPECT_TRUE(is_dialect_flag("-fcontracts"));
112+
EXPECT_TRUE(is_dialect_flag("-fno-char8_t"));
113+
EXPECT_TRUE(is_dialect_flag("-D_GLIBCXX_USE_CXX11_ABI=1"));
114+
// Conservative first list: these stay per-unit.
115+
EXPECT_FALSE(is_dialect_flag("-fno-exceptions"));
116+
EXPECT_FALSE(is_dialect_flag("-fno-rtti"));
117+
EXPECT_FALSE(is_dialect_flag("-O2"));
118+
EXPECT_FALSE(is_dialect_flag("-Wall"));
119+
EXPECT_FALSE(is_dialect_flag("-fPIC"));
120+
}
121+
84122
TEST(MingwModel, TargetPredicate) {
85123
EXPECT_TRUE(is_mingw_target(make_tc(CompilerId::GCC, "x86_64-w64-mingw32")));
86124
EXPECT_FALSE(is_mingw_target(make_tc(CompilerId::GCC, "x86_64-linux-gnu")));

0 commit comments

Comments
 (0)