Skip to content

Stabilize #[cfg(version(...))] #141137

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 3 commits into
base: master
Choose a base branch
from

Conversation

est31
Copy link
Member

@est31 est31 commented May 17, 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

The first test was added by the original implementation PR #71314 , and is mostly a test for the allowed and not allowed ways to specify the version as part of the attribute. It has seen some changes as the implementation of cfg_version has been changed. As of this PR, the test ensures:

  • #[cfg(version(1.20.0))] is not allowed, deviating from the RFC
  • 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)

The stabilization PR also adds a functional test, which ensures that cfg_version actually works.

  • #[cfg(version = "1.20.0")] acts as if the cfg was false due to the wrong syntax, even if the compiler version is above the specified version
  • cfg!(version("1.50.4")) evals as false on 1.50.3, and cfg!(version("1.50.3")) evals as true.

Lastly, there is assume-incomplete.rs using macros instead of RUSTC_OVERRIDE_VERSION_STRING.

This PR makes cfg(version) respect RUSTC_OVERRIDE_VERSION_STRING, to make it easier to test things, and adds a test based on that.

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.

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

See also

Earlier stabilization report

TODOs before stabilization

@rustbot
Copy link
Collaborator

rustbot commented May 17, 2025

r? @jieyouxu

rustbot has assigned @jieyouxu.
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 A-attributes Area: Attributes (`#[…]`, `#![…]`) 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 17, 2025
@rustbot
Copy link
Collaborator

rustbot commented May 17, 2025

Some changes occurred in compiler/rustc_attr_data_structures

cc @jdonszelmann

Some changes occurred in compiler/rustc_attr_parsing

cc @jdonszelmann

@rust-log-analyzer

This comment has been minimized.

@@ -107,6 +107,8 @@ declare_features! (
(accepted, cfg_target_feature, "1.27.0", Some(29717)),
/// Allows `cfg(target_vendor = "...")`.
(accepted, cfg_target_vendor, "1.33.0", Some(29718)),
/// Allow conditional compilation depending on rust version
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// Allow conditional compilation depending on rust version
/// Allow conditional compilation depending on Rust version.

@traviscross traviscross added T-lang Relevant to the language team, which will review and decide on the PR/issue. needs-fcp This change is insta-stable, so needs a completed FCP to proceed. S-waiting-on-documentation Status: Waiting on approved PRs to documentation before merging and removed T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels May 17, 2025
@traviscross
Copy link
Contributor

This looks right to me. Thanks to @est31 for putting this together and for the careful stabilization report. I propose that we do this.

@rfcbot fcp merge

@rfcbot
Copy link
Collaborator

rfcbot commented May 17, 2025

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

No concerns currently listed.

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 17, 2025
@traviscross traviscross added I-lang-nominated Nominated for discussion during a lang team meeting. and removed 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 17, 2025
@traviscross
Copy link
Contributor

Thanks also to @epage for perhaps prodding us along here, in his recent talk, by pointing out the cost to ecosystem compile time performance of not doing this.

@traviscross
Copy link
Contributor

traviscross commented May 17, 2025

Regarding whether we should hold off merging this stabilization until the change to Cargo is ready, if we do want to do that, we can simply mark this as S-blocked without holding up the FCP. On the question itself, I'm curious to hear from the @rust-lang/cargo team their thoughts and any particular reasons for why we might want to hold off merging the lang-side change here pending the Cargo-side work.

@traviscross traviscross added the P-lang-drag-1 Lang team prioritization drag level 1. https://rust-lang.zulipchat.com/#narrow/channel/410516-t-lang label May 17, 2025
Copy link
Member

@Noratrieb Noratrieb May 18, 2025

Choose a reason for hiding this comment

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

(not about this file specifically)
I think the example of the stabilization report, let chains, are a particularly bad example here because they're syntax, and syntax is, except for some old legacy stuff, gated pre-expansion, so cfg(version) won't actually help there. It would probably make sense to mention this in the reference that this can't really be used for conditionally making use of syntax, or rather that such uses requires going through an intermediary macro_rules to avoid parsing it on the old version.

Copy link
Contributor

Choose a reason for hiding this comment

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

Or putting the syntax in a separate module IIRC.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, anything that won't be parsed before the cfg is expanded.

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated the example to use a libs function instead. The example still fails compilation on the old release, because #[cfg(version(...))] errors on compilers that don't have the stabilization yet, so it's only useful when your MSRV is >= the release that #[cfg(version(...))] stabilized in.

It's actually maybe something that one can discuss, whether to support #[cfg(version = "...")] as well. This syntax does not error on older compilers, or on current ones. The disadvantage is two different ways of doing things and possible breakage because it might activate some blocks within #[cfg(version = "...")], but I'd say that breakage is rather theoretical.

In the long term, #[cfg(version = "...")] is not neccessary: it's only helpful for the transitory period.

Copy link
Contributor

@traviscross traviscross May 19, 2025

Choose a reason for hiding this comment

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

If we were to take the breakage here, I'm not sure why we wouldn't just make #[cfg(version = "..")] the one way to write this.

The concern I recall hearing was the worry that people might think this was a version equality constraint rather than a ^ or >= constraint, but that doesn't seem to be a problem for Cargo dependencies, where e.g. time = "0.1.12" means time = "^0.1.12", or for the MSRV field itself, e.g. rust-version = "1.89".

If we were still concerned, we could require people to write the ^ explicitly, e.g. #[cfg(version = "^1.89")].

Copy link
Contributor

@traviscross traviscross May 19, 2025

Choose a reason for hiding this comment

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

Looking at the RFC, the question was considered in the context of cfg(accessible(..)) vs cfg(accessible = ..)), where yes, the former definitely seems better.

