Skip to content

Tracking issue for cfg_select (formerly cfg_match) #115585

Closed

Description

@c410-f3r

View all comments

Provides a native way to easily manage multiple conditional flags without having to rewrite each clause multiple times.

Public API

cfg_select! {
    unix => {
        fn foo() { /* unix specific functionality */ }
    }
    target_pointer_width = "32" => {
        fn foo() { /* non-unix, 32-bit functionality */ }
    }
    _ => {
        fn foo() { /* fallback implementation */ }
    }
}

let is_unix_str = cfg_select! {
    unix => "unix",
    _ => "not unix",
};
println!("{is_unix_str}");

Steps / History

Unresolved Questions

  • What should the final syntax be? A match-like syntax feels more natural in the sense that each macro fragment resembles an arm.
  • Should the macro be supported directly by a language feature?
  • What should the feature name be? cfg_match conflicts with the already existing cfg_match crate.
  • How can we support usage in both expression-position and item position?
  • Support trailing commas to have similar grammar as match statements. (cfg_select: Support unbraced expressions #145233)

References

Implementation history

Tooling support

Activity

  1. added
    C-tracking-issueCategory: An issue tracking the progress of sth. like the implementation of an RFC
    T-libs-api[DEPRECATED; DO NOT USE]
    on Sep 6, 2023
  2. madsmtm commented on Sep 12, 2023

    @madsmtm
    Member

    I think it needs to be considered how this will interact with doc_auto_cfg?

    Also, if this is intended to be match-like, then it seems weird to me that you can omit a fallback implementation? Unless of course all the possible combinations are statically known to be accounted for (which is hard, I know).

  3. CAD97 commented on Sep 25, 2023

    @CAD97
    Contributor

    Modulo (auto) doc(cfg), this implementation is more complex than it needs to be, since it's only supporting items.

    simpler impl
    #[macro_export]
    macro_rules! cfg_match {
        {
            cfg($cfg:meta) => { $($cfg_items:item)* }
            $(cfg($rest_cfg:meta) => { $($rest_cfg_items:item)* })*
            $(_ => { $($fallthrough_items:item)* })?
        } => {
            #[cfg($cfg)]
            $crate::cfg_match! {
                _ => {
                    $($cfg_items)*
                    // validate the inactive cfgs as well
                    #[cfg(any($($rest_cfg),*))]
                    const _: () = ();
                }
            }
            #[cfg(not($cfg))]
            $crate::cfg_match! {
                $(cfg($rest_cfg) => { $($rest_cfg_items)* })*
                $(_ => { $($fallthrough_items)* })?
            }
        };
        {
            $(_ => { $($items:item)* })?
        } => {
            $($($items)*)?
        };
    }

    No internal matching arms necessary; just a normal inductive definition over the input syntax. The one tricky difference is that without the extra #[cfg] const item in the positive arm, later cfg predicates wouldn't be validated where they would be with the current implementation which always builds the full #[cfg(all(yes, not(any(...))))] matrix out.

    This can be rather simply adapted to expression position by matching a block and using break with value.


    It's unfortunate though that implemented without compiler support cfg_match! is effectively limited to either supporting item position or expression position. Supporting both needn't block supporting only item position (the more necessary position, since expression context can use if cfg!() else if chains most of the time), but it'd be nice to ensure there's a path forward for a compiler enhanced version which can expand differently between item and expression position, so the macro can be made to just work on both.

  4. madsmtm commented on Sep 25, 2023

    @madsmtm
    Member

    Something else to consider: This should be able to detect invalid cfgs like the following (ideally regardless of whether the cfg is currently enabled or not):

    match_cfg! {
        cfg(unix) => {}
        cfg(unix) => {} // Ooops, specified same `cfg` twice
        _ => {}
    }
  5. thomcc commented on Sep 25, 2023

    @thomcc
    Member

    I would prefer if it could support both items and expressions. It being part of the stdlib means that implementing it in the compiler would be completely reasonable, and would also make a lint that warns on redundant cfg arms pretty doable.

  6. jhpratt commented on Sep 25, 2023

    @jhpratt
    Member

    For detecting duplicates, it's important to note that this is more difficult than it seems at first. rustdoc has been dealing with this for doc(cfg).

  7. CAD97 commented on Sep 30, 2023

    @CAD97
    Contributor

    I tested the current (1.74.0-nightly (e6aabe8b3 2023-09-26)) behavior of doc_auto_cfg: items defined in cfg_match! do not receive a portability info box. Theoretically cfg dependencies could be tracked through macro expansion, but that's not done currently; cfg dependencies are only recorded on items.

    Fixing this is actually fairly simple once you understand why this is: instead of expanding to #[cfg($cfg)] identity! { $($items)* }, splat the cfg directly onto each item with $(#[cfg($cfg)] $items)*. The match_cfg crate does this; the cfg_if crate doesn't. A quick scan through the library directory didn't reveal any places where splatting cfg through cfg_if! would change documentation.

    I want to additionally point out that doc_auto_cfg fundamentally can only see what items are defined on the current cfg and mark them as only available on whatever cfg this specific implementation is gated to. If you want to expose a portable API, either define a single portable public version of the API (and use private cfg gates to implement it) or use doc(cfg()) to explicitly tell rustdoc what gate it should display as.

    So what I personally think the macro definition wants to be:

    in item position
    #[macro_export]
    macro_rules! cfg_match {
        // base case without wildcard arm
        () => {};
    
        // base case with wildcard arm (items)
        (_ => { $($wild_item:item)* }) => {
            $($wild_item)*
        };
    
        // process first arm (items; propagate cfg)
        (
            cfg($this_cfg:meta) => { $($this_item:item)* }
            $(cfg($rest_cfg:meta) => { $($rest_item:item)* })*
            $(_ => { $($wild_item:item)* })?
        ) => {
            $(#[cfg($this_cfg)] $this_item)*
            $crate::cfg_match! {
                $(cfg($rest_cfg) => { $(#[cfg(not($this_cfg))] $rest_item)* })*
                $(_ => { $(#[cfg(not($this_cfg))] $wild_item)* })?
            }
        };
    }
    in expression position
    #[macro_export]
    macro_rules! cfg_match {
        // base case without wildcard arm
        () => {};
    
        // base case with wildcard arm (expression block)
        (_ => $wild:block) => {
            $wild
        };
    
        // process first arm (expression block)
        (
            cfg($this_cfg:meta) => $this:block
            $(cfg($rest_cfg:meta) => $rest:block)*
            $(_ => $wild:block)?
        ) => {
            {
                #[cfg($this_cfg)]
                $crate::cfg_match! {
                    _ => $this
                }
                #[cfg(not($this_cfg))]
                $crate::cfg_match! {
                    $(cfg($rest_cfg) => $rest)*
                    $(_ => $wild)?
                }
            }
        };
    }
    in statement position
    #[macro_export]
    macro_rules! cfg_match {
        // base case without wildcard arm
        () => {};
    
        // base case with wildcard arm
        (_ => { $($wild:tt)* }) => {
            $($wild)*
        };
    
        // process first arm
        (
            cfg($this_cfg:meta) => { $($this:tt)* }
            $(cfg($rest_cfg:meta) => { $($rest:tt)* })*
            $(_ => { $($wild:tt)* })?
        ) => {
            #[cfg($this_cfg)]
            $crate::cfg_match! {
                _ => { $($this)* }
            }
            #[cfg(not($this_cfg))]
            $crate::cfg_match! {
                $(cfg($rest_cfg) => { $($rest)* })*
                $(_ => { $($wild)* })?
            }
        };
    }

    This technically is the most flexible spelling just passing through tt sequences; it works perfectly for items and statements (modulo preserving doc_auto_cfg) and can even be used for expressions if wrapped inside an outer block (e.g. { cfg_match! { ... } }).


    There's also the further question of how much syntax checking is expected for inactive arms. The current implementation that matches everything as $($:item)* does full syntax validation, but a naive implementation forwarding tokens context-antagonistically as $($:tt)* can easily cfg out tokens without ever placing them in a syntax-validated position -- in fact the above statement macro form does this.

    A far too overly clever implementation:
    #[macro_export]
    macro_rules! cfg_match {
        // base case (wildcard arm)
        () => {};
    
        // base case (no wildcard arm)
        (_ => { $($wild:tt)* }) => {
            $($wild)*
        };
    
        // Note: macro patterns matching $:stmt need to come before those matching
        // $:item, as otherwise statements will match as the $:item pattern instead
        // even when not valid as items (and obviously thus cause a syntax error).
    
        // apply cfg to wildcard arm (expression position)
        (_ => #[cfg($cfg:meta)] { $wild:expr }) => {
            { #[cfg($cfg)] { $wild } }
        };
    
        // apply cfg to wildcard arm (statement position)
        (_ => #[cfg($cfg:meta)] { $wild_stmt:stmt; $($wild:tt)* }) => {
            #[cfg($cfg)]
            $crate::cfg_match! {
                _ => { $wild_stmt; $($wild)* }
            }
    
            // We only parse the first statement in the macro pattern, so we need to
            // emit the captured syntax even when the cfg is inactive such that any
            // syntax errors still get reported. We do the macro capture this way as
            // if we match as statements, minor typos try to parse as items instead.
            #[cfg(any())]
            const _: () = { wild_stmt; $($wild)* };
        };
    
        // apply cfg to wildcard arm (item position)
        (_ => #[cfg($cfg:meta)] { $($wild:item)* }) => {
            $(#[cfg($cfg)] $wild)*
        };
    
        // merge multiple cfg into a single cfg(all()) predicate
        (_ => $(#[cfg($cfg:meta)])+ { $($wild:tt)* }) => {
            $crate::cfg_match! {
                _ => #[cfg(all($($cfg),+))] { $($wild)* }
            }
        };
    
        // split off first arm (empty)
        (
            cfg($this_cfg_arm:meta) => $(#[cfg($this_cfg_pre:meta)])* { /* empty */ }
            $(cfg($rest_cfg_arm:meta) => $(#[cfg($rest_cfg_pre:meta)])* { $($rest:tt)* })*
            $(_ => $(#[cfg($wild_cfg_pre:meta)])* { $($wild:tt)* })?
        ) => {
            $crate::cfg_match! {
                $(cfg($rest_cfg_arm) => $(#[cfg($rest_cfg_pre)])* #[cfg(not($this_cfg_arm))] { $($rest)* })*
                $(_ => $(#[cfg($wild_cfg_pre)])* #[cfg(not($this_cfg_arm))] { $($wild)* })?
            }
        };
    
        // split off first arm (expression position)
        (
            cfg($this_cfg_arm:meta) => $(#[cfg($this_cfg_pre:meta)])* { $this:expr }
            $(cfg($rest_cfg_arm:meta) => $(#[cfg($rest_cfg_pre:meta)])* { $rest:expr })*
            $(_ => $(#[cfg($wild_cfg_pre:meta)])* { $wild:expr })?
        ) => {{
            $crate::cfg_match! {
                _ => $(#[cfg($this_cfg_pre)])* #[cfg($this_cfg_arm)] { $this }
            }
            $crate::cfg_match! {
                $(cfg($rest_cfg_arm) => $(#[cfg($rest_cfg_pre)])* #[cfg(not($this_cfg_arm))] { $rest })*
                $(_ => $(#[cfg($wild_cfg_pre)])* #[cfg(not($this_cfg_arm))] { $wild })?
            }
        }};
    
        // split off first arm (statement position)
        (
            cfg($this_cfg_arm:meta) => $(#[cfg($this_cfg_pre:meta)])* { $this_stmt:stmt; $($this:tt)* }
            $(cfg($rest_cfg_arm:meta) => $(#[cfg($rest_cfg_pre:meta)])* { $($rest:tt)* })*
            $(_ => $(#[cfg($wild_cfg_pre:meta)])* { $($wild:tt)* })?
        ) => {
            $crate::cfg_match! {
                _ => $(#[cfg($this_cfg_pre)])* #[cfg($this_cfg_arm)] { $this_stmt; $($this)* }
            }
            // Ensure all arms infer statement position by prefixing a statement. We
            // only match the first arm in the macro pattern because otherwise minor
            // typos unhelpfully cause all arms to parse as item definitions instead
            // and thus reporting an error nowhere near the actual problem, or even
            // more amusingly, accidentally providing automatic semicolon insertion.
            $crate::cfg_match! {
                $(cfg($rest_cfg_arm) => $(#[cfg($rest_cfg_pre)])* #[cfg(not($this_cfg_arm))] { {}; $($rest)* })*
                $(_ => $(#[cfg($wild_cfg_pre)])* #[cfg(not($this_cfg_arm))] { {}; $($wild)* })?
            }
        };
    
        // split off first arm (item position)
        (
            cfg($this_cfg_arm:meta) => $(#[cfg($this_cfg_pre:meta)])* { $($this:item)* }
            $(cfg($rest_cfg_arm:meta) => $(#[cfg($rest_cfg_pre:meta)])* { $($rest:item)* })*
            $(_ => $(#[cfg($wild_cfg_pre:meta)])* { $($wild:item)* })?
        ) => {
            $crate::cfg_match! {
                _ => $(#[cfg($this_cfg_pre)])* #[cfg($this_cfg_arm)] { $($this)* }
            }
            // Items just match as item in the macro pattern. The error messages
            // would be improved if we could pass through tokens without eagerly
            // matching them as items, but we need to parse the items in the macro
            // so that we can apply the cfg attributes to every item in the arm.
            $crate::cfg_match! {
                $(cfg($rest_cfg_arm) => $(#[cfg($rest_cfg_pre)])* #[cfg(not($this_cfg_arm))] { $($rest)* })*
                $(_ => $(#[cfg($wild_cfg_pre)])* #[cfg(not($this_cfg_arm))] { $($wild)* })?
            }
        };
    }

    This does almost always behave as desired by using the arm bodies to try to guess how to expand, but is unreasonably complicated by attempting to do so and as a result causes some very poor errors for reasonable syntax errors.

  8. c410-f3r commented on Sep 30, 2023

    @c410-f3r
    ContributorAuthor

    Well, since #115416 (comment) I have been working in transposing cfg_match to a builtin in order to easily allow single elements in an arm without brackets. Such thing will also permit any other feature that is being discussed here AFAICT.

    It is worth noting that the current implementation mimics cfg_if and allows the expansion of token trees because cfgs are applied to macro calls as @CAD97 previously commented.

    cfg_match! {
        cfg(unix) => { fn foo() -> i32 { 1 } },
        _ => { fn foo() -> i32 { 2 } },
    }
    
    # Expands to ->
    
    #[cfg(all(unix, not(any())))]
    cfg_match! { @__identity fn foo() -> i32 { 1 } }
    
    #[cfg(all(not(any(unix))))]
    cfg_match! { @__identity fn foo() -> i32 { 2 } }

    That said, changing the strategy to apply cfg on each element inside a block will trigger stmt_expr_attributes (https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=4cf4facc19b052053cca655b2b42f782) for expressions and conceivably/possibly/maybe/perhaps, a large set of elements along a large set of cfgs could affect compilation times. But, it seems to be the correct approach due to doc_auto_cfg.

  9. petrochenkov commented on Oct 11, 2023

    @petrochenkov
    Contributor

    FWIW, #[cfg(TRUE)] not being removed from items during expansion is a bug, and reliance of rustdoc on that bug is very unfortunate.

    What we need to do is to keep expansion backtraces for cfgs like we do for all other macros.
    In that case it won't be important whether a cfg is placed directly on an item, or somewhere higher in the expansion tree (like on a macro producing that item), it will be available from the macro backtrace anyway.

  10. added 2 commits that reference this issue on Oct 13, 2023
  11. added a commit that references this issue on Oct 21, 2023
  12. added a commit that references this issue on Oct 21, 2023
  13. added a commit that references this issue on Oct 22, 2023
  14. 148 remaining items

  15. added a commit that references this issue on Feb 23, 2026
  16. added 4 commits that reference this issue on Jul 31, 2026
  17. added a commit that references this issue on Jul 31, 2026
  18. blyxyas commented on Aug 12, 2026

    @blyxyas
    Member

    Should cfg_select keep their expansion markers? We're having problems at Clippy because cfg_select results are indistinguishable of real code (and the SyntaxContext is identical), producing weird warnings.

    See rust-lang/rust-clippy#17498

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-cfg_selectArea: `cfg_select!`C-tracking-issueCategory: An issue tracking the progress of sth. like the implementation of an RFCI-lang-radarItems that are on lang's radar and will need eventual work or consideration.T-langRelevant to the language teamT-libs-api[DEPRECATED; DO NOT USE]

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions