Skip to content

Commit 7c5ec68

Browse files
authored
feat: standard = "c++fly" — one knob for latest std + all experimental features (0.0.91) (#214)
* feat(toolchain): cppfly registry — latest-std + experimental gates per compiler * feat(manifest,build): standard=c++fly — latest std + all experimental gates; fix c++latest GNU spelling * test(e2e): c++fly hard/soft paths + c++latest spelling regression * docs(release): 0.0.91 — changelog, c++fly design + plan docs, manifest docs, version bump * fix(e2e): 101 — assert stdlib gate consistency instead of hardcoding libc++ (clang+MSVC STL on windows); .exe-aware binary find
1 parent ce34a70 commit 7c5ec68

14 files changed

Lines changed: 1457 additions & 14 deletions

.agents/docs/2026-07-14-single-pr-091-implementation-plan.md

Lines changed: 451 additions & 0 deletions
Large diffs are not rendered by default.

.agents/docs/2026-07-14-std-features-experimental-gate-design.md

Lines changed: 341 additions & 0 deletions
Large diffs are not rendered by default.

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,29 @@
33
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
44
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
55
6+
## [0.0.91] — 2026-07-15
7+
8+
### 新增
9+
10+
- **`standard = "c++fly"` — 一行启用"最新标准 + 全部实验特性"(语言 + 标准库)**
11+
语义三件套,全部按 resolved 工具链自动判定:①族最新 `-std=` 档位(GCC16→c++26、
12+
Clang→c++2c、MSVC→/std:c++latest);②该工具链支持的全部实验性语言特性门
13+
(GCC≥16:反射 `-freflection`;契约随 `-std=c++26` 默认启用,实机探测定案);
14+
③标准库实验门(libc++→`-fexperimental-library`)。不支持的特性**软跳过**并打印
15+
summary(`c++fly on <toolchain>: <std>; enabled: ...; skipped: ...`)。
16+
新模块 `src/toolchain/cppfly.cppm` 承载三张数据表(族×版本×stdlib)——首个真正
17+
使用 `Toolchain::version` 做门控的查询点;产物汇入 0.0.90 的图全局方言旗标通道
18+
(全图 TU + P1689 扫描 + std BMI 预构建同源),派生旗标并入指纹。
19+
设计:`.agents/docs/2026-07-14-std-features-experimental-gate-design.md`
20+
e2e 100(gcc16 硬路径:零手写旗标跑通 std::meta 反射)/ 101(clang 软路径:
21+
c++2c + skipped summary)。
22+
23+
### 修复
24+
25+
- **`standard = "c++latest"` 在 GNU 族误拼 `-std=c++latest`**(GCC/Clang 不识别,
26+
构建必败):canonical 现经 cppfly 的"族最新档"表解析为真实档位(GCC16→
27+
`-std=c++26`)。e2e 100 尾段回归覆盖。
28+
629
## [0.0.90] — 2026-07-13
730

831
### 新增

docs/05-mcpp-toml.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ repo = "https://github.com/user/myapp" # Repository URL (optional)
5454
- `c++26`: use when you need C++26 language features.
5555
- `c++2c`: a compatibility alias, normalized to `c++26` after parsing.
5656
- `gnu++23` / `gnu++26`: use when you need a GNU dialect; this enters the fingerprint and the std BMI cache key.
57-
- `c++latest`: follows the newest standard mcpp currently supports. Good for local experimentation, but not recommended for release packages that require reproducibility.
57+
- `c++latest`: resolves to the newest standard level the resolved toolchain supports. Good for local experimentation, but not recommended for release packages that require reproducibility.
58+
- `c++fly`: `c++latest` **plus every experimental standard feature the resolved toolchain can enable** (language + standard library). On GCC ≥ 16 this turns on C++26 reflection (`-freflection`) and contracts; on Clang/libc++ it adds `-fexperimental-library`; unsupported gates are skipped with a printed summary. Deliberately toolchain-dependent — the bleeding-edge playground mode, never for published packages.
5859

5960
### 2.2 `[targets.<name>]` — Build Targets
6061

@@ -718,7 +719,7 @@ mcpp build --target x86_64-linux-musl
718719
| Source files | `src/**/*.{cppm,cpp,cc,c}` | Scanned recursively and automatically |
719720
| Entry point | `src/main.cpp` | If this file exists, a `bin` target is inferred |
720721
| Library root | `src/<pkg-tail>.cppm` | Override with `[lib].path` |
721-
| C++ standard | `c++23` | Configure with `[package].standard`; supports `c++26` / `c++2c` |
722+
| C++ standard | `c++23` | Configure with `[package].standard`; supports `c++26` / `c++2c` / `c++latest` / `c++fly` (experimental playground) |
722723
| C standard | `c11` | `.c` files go through the C compiler automatically |
723724
| Static stdlib | `true` | Portable binary |
724725
| Headers | `include/` (if present) | Added to `-I` automatically |

mcpp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mcpp"
3-
version = "0.0.90"
3+
version = "0.0.91"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

src/build/plan.cppm

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import std;
99
import mcpp.manifest;
1010
import mcpp.modgraph.graph;
1111
import mcpp.modgraph.scanner;
12+
import mcpp.toolchain.cppfly;
1213
import mcpp.toolchain.detect;
1314
import mcpp.toolchain.dialect;
1415
import mcpp.toolchain.fingerprint;
@@ -319,13 +320,21 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest,
319320
plan.manifest = manifest;
320321
plan.toolchain = tc;
321322
plan.fingerprint = fp;
323+
bool experimentalStd = false;
322324
if (auto stdCfg = mcpp::manifest::normalize_cpp_standard(manifest.package.standard)) {
323325
plan.cppStandard = stdCfg->canonical;
324-
// Spelled per-dialect: "-std=c++26" (gnu) vs "/std:c++latest" (msvc).
325-
plan.cppStandardFlag = mcpp::toolchain::std_flag_for(
326-
mcpp::toolchain::dialect_for(tc), stdCfg->canonical, stdCfg->level);
326+
experimentalStd = stdCfg->experimental;
327+
// Spelled per-dialect ("-std=c++26" gnu vs "/std:c++latest" msvc) AND
328+
// per-toolchain-latest for c++latest/c++fly (raw canonical is not a
329+
// valid -std= spelling on GNU).
330+
plan.cppStandardFlag = mcpp::toolchain::cppfly::std_flag(
331+
tc, stdCfg->canonical, stdCfg->level);
327332
}
328-
for (auto& f : mcpp::manifest::dialect_flags(manifest.buildConfig)) {
333+
// Graph-global dialect flags: manifest-declared ∪ c++fly gates — the same
334+
// merge prepare.cppm feeds the scan/std-BMI with (single source, #210).
335+
for (auto& f : mcpp::toolchain::cppfly::effective_dialect_flags(
336+
tc, experimentalStd,
337+
mcpp::manifest::dialect_flags(manifest.buildConfig))) {
329338
plan.dialectFlags += ' ';
330339
plan.dialectFlags += f;
331340
}

src/build/prepare.cppm

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import mcpp.modgraph.graph;
1616
import mcpp.modgraph.scanner;
1717
import mcpp.modgraph.validate;
1818
import mcpp.toolchain.clang;
19+
import mcpp.toolchain.cppfly;
1920
import mcpp.toolchain.detect;
2021
import mcpp.toolchain.dialect;
2122
import mcpp.toolchain.fingerprint;
@@ -2586,11 +2587,31 @@ prepare_build(bool print_fingerprint,
25862587
// The dialect-complete standard flag: spelled per-dialect and carrying
25872588
// the module-graph-global dialect flags (issue #210). ONE string shared
25882589
// by the p1689 scan and the std BMI prebuild so scan-time, prebuild-time
2589-
// and compile-time dialect provably agree.
2590-
std::string stdFlagAndDialect = mcpp::toolchain::std_flag_for(
2591-
mcpp::toolchain::dialect_for(*tc),
2592-
m->cppStandard.canonical, m->cppStandard.level);
2593-
for (auto& f : mcpp::manifest::dialect_flags(m->buildConfig)) {
2590+
// and compile-time dialect provably agree. Both this and make_plan go
2591+
// through the same cppfly merge, so the c++fly gates (and the
2592+
// c++latest/c++fly per-toolchain std spelling) stay graph-consistent.
2593+
std::string stdFlagAndDialect = mcpp::toolchain::cppfly::std_flag(
2594+
*tc, m->cppStandard.canonical, m->cppStandard.level);
2595+
if (m->cppStandard.experimental) {
2596+
// c++fly is best-effort by design: say exactly what this toolchain
2597+
// got and what it lacks (the value's contract, design §5.4).
2598+
auto fly = mcpp::toolchain::cppfly::resolve(*tc);
2599+
std::string enabled, skipped;
2600+
for (auto& f : fly.features) {
2601+
auto& dst = f.enabled ? enabled : skipped;
2602+
if (!dst.empty()) dst += ", ";
2603+
dst += f.name;
2604+
if (f.enabled && !f.flags.empty()) dst += std::format(" ({})", f.flags);
2605+
if (!f.enabled) dst += std::format(" ({})", f.reason);
2606+
}
2607+
std::println("c++fly on {}: {}; enabled: {}; skipped: {}",
2608+
tc->label(), stdFlagAndDialect,
2609+
enabled.empty() ? "(none)" : enabled,
2610+
skipped.empty() ? "(none)" : skipped);
2611+
}
2612+
for (auto& f : mcpp::toolchain::cppfly::effective_dialect_flags(
2613+
*tc, m->cppStandard.experimental,
2614+
mcpp::manifest::dialect_flags(m->buildConfig))) {
25942615
stdFlagAndDialect += ' ';
25952616
stdFlagAndDialect += f;
25962617
}
@@ -2644,6 +2665,14 @@ prepare_build(bool print_fingerprint,
26442665
fpi.cppStandard = m->package.standard;
26452666
fpi.compileFlags = canonical_compile_flags(*m)
26462667
+ canonical_package_build_metadata(packages);
2668+
if (m->cppStandard.experimental) {
2669+
// c++fly gate flags are derived (not manifest-declared): fold them in
2670+
// so a cppfly table change across mcpp versions re-fingerprints.
2671+
for (auto& f : mcpp::toolchain::cppfly::resolve(*tc).flags) {
2672+
fpi.compileFlags += ' ';
2673+
fpi.compileFlags += f;
2674+
}
2675+
}
26472676
fpi.dependencyLockHash = ""; // M2
26482677
fpi.stdBmiHash = ""; // updated after stdmod build (chicken/egg ok for M1)
26492678
auto fp = mcpp::toolchain::compute_fingerprint(fpi);

src/manifest/types.cppm

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ struct CppStandardConfig {
2929
std::string flag = "-std=c++23";
3030
int level = 23;
3131
bool gnuDialect = false;
32+
// standard = "c++fly": latest level + all experimental gates the
33+
// resolved toolchain supports (toolchain/cppfly.cppm owns the mapping).
34+
bool experimental = false;
3235
};
3336

3437
struct Package {
@@ -532,9 +535,17 @@ std::expected<CppStandardConfig, std::string> normalize_cpp_standard(std::string
532535
out.gnuDialect = false;
533536
return out;
534537
}
538+
if (s == "c++fly") {
539+
out.canonical = "c++fly";
540+
out.flag = "-std=c++26"; // static GNU fallback; the real spelling
541+
out.level = 1000; // comes from cppfly::std_flag (per-toolchain
542+
out.gnuDialect = false; // latest, > c++latest's 999)
543+
out.experimental = true;
544+
return out;
545+
}
535546

536547
return std::unexpected(std::format(
537-
"unsupported C++ standard '{}'; expected c++23, c++26, c++2c, gnu++23, gnu++26, or c++latest",
548+
"unsupported C++ standard '{}'; expected c++23, c++26, c++2c, gnu++23, gnu++26, c++latest, or c++fly",
538549
raw));
539550
}
540551

src/toolchain/cppfly.cppm

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
// mcpp.toolchain.cppfly — the `standard = "c++fly"` capability: per-compiler
2+
// support & mapping for "latest -std= level + every enableable experimental
3+
// gate (language + stdlib)". Pure data tables + query functions, the fourth
4+
// sibling of CommandDialect / BmiTraits / ProviderCapabilities — and the
5+
// first consumer of Toolchain::version for gating.
6+
//
7+
// Also owns the "latest level for this toolchain" table, which fixes
8+
// standard = "c++latest" reaching the GNU driver as the invalid spelling
9+
// -std=c++latest (design §11-Q5).
10+
//
11+
// Table facts pinned by on-machine probes (gcc 16.1.0, 2026-07-14):
12+
// contracts are enabled by -std=c++26 alone (__cpp_contracts defined);
13+
// reflection additionally needs -freflection (__cpp_impl_reflection).
14+
// See .agents/docs/2026-07-14-std-features-experimental-gate-design.md.
15+
16+
export module mcpp.toolchain.cppfly;
17+
18+
import std;
19+
import mcpp.toolchain.dialect;
20+
import mcpp.toolchain.model;
21+
22+
export namespace mcpp::toolchain::cppfly {
23+
24+
struct FeatureState {
25+
std::string name; // "reflection"
26+
std::string paper; // "P2996" — summary/diagnostics
27+
std::string flags; // enabled: extra flags ("" = enabled by std level alone)
28+
std::string reason; // skipped: why
29+
bool enabled = false;
30+
};
31+
32+
struct Resolution {
33+
std::string stdCanonical; // "c++26" / "c++2c" / "c++23"
34+
int stdLevel = 26;
35+
std::vector<std::string> flags; // union of enabled gate flags, deduped
36+
std::vector<FeatureState> features; // enabled + skipped, declaration order
37+
};
38+
39+
// Leading integer of Toolchain::version ("16.1.0" → 16; unparsable → 0).
40+
int compiler_major(const Toolchain& tc);
41+
42+
// Latest -std= canonical the resolved toolchain accepts. Used for both
43+
// c++fly and c++latest (the raw canonicals are not valid -std= spellings).
44+
std::string latest_std_canonical(const Toolchain& tc, int* levelOut = nullptr);
45+
46+
// The full c++fly answer for a toolchain: latest level + every gate it
47+
// supports (enabled) and every gate it lacks (skipped, with reason).
48+
Resolution resolve(const Toolchain& tc);
49+
50+
// The dialect-complete std flag for any normalized standard, including
51+
// c++latest/c++fly which resolve per-toolchain. Plain canonicals delegate
52+
// to std_flag_for unchanged.
53+
std::string std_flag(const Toolchain& tc, std::string_view canonical, int level);
54+
55+
// Graph-global dialect flags: manifest-declared ∪ (experimental ? fly gate
56+
// flags : ∅), deduped, declaration order kept. The ONE merge both consumers
57+
// (BuildPlan::dialectFlags and prepare's stdFlagAndDialect) go through so
58+
// scan-time, prebuild-time and compile-time dialect provably agree.
59+
std::vector<std::string> effective_dialect_flags(const Toolchain& tc,
60+
bool experimental, std::vector<std::string> manifestDialectFlags);
61+
62+
} // namespace mcpp::toolchain::cppfly
63+
64+
namespace mcpp::toolchain::cppfly {
65+
66+
namespace {
67+
68+
// ── Table 1: family × min major → latest supported -std= canonical ──────
69+
// First matching row wins (rows per family ordered newest-first).
70+
struct LatestStdRule {
71+
CompilerId family;
72+
int minMajor;
73+
std::string_view canonical;
74+
int level;
75+
};
76+
constexpr LatestStdRule kLatestStd[] = {
77+
{ CompilerId::GCC, 14, "c++26", 26 },
78+
{ CompilerId::GCC, 0, "c++23", 23 },
79+
{ CompilerId::Clang, 17, "c++2c", 26 },
80+
{ CompilerId::Clang, 0, "c++23", 23 },
81+
// MSVC has no /std:c++26 — level > 20 is spelled /std:c++latest by
82+
// std_flag_for; the canonical here only feeds diagnostics.
83+
{ CompilerId::MSVC, 0, "c++26", 26 },
84+
};
85+
86+
// ── Table 2: experimental language-feature gates (version-gated) ────────
87+
// A family with no row = unsupported. flags == "" = supported at the fly
88+
// std level with no extra gate flag (still reported for the summary).
89+
struct GateRule {
90+
CompilerId family;
91+
int minMajor;
92+
std::string_view flags;
93+
};
94+
constexpr GateRule kReflectionRules[] = {
95+
{ CompilerId::GCC, 16, "-freflection" },
96+
};
97+
constexpr GateRule kContractsRules[] = {
98+
{ CompilerId::GCC, 16, "" },
99+
};
100+
struct Gate {
101+
std::string_view name;
102+
std::string_view paper;
103+
std::span<const GateRule> rules;
104+
};
105+
constexpr Gate kGates[] = {
106+
{ "reflection", "P2996", kReflectionRules },
107+
{ "contracts", "P2900", kContractsRules },
108+
};
109+
110+
// ── Table 3: stdlib experimental gates (stdlib dimension) ───────────────
111+
struct StdlibGateRule {
112+
std::string_view name;
113+
std::string_view stdlibId;
114+
std::string_view flags;
115+
};
116+
constexpr StdlibGateRule kStdlibGates[] = {
117+
// libstdc++ and MSVC STL ship their unstable bits ungated; libc++ hides
118+
// them behind -fexperimental-library.
119+
{ "experimental-library", "libc++", "-fexperimental-library" },
120+
};
121+
122+
void add_unique(std::vector<std::string>& v, std::string_view f) {
123+
if (f.empty()) return;
124+
if (std::find(v.begin(), v.end(), f) == v.end()) v.emplace_back(f);
125+
}
126+
127+
} // namespace
128+
129+
int compiler_major(const Toolchain& tc) {
130+
int major = 0;
131+
bool any = false;
132+
for (char c : tc.version) {
133+
if (c < '0' || c > '9') break;
134+
major = major * 10 + (c - '0');
135+
any = true;
136+
}
137+
return any ? major : 0;
138+
}
139+
140+
std::string latest_std_canonical(const Toolchain& tc, int* levelOut) {
141+
const int major = compiler_major(tc);
142+
for (auto& r : kLatestStd) {
143+
if (r.family != tc.compiler) continue;
144+
if (major >= r.minMajor) {
145+
if (levelOut) *levelOut = r.level;
146+
return std::string(r.canonical);
147+
}
148+
}
149+
if (levelOut) *levelOut = 26; // unknown family: newest ratified level
150+
return "c++26";
151+
}
152+
153+
Resolution resolve(const Toolchain& tc) {
154+
Resolution r;
155+
r.stdCanonical = latest_std_canonical(tc, &r.stdLevel);
156+
const int major = compiler_major(tc);
157+
for (auto& g : kGates) {
158+
FeatureState st;
159+
st.name = std::string(g.name);
160+
st.paper = std::string(g.paper);
161+
const GateRule* hit = nullptr;
162+
for (auto& rule : g.rules) {
163+
if (rule.family == tc.compiler) { hit = &rule; break; }
164+
}
165+
if (!hit) {
166+
st.reason = std::format("{}: unsupported", tc.compiler_name());
167+
} else if (major < hit->minMajor) {
168+
st.reason = std::format("{} {} < {}", tc.compiler_name(), major, hit->minMajor);
169+
} else {
170+
st.enabled = true;
171+
st.flags = std::string(hit->flags);
172+
add_unique(r.flags, hit->flags);
173+
}
174+
r.features.push_back(std::move(st));
175+
}
176+
for (auto& sg : kStdlibGates) {
177+
FeatureState st;
178+
st.name = std::string(sg.name);
179+
st.paper = "stdlib";
180+
if (tc.stdlibId == sg.stdlibId) {
181+
st.enabled = true;
182+
st.flags = std::string(sg.flags);
183+
add_unique(r.flags, sg.flags);
184+
} else {
185+
st.reason = std::format("stdlib is {}, gate applies to {}",
186+
tc.stdlibId.empty() ? "unknown" : tc.stdlibId, sg.stdlibId);
187+
}
188+
r.features.push_back(std::move(st));
189+
}
190+
return r;
191+
}
192+
193+
std::string std_flag(const Toolchain& tc, std::string_view canonical, int level) {
194+
std::string resolvedCanonical(canonical);
195+
int resolvedLevel = level;
196+
// c++latest (999) / c++fly (1000): resolve to the family's real latest
197+
// level — the raw canonical is not a valid -std= spelling on GNU.
198+
if (level >= 999) resolvedCanonical = latest_std_canonical(tc, &resolvedLevel);
199+
return std_flag_for(dialect_for(tc), resolvedCanonical, resolvedLevel);
200+
}
201+
202+
std::vector<std::string> effective_dialect_flags(const Toolchain& tc,
203+
bool experimental, std::vector<std::string> manifestDialectFlags)
204+
{
205+
if (!experimental) return manifestDialectFlags;
206+
for (auto& f : resolve(tc).flags) add_unique(manifestDialectFlags, f);
207+
return manifestDialectFlags;
208+
}
209+
210+
} // namespace mcpp::toolchain::cppfly

src/toolchain/fingerprint.cppm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import mcpp.toolchain.detect;
1818

1919
export namespace mcpp::toolchain {
2020

21-
inline constexpr std::string_view MCPP_VERSION = "0.0.90";
21+
inline constexpr std::string_view MCPP_VERSION = "0.0.91";
2222

2323
struct FingerprintInputs {
2424
Toolchain toolchain;

0 commit comments

Comments
 (0)