Skip to content

Conversation

jhpratt
Copy link
Member

@jhpratt jhpratt commented Mar 30, 2025

About as simple as it gets.

Rendered

@ehuss ehuss added the T-lang Relevant to the language team, which will review and decide on the RFC. label Mar 31, 2025
@clarfonthey
Copy link

I think that a feature != "string" syntax as an alternative to not(feature = "string") would be a nice addition here. !(feature = "string") feels weirder to me than not(feature = "string"), honestly.

@jhpratt
Copy link
Member Author

jhpratt commented Mar 31, 2025

The problem I see with feature != "foo" is that it could plausibly be interpreted as "any feature other than foo".

@clarfonthey
Copy link

That interpretation feels like a stretch to me, but it's also understandable. Perhaps a feature("string") syntax might be a bit more natural with !feature("string"), and also extensible to feature("string", "other string").

@jhpratt
Copy link
Member Author

jhpratt commented Mar 31, 2025

Updated the following:

  • !feature = "foo" is no longer permitted. Negation can be followed by further negation, parentheses, a single identifier (so #[cfg(!unix)]), and any function-like predicate.
  • &/| added as alternative to &&/||
  • feature != "foo" added as alternative to !(feature = "foo")
  • added table with examples
  • linked to operator precedence for expressions for clarity

@RalfJung
Copy link
Member

RalfJung commented Apr 1, 2025

I think that a feature != "string" syntax as an alternative to not(feature = "string") would be a nice addition here. !(feature = "string") feels weirder to me than not(feature = "string"), honestly.

The problem here is that = in a cfg expression really means "contains", since feature (and also all other cfg variables) are really sets of strings, not just strings. So != would then mean "does not contain".

= is at least a different symbol from == so there's some hint that the semantics might be different, though it's not a very clear hint at all. != would exactly match expressions so there's not even a hint that the semantics are different.

@m-ou-se
Copy link
Member

m-ou-se commented Apr 1, 2025

I read #[cfg(feature = "asdf")] similar to how I read things like #[serde(default = "asdf")] or #[link_name = "asdf"]: The = syntax is used as a key-value pair, like in all attributes.

As mentioned above, feature = "asdf" is not at all like a Rust expression. It doesn't behave like a = expression (it's not an assignment). It doesn't behave like an == expression (it doesn't check equality either). There is no variable/concept named feature that is compared to anything.

It isn't even commutative: #[cfg("asdf" = feature)] doesn't work.

Using key = "value" syntax makes sense as it matches the syntax for all other attributes. But I don't think it makes much sense to take && and || from Rust expressions and trying to force them into attribute syntax. The result feels weirdly inconsistent, with things like !(feature = "asdf") not being the same as feature != "asdf", and so on.

If we do want to make cfg() take something that looks like an expression, we should also change the feature = "asdf" syntax to something that fits a Rust expression. E.g. #[cfg(has_feature("asdf") && has_target_feature("asdf"))].

@m-ou-se
Copy link
Member

m-ou-se commented Apr 1, 2025

Let's look at a real world example:

#[cfg(any(target_os = "android", target_os = "linux", target_os = "emscripten", target_os = "freebsd"))]

This proposal would allow rewriting that as:

#[cfg(target_os = "android" || target_os = "linux" || target_os = "emscripten" || target_os = "freebsd"))]

Which looks a bit nicer.

But instead of just replacing any() by a series of ||, I think we can do much better without giving up on the key = value syntax with something like:

#[cfg(target_os = "android" | "linux" | "emscripten" | "freebsd")]

I don't want to derail this thread with alternative proposals, but I do think that && and || is not getting us that much. If we go in that direction of making cfg() accept expression-like syntax, then we might be closing ourselves off from improvements to the key = value syntax that might serve the actual use cases better.

@RalfJung
Copy link
Member

RalfJung commented Apr 1, 2025

The result feels weirdly inconsistent, with things like !(feature = "asdf") not being the same as feature != "asdf"

How would they not be the same? The RFC, I think, intents for both of these to mean the same thing: the feature variable does not contain "asdf".

@m-ou-se
Copy link
Member

m-ou-se commented Apr 1, 2025

!(feature = "asdf") would work, while feature != "asdf" would be an error.

@RalfJung
Copy link
Member

RalfJung commented Apr 1, 2025

Ah. That seems entirely consistent to me, it's = and not == after all.

I like target_os = "android" | "linux" | "emscripten" | "freebsd" but it doesn't when you need to look at more than one key.

@m-ou-se
Copy link
Member

m-ou-se commented Apr 1, 2025

My point is that we need tot look at real world examples of code we're trying to simplify.

