Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

epage
Copy link
Contributor

@epage epage commented Sep 15, 2025

Tracking issue (#[cfg(version(..))]

Rendered

Summary

Allow Rust-version conditional compilation by adding a built-in --cfg rust=<version> and a minimum-version #[cfg] predicate.

Say this was added before 1.70, you could do:

[target.'cfg(not(version_since(rust, "1.70")))'.dependencies"]
is-terminal = "0.4.16"
fn is_stderr_terminal() -> bool {
    #[cfg(version_since(rust, "1.70"))]
    use std::io::IsTerminal as _;
    #[cfg(not(version_since(rust, "1.70")))]
    use is_terminal::IsTerminal as _;

    std::io::stderr().is_terminal()
}

This supersedes the cfg_version subset of RFC 2523.

@epage epage added the T-lang Relevant to the language team, which will review and decide on the RFC. label Sep 15, 2025
@epage
Copy link
Contributor Author

epage commented Sep 15, 2025

RE my assumptions around owning teams for this decision:

  • T-lang: primarily geared towards them
  • T-compiler: unsure if any of the parts related to the compiler are sufficient for adding T-compiler.
  • T-clippy: the clippy sections are written with the intention of looking at the whole picture and call out that they are not prescriptive and I don't think T-clippy needs to be pulled into the decision though input is warranted
  • T-cargo: this is an area where Cargo just copies what T-lang/T-compiler do and are not co-owners but input is warranted

CC @rust-lang/compiler , @rust-lang/clippy , @rust-lang/cargo

@epage
Copy link
Contributor Author

epage commented Sep 15, 2025

CC @Urgau as this touches a lot on check-cfg and would appreciate your input on how all of that is handled in this.

# Drawbacks
[drawbacks]: #drawbacks

People may be using `--cfg rust` already and would be broken by this change.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

At least with my initial search, I did not see any public uses


This does not help with probes for specific implementations of nightly features which still requires having a `build.rs`.

Libraries could having ticking time bombs that accidentally break or have undesired behavior for some future Rust version that can't be found until we hit that version.
Copy link
Member

Choose a reason for hiding this comment

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

I assume you mean because of them using #[cfg(since(rust, "1.$BIG_NUMBER"))] or similar "point a loaded firearm directly at my lower limb" gestures? They sort of already can do that, with the classic example being mem::transmute of std entities with entirely-unspecified layouts, and then us changing it in a future version. But yes, strictly speaking it does introduce a new vector, and one we will inevitably hit in time rather than merely potentially.

Copy link

Choose a reason for hiding this comment

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

I wonder if this is something that could be warn-by-default. Or at least warn-by-default in clippy. The warning would only be an issue when a workspace is worked on by rust versions older than their highest version cfg.

Copy link
Member

Choose a reason for hiding this comment

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

I don't think we have any way of detecting this. No toolchain can flag "too new" version requirements, because it's reasonable to detect things newer than that toolchain.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also I would assume this problem already exists with rustversion.

Copy link
Member

@workingjubilee workingjubilee Sep 18, 2025

Choose a reason for hiding this comment

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

Yes. I expect it will be very common to have a crate that is developed and tested primarily with an old rustc version and then have since used against a much later one so that new features can also be used selectively, so warning against this issue will mostly interfere with what may be the most popular pattern for using the feature.

Comment on lines +308 to +309
*but* maybe that would just be the start to natively supporting more types in `cfg`,
like integers which are of interest to embedded folks.
Copy link
Member

Choose a reason for hiding this comment

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

It feels somewhat unclear to me why we would start this journey by supporting this new "version literal" type in cfg only and not in the language? As opposed to supporting integers first, which are supported in the language.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note that I'm saying doing so is a potential alternative; I am not proposed we do. If we did so, the reason would be "because this RFC came first".

Comment on lines 596 to 599
- Contort the version into SemVer
- This can run into problems either with having more precision (e.g. `120.0.1.10` while SemVer only allows `X.Y.Z`) or post-release versions (e.g. `1.2.0.post1` which a SemVer predicate would treat as a pre-release).
- Add an optional third field for specifying the version format (e.g. `#[cfg(since(windows, "10.0.10240", <policy-name>)]`)
- Make `--check-cfg` load-bearing by having the version policy name be specified in the `--check-cfg` predicate
Copy link
Member

Choose a reason for hiding this comment

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

One depressingly verbose option, but one that will work without modification to the proposal, is to attach an additional "epoch version" via cfg, e.g.

#[cfg(
    all(
        since(thing, "1.0.0"), since(thing_epoch, "2")
    )
)]
mod something {}

