diff --git a/.agents/docs/2026-07-14-single-pr-091-implementation-plan.md b/.agents/docs/2026-07-14-single-pr-091-implementation-plan.md new file mode 100644 index 0000000..74f6f24 --- /dev/null +++ b/.agents/docs/2026-07-14-single-pr-091-implementation-plan.md @@ -0,0 +1,451 @@ +# c++fly(0.0.91)单 PR 实施计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** `standard = "c++fly"` 一键启用"resolved 工具链最新 -std= 档位 + 全部实验特性门(语言 + 标准库)",并顺手修复 `c++latest` 在 GNU 族误拼 `-std=c++latest` 的暗雷(设计 §11-Q5,已确认为真 bug)。 + +**Architecture:** 新模块 `mcpp.toolchain.cppfly`(三表数据 + 纯函数查询点)按〈CompilerId × major 版本 × stdlibId〉解析档位与旗标;产物经既有图全局方言旗标通道(`BuildPlan::dialectFlags` + prepare 的 `stdFlagAndDialect`)下发,两消费点通过同一对纯函数(`cppfly::std_flag` / `cppfly::effective_dialect_flags`)保证同源。设计文档:`.agents/docs/2026-07-14-std-features-experimental-gate-design.md`。 + +**Tech Stack:** C++23 modules(.cppm)、gtest 单测(`mcpp test`)、bash e2e(`tests/e2e/run_all.sh`)。 + +## Global Constraints + +- 版本号:0.0.91(`mcpp.toml:3` `version` + `src/toolchain/fingerprint.cppm:21` `MCPP_VERSION` 两处)。 +- 不写 `c++fly` 的 manifest 行为不变;`c++latest` 行为从"GNU 上产出非法 `-std=c++latest`"变为"解析到族最新档"(bug 修复,CHANGELOG 记 Fixed)。 +- 已实机定案的表数据:reflection→GCC≥16 `-freflection`;contracts→GCC≥16 空串(随 `-std=c++26` 默认启用);libc++→`-fexperimental-library`;GCC≥14→c++26/GCC≤13→c++23;Clang≥17→c++2c;MSVC 由 `std_flag_for` 既有分支出 `/std:c++latest`。 +- 单测跑法:`"$MCPP_FRESH" test`(自举:先 `mcpp build`);e2e:`MCPP= tests/e2e/run_all.sh`(或单跑单脚本 `MCPP= bash tests/e2e/.sh`)。 +- 本机注意:`~/.xlings` 的 g++ shim 坏(interp 烙死已消失的 /tmp 路径);构建全部通过 mcpp 自身工具链解析(`~/.mcpp/registry`)走,别依赖 PATH 上的 g++。 +- commit 风格参照 git log:`feat(scope): ...` / `fix(scope): ...`,本 PR 最终 squash。 + +--- + +### Task 1: `src/toolchain/cppfly.cppm` — 三表 + 查询点 + +**Files:** +- Create: `src/toolchain/cppfly.cppm` +- Test: `tests/unit/test_cppfly.cpp` + +**Interfaces (Produces):** +```cpp +namespace mcpp::toolchain::cppfly { + struct FeatureState { std::string name, paper, flags, reason; bool enabled; }; + struct Resolution { std::string stdCanonical; int stdLevel; + std::vector flags; // 全部启用门旗标,去重 + std::vector features; }; // enabled+skipped,summary 用 + int compiler_major(const Toolchain& tc); // "16.1.0"→16;不可解析→0 + std::string latest_std_canonical(const Toolchain& tc, int* levelOut = nullptr); + Resolution resolve(const Toolchain& tc); + // c++latest/c++fly 感知的 std 旗标(其余 canonical 原样走 std_flag_for) + std::string std_flag(const Toolchain& tc, std::string_view canonical, int level); + // manifest 方言旗标 ∪ fly 门旗标(去重,保持声明序)——两消费点共用 + std::vector effective_dialect_flags(const Toolchain& tc, + bool experimental, std::vector manifestDialectFlags); +} +``` + +- [ ] **Step 1: 写失败单测** `tests/unit/test_cppfly.cpp`(gtest 风格仿 `test_toolchain_dialect.cpp`;`make_tc(id, version, stdlibId)` 本地 helper): + +```cpp +#include +import std; +import mcpp.toolchain.cppfly; +import mcpp.toolchain.model; +using namespace mcpp::toolchain; + +namespace { +Toolchain tc_of(CompilerId id, std::string ver, std::string stdlib = "libstdc++") { + Toolchain tc; tc.compiler = id; tc.version = std::move(ver); + tc.stdlibId = std::move(stdlib); return tc; +} +bool has_flag(const std::vector& v, std::string_view f) { + return std::find(v.begin(), v.end(), f) != v.end(); +} +const cppfly::FeatureState* feature(const cppfly::Resolution& r, std::string_view n) { + for (auto& f : r.features) if (f.name == n) return &f; + return nullptr; +} +} // namespace + +TEST(CppFly, CompilerMajor) { + EXPECT_EQ(cppfly::compiler_major(tc_of(CompilerId::GCC, "16.1.0")), 16); + EXPECT_EQ(cppfly::compiler_major(tc_of(CompilerId::Clang, "22.1.8")), 22); + EXPECT_EQ(cppfly::compiler_major(tc_of(CompilerId::GCC, "")), 0); +} + +TEST(CppFly, LatestStdCanonical) { + int lvl = 0; + EXPECT_EQ(cppfly::latest_std_canonical(tc_of(CompilerId::GCC, "16.1.0"), &lvl), "c++26"); + EXPECT_EQ(lvl, 26); + EXPECT_EQ(cppfly::latest_std_canonical(tc_of(CompilerId::GCC, "13.2.0")), "c++23"); + EXPECT_EQ(cppfly::latest_std_canonical(tc_of(CompilerId::Clang, "22.1.8")), "c++2c"); + EXPECT_EQ(cppfly::latest_std_canonical(tc_of(CompilerId::MSVC, "19.44")), "c++26"); +} + +TEST(CppFly, ResolveGcc16) { + auto r = cppfly::resolve(tc_of(CompilerId::GCC, "16.1.0")); + EXPECT_EQ(r.stdCanonical, "c++26"); + EXPECT_TRUE(has_flag(r.flags, "-freflection")); + auto* refl = feature(r, "reflection"); + ASSERT_NE(refl, nullptr); EXPECT_TRUE(refl->enabled); EXPECT_EQ(refl->paper, "P2996"); + auto* con = feature(r, "contracts"); + ASSERT_NE(con, nullptr); EXPECT_TRUE(con->enabled); EXPECT_TRUE(con->flags.empty()); + EXPECT_FALSE(has_flag(r.flags, "-fexperimental-library")); // libstdc++ 无门 +} + +TEST(CppFly, ResolveGcc15SkipsByVersion) { + auto r = cppfly::resolve(tc_of(CompilerId::GCC, "15.1.0")); + EXPECT_TRUE(r.flags.empty()); + auto* refl = feature(r, "reflection"); + ASSERT_NE(refl, nullptr); EXPECT_FALSE(refl->enabled); + EXPECT_NE(refl->reason.find("16"), std::string::npos); // 提示最低版本 +} + +TEST(CppFly, ResolveClangLibcxx) { + auto r = cppfly::resolve(tc_of(CompilerId::Clang, "22.1.8", "libc++")); + EXPECT_EQ(r.stdCanonical, "c++2c"); + EXPECT_TRUE(has_flag(r.flags, "-fexperimental-library")); + auto* refl = feature(r, "reflection"); + ASSERT_NE(refl, nullptr); EXPECT_FALSE(refl->enabled); + auto* lib = feature(r, "experimental-library"); + ASSERT_NE(lib, nullptr); EXPECT_TRUE(lib->enabled); +} + +TEST(CppFly, ResolveMsvcAllSkipped) { + auto r = cppfly::resolve(tc_of(CompilerId::MSVC, "19.44", "msvc-stl")); + EXPECT_TRUE(r.flags.empty()); + for (auto& f : r.features) EXPECT_FALSE(f.enabled); +} + +TEST(CppFly, StdFlagResolvesLatestAndFly) { + auto gcc16 = tc_of(CompilerId::GCC, "16.1.0"); + EXPECT_EQ(cppfly::std_flag(gcc16, "c++fly", 1000), "-std=c++26"); + EXPECT_EQ(cppfly::std_flag(gcc16, "c++latest", 999), "-std=c++26"); // Q5 修复 + EXPECT_EQ(cppfly::std_flag(gcc16, "c++23", 23), "-std=c++23"); // 原样 + EXPECT_EQ(cppfly::std_flag(tc_of(CompilerId::Clang, "22.1.8"), "c++fly", 1000), "-std=c++2c"); + EXPECT_EQ(cppfly::std_flag(tc_of(CompilerId::MSVC, "19.44"), "c++fly", 1000), "/std:c++latest"); +} + +TEST(CppFly, EffectiveDialectFlagsDedup) { + auto gcc16 = tc_of(CompilerId::GCC, "16.1.0"); + auto out = cppfly::effective_dialect_flags(gcc16, true, {"-freflection", "-fchar8_t"}); + EXPECT_EQ(std::count(out.begin(), out.end(), std::string("-freflection")), 1); + EXPECT_TRUE(has_flag(out, "-fchar8_t")); + auto off = cppfly::effective_dialect_flags(gcc16, false, {"-fchar8_t"}); + EXPECT_EQ(off, (std::vector{"-fchar8_t"})); +} +``` + +- [ ] **Step 2: 跑单测确认失败**(模块不存在 → 编译错) + Run: `"$MCPP_FRESH" test 2>&1 | tail -20` — Expected: FAIL(`mcpp.toolchain.cppfly` 未找到) + +- [ ] **Step 3: 实现 `src/toolchain/cppfly.cppm`**: + +```cpp +// 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 that fixes +// standard = "c++latest" mis-spelling -std=c++latest on the GNU family. +// +// 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 §2/§11-Q3. + +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" / ... + int stdLevel = 26; + std::vector flags; // union of enabled gate flags, deduped + std::vector features; // enabled + skipped, declaration order +}; + +int compiler_major(const Toolchain& tc); +std::string latest_std_canonical(const Toolchain& tc, int* levelOut = nullptr); +Resolution resolve(const Toolchain& tc); +std::string std_flag(const Toolchain& tc, std::string_view canonical, int level); +std::vector effective_dialect_flags(const Toolchain& tc, + bool experimental, std::vector manifestDialectFlags); + +} // namespace mcpp::toolchain::cppfly + +namespace mcpp::toolchain::cppfly { + +namespace { + +// ── Table 1: family × min major → latest supported -std= canonical ────── +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 }, + { CompilerId::MSVC, 0, "c++26", 26 }, // spelled /std:c++latest by std_flag_for +}; + +// ── Table 2: experimental language-feature gates (version-gated) ──────── +struct GateRule { CompilerId family; int minMajor; std::string_view flags; }; +struct Gate { std::string_view name, paper; std::span rules; }; +constexpr GateRule kReflectionRules[] = { { CompilerId::GCC, 16, "-freflection" } }; +constexpr GateRule kContractsRules[] = { { CompilerId::GCC, 16, "" } }; // on by -std=c++26 (probe-pinned) +constexpr Gate kGates[] = { + { "reflection", "P2996", kReflectionRules }, + { "contracts", "P2900", kContractsRules }, +}; + +// ── Table 3: stdlib experimental gates (stdlib dimension) ─────────────── +struct StdlibGateRule { std::string_view name, stdlibId, flags; }; +constexpr StdlibGateRule kStdlibGates[] = { + { "experimental-library", "libc++", "-fexperimental-library" }, +}; + +void add_unique(std::vector& 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; + for (char c : tc.version) { + if (c < '0' || c > '9') break; + major = major * 10 + (c - '0'); + } + return major; +} + +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{ std::string(g.name), std::string(g.paper), {}, {}, false }; + 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{ std::string(sg.name), "stdlib", {}, {}, false }; + 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 effective_dialect_flags(const Toolchain& tc, + bool experimental, std::vector manifestDialectFlags) +{ + if (!experimental) return manifestDialectFlags; + for (auto& f : resolve(tc).flags) add_unique(manifestDialectFlags, f); + return manifestDialectFlags; +} + +} // namespace mcpp::toolchain::cppfly +``` + +- [ ] **Step 4: 跑单测确认通过** — Run: `"$MCPP_FRESH" test`(至少 CppFly.* 全 PASS) +- [ ] **Step 5: Commit** — `git add src/toolchain/cppfly.cppm tests/unit/test_cppfly.cpp && git commit -m "feat(toolchain): cppfly registry — latest-std + experimental gates per compiler"` + +--- + +### Task 2: manifest `c++fly` 值 + 消费点接线(plan/prepare/fingerprint)+ summary + +**Files:** +- Modify: `src/manifest/types.cppm:27-32`(CppStandardConfig)、`types.cppm:528-539`(normalize) +- Modify: `src/build/plan.cppm:322-331`(std flag + dialect flags) +- Modify: `src/build/prepare.cppm:2586-2596`(stdFlagAndDialect)、`prepare.cppm:2642-2649`(fingerprint) +- Test: `tests/unit/test_manifest.cpp`(normalize 断言;若 normalize 测试实际在 test_toml.cpp 则加在彼处,先 grep `normalize_cpp_standard` 定位) + +**Interfaces:** +- Consumes: Task 1 的 `cppfly::std_flag` / `cppfly::effective_dialect_flags` / `cppfly::resolve`。 +- Produces: `CppStandardConfig::experimental`(bool,`normalize_cpp_standard("c++fly")` 置 true,level=1000)。 + +- [ ] **Step 1: 写失败单测**(定位既有 normalize 测试后追加): + +```cpp +TEST(NormalizeCppStandard, CppFly) { + auto cfg = mcpp::manifest::normalize_cpp_standard("c++fly"); + ASSERT_TRUE(cfg.has_value()); + EXPECT_EQ(cfg->canonical, "c++fly"); + EXPECT_EQ(cfg->level, 1000); + EXPECT_TRUE(cfg->experimental); + EXPECT_FALSE(mcpp::manifest::normalize_cpp_standard("c++23")->experimental); + auto bad = mcpp::manifest::normalize_cpp_standard("c++flyy"); + ASSERT_FALSE(bad.has_value()); + EXPECT_NE(bad.error().find("c++fly"), std::string::npos); // 白名单消息含新拼写 +} +``` + +- [ ] **Step 2: 跑单测确认失败**(experimental 字段不存在 → 编译错) +- [ ] **Step 3: 实现**—— + - `types.cppm` CppStandardConfig 加 `bool experimental = false;`;normalize 在 `c++latest` 分支后加: +```cpp + if (s == "c++fly") { + out.canonical = "c++fly"; + out.flag = "-std=c++26"; // GNU 静态兜底;真实拼写走 cppfly::std_flag + out.level = 1000; // > c++latest(999):latest 之上再加实验门 + out.gnuDialect = false; + out.experimental = true; + return out; + } +``` + 错误消息补 `c++fly`("expected c++23, c++26, c++2c, gnu++23, gnu++26, c++latest, or c++fly")。 + - `plan.cppm:322-331` 改为(import mcpp.toolchain.cppfly;顶部 import 区补): +```cpp + bool experimentalStd = false; + if (auto stdCfg = mcpp::manifest::normalize_cpp_standard(manifest.package.standard)) { + plan.cppStandard = stdCfg->canonical; + experimentalStd = stdCfg->experimental; + // Spelled per-dialect AND per-toolchain-latest for c++latest/c++fly. + plan.cppStandardFlag = mcpp::toolchain::cppfly::std_flag( + tc, stdCfg->canonical, stdCfg->level); + } + for (auto& f : mcpp::toolchain::cppfly::effective_dialect_flags( + tc, experimentalStd, mcpp::manifest::dialect_flags(manifest.buildConfig))) { + plan.dialectFlags += ' '; + plan.dialectFlags += f; + } +``` + - `prepare.cppm:2586-2596` 同型改写(同一对函数 → 两点同源),并紧随其后加 summary(仅 experimental 时,一次): +```cpp + std::string stdFlagAndDialect = mcpp::toolchain::cppfly::std_flag( + *tc, m->cppStandard.canonical, m->cppStandard.level); + for (auto& f : mcpp::toolchain::cppfly::effective_dialect_flags( + *tc, m->cppStandard.experimental, + mcpp::manifest::dialect_flags(m->buildConfig))) { + stdFlagAndDialect += ' '; + stdFlagAndDialect += f; + } + if (m->cppStandard.experimental) { + 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 {}: -std flag `{}`; enabled: {}; skipped: {}", + tc->label(), plan_std_flag_placeholder /* 用 stdFlagAndDialect 首段 */, + enabled.empty() ? "(none)" : enabled, + skipped.empty() ? "(none)" : skipped); + } +``` + (实现时用局部变量存 `cppfly::std_flag(...)` 结果避免占位写法;summary 打到 stdout,e2e grep。) + - fingerprint 卫生(`prepare.cppm:2645` 附近): +```cpp + fpi.compileFlags = canonical_compile_flags(*m) + + canonical_package_build_metadata(packages); + if (m->cppStandard.experimental) + for (auto& f : mcpp::toolchain::cppfly::resolve(*tc).flags) + { fpi.compileFlags += ' '; fpi.compileFlags += f; } +``` +- [ ] **Step 4: 全量单测通过** — Run: `"$MCPP_FRESH" test` +- [ ] **Step 5: 冒烟**——临时目录 `mcpp new flydemo && cd flydemo`,manifest 改 `standard = "c++fly"`,`mcpp build`(gcc16)成功且输出 summary 行;`standard = "c++latest"` 也构建成功(Q5 修复实证)。 +- [ ] **Step 6: Commit** — `git commit -m "feat(manifest,build): standard=c++fly — latest std + all experimental gates; fix c++latest GNU spelling"` + +--- + +### Task 3: e2e ×3 + +**Files:** +- Create: `tests/e2e/100_cppfly_reflection.sh`(gcc16 硬路径;开头 `# requires: gcc`,复用 98 的 payload 探测) +- Create: `tests/e2e/101_cppfly_llvm_soft.sh`(llvm 软路径;探测 llvm payload,无则 SKIP-INLINE) +- Modify: 确认 `tests/e2e/run_all.sh` 的编号 glob 覆盖三位数(不覆盖则改 glob) + +- [ ] **Step 1: `100_cppfly_reflection.sh`** — 工程仅 `standard = "c++fly"`(无任何手写旗标),src/main.cpp 用 98 的反射示例(std::meta 遍历 Point 成员);断言:`mcpp run` 输出 `x 2`/`y 3`;stdout 含 `c++fly on` 与 `reflection (-freflection)`;`std-module.json` 的 std_flag 含 `-std=c++26` 且含 `-freflection`。 +- [ ] **Step 2: `101_cppfly_llvm_soft.sh`** — 探测 `${MCPP_HOME}/registry/data/xpkgs/xim-x-llvm*` 有 clang++ payload,无则 SKIP;工程 `standard = "c++fly"` + 平凡 `import std;` main;断言:构建成功;stdout 含 `skipped: reflection`;compile_commands.json 含 `-std=c++2c`;stdlib 为 libc++ 时含 `-fexperimental-library`。 +- [ ] **Step 3: c++latest 回归**——并入 100 号脚本尾部:同工程改 `standard = "c++latest"`,`mcpp build` 成功且 compile_commands.json 含 `-std=c++26`、不含 `-std=c++latest`。 +- [ ] **Step 4: 本地跑三条**:`MCPP= bash tests/e2e/100_cppfly_reflection.sh` 等,全 PASS(llvm 无 payload 则 SKIP 视为通过)。 +- [ ] **Step 5: Commit** — `git commit -m "test(e2e): c++fly hard/soft paths + c++latest spelling regression"` + +--- + +### Task 4: 版本号 + CHANGELOG + README + +- [ ] `mcpp.toml:3` → `version = "0.0.91"`;`src/toolchain/fingerprint.cppm:21` → `MCPP_VERSION = "0.0.91"`。 +- [ ] `CHANGELOG.md` 顶部加 `## [0.0.91] — 2026-07-14`:新增 `standard = "c++fly"`(语义三件套 + summary + 软跳过);修复 `c++latest` GNU 拼写;设计文档链接。 +- [ ] README(两语言)manifest/standard 说明处补一行 `c++fly`(grep `c++latest` 定位;没有则跳过 README,CHANGELOG 已覆盖)。 +- [ ] Commit — `git commit -m "chore(release): 0.0.91 — changelog + version bump"` + +--- + +### Task 5: 本地全量验证 → PR → CI → squash 合入 + +- [ ] 分支 `feat/cppfly-091`;全量:`mcpp build && "$MCPP_FRESH" test && MCPP=$MCPP_FRESH tests/e2e/run_all.sh`(基线对照:环境性失败清单见记忆 2026-07-07,新失败为回归)。 +- [ ] `gh pr create` 标题 `feat: standard=c++fly — one-knob latest std + experimental features (#design 2026-07-14) — 0.0.91`;正文含设计文档路径、实机探测结论、验收清单勾选。 +- [ ] 等 CI 全绿(ci-linux + ci-linux-e2e + ci-macos + ci-windows);失败按 systematic-debugging 修。 +- [ ] `gh pr merge --squash`(仓库惯例 bypass 分支保护:`--admin`)。 + +### Task 6: release 0.0.91 + xlings 生态闭环 + 真实验证 + +按记忆 `release-publish-pipeline` 执行(此阶段开始前重读该记忆文件): +- [ ] main 上触发 release(tag v0.0.91 / release.yml),产出各平台资产。 +- [ ] 镜像资产到 xlings-res(gh + gtc 双端;**绝不删既有资产**,`--clobber` 只对非 serving 资产)。 +- [ ] 更新 xim-pkgindex 的 mcpp 0.0.91 条目(gitee 自动镜像 github)。 +- [ ] `xlings update` + `xlings install mcpp -y` 装出 0.0.91,`mcpp --version` 验证。 +- [ ] bump 本仓 CI bootstrap pin(仿 `ce34a70^` 的 `ci: workspace mcpp bootstrap pin -> 0.0.89` 提交)。 +- [ ] 真实端到端:xlings 装的 0.0.91 新建工程,`standard = "c++fly"`,gcc16 真实构建运行反射示例,记录输出作为汇报证据。 + +## Self-Review 备忘 + +类型一致性:`cppfly::std_flag(tc, canonical, level)` 三处调用(plan/prepare×2)签名一致;`effective_dialect_flags` 返回 vector 两处消费者各自 join。Spec 覆盖:设计 §5.1-5.6/§9 验收 1-5 全部有任务对应(验收 2 的 llvm 硬错→软跳过 summary 即 e2e 101)。 diff --git a/.agents/docs/2026-07-14-std-features-experimental-gate-design.md b/.agents/docs/2026-07-14-std-features-experimental-gate-design.md new file mode 100644 index 0000000..f7c92de --- /dev/null +++ b/.agents/docs/2026-07-14-std-features-experimental-gate-design.md @@ -0,0 +1,341 @@ +# c++fly:一键启用"最新标准 + 全部实验特性"(语言 + 标准库)设计 + +> 日期:2026-07-14 · 基线:ce34a70(0.0.90 已含 #210 方言旗标全图化) +> 输入:用户需求——"mcpp.toml 里指定/enable 一项即可启用 C++26 反射等实验特性,跨编译器"; +> review 反馈(2026-07-14)——"收敛到:必然当前编译器最新 C++ 标准 + 可开启的所有实验特性,核心是语言特性 + 标准库" +> 前置文档:`2026-07-13-post-089-roadmap-and-std-dialect-flags-design.md`(§1.3/§1.5)、 +> `2026-07-13-single-pr-090-implementation-plan.md`(S1)、`2026-06-01-cpp-standard-first-class-design.md` +> 本文是 post-089 路线图 §1.5 预留位的续篇:"`reflection = true` 这类糖等用户需求明确再加"——需求现已明确。 +> review 定案(2026-07-14):值命名 = **`c++fly`**(§11-Q1);index 收录策略 = **v1 warn 观察**(§11-Q2);GCC16 契约旗标已实机定案(§11-Q3)。 + +## 0. 一句话结论 + +**新增一个版本无关的 `standard` 值 `c++fly`(命名已定案,§11-Q1):** + +```toml +[package] +standard = "c++fly" # 一键:工具链最新 -std= 档位 + 它能开的全部实验特性门 +``` + +语义 = ①resolved 工具链支持的**最新 `-std=` 档位**(GCC16→`-std=c++26`,Clang→`-std=c++2c`, +MSVC→`/std:c++latest`)②该工具链支持的**全部实验性语言特性门**(GCC16→`-freflection` 等) +③**标准库实验门**(libc++→`-fexperimental-library`;libstdc++/MSVC STL 无需)。 +判定由工具链层新数据表 `cppfly.cppm` 按〈编译器族 × 版本 × stdlib〉给出,产物汇入 +0.0.90 已建成的图全局方言旗标通道——**机制零新管线,新增的只是"意图 → 档位+旗标"这层数据**。 +不支持的特性**软跳过**并打印 summary(该值的本义就是"这套工具链能玩的都给我")。 + +用户体验对比: + +```toml +# 0.0.90 现状:自己查档位、自己查旗标拼写,换 Clang/MSVC 即坏 +[package] +standard = "c++26" +[build] +dialect_cxxflags = ["-freflection"] + +# 本设计:一行,意图完整,跨编译器 +[package] +standard = "c++fly" +``` + +## 1. 现状盘点:0.0.90 已有什么、还缺什么 + +### 1.1 已有(#210 落地件,全部可复用) + +| 件 | 位置 | 作用 | +|---|---|---| +| known-list `is_dialect_flag()` | `src/manifest/types.cppm:453-467` | `-freflection/-fcontracts/-fchar8_t/-D_GLIBCXX_USE_CXX11_ABI=` 识别 | +| 逃生舱 `[build] dialect_cxxflags` | `src/manifest/toml.cppm:648` | 用户显式声明图全局旗标 | +| 合并去重 `dialect_flags(bc)` | `src/manifest/types.cppm:469-479` | 显式 ∪ 自动提升 | +| 图全局注入 | `plan.cppm:328-331` → `flags.cppm:304-310` | 进全局 `$cxxflags`(全图 TU) | +| scan/std-BMI 同步 | `prepare.cppm:2590-2607`(`stdFlagAndDialect`) | 扫描与 std/std.compat 预构建同方言 | +| 指纹自动失效 | std-module.json 哈希完整命令串 | 旗标变更 → BMI 重建,零新缓存逻辑 | +| e2e | `98_reflection_import_std.sh` | gcc16 + `-freflection` + import std 回归 | +| standard 归一化 | `types.cppm:481-539`(`normalize_cpp_standard`) | c++23/26/2c/gnu++/latest 白名单,level 数值化 | +| 各家 `-std=` 拼写 | `dialect.cppm:116-123`(`std_flag_for`) | GNU `-std=` / MSVC `/std:c++latest` | + +### 1.2 缺口(本设计要补的四件事) + +1. **无"最新档位"概念**:`c++latest` 是静态映射(canonical 固定,GNU 侧落到 + `-std=c++26`),不随 resolved 工具链取其真正的最新档;更没有"档位 + 实验门"的捆绑。 +2. **无能力/版本校验**:代码里没有任何 `version >= N` 门控(`provider.cppm` 的 + `capabilities_for()` 只按 CompilerId+triple,未用 `Toolchain::version`);`gcc@15` + + `-freflection` 只能等编译器报未知选项。 +3. **无一键全开**:每个特性一个旗标,用户要自己攒清单、自己跟进编译器演进。 +4. **拼写族问题悬空**:`dialect_cxxflags` 存 GNU 拼写;MSVC 原生后端(0.0.90 S2-S6) + 落地后同一 manifest 在 cl.exe 上是非法选项。 + +### 1.3 概念模型:两个轴,一个捆绑值 + +- `standard` 决定 `-std=` 档位(23/26/latest),是**语言基线**; +- 实验特性是**基线之上的附加门**:同一 `-std=c++26` 下,GCC 16 的反射仍需 `-freflection` + (libstdc++ `` 整体被 `__cpp_impl_reflection` 守卫,该宏仅由此旗标定义—— + post-089 文档 §1.1)。 +- 初版设计曾把两轴拆开暴露(`standard` + 具名 `std_features` 列表);review 反馈明确 + **场景是"前沿尝鲜"整体模式**——要的是"必然最新档 + 全部能开的门"这一个捆绑值, + 细粒度选择不是当前需求(§4-C 记录取舍,§10 保留延伸位)。 + +## 2. 外部事实:编译器实验特性支持矩阵(2026-07 快照) + +| 特性(提案) | GCC 16.1 | Clang 上游 22/23 | Bloomberg clang-p2996 | MSVC v145 (VS2026 18.6) | +|---|---|---|---|---| +| 最新 `-std=` 档位 | `-std=c++26` | `-std=c++2c` | `-std=c++26` | 仅 `/std:c++latest`(无 `/std:c++26`) | +| 反射 P2996+P3394 | ✅ 需 **`-freflection`**(实验) | ❌ 无 | ✅ `-freflection`/`-freflection-latest`(含 P1306/P3096/P3394/P3491) | ❌ 无,无公开 ETA | +| 契约 P2900R14 | ✅ 实验(**随 `-std=c++26` 默认启用,无需附加旗标**;`-fcontracts` 仅用于在更早标准下强开;evaluation-semantic 由 `-fcontract-evaluation-semantic=` 控制。已实机定案,§11-Q3) | ❌ 无 | — | ❌ 无 | +| 展开语句 P1306 | ✅ 随 `-std=c++26` | Clang 23 partial | 经 `-freflection-latest` | ❌ | +| 标准库实验门 | 无需(libstdc++ 不设门) | libc++ 需 **`-fexperimental-library`** | 同 | 无需(MSVC STL 随 `/std:c++latest`) | + +要点: +- 反射已于 2025 Sofia 会议进入 C++26;GCC 16.1(2026-04 发布)是**唯一上游落地**, + 且仍标注 experimental;Clang 上游没有,只有 Bloomberg fork;MSVC 没有。 +- 即 **v1 语言特性侧的现实收益面 = GCC ≥ 16**(mcpp 生态已有 gcc16 系工具链包,winlibs + MinGW 亦然);Clang 侧 v1 收益 = 最新档 + `-fexperimental-library`;Clang/MSVC 缺失的 + 特性由 summary 如实呈现,而非编译器原始报错。 +- 该矩阵是**易变数据**——这正是它必须住在 mcpp 工具链层数据表里、而不是让每个用户 + manifest 各自硬编码旗标的根本原因(Clang 落地 P2996 后,mcpp 发版加一行,全生态受益)。 + +来源:gcc.gnu.org/projects/cxx-status.html、gcc-16/changes.html、clang.llvm.org/cxx_status.html、 +github.com/bloomberg/clang-p2996、learn.microsoft.com(/std 文档、VS2026 release notes)、 +libcxx docs(UsingLibcxx)。 + +## 3. 先例调研 + +| 系统 | 机制 | 对本设计的启示 | +|---|---|---| +| MSVC | `/std:c++latest` = "下一标准草案的全部已实现特性 + 部分实验特性"一键聚合 | 与本设计同构的**厂商级先例**:一个档位值捆绑"最新 + 实验" | +| libc++ | `-fexperimental-library` 一个旗标聚合全部不稳定库件 | 库侧聚合开关成立;mcpp 把它并进同一个值 | +| Rust nightly | `#![feature(x)]` 具名注册表 + stable 拒绝 | 具名粒度模型,对应 §10 的延伸位;其"注册表 + 工具链门控"内核被本设计沿用 | +| CMake `import std` | `CMAKE_EXPERIMENTAL_CXX_IMPORT_STD` = 按版本变化的 UUID,升级即失效 | 反面参照:故意摩擦表达"无稳定性承诺"。mcpp 用 summary 警示而非 UUID 摩擦(§5.4) | +| xmake/meson/bazel | 只有 `-std=` 档位设定,无实验特性抽象 | 空白区,mcpp 的差异化点 | + +## 4. 方案空间与取舍 + +### 方案 A:钉版本的伪标准值——`standard = "c++26-fly"`(最初提案) + +三个结构性问题:**钉死 26** → 每个标准期一个新值,gnu++26-fly/c++latest-fly 组合爆炸; +GCC 17 若默认启用反射,"26-fly" 对它失义;canonical 白名单每档 ×2。 +判定:否决**钉版本**的拼法;其"一键"诉求由方案 D 的版本无关值承接。 + +### 方案 B:布尔总开关——`[build] experimental = true` + +"全部"本就是随工具链漂移的移动目标,这一点方案 D 同样存在(见 §5.5 如何面对); +B 真正的问题是**放错了地方**:它与 `standard` 分居两表、且允许 `standard = "c++23"` + +`experimental = true` 这种自相矛盾的组合(实验特性几乎全部要求最新档)。review 反馈 +"必然当前编译器最新 C++ 标准"指明二者不该独立配置。判定:否决独立布尔键。 + +### 方案 C:具名特性列表——`std_features = ["reflection", ...]`(初版推荐) + +粒度最细、显式具名可做硬错诊断,Rust nightly 同构。review 反馈指出其错位:名字 +"体现不出是开最新支持的",且细粒度选择不是当前需求——用户场景是**整体尝鲜模式**, +不是逐特性挑选。判定:**不作为 v1 表面**;其内核(注册表 + 版本门控)全部保留为 +方案 D 的实现机制;若日后出现"库硬依赖单一特性"的需求,再把列表作为高级表面加回 +(§10),届时零机制返工。 + +### 方案 D(采纳):版本无关的捆绑档位值——`standard = "c++fly"` + +一个值 = 最新档 + 全部实验门(语言 + 标准库)。关键设计判断: + +1. **版本无关命名**消解了 A 的全部问题:不钉 26,永远指"当前工具链的最前沿", + 特性毕业(变默认)后该值语义自动收窄到"剩余的实验门",名字永不失义; + 白名单只加一个值。 +2. **放在 `standard` 键上**消解了 B 的问题:与档位天然捆绑,不存在矛盾组合; + 用户"enable 一个项"的诉求被字面满足。 +3. `c++latest` 与它的分工清晰:`latest` = 最新**已定档**(仍是确定性构建), + `c++fly` = latest **+ 实验门**(明示放弃跨工具链确定性,见 §5.5)。 +4. 机制上它只是方案 C 注册表的一个"全选 + 取最新档"查询,实现同一张表。 + +## 5. 设计(方案 D 详述) + +### 5.1 manifest 表面与解析 + +```toml +[package] +standard = "c++fly" # 唯一新增;不需要任何其他键 +``` + +- `normalize_cpp_standard`(`types.cppm:481`)增一个值:canonical `"c++fly"`, + `level = 1000`(> `c++latest` 的 999,语义"latest 之上"),新增 + `CppStandardConfig::experimental = true` 字段。白名单错误消息同步补拼写。 +- **不做 gnu 变体**(`gnu++fly`):实验模式下 GNU 扩展无额外意义,YAGNI。 +- parse 期无其他新键、无形态校验负担。 + +### 5.2 工具链层:`src/toolchain/cppfly.cppm`(新模块,方案 C 内核) + +模块名对齐 manifest 值(review 拍板):收敛后本模块的单一职责就是"`c++fly` 在各 +编译器上的支持情况与映射",以值为名最可检索、职责边界自解释("职责命名、禁口袋名"); +若日后加回具名列表表面(§10 延伸位),注册表仍住这里,届时再评估是否更名。 +三张数据表 + 一个查询点: + +```cpp +// 表 1:族 × 版本 → 最新已支持 -std= 档位(canonical) +struct LatestStdRule { CompilerId family; int minMajor; std::string_view canonical; }; +// GCC≥14→"c++26"、GCC13→"c++23"、Clang≥17→"c++2c"、MSVC→哨兵(std_flag_for 出 /std:c++latest) + +// 表 2:实验性语言特性门(版本门控) +struct StdFeatureRule { CompilerId family; int minMajor; std::string_view flags; }; +struct StdFeature { std::string_view name; // "reflection" + std::string_view paper; // "P2996" —— summary/文档用 + std::span rules; }; // 无该族行 = 不支持 + +// 表 3:标准库实验门(stdlib 维度,非 compiler 维度) +struct StdlibGateRule { std::string_view stdlibId; // "libc++" + std::string_view flags; }; // "-fexperimental-library" + +struct CppFlyResolution { + std::string stdCanonical; // 档位(交给既有 std 通道) + std::vector flags; // 全部实验门旗标(该族拼写,去重) + std::vector enabled; // 名字 + 旗标 —— summary 用 + std::vector skipped; // 名字 + 原因 —— summary 用 +}; +CppFlyResolution +cppfly::resolve(const Toolchain& tc); // 唯一查询点;首个 Toolchain::version 消费者 +``` + +v1 表内容(刻意最小): + +| 表 | 内容 | +|---|---| +| LatestStd | GCC≥14→c++26 / GCC13→c++23 / Clang≥17→c++2c / MSVC→latest 哨兵 | +| 语言特性 | `reflection`(P2996:GCC≥16→`-freflection`)、`contracts`(P2900:GCC≥16→**空串**,随 c++26 档位默认启用,§11-Q3 已定案——空串行"支持但无需附加旗标"从第一天就有真实客户) | +| 标准库门 | libc++→`-fexperimental-library`(libstdc++/MSVC STL 无行 = 无需) | + +**毕业演化**:某族新版本默认启用某特性后,把该行 `flags` 改为空串(summary 仍列为 +enabled,佐证"该值语义自动收窄");Clang 落地 P2996 后加一行。manifest 永不用改。 + +### 5.3 注入:复用 0.0.90 通道,单点计算、两点消费 + +toolchain resolve 之后(此时才知道族/版本/stdlib): + +``` +cfg.experimental? + → r = cppfly::resolve(tc) + → std 档位:std_flag_for 按 r.stdCanonical 出旗标(签名扩为接收 Toolchain 或预解析结果; + 顺带审计现状 c++latest 在 GNU 侧的 canonical→旗标路径,见 §11-Q5) + → effectiveDialect = dialect_flags(bc) ∪ r.flags // 既有合并函数扩一个入参,去重 +``` + +`effectiveDialect` 在**单一 helper**里计算,喂给现有两个消费点—— +`plan.cppm:328-331`(→ `BuildPlan::dialectFlags` → `flags.cppm:304-310` 全局 `$cxxflags`) +和 `prepare.cppm:2590-2607`(`stdFlagAndDialect` → P1689 扫描 + std/std.compat BMI 预构建)。 +两点必须同源,否则重演"scan/BMI 少旗标"一类分裂 bug。下游(指纹失效、ninja 重建、 +compile_commands.json)全部自动正确,零新管线。 + +**拼写族问题(§1.2-4)顺带解决**:`cppfly::resolve` 按 resolved 族输出拼写, +注入通道的天然是对的;raw `dialect_cxxflags` 逃生舱维持"用户自负拼写"。 + +### 5.4 软语义 + summary(该值的契约) + +`c++fly` 的本义是"这套工具链能玩的都给我"——**逐特性软判定,不硬错** +(硬错会让该值在 Clang/MSVC 上不可用,违背本义)。作为交换,构建头部**必打印** summary +(非 verbose 也打;呼应 CMake"无稳定性承诺"的意图但不设 UUID 摩擦): + +``` +c++fly on gcc@16.1.0: -std=c++26; enabled reflection (-freflection), contracts; skipped: (none) +c++fly on llvm@22.1.8: -std=c++2c; enabled experimental-library (-fexperimental-library); skipped reflection (clang: unsupported), contracts (clang: unsupported) +``` + +`mcpp why toolchain` / `self doctor` 输出追加同一份解析结果(与 0.0.90 §1.3d 的 +dialectFlags 输出并列)。生态衔接:summary/doctor 里对 skipped 特性给出可执行 hint +(如 `hint: reflection needs [toolchain] linux = "gcc@16"`)——mcpp 自分发工具链, +这个修复闭环是 CMake/meson 给不了的。 + +### 5.5 确定性边界与图语义(明示,不遮掩) + +- 该值**按设计放弃跨工具链确定性**(同一 manifest 在 gcc16/llvm22 上档位与旗标不同)—— + 这正是"尝鲜模式"的定义,由 summary 保证可解释性。适用场景:playground、实验分支、 + 跟进标准演进的库的 CI 矩阵;**不适用**:追求可复现分发的包(§11-Q2:index 收录是否 + 警告/拒绝该值)。 +- 图语义零新规则:`standard` 本就 root 统治全图(`prepare.cppm:172`,依赖 manifest 的 + standard 被 root 覆盖),`c++fly` 继承之;实验旗标走图全局方言通道, + 与档位天然同图一致。依赖自己声明 `c++fly` 而 root 没写 → 与今天任何 + standard 不一致的处理相同(root 赢),不新增分歧面。 + +### 5.6 缓存与指纹 + +无新逻辑:档位旗标与实验旗标都进全局 `$cxxflags` 与 std-BMI 命令串,ninja 命令行 +变更与 std-module.json 命令哈希已覆盖失效。**注意**:指纹的输入是 resolve **后**的 +旗标串(非 canonical 名),所以换工具链/升级 mcpp 表数据 → 命令串变 → 自动重建, +"移动目标"不会产生陈腐缓存。 + +## 6. 测试 + +- **单测**(表驱动,仿 `test_toolchain_dialect.cpp`): + - `cppfly::resolve`:gcc13/15/16 × clang17/22 × msvc,断言档位、enabled/skipped + 切分、旗标拼写;stdlib 维度(libc++ vs libstdc++ vs MSVC STL); + - `normalize_cpp_standard("c++fly")`:canonical/level/experimental 字段; + 白名单错误消息含新拼写; + - 与 `dialect_cxxflags` 合并去重(用户手写 `-freflection` + experimental 不重复)。 +- **e2e**: + - `98` 系增变体:manifest 仅 `standard = "c++fly"`,gcc16 上断言全图 TU、 + scan、std-BMI 命令串含 `-std=c++26` 与 `-freflection`,构建运行绿 + (`# requires: gcc`,host gcc ≥ 16 才跑,遵守既有 host-aware 纪律); + - llvm 工具链 + 同 manifest → 构建照常,断言 `-std=c++2c`、`-fexperimental-library` + (libc++ 时)与 skipped summary 文案(软路径回归,不依赖 gcc16,CI 可跑); + - 既有 `standard = "c++23"/"c++26"/"c++latest"` 工程全量回归(字节不变)。 + +## 7. 工作量分解(单 PR,目标 0.0.91) + +| 步 | 内容 | 触点 | +|---|---|---| +| S1 | `cppfly.cppm` 三表 + `cppfly::resolve` + 单测 | `src/toolchain/`(新) | +| S2 | `normalize_cpp_standard` 增值 + `CppStandardConfig::experimental` + `std_flag_for` 版本化(含 c++latest GNU 路径审计 §11-Q5)+ 单测 | `types.cppm`、`dialect.cppm` | +| S3 | prepare 接线:experimental 分支 → resolve → 方言合并 helper(两消费点同源)→ summary/doctor 输出 | `prepare.cppm`、`plan.cppm`、`flags.cppm`(仅合并点) | +| S4 | e2e ×3(§6) | `tests/e2e/` | +| S5 | CHANGELOG、README manifest 一节 | docs | + +预估含测试 400–700 行;S1/S2 可并行,S3 依赖两者。 + +## 8. 兼容与迁移 + +纯增量:不写 `c++fly` 的 manifest 行为逐字节不变;`c++latest` 语义不动 +(最新**已定档**,确定性构建);`dialect_cxxflags` 与 cxxflags known-list 自动提升 +继续工作(定位:注册表没有的新旗标 / 单点精细控制的逃生舱,README 如此表述); +`98` 号 e2e 原变体保留(逃生舱回归)。无格式版本升级。xpkg 路径 +(`xpkg.cppm:1226` 一带)v1 不加该值(index 包无此需求,见 §11-Q2)。 + +## 9. 验收标准 + +1. `standard = "c++fly"` 一行,gcc@16 上:全图 TU、扫描、std BMI 均带 + `-std=c++26 -freflection`(契约随档位默认启用,无附加旗标,§11-Q3), + 反射示例工程构建运行绿,manifest 零手写旗标。 +2. 同一 manifest 切 llvm@22:构建照常,档位 `-std=c++2c`,libc++ 下带 + `-fexperimental-library`,summary 列出 skipped reflection/contracts 及 hint。 +3. summary 在两种工具链上均打印且内容与实际命令串一致(std-module.json 可核对)。 +4. `c++latest`/`c++26`/`c++23` 工程全量 e2e 无回归,行为字节不变。 +5. gcc@15 上 experimental:档位 `-std=c++26`,reflection 因版本门控进 skipped + (而非编译器报未知选项)。 + +## 10. 非目标(显式出圈) + +- **具名特性列表表面**(`std_features = ["reflection"]`,初版方案 C):机制(注册表) + 本 PR 全部就绪,列表表面等"库硬依赖单一特性、需要硬错诊断"的需求出现再加,零返工。 +- **`gnu++fly` 变体**(§5.1,YAGNI)。 +- **契约评估语义的语义化配置**(GCC 拼写 `-fcontract-evaluation-semantic=` + `ignored|observed|enforce|quick_enforce`,以 gcc16 man page 为准):属"特性的参数" + 而非"开关",走 `dialect_cxxflags` 直传。 +- **clang-p2996 / 未来 llvm-p2996 fork 打包进 mcpp 生态**:机制已就绪(表里加行), + 打包/分发是独立 issue;落地后 Clang 侧 experimental 自动获得反射。 +- **把任意依赖 cxxflags 全图化**(0.0.72 包所有制不变,与 post-089 §1.5 一致)。 + +## 11. 开放问题(review 决策点) + +- **Q1 值命名(已定案:`c++fly`)**:用户 review 拍板——短、有项目气质,且 + "fly" 不与任何真实 `-std=` 拼写冲突。落选备选记录:`c++experimental` + (自文档化但长)、`c++next`、`c++edge`。版本无关性(§4-D 核心论证)与拼写无关。 +- **Q2 index 收录策略(已定案:v1 warn 观察)**:发布到 index 的包声明 `c++fly` + 时收录侧输出警告(非确定性语义提示),不拒绝;观察生态用法后再定是否收紧。 + 落点在 mcpp-index 收录校验,独立小 PR,不进本 PR 范围。 +- **Q3 GCC 16 契约的确切 enable 旗标(已定案,2026-07-14 实机探测)**:本机 + `xim-x-gcc/16.1.0` cc1plus `-fsyntax-only` 探测结论——`-std=c++26` 单独即定义 + `__cpp_contracts` 且 `pre()` 语法可编译(`-std=c++23` 下报语法错,探针有效); + `-fcontracts` 的作用是在更早标准下强开(`-std=c++23 -fcontracts` 亦定义宏)。 + 对照:反射不对称——`-std=c++26` 单独**不**定义 `__cpp_impl_reflection`,必须 + `-freflection`。故表数据:contracts → 空串,reflection → `-freflection`。 + (0.0.90 known-list 里的 `-fcontracts` 保留不动:它仍是合法的图全局方言旗标, + 只是本设计的 experimental 展开不需要发它。) +- **Q4 CI**:硬路径 e2e 需要 host gcc ≥ 16;若 CI 镜像未达,首版仅本地跑 + (`# requires: gcc` 门已有),llvm 软路径 e2e 不受影响。 +- **Q5 c++latest GNU 路径审计**:`std_flag_for`(`dialect.cppm:122`)GNU 分支为 + `stdPrefix + canonical`,而 `c++latest` 的 canonical 即 "c++latest"(GCC 不识别 + `-std=c++latest`)——实现 S2 时核查现状调用链是否已在上游换算,若是历史暗雷则 + 顺手修复(独立小 commit)。 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f55b80..9606263 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 : ; 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 ### 新增 diff --git a/docs/05-mcpp-toml.md b/docs/05-mcpp-toml.md index 3af1af2..892c583 100644 --- a/docs/05-mcpp-toml.md +++ b/docs/05-mcpp-toml.md @@ -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.]` — Build Targets @@ -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/.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 | diff --git a/mcpp.toml b/mcpp.toml index 29b5397..4b2982c 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -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"] diff --git a/src/build/plan.cppm b/src/build/plan.cppm index d7fcb76..7f3febd 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -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; @@ -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; } diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index b4d2c4e..bae3091 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -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; @@ -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; } @@ -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); diff --git a/src/manifest/types.cppm b/src/manifest/types.cppm index 3b5767f..c79a553 100644 --- a/src/manifest/types.cppm +++ b/src/manifest/types.cppm @@ -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 { @@ -532,9 +535,17 @@ std::expected 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)); } diff --git a/src/toolchain/cppfly.cppm b/src/toolchain/cppfly.cppm new file mode 100644 index 0000000..b818561 --- /dev/null +++ b/src/toolchain/cppfly.cppm @@ -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 flags; // union of enabled gate flags, deduped + std::vector 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 effective_dialect_flags(const Toolchain& tc, + bool experimental, std::vector 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 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& 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 effective_dialect_flags(const Toolchain& tc, + bool experimental, std::vector manifestDialectFlags) +{ + if (!experimental) return manifestDialectFlags; + for (auto& f : resolve(tc).flags) add_unique(manifestDialectFlags, f); + return manifestDialectFlags; +} + +} // namespace mcpp::toolchain::cppfly diff --git a/src/toolchain/fingerprint.cppm b/src/toolchain/fingerprint.cppm index 45047bb..98bf167 100644 --- a/src/toolchain/fingerprint.cppm +++ b/src/toolchain/fingerprint.cppm @@ -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; diff --git a/tests/e2e/100_cppfly_reflection.sh b/tests/e2e/100_cppfly_reflection.sh new file mode 100755 index 0000000..8177be7 --- /dev/null +++ b/tests/e2e/100_cppfly_reflection.sh @@ -0,0 +1,149 @@ +#!/usr/bin/env bash +# requires: gcc +# 100_cppfly_reflection.sh — standard = "c++fly" (design 2026-07-14): ONE +# manifest line gives the toolchain's latest -std= level + every experimental +# gate it supports. On gcc >= 16 that means -std=c++26 -freflection with zero +# hand-written flags, reaching every TU, the scan and the std BMI prebuild. +# Tail section: standard = "c++latest" regression — it used to reach the GNU +# driver as the invalid spelling -std=c++latest (design §11-Q5). +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT + +# Capability probe (mirrors 98): newest gcc payload must accept -freflection. +USER_MCPP="${MCPP_HOME:-$HOME/.mcpp}" +GXX=$(find "$USER_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 +GCC_VER=$(basename "$(dirname "$(dirname "$GXX")")") + +export MCPP_HOME="$TMP/mcpp-home" +source "$(dirname "$0")/_inherit_toolchain.sh" + +mkdir -p "$TMP/proj/src" +cd "$TMP/proj" + +cat > mcpp.toml < src/main.cpp <<'EOF' +import std; + +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}); +} +EOF + +"$MCPP" build --no-cache > "$TMP/build.log" 2>&1 || { + cat "$TMP/build.log" + echo "FAIL: c++fly build failed" + exit 1 +} + +# The c++fly summary: the value's contract is saying what was enabled/skipped. +grep -q 'c++fly on gcc' "$TMP/build.log" || { + cat "$TMP/build.log" + echo "FAIL: build log missing c++fly summary line" + exit 1 +} +grep -q 'reflection (-freflection)' "$TMP/build.log" || { + cat "$TMP/build.log" + echo "FAIL: summary missing enabled reflection gate" + exit 1 +} +grep -q 'enabled: .*contracts' "$TMP/build.log" || { + cat "$TMP/build.log" + echo "FAIL: summary missing enabled contracts (on by -std=c++26 on gcc16)" + exit 1 +} + +# Latest level + gate flags reach the global cxxflags (every TU, deps incl.). +build_ninja="$(find target -name build.ninja | head -1)" +grep -qE '^cxxflags = -std=c\+\+26 -freflection' "$build_ninja" || { + grep '^cxxflags' "$build_ninja" || true + echo "FAIL: build.ninja cxxflags lacks -std=c++26 -freflection" + exit 1 +} +if grep -q -- '-std=c++fly' "$build_ninja" compile_commands.json; then + echo "FAIL: raw c++fly canonical leaked into a command line" + exit 1 +fi +grep -q '"-freflection"' compile_commands.json || { + echo "FAIL: compile_commands.json missing -freflection" + exit 1 +} + +# Reflection actually works at runtime (std::meta over struct members). +binary=$(find target -type f -path '*/bin/flydemo' | head -1) +out=$("$binary") +[[ "$out" == *"x 2"* && "$out" == *"y 3"* ]] || { + echo "FAIL: reflection output: $out" + exit 1 +} + +# The std BMI prebuild carries the same dialect (scan/prebuild/compile agree). +metadata="$(find "$MCPP_HOME/bmi" -name std-module.json | head -1)" +grep -q '"std_flag": "-std=c++26 -freflection' "$metadata" || { + cat "$metadata" + echo "FAIL: std-module.json std_flag lacks -std=c++26 -freflection" + exit 1 +} + +# ── c++latest regression (design §11-Q5) ──────────────────────────────── +# Used to emit the invalid GNU spelling -std=c++latest; must now resolve to +# the family latest level. +mkdir -p "$TMP/latest/src" +cd "$TMP/latest" +cat > mcpp.toml < src/main.cpp <<'EOF' +import std; +int main() { std::println("latest ok"); } +EOF + +"$MCPP" build --no-cache > "$TMP/latest-build.log" 2>&1 || { + cat "$TMP/latest-build.log" + echo "FAIL: c++latest build failed" + exit 1 +} +latest_ninja="$(find target -name build.ninja | head -1)" +grep -qE '^cxxflags = -std=c\+\+26' "$latest_ninja" || { + grep '^cxxflags' "$latest_ninja" || true + echo "FAIL: c++latest did not resolve to -std=c++26" + exit 1 +} +if grep -q -- '-std=c++latest' "$latest_ninja" compile_commands.json; then + echo "FAIL: invalid -std=c++latest spelling reached a command line" + exit 1 +fi + +echo "PASS: c++fly = latest std + experimental gates; c++latest spelling fixed" diff --git a/tests/e2e/101_cppfly_llvm_soft.sh b/tests/e2e/101_cppfly_llvm_soft.sh new file mode 100755 index 0000000..a304f87 --- /dev/null +++ b/tests/e2e/101_cppfly_llvm_soft.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# 101_cppfly_llvm_soft.sh — standard = "c++fly" soft path on Clang (design +# §5.4): upstream Clang has no reflection/contracts, so the build must still +# succeed at the family's latest level (-std=c++2c), with the skipped gates +# reported in the summary — mcpp diagnostics instead of raw compiler errors. +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT + +source "$(dirname "$0")/_llvm_env.sh" +if [[ ! -d "$LLVM_ROOT" ]]; then + echo "SKIP: xlings llvm@${LLVM_VERSION:-none} is not installed" + exit 0 +fi + +export MCPP_HOME="$TMP/mcpp-home" +source "$(dirname "$0")/_inherit_toolchain.sh" + +mkdir -p "$TMP/proj/src" +cd "$TMP/proj" + +cat > mcpp.toml < src/main.cpp <<'EOF' +import std; +int main() { std::println("fly soft ok"); } +EOF + +"$MCPP" build --no-cache > "$TMP/build.log" 2>&1 || { + cat "$TMP/build.log" + echo "FAIL: c++fly soft-path build failed on llvm" + exit 1 +} + +grep -q 'c++fly on clang' "$TMP/build.log" || { + cat "$TMP/build.log" + echo "FAIL: build log missing c++fly summary line" + exit 1 +} +grep -q 'skipped: .*reflection' "$TMP/build.log" || { + cat "$TMP/build.log" + echo "FAIL: summary does not report reflection as skipped on clang" + exit 1 +} + +build_ninja="$(find target -name build.ninja | head -1)" +grep -qE '^cxxflags = -std=c\+\+2c' "$build_ninja" || { + grep '^cxxflags' "$build_ninja" || true + echo "FAIL: clang c++fly did not resolve to -std=c++2c" + exit 1 +} + +# stdlib gate: libc++ (linux/macos llvm payloads) → enabled + flag present; +# clang on MSVC STL (windows) → reported skipped + flag absent. Assert the +# summary and the emitted flags AGREE rather than hardcoding one stdlib. +if grep -q 'experimental-library (-fexperimental-library)' "$TMP/build.log"; then + grep -q -- '-fexperimental-library' "$build_ninja" || { + echo "FAIL: summary claims experimental-library but build.ninja lacks the flag" + exit 1 + } +else + grep -q 'skipped: .*experimental-library' "$TMP/build.log" || { + cat "$TMP/build.log" + echo "FAIL: experimental-library neither enabled nor reported skipped" + exit 1 + } + if grep -q -- '-fexperimental-library' "$build_ninja"; then + echo "FAIL: -fexperimental-library emitted for a non-libc++ stdlib" + exit 1 + fi +fi + +binary=$(find target -type f \( -path '*/bin/flysoft' -o -path '*/bin/flysoft.exe' \) | head -1) +out=$("$binary") +[[ "$out" == *"fly soft ok"* ]] || { + echo "FAIL: runtime output: $out" + exit 1 +} + +echo "PASS: c++fly soft path on clang — latest level + skipped summary" diff --git a/tests/unit/test_cppfly.cpp b/tests/unit/test_cppfly.cpp new file mode 100644 index 0000000..806da08 --- /dev/null +++ b/tests/unit/test_cppfly.cpp @@ -0,0 +1,115 @@ +// tests/unit/test_cppfly.cpp — mcpp.toolchain.cppfly: the `standard = "c++fly"` +// capability tables (latest std level + experimental gates per compiler) and +// the c++latest/c++fly-aware std-flag spelling. +// Table facts pinned by on-machine probes (gcc 16.1.0, 2026-07-14) — see +// .agents/docs/2026-07-14-std-features-experimental-gate-design.md §2/§11-Q3. +#include + +import std; +import mcpp.toolchain.cppfly; +import mcpp.toolchain.model; + +using namespace mcpp::toolchain; + +namespace { +Toolchain tc_of(CompilerId id, std::string ver, std::string stdlib = "libstdc++") { + Toolchain tc; + tc.compiler = id; + tc.version = std::move(ver); + tc.stdlibId = std::move(stdlib); + return tc; +} +bool has_flag(const std::vector& v, std::string_view f) { + return std::find(v.begin(), v.end(), f) != v.end(); +} +const cppfly::FeatureState* feature(const cppfly::Resolution& r, std::string_view n) { + for (auto& f : r.features) + if (f.name == n) return &f; + return nullptr; +} +} // namespace + +TEST(CppFly, CompilerMajor) { + EXPECT_EQ(cppfly::compiler_major(tc_of(CompilerId::GCC, "16.1.0")), 16); + EXPECT_EQ(cppfly::compiler_major(tc_of(CompilerId::Clang, "22.1.8")), 22); + EXPECT_EQ(cppfly::compiler_major(tc_of(CompilerId::GCC, "")), 0); +} + +TEST(CppFly, LatestStdCanonical) { + int lvl = 0; + EXPECT_EQ(cppfly::latest_std_canonical(tc_of(CompilerId::GCC, "16.1.0"), &lvl), "c++26"); + EXPECT_EQ(lvl, 26); + EXPECT_EQ(cppfly::latest_std_canonical(tc_of(CompilerId::GCC, "13.2.0")), "c++23"); + EXPECT_EQ(cppfly::latest_std_canonical(tc_of(CompilerId::Clang, "22.1.8")), "c++2c"); + EXPECT_EQ(cppfly::latest_std_canonical(tc_of(CompilerId::MSVC, "19.44")), "c++26"); +} + +TEST(CppFly, ResolveGcc16) { + auto r = cppfly::resolve(tc_of(CompilerId::GCC, "16.1.0")); + EXPECT_EQ(r.stdCanonical, "c++26"); + EXPECT_TRUE(has_flag(r.flags, "-freflection")); + auto* refl = feature(r, "reflection"); + ASSERT_NE(refl, nullptr); + EXPECT_TRUE(refl->enabled); + EXPECT_EQ(refl->paper, "P2996"); + // Contracts: enabled by -std=c++26 alone on GCC 16 (probe-pinned) — no + // extra flag, but reported enabled for the summary. + auto* con = feature(r, "contracts"); + ASSERT_NE(con, nullptr); + EXPECT_TRUE(con->enabled); + EXPECT_TRUE(con->flags.empty()); + // libstdc++ ships experimental bits ungated. + EXPECT_FALSE(has_flag(r.flags, "-fexperimental-library")); +} + +TEST(CppFly, ResolveGcc15SkipsByVersion) { + auto r = cppfly::resolve(tc_of(CompilerId::GCC, "15.1.0")); + EXPECT_TRUE(r.flags.empty()); + auto* refl = feature(r, "reflection"); + ASSERT_NE(refl, nullptr); + EXPECT_FALSE(refl->enabled); + EXPECT_NE(refl->reason.find("16"), std::string::npos); // names the version floor +} + +TEST(CppFly, ResolveClangLibcxx) { + auto r = cppfly::resolve(tc_of(CompilerId::Clang, "22.1.8", "libc++")); + EXPECT_EQ(r.stdCanonical, "c++2c"); + EXPECT_TRUE(has_flag(r.flags, "-fexperimental-library")); + auto* refl = feature(r, "reflection"); + ASSERT_NE(refl, nullptr); + EXPECT_FALSE(refl->enabled); + auto* lib = feature(r, "experimental-library"); + ASSERT_NE(lib, nullptr); + EXPECT_TRUE(lib->enabled); +} + +TEST(CppFly, ResolveMsvcAllSkipped) { + auto r = cppfly::resolve(tc_of(CompilerId::MSVC, "19.44", "msvc-stl")); + EXPECT_TRUE(r.flags.empty()); + for (auto& f : r.features) EXPECT_FALSE(f.enabled); +} + +TEST(CppFly, StdFlagResolvesLatestAndFly) { + auto gcc16 = tc_of(CompilerId::GCC, "16.1.0"); + EXPECT_EQ(cppfly::std_flag(gcc16, "c++fly", 1000), "-std=c++26"); + // standard = "c++latest" used to reach the GNU driver as the invalid + // spelling -std=c++latest (design §11-Q5) — now resolves to the family + // latest. + EXPECT_EQ(cppfly::std_flag(gcc16, "c++latest", 999), "-std=c++26"); + EXPECT_EQ(cppfly::std_flag(gcc16, "c++23", 23), "-std=c++23"); + EXPECT_EQ(cppfly::std_flag(gcc16, "gnu++23", 23), "-std=gnu++23"); + EXPECT_EQ(cppfly::std_flag(tc_of(CompilerId::Clang, "22.1.8"), "c++fly", 1000), "-std=c++2c"); + EXPECT_EQ(cppfly::std_flag(tc_of(CompilerId::MSVC, "19.44"), "c++fly", 1000), "/std:c++latest"); + EXPECT_EQ(cppfly::std_flag(tc_of(CompilerId::MSVC, "19.44"), "c++latest", 999), "/std:c++latest"); +} + +TEST(CppFly, EffectiveDialectFlagsDedup) { + auto gcc16 = tc_of(CompilerId::GCC, "16.1.0"); + // User hand-wrote -freflection too — the fly gate must not duplicate it. + auto out = cppfly::effective_dialect_flags(gcc16, true, {"-freflection", "-fchar8_t"}); + EXPECT_EQ(std::count(out.begin(), out.end(), std::string("-freflection")), 1); + EXPECT_TRUE(has_flag(out, "-fchar8_t")); + // Not experimental → manifest flags pass through untouched. + auto off = cppfly::effective_dialect_flags(gcc16, false, {"-fchar8_t"}); + EXPECT_EQ(off, (std::vector{"-fchar8_t"})); +} diff --git a/tests/unit/test_manifest.cpp b/tests/unit/test_manifest.cpp index 2ab9851..16b3e50 100644 --- a/tests/unit/test_manifest.cpp +++ b/tests/unit/test_manifest.cpp @@ -4,6 +4,21 @@ import std; import mcpp.manifest; import mcpp.platform; +TEST(Manifest, CppFlyStandard) { + // standard = "c++fly": latest level + all experimental gates (design + // 2026-07-14-std-features-experimental-gate-design.md §5.1). + auto cfg = mcpp::manifest::normalize_cpp_standard("c++fly"); + ASSERT_TRUE(cfg.has_value()); + EXPECT_EQ(cfg->canonical, "c++fly"); + EXPECT_EQ(cfg->level, 1000); // above c++latest's 999 + EXPECT_TRUE(cfg->experimental); + EXPECT_FALSE(mcpp::manifest::normalize_cpp_standard("c++latest")->experimental); + EXPECT_FALSE(mcpp::manifest::normalize_cpp_standard("c++26")->experimental); + auto bad = mcpp::manifest::normalize_cpp_standard("c++flyy"); + ASSERT_FALSE(bad.has_value()); + EXPECT_NE(bad.error().find("c++fly"), std::string::npos); // listed in the allow-list message +} + TEST(Manifest, MinimalValid) { constexpr auto src = R"( [package]