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
| ('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.
Copy link
Member

Choose a reason for hiding this comment

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

what about versions that have more than three components, e.g. Chromium versions? like: Chromium 143.0.7499.146

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, there's an example I needed. Generalized to any number of components in 14e2c4e.

Choose a reason for hiding this comment

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

A lot of closed-source systems, like Chromium but particularly games, will use four numbers; the last number is generally a build number so that each build passed around is identified, even if they still use the main three numbers for "official" builds. I haven't personally seen more than four, but I don't see a compelling reason to limit it.

Even if the compiler wanted to avoid a heap allocation here, it could do something like a smallvec where the first four numbers are stored in an array and anything else is moved to a heap vector.

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.


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("...")'`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there any way for downstream crates to consume the version set by an upstream crate? The usecase here would be *-sys crates that set the version of the linked library. A downstream crate then might want to adjust the API based on the version of the linked library to restrict it to functionality available there

Copy link
Member Author

Choose a reason for hiding this comment

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

Not within this RFC, but it could be a future cargo extension to set cfgs for all of your dependencies (or dependencies that you specifically request).

Copy link
Member Author

@tmandry tmandry Jan 7, 2026

Choose a reason for hiding this comment

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

For the sys crate example you cite that might be a more specialized feature, akin to cargo::metadata in build scripts and the links manifest key: https://doc.rust-lang.org/cargo/reference/build-scripts.html#the-links-manifest-key

NUMERIC_COMPONENT :
'0'
| ('1'...'9') ('0'...'9')*
Copy link
Member

Choose a reason for hiding this comment

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

is there a maximum number of digits in a component? some software uses a very large number of digits, such as TeX and METAFONT, which currently use versions 3.141592653 and 2.7182818 with each new version adding a digit of π and e -- the final versions will be exactly π and e which I don't expect rust to reasonably support

Copy link
Member Author

Choose a reason for hiding this comment

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

We'll be waiting an infinitely long time to reach those final versions, which hopefully will be long enough to overcome our collective (digital and cognitive) resource constraints.

More seriously, I don't see a good reason to limit it. But if there is a reason I wouldn't be opposed to a limit of some high number of digits, say, 64 or 256.

Copy link
Member

@kennytm kennytm Jan 8, 2026

Choose a reason for hiding this comment

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

with all due respect to Donald Knuth, I don't think there will be too many releases on TeX and Metafont that will bump the version number beyond u64 range (20 digits). 141592653 is within 32-bit range even.

beyond TeX I think the longest version I've seen is Python's html5lib 0.999999999 (9 nines) which still fits within 32-bit.

but comparing components can be done easily as string operations, without parsing each component into integers, so I don't see much reason to place a limit.

Copy link
Member

Choose a reason for hiding this comment

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

We'll be waiting an infinitely long time to reach those final versions, which hopefully will be long enough to overcome our collective (digital and cognitive) resource constraints.

Knuth has more or less stated that the versions will be fixed at exactly π and e when he dies and all remaining bugs will become features. So I don't expect we'll need to wait an infinitely long time.

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

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

Choose a reason for hiding this comment

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

The concern is with tools being able to parse version(...), correct? Are there examples of crates that aren't robust against this? I'm thinking that most parsers of cfg output split once on = and parse the specific flags they need or saving them whole to re-emit to a future rustc call, without inspecting the RHS.

If we could get away without this compatibility hack then that seems cleaner, and more consistent.

Copy link
Member

Choose a reason for hiding this comment

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

I'm pretty sure Cargo will error if the value (so rhs) is not a string literal.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, thanks for the reference.

I guess we could have a --print full-cfg that includes these, otherwise things would get tricky if we have options set by rustc that can't easily be queried otherwise (e.g. OS version). Would of course be nicest if build systems could adapt so this isn't needed, but that would take a long while.

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'd be willing to leave this as an unresolved question that we decide based on implementation experience. If we can implement it without much breakage, then great; otherwise we might want to phase it in over an edition or something.

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


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.
Copy link

@hanna-kruppe hanna-kruppe Jan 8, 2026

Choose a reason for hiding this comment

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

That RFC currently says:

Version strings can take on nearly any form and while there are some standard formats, such as semantic versioning or release dates, projects/platforms can change schemas or provide aliases for some or all of their releases. Because of this diversity in version strings, each target will be responsible for validating the version, and defining comparisons on it.

In contrast, this RFC nails down a specific version syntax and version "order" (which is probably the right choice for a proper language feature). Is the other RFC overly cautious, or is the assumption that this RFC can address all target-specific needs overly optimistic?

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.