Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
451 changes: 451 additions & 0 deletions .agents/docs/2026-07-14-single-pr-091-implementation-plan.md

Large diffs are not rendered by default.

341 changes: 341 additions & 0 deletions .agents/docs/2026-07-14-std-features-experimental-gate-design.md

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,29 @@
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。

## [0.0.91] — 2026-07-15

### 新增

- **`standard = "c++fly"` — 一行启用"最新标准 + 全部实验特性"(语言 + 标准库)**。
语义三件套,全部按 resolved 工具链自动判定:①族最新 `-std=` 档位(GCC16→c++26、
Clang→c++2c、MSVC→/std:c++latest);②该工具链支持的全部实验性语言特性门
(GCC≥16:反射 `-freflection`;契约随 `-std=c++26` 默认启用,实机探测定案);
③标准库实验门(libc++→`-fexperimental-library`)。不支持的特性**软跳过**并打印
summary(`c++fly on <toolchain>: <std>; enabled: ...; skipped: ...`)。
新模块 `src/toolchain/cppfly.cppm` 承载三张数据表(族×版本×stdlib)——首个真正
使用 `Toolchain::version` 做门控的查询点;产物汇入 0.0.90 的图全局方言旗标通道
(全图 TU + P1689 扫描 + std BMI 预构建同源),派生旗标并入指纹。
设计:`.agents/docs/2026-07-14-std-features-experimental-gate-design.md`。
e2e 100(gcc16 硬路径:零手写旗标跑通 std::meta 反射)/ 101(clang 软路径:
c++2c + skipped summary)。

### 修复

- **`standard = "c++latest"` 在 GNU 族误拼 `-std=c++latest`**(GCC/Clang 不识别,
构建必败):canonical 现经 cppfly 的"族最新档"表解析为真实档位(GCC16→
`-std=c++26`)。e2e 100 尾段回归覆盖。

## [0.0.90] — 2026-07-13

### 新增
Expand Down
5 changes: 3 additions & 2 deletions docs/05-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ repo = "https://github.com/user/myapp" # Repository URL (optional)
- `c++26`: use when you need C++26 language features.
- `c++2c`: a compatibility alias, normalized to `c++26` after parsing.
- `gnu++23` / `gnu++26`: use when you need a GNU dialect; this enters the fingerprint and the std BMI cache key.
- `c++latest`: follows the newest standard mcpp currently supports. Good for local experimentation, but not recommended for release packages that require reproducibility.
- `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.
- `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.

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

