-
Notifications
You must be signed in to change notification settings - Fork 1.6k
RFC: #[cfg(version_since(rust, "1.95"))]
for Rust-version conditional compilation
#3857
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
base: master
Are you sure you want to change the base?
Conversation
RE my assumptions around owning teams for this decision:
CC @rust-lang/compiler , @rust-lang/clippy , @rust-lang/cargo |
CC @Urgau as this touches a lot on |
# Drawbacks | ||
[drawbacks]: #drawbacks | ||
|
||
People may be using `--cfg rust` already and would be broken by this change. |
There was a problem hiding this comment.
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 reverts commit e51caf2.
I forgot where I landed on how to handle them
|
||
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
*but* maybe that would just be the start to natively supporting more types in `cfg`, | ||
like integers which are of interest to embedded folks. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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".
Co-authored-by: bjorn3 <[email protected]>
text/3857-cfg-version.md
Outdated
- 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 |
There was a problem hiding this comment.
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".
There was a problem hiding this comment.
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
.
Co-authored-by: Ralf Jung <[email protected]>
This comment was marked as duplicate.
This comment was marked as duplicate.
This comment was marked as duplicate.
This comment was marked as duplicate.
This comment was marked as duplicate.
This comment was marked as duplicate.
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 The question is how to introduce values of a type. Following Rust, we can introduce them as #[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 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
For the record, I'm very much open to this direction. I don't think it cuts strongly against a comparison operator like |
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 |
Semantics: | ||
- Language version: versioning of expected / defined behavior, based on the canonical compiler | ||
- Vendor name/version: identifying the specific compiler being used |
There was a problem hiding this comment.
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 asgccrs
? The current proposal treats therustc
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.
There was a problem hiding this comment.
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 asgccrs
? The current proposal treats therustc
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.
There was a problem hiding this comment.
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.
### 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. |
There was a problem hiding this comment.
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?".
There was a problem hiding this comment.
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 likeahash
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 supportturboencabulator
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 between1.y-nightly
and1.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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 ofall()
/any()
. Syntactically, this will end up allowing everything matching macroexpr
insidecfg()
.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 givingsince
multiple roles.
There was a problem hiding this comment.
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 likecratename = 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 ofall()
/any()
. Syntactically, this will end up allowing everything matching macroexpr
insidecfg()
.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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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".
text/3857-cfg-version.md
Outdated
|
||
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`. |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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"))`? |
There was a problem hiding this comment.
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"))
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Recorded this in 79f50aa
#[cfg(since(rust, "1.95"))]
for Rust-version conditional compilation#[cfg(version_since(rust, "1.95"))]
for Rust-version conditional compilation
Since #2523 also mentions |
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:
This supersedes the
cfg_version
subset of RFC 2523.