#[cfg(
    all(
        since(thing, "1.1.0"), not(since(thing_epoch, "2"))
    )
)]
mod something {}

Obviously, since(thing, "1.0.0") would be satisfied by "1.1.0" in ordering without the all including an additional "version epoch".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unsure how much we need to explore this as its a future possibility. The main question is whether we paint ourselves into a corner for target_version.

@kornelski

This comment was marked as duplicate.

@kornelski

This comment was marked as duplicate.

@0xllx0

This comment was marked as duplicate.

@tmandry
Copy link
Member

tmandry commented Sep 23, 2025

What strikes me when considering @kornelski's comment above is that we haven't yet explored the idea of introducing a new type of cfg value for versions to the language.

This RFC introduces a new cfg predicate and a new check-cfg predicate to match. If we instead had typed cfg values we could redefine predicates like = to do something more specific for that type, in addition to defining additional predicates like >= and <.

The question is how to introduce values of a type. Following Rust, we can introduce them as version("1.85"). So on the command line you would say --cfg rust=version("1.85"), and in code you could write

#[cfg(rust >= version("1.85")]
#[cfg(rust < version("1.85")]

Again, following Rust, we could choose to allow comparison between a version on the left and a string literal on the right. Probably I would defer this for now, but could be convinced either way.

Beyond the syntactic improvements, this gives us some extra flexibility. For example:

  • I can write --check-cfg cfg(rust >= version("1.85")) and this defines the set of operators that are allowed with that value.
  • We can choose to enforce that all values of a cfg are the same type. Or in the case of versions, we can enforce that you only have one value to begin with.
  • As a future possibility we could have something like cratename = version("1.100+metadata") check for an exact match to +metadata on the LHS, while a more general requirement like cratename = version("1.100") would ignore any metadata that happens to exist on the LHS. Supporting this kind of comparison in the current RFC is less straightforward.

I still support the current RFC; I think it's better than anything we've considered before and it gets the details right. But this other direction is worth considering as a possibility. Using typed comparisons instead of operators on strings feels more Rusty to me.

We've already reserved space for cfg types with rustc's --cfg foo="bar" syntax that requires the double quotes, so we might as well make use of it. And of course, another future possibility is to add integer configs.

Currently #[cfg()] has its own microsyntax, being a language inside a language, with its own design that is quite different from Rust's expressions. There's a proposal to move away from that, and make it behave more like normal expressions, using &&/|| instead of all()/any(). Syntactically, this will end up allowing everything matching macro expr inside cfg().

For the record, I'm very much open to this direction. I don't think it cuts strongly against a comparison operator like since(), since that already looks like a function call.

@traviscross
Copy link
Contributor

If we instead had typed cfg values we could...

What I'd really like eventually is to not have a DSL here at all and just use Rust within cfg attributes. We already have a Rust interpreter on hand -- consteval. Obviously nothing from the crate itself would be in scope.

Clearly I don't think cfg_version should wait for that. But when we start talking about typed cfg values, generic predicates, and, presumably therefore, type inference and type checking for cfg, it does make me wonder whether using Rust here and consteval might not be more straightforward after all.

Comment on lines +48 to +50
Semantics:
- Language version: versioning of expected / defined behavior, based on the canonical compiler
- Vendor name/version: identifying the specific compiler being used
Copy link
Contributor Author

Choose a reason for hiding this comment

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

From @jmillikin at #3857 (comment)

How would this work with non-rustc compilers, such as gccrs? The current proposal treats the rustc release version as a Rust language version, which only really works in languages that have a single implementation (Python, Swift).

Looking forward into Rust's future slightly, imagine widespread adoption lead to something like today's C/C++ ecosystem. There are the three major toolchains (GCC, Clang, MSVC) plus various vendor compilers. Language version detection uses a vendor-neutral standard (__STDC_VERSION__, __cplusplus). Compiler-specific feature detection in libraries is uncommon and generally causes a lot of complaints (c.f. gnulib).

Rust doesn't currently have a versioned language specification, so the available non-vendor identifiers are the edition and the stability annotation feature names. A way to conditionally compile based on those identifiers might be something like this:

#[cfg(edition >= 2021)]
mod some_mod_for_2021_edition;

