Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tracking issue for cfg_match #115585

Open
1 of 3 tasks
c410-f3r opened this issue Sep 6, 2023 · 21 comments
Open
1 of 3 tasks

Tracking issue for cfg_match #115585

c410-f3r opened this issue Sep 6, 2023 · 21 comments
Labels
C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.

Comments

@c410-f3r
Copy link
Contributor

c410-f3r commented Sep 6, 2023

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

Public API

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

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.

References

@c410-f3r c410-f3r added C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. labels Sep 6, 2023
@madsmtm
Copy link
Contributor

madsmtm commented Sep 12, 2023

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).

@CAD97
Copy link
Contributor

CAD97 commented Sep 25, 2023

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.

@madsmtm
Copy link
Contributor

madsmtm commented Sep 25, 2023

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
    _ => {}
}

@thomcc
Copy link
Member

thomcc commented Sep 25, 2023

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.

@jhpratt
Copy link
Member

jhpratt commented Sep 25, 2023

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).

@CAD97
Copy link
Contributor

CAD97 commented Sep 30, 2023

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.

@c410-f3r
Copy link
Contributor Author

c410-f3r commented Sep 30, 2023

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.

@petrochenkov
Copy link
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.

matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Oct 13, 2023
Initiate the inner usage of `cfg_match` (Compiler)

cc rust-lang#115585

Dogfood to test the implementation and remove dependencies.
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Oct 14, 2023
Initiate the inner usage of `cfg_match` (Compiler)

cc rust-lang#115585

Dogfood to test the implementation and remove dependencies.
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Oct 21, 2023
Initiate the inner usage of `cfg_match` (Compiler)

cc rust-lang#115585

Dogfood to test the implementation and remove dependencies.
rust-timer added a commit to rust-lang-ci/rust that referenced this issue Oct 21, 2023
Rollup merge of rust-lang#116312 - c410-f3r:try, r=Mark-Simulacrum

Initiate the inner usage of `cfg_match` (Compiler)

cc rust-lang#115585

Dogfood to test the implementation and remove dependencies.
bors added a commit to rust-lang-ci/rust that referenced this issue Oct 22, 2023
Initiate the inner usage of `cfg_match` (Library)

cc rust-lang#115585

Continuation of rust-lang#116312
workingjubilee added a commit to workingjubilee/rustc that referenced this issue Oct 28, 2023
workingjubilee added a commit to workingjubilee/rustc that referenced this issue Oct 29, 2023
rust-timer added a commit to rust-lang-ci/rust that referenced this issue Oct 29, 2023
Rollup merge of rust-lang#117162 - c410-f3r:try, r=workingjubilee

Remove `cfg_match` from the prelude

Fixes rust-lang#117057

cc rust-lang#115585
bors added a commit to rust-lang-ci/rust that referenced this issue Feb 18, 2024
Initiate the inner usage of `cfg_match` (Library)

cc rust-lang#115585

Continuation of rust-lang#116312
CKingX added a commit to CKingX/rust that referenced this issue Feb 18, 2024
Initiate the inner usage of `cfg_match` (Library)

cc rust-lang#115585

Continuation of rust-lang#116312
bors added a commit to rust-lang-ci/rust that referenced this issue Feb 19, 2024
Initiate the inner usage of `cfg_match` (Library)

cc rust-lang#115585

Continuation of rust-lang#116312
@Nemo157
Copy link
Member

Nemo157 commented Sep 12, 2024

I just found out about this today, and my first impression is that semantically this isn't really a "match", given the equivalent way to write a similar runtime match would be:

match () {
    () if unix => { ... }
    () if target_pointer_width.contains("32") => { ... }
    _ => { ... }
}

A more similar construct in my mind are the select! family of macros, std::select!, crossbeam::select!, futures::select!, tokio::select!. The primary difference between those macros and this one is that in this one all the patterns are boolean valued so don't need a pattern, just the <expr> => { <arm> },.

All that is to say that cfg_select! seems like a better name to me.

matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Jan 16, 2025
…joshtriplett

[cfg_match] Adjust syntax

A year has passed since the creation of rust-lang#115585 and the feature, as expected, is not moving forward. Let's change that.

This PR proposes changing the arm's syntax from  `cfg(SOME_CONDITION) => { ... }` to `SOME_CODITION => {}`.

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

Why? Because after several manual migrations in rust-lang#116342 it became clear,  at least for me, that `cfg` prefixes are unnecessary, verbose and redundant.

Again, everything is just a proposal to move things forward. If the shown syntax isn't ideal, feel free to close this PR or suggest other alternatives.
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Jan 16, 2025
…joshtriplett

[cfg_match] Adjust syntax

A year has passed since the creation of rust-lang#115585 and the feature, as expected, is not moving forward. Let's change that.

This PR proposes changing the arm's syntax from  `cfg(SOME_CONDITION) => { ... }` to `SOME_CODITION => {}`.

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

