Skip to content

Stabilize #[cfg(version(...))], take 2 #141766

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

est31
Copy link
Member

@est31 est31 commented May 30, 2025

Stabilization report

This proposes the stabilization of cfg_version (tracking issue, RFC 2523).

What is being stabilized

Permit users to cfg gate code sections based on the currently present rust version.

#[cfg(version("1.87"))]
pub fn from_utf8_unwrap(buf: &[u8]) -> &str {
    str::from_utf8(buf).unwrap()
}

#[cfg(not(version("1.87")))]
pub fn from_utf8_unwrap(buf: &[u8]) -> &str {
    std::str::from_utf8(buf).unwrap()
}

Tests

cfg-version-expand.rs: a functional test that makes rustc pretend to be 1.50.3, then tries with 1.50.0, 1.50.3, and 1.50.4, as well as other version numbers.

syntax.rs: tries various ways to pass wrong syntax to cfg(version):

  • The expected syntax is #[cfg(version("1.20.0"))]
  • small shortenings like #[cfg(version("1.20"))] are allowed, but #[cfg(version("1"))] is not
  • postfixes to the version, like #[cfg(version("1.20.0-stable"))] are not allowed
  • #[cfg(version = "1.20.0")] is not supported, and there is a warning of the unexpected_cfgs lint (but no compilation error)

assume-incomplete.rs: another functional test, that uses macros. It also tests the -Z assume-incomplete-release flag added by #81468.

wrong-version-syntax.rs ensures that check_cfg gives a nice suggestion for #[cfg(version("1.2.3"))] when someone tries to do #[cfg(version = "abc")].

Development of the implementation

The initial implementation was added by PR #71314 which used the version_check crate.

PR #72001 made cfg(version(1.20)) eval to false on nightly builds with version 1.20.0, upon request from the lang team. This decision was pushed back on by dtolnay in this comment, leading to nikomatsakis reversing his decision.

Ultimately, a compromise was agreed upon, in which nightly releases are treated as "complete" i.e. cfg(version(1.20)) evals to true on nightly builds with version 1.20.0, but there is a nightly flag -Z assume-incomplete-release to opt into the behaviour that doesn't do this assumption. This compromise was implemented in PR #81468.

PR #81259 made us adopt our own parsing code instead of using the version_check crate.

PR #141552 pulled out the syntactic checks from the feature gate test into its own dedicated test.

PR #141413 made #[cfg(version)] more testable by making it respect RUSTC_OVERRIDE_VERSION_STRING.

Prior stabilization attempts were #64796 (comment) and #141137.

cfg_has_version

In the course of the earlier stabilization attempt, it came up that due to the way #[cfg(version)] uses "new" syntax, one can only adopt it if the MSRV includes the version that stabilized #[cfg(version)]. So it won't be immediately useful: For a long time, many crates will still use the alternatives that #[cfg(version)] meant to displace, until the stabilization of #[cfg(version)] was sufficiently long ago.

In order to solve this, cfg_has_version was proposed: a builtin, always true cfg variable. Ultimately, the lang team decided in #141401 to not immediately include cfg_has_version into the stabilization (#141137 included it), but go via a proper RFC instead. Implementation wise, cfg_has_version is not hard to implement, but semantically, a cfg variable is not a small deal, it will be present everywhere, e.g. in rustc --print cfg.

There is no such thing as unstable cfg variables (and even if there were, it would counteract the purpose of cfg_has_version), so its addition would have an immediate-stable effect.

In a couple of months to a couple of years, this will not be a problem, as the MSRV of even slower moving projects like serde gets bumped every now and then. We probably feel the desire for cfg_has_version the strongest directly after the stabilization of #[cfg(version)], then it decreases monotonically.

Unresolved questions

Should we lint for cfg(version) probing for a compiler version below the specified MSRV? Part of a larger discussion on MSRV specific behaviour in the Rust compiler. It feels like it should be a rustc lint though instead of a clippy lint.

Future work

The stabilization doesn't close the tracking issue, as the #[cfg(accessible(...))] part of the work is still not stabilized, currently requiring an implementation (if an implementation is something we'd want to merge in the first place).

We also explicitly opt to treat cfg_has_version separately.

TODOs before stabilization

@rustbot
Copy link
Collaborator

rustbot commented May 30, 2025

r? @eholk

rustbot has assigned @eholk.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels May 30, 2025
@traviscross traviscross changed the title Stabilize #[cfg(version(...))] frfr Stabilize #[cfg(version(...))], take 2 May 30, 2025
@traviscross traviscross added T-lang Relevant to the language team needs-fcp This change is insta-stable, or significant enough to need a team FCP to proceed. S-waiting-on-documentation Status: Waiting on approved PRs to documentation before merging I-lang-nominated Nominated for discussion during a lang team meeting. P-lang-drag-1 Lang team prioritization drag level 1. https://rust-lang.zulipchat.com/#narrow/channel/410516-t-lang and removed T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels May 30, 2025
@traviscross
Copy link
Contributor