#[cfg(feature = "fs_try_exists")] // stabilized in rustc v1.81
use std::fs::exists as fs_exists;

#[cfg(not(feature = "fs_try_exists"))]
fn fs_exists(...) { /* open-coded impl of std::fs::exists for older toolchains */ }

Regarding conditional compilation based on edition, it's difficult to think of a concrete use case. The edition is part of the Cargo.toml library definition, so there's no case where the same source file will be compiled with different editions. In build systems that let the edition vary by detected toolchain (Bazel) the same mechanism can be used to select different source files and --cfg flags.

The stability annotation feature names haven't historically been exposed to stable Rust, so if they or something like them are to be used for compile-time toolchain feature detection then it might be worth considering whether a more structured format is appropriate.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

From @matthieu-m at #3857 (comment)

@jmillikin I believe the preferred way to raise concerns is to raise targeted concerns as comments anchored on specified pieces of the RFC, in order to consolidate all related concerns in one place.

How would this work with non-rustc compilers, such as gccrs? The current proposal treats the rustc release version as a Rust language version, which only really works in languages that have a single implementation (Python, Swift).

First of all, 1.70 is a Rust language version. Thus if gccrs advertised 1.70 as the language version it would be expected to match the behavior of rustc 1.70.

Apart from that, I believe I already raised your concerns in my two reviews comments, on:

[Line 53] We are scoping this RFC down to "language version" but considering "vendor version" as it can be approximated by the "language version" and in case it breaks any ties in decisions.

I raised the concern about distinguishing language, compiler & standard library versions, and about introducing the concept of vendors for compiler & standard library.

[Line 512] ## Alternative designs

I raised the concern of available alternatives for a large variety of pieces: detecting language features, compiler features, and library features.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

From @jmillikin at #3857 (comment)

@matthieu-m I expanded every review comment on this PR and didn't see any you posted. There's multiple RFCs related to conditional compilation depending on the compiler version, perhaps your comments were on a different one?

My concerns regarding conflating the version of Rust the language and rustc the compiler are not localized to any specific line in the file; if you'd prefer comments in the form of a per-line code review then I can do that, but the selection of the line would be ~arbitrary.

Comment on lines 486 to 525
### Pre-release

When translating `rustc --version` to a language version, we have several choices when it comes to pre-releases, including:
- Treat the nightly as fully implementing that language version
- Treat the nightly as not implementing that language version at all, only the previous
- Leave a marker that that language version is incomplete, while the previous language version is complete