Why? Because after several manual migrations in rust-lang#116342 it became clear,  at least for me, that `cfg` prefixes are unnecessary, verbose and redundant.

Again, everything is just a proposal to move things forward. If the shown syntax isn't ideal, feel free to close this PR or suggest other alternatives.
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Jan 16, 2025
…joshtriplett

[cfg_match] Adjust syntax

A year has passed since the creation of rust-lang#115585 and the feature, as expected, is not moving forward. Let's change that.

This PR proposes changing the arm's syntax from  `cfg(SOME_CONDITION) => { ... }` to `SOME_CODITION => {}`.

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

Why? Because after several manual migrations in rust-lang#116342 it became clear,  at least for me, that `cfg` prefixes are unnecessary, verbose and redundant.

Again, everything is just a proposal to move things forward. If the shown syntax isn't ideal, feel free to close this PR or suggest other alternatives.
rust-timer added a commit to rust-lang-ci/rust that referenced this issue Jan 16, 2025
Rollup merge of rust-lang#133720 - c410-f3r:cfg-match-foo-bar-baz, r=joshtriplett

[cfg_match] Adjust syntax

A year has passed since the creation of rust-lang#115585 and the feature, as expected, is not moving forward. Let's change that.

This PR proposes changing the arm's syntax from  `cfg(SOME_CONDITION) => { ... }` to `SOME_CODITION => {}`.

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

Why? Because after several manual migrations in rust-lang#116342 it became clear,  at least for me, that `cfg` prefixes are unnecessary, verbose and redundant.

Again, everything is just a proposal to move things forward. If the shown syntax isn't ideal, feel free to close this PR or suggest other alternatives.
jieyouxu added a commit to jieyouxu/rust that referenced this issue Jan 21, 2025
…tgross35,jhpratt

[cfg_match] Document the use of expressions.

cc rust-lang#115585

Adds documentation to this new feature introduced in rust-lang#133720.
jieyouxu added a commit to jieyouxu/rust that referenced this issue Jan 21, 2025
…tgross35,jhpratt

[cfg_match] Document the use of expressions.

cc rust-lang#115585

Adds documentation to this new feature introduced in rust-lang#133720.
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Jan 21, 2025
…tgross35,jhpratt

[cfg_match] Document the use of expressions.

cc rust-lang#115585

Adds documentation to this new feature introduced in rust-lang#133720.
Zalathar added a commit to Zalathar/rust that referenced this issue Jan 29, 2025
…tgross35,jhpratt

[cfg_match] Document the use of expressions.

cc rust-lang#115585

Adds documentation to this new feature introduced in rust-lang#133720.
fmease added a commit to fmease/rust that referenced this issue Jan 29, 2025
…tgross35,jhpratt

[cfg_match] Document the use of expressions.

cc rust-lang#115585

Adds documentation to this new feature introduced in rust-lang#133720.
rust-timer added a commit to rust-lang-ci/rust that referenced this issue Jan 29, 2025
Rollup merge of rust-lang#135625 - c410-f3r:cfg-match-foo-bar-baz, r=tgross35,jhpratt

[cfg_match] Document the use of expressions.

cc rust-lang#115585

Adds documentation to this new feature introduced in rust-lang#133720.
@joshtriplett
Copy link
Member

I tested this, and trailing commas seem to work:

cfg_match! {
    unix => {
        fn f1() {
            println!("unix");
        }
    },
    _ => {
        fn f1() {
            println!("not unix");
        }
    },
}

It also works in function bodies, for whole statements.

fn f2() {
    cfg_match! {
        unix => {
            println!("unix");
        },
        _ => {
            println!("not unix");
        },
    }
}

It doesn't seem to work in expression position, but that's something that could be added compatibly in the future.

Based on all of that, I think this is ready to consider stabilizing:

@rfcbot merge

@rfcbot
Copy link

rfcbot commented Feb 14, 2025

Team member @joshtriplett has proposed to merge this. The next step is review by the rest of the tagged team members:

No concerns currently listed.

Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

See this document for info about what commands tagged team members can give me.

@rfcbot rfcbot added proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. labels Feb 14, 2025
@Skgland
Copy link
Contributor

Skgland commented Feb 14, 2025

It doesn't seem to work in expression position, but that's something that could be added compatibly in the future.

It appears to work as a blocks tail expression though, but not as a general expression: play-ground

@madsmtm
Copy link
Contributor

madsmtm commented Feb 17, 2025

It'd be nice if the tracking issue could be updated with places where the decisions for the unresolved questions were made?


I still feel like it's a mistake to stabilize this without compiler support, there's just so many things that are still undesirable about the current implementation:

  • doc_auto_cfg doesn't work (because the #[cfg] attributes are not placed on items, but on the macro).
  • The macro does now support expression position, but it requires extra braces, which is quite confusing.
  • The braces around the resulting item/expression are unnecessary. Ideally, we'd be able to do:
    let res = cfg_match! {
        unix => libc::foo(),
        _ => libc::bar(),
    };

