Skip to content

Conversation

@tmandry
Copy link
Member

@tmandry tmandry commented Jan 7, 2026

This RFC proposes a general mechanism for version-based conditional compilation called "typed cfgs".

Summary

This RFC proposes "typed cfgs", a new form of conditional compilation predicate that understands types. Initially, this RFC proposes to add support for version-typed cfgs, 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.

History

There have been several previous attempts to solve the problem of conditional compilation by Rust version.

This RFC takes the lessons from both previous attempts. It proposes a path to the ergonomic rust_version >= "..." syntax that was preferred during language team discussions, while providing a clear MSRV-compatibility story from day one.

The RFC also incorporates use cases from the cfg_target_version RFC (#3750), which proposed a way to compare against the version of the target platform's SDK (e.g., #[cfg(target_version(macos >= "10.15"))]). Version-typed cfgs provide a path to supporting these comparions.

Finally, it takes cues from previous discussions around mutually exclusive features and a cfg_value!() macro, and lays out a path toward more single-valued config types that could support these features.

Rendered

@ehuss ehuss added the T-lang Relevant to the language team, which will review and decide on the RFC. label Jan 7, 2026
Copy link
Contributor

@weiznich weiznich left a comment

Choose a reason for hiding this comment

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

Overall I linke the idea, but I have some questions and remarks

(Obvious disclaimer: I'm not associated with any team, that's just my personal opinion, so feel free to ignore that)

Comment on lines +79 to +85
#[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 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.
Copy link
Contributor

Choose a reason for hiding this comment

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

I would be interested in seeing an actual example where this would have been useful. As far as I know macros use the same edition of the crate defining the macro, not the edition of the calling crate.

(Well beside emitting errors that a crate/generated code doesn't support edition 2024 or something like that, which might be problematic for the ecosystem overall)

Copy link
Contributor

Choose a reason for hiding this comment

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

Code generation is the motivating case. In shipping the 2024 edition, we tracked various problems related to this such as:

(While we shipped unsafe extern in all editions, the generator could maintain a lower MSRV on its output if that output included #[cfg(rust_edition))] in some cases. The generator could support, of course, an --edition flag to conditionalize its output rather than conditionalizing in the output, but that asks more of users, and in general it's convenient when the same generated code can be accepted across editions.)

We also tracked certain macro-related problems such as:

With macros that define macros, edition hygiene does not actually work in the way that we might ideally like. Maybe or maybe not #[cfg(rust_edition))] could have helped in averting some problems. Even if not, it doesn't bother me; it's the code generation case I find motivating.

@tgross35
Copy link
Contributor

tgross35 commented Jan 7, 2026

(Obvious disclaimer: I'm not associated with any team, that's just my personal opinion, so feel free to ignore that)

RFCs are the place for community feedback. Insight from future feature users that aren't team members is just as (if not more) valuable, and always welcome in all discussion areas ❤️

@@ -0,0 +1,265 @@
- Feature Name: typed_cfgs
Copy link
Contributor

Choose a reason for hiding this comment

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

Sort of a meta comment: this RFC is called typed_cfgs Unless I overlooked something, however, it only talks about version-typed configs, and that is of course the main content of the RFC. Maybe "Version-typed cfgs" would be a more fitting name?

Copy link
Contributor

Choose a reason for hiding this comment

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

Title aside, if accepting this RFC means accepting the concept of "typed cfgs", I think something about the mental model here is worth introducing in its own section. E.g. is existing config syntax "untyped"? Or is this proposing that cfg(all(foo, bar = "baz", version >= "1.2.3")) can be thought of as something like:

struct Version(/* ... */);
impl PartialOrd<&str> for Version { /* ... */ }

fn cfg_predicate(foo: bool, bar: Set, version: Version, ...) -> bool {
    [foo, bar.contains("baz"), version >= "1.2.3"].iter().all()
}

For some prior art, C says that everything in an #if declaration gets intmax_t type and the expression must evaluate to an integer, which is then compared to 0.

Copy link
Member

@yoshuawuyts yoshuawuyts Jan 8, 2026

Choose a reason for hiding this comment

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

Hah, I was just typing up a question about this myself, only to realize this is asking basically the same thing. The Effectful Target Tracking experiment wants to bring cfg-predicates into the type system as effect types. If we can spell out how typed cfgs could (at least conceptually) be lowered to types, that would be what we need to figure out how to bridge the two.

I believe @traviscross asked something similar in a previous lang team meeting, about potentially making cfg-features resolvable by const-eval/comptime functions. I believe to explore that too, having a (conceptual) type system representation would be helpful as well.

Copy link
Contributor

Choose a reason for hiding this comment

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

TC said:

If we instead had typed cfg values we could...

What I'd really like eventually is to not have a DSL here at all and just use Rust within cfg attributes. We already have a Rust interpreter on hand -- consteval. Obviously nothing from the crate itself would be in scope.

Clearly I don't think cfg_version should wait for that. But when we start talking about typed cfg values, generic predicates, and, presumably therefore, type inference and type checking for cfg, it does make me wonder whether using Rust here and consteval might not be more straightforward after all.

Copy link
Contributor

@traviscross traviscross Jan 9, 2026

Choose a reason for hiding this comment

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

Or is this proposing that cfg(all(foo, bar = "baz", version >= "1.2.3")) can be thought of as something like:

struct Version(/* ... */);
impl PartialOrd<&str> for Version { /* ... */ }

fn cfg_predicate(foo: bool, bar: Set, version: Version, ...) -> bool {
    [foo, bar.contains("baz"), version >= "1.2.3"].iter().all()
}

I'd rather prefer, e.g., if we could write something like:

#[cfg_eval(OPTS.contains("foo") && OPTS.get_eq("bar", "baz") && version >= "1.2.3")]

Copy link
Member Author

Choose a reason for hiding this comment

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

You're right about the title. I renamed the RFC in b5e9e5c.

Copy link
Member Author

Choose a reason for hiding this comment

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

My mental model is that existing cfgs are a set of strings-or-none, like you suggest with the code sample. I'm not sure where this would go in the RFC.


* 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).
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this indented? This is not further information about a number of components lint but describing a completely different lint, making it harder to notice (case in point: #3905 (comment))


* **`--print check-cfg`**: The built-in `rust_version` and `rust_edition` cfgs are implicitly included, so `rustc --print=check-cfg` will always list them. We can add these immediately because `--print check-cfg` is unstable.

* **Clippy**: Clippy's `incompatible_msrv` lint should be updated to respect `rust_version` checks, avoiding false positives when code is guarded by a sufficient `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.

This seems unrelated to the section it is under:

The version type integrates with existing compiler flags.

This will accept any version value, but lint when the option is used in a non-version comparison (note that this is an error if the option actually has a version-typed value). This is a more sensible default for versions, which don't have the equivalent of `values(none())`.

* **`--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.
* Note: Using editions being careful about passing `--edition` to `rustc --print cfg` invocations, which `cargo` for example does not currently do. This could introduce unexpected inconsistencies.
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like this could easily be lost when discussing the unresolved question around this. Should this be moved under the unresolved question?

GuillaumeGomez added a commit to GuillaumeGomez/rust that referenced this pull request Jan 13, 2026
…nethercote

Make `--print=check-cfg` output compatible `--check-cfg` arguments

This PR changes significantly the output of the unstable `--print=check-cfg` option.

Specifically it removes the ad-hoc resemblance with `--print=cfg` in order to output a simplified but still compatible `--check-cfg` arguments.

The goal is to future proof the output of `--print=check-cfg` like `--check-cfg` is, and the best way to do that is to use it's syntax.

This is particularly relevant for [RFC3905](rust-lang/rfcs#3905) which wants to introduce a new predicate: `version(...)`.
GuillaumeGomez added a commit to GuillaumeGomez/rust that referenced this pull request Jan 13, 2026
…nethercote

Make `--print=check-cfg` output compatible `--check-cfg` arguments

This PR changes significantly the output of the unstable `--print=check-cfg` option.

Specifically it removes the ad-hoc resemblance with `--print=cfg` in order to output a simplified but still compatible `--check-cfg` arguments.

The goal is to future proof the output of `--print=check-cfg` like `--check-cfg` is, and the best way to do that is to use it's syntax.

This is particularly relevant for [RFC3905](rust-lang/rfcs#3905) which wants to introduce a new predicate: `version(...)`.
rust-timer added a commit to rust-lang/rust that referenced this pull request Jan 14, 2026
Rollup merge of #150840 - print-check-cfg-rework-output, r=nnethercote

Make `--print=check-cfg` output compatible `--check-cfg` arguments

This PR changes significantly the output of the unstable `--print=check-cfg` option.

Specifically it removes the ad-hoc resemblance with `--print=cfg` in order to output a simplified but still compatible `--check-cfg` arguments.

The goal is to future proof the output of `--print=check-cfg` like `--check-cfg` is, and the best way to do that is to use it's syntax.

This is particularly relevant for [RFC3905](rust-lang/rfcs#3905) which wants to introduce a new predicate: `version(...)`.
### Why this design?
The syntax `rust_version >= "1.85"` is highly intuitive and directly expresses the user's intent. It is a general design that can be used to solve an entire class of adjacent problems, including platform versioning. It is a principled design, as by introducing a `version` type to the `cfg` system, we create a sound basis for comparison operators and other config types in the future. The syntax avoids the semantic confusion of proposals like `rust_version = "1.85"` which would have overloaded the meaning of `=` for a single special case.
This design directly solves the MSRV problem in a way that RFC 2523 did not. The fact that crates maintaining an MSRV will be able to adopt it for newer version constraints buys back some of the time that was spent designing and implementing the newer iteration of this feature.[^buy-back] While sometimes it is better to ship something functional quickly, the fact that users have an functional workaround in the form of build scripts pushes the balance more in the direction of waiting to deliver a high quality solution.
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. The "MSRV problem" is much smaller than this RFC makes it out to
  2. This feels contrary to the understanding I walked away from RustWeek and the initial revived discussions from RFC 2523. Even the conversations we've had in private last month about this. At those it made it sound like "we need this sooner than later to resolve the build script performance issue" while this is saying "we can take our time".

Single-valued config types give us a chance to revisit some earlier decisions like the use of `=` in predicates. For now these are a hard error. Future extensions might add `==` comparisons with a more natural meaning for single-valued configs.
[^buy-back]:
A quick [sample][crate-sample] of two MSRV-preserving popular crates that already make use of feature gating, serde and proc-macro2, showed that those crates would be able to drop their build scripts roughly **a year earlier** with a solution that did not break MSRV compatibility. Obviously, this analysis is incomplete, but it has the benefit of emphasizing popular crates that show up in the critical path of many build graphs.
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not seeing the logical connection between that link, the data, and this statement.

Copy link
Member Author

Choose a reason for hiding this comment

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

I model it as a sliding window where the crates want to support versions some number of years back. Eventually they bump their MSRV past the point where they have no feature gates that exist prior to this feature being stable. Example using made up versions...

1.25 - Current MSRV
1.30 - Feature A
1.35 - Feature B
1.40 - Feature C
1.45 - cfg_version
1.50 - Feature D
1.55 - Feature E

In this RFC you can bump MSRV to 1.40 and not need a build script anymore. Otherwise you have to wait to bump it to 1.45.

Of course it is a simplistic model. I don't think MSRVs of these crates are entirely time based but I think there are time-based constraints, imposed by e.g. Linux distros.

The year time frame is based on sampling; I recall looking at how far back the most recent feature gate was for each crate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-lang Relevant to the language team, which will review and decide on the RFC.

Projects

None yet

Development

Successfully merging this pull request may close these issues.