I'll highlight for our awareness that this test passes:

//@ run-pass
//@ rustc-env:RUSTC_OVERRIDE_VERSION_STRING=1.86.0

#![feature(cfg_version)]

fn main() {
    assert!(cfg!(not(version("1.85.65536"))));
}

That is, we're exposing that "1.85.65536 > 1.86.0". I don't really love that, in terms of specifying the language, but I understand the motivation for it. Probably we should make sure to say in the Reference that the behavior when the version string does not conform to the current requirements is unspecified and may change in the future.

@traviscross
Copy link
Contributor

This all still looks right to me. Inclusive of taking the normative position that the behavior of cfg(version("..")) with unsupported version literal strings is unspecified and may change in the future, I propose that we do this.

The best day to add cfg(version("..")) was yesterday. The second best day is today.

@rfcbot fcp merge

@rfcbot
Copy link
Collaborator

rfcbot commented May 30, 2025

Team member @traviscross 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 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 May 30, 2025
@eholk
Copy link
Contributor

eholk commented May 31, 2025

That is, we're exposing that "1.85.65536 > 1.86.0".

What's the reason for this? Is it just an accident of implementation, or is there a use case for it?

@traviscross
Copy link
Contributor

If we change the versioning scheme in the future -- we start naming our versions "25-06" or something -- then you'd want older versions of Rust to just accept those and assume they must be from the future. So it treats parse errors as "must be from the future". It's parsing each segment into a u16, and so "1.85.65536 > 1.86.0".

@est31
Copy link
Member Author

est31 commented May 31, 2025

yeah, if you do rust-version = "1.80.10000000000000000000000000000000000000" in Cargo.toml, it will also error.

error: expected a version like "1.32"
  --> Cargo.toml:5:16
   |
11 | rust-version = "1.80.10000000000000000000000000000000000000"
   |                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |

It's possible to use bignums to fix this but I'd say it's a non-issue: even u16 gives a millenium worth of releases (at the current cadence). And we can always extend it in the future should we anticipate an increase in velocity.

@PoignardAzur
Copy link
Contributor

PoignardAzur commented May 31, 2025

That is, we're exposing that "1.85.65536 > 1.86.0".

Seems like something that would deserve at least a warn-by-default lint, if not deny-by-default?

(Although that's not a blocker for stabilization, the lint can always be added later.)

@traviscross
Copy link
Contributor

It's possible to use bignums to fix this but I'd say it's a non-issue: even u16 gives a millenium worth of releases (at the current cadence). And we can always extend it in the future should we anticipate an increase in velocity.

Rather than bignum, what I had nearly proposed (before deciding I was OK with how it is) was to saturate anything matching [0-9]+ and greater than u16::MAX.

@traviscross
Copy link
Contributor

traviscross commented May 31, 2025

Seems like something that would deserve at least a warn-by-default lint, if not deny-by-default?

It does warn if the version string literal does not parse.

@est31
Copy link
Member Author

est31 commented Jun 2, 2025

Yeah, it's a builtin warning, without a way to opt out. Which is not convenient to deal with if you face it, but this also serves as a good deterrent, better than a warn-by-default lint. IF a new versioning scheme is being introduced, hopefully most of the population will be on newer compilers that either recognize the versioning scheme, or it has been turned into a lint at that point.

That at least was my original reasoning why I made it into a warning and not a lint. In any case, warning or lint, we don't lock ourselves in in any way due to this stabilization, even if we turn it into an error-by-default lint let's say.

@nikomatsakis
Copy link
Contributor

@rfcbot reviewed

@rfcbot rfcbot added final-comment-period In the final comment period and will be merged soon unless new substantive objections are raised. and removed proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. labels Jun 4, 2025
@rfcbot
Copy link
Collaborator

rfcbot commented Jun 4, 2025

🔔 This is now entering its final comment period, as per the review above. 🔔

@scottmcm
Copy link
Member

scottmcm commented Jun 4, 2025

@rfcbot reviewed

@jhpratt
Copy link
Member

jhpratt commented Jun 5, 2025

yeah tbh if I had a time machine, I'd suggest we use some other syntax for feature. maybe worth to think about for the 2027 edition, idk.

For what it's worth there was discussion in rust-lang/rfcs#3796 to permit feature("foo"), which I intend to submit as a follow-up RFC. My thoughts for the follow-up would be to permit it for all key-value pairs, which would necessarily include whatever is decided as the outcome of this.

@Urgau
Copy link
Member

Urgau commented Jun 5, 2025

@epage The only way #[cfg(rust_version = "...")] could work is if rustc dynamically added every the set of every versions less and equal to the current one, that is for Rust 1.86 we would have:

  • rust_version = "1.86.0"
  • rust_version = "1.85.1"
  • rust_version = "1.85.0"
  • ...
  • rust_version = "1.0.0"

Regarding --check-cfg and the unexpected_cfgs lint, it would depend on what we would want:

  • if we want to be "forward" compatible, than it's going to be a issue, since we won't know if 1.91.3 will exist or not
    • one possibility here would be disabling checking for rust_version
  • if we are okay warning on rust_version = "1.86.0" from Rust 1.84, then it's not going to be an issue, check-cfg can "just" re-use the same list of versions

@epage
Copy link
Contributor

epage commented Jun 5, 2025

The only way #[cfg(rust_version = "...")] could work is if rustc dynamically added every the set of every versions less and equal to the current one, that is for Rust 1.86 we would have:

Except if we release 1.85.2 then 1.86.0 won't know about it.

@jhpratt
Copy link
Member

jhpratt commented Jun 5, 2025

Perhaps a silly question, but what is the use case for wanting to gate something on a point release?

@tmandry
Copy link
Member

tmandry commented Jun 5, 2025

@rfcbot concern let's take rust_version seriously

Having given this some thought, I think #[cfg(rust_version = "...")] is the best overall way to spell this feature. I think we should consider it carefully, hearing agreement on this point from both @m-ou-se and @nikomatsakis. As they have said, it matters in a practical sense because its users will be able to use it years earlier; this feature is first and foremost a practical tool for those users, so I think this is important. The second reason is that it mirrors the way the = syntax works in cfg(feature); if we want this to work differently, we should change the way both of them work in the future.

While I agree that the = can be a little confusing, I also don't think that's an overriding concern. Experience shows that people who use cfgs are already able to tolerate this; it's "just the way you spell it". Of course, we don't have to be stuck with this choice forever: I would welcome a later proposal, from @jhpratt or someone else, that offers a more intuitive way of writing both cfg(rust_version) and cfg(feature) together. These niceties would only be available on newer Rust MSRVs, but that is how syntax improvements usually go.

Previously I had wanted to use something like #[cfg(has_cfg_version)] to make this available to crates with earlier MSRVs, so we could sidestep the syntax questions. But as @joshtriplett pointed out in #141137 (comment), there's not a good way to invert a config that uses it. You frequently need to do this, as you need to provide two different version of an item depending on a cfg. Instead of providing two versions, you now would need to provide three:

#[cfg(has_cfg_version)]
#[cfg(version("1.80"))]
fn foo() {}

#[cfg(not(has_cfg_version))]
fn foo() {}

#[cfg(has_cfg_version)]
#[cfg(not(version("1.80")))]
fn foo() {}

That's actually quite important, and something I had missed in our earlier discussion on this. It means that there isn't, in my view, a satisfactory way to retrofit something like has_cfg_version on top of the version(...) syntax.

So as much as I would like to get this feature today, I don't think we should rush it out the door. As this discussion and previous discussions have shown, there have been more design concerns hiding underneath the question of whether we can ship cfg(version) at all. I want us to take a reasonable amount of time to get those questions right.

I'm also happy to put time in to writing an RFC, FCP, or whatever's needed and drive consensus on it – and having raised a concern, driving this will be a priority for me. On that note, a huge thanks to @est31 for being willing to roll with the punches as we work out the issues here.

@rfcbot rfcbot added proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. and removed final-comment-period In the final comment period and will be merged soon unless new substantive objections are raised. labels Jun 5, 2025
@Urgau
Copy link
Member

Urgau commented Jun 5, 2025

Except if we release 1.85.2 then 1.86.0 won't know about it.

There is no reason Rust 1.86 wouldn't know about it, when we would do a stable-point release we would backport that knowledge to the current stable/beta/nightly releases, technically 1.86.0 would still not know about it, while 1.86.1 would, but that seems fine to me. It's also a unlikely situation as we typically don't support previous stable versions.

This also assumes that we do want to distingues about the point releases, we could also simply have: rust_version = "1.85".

@traviscross
Copy link
Contributor

traviscross commented Jun 5, 2025

As they have said, it matters in a practical sense because its users will be able to use it years earlier...

One of the points that's been made in this thread that stands out strongly to me is the potentially limited window of utility for not giving an error in earlier versions. If you're maintaining an MSRV of 1.48 while also using build.rs-based version or feature detection to support features that we shipped in e.g. Rust 1.55, 1.68, and 1.85 -- which seems like a common pattern -- and we ship cfg(rust_version = "..") in Rust 1.89, then this new mechanism still doesn't really help until you bump your MSRV to 1.85. So we would have maybe only bought these people a few versions.

If doing something like this were a big help to people with 1.48 MSRVs today, then that would be one thing. But I sense that it's probably not.

In any event, there is, I think, more work for us to eventually do here. E.g., I'd like to see us add a cfg(edition("..")) as well. But it seems OK to me to ship what's here and do this other work later. If we block this and send it back for a cycle of reviewing and maybe accepting a new RFC, addressing the sort of implementation concerns that are being raised in this thread, etc., I just feel like we're going to have likely lost more than we actually gained.

@tmandry
Copy link
Member

tmandry commented Jun 6, 2025

I think it's a solid point @traviscross. I haven't done a comprehensive analysis, but in the two crates I looked at we might gain only a year; perhaps less, if we take too long.

There are also a couple of things that push me in the other direction:

  • Old-MSRV crates might use this, for desirable things like adding #[diagnostic] attributes, if the cost of doing version-dependent configs weren't as high as adding a build script.
  • Old-MSRV crates can still use cfg(rust_version = "..") to switch on newer versions, even if they have build scripts doing the old thing to switch on older versions. Doing this doesn't buy much in terms of compile times, but it does make their lives easier, and it makes it clearer to maintainers when those crates can drop the build script altogether.

Certainly there is still time pressure to maximize the value of the thing we ship. I just don't want that to be the overriding theme when there are legitimate design concerns, especially if they can (arguably) have a similarly sized impact.

@est31
Copy link
Member Author

est31 commented Jun 6, 2025

then this new mechanism still doesn't really help until you bump your MSRV to 1.85.

Thanks @traviscross for putting it in better words than what I wrote in #141766 (comment)

I'd say the major advantage of cfg(rust_version="...") is new stabilizations: those can immediately use cfg(rust_version="..."). With cfg(version(...)), they'd need to first keep using build.rs and then once the MSRV advances to the version that added cfg(version(...)), they can use it. This means that with increasing MSRV over time, there is two migration steps involved for code: first to abandon the build.rs, then later to abandon the cfgs. With cfg(rust_version="...") it's only the latter. Those two steps might be months and years apart, but still.

So cfg(rust_version="...") will have some utility from the get go, but the big returns will also come after years, similar to cfg(version(...)).

@traviscross
Copy link
Contributor

traviscross commented Jun 6, 2025

Certainly there is still time pressure to maximize the value of the thing we ship. I just don't want that to be the overriding theme when there are legitimate design concerns, especially if they can (arguably) have a similarly sized impact.

For me, my overriding sensation isn't time pressure to ship, but simple humility. The tradeoffs we're discussing today were also largely discussed at the time of RFC acceptance and would have been reviewed at the time of the original stabilization attempt. Many good and reasonable points were raised, and the team seems to have acted reasonably in making adjustments to take those into account, leading to what's before us today and what would likely have been stabilized in Rust 1.53 had it not been for the concern about stabilizing cfg_accessible first.

It's just not yet clear to me, in this case, that much has changed or that we're certain to do better. It seems within the realm of the possible that we go through another round and come back with the same design. Conversely, the cost of shipping this design today and making any later extensions or adjustments, if we were to decide we wanted them, seems low.

My humility extends as well to our team bandwidth. We have a lot of important work stacked up. Perhaps I just don't have that much appetite for putting this one back into our queue.

@workingjubilee
Copy link
Member

workingjubilee commented Jun 6, 2025

If you were to adopt a syntax using =, I don't see why you would adopt a syntax that doesn't resemble the kind used by most package managers for SemVer constraints, e.g. <= or >=

@est31
Copy link
Member Author

est31 commented Jun 7, 2025

Cargo already uses = though, at least for >95% of dependency specifications out there, which should be stronger precedent than stuff like PEP 621.

@workingjubilee
Copy link
Member

eh, that's okay I guess.

having drafted a PR using it, I guess cfg(rust_version = "...") is nice enough.

  • It is definitely true that it is "supported" now. Like, now-now. I can send a PR based on something that I have put in a PR to switch on to breaking in "CURRENT_RUST_VERSION"... so 1.89, probably... and the cfg evaluates correctly.
  • It does disambiguate between the notion of the crate and the compiler version. That's nice.

However, I do not understand the current implementation treating nightly versions as "complete".

I understand dtolnay's opinion re: nightly versions being treated as complete as "providing bad DX", but I believe that flags like the one currently implemented in rustc... the ability to "opt-out" of the completeness assumption... should have their polarity run the other way. If you want to opt in to a feature immediately, as in the case described, you are more motivated to learn how to make that work. We can even have check_cfg-style linting on using the version without passing that flag if we want, which means we can skip letting library authors "learn how to make it work" by just telling them.

Far more people are going to compile code instead of write it, so telling every distro and every dependent to add this flag whenever they run the nightly compiler seems strictly worse than guiding upstreams... the ones interactively using the compiler repeatedly... to the thing they want. I also think library authors would rather cfg(rust_version) lag in this way so that they do not get spurious reports from users of nightly compilers who have already had their problem fixed if they would just update their nightly. And essentially every stabilization of a feature would come with that.

And yes, I understand the opinion regarding version pinning needing to be propagated, I have said as much myself, but I will spare everyone the much longer dissertation on how that simply doesn't fully add up and unnecessarily removes degrees of freedom.

@traviscross
Copy link
Contributor

traviscross commented Jun 8, 2025

However, I do not understand the current implementation treating nightly versions as "complete".

How nightly compilers interpret this syntax is not, I'd suggest, anything to which we necessarily need to commit in this stabilization. We could always change that without breaking any stability guarantees.

@tmandry
Copy link
Member

tmandry commented Jun 11, 2025

The tradeoffs we're discussing today were also largely discussed at the time of RFC acceptance and would have been reviewed at the time of the original stabilization attempt.

I'm generally sympathetic, and I am quite happy applying this argument to many of the questions settled in the RFC. It considered quite a large design space, including things like whether we should stabilize feature flags instead of using versions. Even if we want those things, I think we should do them after the functionality we already agreed to.

I don't think the specific syntax was ever settled though. It's true that this issue was raised on the original RFC – by the maintainer of the libc crate, 3 days before FCP ended – and the result is actually that the RFC author said we should reconsider the syntax ahead of stabilization. We also see an unresolved question about the = syntax on the tracking issue. However, this question largely went undiscussed until the recent stabilization attempt in #141137 a few weeks ago.

There is a big picture argument that I think also applies. This RFC was written in 2018; since then, Rust's success and its proliferation in places like Linux distros has made maintaining an MSRV much more common than in extreme cases like libc. Rust has also shipped a lot of useful features. So I think some of the qualitative tradeoffs have shifted in that time. Of course, if we had stabilized this feature at exactly the time the RFC was accepted it wouldn't matter, but it does now.

Note: The point about the unresolved syntax question makes me wonder whether there should be a new RFC to consider the rust_version = ".." syntax. I am happy to write one, and having sat down to draft it I have easily found enough content to fill one out. So my preference is still to open an RFC sometime this week, framed as a either a refinement or alternative to the version part of the original RFC. But it's worth noting that we technically have had this open question lying around the entire time, and could choose to resolve it in another way.

@joshtriplett
Copy link
Member

FWIW, while I think there are several reasons not to switch the syntax, I don't think "momentum" should be one of them. If we actually think there's a better alternative, we should consider it. I think the important question is whether the alternative is actually better.

Also, orthogonally to the question of whether we should use =, I do think using the identifier rust_version instead of version is a small but appreciable improvement. cfg(rust_version("...")) seems reasonable.

@m-ou-se wrote:

So cfg(version) referring to the Rust language version instead of the crate version seems a bit inconsistent. cfg(version) might reasonable refer to the crate version, a platform version, a protocol version, an (alternative) compiler version, etc. But cfg(rust_version) very clearly refers to the version of the Rust language. rust-version is also the key we use in Cargo.toml.

I agree with this.

@m-ou-se wrote:

#[cfg(version(".."))] results in an error in any current and past stable Rust compiler, which means you'll have to set your MSRV to 1.89.0 to use this. That makes the feature a lot less useful.

I'm not excited to see #[cfg_attr(rust_1_89, cfg(version("..")))] in the wild. (With a build.rs to set --cfg=rust_1_89.)

I wouldn't expect to see that. I'd expect a project to wait until their MSRV allows cfg(version(...)), and until then keep using build.rs.

If we use #[cfg(rust_version = "1.90.0")] as syntax instead, it'll just be skipped on older versions, which is exactly what you'd want and expect.#[cfg(not(rust_version = "1.90.0"))] and so on would all also work as expected.

I see. You're arguing that, as long as current versions emit a warning saying that cfg(not(rust_version = "1.60")) will give incorrect results, it's OK that older compilers (e.g. 1.62) will give an incorrect result for that?

That seems like we'd be deliberately choosing more error-prone behavior, in order to gain partial backwards compatibility for the sole benefit of programs that want to detect newer rust versions than their MSRV but don't need to detect rust versions between their MSRV and when we ship this version detection mechanism. Any program that wants to have behavior for e.g. "1.62 or newer" or "older than 1.62" would still need a build script.

The only counter-arguments for the = syntax I've heard seem weak. One of the arguments is that we're not testing for equality so shouldn't use =. However, the = in cfg() has never stood for equality. It's the same thing as in cfg(feature = "bla"), to check if a feature is included. In the same way, it's fair to say that Rustc 1.90 has 1.89 (and all previous versions) included (because we're backwards compatible), so it seems very reasonable that cfg(rust_version = "1.89.0") matches even on 1.90, just like feature = "a" matches even if both the a and b features are enabled.

I will observe that, in other discussions, we've been talking about moving in the other direction, and writing feature("...").

jhpratt wrote the same thing here: #141766 (comment)

@est31 wrote:

Edit: also, the further back you go with your MSRV, the more likely folks will want to use features of compilers that don't have cfg_version yet, starting with the compiler that stabilized that feature. So say the project has an MSRV of 1.48.0. Then it maybe wants to use a feature from 1.61.0 conditionally, but it doesn't want to have the users of compilers 1.61.0 to 1.88.0 not enjoy the feature, so it can't use #[cfg(version(...))] for that feature, but needs to do version detection for that feature. So even if it could adopt #[cfg(version(...))] for something stabilized on 1.90.0, it doesn't get to enjoy the benefit of not having version detection: there is still a build.rs script.

This is exactly what I'm getting at. The cfg(rust_version = "...") syntax would only have a benefit for code that has an old MSRV but only wants to distinguish more recent compilers, and doesn't care about distinguishing between compilers before the version that ships this feature.

@traviscross wrote:

One of the points that's been made in this thread that stands out strongly to me is the potentially limited window of utility for not giving an error in earlier versions. If you're maintaining an MSRV of 1.48 while also using build.rs-based version or feature detection to support features that we shipped in e.g. Rust 1.55, 1.68, and 1.85 -- which seems like a common pattern -- and we ship cfg(rust_version = "..") in Rust 1.89, then this new mechanism still doesn't really help until you bump your MSRV to 1.85. So we would have maybe only bought these people a few versions.

This is exactly my expectation. A crate (and the crates that depend on it) don't get a substantive benefit until the build.rs can go away.


To be explicitly clear, if everyone else prefers the rust_version = "..." syntax, so be it. I don't think it's the right syntax, but I think shipping one of these syntaxes is something we should do soon, so that people can start using it.

@traviscross
Copy link
Contributor

traviscross commented Jun 12, 2025

Some thoughts.

One, it influences me here that there's an existing way to do this (with build.rs), that the existing mechanism will continue to work for people, that it works correctly in earlier versions whereas anything we do here will be at best ignored, and that I suspect people will keep using that existing solution (due to its correctness and the limited gains of using this instead) until they can bump their MSRVs enough to move off of it.

This isn't a case where, in focusing on what's best for the language going forward, we'd be leaving people in a lurch. People will continue to do what they've been doing successfully, and it will work just as well as it always has.

Two, the cfg(feature = "..") syntax seems to stand out to many of us as a bit odd and surprising (we all get used to it, of course). I'm not sure I want to use that as the model to expand on, and I'm interested in seeing the RFC from @jhpratt to analyze whether maybe we could move to cfg(feature("..")). I'd want to consider that RFC before really digging into this cfg(rust_version = "..") question.

Three, my reaction to the rust_ prefix is somewhat negative. In Reference text, we try to avoid saying, "in Rust", because of course that's the default everywhere. I feel similarly about this (and would as well about prefixing cfg(edition(".."))). If it's in the language in this kind of place, and it doesn't say otherwise (as with os_version or similar) then "of course it's the Rust version (or edition)."