Copy link
Contributor

@madsmtm madsmtm May 19, 2025

Choose a reason for hiding this comment

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

Re cfg(version = "..."), see the RFC text. This is actually also listed in the unresolved questions in the tracking issue, and was discussed briefly on the tracking issue.

IMO, I think we should only support cfg(version = "...") if we emitted all the (hundreds of) valid version="1.0", version="1.0.0", version="1.1", version="1.1.0", etc. as part of rustc --print cfg too.

Unless we want to diverge from cfg(key = "value") being a static thing? I can see the value in that as well, it'd be useful for many other cfg attributes.

EDIT: Sorry, GH didn't update, so posted this before I saw TC's response.

Copy link
Contributor

@traviscross traviscross May 19, 2025

Choose a reason for hiding this comment

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

It does seem at this point that we should probably just stabilize cfg(version("..")). We could always decide later about doing more.

Copy link
Member

Choose a reason for hiding this comment

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

Regarding cfg(version = "..."), see #141137 (comment) .

@fmease fmease changed the title Stabilize #cfg[version(...)] Stabilize #[cfg(version(...))] May 18, 2025
@@ -1,49 +1,35 @@
#[cfg(version(42))] //~ ERROR: expected a version literal
//~^ ERROR `cfg(version)` is experimental and subject to change
fn foo() {}
#[cfg(version(1.20))] //~ ERROR: expected a version literal
Copy link
Contributor

@madsmtm madsmtm May 19, 2025

Choose a reason for hiding this comment

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

What's the reasoning we don't use this syntax again?

From reading #71314, it seems like it was punted on for later because of implementation difficulty. Maybe that's not true any more?

I'd prefer writing #[cfg(version(1.87.1))] over #[cfg(version("1.87.1"))]. Having two levels of grouping, both parentheses and quotes, seems redundant.

Copy link
Contributor

Choose a reason for hiding this comment

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

It makes a certain sense to me to quote it as it is a "version string". In attributes, we generally still try to match the general grammar of Rust, and we wouldn't accept an unquoted 1.87.1 as an argument anywhere else.

Copy link
Contributor

Choose a reason for hiding this comment

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

we wouldn't accept an unquoted 1.87.1 as an argument anywhere else.

I can see the argument, though I don't see why that should necessarily be an absolute? Syntactically, you can write let x = 1.87.1, though that's of course parsed as "literal 1.87" + "field access 1" (which then fails because floats don't have any fields).

Idk., I just think it's unfortunate that we limit ourselves here, since I suspect the vast majority of cases where cfg(version(...)) is useful, you wouldn't need to specify the patch version - looking at the changelog, I actually cannot find a single use-case where you'd want cfg(version(major.minor.patch)) where patch wouldn't just be 0?

If we wanted this syntax, an option could be to disallow patch versions to begin with?


A difficulty with major.minor.patch is on the macro-side, I see a few ways to solve this:

  1. The heavy-weight: Introduce a new token literal LitKind::Version and change $x:literal over an edition to contain this as well.

  2. Define that the token parsing is cfg(version($major_minor:literal $(. $patch:tt)?)).

  3. Specialize parsing of this as one token, but try to hide it from the language. Though that has unclear interactions with macros such as:

    macro_rules! split {
        ($x:tt) => { cfg!(version(1.87 . $x)) };
    }
    
    macro_rules! split {
        ($x:tt) => { cfg!(version($x . 87 . 1)) };
    }

    Maybe we can disallow those? But we'd want a stable way for proc-macros to create the required TokenTree::Literal.

Regardless, I think what I'm saying with this is that while this doesn't have an immediately clean solution, the solution space isn't empty, and I think it's worthwhile it to choose one of these options to get the cleaner syntax. But I guess I'm also motivated here by future attributes with versions in them.

Copy link
Contributor

Choose a reason for hiding this comment

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

CC @petrochenkov who reviewed the implementation PR that did cfg(version("major.minor.patch")) (with the quotes).

Copy link
Contributor

Choose a reason for hiding this comment

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

From a tokenization perspective, a string fits best. A major/minor version could look like a float which we don't have now but I do want to explore bools, ints, and floats for --cfg for cargo "globals" though I don't have use cases for #[cfg] operators for these at this time

@jieyouxu
Copy link
Member

I'll hold off on reviewing the impl until some of the design questions are settled (looks like there are unresolved design concerns), please ping me if it's ready for impl review.

@joshtriplett
Copy link
Member

joshtriplett commented May 19, 2025

The proposal as originally reviewed and approved was for cfg(version(...)), and adding cfg(version = "...") was considered and rejected, in part because of the potential confusion of "equals". But this discussion has given an additional reason we shouldn't accept it:

It's already not going to be functional in any version older than the version that stabilizes cfg(version(...)). Changing the syntax to make it parse and evaluate as always false in old versions is not a feature, since code that still supports those older versions may need to do version detection that distinguishes some of those older versions (e.g. "is this 1.70 or newer").

I would argue that it's a bug to silently ignore it in older versions, precisely because it'll be non-functional: cfg(version = "1.70") will be silently false in 1.70, 1.71, 1.72, up through whatever version stabilizes version checks.

Pieces of the ecosystem can switch to cfg(version(...)) as their MSRVs become sufficiently new that it works.

@traviscross
Copy link
Contributor

I would argue that it's a bug to silently ignore it in older versions, precisely because it'll be non-functional: cfg(version = "1.70") will be silently false in 1.70, 1.71, 1.72, up through whatever version stabilizes version checks.

Pieces of the ecosystem can switch to cfg(version(...)) as their MSRVs become sufficiently new that it works.

Agreed.

Copy link
Contributor

Choose a reason for hiding this comment

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

small shortenings like #[cfg(version("1.20"))] are allowed, but #[cfg(version("1"))] is not

This is a bit inconsistent with other places where a Cargo user puts in a version/version range.

  • package.version requires all fields
  • package.rust-version has 1-3 fields
  • dependencies.*.version has 1-3 fields

I know version("1") is pretty meaningless (which so is the same for package.rust-version).

I guess the question for me is whether package.rust-version should be

  • the same syntax with the same parser implementation
  • a different syntax in which we want to make sure our parser is unique to avoid changes in one affecting the other

@ehuss
Copy link
Contributor

ehuss commented May 20, 2025

I'm curious to hear from the @rust-lang/cargo team their thoughts and any particular reasons for why we might want to hold off merging the lang-side change here pending the Cargo-side work.

We think it would be best if we can make sure the change lands in both cargo and rustc in the same release. There is already someone working on it, and I think we are pretty comfortable that we will be able to get that merged in time, so I don't think there is a strong reason to block. I'll try and make sure that everything is ready, though we should be careful about the scheduling in mid-june if the stabilization process here takes a long time.

@rust-log-analyzer

This comment has been minimized.

@est31 est31 force-pushed the stabilize_cfg_version branch from b5a7902 to dd17060 Compare May 21, 2025 01:55
@rust-log-analyzer
Copy link
Collaborator

The job x86_64-gnu-llvm-19 failed! Check out the build log: (web) (plain)

Click to see the possible cause of the failure (guessed by this bot)
#19 exporting to docker image format
#19 sending tarball 20.4s done
#19 DONE 29.1s
##[endgroup]
Setting extra environment values for docker:  --env ENABLE_GCC_CODEGEN=1 --env GCC_EXEC_PREFIX=/usr/lib/gcc/
[CI_JOB_NAME=x86_64-gnu-llvm-19]
[CI_JOB_NAME=x86_64-gnu-llvm-19]
debug: `DISABLE_CI_RUSTC_IF_INCOMPATIBLE` configured.
---
sccache: Listening on address 127.0.0.1:4226
##[group]Configure the build
configure: processing command line
configure: 
configure: build.configure-args := ['--build=x86_64-unknown-linux-gnu', '--llvm-root=/usr/lib/llvm-19', '--enable-llvm-link-shared', '--set', 'rust.randomize-layout=true', '--set', 'rust.thin-lto-import-instr-limit=10', '--set', 'build.print-step-timings', '--enable-verbose-tests', '--set', 'build.metrics', '--enable-verbose-configure', '--enable-sccache', '--disable-manage-submodules', '--enable-locked-deps', '--enable-cargo-native-static', '--set', 'rust.codegen-units-std=1', '--set', 'dist.compression-profile=balanced', '--dist-compression-formats=xz', '--set', 'rust.lld=false', '--disable-dist-src', '--release-channel=nightly', '--enable-debug-assertions', '--enable-overflow-checks', '--enable-llvm-assertions', '--set', 'rust.verify-llvm-ir', '--set', 'rust.codegen-backends=llvm,cranelift,gcc', '--set', 'llvm.static-libstdcpp', '--set', 'gcc.download-ci-gcc=true', '--enable-new-symbol-mangling']
configure: build.build          := x86_64-unknown-linux-gnu
configure: target.x86_64-unknown-linux-gnu.llvm-config := /usr/lib/llvm-19/bin/llvm-config
configure: llvm.link-shared     := True
configure: rust.randomize-layout := True
configure: rust.thin-lto-import-instr-limit := 10

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-attributes Area: Attributes (`#[…]`, `#![…]`) I-lang-nominated Nominated for discussion during a lang team meeting. needs-fcp This change is insta-stable, so needs a completed FCP to proceed. P-lang-drag-1 Lang team prioritization drag level 1. https://rust-lang.zulipchat.com/#narrow/channel/410516-t-lang 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, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.