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

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions compiler/rustc_attr_data_structures/src/version.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::fmt::{self, Display};
use std::sync::OnceLock;

use rustc_macros::{
Decodable, Encodable, HashStable_Generic, PrintAttribute, current_rustc_version,
Expand All @@ -16,8 +17,29 @@ pub struct RustcVersion {

impl RustcVersion {
pub const CURRENT: Self = current_rustc_version!();
pub fn current_configurable() -> Self {
*CURRENT_CONFIGURABLE.get_or_init(|| {
if let Ok(override_var) = std::env::var("RUSTC_OVERRIDE_VERSION_STRING")
&& let Some(override_) = Self::parse_str(&override_var)
{
override_
} else {
Self::CURRENT
}
})
}
fn parse_str(value: &str) -> Option<Self> {
// Ignore any suffixes such as "-dev" or "-nightly".
let mut components = value.split('-').next().unwrap().splitn(3, '.');
let major = components.next()?.parse().ok()?;
let minor = components.next()?.parse().ok()?;
let patch = components.next().unwrap_or("0").parse().ok()?;
Some(RustcVersion { major, minor, patch })
}
}

static CURRENT_CONFIGURABLE: OnceLock<RustcVersion> = OnceLock::new();

impl Display for RustcVersion {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}.{}.{}", self.major, self.minor, self.patch)
Expand Down
4 changes: 2 additions & 2 deletions compiler/rustc_attr_parsing/src/attributes/cfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,9 @@ pub fn eval_condition(

// See https://github.com/rust-lang/rust/issues/64796#issuecomment-640851454 for details
if sess.psess.assume_incomplete_release {
RustcVersion::CURRENT > min_version
RustcVersion::current_configurable() > min_version
} else {
RustcVersion::CURRENT >= min_version
RustcVersion::current_configurable() >= min_version
}
}
MetaItemKind::List(mis) => {
Expand Down
2 changes: 2 additions & 0 deletions compiler/rustc_feature/src/accepted.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,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.

(accepted, cfg_version, "CURRENT_RUSTC_VERSION", Some(64796)),
/// Allows implementing `Clone` for closures where possible (RFC 2132).
(accepted, clone_closures, "1.26.0", Some(44490)),
/// Allows coercing non capturing closures to function pointers.
Expand Down
1 change: 0 additions & 1 deletion compiler/rustc_feature/src/builtin_attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ const GATED_CFGS: &[GatedCfg] = &[
Features::cfg_target_has_atomic,
),
(sym::sanitize, sym::cfg_sanitize, Features::cfg_sanitize),
(sym::version, sym::cfg_version, Features::cfg_version),
(sym::relocation_model, sym::cfg_relocation_model, Features::cfg_relocation_model),
(sym::sanitizer_cfi_generalize_pointers, sym::cfg_sanitizer_cfi, Features::cfg_sanitizer_cfi),
(sym::sanitizer_cfi_normalize_integers, sym::cfg_sanitizer_cfi, Features::cfg_sanitizer_cfi),
Expand Down
2 changes: 0 additions & 2 deletions compiler/rustc_feature/src/unstable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -417,8 +417,6 @@ declare_features! (
(unstable, cfg_target_thread_local, "1.7.0", Some(29594)),
/// Allows the use of `#[cfg(ub_checks)` to check if UB checks are enabled.
(unstable, cfg_ub_checks, "1.79.0", Some(123499)),
/// Allow conditional compilation depending on rust version
(unstable, cfg_version, "1.45.0", Some(64796)),
/// Allows to use the `#[cfi_encoding = ""]` attribute.
(unstable, cfi_encoding, "1.71.0", Some(89653)),
/// Allows `for<...>` on closures and coroutines.
Expand Down
3 changes: 2 additions & 1 deletion compiler/rustc_lint/messages.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -810,9 +810,10 @@ lint_undropped_manually_drops = calls to `std::mem::drop` with `std::mem::Manual
.suggestion = use `std::mem::ManuallyDrop::into_inner` to get the inner value
lint_unexpected_builtin_cfg = unexpected `--cfg {$cfg}` flag
.controlled_by = config `{$cfg_name}` is only supposed to be controlled by `{$controlled_by}`
.incoherent = manually setting a built-in cfg can and does create incoherent behaviors
lint_unexpected_builtin_cfg_controlled_by = config `{$cfg_name}` is only supposed to be controlled by `{$controlled_by}`
lint_unexpected_cfg_add_build_rs_println = or consider adding `{$build_rs_println}` to the top of the `build.rs`
lint_unexpected_cfg_add_cargo_feature = consider using a Cargo feature instead
lint_unexpected_cfg_add_cargo_toml_lint_cfg = or consider adding in `Cargo.toml` the `check-cfg` lint config for the lint:{$cargo_toml_lint_cfg}
Expand Down
8 changes: 7 additions & 1 deletion compiler/rustc_lint/src/early/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,13 @@ pub(super) fn decorate_lint(
lints::OutOfScopeMacroCalls { span, path, location }.decorate_lint(diag)
}
BuiltinLintDiag::UnexpectedBuiltinCfg { cfg, cfg_name, controlled_by } => {
lints::UnexpectedBuiltinCfg { cfg, cfg_name, controlled_by }.decorate_lint(diag)
lints::UnexpectedBuiltinCfg {
cfg,
cfg_name,
controlled_by: controlled_by
.map(|controlled_by| lints::ControlledBy { controlled_by }),
}
.decorate_lint(diag)
}
BuiltinLintDiag::ElidedNamedLifetimes { elided: (span, kind), resolution } => {
match resolution {
Expand Down
8 changes: 7 additions & 1 deletion compiler/rustc_lint/src/lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2498,11 +2498,17 @@ pub(crate) mod unexpected_cfg_value {

#[derive(LintDiagnostic)]
#[diag(lint_unexpected_builtin_cfg)]
#[note(lint_controlled_by)]
#[note(lint_incoherent)]
pub(crate) struct UnexpectedBuiltinCfg {
pub(crate) cfg: String,
pub(crate) cfg_name: Symbol,
#[subdiagnostic]
pub(crate) controlled_by: Option<ControlledBy>,
}

#[derive(Subdiagnostic)]
#[note(lint_unexpected_builtin_cfg_controlled_by)]
pub(crate) struct ControlledBy {
pub(crate) controlled_by: &'static str,
}

Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_lint_defs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -827,7 +827,7 @@ pub enum BuiltinLintDiag {
UnexpectedBuiltinCfg {
cfg: String,
cfg_name: Symbol,
controlled_by: &'static str,
controlled_by: Option<&'static str>,
},
}

Expand Down
10 changes: 9 additions & 1 deletion compiler/rustc_session/src/config/cfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ impl<'a, T: Eq + Hash + Copy + 'a> Extend<&'a T> for ExpectedValues<T> {

/// Disallow builtin cfgs from the CLI.
pub(crate) fn disallow_cfgs(sess: &Session, user_cfgs: &Cfg) {
let disallow = |cfg: &(Symbol, Option<Symbol>), controlled_by| {
let disallow_controlled_by = |cfg: &(Symbol, Option<Symbol>), controlled_by| {
let cfg_name = cfg.0;
let cfg = if let Some(value) = cfg.1 {
format!(r#"{}="{}""#, cfg_name, value)
Expand All @@ -102,6 +102,9 @@ pub(crate) fn disallow_cfgs(sess: &Session, user_cfgs: &Cfg) {
BuiltinLintDiag::UnexpectedBuiltinCfg { cfg, cfg_name, controlled_by },
)
};
let disallow = |cfg: &(Symbol, Option<Symbol>), controlled_by| {
disallow_controlled_by(cfg, Some(controlled_by));
};

// We want to restrict setting builtin cfgs that will produce incoherent behavior
// between the cfg and the rustc cli flag that sets it.
Expand Down Expand Up @@ -147,6 +150,7 @@ pub(crate) fn disallow_cfgs(sess: &Session, user_cfgs: &Cfg) {
| (sym::target_has_reliable_f128, None | Some(_))
| (sym::target_has_reliable_f128_math, None | Some(_))
| (sym::target_thread_local, None) => disallow(cfg, "--target"),
(sym::has_cfg_version, None) => disallow_controlled_by(cfg, None),
(sym::fmt_debug, None | Some(_)) => disallow(cfg, "-Z fmt-debug"),
(sym::emscripten_wasm_eh, None | Some(_)) => disallow(cfg, "-Z emscripten_wasm_eh"),
_ => {}
Expand Down Expand Up @@ -310,6 +314,8 @@ pub(crate) fn default_configuration(sess: &Session) -> Cfg {
ins_none!(sym::contract_checks);
}

ins_none!(sym::has_cfg_version);

ret
}

Expand Down Expand Up @@ -472,6 +478,8 @@ impl CheckCfg {
ins!(sym::ub_checks, no_values);
ins!(sym::contract_checks, no_values);

ins!(sym::has_cfg_version, no_values);

ins!(sym::unix, no_values);
ins!(sym::windows, no_values);
}
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_span/src/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1089,6 +1089,7 @@ symbols! {
guard_patterns,
half_open_range_patterns,
half_open_range_patterns_in_slices,
has_cfg_version,
hash,
hashmap_contains_key,
hashmap_drain_ty,
Expand Down
1 change: 0 additions & 1 deletion compiler/rustc_trait_selection/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
#![feature(assert_matches)]
#![feature(associated_type_defaults)]
#![feature(box_patterns)]
#![feature(cfg_version)]
#![feature(if_let_guard)]
#![feature(iter_intersperse)]
#![feature(iterator_try_reduce)]
Expand Down
35 changes: 0 additions & 35 deletions src/doc/unstable-book/src/language-features/cfg-version.md

This file was deleted.

2 changes: 0 additions & 2 deletions tests/ui/cfg/assume-incomplete-release/assume-incomplete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
//@ revisions: assume no_assume
//@ [assume]compile-flags: -Z assume-incomplete-release

#![feature(cfg_version)]

extern crate ver_cfg_rel;

use ver_cfg_rel::ver_cfg_rel;
Expand Down
28 changes: 28 additions & 0 deletions tests/ui/cfg/cfg-version/cfg-version-expand.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//@ run-pass
//@ rustc-env:RUSTC_OVERRIDE_VERSION_STRING=1.50.3

#[cfg_attr(has_cfg_version, cfg(version("1.49.0")))]
const ON_1_49_0: bool = true;
#[cfg(version("1.50"))]
const ON_1_50_0: bool = true;
#[cfg(not(version("1.51")))]
const ON_1_51_0: bool = false;

// This one uses the wrong syntax, so doesn't eval to true
#[warn(unexpected_cfgs)]
#[cfg(not(version = "1.48.0"))] //~ WARN unexpected `cfg` condition name: `version`
const ON_1_48_0: bool = false;

fn main() {
assert!(!ON_1_48_0);
assert!(ON_1_49_0);
assert!(ON_1_50_0);
assert!(!ON_1_51_0);
assert!(cfg!(version("1.1")));
assert!(cfg!(version("1.49")));
assert!(cfg!(version("1.50.0")));
assert!(cfg!(version("1.50.3")));
assert!(!cfg!(version("1.50.4")));
assert!(!cfg!(version("1.51")));
assert!(!cfg!(version("1.100")));
}
13 changes: 13 additions & 0 deletions tests/ui/cfg/cfg-version/cfg-version-expand.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
warning: unexpected `cfg` condition name: `version`
--> $DIR/cfg-version-expand.rs:13:11
|
LL | #[cfg(not(version = "1.48.0"))]
| ^^^^^^^^^^^^^^^^^^
|
= help: expected names are: `FALSE` and `test` and 32 more
= help: to expect this configuration use `--check-cfg=cfg(version, values("1.48.0"))`
= note: see <https://doc.rust-lang.org/nightly/rustc/check-cfg.html> for more information about checking conditional configuration
= note: `#[warn(unexpected_cfgs)]` on by default

warning: 1 warning emitted

Original file line number Diff line number Diff line change
@@ -1,49 +1,37 @@
#[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

//~^ ERROR `cfg(version)` is experimental and subject to change
fn foo() -> bool { true }
#[cfg(version = "1.20")] //~ WARN: unexpected `cfg` condition name: `version`
fn foo() -> bool { true }
#[cfg(version("1.44"))]
//~^ ERROR `cfg(version)` is experimental and subject to change
fn foo() -> bool { true }
#[cfg(not(version("1.44")))]
//~^ ERROR `cfg(version)` is experimental and subject to change
fn foo() -> bool { false }

#[cfg(version("1.43", "1.44", "1.45"))] //~ ERROR: expected single version literal
//~^ ERROR `cfg(version)` is experimental and subject to change
fn bar() -> bool { false }
#[cfg(version(false))] //~ ERROR: expected a version literal
//~^ ERROR `cfg(version)` is experimental and subject to change
fn bar() -> bool { false }
#[cfg(version("foo"))] //~ WARNING: unknown version literal format
//~^ ERROR `cfg(version)` is experimental and subject to change
fn bar() -> bool { false }
#[cfg(version("999"))] //~ WARNING: unknown version literal format
//~^ ERROR `cfg(version)` is experimental and subject to change
fn bar() -> bool { false }
#[cfg(version("-1"))] //~ WARNING: unknown version literal format
//~^ ERROR `cfg(version)` is experimental and subject to change
fn bar() -> bool { false }
#[cfg(version("65536"))] //~ WARNING: unknown version literal format
//~^ ERROR `cfg(version)` is experimental and subject to change
fn bar() -> bool { false }
#[cfg(version("0"))] //~ WARNING: unknown version literal format
//~^ ERROR `cfg(version)` is experimental and subject to change
fn bar() -> bool { true }
#[cfg(version("1.0"))]
//~^ ERROR `cfg(version)` is experimental and subject to change
fn bar() -> bool { true }
#[cfg(version("1.65536.2"))] //~ WARNING: unknown version literal format
//~^ ERROR `cfg(version)` is experimental and subject to change
fn bar() -> bool { false }
#[cfg(version("1.20.0-stable"))] //~ WARNING: unknown version literal format
//~^ ERROR `cfg(version)` is experimental and subject to change
fn bar() {}

fn main() {
assert!(foo());
assert!(bar());
assert!(cfg!(version("1.42"))); //~ ERROR `cfg(version)` is experimental and subject to change
assert!(cfg!(version("1.42")));
}
Loading
Loading