We haven't yet used rust in basic language tokens -- at least none immediately come to mind (though we do use it, e.g. to name the edition preludes) -- and I wonder about whether that might be a desirable property to uphold. For related reasons, there was discussion about using lang_version instead when this originally came up, starting at #64796 (comment), around the time of the original (2021) stabilization attempt.

The main upside I see to something like lang_version / lang_edition is if we wanted to go forward on the RFC 3239-style cfg shorthands like cfg(target(os = "..", env = "..")) (#96901), as then due to the shared prefix, we could likewise imagine cfg(lang(version(".."), edition(".."))), and that might have an appealing kind of hierarchical symmetry, but I doubt that'd be particularly useful in this case, and we very well might not do these shorthands anyway (#130780).

The one place I could maybe see using rust_ as a prefix is if we were using it to reserve a namespace, e.g. for cfg options. But that's not really the case here.

@joshtriplett
Copy link
Member

@traviscross Fair enough.

For my part, I'd happily sign off on either rust_version(...) or version(...), without preference between them. I would, unhappily, sign off on rust_version = "..." if everyone else prefers that (but it sounds like others don't prefer that either).

@tmandry
Copy link
Member

tmandry commented Jun 12, 2025

This is exactly what I'm getting at. The cfg(rust_version = "...") syntax would only have a benefit for code that has an old MSRV but only wants to distinguish more recent compilers, and doesn't care about distinguishing between compilers before the version that ships this feature.

There are likely low-MSRV crates that would use version gating but for the fact that it requires adding a build script. A good example of this is #[diagnostic] attributes. Those can give end users a quality of life improvement, but at the cost of slower builds and higher maintenance burden if the crate doesn't have a build script already.

