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

@traviscross traviscross removed 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 labels Jun 4, 2025
@m-ou-se
Copy link
Member

m-ou-se commented Jun 4, 2025

I think this syntax is a mistake.

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

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. (And we can simply warn about trying to match on a version where cfg(rust_version) was still meaningless.)

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.

We tend to think a lot about backwards compatibility in the Rust project. But in this case we need to think about forward compatibility: what will old versions of Rust do with the new syntax?

Using any syntax that just immediately errors out on current/old versions makes this feature so much less powerful, to the point where it will be basically useless for the near future: we'd be releasing a Rust 1.89.0 with a new cfg(version()) feature that can only be used if you set your MSRV to 1.89.0. 🙃 It'll only become useful after a long time, once it's acceptable to bump your MSRV to 1.89.0. (Which will take quite some time for some projects.)

If instead, we pick a syntax that's already accepted by current/old rust versions, like #[cfg(rust_version = "..")], it is useful directly in 1.89.0: it can immediately be used to cfg things that are newly accepted in Rust 1.89.0! #[cfg(rust_version = "1.89.0")] will immediately work correctly in all Rust versions!

(Unfortunately, #[cfg(version = "..")] gives an error today, which I also think is a mistake. But rust_version = ".." still works fine.)

@epage
Copy link
Contributor

epage commented Jun 4, 2025

Is there a warning if someone does #[cfg(version("1.0.0")] (or any version before the feature was added)?

At least it is a compile error when testing on that version. Having a check-cfg-like warning could help catch that earlier. I would consider this non-blocking with the current proposal. This may become more important with has_version or #[cfg(rust_version = "1.0.0")]

@est31
Copy link
Member Author

est31 commented Jun 4, 2025

@epage there is no such warning. In the stabilization report I write about integrating into the greater MSRV system, i.e. making it read rust-version and if it finds a version below the MSRV, lint about that.

@m-ou-se most features introduced by rustc are taking from the error territory, but I understand the desire to make an exception for this one, given its purpose.

In any case, the moment right after stabilization is the moment it's least useful, after that it monotonically gains usefulness. In 1-3 years, I think the ecosystem will see a large hit in build.rs scripts to version detect stuff. Most of the MSRV desire is for fairly recent releases, e.g. ones 1-30 versions back. There is a second group for really old ones, say debian oldstable with 1.48.0, or something, but that's super rare in the big picture. Given that we have rust-version now, it would actually be interesting to do a grep over all crates on crates.io for it to get back hard numbers, but I don't have the time for it atm.

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.

@epage
Copy link
Contributor

epage commented Jun 5, 2025

@epage there is no such warning. In the stabilization report I write about integrating into the greater MSRV system, i.e. making it read rust-version and if it finds a version below the MSRV, lint about that.

My concern was less to do with the greater MSRV story and more about correct use of this feature. If someone uses #[cfg(version("1.0.0")], the condition will never be true because the syntax will be unsupported on those versions. Linting for this is akin to check-cfg verifying whether your #[cfg]s even make sense (CC @Urgau).

@tmandry
Copy link
Member

tmandry commented Jun 5, 2025

The top 10 most-downloaded crates on crates.io range from MSRVs of 1.48 (Nov 2020) to 1.65 (Nov 2022), with the median being 1.59 (Feb 2022), using their latest versions as of today. Most do not use version-based feature gating. The ones I thought of that did (serde and proc-macro2) work more or less like @est31 describes: They detect features as recent as 1.81 (Sep 2024) and 1.79 (June 2024) respectively, and would need a build script as long as their MSRV was below that.

It's easy to imagine that more of these crates would use version gating if a build script was not required. A change like adding #[diagnostic] attributes (1.78; May 2024) to a trait might not be worth a build script, but it would be worth it if a build script was not required. With cfg(version(...)), any crate with an MSRV below 1.78 would not be able to use diagnostic attributes without a build script. cfg(rust_version = "...") could work, with the caveat of course that it would only "turn on" in newer Rust versions that support rust_version.

@epage
Copy link
Contributor

epage commented Jun 5, 2025

Note that crates like proc-macro2 have build probes for specific implementations of nightly and wouldn't be able to remove the build scripts until they can be convinced to no longer auto-opt-in everyone to nightly features.

@m-ou-se m-ou-se added the I-lang-nominated Nominated for discussion during a lang team meeting. label Jun 5, 2025
@m-ou-se
Copy link
Member

m-ou-se commented Jun 5, 2025

Following up on my comment above:

(Unfortunately, #[cfg(version = "..")] gives an error today, which I also think is a mistake. But rust_version = ".." still works fine.)

Actually, rust_version is probably better regardless. cfg(feature) refers to features of the current crate, not features of the Rust compiler or language. 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.

@epage
Copy link
Contributor

epage commented Jun 5, 2025

Been thinking over Mara's ideas and finally putting together my thoughts.

First, I am sympathetic to how we introduce features to reduce MSRV issues. The MSRV-resolver opt-in was ignored-with-warning on stable to allow people to opt-in more easily without bumping an MSRV.

I do have a couple of biases against the proposal

  • We have forward momentum on getting this merged and I'd rather see something merged than delaying this as we work out the best nature, and a transition plan to allow that, which could undo the benefits we gain by providing a more graceful forwards-compatibility story
  • #[cfg(rust_version = "")] is not a cfg with a discrete set of values but would dynamically support any value less than the current value which I'm not thrilled with. Unsure how much of this is a problem with "explain to the user", "avoiding privileged feature implementation", or "as an implementer". Unsure if or how we would model this with check-cfg (CC @Urgau).

cfg_has_version was proposed to offer similar benefits. My thoughts on that which also apply here are at #141137 (comment) (tl;dr is the window where this is relevant worth it?). The one difference is that cfg_has_version didn't work in Cargo while Mara's proposal would give the benefit of cfg_has_version to Cargo users.

Assuming rust_version = "" would be a built-in that users could do --cfg rust_version with, then that would be a breaking change. One advantage of version("") is it is not a breaking change.b What would be the transition path for rust_version = ""? We'd at least need to do a crater run to see if anyone uses it. Depending on usage, we may either go forward, do a multi-release transition, or have to re-think things. We did recently do this with true and false though I suspect those might be a bit different because users setting those to anything besides what we did would be confusing.

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 suspect with the way most users interact with =, they don't think of it as a contains operation. Syntactically, it doesn't look like one. Most built-in cfg's people interact with are present/not-present or single-items, not sets. Then there is Cargo's feature. I suspect that just feels magical and people don't think too hard about it, especially because the cfg is singular.

On the other hand, seeing #[cfg(rust_version = "1.80.0")] and understanding that that is a set operation and that rust_version is a set of rustc -V and all previous versions feels like it would be hard for users. Every time I see this, I read it as an equality operation.

I get the framing of "rust_version specifies what is supported" so therefore we don't remove anything from it makes sense but people's framing is likely to be set by looking at code in the wild and not documentation they see.

@nikomatsakis
Copy link
Contributor

I've been thinking about this since the meeting. I think that @tmandry and @m-ou-se argument have a pretty strong argument. It bottoms out to me as: who is this feature for? The answer, is "crates that maintain an MSRV". But crates that maintain an MSRV won't be able to use it, quite possibly for some time.

The strongest argument is blunting the existing momentum. But the fact that the target audience won't be able to use it feels like a problem -- how much does it matter if it lands if the target audience can't use it? I'd rather we open an issue, vote on the new design, and stabilize that. I'm not super keen on making last minute changes to syntax but this feature is old and it's not exactly unknown. (This pattern happens over and over in my experience: there is a long blockage that sucks the oxygen out of the discussion; by the time that block is resolved, it turns out a bunch of people have small issues they'd like to discuss, but there seemed to be no point in discussing them while the whole feature was in existential limbo--and then it's been years. It's super duper frustrating.)

I guess I'd like to hear from @est31 for their take on whether they'd be up to implement the change in syntax if we decided to go that way.

I do see Ed's points about obviousness and the fact that people may be using -D today, they seem like valid points, but also not "homeruns". This is a relatively advanced feature and there is precedent with feature (I've always thought that feature is weird, I admit, but also it was just 🤷‍♀️ and move on, and I suspect this would be the same). In terms of -D, that could shadow the "builtin" meaning with a warning.

@alex
Copy link
Member

alex commented Jun 5, 2025

(Apologies if you don't want to hear from folks here) I'm one of the people who maintains things with an MSRV and is keen for this. Notwithstanding that, my suggestion would be to focus on making the long term right decision. My view is very much, "The best time to plant a tree was 20 years ago, the second best time is today". While I might not be able to use it in the release this is in, I can a) get excited, b) look forward to a glorious future, c) drive my MSRV strategy to take advantage as soon as is practical.

@est31
Copy link
Member Author

est31 commented Jun 5, 2025

I suspect with the way most users interact with =, they don't think of it as a contains operation.

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.

I guess I'd like to hear from @est31 for their take on whether they'd be up to implement the change in syntax if we decided to go that way.

definitely, but I don't want to implement it in a haste: the lang team needs to have consensus for a change of the syntax. once that's established, we should probably close this PR and change the implementation. Then, maybe after a certain waiting period as it represents a major change of the feature (6 weeks?), stabilize.

@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 >=

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.