For example, quite often, any() is used with the same key, such as when selecting for a list of different operating systems. We should optimize the syntax for patterns that occur in actual code.

Large cfg() attributes can be very confusing. But replacing any() and all() by || and && isn't making it much better. Large boolean expressions will still be confusing, especially if their syntax is a hybrid of expressions and (key=value) attribute syntax with inconsistent precedence and operator meaning.

@m-ou-se
Copy link
Member

m-ou-se commented Apr 1, 2025

Rust parses feature = "asdf" && unix as feature = ("asdf" && unix), not as (feature = "asdf") && unix.

One might argue that = in attributes is different, but how would we expect an attribute proc macro to parse this today?

#[my_attribute_macro(expr = a && b, expr2 = c || d)]

I would assume it interprets that as two key = value pairs with the values being a && b and c || d. (It's up to the proc macro to decide, this is what you get if you parse it with a Rust expression parser.)

So even in an attribute, I'd argue that && should have higher precedence than the key=value operator. But that's the opposite of what this RFC proposes.

@programmerjake
Copy link
Member

maybe embrace that they are sets and allow using "abcd" in feature instead of feature = "abcd"? though then that makes me think we should have named it features.

@kpreid
Copy link
Contributor

kpreid commented Apr 1, 2025

Large cfg() attributes can be very confusing. But replacing any() and all() by || and && isn't making it much better.

I would go so far as to argue that any() and all() are better for formatting. Binary operators used repeatedly have an awkward indentation problem. Currently, rustfmt produces:

#[cfg(any(
    feature = "foo",
    feature = "bar",
    feature = "baz",
    feature = "quux",
    feature = "magic",
    test
))]

Whereas a similar boolean expression is less regular and longer:

something(cfg(
    feature == "foo"
        || feature == "bar"
        || feature == "baz"
        || feature == "quux"
        || feature == "magic"
        || test,
));

(And there are probably even messier cases to present with && and ! but I don't feel up to producing an illustrative example right now.) I won’t say that I haven’t wished for conventional binary operators in simple cases, but switching to them completely would have significant disadvantages (as would having a mix of both as standard style, simply due to having two things to learn and the choice of which one to use).

On the other hand, supporting feature = "foo" | "bar" has the inherent advantage of making the expression shorter and less repetitive, while still aligning with existing Rust syntax (patterns). It does raise the question of “why can’t I just write cfg(windows | unix)”, which, if also supported, would have a precedence problem — i.e. you have to either allow or prohibit cfg(feature = "foo" | bar), and if allowed, bar could be misinterpreted as a merely-unquoted value string.

@kennytm
Copy link
Member

kennytm commented Apr 1, 2025

So even in an attribute, I'd argue that && should have higher precedence than the key=value operator. But that's the opposite of what this RFC proposes.

GitHub really needs a 😢 emoji.

To be compatible with this existing syntax, it means all key = "value" cfg has to be wrapped in parenthesis:

#[cfg((target_os="linux") && (target_arch="x86_64") && (target_env="gnu") && !debug_assertions)]

@kennytm
Copy link
Member

kennytm commented Apr 1, 2025

I would go so far as to argue that any() and all() are better for formatting. Binary operators used repeatedly have an awkward indentation problem.

...

Whereas a similar boolean expression is less regular and longer:

...

(And there are probably even messier cases to present with && and ! but I don't feel up to producing an illustrative example right now.) I won’t say that I haven’t wished for conventional binary operators in simple cases, but switching to them completely would have significant disadvantages (as would having a mix of both as standard style, simply due to having two things to learn and the choice of which one to use).

Yes the formatting is strange, but I'd argue that the binary operator makes the cfg more readable for reviewers because you can be sure it is a chain of any rather than all at the first glance (spot the difference:

Existing syntax
#[cfg(any(
    feature = "foo",
    feature = "bar",
    feature = "baz",
    feature = "quux",
    feature = "magic",
    test
))]
#[cfg(all(
    feature = "foo",
    feature = "bar",
    feature = "baz",
    feature = "quux",
    feature = "magic",
    test
))]
This RFC
#[cfg(
    (feature = "foo")
        || (feature = "bar")
        || (feature = "baz")
        || (feature = "quux")
        || (feature = "magic")
        || test
)]
#[cfg(
    (feature = "foo")
        && (feature = "bar")
        && (feature = "baz")
        && (feature = "quux")
        && (feature = "magic")
        && test
)]
Alternative proposal
#[cfg(any(
    feature = "foo" 
        | "bar" 
        | "baz" 
        | "quux" 
        | "magic",
    test,
))]
#[cfg(all(
    feature = "foo" 
        & "bar" 
        & "baz" 
        & "quux" 
        & "magic",
    test,
))]