If we stabilize rust_version = ".." they can just gate the attribute based on the version where rust_version stabilizes. This is less precise than you might want but I think it's fine; the vast majority of active Rust development happens using a recent stable compiler from rustup, not whatever a distro might ship that we need to maintain compatibility with.

For the crates already using build scripts, eventually the old features will move outside the MSRV window. Then the crate author will be able to remove their build script altogether. rust_version = ".." allows this to happen earlier.

I wouldn't expect to see that. I'd expect a project to wait until their MSRV allows cfg(version(...)), and until then keep using build.rs.

Interesting! My thought is that adding a flag in one place is easier than adding it in three (two of them in build.rs: One for the detection and one for check-cfg), and keeping it out of the build script makes it easier to see the reasons why you're still maintaining a build script. But other crate authors might prefer to keep everything as consistent as possible.

I see. You're arguing that, as long as current versions emit a warning saying that cfg(not(rust_version = "1.60")) will give incorrect results, it's OK that older compilers (e.g. 1.62) will give an incorrect result for that?

This is exactly what I was thinking. The Rust survey shows that >90% of development is on current stable, so I think we will mitigate any misuse quite well with that warning.

That seems like we'd be deliberately choosing more error-prone behavior, in order to gain partial backwards compatibility for the sole benefit of programs that want to detect newer rust versions than their MSRV but don't need to detect rust versions between their MSRV and when we ship this version detection mechanism. Any program that wants to have behavior for e.g. "1.62 or newer" or "older than 1.62" would still need a build script.

In practice, I don't think it's more error prone than other uses of cfg. Any time you use cfg, you should verify that your code works as expected on all the variations you support. We'll have a lint that catches misuse anytime you build with a newer compiler. And it's much easier to add compiler versions to your CI than it is to test many other uses of cfg.

Saying that we shouldn't do it because those versions don't implement a lint strikes me as an unfair comparison. This is a feature whose purpose is to allow compatibility with older compiler versions. On older versions of the compiler we don't have check-cfg or updated lints that catch other kinds of misuse, because of course we can't go back and add them.

So the choice is between whether we want something that works at all on those older versions, or whether we want something that doesn't work but is impossible to misuse. When I think of it that way, and include the fact that we can make misuse very unlikely with a lint, the choice seems clear.

@tmandry
Copy link
Member

tmandry commented Jun 12, 2025

This isn't a case where, in focusing on what's best for the language going forward, we'd be leaving people in a lurch. People will continue to do what they've been doing successfully, and it will work just as well as it always has.

I think of it this way: The pressing motivation to stabilize this is to allow more crates to move off of build scripts as soon as possible so that we can improve build times. The most effective way to do that is to choose a syntax that is compatible with old MSRVs, so that we don't force the use of build scripts on old-MSRV crates even when there are no version gates past the stabilization of this cfg.

I'm interested in seeing the RFC from @jhpratt to analyze whether maybe we could move to cfg(feature("..")).

Me too. I'd be happy to consider it.

I'd want to consider that RFC before really digging into this cfg(rust_version = "..") question.

We can support rust_version("..") alongside rust_version = "..", just as we would support feature("..") alongside feature = "..". If we're really happy with the new syntax, we can remove support for both over an edition.

We haven't yet used rust in basic language tokens -- at least none immediately come to mind (though we do use it, e.g. to name the edition preludes) -- and I wonder about whether that might be a desirable property to uphold. For related reasons, there was discussion about using lang_version instead when this originally came up, starting at #64796 (comment), around the time of the original (2021) stabilization attempt.

I can see where you're coming from, but I think our hand might be forced already. Personally, if I consider those two names in isolation, I would rate them about the same. The thing that pushes me in the direction of rust_version is that it's already what Cargo uses.

I have trouble seeing how picking lang_version would be worth maintaining inconsistency between Cargo and the language, or worth a migration on the Cargo side. If the Cargo team is up for it I wouldn't block, but what would go in the blog post where we explain this rename to users?

Part of this is that I don't think it's better in a practical sense. The justification in the RFC thread you linked discussed a fork. Putting aside whether we want to plan for that, a fork could rename rust_version to whatever they wanted. Probably they would want to continue supporting it for legacy code, but add a separate newrust_version to match version numbers for the new language. I think lang_version would actually be more confusing in that regard: you would now have two different versioning schemes, overlapping version numbers, and so on.

@traviscross
Copy link
Contributor

I have trouble seeing how picking lang_version would be worth maintaining inconsistency...

I'd pick version and edition, not lang_version and lang_edition.

The thing that pushes me in the direction of rust_version is that it's already what Cargo uses...