Expand Down Expand Up @@ -718,7 +719,7 @@ mcpp build --target x86_64-linux-musl
| Source files | `src/**/*.{cppm,cpp,cc,c}` | Scanned recursively and automatically |
| Entry point | `src/main.cpp` | If this file exists, a `bin` target is inferred |
| Library root | `src/<pkg-tail>.cppm` | Override with `[lib].path` |
| C++ standard | `c++23` | Configure with `[package].standard`; supports `c++26` / `c++2c` |
| C++ standard | `c++23` | Configure with `[package].standard`; supports `c++26` / `c++2c` / `c++latest` / `c++fly` (experimental playground) |
| C standard | `c11` | `.c` files go through the C compiler automatically |
| Static stdlib | `true` | Portable binary |
| Headers | `include/` (if present) | Added to `-I` automatically |
Expand Down
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mcpp"
version = "0.0.90"
version = "0.0.91"
description = "Modern C++ build & package management tool"
license = "Apache-2.0"
authors = ["mcpp-community"]
Expand Down
17 changes: 13 additions & 4 deletions src/build/plan.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import std;
import mcpp.manifest;
import mcpp.modgraph.graph;
import mcpp.modgraph.scanner;
import mcpp.toolchain.cppfly;
import mcpp.toolchain.detect;
import mcpp.toolchain.dialect;
import mcpp.toolchain.fingerprint;
Expand Down Expand Up @@ -319,13 +320,21 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest,
plan.manifest = manifest;
plan.toolchain = tc;
plan.fingerprint = fp;
bool experimentalStd = false;
if (auto stdCfg = mcpp::manifest::normalize_cpp_standard(manifest.package.standard)) {
plan.cppStandard = stdCfg->canonical;
// 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);
experimentalStd = stdCfg->experimental;
// Spelled per-dialect ("-std=c++26" gnu vs "/std:c++latest" msvc) AND
// per-toolchain-latest for c++latest/c++fly (raw canonical is not a
// valid -std= spelling on GNU).
plan.cppStandardFlag = mcpp::toolchain::cppfly::std_flag(
tc, stdCfg->canonical, stdCfg->level);
}
for (auto& f : mcpp::manifest::dialect_flags(manifest.buildConfig)) {
// Graph-global dialect flags: manifest-declared ∪ c++fly gates — the same
// merge prepare.cppm feeds the scan/std-BMI with (single source, #210).
for (auto& f : mcpp::toolchain::cppfly::effective_dialect_flags(
tc, experimentalStd,
mcpp::manifest::dialect_flags(manifest.buildConfig))) {
plan.dialectFlags += ' ';
plan.dialectFlags += f;
}
Expand Down
39 changes: 34 additions & 5 deletions src/build/prepare.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import mcpp.modgraph.graph;
import mcpp.modgraph.scanner;
import mcpp.modgraph.validate;
import mcpp.toolchain.clang;
import mcpp.toolchain.cppfly;
import mcpp.toolchain.detect;
import mcpp.toolchain.dialect;
import mcpp.toolchain.fingerprint;
Expand Down Expand Up @@ -2586,11 +2587,31 @@ prepare_build(bool print_fingerprint,
// The dialect-complete standard flag: spelled per-dialect and carrying
// the module-graph-global dialect flags (issue #210). ONE string shared
// by the p1689 scan and the std BMI prebuild so scan-time, prebuild-time
// and compile-time dialect provably agree.
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)) {
// and compile-time dialect provably agree. Both this and make_plan go
// through the same cppfly merge, so the c++fly gates (and the
// c++latest/c++fly per-toolchain std spelling) stay graph-consistent.
std::string stdFlagAndDialect = mcpp::toolchain::cppfly::std_flag(
*tc, m->cppStandard.canonical, m->cppStandard.level);
if (m->cppStandard.experimental) {
// c++fly is best-effort by design: say exactly what this toolchain
// got and what it lacks (the value's contract, design §5.4).
auto fly = mcpp::toolchain::cppfly::resolve(*tc);
std::string enabled, skipped;
for (auto& f : fly.features) {
auto& dst = f.enabled ? enabled : skipped;
if (!dst.empty()) dst += ", ";
dst += f.name;
if (f.enabled && !f.flags.empty()) dst += std::format(" ({})", f.flags);
if (!f.enabled) dst += std::format(" ({})", f.reason);
}
std::println("c++fly on {}: {}; enabled: {}; skipped: {}",
tc->label(), stdFlagAndDialect,
enabled.empty() ? "(none)" : enabled,
skipped.empty() ? "(none)" : skipped);
}
for (auto& f : mcpp::toolchain::cppfly::effective_dialect_flags(
*tc, m->cppStandard.experimental,
mcpp::manifest::dialect_flags(m->buildConfig))) {
stdFlagAndDialect += ' ';
stdFlagAndDialect += f;
}
Expand Down Expand Up @@ -2644,6 +2665,14 @@ prepare_build(bool print_fingerprint,
fpi.cppStandard = m->package.standard;
fpi.compileFlags = canonical_compile_flags(*m)
+ canonical_package_build_metadata(packages);
if (m->cppStandard.experimental) {
// c++fly gate flags are derived (not manifest-declared): fold them in
// so a cppfly table change across mcpp versions re-fingerprints.
for (auto& f : mcpp::toolchain::cppfly::resolve(*tc).flags) {
fpi.compileFlags += ' ';
fpi.compileFlags += f;
}
}
fpi.dependencyLockHash = ""; // M2
fpi.stdBmiHash = ""; // updated after stdmod build (chicken/egg ok for M1)
auto fp = mcpp::toolchain::compute_fingerprint(fpi);
Expand Down
13 changes: 12 additions & 1 deletion src/manifest/types.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ struct CppStandardConfig {
std::string flag = "-std=c++23";
int level = 23;
bool gnuDialect = false;
// standard = "c++fly": latest level + all experimental gates the
// resolved toolchain supports (toolchain/cppfly.cppm owns the mapping).
bool experimental = false;
};

struct Package {
Expand Down Expand Up @@ -532,9 +535,17 @@ std::expected<CppStandardConfig, std::string> normalize_cpp_standard(std::string
out.gnuDialect = false;
return out;
}
if (s == "c++fly") {
out.canonical = "c++fly";
out.flag = "-std=c++26"; // static GNU fallback; the real spelling
out.level = 1000; // comes from cppfly::std_flag (per-toolchain
out.gnuDialect = false; // latest, > c++latest's 999)
out.experimental = true;
return out;
}

return std::unexpected(std::format(
"unsupported C++ standard '{}'; expected c++23, c++26, c++2c, gnu++23, gnu++26, or c++latest",
"unsupported C++ standard '{}'; expected c++23, c++26, c++2c, gnu++23, gnu++26, c++latest, or c++fly",
raw));
}

Expand Down
210 changes: 210 additions & 0 deletions src/toolchain/cppfly.cppm
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
// mcpp.toolchain.cppfly — the `standard = "c++fly"` capability: per-compiler
// support & mapping for "latest -std= level + every enableable experimental
// gate (language + stdlib)". Pure data tables + query functions, the fourth
// sibling of CommandDialect / BmiTraits / ProviderCapabilities — and the
// first consumer of Toolchain::version for gating.
//
// Also owns the "latest level for this toolchain" table, which fixes
// standard = "c++latest" reaching the GNU driver as the invalid spelling
// -std=c++latest (design §11-Q5).
//
// Table facts pinned by on-machine probes (gcc 16.1.0, 2026-07-14):
// contracts are enabled by -std=c++26 alone (__cpp_contracts defined);
// reflection additionally needs -freflection (__cpp_impl_reflection).
// See .agents/docs/2026-07-14-std-features-experimental-gate-design.md.

export module mcpp.toolchain.cppfly;

import std;
import mcpp.toolchain.dialect;
import mcpp.toolchain.model;

export namespace mcpp::toolchain::cppfly {

struct FeatureState {
std::string name; // "reflection"
std::string paper; // "P2996" — summary/diagnostics
std::string flags; // enabled: extra flags ("" = enabled by std level alone)
std::string reason; // skipped: why
bool enabled = false;
};

struct Resolution {
std::string stdCanonical; // "c++26" / "c++2c" / "c++23"
int stdLevel = 26;
std::vector<std::string> flags; // union of enabled gate flags, deduped
std::vector<FeatureState> features; // enabled + skipped, declaration order
};

// Leading integer of Toolchain::version ("16.1.0" → 16; unparsable → 0).
int compiler_major(const Toolchain& tc);

// Latest -std= canonical the resolved toolchain accepts. Used for both
// c++fly and c++latest (the raw canonicals are not valid -std= spellings).
std::string latest_std_canonical(const Toolchain& tc, int* levelOut = nullptr);

// The full c++fly answer for a toolchain: latest level + every gate it
// supports (enabled) and every gate it lacks (skipped, with reason).
Resolution resolve(const Toolchain& tc);

// The dialect-complete std flag for any normalized standard, including
// c++latest/c++fly which resolve per-toolchain. Plain canonicals delegate
// to std_flag_for unchanged.
std::string std_flag(const Toolchain& tc, std::string_view canonical, int level);

// Graph-global dialect flags: manifest-declared ∪ (experimental ? fly gate
// flags : ∅), deduped, declaration order kept. The ONE merge both consumers
// (BuildPlan::dialectFlags and prepare's stdFlagAndDialect) go through so
// scan-time, prebuild-time and compile-time dialect provably agree.
std::vector<std::string> effective_dialect_flags(const Toolchain& tc,
bool experimental, std::vector<std::string> manifestDialectFlags);

} // namespace mcpp::toolchain::cppfly

namespace mcpp::toolchain::cppfly {

namespace {

// ── Table 1: family × min major → latest supported -std= canonical ──────
// First matching row wins (rows per family ordered newest-first).
struct LatestStdRule {
CompilerId family;
int minMajor;
std::string_view canonical;
int level;
};
constexpr LatestStdRule kLatestStd[] = {
{ CompilerId::GCC, 14, "c++26", 26 },
{ CompilerId::GCC, 0, "c++23", 23 },
{ CompilerId::Clang, 17, "c++2c", 26 },
{ CompilerId::Clang, 0, "c++23", 23 },
// MSVC has no /std:c++26 — level > 20 is spelled /std:c++latest by
// std_flag_for; the canonical here only feeds diagnostics.
{ CompilerId::MSVC, 0, "c++26", 26 },
};

// ── Table 2: experimental language-feature gates (version-gated) ────────
// A family with no row = unsupported. flags == "" = supported at the fly
// std level with no extra gate flag (still reported for the summary).
struct GateRule {
CompilerId family;
int minMajor;
std::string_view flags;
};
constexpr GateRule kReflectionRules[] = {
{ CompilerId::GCC, 16, "-freflection" },
};
constexpr GateRule kContractsRules[] = {
{ CompilerId::GCC, 16, "" },
};
struct Gate {
std::string_view name;
std::string_view paper;
std::span<const GateRule> rules;
};
constexpr Gate kGates[] = {
{ "reflection", "P2996", kReflectionRules },
{ "contracts", "P2900", kContractsRules },
};

// ── Table 3: stdlib experimental gates (stdlib dimension) ───────────────
struct StdlibGateRule {
std::string_view name;
std::string_view stdlibId;
std::string_view flags;
};
constexpr StdlibGateRule kStdlibGates[] = {
// libstdc++ and MSVC STL ship their unstable bits ungated; libc++ hides
// them behind -fexperimental-library.
{ "experimental-library", "libc++", "-fexperimental-library" },
};

void add_unique(std::vector<std::string>& v, std::string_view f) {
if (f.empty()) return;
if (std::find(v.begin(), v.end(), f) == v.end()) v.emplace_back(f);
}

} // namespace

int compiler_major(const Toolchain& tc) {
int major = 0;
bool any = false;
for (char c : tc.version) {
if (c < '0' || c > '9') break;
major = major * 10 + (c - '0');
any = true;
}
return any ? major : 0;
}

std::string latest_std_canonical(const Toolchain& tc, int* levelOut) {
const int major = compiler_major(tc);
for (auto& r : kLatestStd) {
if (r.family != tc.compiler) continue;
if (major >= r.minMajor) {
if (levelOut) *levelOut = r.level;
return std::string(r.canonical);
}
}
if (levelOut) *levelOut = 26; // unknown family: newest ratified level
return "c++26";
}

Resolution resolve(const Toolchain& tc) {
Resolution r;
r.stdCanonical = latest_std_canonical(tc, &r.stdLevel);
const int major = compiler_major(tc);
for (auto& g : kGates) {
FeatureState st;
st.name = std::string(g.name);
st.paper = std::string(g.paper);
const GateRule* hit = nullptr;
for (auto& rule : g.rules) {
if (rule.family == tc.compiler) { hit = &rule; break; }
}
if (!hit) {
st.reason = std::format("{}: unsupported", tc.compiler_name());
} else if (major < hit->minMajor) {
st.reason = std::format("{} {} < {}", tc.compiler_name(), major, hit->minMajor);
} else {
st.enabled = true;
st.flags = std::string(hit->flags);
add_unique(r.flags, hit->flags);
}
r.features.push_back(std::move(st));
}
for (auto& sg : kStdlibGates) {
FeatureState st;
st.name = std::string(sg.name);
st.paper = "stdlib";
if (tc.stdlibId == sg.stdlibId) {
st.enabled = true;
st.flags = std::string(sg.flags);
add_unique(r.flags, sg.flags);
} else {
st.reason = std::format("stdlib is {}, gate applies to {}",
tc.stdlibId.empty() ? "unknown" : tc.stdlibId, sg.stdlibId);
}
r.features.push_back(std::move(st));
}
return r;
}

std::string std_flag(const Toolchain& tc, std::string_view canonical, int level) {
std::string resolvedCanonical(canonical);
int resolvedLevel = level;
// c++latest (999) / c++fly (1000): resolve to the family's real latest
// level — the raw canonical is not a valid -std= spelling on GNU.
if (level >= 999) resolvedCanonical = latest_std_canonical(tc, &resolvedLevel);
return std_flag_for(dialect_for(tc), resolvedCanonical, resolvedLevel);
}

std::vector<std::string> effective_dialect_flags(const Toolchain& tc,
bool experimental, std::vector<std::string> manifestDialectFlags)
{
if (!experimental) return manifestDialectFlags;
for (auto& f : resolve(tc).flags) add_unique(manifestDialectFlags, f);
return manifestDialectFlags;
}

} // namespace mcpp::toolchain::cppfly
2 changes: 1 addition & 1 deletion src/toolchain/fingerprint.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import mcpp.toolchain.detect;

export namespace mcpp::toolchain {

inline constexpr std::string_view MCPP_VERSION = "0.0.90";
inline constexpr std::string_view MCPP_VERSION = "0.0.91";

struct FingerprintInputs {
Toolchain toolchain;
Expand Down
Loading
Loading