Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix(build): dialect-class flags reach the std BMI prebuild and the wh…
…ole 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.
  • Loading branch information
Sunrisepeak committed Jul 13, 2026
commit 3470d67326a4bee16ee0b4ab3bfced9c56f9eb9c
6 changes: 5 additions & 1 deletion src/build/flags.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,11 @@ CompileFlags compute_flags(const BuildPlan& plan) {
std::string cxx_std_flag =
plan.cppStandardFlag.empty()
? std::format("{}c++23", d.stdPrefix) : plan.cppStandardFlag;
f.cxx = std::format("{}{}{}{}{}{}{}{}{}{}", cxx_std_flag, module_flag, std_module_flag,
// plan.dialectFlags rides right behind -std= (issue #210): module-graph-
// global dialect flags reach every TU (deps included) via this global
// cxxflags string, exactly like the standard flag itself.
f.cxx = std::format("{}{}{}{}{}{}{}{}{}{}{}", cxx_std_flag, plan.dialectFlags,
module_flag, std_module_flag,
std_compat_module_flag, prebuilt_module_flag,
opt_flag, pic_flag, compile_toolchain_flags, b_flag, include_flags);
f.cc = std::format("{}{}{}{}{}{}{}", d.stdPrefix, c_std, opt_flag, pic_flag,
Expand Down
13 changes: 12 additions & 1 deletion src/build/plan.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import mcpp.manifest;
import mcpp.modgraph.graph;
import mcpp.modgraph.scanner;
import mcpp.toolchain.detect;
import mcpp.toolchain.dialect;
import mcpp.toolchain.fingerprint;
import mcpp.platform;

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

std::filesystem::path projectRoot; // where mcpp.toml lives
std::filesystem::path outputDir; // target/<triple>/<fp>/
Expand Down Expand Up @@ -313,7 +318,13 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest,
plan.fingerprint = fp;
if (auto stdCfg = mcpp::manifest::normalize_cpp_standard(manifest.package.standard)) {
plan.cppStandard = stdCfg->canonical;
plan.cppStandardFlag = stdCfg->flag;
// Spelled per-dialect: "-std=c++26" (gnu) vs "/std:c++latest" (msvc).
plan.cppStandardFlag = mcpp::toolchain::std_flag_for(
mcpp::toolchain::dialect_for(tc), stdCfg->canonical, stdCfg->level);
}
for (auto& f : mcpp::manifest::dialect_flags(manifest.buildConfig)) {
plan.dialectFlags += ' ';
plan.dialectFlags += f;
}
plan.projectRoot = projectRoot;
plan.outputDir = outputDir;
Expand Down
22 changes: 21 additions & 1 deletion src/build/prepare.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import mcpp.modgraph.scanner;
import mcpp.modgraph.validate;
import mcpp.toolchain.clang;
import mcpp.toolchain.detect;
import mcpp.toolchain.dialect;
import mcpp.toolchain.fingerprint;
import mcpp.toolchain.msvc;
import mcpp.toolchain.registry;
Expand Down Expand Up @@ -202,6 +203,12 @@ std::string canonical_compile_flags(const mcpp::manifest::Manifest& m) {
s += " cxxflag:";
s += flag;
}
// Explicit [build] dialect_cxxflags (auto-promoted ones are already in
// cxxflags above) — they change every BMI in the graph.
for (auto const& flag : m.buildConfig.dialectCxxflags) {
s += " dialect:";
s += flag;
}
for (auto const& flag : m.buildConfig.ldflags) {
s += " ldflag:";
s += flag;
Expand Down Expand Up @@ -2633,8 +2640,21 @@ prepare_build(bool print_fingerprint,
std::filesystem::path stdCompatBmiPath;
std::filesystem::path stdCompatObjectPath;
if (needsStdModule) {
// The std BMI must be compiled with the SAME dialect set its
// importers use (issue #210: -freflection gates libstdc++'s <meta> —
// a std BMI built without it structurally lacks std::meta). The
// standard flag is spelled per-dialect and the graph-global dialect
// flags ride along; both were already in the fingerprint, so this
// only fixes the COMMAND construction the fingerprint promised.
std::string stdFlagAndDialect = mcpp::toolchain::std_flag_for(
mcpp::toolchain::dialect_for(*tc),
m->cppStandard.canonical, m->cppStandard.level);
for (auto& f : mcpp::manifest::dialect_flags(m->buildConfig)) {
stdFlagAndDialect += ' ';
stdFlagAndDialect += f;
}
auto sm = mcpp::toolchain::ensure_built(
*tc, fp.hex, m->package.standard, m->cppStandard.flag,
*tc, fp.hex, m->package.standard, stdFlagAndDialect,
mcpp::platform::macos::deployment_target(
m->buildConfig.macosDeploymentTarget));
if (!sm) return std::unexpected(sm.error().message);
Expand Down
5 changes: 5 additions & 0 deletions src/manifest/toml.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,11 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
if (auto v = doc->get_bool("build.allow_host_libs")) m.buildConfig.allowHostLibs = *v;
if (auto v = doc->get_string_array("build.cflags")) m.buildConfig.cflags = *v;
if (auto v = doc->get_string_array("build.cxxflags")) m.buildConfig.cxxflags = *v;
// Module-graph-global dialect flags (issue #210) — see types.cppm
// dialect_flags(); this key is the explicit escape hatch for flags the
// known-list doesn't recognize yet.
if (auto v = doc->get_string_array("build.dialect_cxxflags"))
m.buildConfig.dialectCxxflags = *v;
if (auto v = doc->get_string_array("build.ldflags")) m.buildConfig.ldflags = *v;
if (auto v = doc->get_string("build.c_standard")) m.buildConfig.cStandard = *v;
if (auto v = doc->get_string("build.default-profile")) m.buildConfig.defaultProfile = *v;
Expand Down
47 changes: 47 additions & 0 deletions src/manifest/types.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,15 @@ struct BuildConfig {
std::vector<std::string> cflags;
std::vector<std::string> cxxflags;
std::vector<std::string> ldflags;
// Dialect-class C++ flags: flags that change what the standard library's
// headers DECLARE or participate in module dialect checks (issue #210's
// -freflection: libstdc++'s <meta> is gated on __cpp_impl_reflection).
// These are module-graph-global — they ride -std='s channels (global
// cxxflags for every TU incl. deps, the std/std.compat BMI prebuild,
// scan commands). Populated from [build] dialect_cxxflags plus
// auto-promotion of known flags found in [build] cxxflags
// (see dialect_flags()).
std::vector<std::string> dialectCxxflags;
std::string cStandard;
// Escape hatch for the hermetic link check: a sandbox toolchain whose
// CRT/loader resolve OUTSIDE the sandbox is a hard error by default
Expand Down Expand Up @@ -393,6 +402,16 @@ struct ManifestError {

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

// The module-graph-global dialect flag set: explicit [build] dialect_cxxflags
// plus KNOWN dialect-class flags auto-promoted out of [build] cxxflags
// (they also stay per-unit there — duplication is harmless and keeps the
// mechanism explainable). Deduplicated, declaration order preserved.
std::vector<std::string> dialect_flags(const BuildConfig& bc);

// True when `flag` belongs to the known dialect-class list (changes what
// libstdc++/libc++ headers declare, or participates in BMI dialect checks).
bool is_dialect_flag(std::string_view flag);

std::filesystem::path resolve_lib_root_path(const Manifest& manifest);

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


bool is_dialect_flag(std::string_view flag) {
// Deliberately conservative first list (design doc §1.3a):
// -fno-exceptions / -fno-rtti stay per-unit until separately reviewed
// (dependencies may assume exceptions are available).
static constexpr std::string_view exact[] = {
"-freflection", "-fno-reflection", // P2996 (GCC 16+)
"-fcontracts", "-fno-contracts", // P2900
"-fchar8_t", "-fno-char8_t",
};
for (auto e : exact)
if (flag == e) return true;
// libstdc++ dual-ABI switch changes declared symbols/types wholesale.
if (flag.starts_with("-D_GLIBCXX_USE_CXX11_ABI=")) return true;
return false;
}

std::vector<std::string> dialect_flags(const BuildConfig& bc) {
std::vector<std::string> out;
auto add = [&](const std::string& f) {
if (std::find(out.begin(), out.end(), f) == out.end())
out.push_back(f);
};
for (auto& f : bc.dialectCxxflags) add(f);
for (auto& f : bc.cxxflags)
if (is_dialect_flag(f)) add(f);
return out;
}

std::expected<CppStandardConfig, std::string> normalize_cpp_standard(std::string_view raw) {
auto trim_copy = [](std::string_view input) {
std::size_t begin = 0;
Expand Down
16 changes: 16 additions & 0 deletions src/toolchain/dialect.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ struct CommandDialect {
// Dialect lookup. GCC / Clang / MinGW → gnu; MSVC → msvc.
const CommandDialect& dialect_for(const Toolchain& tc);

// 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 {
Expand Down Expand Up @@ -106,4 +113,13 @@ const CommandDialect& dialect_for(const Toolchain& tc) {
return kGnuDialect;
}

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
86 changes: 86 additions & 0 deletions tests/e2e/98_reflection_import_std.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env bash
# requires: gcc
# 98_reflection_import_std.sh — issue #210: dialect-class flags reach the std
# module BMI prebuild AND every TU in the graph:
# - [build] cxxflags = ["-freflection"] → `import std;` exposes std::meta
# - a dependency module that imports std builds in the same graph (the
# fmt.gcm-class secondary failure from the issue)
# - std-module.json records the flag in the std build command
set -e

TMP=$(mktemp -d)
trap "rm -rf $TMP" EXIT

# The resolved toolchain must accept -freflection (GCC 16+). Probe via the
# project's own toolchain resolution: build a trivial reflection TU.
GXX=$(find "${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs/xim-x-gcc" -name "g++" -path "*/bin/*" 2>/dev/null | sort | tail -1)
if [[ -z "$GXX" ]] || ! echo 'int main(){}' | "$GXX" -freflection -std=c++26 -x c++ - -fsyntax-only -o /dev/null 2>/dev/null; then
echo "SKIP-INLINE: no -freflection-capable gcc payload available"
exit 0
fi

mkdir -p "$TMP/proj/src" "$TMP/proj/meta_dep/src"
cd "$TMP/proj"

# Path dependency providing a module that ITSELF imports std — must be
# compiled with the same dialect set or GCC rejects the BMI mix.
cat > meta_dep/mcpp.toml <<'EOF'
[package]
name = "meta_dep"
version = "0.1.0"
standard = "c++26"

[targets.meta_dep]
kind = "lib"
EOF
cat > meta_dep/src/meta_dep.cppm <<'EOF'
export module meta_dep;
import std;
export namespace meta_dep {
std::string tag() { return "dep-ok"; }
}
EOF

cat > mcpp.toml <<'EOF'
[package]
name = "refl"
version = "0.1.0"
standard = "c++26"

[build]
cxxflags = ["-freflection"]

[dependencies]
meta_dep = { path = "meta_dep" }
EOF
cat > src/main.cpp <<'EOF'
import std;
import meta_dep;

void print_struct(auto &&value) {
constexpr auto info = std::meta::remove_cvref(^^decltype(value));
constexpr auto no_check = std::meta::access_context::unchecked();
static constexpr auto members = std::define_static_array(
std::meta::nonstatic_data_members_of(info, no_check));
template for (constexpr auto e : members) {
auto &&member = value.[:e:];
std::println("{} {}", identifier_of(e), member);
}
}

struct Point { double x{}; double y{}; };
int main() {
print_struct(Point{2, 3});
std::println("{}", meta_dep::tag());
}
EOF

out=$("$MCPP" run 2>&1) || { echo "FAIL: build/run: $out"; exit 1; }
[[ "$out" == *"x 2"* && "$out" == *"y 3"* ]] || { echo "FAIL: reflection output: $out"; exit 1; }
[[ "$out" == *"dep-ok"* ]] || { echo "FAIL: dep module output: $out"; exit 1; }

# The std BMI build command must carry the dialect flag.
grep -rl '"std_flag": "[^"]*-freflection' "${MCPP_HOME:-$HOME/.mcpp}/bmi/" >/dev/null \
|| { echo "FAIL: std-module.json lacks -freflection in std_flag"; exit 1; }

echo "PASS: dialect flags reach std BMI + whole module graph (issue #210)"
38 changes: 38 additions & 0 deletions tests/unit/test_toolchain_dialect.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include <gtest/gtest.h>

import std;
import mcpp.manifest;
import mcpp.toolchain.dialect;
import mcpp.toolchain.model;
import mcpp.toolchain.registry;
Expand Down Expand Up @@ -81,6 +82,43 @@ TEST(MingwSpec, DisplayAndDefaultMatching) {
EXPECT_FALSE(matches_default_toolchain("gcc@16.1.0", "mingw-gcc", "16.1.0"));
}

TEST(StdFlagFor, PerDialectSpelling) {
const auto& gnu = dialect_for(make_tc(CompilerId::GCC));
const auto& msvc = dialect_for(make_tc(CompilerId::MSVC));
EXPECT_EQ(std_flag_for(gnu, "c++26", 26), "-std=c++26");
EXPECT_EQ(std_flag_for(gnu, "gnu++23", 23), "-std=gnu++23");
EXPECT_EQ(std_flag_for(msvc, "c++20", 20), "/std:c++20");
EXPECT_EQ(std_flag_for(msvc, "c++23", 23), "/std:c++latest");
EXPECT_EQ(std_flag_for(msvc, "c++26", 26), "/std:c++latest");
}

// ─── issue #210: dialect-class flag extraction ───────────────────────────

TEST(DialectFlags, KnownListAndEscapeHatch) {
mcpp::manifest::BuildConfig bc;
bc.cxxflags = {"-freflection", "-O3", "-Wall", "-D_GLIBCXX_USE_CXX11_ABI=0"};
bc.dialectCxxflags = {"-fcustom-std-thing", "-freflection"}; // dup dedups
auto flags = mcpp::manifest::dialect_flags(bc);
ASSERT_EQ(flags.size(), 3u);
EXPECT_EQ(flags[0], "-fcustom-std-thing"); // explicit first, order kept
EXPECT_EQ(flags[1], "-freflection"); // deduped against cxxflags copy
EXPECT_EQ(flags[2], "-D_GLIBCXX_USE_CXX11_ABI=0"); // auto-promoted
}

TEST(DialectFlags, ExtractionDetails) {
using mcpp::manifest::is_dialect_flag;
EXPECT_TRUE(is_dialect_flag("-freflection"));
EXPECT_TRUE(is_dialect_flag("-fcontracts"));
EXPECT_TRUE(is_dialect_flag("-fno-char8_t"));
EXPECT_TRUE(is_dialect_flag("-D_GLIBCXX_USE_CXX11_ABI=1"));
// Conservative first list: these stay per-unit.
EXPECT_FALSE(is_dialect_flag("-fno-exceptions"));
EXPECT_FALSE(is_dialect_flag("-fno-rtti"));
EXPECT_FALSE(is_dialect_flag("-O2"));
EXPECT_FALSE(is_dialect_flag("-Wall"));
EXPECT_FALSE(is_dialect_flag("-fPIC"));
}

TEST(MingwModel, TargetPredicate) {
EXPECT_TRUE(is_mingw_target(make_tc(CompilerId::GCC, "x86_64-w64-mingw32")));
EXPECT_FALSE(is_mingw_target(make_tc(CompilerId::GCC, "x86_64-linux-gnu")));
Expand Down