It appears in Cargo.toml and is interpreted by the Cargo build tool. It doesn't surprise me that they wanted to clarify that they were referring to the Rust version. The presumption that an unqualified thing is referring to Rust is less strong in their case.

The situation for something in the language itself is different, for me, and I don't find myself compelled to mirror what they've done, just as we've departed in the language from choices of our other tools in other cases. Our situation, in the language, is just different.

@traviscross
Copy link
Contributor

traviscross commented Jun 13, 2025

The thing that pushes me in the direction of rust_version is that it's already what Cargo uses...

Cargo also uses edition, not rust-edition, but I struggle to imagine that in the language we would go with cfg(rust_version("..")) but then cfg(edition("..")) rather than, in that world, cfg(rust_edition("..")).

So I'd anticipate that we're going to break from Cargo here in one place or the other.

@joshtriplett
Copy link
Member

joshtriplett commented Jun 13, 2025

It appears in Cargo.toml and is interpreted by the Cargo build tool. It doesn't surprise me that they wanted to clarify that they were referring to the Rust version. The presumption that an unqualified thing is referring to Rust is less strong in their case.

In the case of Cargo, an unqualified version would have been confused with a package version; the debate was between e.g. rust-version and min-rust-version and msrv and similar.

@joshtriplett
Copy link
Member

joshtriplett commented Jun 13, 2025

In practice, I don't think it's more error prone than other uses of cfg. Any time you use cfg, you should verify that your code works as expected on all the variations you support. We'll have a lint that catches misuse anytime you build with a newer compiler.

That's not the only thing I mean by "error-prone", here. By way of example, rust_version = "..." will give a warning on every compile on many old versions of Rust. That contributes to warning fatigue, which makes it all the more likely that the user will miss important warnings. It also means their MSRV-testing CI job either needs to not use -D warnings, or the user needs to suppress that warning somehow to keep the build warning-clean.

They can't necessarily suppress it with check-cfg, since we didn't add support for that in Cargo.toml until more recently, and before that, setting it requires a build script, which we've been trying to help them remove. And, they might be tempted for simplicity to just suppress it with allow(unexpected_cfgs), but this doesn't work:

#[allow(unexpected_cfgs)] // This doesn't work, the warning still appears
#[cfg(rust_version = "...")]
fn item() { ... }

(And even if that did work, it would also suppress the warning inside the item.)

What does work is #![allow(unexpected_cfgs)] at the top of the module, which is error-prone as it suppresses the warning for other unexpected cfgs.

So, in a variety of ways, using cfg(rust_version = "...") because it's only a warning in old Rust versions that don't understand it is more error-prone, in ways that a lint in current stable doesn't help with at all.

I also find it quite motivating to observe that, in a few years, the versions that don't support cfg(version(...)) will have aged out of use.

@tmandry
Copy link
Member

tmandry commented Jun 13, 2025

They can't necessarily suppress it with check-cfg, since we didn't add support for that in Cargo.toml until more recently, and before that, setting it requires a build script, which we've been trying to help them remove.

I don't think this is right, are you sure? The two mechanisms were announced together: https://blog.rust-lang.org/2024/05/06/check-cfg/, and this was after going through compiler and cargo FCPs at around the same time. (Actually, looks like the blog post was edited to include the Cargo piece two weeks after it posted, but that's still within a release cycle.)

That's not the only thing I mean by "error-prone", here. By way of example, rust_version = "..." will give a warning on every compile on many old versions of Rust. That contributes to warning fatigue, which makes it all the more likely that the user will miss important warnings. It also means their MSRV-testing CI job either needs to not use -D warnings, or the user needs to suppress that warning somehow to keep the build warning-clean.

I would expect a user to have an MSRV-testing job; I would not expect them to use -D warnings on it. The point, to me, of such a job is to make sure it continues to work in the deps of people using the MSRV. Warnings get ignored in deps so they can be ignored in this job. If the warning does matter it will likely show up in your latest stable builds too, so you might as well just focus on warnings from the latest toolchain.

I suppose if you want to be conservative you might look at warnings that only occur in your MSRV just in case they matter in only that version. I still don't think -D warnings is a great policy there. If it's true that there would be warnings for unknown cfgs and you can't turn them off in Cargo.toml – and as I said above I'm not sure this situation actually exists; it would be in a narrow version range if it did – maybe then -A unexpected_cfgs is warranted specifically for your MSRV build.

My point: Whatever the ground truth is of how old versions of the compiler worked, we can't change it, but users can make a range of reasonable policy decisions in their CI based on it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. I-lang-nominated Nominated for discussion during a lang team meeting. needs-fcp This change is insta-stable, or significant enough to need a team FCP to proceed. proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. S-waiting-on-documentation Status: Waiting on approved PRs to documentation before merging S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-lang Relevant to the language team
Projects
None yet
Development

Successfully merging this pull request may close these issues.