@SabrinaJewson
Copy link

cfg(any()) and cfg(all()) are very useful, often in macros, as always-false and always-true cfg values respectively. This RFC does not need to supplant all existing use cases of all and any, but it does seem weird to replace most things except this one edge case.

@Jules-Bertholet
Copy link
Contributor

cfg(any()) and cfg(all()) are very useful, often in macros, as always-false and always-true cfg values respectively.

rust-lang/rust#131204

@joshtriplett
Copy link
Member

@m-ou-se wrote:

If we do want to make cfg() take something that looks like an expression, we should also change the feature = "asdf" syntax to something that fits a Rust expression. E.g. #[cfg(has_feature("asdf") && has_target_feature("asdf"))].

Agreed completely. I don't think it would be ambiguous here to reuse "feature" without adding "has_": cfg(feature("asdf")). And this would also be consistent with other parts of cfg syntax.

@kennytm
Copy link
Member

kennytm commented Apr 2, 2025

I don't think it would be ambiguous here to reuse "feature" without adding "has_": cfg(feature("asdf")). And this would also be consistent with other parts of cfg syntax.

Technically this change of syntax will conflict with the cfg_version feature but since it is unstable it is fine...? (plus can't use version="x" on stable at all, it somehow triggers cfg_version too)

#![feature(cfg_version)]
fn main() {
	dbg!(cfg!(version("1.69.0")));
	dbg!(cfg!(version("2.0.0")));
	dbg!(cfg!(version = "1.69.0"));
	dbg!(cfg!(version = "2.0.0"));
}
$ rustc +nightly --cfg 'version="2.0.0"' 1.rs
$ ./1
[1.rs:3:2] cfg!(version("1.69.0")) = true
[1.rs:4:2] cfg!(version("2.0.0")) = false
[1.rs:5:2] cfg!(version = "1.69.0") = false
[1.rs:6:2] cfg!(version = "2.0.0") = true

@kennytm
Copy link
Member

kennytm commented Apr 25, 2025

The problem adapting to cfg_match! is that a && b etc. are not valid :meta in a macro. This is not a problem of cfg_match! alone, any other decl-macro expecting #[cfg($cfg:meta)] will be affected

@joshtriplett
Copy link
Member

Attempting to summarize the current state:

I think the current RFC proposing &&, ||, and ! would likely get a signoff.

|| has less strong support than &&, but seems important for consistency, and people will continue to have the option of writing any in any case where they consider it more readable.


I agree with the many comments saying that ! becomes even more useful if we add feature("...") and target_os("...") and similar. That seems worth writing up as a separate simple proposal.

| inside those patterns (target_os("..." | "...")) likewise seems useful, though it has less widespread support.

The combination of those two (function-style predicates and | inside them) seems like it would make a consistent proposal. I'd suggest leaving that in the "future work" of this RFC, and proposing it as an additional small RFC that complements this one.


I think != can be fully ruled out.

I also don't think there's been much support for &.

@joshtriplett
Copy link
Member

@rust-lang/lang, shall we accept the RFC as currently proposed, for adding &&, ||, and ! to cfg syntax?

As currently written, the RFC defers all other potential additions to future proposals.

@rfcbot merge

@rfcbot
Copy link

rfcbot commented May 28, 2025

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

Concerns:

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!

cc @rust-lang/lang-advisors: FCP proposed for lang, please feel free to register concerns.
See this document for info about what commands tagged team members can give me.

@rfcbot rfcbot added proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. labels May 28, 2025
@jhpratt
Copy link
Member Author

jhpratt commented May 28, 2025

That is my understanding of the current state as well. If accepted, I will almost certainly follow up with a function-like feature("foo") proposal as a standalone RFC.

Thanks for moving this forward, Josh!

@traviscross
Copy link
Contributor

First, thanks to @jhpratt for putting this together. The current syntax of cfg overall does seem less than ideal, and there's appeal in trying to do better.

Some thoughts:

One, I agree with @m-ou-se that, on its own, the degree of improvement here is somewhat limited, with @RalfJung that any(..) has some virtues, and with @kpreid that infix is often going to result in worse formatting.

Two, I'm certain that S = e is a flawed way to represent e ∈ S, but I'm uncertain about representing that as S(e) here for user-specified cfg options (like feature, which is just a convention as far as the language is concerned). Maybe. But it would mean that later adding any new operator would be a breaking change. And if we're touching it, it does seem that we should also consider having a concise way to represent "the set X contains at least one element of the set Y (i.e., X ∩ Y ≠ ∅)", as @m-ou-se suggested, and correspondingly "the set X contains every element of the set Y (i.e. X ⊇ Y)". It seems worth exploring some options for how best to encode those.

That's not directly part of this RFC, of course, but I think probably this RFC makes less sense if we're not headed in a direction like that, so I do maybe want to consider more of this together.

Three, this RFC specifies an alternate precedence for = in this context. I understand why it does that, but our story around precedence is already something I'd like to see us try to simplify (with editions) as we go about specifying the language, and so adding a little carve-out here raises a concern for me, including for the reasons that @m-ou-se mentioned. If we were to otherwise accept this RFC, I'd rather keep normal precedence and require the parens.

@rfcbot concern uncomfortable-with-precedence-carve-out

@RalfJung
Copy link
Member

RalfJung commented Jun 23, 2025

I'm certain that S = e is a flawed way to represent e ∈ S, but I'm uncertain about representing that as S(e) here for user-specified cfg options

FWIW I find S(e) to be a quite natural way to represent e ∈ S, but that is probably because of my background in type theory where sets are often modeled as predicates ("functions with return type bool", basically), and so asking "is this value in the set" is literally a function call.

@traviscross
Copy link
Contributor

FWIW I find S(e) to be a quite natural way to represent e ∈ S, but that is probably because of my background in type theory where sets are often modeled as predicates ("functions with return type bool", basically), and so asking "is this value in the set" is literally a function call.

Yes; I have a similar intuition for it, and agree that it's natural in that way. I'm just not sure about putting this set-membership predicate in the same syntax space as arbitrary predicates, and thereby making it a breaking change to add any new such predicates.

Comment on lines +49 to +52
| `feature="foo" \|\| feature="bar"` | `any(feature="foo", feature="bar")` | `\|\|` has lower precedence than `=` |
| `feature="foo" && feature="bar"` | `all(feature="foo", feature="bar")` | `&&` has lower precedence than `=` |
| `!feature="foo"` | _syntax error_ | `!` has higher precedence than `=`, which may be confusing, so we ban this syntax |
| `!(feature="foo")` | `not(feature="foo")` | use `()` for grouping |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(starting an inline discussion for brainstorming key = "val" syntax stemming from #3796 (comment))

FWIW I find S(e) to be a quite natural way to represent e ∈ S, but that is probably because of my background in type theory where sets are often modeled as predicates ("functions with return type bool", basically), and so asking "is this value in the set" is literally a function call.

Yes; I have a similar intuition for it, and agree that it's natural in that way. I'm just not sure about putting this set-membership predicate in the same syntax space as arbitrary predicates, and thereby making it a breaking change to add any new such predicates.

Could we add a new named predicate for this? set(feature, "foo") is terse and could work, though I don't love that the intuition here would probably indicate a 1:1 mapping. contains or includes are probably how we would spell this on a collection, but I think contains(feature, "foo") sounds a bit worse than plural would be contains(features, "foo").

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll have an RFC together soon, though deciding what the syntax should be will definitely be better before it's fully written.

Random thought, which admittedly isn't very Rust-like, is feature?("foo"), along with feature?("foo" | "bar") and feature?("foo" & "bar"). There is some precedence in Ruby where ? means a function that returns a boolean.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is some precedence in Ruby where ? means a function that returns a boolean.

It comes from Scheme, which uses the same convention for predicates, e.g. odd?.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me the important effect of adding &&, etc. to cfg is making it more consistent with the regular Rust syntax, so that it's less of a second micro language within a language.

Using ? in cfg in a way that is different from regular Rust code would be just another version of the same problem of using = in cfg in a way that is different from regular Rust code.

feature("val") is not too bad. It's probably the tersest syntax that regular Rust code could implement for the same purpose.

For multiple values, maybe something like this:

feature(any("val1", "val2"))

This would also be implementable in a regular Rust syntax with a bit of generics.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One potential issue is that feature(..) is currently used for enabling language features, whereas feature = ".." is used for checking crate features. Not really sure how to reconcile this, honestly, but worth pointing out that making the syntax closer to feature(..) may not be best.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rust typically uses foo() for getters and set_foo() for setters, and cfg() is a context that is querying the config rather than setting it, so to be this doesn't look like a setter.

has_feature() would be even clearer.

@jmjoy
Copy link

jmjoy commented Jun 25, 2025

Simple Boolean operations are one thing, but what I’m really hoping for is the ability to compare values, something that’s quite common in C macros, like:

#if FOO > 1000

But in Rust, there’s currently no way to express this, which leads to some rather awkward workarounds.

Right now, the values in Rust’s cfg are all strings, so it seems difficult to support something like:

#[cfg(FOO > 1000)]

@jhpratt
Copy link
Member Author

jhpratt commented Jun 25, 2025

@jmjoy That would fundamentally change what cfg is.

@joshtriplett
Copy link
Member

There have been proposals for key-value features, and I'd expect such a proposal to talk about how to use them in cfg, but that's off-topic for this RFC.

@Jules-Bertholet
Copy link
Contributor

It would be nice if this RFC also specified a new macro_rules! fragment specifier to match an arbitrary cfg predicate, for use by macros like cfg_if. Currently, you can use meta, which excludes only true and false, but with this RFC, that would become completely inadequate.

@traviscross
Copy link
Contributor

It would be nice if this RFC also specified a new macro_rules! fragment specifier to match an arbitrary cfg predicate, for use by macros like cfg_if. Currently, you can use meta, which excludes only true and false, but with this RFC, that would become completely inadequate.

That's a good point. This document should, I think, discuss the effect of this on macro fragment specifiers and propose solutions, perhaps using our guidance in #3531, if appropriate.

@rfcbot concern discuss-effect-on-macro-fragment-specifiers

@jhpratt
Copy link
Member Author

jhpratt commented Sep 21, 2025

meta already works, though, as it matches the entire attribute inside the brackets, not just what's inside cfg(). Playground link for proof.

Sure, things like cfg_if and cfg_select need to work around some things because they match on the inner parts, but isn't that already the case?

@Jules-Bertholet
Copy link
Contributor

Jules-Bertholet commented Sep 21, 2025

Sure, things like cfg_if and cfg_select need to work around some things because they match on the inner parts, but isn't that already the case?

Right now, the only thing they “need to work around” is true/false:

Footnotes

  1. If this RFC were to be rejected, I would probably propose changing the meta specifier to accept true and false, in order to address this issue—it would be a backward-compatible change that would not require an edition. But if this RFC is accepted, a dedicated cfg specifier would make more sense, and there wouldn’t be much point in changing meta.

@jhpratt
Copy link
Member Author

jhpratt commented Sep 21, 2025

How common are macros that need to handle the internal part of cfg? Ultimately that's what we're talking about here. The fact that meta, prior to true/false, matched the internal part perfectly was largely a historical artifact from before arbitrary tokens could be accepted. That restriction was lifted in April 2019.

Essentially, my argument is that this scenario is sufficiently uncommon that it is not worth the added language complexity for a rarely used macro fragment specifier.

@Jules-Bertholet
Copy link
Contributor

How common are macros that need to handle the internal part of cfg?

I just cited three examples.

If you feel that defining an entirely new specifier is too much complexity, one alternative would be to re-use expr.

@jhpratt
Copy link
Member Author

jhpratt commented Sep 21, 2025

I just cited three examples.

I meant in general, everyday code. Defining macros like that is not common, presumably, hence my question. My belief is that manipulating the contents of cfg is quite rare, as nearly all cases will want to merely pass the contents on to the output, for which purpose meta or $(x:tt)* would suffice. All existing macro fragment specifiers are used with sufficient frequency (or are fundamentally required due to "follows" ambiguity) that having one here seems overly niche.

For what it's worth, meta does not appear to have been intended to be used in this manner in the first place; it seems to be a historical quirk that the internal syntax followed the same pattern as the attribute as a whole.

@Jules-Bertholet
Copy link
Contributor

Actually, I realized that cfg_if doesn’t need a dedicated fragment specifier, it can just switch to tt: rust-lang/cfg-if#99. That wouldn’t work for cfg_match or rust-lang/rust#146281, however.

@tgross35
Copy link
Contributor

See rust-lang/rust#145060 for previous discussion on meta matching true and false. Quoting @petrochenkov:

meta is not supposed to match syntax of cfg predicates, or other syntaxes that other attributes may define, it matches the stuff inside the attribute brackets #[...].

* `#[attr]` - ok, `attr` is `meta`
* `#[attr { a b c }]` - ok, `attr { a b c }` is `meta`
* `#[true]` - not ok, `true` is not `meta`

@Jules-Bertholet
Copy link
Contributor

one alternative would be to re-use expr.

Increasingly I am thinking that this is probably the best solution. expr already accepts all the syntax in this RFC, the restriction on its use in cfg simply needs to be lifted.

@traviscross
Copy link
Contributor

@rfcbot resolve discuss-effect-on-macro-fragment-specifiers

@Jules-Bertholet
Copy link
Contributor

Increasingly I am thinking that this is probably the best solution. expr already accepts all the syntax in this RFC, the restriction on its use in cfg simply needs to be lifted.

rust-lang/rust#146961

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.