Skip to content

Commit 65a2c2a

Browse files
committed
Merge branch 'feat/622-t4-abi' into feat/622-ui-framework-on-three-rows
2 parents bbb3b98 + 7b86f9f commit 65a2c2a

7 files changed

Lines changed: 824 additions & 39 deletions

File tree

‎modules/manifest/src/toml.cppm‎

Lines changed: 94 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -762,19 +762,22 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
762762
read_str_array(ft, "requires", reqs);
763763
read_str_array(ft, "provides", provs);
764764
if (!reqs.empty()) m.featureRequires[fname] = std::move(reqs);
765-
// `requires_abi = { threads = true }`: this feature needs the
766-
// artefact's ABI switch on. See Manifest::featureRequiresAbiThreads.
765+
// `requires_abi = { threads = true, exceptions = true }`:
766+
// this feature needs the artefact's ABI switch(es) on. See
767+
// Manifest::featureRequiresAbiThreads /
768+
// Manifest::featureRequiresAbiExceptions.
767769
if (auto rait = ft.find("requires_abi"); rait != ft.end()) {
768770
if (!rait->second.is_table())
769771
return std::unexpected(error(origin, std::format(
770772
"features.{}.requires_abi must be a table such as "
771773
"`{{ threads = true }}`", fname)));
772774
for (auto& [ak, av] : rait->second.as_table()) {
773-
if (ak != "threads" || !av.is_bool())
775+
if ((ak != "threads" && ak != "exceptions") || !av.is_bool())
774776
return std::unexpected(error(origin, std::format(
775777
"features.{}.requires_abi.{}: the members are "
776-
"`threads`, a boolean", fname, ak)));
777-
m.featureRequiresAbiThreads[fname] = av.as_bool();
778+
"`threads`, `exceptions`, booleans", fname, ak)));
779+
if (ak == "threads") m.featureRequiresAbiThreads[fname] = av.as_bool();
780+
else m.featureRequiresAbiExceptions[fname] = av.as_bool();
778781
}
779782
}
780783
if (!provs.empty()) m.featureProvides[fname] = std::move(provs);
@@ -945,16 +948,19 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
945948
m.unknownCapabilities.push_back(entry);
946949
m.requires_ = *v;
947950
}
948-
// [package] requires_abi -- see Manifest::requiresAbiThreads.
951+
// [package] requires_abi -- see Manifest::requiresAbiThreads /
952+
// Manifest::requiresAbiExceptions.
949953
if (auto* ra = doc->get("package.requires_abi")) {
950954
if (!ra->is_table())
951955
return std::unexpected(error(origin,
952956
"[package] requires_abi must be a table such as `{ threads = true }`"));
953957
for (auto& [ak, av] : ra->as_table()) {
954-
if (ak != "threads" || !av.is_bool())
958+
if ((ak != "threads" && ak != "exceptions") || !av.is_bool())
955959
return std::unexpected(error(origin, std::format(
956-
"[package] requires_abi.{}: the members are `threads`, a boolean", ak)));
957-
m.requiresAbiThreads = av.as_bool();
960+
"[package] requires_abi.{}: the members are `threads`, "
961+
"`exceptions`, booleans", ak)));
962+
if (ak == "threads") m.requiresAbiThreads = av.as_bool();
963+
else m.requiresAbiExceptions = av.as_bool();
958964
}
959965
}
960966
// [package] exclusive — capabilities this package claims sole provision of.
@@ -2764,24 +2770,95 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
27642770
// keys only, and the same two `[runtime]` already has at the top
27652771
// level: this makes them per-target, it does not invent a vocabulary.
27662772
// `[target.<pred>.abi]` -- graph-wide ABI switches as typed members
2767-
// rather than flags (design 2026-09-12, section 5.2). One member
2768-
// today, and an unknown member is refused, so the table cannot
2769-
// become a second flag list.
2773+
// rather than flags (design 2026-09-12, section 5.2; `exceptions`
2774+
// added by section 2.1, A1). Two members, and an unknown one is
2775+
// refused naming both, so the table cannot become a second flag
2776+
// list.
27702777
if (auto ait = body.find("abi"); ait != body.end()) {
27712778
if (!ait->second.is_table())
27722779
return std::unexpected(error(origin, std::format(
27732780
"[target.{}].abi must be a table, e.g. `[target.{}.abi]` "
27742781
"with `threads = true`", triple, triple)));
27752782
for (auto& [ak, av] : ait->second.as_table()) {
2776-
if (ak != "threads")
2783+
if (ak != "threads" && ak != "exceptions")
27772784
return std::unexpected(error(origin, std::format(
27782785
"[target.{}.abi] has no member '{}'; the members are: "
2779-
"threads", triple, ak)));
2786+
"threads, exceptions", triple, ak)));
27802787
if (!av.is_bool())
27812788
return std::unexpected(error(origin, std::format(
2782-
"[target.{}.abi].threads must be true or false", triple)));
2783-
cc.abiThreads = av.as_bool();
2784-
cc.abiThreadsDeclared = true;
2789+
"[target.{}.abi].{} must be true or false", triple, ak)));
2790+
if (ak == "threads") {
2791+
cc.abiThreads = av.as_bool();
2792+
cc.abiThreadsDeclared = true;
2793+
} else {
2794+
cc.abiExceptions = av.as_bool();
2795+
cc.abiExceptionsDeclared = true;
2796+
}
2797+
}
2798+
}
2799+
// `[target.<pred>] requires_abi = { ... }` -- design 2026-09-12
2800+
// (the UI framework record), section 2.6, A6: the requirement can
2801+
// sit on the TARGET axis, because the sources it gates
2802+
// (`[target.<pred>.build] sources`) are selected by this same
2803+
// predicate. A DIRECT key of the selector table (an inline-table
2804+
// VALUE), not a sub-section -- which is why the unsupported-key
2805+
// sweep above does not have to name it: that sweep skips every
2806+
// table-valued key, on the reasoning that a table is the
2807+
// conditional channel, and an inline table is the same TOML value
2808+
// shape as a dotted sub-section. Stored on `cc` and unioned into
2809+
// the package's requirement set by `merge_conditional_config`
2810+
// when the predicate matches the resolved target, keeping the
2811+
// selector text so a refusal can name what asked.
2812+
//
2813+
// MEASURED: an mcpp before this change (2026.9.12.2) neither
2814+
// warns nor errors on this key -- it is a table, so the same
2815+
// "skip tables" sweep already silently drops it there too. The
2816+
// design record's claim that an older engine warns
2817+
// "[target.cfg(linux)] has unsupported key 'requires_abi'
2818+
// (ignored)" does not hold for 2026.9.12.2; verified by running
2819+
// it against `[target.'cfg(linux)'] requires_abi = { threads =
2820+
// true }`, which built with no diagnostic at all.
2821+
if (auto rait2 = body.find("requires_abi"); rait2 != body.end()) {
2822+
if (!rait2->second.is_table())
2823+
return std::unexpected(error(origin, std::format(
2824+
"[target.{}] requires_abi must be a table such as "
2825+
"`{{ threads = true }}`", triple)));
2826+
for (auto& [ak, av] : rait2->second.as_table()) {
2827+
if ((ak != "threads" && ak != "exceptions") || !av.is_bool())
2828+
return std::unexpected(error(origin, std::format(
2829+
"[target.{}].requires_abi.{}: the members are "
2830+
"`threads`, `exceptions`, booleans", triple, ak)));
2831+
if (ak == "threads") cc.requiresAbiThreads = av.as_bool();
2832+
else cc.requiresAbiExceptions = av.as_bool();
2833+
}
2834+
}
2835+
// `[target.<pred>.feature-requires-abi] <feature> = { ... }` --
2836+
// the per-feature form of the same requirement, on the target
2837+
// axis. Named after `feature-deps` and `feature-xlings` below,
2838+
// the two existing per-target-per-feature tables. The feature is
2839+
// registered UNCONDITIONALLY, exactly as `feature-deps` does a
2840+
// few dozen lines down: whether the predicate matches decides
2841+
// whether the requirement counts, not whether the feature exists.
2842+
if (auto frait = body.find("feature-requires-abi");
2843+
frait != body.end() && frait->second.is_table()) {
2844+
for (auto& [fname, fval] : frait->second.as_table()) {
2845+
if (!fval.is_table())
2846+
return std::unexpected(error(origin, std::format(
2847+
"[target.{}.feature-requires-abi.{}] must be a "
2848+
"table such as `{{ threads = true }}`", triple, fname)));
2849+
for (auto& [ak, av] : fval.as_table()) {
2850+
if ((ak != "threads" && ak != "exceptions") || !av.is_bool())
2851+
return std::unexpected(error(origin, std::format(
2852+
"[target.{}.feature-requires-abi.{}].{}: the "
2853+
"members are `threads`, `exceptions`, booleans",
2854+
triple, fname, ak)));
2855+
if (ak == "threads")
2856+
cc.featureRequiresAbiThreads[std::string(fname)] = av.as_bool();
2857+
else
2858+
cc.featureRequiresAbiExceptions[std::string(fname)] = av.as_bool();
2859+
}
2860+
m.featuresMap.try_emplace(std::string(fname),
2861+
std::vector<std::string>{});
27852862
}
27862863
}
27872864
if (auto rit = body.find("runtime"); rit != body.end() && rit->second.is_table()) {

‎modules/manifest/src/types.cppm‎

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,20 @@ struct BuildConfig : BuildInputs {
821821
// value because `threads = false` is also a statement.
822822
bool abiThreads = false;
823823
bool abiThreadsDeclared = false;
824+
// `[target.<selector>.abi] exceptions` -- design 2026-09-12 (the UI
825+
// framework record) section 2.1, A1: the second member of the table.
826+
// Whether the artefact is built with C++ exceptions on. A GRAPH-WIDE ABI
827+
// SWITCH for the same reason `threads` is one: clang records the
828+
// exception model in a BMI and refuses an importer that disagrees, so
829+
// the switch must reach the standard library prebuild, the scan, every
830+
// translation unit, and the link. Rendered only where the target's
831+
// default is OFF -- Emscripten; every hosted target already links with
832+
// exceptions on, so the member is satisfied by default there, the same
833+
// property `threads` has on PE. Declared is kept apart from the value
834+
// for the same reason `threads` keeps it: `exceptions = false` is also
835+
// a statement.
836+
bool abiExceptions = false;
837+
bool abiExceptionsDeclared = false;
824838
std::string cStandard;
825839
// Escape hatch for the hermetic link check: a sandbox toolchain whose
826840
// CRT/loader resolve OUTSIDE the sandbox is a hard error by default
@@ -1298,6 +1312,24 @@ struct ConditionalConfig {
12981312
// BuildConfig::abiThreads for what the value does and where it applies.
12991313
bool abiThreads = false;
13001314
bool abiThreadsDeclared = false;
1315+
// `[target.<sel>.abi] exceptions` -- see BuildConfig::abiExceptions.
1316+
bool abiExceptions = false;
1317+
bool abiExceptionsDeclared = false;
1318+
// `[target.<sel>] requires_abi = { ... }` -- design 2026-09-12 (the UI
1319+
// framework record) section 2.6, A6: a requirement can sit on the target
1320+
// axis, because the sources it gates (`[target.<sel>.build] sources`) are
1321+
// selected by this SAME predicate. No "declared" pair: a requirement is a
1322+
// union (this selector asks, or it does not), so `false` carries nothing
1323+
// a plain absence would not -- unlike `abi`'s value, which the root
1324+
// renders and where `false` overrides an outer `true`.
1325+
bool requiresAbiThreads = false;
1326+
bool requiresAbiExceptions = false;
1327+
// `[target.<sel>.feature-requires-abi] <feature> = { ... }` -- the
1328+
// per-feature form of the same requirement, on the target axis. Named
1329+
// after `feature-deps`/`feature-xlings` below, the two existing
1330+
// per-target-per-feature tables (SPEC-004 section 4; #359).
1331+
std::map<std::string, bool> featureRequiresAbiThreads;
1332+
std::map<std::string, bool> featureRequiresAbiExceptions;
13011333
// Conditional dependencies (Phase 1b): merged into the corresponding
13021334
// manifest maps in prepare_build when the predicate matches the resolved
13031335
// target — before dependency resolution, so they resolve like any dep.
@@ -1365,7 +1397,9 @@ inline bool is_empty(const ConditionalConfig& c) {
13651397
&& c.frameworks.empty()
13661398
&& c.dependencies.empty() && c.devDependencies.empty()
13671399
&& c.buildDependencies.empty() && c.featureDeps.empty()
1368-
&& c.xlings.empty() && !c.abiThreadsDeclared;
1400+
&& c.xlings.empty() && !c.abiThreadsDeclared && !c.abiExceptionsDeclared
1401+
&& !c.requiresAbiThreads && !c.requiresAbiExceptions
1402+
&& c.featureRequiresAbiThreads.empty() && c.featureRequiresAbiExceptions.empty();
13691403
}
13701404

13711405
// `[lib]` — library "root" interface convention.
@@ -1697,12 +1731,28 @@ struct Manifest {
16971731
// through untouched, exactly as they do in `provides`.
16981732
// The spelling is `requires_` because `requires` is a keyword.
16991733
std::vector<std::string> requires_;
1700-
// `requires_abi = { threads = true }` at package level, and per feature.
1701-
// A statement that the ARTEFACT's ABI has a switch on, compared at
1702-
// resolution with the root's `[target.<selector>.abi]`. Only `threads`
1703-
// exists; a feature's entry counts only when that feature is active.
1734+
// `requires_abi = { threads = true, exceptions = true }` at package
1735+
// level, and per feature. A statement that the ARTEFACT's ABI has a
1736+
// switch on, compared at resolution with the root's
1737+
// `[target.<selector>.abi]`. A feature's entry counts only when that
1738+
// feature is active.
17041739
bool requiresAbiThreads = false;
1740+
bool requiresAbiExceptions = false;
17051741
std::map<std::string, bool> featureRequiresAbiThreads;
1742+
std::map<std::string, bool> featureRequiresAbiExceptions;
1743+
// The TARGET-AXIS form of the same requirement (design 2026-09-12, the UI
1744+
// framework record, section 2.6, A6): `[target.<sel>] requires_abi =
1745+
// {...}` and `[target.<sel>.feature-requires-abi] <f> = {...}`, unioned
1746+
// in by `merge_conditional_config` when the selector matches the resolved
1747+
// target. Kept apart from the two members above -- rather than folded
1748+
// into them -- so the check that reports an unmet requirement can name
1749+
// the SELECTOR that asked, as written, and not just "the package": each
1750+
// vector holds the predicate text of every matching selector that
1751+
// declared the member, in the order merge_conditional_config visited them.
1752+
std::vector<std::string> targetRequiresAbiThreads;
1753+
std::vector<std::string> targetRequiresAbiExceptions;
1754+
std::map<std::string, std::vector<std::string>> targetFeatureRequiresAbiThreads;
1755+
std::map<std::string, std::vector<std::string>> targetFeatureRequiresAbiExceptions;
17061756
// [package] exclusive — the capabilities this package claims it is the ONLY
17071757
// provider of.
17081758
//

0 commit comments

Comments
 (0)