I recognize that none of these are blocking concerns, but at the very least we should consider not stabilizing the expression position support, as that is certainly something a future compiler implementation would want to avoid.

tgross35 added a commit to tgross35/rust that referenced this issue Feb 18, 2025
At [1] it was pointed out that `cfg_match!` syntax does not actually
align well with match syntax, which is a possible source of confusion.
The comment points out that usage is instead more similar to ecosystem
`select!` macros. Rename `cfg_match!` to `cfg_select!` to match this.

Tracking issue: rust-lang#115585

[1]: rust-lang#115585 (comment)
@tgross35
Copy link
Contributor

I just found out about this today, and my first impression is that semantically this isn't really a "match"

[...]

All that is to say that cfg_select! seems like a better name to me.

As-is, this seems worth changing; I would expect a macro named after match to support a lot of match syntax, but we don't. I opened #137198 to do this.

Alternatively, maybe we should make this look and work like match. This could look like:

cfg_match! {
    unix => 0,           // Simple match
    unix | windows => 0, // One of many
    target_pointer_width: "32" => 0,        // Match a single `=` config value
    target_pointer_width: "16" | "32" => 0, // One of many `=` values
    feature: ["std", "derive"] => 0,        // `feature = "std"`, `feature = "derive"`, no other `feature`
    feature: ["std", "derive", ..] => 0,    // std, derive, and possibly others
    _ => 0
}

Which can effectively expand to something like this:

pub struct Cfg {
    unix: bool,
    windows: bool,
    target_pointer_width: &'static [&'static str],
    feature: &'static [&'static str]
}

match config {
    Cfg { unix: true, .. } => 0,
    Cfg { unix: true, .. } | Cfg { windows: true, .. } => 0,
    Cfg { target_pointer_width: ["16" | "32"], .. } => 0,
    // Ordering does not matter
    Cfg { feature: ["std", "derive"], .. } => 0,
    Cfg { feature: ["std", "derive", ..], .. } => 0,
    _ => 0
}

@tgross35
Copy link
Contributor

What is the motivation for this being based on match rather than if in the first place? match has a much more complicated syntax and set of rules to replicate as a macro, this macro isn't really consistent with those rules now (I don't think it can be), and it seems unclear if we gain anything substantial in exchange for the complexity. The top post's example could just as easily be:

static_cfg! {
    if cfg(unix) {
        fn foo() {}
    } else if cfg(target_pointer_width = "32") {
        fn foo() {}
    } else {
        fn foo() {}
    }
}

(I am including the cfg wrapper because a visual separator between expression syntax and cfg predicates seems useful. E.g. cfg(a && b) is not valid, cfg(a) && cfg(b) could one day be added).

@c410-f3r
Copy link
Contributor Author

Unbelievable, once again a feature has blocking concerns at the final stages of stabilization. It may be worth adding an official blog post for each new feature as well as each new substantial change to increase awareness.

For any following discussion, try to focus on a simple but extensible MVP for shipping purposes. Further improvements that require further elaborations could then be postponed to future versions.

Perfection is the enemy of good and everyone here knows the usefulness of this macro.

@jhpratt
Copy link
Member

jhpratt commented Feb 18, 2025

Unbelievable, once again a feature has blocking concerns at the final stages of stabilization.

@c410-f3r That's literally the purpose of FCP.

@joshtriplett
Copy link
Member

* `doc_auto_cfg` doesn't work (because the `#[cfg]` attributes are not placed on items, but on the macro).

I definitely wouldn't consider this a blocker, though it'd be nice if someone wanted to provide a fix.

* The macro [does now](https://github.com/rust-lang/rust/pull/135625) support expression position, but it requires extra braces, which is quite confusing.
* The braces around the resulting item/expression are unnecessary. Ideally, we'd be able to do:
  let res = cfg_match! {
      unix => libc::foo(),
      _ => libc::bar(),
  };

I agree, but that's something we could fix in the future.

I recognize that none of these are blocking concerns, but at the very least we should consider not stabilizing the expression position support, as that is certainly something a future compiler implementation would want to avoid.

Can you clarify whether there's a backwards compatibility concern here, such that the compiler implementation would want to remove a feature relative to the current macro version?

If there's a backwards compatibility concern, happy to block this waiting on a solution. If there's no backwards compatibility concern, then I continue to think we should ship this and let it add more features in the future.

@joshtriplett
Copy link
Member

joshtriplett commented Feb 18, 2025

@tgross35 The motivation was that since this largely exists to (substantially) reduce typing, it seemed preferable to further reduce typing by not requiring the user to repeatedly write cfg().

It's a macro that's evocative of a match; that doesn't mean it has to support all the syntax of match and pattern matching. We could add more in the future. (We could also add more operator-based syntax to cfg, too.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests