From 0ac874862d6b37385391de6c1fadc40e75ad96cd Mon Sep 17 00:00:00 2001 From: Tyler Mandry Date: Tue, 6 Jan 2026 18:36:51 -0800 Subject: [PATCH 01/21] Add typed cfgs RFC --- text/0000-typed-cfgs.md | 253 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 text/0000-typed-cfgs.md diff --git a/text/0000-typed-cfgs.md b/text/0000-typed-cfgs.md new file mode 100644 index 00000000000..8fb91dfbe9f --- /dev/null +++ b/text/0000-typed-cfgs.md @@ -0,0 +1,253 @@ +- Feature Name: typed_cfgs +- Start Date: 2026-01-06 +- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) +- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) + +# Summary +[summary]: #summary + +This RFC proposes "typed `cfg`s", a new form of conditional compilation predicate that understands types. Initially, this RFC proposes to add support for version-typed `cfg`s, allowing for ergonomic version comparisons against the *language version* supported by the compiler. This would be exposed through two new built-in `cfg` names: + +* `rust_version`, which can be compared against a language version literal, e.g., `#[cfg(rust_version >= "1.85")]`. +* `rust_edition`, which can be compared against an edition literal, e.g., `#[cfg(rust_edition >= "2024")]`. + +This design solves a long-standing problem of conditionally compiling code for different Rust versions without requiring build scripts or forcing libraries to increase their Minimum Supported Rust Version (MSRV). It also replaces the `cfg(version(..))` part of RFC 2523. + +# Motivation +[motivation]: #motivation + +There are two primary use cases for conditional compilation based on versions, whether that is the version of the Rust language or the version of a dependency: + +1. **Exposing new features** when code is compiled with newer versions of the language or a library. +2. **Straddling a breaking change** in an API, allowing code to compile against both the old and new versions. + +The `rust_version` cfg proposed in this RFC is designed for the first use case, while `rust_edition` is a tool for the second. This RFC also provides a general mechanism for `version`-typed `cfg`s that can be used for libraries, which addresses both use cases. + +This RFC also aims to solve long-standing problems that have been approached with more specific RFCs in the past. For example, the `cfg_target_version` RFC ([#3750](https://github.com/rust-lang/rfcs/pull/3750)) proposed a way to compare against the version of the target platform's SDK (e.g., `#[cfg(target_version(macos >= "10.15"))]`). This is crucial for safely using platform-specific APIs that are only available in newer OS versions. Instead of a one-off feature, the general `version` type proposed in this RFC provides a robust foundation to solve this problem. A build script could detect the platform version and emit a `version`-typed cfg (`--cfg 'macos_version=version("10.15")'`), allowing for the same ergonomic comparisons in code: `#[cfg(macos_version >= "10.15")]`. These platform-specific version keys can be added into the language in future RFCs. + +The primary blockers for existing solutions have been: + +- **Build Scripts are a Poor Solution:** The only stable tool for this today is a build script (`build.rs`). However, build scripts add significant compilation overhead and are clunky to write and maintain. +- **Previous Attempts had Flaws:** Past RFCs have tried to solve this, but ran into an unfortunate issue: their proposed syntax, e.g. `#[cfg(version(1.85))]`, was a syntax error on older compilers. This means that to use the feature, a library would first have to bump its MSRV to the version that introduced the syntax, defeating the primary purpose of the feature. + +This RFC proposes a solution that avoids these pitfalls. + +A key motivating example is making it ergonomic to adopt attributes that were stabilized after a crate's MSRV. For example, the `#[diagnostic::on_unimplemented]` attribute is a stable feature that library authors can use to provide better error messages. However, if a library has an MSRV from before this attribute was stabilized, they cannot use it without a build script. A build script is often too much overhead for such a small, non-essential feature. + +This RFC makes it trivial to adopt. In a hypothetical world where this RFC landed in Rust 1.55 and `#[diagnostic::on_unimplemented]` landed in 1.60, a crate with an MSRV of 1.50 could use the attribute like this: + +```rust +// MSRV = 1.50, this RFC = 1.55, diagnostic attribute = 1.60 +#[cfg_attr(rust_version, cfg_attr(rust_version >= "1.60", diagnostic::on_unimplemented( + message = "`{Self}` does not implement `MyTrait`", + label = "missing implementation for `{Self}`", +)))] +impl MyTrait for T { /* ... */ } +``` + +With this feature, we will hopefully see more people using useful attributes like `on_unimplemented` even with MSRVs before when the diagnostic attribute namespace was added. The ability to conditionally add attributes for newer features is further detailed in the guide-level explanation. + +# Guide-level explanation +[guide-level-explanation]: #guide-level-explanation + +If your crate's MSRV is at least the version where typed `cfg`s were stabilized, you can directly use the version comparison. For example, imagine a new function `pretty_print()` is stabilized in Rust 1.92: + +```rust +#[cfg(rust_version >= "1.92")] +fn use_new_feature() { + pretty_print(); +} +``` + +However, the primary benefit of this feature is to allow conditional compilation while maintaining a *lower* MSRV. The key is to first check for the existence of the `rust_version` configuration itself before trying to use it in a comparison. + +For example, to conditionally apply the `#[diagnostic::on_unimplemented]` attribute itself (if, for example, your MSRV is older than when that attribute was stabilized in 1.60), you can use nested `cfg_attr`: + +```rust +// For an MSRV of 1.50, where this RFC is in 1.55 and the diagnostic attribute is in 1.60 +#[cfg_attr(rust_version, cfg_attr(cfg(rust_version >= "1.60", diagnostic::on_unimplemented( + message = "`{Self}` does not implement `MyTrait`", + label = "missing implementation for `{Self}`", +))))] +impl MyTrait for T { /* ... */ } +``` + +This pattern ensures that on compilers older than 1.55, nothing is emitted. On compilers between 1.55 and 1.59, only the outer `cfg_attr` is processed, and since `rust_version >= "1.60"` is false, nothing is emitted. On compilers 1.60 and newer, the full attribute is applied. + +Similarly, you can provide different diagnostics depending on whether a new field has been added to an existing attribute. Imagine the `admonition` field is added in version 1.Y: + +```rust +// If your crate's MSRV is high enough to assume `diagnostic::on_unimplemented` exists +#[cfg_attr(rust_version < "1.Y", diagnostic::on_unimplemented( + message = "`{Self}` does not implement `MyTrait`; perhaps you need to enable feature 'foo'?", + label = "missing implementation for `{Self}`", +))] +#[cfg_attr(rust_version >= "1.Y", diagnostic::on_unimplemented( + message = "`{Self}` does not implement `MyTrait`", + label = "missing implementation for `{Self}`", + admonition = "Perhaps you need to enable feature 'foo'?" // Using a hypothetical new field +))] +impl MyTrait for T { /* ... */ } +``` + +Similarly, you can check the Rust edition: + +```rust +#[cfg(rust_edition >= "2021")] +fn my_function() { + // use a feature only available from the 2021 edition onwards +} +``` + +Note that because new compilers can still compile older editions, the `#[cfg(rust_edition)]` stacking pattern is less broadly useful than it is for `rust_version`. Its primary use case is within macros or code generation that needs to produce different code depending on the edition context it's being expanded into. + +For this initial proposal, the only supported comparison operators are `>=` and `<`. + +# Reference-level explanation +[reference-level-explanation]: #reference-level-explanation + +This RFC adds a new kind of predicate to the `cfg` attribute, allowing for comparison against version identifiers. + +### `version` predicates + +A `version` predicate is available for `cfg` identifiers that are declared to be of type `version`. + +The following grammar will be added for `cfg` predicates: + +```text +cfg_predicate : + IDENTIFIER ('>=' | '<') STRING_LITERAL + | ... +``` + +This form of predicate is only valid when the `IDENTIFIER` on the left-hand side is a known `cfg` of type `version`. This RFC proposes two built-in `version` identifiers, `rust_version` and `rust_edition`, and a mechanism for build scripts and command-line tools to introduce new ones. + +A `cfg` identifier is of type `version` if: +* It is one of the built-in identifiers `rust_version` or `rust_edition`. +* It is declared with `--check-cfg 'cfg(name, version)'` and is passed to the compiler with the special syntax `--cfg 'name=version("...")'`. + +The `ident` must be a known `cfg` identifier of type `version`. The `literal` must be a string literal that represents a valid version. + +A `version` predicate evaluates to `true` if the comparison is true, and `false` otherwise. If the identifier is not a known version `cfg`, or the literal is not a valid version string, a compile-time error is produced. + +```rust +#[cfg(rust_version >= "1.90")] +fn new_impl() { /* ... */ } + +#[cfg(rust_version < "1.90")] +fn old_impl() { /* ... */ } +``` + +#### Version Literals +The `STRING_LITERAL` in a version predicate must conform to the following grammar: + +```text +version_literal : + NUMERIC_COMPONENT ('.' NUMERIC_COMPONENT ('.' NUMERIC_COMPONENT)?)? + +NUMERIC_COMPONENT : + '0' + | ('1'...'9') ('0'...'9')* +``` + +This grammar defines a version as one, two, or three non-negative integer components separated by dots. Each component must not have leading zeros, unless the component itself is `0`. For example, `"1.90"` and `"0.2.0"` are valid, but `"1.09"` is not. + +There is a single, unified parsing and comparison logic that is part of the language's semantics. Additional checks for the built-in version keys are implemented as lints. + +* The comparison is performed component-by-component, filling in any missing components with `0`. For example, a predicate `my_cfg >= "1.5"` will evaluate to true for versions `1.5.0`, `1.6.0`, and `2.0`, but false for `1.4.9`. +* For `rust_version`, a lint will be issued if the literal has more than two components (e.g., `"1.92.0"`). This is because language features should not depend on patch releases. + * A new lint, `useless_version_constraint`, warns for version checks that are logically guaranteed to be true or false (e.g., `rust_version >= "1.20"` when the feature was stabilized in 1.90). +* For `rust_edition`, a lint will be issued if the literal has more than one component or if the value is not a recognized Rust edition. +* Pre-release identifiers (e.g., `"1.92-beta"`) are not supported in this RFC. They will be ignored during comparison and a lint will be emitted. See the "Unresolved Questions" section for further discussion. + +### Interaction with Compiler Flags + +The `version` type integrates with existing compiler flags. + +* **`--cfg`**: To define a `version`-typed `cfg`, the following syntax must be used: + ```sh + --cfg 'my_app_version=version("2.1.0")' + ``` + This can also be used to override built-in version cfgs (e.g. `--cfg 'rust_version=version("1.50.0")'`), which is primarily useful for testing. + + * If a version cfg is used with a string literal in a comparison that is not a valid version string, a hard error is emitted. + * If a cfg that is not a version type is used in a version comparison, a hard error is emitted. For undefined cfgs, this could be relaxed to evaluate to false in the future. + * If a cfg + +* **`--print cfg`**: For the built-in `rust_version` and `rust_edition` cfgs, this flag will *not* print them by default to avoid breaking tools that parse this output. They are only printed if overridden via `--cfg`. User-defined version cfgs are printed in the `name=version("...")` format. + +* **`--check-cfg`**: To inform the compiler that a `cfg` is expected to be a version, and to enable linting, use: + ```sh + --check-cfg 'cfg(my_app_version, version)' + ``` + The built-in `rust_version` and `rust_edition` cfgs are implicitly included, so `rustc --print=check-cfg` will always list them. + +### Stabilization + +The features described in this RFC can be stabilized in phases: + +1. The initial stabilization can include the built-in `rust_version` and `rust_edition` cfgs and the ability to compare them with `>=` and `<`. +2. The ability for users to define their own `version`-typed `cfg`s via `--cfg` and `--check-cfg` can be stabilized later. + +This approach delivers the most critical functionality to users quickly, while allowing more time to finalize the design for user-defined version predicates. + +# Drawbacks +[drawbacks]: #drawbacks + +- Increased compiler complexity. This introduces a new concept of "typed" `cfg`s into the compiler, which adds a small amount of complexity to the parsing and evaluation logic for conditional compilation. +- Subtlety of MSRV-preserving patterns: The need for the "stacked `cfg`" pattern (`#[cfg(rust_version)] #[cfg(rust_version >= ...)]` and `#[cfg_attr(rust_version, cfg(rust_version >= ...))]`) is subtle. While we will add lints to guide users, it's less direct than a simple predicate. However, this subtlety is the explicit tradeoff made to achieve MSRV compatibility. + +# Rationale and alternatives +[rationale-and-alternatives]: #rationale-and-alternatives + +### Why this design? +This design directly solves the MSRV problem in a way that previous attempts did not. The syntax `rust_version >= "1.85"` is highly intuitive and directly expresses the user's intent once the feature is available. It is a **principled** design, as by introducing a `version` type to the `cfg` system, we create a sound basis for comparison operators. This avoids the semantic confusion of proposals like `rust_version = "1.85"` which would have overloaded the meaning of `=` for a single special case. Furthermore, it's an extensible design that paves the way for other comparison operators, `cfg(some_dependency >= "1.2.3")`, or other typed `cfg`s in the future. + +### Alternative 1: `#[cfg(version(1.85))]` (RFC 2523) +This was the original accepted RFC for version-based conditional compilation. + +#### Rationale for not choosing +This syntax has several drawbacks. Most importantly, it introduces a new syntax that is a hard error on older compilers, making it unusable for its primary purpose of maintaining a low MSRV. The syntax `version("1.85")` is ambiguous; it is not clear from context whether this refers to the Rust version, the crate version, or some other dependency's version. The function-call-like syntax adds a level of nesting and is not necessarily intuitive for a `cfg` predicate. It evolves the language along two axes at once: adding a new capability *and* a new syntax paradigm for `cfg`. The current proposal, by contrast, builds on the existing `cfg` syntax in a more minimal way. + +### Alternative 2: `#[cfg(rust_version = "1.85")]` (meaning `>=`) +This syntax is parseable by older compilers, which is a significant advantage for MSRV compatibility. + +#### Rationale for not choosing +The use of `=` was highly controversial. In prior art, the `cfg` syntax has two conceptual models: "set inclusion" for multi-valued cfgs (e.g., `feature = "serde"`) and "queries" for boolean flags (e.g., `unix`). For set inclusion, `=` makes sense. However, checking the compiler version is a query for a single value, not a check for inclusion in a set. In this context, `=` strongly implies exact equality (`== 1.85`), while the overwhelming use case is for a lower bound (`>= 1.85`). Overloading `=` for this purpose was considered unprincipled and confusing. This RFC's approach of introducing a `version` type provides a proper semantic foundation for comparison operators like `>=`. + +#### Advantage +This approach *could* potentially be made to work inside `Cargo.toml` (e.g., for conditional dependencies), which currently cannot use the stacked-cfg trick. However, the disadvantages in terms of semantic clarity for the language itself outweigh this benefit for an in-language feature. + +### Alternative 3: `#[cfg(version_since(rust, "1.85"))]` (RFC 3857) +This alternative also avoids the MSRV problem and is extensible, similar to the current proposal. + +#### Rationale for not choosing +While a good design, the "typed cfgs" approach with an actual comparison operator (`>=`, `<`) is arguably more natural and ergonomic. A language team poll indicated a preference for `rust_version >= "1.85"` if it could be made to work. This RFC provides the mechanism to make it work in a principled way. + +# Prior art +[prior-art]: #prior-art + +- **Cargo's `rust-version`:** The `[package]` section of `Cargo.toml` can specify a `rust-version` field. This allows Cargo to select appropriate versions of dependencies and fail early if the compiler is too old. However, it does not provide fine-grained, in-code conditional compilation. This RFC brings a similar capability directly into the language, but for controlling code within a crate rather than for dependency resolution. + +- **C++ (`__cplusplus`)**: The C++ standard defines the `__cplusplus` macro, which expands to an integer literal that increases with each new version of the standard (e.g., `201103L` for C++11, `202002L` for C++20). This allows for preprocessor checks like `#if __cplusplus >= 201103L`. This is very similar to the `rust_version >= "..."` proposal in that it uses standard comparison operators against a monotonically increasing value. However, it is less granular, as several years pass between new C++ versions. + +- **Clang/GCC (`__has_feature`, `__has_attribute`)**: These function-like macros allow for checking for the presence of specific compiler features, rather than the overall language version. For example, `__has_feature(cxx_rvalue_references)` checks for a specific language feature. This approach is more granular but also more verbose if one needs to check for many features at once. This approach was discussed in RFC #2523, but rejected, in part because we wanted to reinforce the idea of Rust as "one language" instead of a common subset with many compiler-specific extensions. + +- **Python (`sys.version_info`)**: Python exposes its version at runtime via `sys.version_info`, a tuple of integers `(major, minor, micro, ...)`. Code can check the version with standard tuple comparison, e.g., `if sys.version_info >= (3, 8):`. This component-wise comparison is very similar to the logic proposed in this RFC. However, because Python is interpreted, a file must be syntactically valid for the interpreter that is running it, which makes it difficult to use newer syntax in a file that must also run on an older interpreter. Rust, being a compiled language with a powerful conditional compilation system, does not have this limitation, and this RFC's design takes full advantage of that. + +# Unresolved questions +[unresolved-questions]: #unresolved-questions + +- How should pre-release identifiers in version strings be handled? This RFC proposes not supporting pre-release identifiers in version strings passed on the command line for now. For comparisons, this RFC proposes that if a pre-release identifier is present in a `cfg` predicate (e.g., `rust_version < "2.0-alpha"`), the pre-release part is ignored for the comparison (so it's treated as `2.0`), and a lint is emitted. This ensures forward compatibility, as comparisons like `cfg(all(foo >= "2.0-alpha", foo < "2.0"))` become trivially false on older compilers, which is a safe outcome. This behavior can be refined before stabilization. + +# Future possibilities +[future-possibilities]: #future-possibilities + +- **"Compatible-with" operator:** We could introduce a `~=` operator that works like Cargo's caret requirements. For example, `cfg(some_dep ~= "1.5")` would be equivalent to `cfg(all(some_dep >= "1.5", some_dep < "2.0"))`. The rationale for not doing this now is that it's easy enough to write by hand. +- **More comparison operators:** While this RFC only proposes `>=` and `<`, the underlying `version` type makes it natural to add support for `<=`, `==`, `!=`, etc., in the future. +- **More flexible version strings:** The version string parsing could be extended to support pre-release identifiers (`-beta`, `-nightly`), though this adds complexity to the comparison logic. +- **Dependency Version `cfg`s:** The "typed `cfg`" infrastructure could be extended to query the versions of direct dependencies, e.g., `#[cfg(serde >= "1.0.152")]`. This would require significant integration with Cargo. +- **Other `cfg` types:** We could introduce other types, such as integers or single-valued strings. This could be useful for a variety of features, from system library versioning schemes ([kconfig](https://docs.kernel.org/kbuild/kconfig-language.html)) to enabling things like [mutually exclusive global features](https://internals.rust-lang.org/t/pre-rfc-mutually-excusive-global-features/19618). +- **Namespaced `cfg`s:** We could group Rust-specific `cfg`s under a `rust::` namespace, e.g., `#[cfg(rust::version >= "1.85")]`. This RFC intentionally keeps `rust_version` at the top level to simplify the initial implementation and stabilization, but namespacing could be explored in the future to better organize the growing number of built-in `cfg`s. +- **Short-circuiting `cfg` predicates:** Change `any` and `all` predicates to short-circuit instead of evaluating all their arguments. This would make introducing new predicates and comparison operators much easier. +- **More expressive check-cfg:** We can support specifying an expected number of components in check-cfg, or an expected set of values to compare against, as in editions. From 9e41ebfb6617ced9f66a1e1dcc9621a31418242f Mon Sep 17 00:00:00 2001 From: Tyler Mandry Date: Tue, 6 Jan 2026 21:25:58 -0800 Subject: [PATCH 02/21] Improve guide-level section and motivation --- text/0000-typed-cfgs.md | 51 ++++++++++++++--------------------------- 1 file changed, 17 insertions(+), 34 deletions(-) diff --git a/text/0000-typed-cfgs.md b/text/0000-typed-cfgs.md index 8fb91dfbe9f..f4ed661526c 100644 --- a/text/0000-typed-cfgs.md +++ b/text/0000-typed-cfgs.md @@ -34,14 +34,13 @@ This RFC proposes a solution that avoids these pitfalls. A key motivating example is making it ergonomic to adopt attributes that were stabilized after a crate's MSRV. For example, the `#[diagnostic::on_unimplemented]` attribute is a stable feature that library authors can use to provide better error messages. However, if a library has an MSRV from before this attribute was stabilized, they cannot use it without a build script. A build script is often too much overhead for such a small, non-essential feature. -This RFC makes it trivial to adopt. In a hypothetical world where this RFC landed in Rust 1.55 and `#[diagnostic::on_unimplemented]` landed in 1.60, a crate with an MSRV of 1.50 could use the attribute like this: +This RFC makes it trivial to adopt even in a crate that doesn't want to use a build script. In this case, since the diagnostic attribute namespace landed before `rust_version`, you would write ```rust -// MSRV = 1.50, this RFC = 1.55, diagnostic attribute = 1.60 -#[cfg_attr(rust_version, cfg_attr(rust_version >= "1.60", diagnostic::on_unimplemented( +#[cfg_attr(rust_version, diagnostic::on_unimplemented( message = "`{Self}` does not implement `MyTrait`", label = "missing implementation for `{Self}`", -)))] +))] impl MyTrait for T { /* ... */ } ``` @@ -53,43 +52,27 @@ With this feature, we will hopefully see more people using useful attributes lik If your crate's MSRV is at least the version where typed `cfg`s were stabilized, you can directly use the version comparison. For example, imagine a new function `pretty_print()` is stabilized in Rust 1.92: ```rust -#[cfg(rust_version >= "1.92")] -fn use_new_feature() { +fn print_something() { + #[cfg(rust_version >= "1.92")] pretty_print(); } ``` -However, the primary benefit of this feature is to allow conditional compilation while maintaining a *lower* MSRV. The key is to first check for the existence of the `rust_version` configuration itself before trying to use it in a comparison. - -For example, to conditionally apply the `#[diagnostic::on_unimplemented]` attribute itself (if, for example, your MSRV is older than when that attribute was stabilized in 1.60), you can use nested `cfg_attr`: +`rust_version` also allows the use of these predicates while maintaining a lower MSRV than the version `rust_version` itself ships in. The key is to first check for the existence of the `rust_version` configuration itself before trying to use it in a comparison. ```rust -// For an MSRV of 1.50, where this RFC is in 1.55 and the diagnostic attribute is in 1.60 -#[cfg_attr(rust_version, cfg_attr(cfg(rust_version >= "1.60", diagnostic::on_unimplemented( - message = "`{Self}` does not implement `MyTrait`", - label = "missing implementation for `{Self}`", -))))] -impl MyTrait for T { /* ... */ } -``` - -This pattern ensures that on compilers older than 1.55, nothing is emitted. On compilers between 1.55 and 1.59, only the outer `cfg_attr` is processed, and since `rust_version >= "1.60"` is false, nothing is emitted. On compilers 1.60 and newer, the full attribute is applied. - -Similarly, you can provide different diagnostics depending on whether a new field has been added to an existing attribute. Imagine the `admonition` field is added in version 1.Y: +fn print_something() { + #[cfg(rust_version)] + #[cfg(rust_version >= "1.92")] + pretty_print(); -```rust -// If your crate's MSRV is high enough to assume `diagnostic::on_unimplemented` exists -#[cfg_attr(rust_version < "1.Y", diagnostic::on_unimplemented( - message = "`{Self}` does not implement `MyTrait`; perhaps you need to enable feature 'foo'?", - label = "missing implementation for `{Self}`", -))] -#[cfg_attr(rust_version >= "1.Y", diagnostic::on_unimplemented( - message = "`{Self}` does not implement `MyTrait`", - label = "missing implementation for `{Self}`", - admonition = "Perhaps you need to enable feature 'foo'?" // Using a hypothetical new field -))] -impl MyTrait for T { /* ... */ } + #[cfg_attr(rust_version, cfg(rust_version < "1.92"))] + println!("something less pretty"); +} ``` +This chained config pattern is only necessary when your MSRV straddles both the `rust_version` feature and a new feature that shipped after it. + Similarly, you can check the Rust edition: ```rust @@ -99,9 +82,9 @@ fn my_function() { } ``` -Note that because new compilers can still compile older editions, the `#[cfg(rust_edition)]` stacking pattern is less broadly useful than it is for `rust_version`. Its primary use case is within macros or code generation that needs to produce different code depending on the edition context it's being expanded into. +Note that because new compilers can still compile older editions, the `#[cfg(rust_edition)]` stacking pattern is less useful than it is for `rust_version`. The primary use case for rust_edition is within macros or code generation that needs to produce different code depending on the edition context it's being expanded into. -For this initial proposal, the only supported comparison operators are `>=` and `<`. +For this RFC, the only supported comparison operators are `>=` and `<`. # Reference-level explanation [reference-level-explanation]: #reference-level-explanation From 7c6744df0f8f1b1b64499108bea9a5791c639d56 Mon Sep 17 00:00:00 2001 From: Tyler Mandry Date: Tue, 6 Jan 2026 21:31:13 -0800 Subject: [PATCH 03/21] Clarify that = with version is a hard error --- text/0000-typed-cfgs.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/text/0000-typed-cfgs.md b/text/0000-typed-cfgs.md index f4ed661526c..e28879ada22 100644 --- a/text/0000-typed-cfgs.md +++ b/text/0000-typed-cfgs.md @@ -143,6 +143,8 @@ There is a single, unified parsing and comparison logic that is part of the lang * For `rust_edition`, a lint will be issued if the literal has more than one component or if the value is not a recognized Rust edition. * Pre-release identifiers (e.g., `"1.92-beta"`) are not supported in this RFC. They will be ignored during comparison and a lint will be emitted. See the "Unresolved Questions" section for further discussion. +Using version-typed config values with the `=` predicate results in a hard error. + ### Interaction with Compiler Flags The `version` type integrates with existing compiler flags. @@ -155,7 +157,7 @@ The `version` type integrates with existing compiler flags. * If a version cfg is used with a string literal in a comparison that is not a valid version string, a hard error is emitted. * If a cfg that is not a version type is used in a version comparison, a hard error is emitted. For undefined cfgs, this could be relaxed to evaluate to false in the future. - * If a cfg + * If a cfg that is a version type is used in a non-version comparison (`=`), a hard error is emitted. * **`--print cfg`**: For the built-in `rust_version` and `rust_edition` cfgs, this flag will *not* print them by default to avoid breaking tools that parse this output. They are only printed if overridden via `--cfg`. User-defined version cfgs are printed in the `name=version("...")` format. From a26e0ec14afbb00590fa973e3f9410c0eca23c47 Mon Sep 17 00:00:00 2001 From: Tyler Mandry Date: Tue, 6 Jan 2026 21:35:39 -0800 Subject: [PATCH 04/21] Don't lint on potential future editions --- text/0000-typed-cfgs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-typed-cfgs.md b/text/0000-typed-cfgs.md index e28879ada22..c95754ace8e 100644 --- a/text/0000-typed-cfgs.md +++ b/text/0000-typed-cfgs.md @@ -140,7 +140,7 @@ There is a single, unified parsing and comparison logic that is part of the lang * The comparison is performed component-by-component, filling in any missing components with `0`. For example, a predicate `my_cfg >= "1.5"` will evaluate to true for versions `1.5.0`, `1.6.0`, and `2.0`, but false for `1.4.9`. * For `rust_version`, a lint will be issued if the literal has more than two components (e.g., `"1.92.0"`). This is because language features should not depend on patch releases. * A new lint, `useless_version_constraint`, warns for version checks that are logically guaranteed to be true or false (e.g., `rust_version >= "1.20"` when the feature was stabilized in 1.90). -* For `rust_edition`, a lint will be issued if the literal has more than one component or if the value is not a recognized Rust edition. +* For `rust_edition`, a lint will be issued if the literal has more than one component or if we know the value is never going to be a Rust edition (for example, `"2019"`). * Pre-release identifiers (e.g., `"1.92-beta"`) are not supported in this RFC. They will be ignored during comparison and a lint will be emitted. See the "Unresolved Questions" section for further discussion. Using version-typed config values with the `=` predicate results in a hard error. From 14e2c4e30dfb5d80681242d77dad069b0075f101 Mon Sep 17 00:00:00 2001 From: Tyler Mandry Date: Tue, 6 Jan 2026 21:49:33 -0800 Subject: [PATCH 05/21] Generalize to any number of version components Thanks to @programmerjake for the Chromium example. --- text/0000-typed-cfgs.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/text/0000-typed-cfgs.md b/text/0000-typed-cfgs.md index c95754ace8e..268c0b50b6e 100644 --- a/text/0000-typed-cfgs.md +++ b/text/0000-typed-cfgs.md @@ -126,14 +126,14 @@ The `STRING_LITERAL` in a version predicate must conform to the following gramma ```text version_literal : - NUMERIC_COMPONENT ('.' NUMERIC_COMPONENT ('.' NUMERIC_COMPONENT)?)? + NUMERIC_COMPONENT ('.' NUMERIC_COMPONENT)* NUMERIC_COMPONENT : '0' | ('1'...'9') ('0'...'9')* ``` -This grammar defines a version as one, two, or three non-negative integer components separated by dots. Each component must not have leading zeros, unless the component itself is `0`. For example, `"1.90"` and `"0.2.0"` are valid, but `"1.09"` is not. +This grammar defines a version as one or more non-negative integer components separated by dots. Each component must not have leading zeros, unless the component itself is `0`. For example, `"1.90"` and `"0.2.0"` are valid, but `"1.09"` is not. There is a single, unified parsing and comparison logic that is part of the language's semantics. Additional checks for the built-in version keys are implemented as lints. @@ -212,14 +212,24 @@ While a good design, the "typed cfgs" approach with an actual comparison operato # Prior art [prior-art]: #prior-art +## Rust + - **Cargo's `rust-version`:** The `[package]` section of `Cargo.toml` can specify a `rust-version` field. This allows Cargo to select appropriate versions of dependencies and fail early if the compiler is too old. However, it does not provide fine-grained, in-code conditional compilation. This RFC brings a similar capability directly into the language, but for controlling code within a crate rather than for dependency resolution. +## Other languages + - **C++ (`__cplusplus`)**: The C++ standard defines the `__cplusplus` macro, which expands to an integer literal that increases with each new version of the standard (e.g., `201103L` for C++11, `202002L` for C++20). This allows for preprocessor checks like `#if __cplusplus >= 201103L`. This is very similar to the `rust_version >= "..."` proposal in that it uses standard comparison operators against a monotonically increasing value. However, it is less granular, as several years pass between new C++ versions. - **Clang/GCC (`__has_feature`, `__has_attribute`)**: These function-like macros allow for checking for the presence of specific compiler features, rather than the overall language version. For example, `__has_feature(cxx_rvalue_references)` checks for a specific language feature. This approach is more granular but also more verbose if one needs to check for many features at once. This approach was discussed in RFC #2523, but rejected, in part because we wanted to reinforce the idea of Rust as "one language" instead of a common subset with many compiler-specific extensions. - **Python (`sys.version_info`)**: Python exposes its version at runtime via `sys.version_info`, a tuple of integers `(major, minor, micro, ...)`. Code can check the version with standard tuple comparison, e.g., `if sys.version_info >= (3, 8):`. This component-wise comparison is very similar to the logic proposed in this RFC. However, because Python is interpreted, a file must be syntactically valid for the interpreter that is running it, which makes it difficult to use newer syntax in a file that must also run on an older interpreter. Rust, being a compiled language with a powerful conditional compilation system, does not have this limitation, and this RFC's design takes full advantage of that. +## Versioning systems + +Not every system uses Rust's standard three-part semver versioning scheme, but many are close. In this section are examples of more bespoke versioning systems that this feature can accommodate. + +- **Chromium**: Chromium's version format is a four-part number: MAJOR.MINOR.BUILD.PATCH, where MAJOR increments with significant releases, MINOR is often 0, BUILD tracks trunk builds, and PATCH reflects updates from a specific branch, with BUILD and PATCH together identifying the exact code revision. + # Unresolved questions [unresolved-questions]: #unresolved-questions From 0b9c25f0b16c8d277719fb5b2951a79acc7da517 Mon Sep 17 00:00:00 2001 From: Tyler Mandry Date: Wed, 7 Jan 2026 15:41:00 -0800 Subject: [PATCH 06/21] Clean up wording in reference-level and alternatives sections --- text/0000-typed-cfgs.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/text/0000-typed-cfgs.md b/text/0000-typed-cfgs.md index 268c0b50b6e..b25533207a6 100644 --- a/text/0000-typed-cfgs.md +++ b/text/0000-typed-cfgs.md @@ -91,19 +91,19 @@ For this RFC, the only supported comparison operators are `>=` and `<`. This RFC adds a new kind of predicate to the `cfg` attribute, allowing for comparison against version identifiers. -### `version` predicates +### Version predicates -A `version` predicate is available for `cfg` identifiers that are declared to be of type `version`. +A version predicate is available for `cfg` identifiers that are declared to be of type `version`. The following grammar will be added for `cfg` predicates: ```text -cfg_predicate : +ConfigurationPredicate : IDENTIFIER ('>=' | '<') STRING_LITERAL | ... ``` -This form of predicate is only valid when the `IDENTIFIER` on the left-hand side is a known `cfg` of type `version`. This RFC proposes two built-in `version` identifiers, `rust_version` and `rust_edition`, and a mechanism for build scripts and command-line tools to introduce new ones. +This form of predicate is only valid when the `IDENTIFIER` on the left-hand side names a known `cfg` option of type `version`. A `cfg` identifier is of type `version` if: * It is one of the built-in identifiers `rust_version` or `rust_edition`. @@ -111,7 +111,7 @@ A `cfg` identifier is of type `version` if: The `ident` must be a known `cfg` identifier of type `version`. The `literal` must be a string literal that represents a valid version. -A `version` predicate evaluates to `true` if the comparison is true, and `false` otherwise. If the identifier is not a known version `cfg`, or the literal is not a valid version string, a compile-time error is produced. +A `version` predicate evaluates to `true` if the comparison is true, and `false` otherwise. If the identifier is not a known version-typed `cfg`, or the literal is not a valid version string, a compile-time error is produced. ```rust #[cfg(rust_version >= "1.90")] @@ -186,7 +186,7 @@ This approach delivers the most critical functionality to users quickly, while a [rationale-and-alternatives]: #rationale-and-alternatives ### Why this design? -This design directly solves the MSRV problem in a way that previous attempts did not. The syntax `rust_version >= "1.85"` is highly intuitive and directly expresses the user's intent once the feature is available. It is a **principled** design, as by introducing a `version` type to the `cfg` system, we create a sound basis for comparison operators. This avoids the semantic confusion of proposals like `rust_version = "1.85"` which would have overloaded the meaning of `=` for a single special case. Furthermore, it's an extensible design that paves the way for other comparison operators, `cfg(some_dependency >= "1.2.3")`, or other typed `cfg`s in the future. +This design directly solves the MSRV problem in a way that previous attempts did not. The syntax `rust_version >= "1.85"` is highly intuitive and directly expresses the user's intent once the feature is available. It is a principled design, as by introducing a `version` type to the `cfg` system, we create a sound basis for comparison operators. This avoids the semantic confusion of proposals like `rust_version = "1.85"` which would have overloaded the meaning of `=` for a single special case. Furthermore, it's an extensible design that paves the way for other comparison operators, `cfg(some_dependency >= "1.2.3")`, or other typed `cfg`s in the future. ### Alternative 1: `#[cfg(version(1.85))]` (RFC 2523) This was the original accepted RFC for version-based conditional compilation. From 49b34d81de718107830503985a371d95f34a70cf Mon Sep 17 00:00:00 2001 From: Tyler Mandry Date: Wed, 7 Jan 2026 15:41:17 -0800 Subject: [PATCH 07/21] Reference-level: Version-typed cfgs as options --- text/0000-typed-cfgs.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/text/0000-typed-cfgs.md b/text/0000-typed-cfgs.md index b25533207a6..17cde4adbb0 100644 --- a/text/0000-typed-cfgs.md +++ b/text/0000-typed-cfgs.md @@ -145,7 +145,16 @@ There is a single, unified parsing and comparison logic that is part of the lang Using version-typed config values with the `=` predicate results in a hard error. -### Interaction with Compiler Flags +### Version-typed cfgs as options + +When cfg option with a version type and value is used as a bare option, it evalutes to true: + +```rust +#[cfg(rust_version)] +fn new_impl() { /* compiles */ } +``` + +### Defining version-typed configs and interaction with compiler flags The `version` type integrates with existing compiler flags. From 99ad7cb825c11a7acfbde4b5ff87c8a4204be324 Mon Sep 17 00:00:00 2001 From: Tyler Mandry Date: Wed, 7 Jan 2026 16:01:59 -0800 Subject: [PATCH 08/21] Clarify what happens when the same cfg is specified multiple times --- text/0000-typed-cfgs.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/text/0000-typed-cfgs.md b/text/0000-typed-cfgs.md index 17cde4adbb0..350fdb88462 100644 --- a/text/0000-typed-cfgs.md +++ b/text/0000-typed-cfgs.md @@ -167,6 +167,8 @@ The `version` type integrates with existing compiler flags. * If a version cfg is used with a string literal in a comparison that is not a valid version string, a hard error is emitted. * If a cfg that is not a version type is used in a version comparison, a hard error is emitted. For undefined cfgs, this could be relaxed to evaluate to false in the future. * If a cfg that is a version type is used in a non-version comparison (`=`), a hard error is emitted. + * Version typed cfgs are single-valued. Setting more than one value for the flag is a hard error. This includes values of other types, so given the example above, adding both `--cfg my_app_version` and `--cfg my_app_version="foo"` would cause a hard error. + * Setting the _same_ value multiple times on the command line should also be a hard error initially. This is a conservative choice that the compiler team may choose to relax, e.g. for build system integration reasons. * **`--print cfg`**: For the built-in `rust_version` and `rust_edition` cfgs, this flag will *not* print them by default to avoid breaking tools that parse this output. They are only printed if overridden via `--cfg`. User-defined version cfgs are printed in the `name=version("...")` format. From 4131682e5afc74bd155a22ddaea73167d62b90c3 Mon Sep 17 00:00:00 2001 From: Tyler Mandry Date: Wed, 7 Jan 2026 16:16:54 -0800 Subject: [PATCH 09/21] Elaborate the diagnostic use case --- text/0000-typed-cfgs.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/text/0000-typed-cfgs.md b/text/0000-typed-cfgs.md index 350fdb88462..94f094a7256 100644 --- a/text/0000-typed-cfgs.md +++ b/text/0000-typed-cfgs.md @@ -32,7 +32,7 @@ The primary blockers for existing solutions have been: This RFC proposes a solution that avoids these pitfalls. -A key motivating example is making it ergonomic to adopt attributes that were stabilized after a crate's MSRV. For example, the `#[diagnostic::on_unimplemented]` attribute is a stable feature that library authors can use to provide better error messages. However, if a library has an MSRV from before this attribute was stabilized, they cannot use it without a build script. A build script is often too much overhead for such a small, non-essential feature. +One motivating example is making it ergonomic to adopt attributes that were stabilized after a crate's MSRV. For example, the `#[diagnostic::on_unimplemented]` attribute is a stable feature that library authors can use to provide better error messages. However, if a library has an MSRV from before this attribute was stabilized, they cannot use it without a build script. A build script is often too much overhead for such a small, non-essential feature. This RFC makes it trivial to adopt even in a crate that doesn't want to use a build script. In this case, since the diagnostic attribute namespace landed before `rust_version`, you would write @@ -44,7 +44,7 @@ This RFC makes it trivial to adopt even in a crate that doesn't want to use a bu impl MyTrait for T { /* ... */ } ``` -With this feature, we will hopefully see more people using useful attributes like `on_unimplemented` even with MSRVs before when the diagnostic attribute namespace was added. The ability to conditionally add attributes for newer features is further detailed in the guide-level explanation. +With this feature we hope to see more people using useful attributes like `on_unimplemented`, even with MSRVs before when the diagnostic attribute namespace was added. Gated `diagnostic` attributes like this will not be active until the Rust version where this feature ships, but adding them still adds value. While some crates must hold a low MSRV to allow building in environments with older compilers, like Linux distros, most active Rust development still takes place on recent compiler versions. Using this gating mechanism will mean that most users of a crate benefit from the attributes, without changing the crate's MSRV or adding a build script. # Guide-level explanation [guide-level-explanation]: #guide-level-explanation From 3545fd1cd7d52380f43b23def7b90d7d8a0c3d78 Mon Sep 17 00:00:00 2001 From: Tyler Mandry Date: Wed, 7 Jan 2026 16:32:47 -0800 Subject: [PATCH 10/21] Fix parsing ambiguity in check-cfg --- text/0000-typed-cfgs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-typed-cfgs.md b/text/0000-typed-cfgs.md index 94f094a7256..133398a8082 100644 --- a/text/0000-typed-cfgs.md +++ b/text/0000-typed-cfgs.md @@ -174,7 +174,7 @@ The `version` type integrates with existing compiler flags. * **`--check-cfg`**: To inform the compiler that a `cfg` is expected to be a version, and to enable linting, use: ```sh - --check-cfg 'cfg(my_app_version, version)' + --check-cfg 'cfg(my_app_version, version())' ``` The built-in `rust_version` and `rust_edition` cfgs are implicitly included, so `rustc --print=check-cfg` will always list them. From ccb85c5e9113cdd70b586602010691a19829d0fc Mon Sep 17 00:00:00 2001 From: Tyler Mandry Date: Wed, 7 Jan 2026 16:37:20 -0800 Subject: [PATCH 11/21] Elaborate check-cfg possibilities --- text/0000-typed-cfgs.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/text/0000-typed-cfgs.md b/text/0000-typed-cfgs.md index 133398a8082..4c648d6c286 100644 --- a/text/0000-typed-cfgs.md +++ b/text/0000-typed-cfgs.md @@ -249,11 +249,14 @@ Not every system uses Rust's standard three-part semver versioning scheme, but m # Future possibilities [future-possibilities]: #future-possibilities +- **More expressive check-cfg:** We can support specifying an expected number of components in check-cfg, or an expected set of values to compare against, as in editions: + - `--check-cfg 'cfg(foo, version("2018", "2022", "2025"))'` + - `--check-cfg 'cfg(foo, version(values >= "1.75"))'` + - `--check-cfg 'cfg(foo, version(components <= 2))'` - **"Compatible-with" operator:** We could introduce a `~=` operator that works like Cargo's caret requirements. For example, `cfg(some_dep ~= "1.5")` would be equivalent to `cfg(all(some_dep >= "1.5", some_dep < "2.0"))`. The rationale for not doing this now is that it's easy enough to write by hand. - **More comparison operators:** While this RFC only proposes `>=` and `<`, the underlying `version` type makes it natural to add support for `<=`, `==`, `!=`, etc., in the future. -- **More flexible version strings:** The version string parsing could be extended to support pre-release identifiers (`-beta`, `-nightly`), though this adds complexity to the comparison logic. +- **Pre-releases:** The version string parsing could be extended to support pre-release identifiers (`-beta`, `-nightly`), though this adds complexity to the comparison logic. - **Dependency Version `cfg`s:** The "typed `cfg`" infrastructure could be extended to query the versions of direct dependencies, e.g., `#[cfg(serde >= "1.0.152")]`. This would require significant integration with Cargo. - **Other `cfg` types:** We could introduce other types, such as integers or single-valued strings. This could be useful for a variety of features, from system library versioning schemes ([kconfig](https://docs.kernel.org/kbuild/kconfig-language.html)) to enabling things like [mutually exclusive global features](https://internals.rust-lang.org/t/pre-rfc-mutually-excusive-global-features/19618). - **Namespaced `cfg`s:** We could group Rust-specific `cfg`s under a `rust::` namespace, e.g., `#[cfg(rust::version >= "1.85")]`. This RFC intentionally keeps `rust_version` at the top level to simplify the initial implementation and stabilization, but namespacing could be explored in the future to better organize the growing number of built-in `cfg`s. - **Short-circuiting `cfg` predicates:** Change `any` and `all` predicates to short-circuit instead of evaluating all their arguments. This would make introducing new predicates and comparison operators much easier. -- **More expressive check-cfg:** We can support specifying an expected number of components in check-cfg, or an expected set of values to compare against, as in editions. From 00ec29328a1c0865c5d2e932951fe6a7bcfeec8e Mon Sep 17 00:00:00 2001 From: Tyler Mandry Date: Wed, 7 Jan 2026 16:43:13 -0800 Subject: [PATCH 12/21] Clarify check-cfg version() behavior --- text/0000-typed-cfgs.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/text/0000-typed-cfgs.md b/text/0000-typed-cfgs.md index 4c648d6c286..c5e68d51814 100644 --- a/text/0000-typed-cfgs.md +++ b/text/0000-typed-cfgs.md @@ -170,13 +170,16 @@ The `version` type integrates with existing compiler flags. * Version typed cfgs are single-valued. Setting more than one value for the flag is a hard error. This includes values of other types, so given the example above, adding both `--cfg my_app_version` and `--cfg my_app_version="foo"` would cause a hard error. * Setting the _same_ value multiple times on the command line should also be a hard error initially. This is a conservative choice that the compiler team may choose to relax, e.g. for build system integration reasons. -* **`--print cfg`**: For the built-in `rust_version` and `rust_edition` cfgs, this flag will *not* print them by default to avoid breaking tools that parse this output. They are only printed if overridden via `--cfg`. User-defined version cfgs are printed in the `name=version("...")` format. - * **`--check-cfg`**: To inform the compiler that a `cfg` is expected to be a version, and to enable linting, use: ```sh --check-cfg 'cfg(my_app_version, version())' ``` - The built-in `rust_version` and `rust_edition` cfgs are implicitly included, so `rustc --print=check-cfg` will always list them. + + This will accept any version value, but lint when the option is used as something other than a version. This is a more sensible default for versions, which don't have the equivalent of `values(none())`. + +* **`--print cfg`**: For the built-in `rust_version` and `rust_edition` cfgs, this flag will *not* print them by default to avoid breaking tools that parse this output. They are only printed if overridden via `--cfg`. User-defined version cfgs are printed in the `name=version("...")` format. + +* **`--print check-cfg`**: The built-in `rust_version` and `rust_edition` cfgs are implicitly included, so `rustc --print=check-cfg` will always list them. ### Stabilization From cb977fb8fcdd4f6785ab56d885acbd5d8e7e0b39 Mon Sep 17 00:00:00 2001 From: Tyler Mandry Date: Thu, 8 Jan 2026 16:39:20 -0800 Subject: [PATCH 13/21] Rename feature to version_cfgs --- text/0000-typed-cfgs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-typed-cfgs.md b/text/0000-typed-cfgs.md index c5e68d51814..21266d9f4e3 100644 --- a/text/0000-typed-cfgs.md +++ b/text/0000-typed-cfgs.md @@ -1,4 +1,4 @@ -- Feature Name: typed_cfgs +- Feature Name: version_cfgs - Start Date: 2026-01-06 - RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) - Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) From 359d341298ef2372f806de536319e5df3fdd3f5e Mon Sep 17 00:00:00 2001 From: Tyler Mandry Date: Thu, 8 Jan 2026 16:39:44 -0800 Subject: [PATCH 14/21] Drawbacks wording --- text/0000-typed-cfgs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-typed-cfgs.md b/text/0000-typed-cfgs.md index 21266d9f4e3..a5dce3ffabc 100644 --- a/text/0000-typed-cfgs.md +++ b/text/0000-typed-cfgs.md @@ -193,7 +193,7 @@ This approach delivers the most critical functionality to users quickly, while a # Drawbacks [drawbacks]: #drawbacks -- Increased compiler complexity. This introduces a new concept of "typed" `cfg`s into the compiler, which adds a small amount of complexity to the parsing and evaluation logic for conditional compilation. +- Increased compiler complexity. This introduces a new concept of "typed" `cfg`s into the compiler, which adds complexity to the parsing and evaluation logic for conditional compilation. - Subtlety of MSRV-preserving patterns: The need for the "stacked `cfg`" pattern (`#[cfg(rust_version)] #[cfg(rust_version >= ...)]` and `#[cfg_attr(rust_version, cfg(rust_version >= ...))]`) is subtle. While we will add lints to guide users, it's less direct than a simple predicate. However, this subtlety is the explicit tradeoff made to achieve MSRV compatibility. # Rationale and alternatives From c66cab031ad4a4d9da7b0cbcccfb80cd33c8af30 Mon Sep 17 00:00:00 2001 From: Tyler Mandry Date: Thu, 8 Jan 2026 17:05:24 -0800 Subject: [PATCH 15/21] Include more prior art from RFC 3857 --- text/0000-typed-cfgs.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/text/0000-typed-cfgs.md b/text/0000-typed-cfgs.md index a5dce3ffabc..8b018df29ff 100644 --- a/text/0000-typed-cfgs.md +++ b/text/0000-typed-cfgs.md @@ -226,12 +226,30 @@ While a good design, the "typed cfgs" approach with an actual comparison operato # Prior art [prior-art]: #prior-art -## Rust +_Parts of this section are adapted from [RFC 3857](https://github.com/rust-lang/rfcs/pull/3857)._ -- **Cargo's `rust-version`:** The `[package]` section of `Cargo.toml` can specify a `rust-version` field. This allows Cargo to select appropriate versions of dependencies and fail early if the compiler is too old. However, it does not provide fine-grained, in-code conditional compilation. This RFC brings a similar capability directly into the language, but for controlling code within a crate rather than for dependency resolution. +## Rust Ecosystem + +There are very widely used crates designed to work around the lack of native version-based conditional compilation. These rely on build scripts to detect the compiler version and set custom `cfg` flags. `rustversion` also has a proc macro component for the nicest user experience. + +- **`rustversion`**: A popular proc-macro (over 260 million downloads) that allows checks like `#[rustversion::since(1.34)]`. It supports channel checks (stable, beta, nightly), equality, and range comparisons. +- **Build Script Helpers**: Crates like **`rustc_version`** and **`version_check`** are widely used in `build.rs` scripts to query the compiler version and emit `cargo:rustc-cfg` instructions. They provide programmatic access to version components, channels, and release dates. +- **Release Info**: Crates like **`shadow-rs`** and **`vergen`** expose build information, including the compiler version, to the compiled binary. +- **Polyfills**: Some crates, like `is_terminal_polyfill`, maintain separate versions for different MSRVs, relying on Cargo's [MSRV-aware resolver](https://rust-lang.github.io/rfcs/3537-msrv-resolver.html) to select the correct implementation. + +This RFC aims to obviate the need for these external dependencies for the common case of checking the language version, reducing build times and complexity. + +## Cargo + +- **`rust-version`:** The `[package]` section of `Cargo.toml` can specify a `rust-version` field. This allows Cargo to select appropriate versions of dependencies and fail early if the compiler is too old. However, it does not provide fine-grained, in-code conditional compilation. This RFC brings a similar capability directly into the language, but for controlling code within a crate rather than for dependency resolution. ## Other languages +- **Swift (`#if compiler`, `#if swift`)**: Swift provides platform conditions for both the compiler version and the language mode. + - `compiler(>=5)` checks the version of the compiler. + - `swift(>=4.2)` checks the active language version mode. + - These support `>=` and `<` operators, similar to this proposal. + - **C++ (`__cplusplus`)**: The C++ standard defines the `__cplusplus` macro, which expands to an integer literal that increases with each new version of the standard (e.g., `201103L` for C++11, `202002L` for C++20). This allows for preprocessor checks like `#if __cplusplus >= 201103L`. This is very similar to the `rust_version >= "..."` proposal in that it uses standard comparison operators against a monotonically increasing value. However, it is less granular, as several years pass between new C++ versions. - **Clang/GCC (`__has_feature`, `__has_attribute`)**: These function-like macros allow for checking for the presence of specific compiler features, rather than the overall language version. For example, `__has_feature(cxx_rvalue_references)` checks for a specific language feature. This approach is more granular but also more verbose if one needs to check for many features at once. This approach was discussed in RFC #2523, but rejected, in part because we wanted to reinforce the idea of Rust as "one language" instead of a common subset with many compiler-specific extensions. From b148dbf3537a7f05b7f366a7f1c050c8025b0712 Mon Sep 17 00:00:00 2001 From: Tyler Mandry Date: Thu, 8 Jan 2026 17:29:17 -0800 Subject: [PATCH 16/21] Include more points from RFC 3857 --- text/0000-typed-cfgs.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/text/0000-typed-cfgs.md b/text/0000-typed-cfgs.md index 8b018df29ff..0400077f673 100644 --- a/text/0000-typed-cfgs.md +++ b/text/0000-typed-cfgs.md @@ -18,8 +18,8 @@ This design solves a long-standing problem of conditionally compiling code for d There are two primary use cases for conditional compilation based on versions, whether that is the version of the Rust language or the version of a dependency: -1. **Exposing new features** when code is compiled with newer versions of the language or a library. -2. **Straddling a breaking change** in an API, allowing code to compile against both the old and new versions. +1. **Exposing new features** when code is compiled with newer versions of the language or library while maintaining compatibility with older versions. +2. **Straddling a breaking change** in an API, allowing code to compile against both the old and new versions, which are mutually incompatible. The `rust_version` cfg proposed in this RFC is designed for the first use case, while `rust_edition` is a tool for the second. This RFC also provides a general mechanism for `version`-typed `cfg`s that can be used for libraries, which addresses both use cases. @@ -28,7 +28,7 @@ This RFC also aims to solve long-standing problems that have been approached wit The primary blockers for existing solutions have been: - **Build Scripts are a Poor Solution:** The only stable tool for this today is a build script (`build.rs`). However, build scripts add significant compilation overhead and are clunky to write and maintain. -- **Previous Attempts had Flaws:** Past RFCs have tried to solve this, but ran into an unfortunate issue: their proposed syntax, e.g. `#[cfg(version(1.85))]`, was a syntax error on older compilers. This means that to use the feature, a library would first have to bump its MSRV to the version that introduced the syntax, defeating the primary purpose of the feature. +- **Previous Attempts had Flaws:** Past RFCs have tried to solve this, but ran into an unfortunate issue: their proposed syntax, e.g. `#[cfg(version(1.85))]`, was a syntax error on older compilers. This means that to use the feature, a library would first have to bump its MSRV to the version that introduced the syntax, somewhat defeating the primary purpose of the feature. This RFC proposes a solution that avoids these pitfalls. @@ -195,6 +195,9 @@ This approach delivers the most critical functionality to users quickly, while a - Increased compiler complexity. This introduces a new concept of "typed" `cfg`s into the compiler, which adds complexity to the parsing and evaluation logic for conditional compilation. - Subtlety of MSRV-preserving patterns: The need for the "stacked `cfg`" pattern (`#[cfg(rust_version)] #[cfg(rust_version >= ...)]` and `#[cfg_attr(rust_version, cfg(rust_version >= ...))]`) is subtle. While we will add lints to guide users, it's less direct than a simple predicate. However, this subtlety is the explicit tradeoff made to achieve MSRV compatibility. +- The "stacked `cfg`" pattern does not work inside Cargo, so users will not be able to use this feature in Cargo until their MSRV is bumped. For cases where a dependency needs to be conditional on the Rust version, one can define a "polyfill" crate and make use of the MSRV-aware feature resolver, like the `is_terminal_polyfill` crate does. +- Conditional compilation adds testing complexity. In practice, most crate maintainers only test their MSRV and the latest stable. +- This does not support branching on specific nightly versions. rustversion supports this with syntax like `#[rustversion::since(2025-01-01)]`. # Rationale and alternatives [rationale-and-alternatives]: #rationale-and-alternatives @@ -276,8 +279,9 @@ Not every system uses Rust's standard three-part semver versioning scheme, but m - `--check-cfg 'cfg(foo, version(components <= 2))'` - **"Compatible-with" operator:** We could introduce a `~=` operator that works like Cargo's caret requirements. For example, `cfg(some_dep ~= "1.5")` would be equivalent to `cfg(all(some_dep >= "1.5", some_dep < "2.0"))`. The rationale for not doing this now is that it's easy enough to write by hand. - **More comparison operators:** While this RFC only proposes `>=` and `<`, the underlying `version` type makes it natural to add support for `<=`, `==`, `!=`, etc., in the future. -- **Pre-releases:** The version string parsing could be extended to support pre-release identifiers (`-beta`, `-nightly`), though this adds complexity to the comparison logic. +- **Pre-releases:** The version string parsing could be extended to support pre-release identifiers (`-beta`, `-nightly`), though this adds complexity to the comparison logic. RFC 3857 discusses this possibility for [generic versions](https://github.com/rust-lang/rfcs/blob/4551bbd827eb84fc6673ac0204506321274ea839/text/3857-cfg-version.md#relaxing-version) as well as for the [language itself](https://github.com/rust-lang/rfcs/blob/4551bbd827eb84fc6673ac0204506321274ea839/text/3857-cfg-version.md#pre-release). - **Dependency Version `cfg`s:** The "typed `cfg`" infrastructure could be extended to query the versions of direct dependencies, e.g., `#[cfg(serde >= "1.0.152")]`. This would require significant integration with Cargo. - **Other `cfg` types:** We could introduce other types, such as integers or single-valued strings. This could be useful for a variety of features, from system library versioning schemes ([kconfig](https://docs.kernel.org/kbuild/kconfig-language.html)) to enabling things like [mutually exclusive global features](https://internals.rust-lang.org/t/pre-rfc-mutually-excusive-global-features/19618). - **Namespaced `cfg`s:** We could group Rust-specific `cfg`s under a `rust::` namespace, e.g., `#[cfg(rust::version >= "1.85")]`. This RFC intentionally keeps `rust_version` at the top level to simplify the initial implementation and stabilization, but namespacing could be explored in the future to better organize the growing number of built-in `cfg`s. +- **Macro that evaluates to a cfg value:** We can add a `cfg_value!()` macro for single-valued configs that evalutes to its value. - **Short-circuiting `cfg` predicates:** Change `any` and `all` predicates to short-circuit instead of evaluating all their arguments. This would make introducing new predicates and comparison operators much easier. From b5e9e5c2abbf70b1616fcecc539931c4ae205a86 Mon Sep 17 00:00:00 2001 From: Tyler Mandry Date: Thu, 8 Jan 2026 17:30:11 -0800 Subject: [PATCH 17/21] Rename RFC to version-typed cfgs --- text/{0000-typed-cfgs.md => 0000-version-typed-cfgs.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename text/{0000-typed-cfgs.md => 0000-version-typed-cfgs.md} (100%) diff --git a/text/0000-typed-cfgs.md b/text/0000-version-typed-cfgs.md similarity index 100% rename from text/0000-typed-cfgs.md rename to text/0000-version-typed-cfgs.md From 042d5078913cbe2cca46dc6849f0c5b430dae370 Mon Sep 17 00:00:00 2001 From: Tyler Mandry Date: Thu, 8 Jan 2026 17:53:08 -0800 Subject: [PATCH 18/21] Expand versioning systems section --- text/0000-version-typed-cfgs.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/text/0000-version-typed-cfgs.md b/text/0000-version-typed-cfgs.md index 0400077f673..8d443d49dba 100644 --- a/text/0000-version-typed-cfgs.md +++ b/text/0000-version-typed-cfgs.md @@ -261,9 +261,18 @@ This RFC aims to obviate the need for these external dependencies for the common ## Versioning systems -Not every system uses Rust's standard three-part semver versioning scheme, but many are close. In this section are examples of more bespoke versioning systems that this feature can accommodate. +Not every system uses Rust's standard three-part semver versioning scheme, but many are close. In this section are examples of more bespoke versioning systems that this feature can accommodate. The "escape hatch" for when your version numbers are not semver like is to split them into different version cfgs, each of which is semver like (in the simplest case, just a number). -- **Chromium**: Chromium's version format is a four-part number: MAJOR.MINOR.BUILD.PATCH, where MAJOR increments with significant releases, MINOR is often 0, BUILD tracks trunk builds, and PATCH reflects updates from a specific branch, with BUILD and PATCH together identifying the exact code revision. +- **Chromium**: Chromium's version format is a four-part number: MAJOR.MINOR.BUILD.PATCH, where MAJOR increments with significant releases, MINOR is often 0, BUILD tracks trunk builds, and PATCH reflects updates from a specific branch, with BUILD and PATCH together identifying the exact code revision. (Thanks to Jacob Lifshay [on github](https://github.com/rust-lang/rfcs/pull/3905#discussion_r2666956191).) + +### Operating systems + +Operating systems include many versions, including kernel versions, public OS version, and system API versions. Usually API versions are the most relevant for conditional compilation. Most APIs are preserved across versions. Some operating systems, like Windows, prioritize backward compatibility of applications, while others balance backward compatibility with the deprecation and removal of APIs. + +- **Windows API version**: XP is 5.1, Vista is 6.0, 7 is 7.0, 7 with Service Pack 1 is 7.1, 8 is 8.0, 8.1 is 8.1 and Windows 10/11 currently ranges from 10.0.10240 to 10.0.26200. There are "friendlier" names such as 1507 for 10.0.10240 but I think those are better done as some kind of cfg alias rather than being built-in. (Thanks to Chris Denton [on zulip](https://rust-lang.zulipchat.com/#narrow/channel/213817-t-lang/topic/cfg.28version.28.2E.2E.29.29.20as.20a.20version.20comparison.20predicate/near/540907580).) +- **Android API level**: Single, sequential integer value like "34", "35". +- **macOS version**: Based on the operating system version; there is not a separate API level concept. In general these use multi-part versions like "10.15". Starting with macOS 11.0, the major version number has increased with each new version and the second component has been 0. +- **Fuchsia API version**: Single number like "30", similar to Android, but released on a cadence closer to Rust's 6-week release cadence. Fuchsia itself uses Rust along with some build system hacks to express predicates like `fuchsia_api_level_less_than = "20"`. (Thanks to Hunter Freyer [on zulip](https://rust-lang.zulipchat.com/#narrow/channel/213817-t-lang/topic/cfg.28version.28.2E.2E.29.29.20as.20a.20version.20comparison.20predicate/near/539610890).) # Unresolved questions [unresolved-questions]: #unresolved-questions From d7b6f8f2d9484c0b669e351b9cb35397e4b14784 Mon Sep 17 00:00:00 2001 From: Tyler Mandry Date: Thu, 8 Jan 2026 17:56:51 -0800 Subject: [PATCH 19/21] Mention link between --print cfg and = --- text/0000-version-typed-cfgs.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/text/0000-version-typed-cfgs.md b/text/0000-version-typed-cfgs.md index 8d443d49dba..9a8c4b83f82 100644 --- a/text/0000-version-typed-cfgs.md +++ b/text/0000-version-typed-cfgs.md @@ -217,6 +217,8 @@ This syntax is parseable by older compilers, which is a significant advantage fo #### Rationale for not choosing The use of `=` was highly controversial. In prior art, the `cfg` syntax has two conceptual models: "set inclusion" for multi-valued cfgs (e.g., `feature = "serde"`) and "queries" for boolean flags (e.g., `unix`). For set inclusion, `=` makes sense. However, checking the compiler version is a query for a single value, not a check for inclusion in a set. In this context, `=` strongly implies exact equality (`== 1.85`), while the overwhelming use case is for a lower bound (`>= 1.85`). Overloading `=` for this purpose was considered unprincipled and confusing. This RFC's approach of introducing a `version` type provides a proper semantic foundation for comparison operators like `>=`. +The interaction with `--print cfg` was also unclear. See [RFC 3857](https://github.com/rust-lang/rfcs/blob/4551bbd827eb84fc6673ac0204506321274ea839/text/3857-cfg-version.md#cfgrust--195-1) for more context. + #### Advantage This approach *could* potentially be made to work inside `Cargo.toml` (e.g., for conditional dependencies), which currently cannot use the stacked-cfg trick. However, the disadvantages in terms of semantic clarity for the language itself outweigh this benefit for an in-language feature. From 6b1476036460796f7b184f3c12e2390f7f0b570c Mon Sep 17 00:00:00 2001 From: Tyler Mandry Date: Thu, 8 Jan 2026 18:03:52 -0800 Subject: [PATCH 20/21] Make "print cfg" an unresolved question --- text/0000-version-typed-cfgs.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/text/0000-version-typed-cfgs.md b/text/0000-version-typed-cfgs.md index 9a8c4b83f82..4b2e7c71abf 100644 --- a/text/0000-version-typed-cfgs.md +++ b/text/0000-version-typed-cfgs.md @@ -177,7 +177,7 @@ The `version` type integrates with existing compiler flags. This will accept any version value, but lint when the option is used as something other than a version. This is a more sensible default for versions, which don't have the equivalent of `values(none())`. -* **`--print cfg`**: For the built-in `rust_version` and `rust_edition` cfgs, this flag will *not* print them by default to avoid breaking tools that parse this output. They are only printed if overridden via `--cfg`. User-defined version cfgs are printed in the `name=version("...")` format. +* **`--print cfg`**: User-defined version cfgs are printed in the `name=version("...")` format. Whether to print the built-in `rust_version` and `rust_edition` cfgs is left as an unresolved question to be determined based on tool compatibility. In future editions, the builtin cfgs should always be printed. * **`--print check-cfg`**: The built-in `rust_version` and `rust_edition` cfgs are implicitly included, so `rustc --print=check-cfg` will always list them. @@ -280,6 +280,7 @@ Operating systems include many versions, including kernel versions, public OS ve [unresolved-questions]: #unresolved-questions - How should pre-release identifiers in version strings be handled? This RFC proposes not supporting pre-release identifiers in version strings passed on the command line for now. For comparisons, this RFC proposes that if a pre-release identifier is present in a `cfg` predicate (e.g., `rust_version < "2.0-alpha"`), the pre-release part is ignored for the comparison (so it's treated as `2.0`), and a lint is emitted. This ensures forward compatibility, as comparisons like `cfg(all(foo >= "2.0-alpha", foo < "2.0"))` become trivially false on older compilers, which is a safe outcome. This behavior can be refined before stabilization. +- Should the builtin `rust_version` and `rust_edition` be printed with `--print cfg` on the command line? We'd like the eventual answer to be "yes", but existing tools that parse the output might break with the new `rust_version=version("1.99")` syntax. If we can manage the breakage we should; otherwise we can gate it on a future edition. # Future possibilities [future-possibilities]: #future-possibilities From a856c55c1e534e2ba20738ddaf01cf2be21704d3 Mon Sep 17 00:00:00 2001 From: Tyler Mandry Date: Thu, 8 Jan 2026 18:20:26 -0800 Subject: [PATCH 21/21] Future possibility for sys crates --- text/0000-version-typed-cfgs.md | 1 + 1 file changed, 1 insertion(+) diff --git a/text/0000-version-typed-cfgs.md b/text/0000-version-typed-cfgs.md index 4b2e7c71abf..2bf03c58d37 100644 --- a/text/0000-version-typed-cfgs.md +++ b/text/0000-version-typed-cfgs.md @@ -293,6 +293,7 @@ Operating systems include many versions, including kernel versions, public OS ve - **More comparison operators:** While this RFC only proposes `>=` and `<`, the underlying `version` type makes it natural to add support for `<=`, `==`, `!=`, etc., in the future. - **Pre-releases:** The version string parsing could be extended to support pre-release identifiers (`-beta`, `-nightly`), though this adds complexity to the comparison logic. RFC 3857 discusses this possibility for [generic versions](https://github.com/rust-lang/rfcs/blob/4551bbd827eb84fc6673ac0204506321274ea839/text/3857-cfg-version.md#relaxing-version) as well as for the [language itself](https://github.com/rust-lang/rfcs/blob/4551bbd827eb84fc6673ac0204506321274ea839/text/3857-cfg-version.md#pre-release). - **Dependency Version `cfg`s:** The "typed `cfg`" infrastructure could be extended to query the versions of direct dependencies, e.g., `#[cfg(serde >= "1.0.152")]`. This would require significant integration with Cargo. +- **System library versions supplied by `sys` crates:** Cargo could allow `sys` crates to expose the versions of their system libraries to dependents as version-typed cfgs. - **Other `cfg` types:** We could introduce other types, such as integers or single-valued strings. This could be useful for a variety of features, from system library versioning schemes ([kconfig](https://docs.kernel.org/kbuild/kconfig-language.html)) to enabling things like [mutually exclusive global features](https://internals.rust-lang.org/t/pre-rfc-mutually-excusive-global-features/19618). - **Namespaced `cfg`s:** We could group Rust-specific `cfg`s under a `rust::` namespace, e.g., `#[cfg(rust::version >= "1.85")]`. This RFC intentionally keeps `rust_version` at the top level to simplify the initial implementation and stabilization, but namespacing could be explored in the future to better organize the growing number of built-in `cfg`s. - **Macro that evaluates to a cfg value:** We can add a `cfg_value!()` macro for single-valued configs that evalutes to its value.