@@ -187,6 +187,8 @@ pub struct AliasPair {
187187pub fn build_feature_sets ( cli : & Cli ) -> Result < Vec < FeatureSet > > {
188188 let requests = cli. api_requests ( ) ?;
189189 let ext_filter = cli. extension_filter ( ) ?;
190+ let promoted = cli. promoted ;
191+ let predecessors = cli. predecessors ;
190192
191193 let alias = match & cli. generator {
192194 crate :: cli:: Generator :: C ( c) => c. alias ,
@@ -209,16 +211,24 @@ pub fn build_feature_sets(cli: &Cli) -> Result<Vec<FeatureSet>> {
209211 for ( spec_name, reqs) in & by_spec {
210212 let sources = fetch:: load_spec ( spec_name, cli. fetch ) ?;
211213 let raw = parse:: parse ( & sources, spec_name) ?;
212- let fs = resolve_feature_set ( & raw , reqs, & ext_filter, true , alias) ?;
214+ let fs =
215+ resolve_feature_set ( & raw , reqs, & ext_filter, true , alias, promoted, predecessors) ?;
213216 feature_sets. push ( fs) ;
214217 }
215218 } else {
216219 for req in & requests {
217220 let spec_name = req. spec_name ( ) ;
218221 let sources = fetch:: load_spec ( spec_name, cli. fetch ) ?;
219222 let raw = parse:: parse ( & sources, spec_name) ?;
220- let fs =
221- resolve_feature_set ( & raw , std:: slice:: from_ref ( req) , & ext_filter, false , alias) ?;
223+ let fs = resolve_feature_set (
224+ & raw ,
225+ std:: slice:: from_ref ( req) ,
226+ & ext_filter,
227+ false ,
228+ alias,
229+ promoted,
230+ predecessors,
231+ ) ?;
222232 feature_sets. push ( fs) ;
223233 }
224234 }
@@ -236,6 +246,8 @@ fn resolve_feature_set(
236246 ext_filter : & Option < Vec < String > > ,
237247 is_merged : bool ,
238248 want_aliases : bool ,
249+ want_promoted : bool ,
250+ want_predecessors : bool ,
239251) -> Result < FeatureSet > {
240252 let spec_name = & raw . spec_name;
241253 let is_vulkan = spec_name == "vk" ;
@@ -265,10 +277,14 @@ fn resolve_feature_set(
265277 let mut req_commands: IndexMap < String , ( ) > = IndexMap :: new ( ) ; // preserves order
266278 let mut removed_commands: HashSet < String > = HashSet :: new ( ) ;
267279 let mut removed_enums: HashSet < String > = HashSet :: new ( ) ;
280+ // Per-API core command sets used by --promoted to scope promotion checks.
281+ // Keyed by API name (e.g. "gl", "gles2"); values exclude profile-removed commands.
282+ let mut per_api_core_cmds: HashMap < String , HashSet < String > > = HashMap :: new ( ) ;
268283
269284 for feat in & selected_features {
270285 let req_for_api = requests. iter ( ) . find ( |r| r. name == feat. api ) ;
271286 let profile = req_for_api. and_then ( |r| r. profile . as_deref ( ) ) ;
287+ let api_cmds = per_api_core_cmds. entry ( feat. api . clone ( ) ) . or_default ( ) ;
272288
273289 for require in & feat. raw . requires {
274290 if !api_profile_matches (
@@ -283,6 +299,7 @@ fn resolve_feature_set(
283299 req_enums. extend ( require. enums . iter ( ) . cloned ( ) ) ;
284300 for cmd in & require. commands {
285301 req_commands. entry ( cmd. clone ( ) ) . or_insert ( ( ) ) ;
302+ api_cmds. insert ( cmd. clone ( ) ) ;
286303 }
287304 }
288305 for remove in & feat. raw . removes {
@@ -291,6 +308,11 @@ fn resolve_feature_set(
291308 }
292309 removed_commands. extend ( remove. commands . iter ( ) . cloned ( ) ) ;
293310 removed_enums. extend ( remove. enums . iter ( ) . cloned ( ) ) ;
311+ // Apply removes inline — features are processed in version order so
312+ // each version's removes are applied immediately after its requires.
313+ for cmd in & remove. commands {
314+ api_cmds. remove ( cmd. as_str ( ) ) ;
315+ }
294316 }
295317 }
296318 for cmd in & removed_commands {
@@ -300,7 +322,15 @@ fn resolve_feature_set(
300322 // ------------------------------------------------------------------
301323 // Step 3: Determine which extensions are selected.
302324 // ------------------------------------------------------------------
303- let selected_exts = select_extensions ( raw, requests, ext_filter, spec_name) ;
325+ let selected_exts = select_extensions (
326+ raw,
327+ requests,
328+ ext_filter,
329+ spec_name,
330+ & per_api_core_cmds,
331+ want_promoted,
332+ want_predecessors,
333+ ) ;
304334
305335 // ------------------------------------------------------------------
306336 // Step 4: Collect additional required names from extensions.
@@ -578,6 +608,9 @@ fn select_extensions<'a>(
578608 requests : & [ ApiRequest ] ,
579609 filter : & Option < Vec < String > > ,
580610 spec_name : & str ,
611+ per_api_core_cmds : & HashMap < String , HashSet < String > > ,
612+ want_promoted : bool ,
613+ want_predecessors : bool ,
581614) -> Vec < SelectedExt < ' a > > {
582615 let api_set: HashSet < & str > = requests. iter ( ) . map ( |r| r. name . as_str ( ) ) . collect ( ) ;
583616 // WGL mandatory extensions (spec gotcha #9).
@@ -590,7 +623,8 @@ fn select_extensions<'a>(
590623 HashSet :: new ( )
591624 } ;
592625
593- raw. extensions
626+ let mut selected: Vec < SelectedExt < ' a > > = raw
627+ . extensions
594628 . iter ( )
595629 . filter ( |e| {
596630 let supported = e. supported . iter ( ) . any ( |s| api_set. contains ( s. as_str ( ) ) ) ;
@@ -606,7 +640,131 @@ fn select_extensions<'a>(
606640 }
607641 } )
608642 . map ( |e| SelectedExt { raw : e } )
609- . collect ( )
643+ . collect ( ) ;
644+
645+ // Build the bidirectional alias map once — it's used by both the
646+ // --promoted and --predecessors passes.
647+ let cmd_to_alias: HashMap < & str , & str > = if want_promoted || want_predecessors {
648+ let mut m = HashMap :: new ( ) ;
649+ for ( name, cmd) in & raw . commands {
650+ if let Some ( ref alias) = cmd. alias {
651+ m. insert ( name. as_str ( ) , alias. as_str ( ) ) ;
652+ m. insert ( alias. as_str ( ) , name. as_str ( ) ) ;
653+ }
654+ }
655+ m
656+ } else {
657+ HashMap :: new ( )
658+ } ;
659+
660+ if want_promoted {
661+ // Snapshot names already selected so we don't duplicate them.
662+ let already: HashSet < & str > = selected. iter ( ) . map ( |e| e. raw . name . as_str ( ) ) . collect ( ) ;
663+
664+ for ext in & raw . extensions {
665+ if already. contains ( ext. name . as_str ( ) ) {
666+ continue ;
667+ }
668+
669+ // An extension is considered promoted if, for at least one API A that:
670+ // (a) the extension claims to support, and
671+ // (b) we are generating,
672+ // any of the extension's commands for that API appear in A's core
673+ // command set — either directly (same-name promotion) or via the
674+ // alias graph (renamed promotion, e.g. glActiveTextureARB → glActiveTexture).
675+ //
676+ // Checking per-API (rather than against the unified req_commands) prevents
677+ // cross-contamination in merged builds: a GLES2-only extension whose
678+ // commands happen to match GLES2 core will not be auto-included for gl:core.
679+ let is_promoted = ext
680+ . supported
681+ . iter ( )
682+ . filter ( |s| api_set. contains ( s. as_str ( ) ) )
683+ . any ( |api| {
684+ let Some ( core_cmds) = per_api_core_cmds. get ( api. as_str ( ) ) else {
685+ return false ;
686+ } ;
687+ ext. requires
688+ . iter ( )
689+ // Only consider require blocks that apply to this API.
690+ . filter ( |req| api_profile_matches ( req. api . as_deref ( ) , None , api, None ) )
691+ . any ( |req| {
692+ req. commands . iter ( ) . any ( |c| {
693+ // Same-name promotion: the command landed in core
694+ // with the same name (e.g. ARB_copy_buffer →
695+ // glCopyBufferSubData is unchanged).
696+ core_cmds. contains ( c. as_str ( ) )
697+ // Renamed promotion: the command has an alias
698+ // that is in core (e.g. glActiveTextureARB →
699+ // glActiveTexture).
700+ || cmd_to_alias
701+ . get ( c. as_str ( ) )
702+ . is_some_and ( |a| core_cmds. contains ( * a) )
703+ } )
704+ } )
705+ } ) ;
706+
707+ if is_promoted {
708+ selected. push ( SelectedExt { raw : ext } ) ;
709+ }
710+ }
711+ }
712+
713+ if want_predecessors {
714+ // Build the set of all commands contributed by the currently selected
715+ // extensions (after --promoted may have expanded the set).
716+ // An unselected extension is a "predecessor" of the selected set if any
717+ // of its commands are aliases of commands in this set — i.e. the
718+ // extension was superseded by one already selected.
719+ //
720+ // We iterate to a fixed point because adding a predecessor may itself
721+ // have predecessors not yet in the set.
722+ loop {
723+ // Collect commands from all currently selected extensions.
724+ let selected_ext_cmds: HashSet < & str > = selected
725+ . iter ( )
726+ . flat_map ( |e| {
727+ e. raw
728+ . requires
729+ . iter ( )
730+ . flat_map ( |req| req. commands . iter ( ) . map ( String :: as_str) )
731+ } )
732+ . collect ( ) ;
733+
734+ let already: HashSet < & str > = selected. iter ( ) . map ( |e| e. raw . name . as_str ( ) ) . collect ( ) ;
735+
736+ let mut added_any = false ;
737+ for ext in & raw . extensions {
738+ if already. contains ( ext. name . as_str ( ) ) {
739+ continue ;
740+ }
741+ let supported = ext. supported . iter ( ) . any ( |s| api_set. contains ( s. as_str ( ) ) ) ;
742+ if !supported {
743+ continue ;
744+ }
745+ let is_predecessor = ext. requires . iter ( ) . any ( |req| {
746+ req. commands . iter ( ) . any ( |c| {
747+ // This extension's command is in the selected set directly,
748+ // or its alias is — meaning a newer extension absorbed it.
749+ selected_ext_cmds. contains ( c. as_str ( ) )
750+ || cmd_to_alias
751+ . get ( c. as_str ( ) )
752+ . is_some_and ( |a| selected_ext_cmds. contains ( * a) )
753+ } )
754+ } ) ;
755+ if is_predecessor {
756+ selected. push ( SelectedExt { raw : ext } ) ;
757+ added_any = true ;
758+ }
759+ }
760+
761+ if !added_any {
762+ break ;
763+ }
764+ }
765+ }
766+
767+ selected
610768}
611769
612770// ---------------------------------------------------------------------------
0 commit comments