In RFC 2523, this was left as an
[unresolved question](https://rust-lang.github.io/rfcs/2523-cfg-path-version.html#unresolved-questions).

The initial implementation treated nightlies as complete.
This was [changed to incomplete](https://github.com/rust-lang/rust/pull/72001) after
[some discussion](https://github.com/rust-lang/rust/issues/64796#issuecomment-624673206).
In particular, this is important for
- the case of package `bleeding-edge` starting to use a new feature behind `#[cfg(since)]` and package `nightly-only` has their toolchain pinned to a nightly before the feature was stabilized (to ensure consistent behavior of unstable features), package `nightly-only` cannot add or update their dependency on `bleeding-edge` without getting a "feature gate needed" error.
- bisecting nightlies.

This was [changed back to complete](https://github.com/rust-lang/rust/pull/81468) after
[some more discussion](https://github.com/rust-lang/rust/issues/64796#issuecomment-634546711).
In particular, this is important for
- keeping friction down for packages preparing for stabilized-on-nightly features as their `#[cfg(since)]`s can be inserted and "just work" which can be important for getting feedback quickly while the feature is easier to adapt to feedback that can be gained from these users
- releasing the package while its in this state puts it at risk to be broken if the feature is changed after stabilization

For RFC 2523, they settled on pre-releases being incomplete,
favoring maintainers to adopt stabilized-on-nightly features immediately
while letting people on pinned nightlies or bisecting nightlies to set a `-Z` to mark the version as incomplete.

In this RFC, we settled on translating `-nightly` to `-incomplete` because:
- Maintainers can adopt stabilized-on-nightly features with `#[cfg(since(rust, "1.100.0-0"))]` (the lowest pre-release for `1.100.0`), keeping friction low while explicitly acknowledging that the unstable feature may change
- `-0` is recommended over `-incomplete` or any other value as the exact pre-release value is unspecified.
- Allows build scripts to experiment with other logic when approximating the vendor version from the language version with less of a chance of needing to invoke `rustc` (e.g. detecting nightly)
- It provides extra context when approximating the vendor version from the language version when populating build information

We called the pre-release `-incomplete` to speak to the relationship to the language version.
Other terms like `partial` could as easily apply.
The term `-nightly` would be more discoverable but would convey more of a relationship to the vendor than the language.

As for differentiating between nightlies,
that corresponds more to the vendor version than the language version,
so we do not include that information.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

From @kornelski at #3857 (comment)

I don't think this comparison should support nightly versions. For purpose of this operator, a nightly Rust version should appear to have the last stable version preceding it (rustc "1.100-nightly" should act like "1.99").

This is because only the stable subset of features in nightlies can be expected to exist (hopefully) indefinitely in the future, and stable features can be checked using stable versions.

Not-yet-stable features don't have any guaranteed continuity, so a nightly feature available in "1.100-nightly" may not exist in "1.102-nightly", and may not even exist in "1.100" stable.

We have crates like ahash that are a ticking time bomb due to assuming that nightly features won't change in the future. Exposing nightly versions in this operator will encourage such assumptions.

For detecting what's in nightly Rust versions there could be a different operator, e.g. based on specific features and dates, checking "do you support turboencabulator feature the way it worked on 2026-01-01?".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

From @0xllx0 at #3857 (comment)

I don't think this comparison should support nightly versions. For purpose of this operator, a nightly Rust version should appear to have the last stable version preceding it (rustc "1.100-nightly" should act like "1.99").
This is because only the stable subset of features in nightlies can be expected to exist (hopefully) indefinitely in the future, and stable features can be checked using stable versions.
Not-yet-stable features don't have any guaranteed continuity, so a nightly feature available in "1.100-nightly" may not exist in "1.102-nightly", and may not even exist in "1.100" stable.
We have crates like ahash that are a ticking time bomb due to assuming that nightly features won't change in the future. Exposing nightly versions in this operator will encourage such assumptions.
For detecting what's in nightly Rust versions there could be a different operator, e.g. based on specific features and dates, checking "do you support turboencabulator feature the way it worked on 2026-01-01?".

I don't know if I understand your argument here. Nightly features are inherently unstable, but a specific Nightly release will obviously have all the features of that release. It may also be known that feature x exists between 1.y-nightly and 1.z-nightly. Of course, users could match against the specific feature like you suggest, but that could get very verbose, very quickly. Checking against nightly version ranges allows one to target the common set of unstable features in those ranges.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note that it is:

  • nightly or not
  • whether its included and the value is unstable

I was also annoyed with ahash and other packages that auto-enable nightly features and find the auto-enabling of nightly features to run counter to the spirit of nightly (unstable functionality is opt-in).

The focus here isn't on nightly detection but easing the way for adopting stabilized features. Yes, there is a risk that the feature may change. This is one of the reasons I added the pre-release marker compared to what T-lang had previously agreed to, to make it a more explicit opt-in. This will be up to T-lang who were previously convinced to not consider nightlies "complete" but were thoroughly convinced enough to go the other direction. If we find it becomes a problem, this can always be changed as the behavior is not specified.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In discussing this with T-lang today, there was interest in scoping this RFC down and that included punting on using pre-release for marking the language implementation is incomplete.

We ended up changing directions and going with a nightly is "incomplete".

I updated the RFC in 56ddfc3.

that corresponds more to the vendor version than the language version,
so we do not include that information.

## Alternative designs
Copy link
Contributor Author

Choose a reason for hiding this comment

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

From @kornelski at #3857 (comment)

Currently #[cfg()] has its own microsyntax, being a language inside a language, with its own design that is quite different from Rust's expressions. There's a proposal to move away from that, and make it behave more like normal expressions, using &&/|| instead of all()/any(). Syntactically, this will end up allowing everything matching macro expr inside cfg().

I think it would be great to generally move towards using a syntax that's more natural as Rust expressions, and use >= and < for comparisons.

#[cfg(rust_version >= "1.99")]
#[cfg(rust_version < "1.99")]

There's a proposal for #[stable(since = "semver")] or #[doc(since = "semver")] attribute: rust-lang/rust#74182 and there's #[deprecated(since = "v")] already. Use of >= operator avoids giving since multiple roles.

Copy link
Contributor Author

@epage epage Sep 24, 2025

Choose a reason for hiding this comment

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

From @tmandry at #3857 (comment)

What strikes me when considering @kornelski's comment above is that we haven't yet explored the idea of introducing a new type of cfg value for versions to the language.

This RFC introduces a new cfg predicate and a new check-cfg predicate to match. If we instead had typed cfg values we could redefine predicates like = to do something more specific for that type, in addition to defining additional predicates like >= and <.

The question is how to introduce values of a type. Following Rust, we can introduce them as version("1.85"). So on the command line you would say --cfg rust=version("1.85"), and in code you could write

#[cfg(rust >= version("1.85")]
#[cfg(rust < version("1.85")]

Again, following Rust, we could choose to allow comparison between a version on the left and a string literal on the right. Probably I would defer this for now, but could be convinced either way.

Beyond the syntactic improvements, this gives us some extra flexibility. For example:

  • I can write --check-cfg cfg(rust >= version("1.85")) and this defines the set of operators that are allowed with that value.

  • We can choose to enforce that all values of a cfg are the same type. Or in the case of versions, we can enforce that you only have one value to begin with.

  • As a future possibility we could have something like cratename = version("1.100+metadata") check for an exact match to +metadata on the LHS, while a more general requirement like cratename = version("1.100") would ignore any metadata that happens to exist on the LHS. Supporting this kind of comparison in the current RFC is less straightforward.

I still support the current RFC; I think it's better than anything we've considered before and it gets the details right. But this other direction is worth considering as a possibility. Using typed comparisons instead of operators on strings feels more Rusty to me.

We've already reserved space for cfg types with rustc's --cfg foo="bar" syntax that requires the double quotes, so we might as well make use of it. And of course, another future possibility is to add integer configs.

Currently #[cfg()] has its own microsyntax, being a language inside a language, with its own design that is quite different from Rust's expressions. There's a proposal to move away from that, and make it behave more like normal expressions, using &&/|| instead of all()/any(). Syntactically, this will end up allowing everything matching macro expr inside cfg().

For the record, I'm very much open to this direction. I don't think it cuts strongly against a comparison operator like since(), since that already looks like a function call.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

From @traviscross at #3857 (comment)

If we instead had typed cfg values we could...

What I'd really like eventually is to not have a DSL here at all and just use Rust within cfg attributes. We already have a Rust interpreter on hand -- consteval. Obviously nothing from the crate itself would be in scope.

Clearly I don't think cfg_version should wait for that. But when we start talking about typed cfg values, generic predicates, and, presumably therefore, type inference and type checking for cfg, it does make me wonder whether using Rust here and consteval might not be more straightforward after all.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've covered binary operators and types in 674ec04. This greatly enlarges the scope of this RFC which has me worried about this being available in a reasonable time frame and, if we go down this route, I wonder if we should have a short-term solution, like rust_version("1.95").

From @traviscross

What I'd really like eventually is to not have a DSL here at all and just use Rust within cfg attributes. We already have a Rust interpreter on hand -- consteval. Obviously nothing from the crate itself would be in scope.

Keep in mind that Cargo also parses and evaluates the syntax.

We'd also have to figure out how concepts like name-only cfg's translates to "rust".


With typing,
`cfg_values!` (a future possibility) could evaluate to the given type.
So for `--cfg foo=integer("1')`, `cfg_value!(foo)` would be as if you typed `1`.
Copy link
Member

Choose a reason for hiding this comment

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

Can't we write this as --cfg foo=1?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can also consider implicit types. I recorded this in be94add.


Another is how to handle check-cfg.
The proposed syntax is a binary operator but there is no left-hand side in check-cfg.
Would we accept `cfg(rust, values(>="1.95"))`?
Copy link
Member

Choose a reason for hiding this comment

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

My suggestion is to use cfg(rust >= version("1.95")).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Recorded this in 79f50aa

@epage epage changed the title RFC: #[cfg(since(rust, "1.95"))] for Rust-version conditional compilation RFC: #[cfg(version_since(rust, "1.95"))] for Rust-version conditional compilation Sep 24, 2025
@clarfonthey
Copy link

Since #2523 also mentions cfg(accessible(..)), I think this RFC should explicitly clarify that it's not commenting on that syntax, just the cfg(version(..)) syntax offered in that RFC. Since I do think that syntax is particularly useful for target-specific APIs that might have complex cfg attributes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
I-lang-radar Items that are on lang's radar and will need eventual work or consideration. T-compiler Relevant to the compiler team, which will review and decide on the RFC. 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.