@@ -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 ()) {
0 commit comments