Editions

This chapter gives an overview of how Edition support works in rustc. This assumes that you are familiar with what Editions are (see the Edition Guide).

Edition definition

The --edition CLI flag specifies the edition to use for a crate. This can be accessed from Session::edition. There are convenience functions like Session::at_least_rust_2021 for checking the crate's edition, though you should be careful about whether you check the global session or the span, see Edition hygiene below.

As an alternative to the at_least_rust_20xx convenience methods, the Edition type also supports comparisons for doing range checks, such as span.edition() >= Edition::Edition2021.

Adding a new edition

Adding a new edition mainly involves adding a variant to the Edition enum and then fixing everything that is broken. See #94461 for an example.

Features and Edition stability

The Edition enum defines whether or not an edition is stable. If it is not stable, then the -Zunstable-options CLI option must be passed to enable it.

When adding a new feature, there are two options you can choose for how to handle stability with a future edition:

  • Just check the edition of the span like span.at_least_rust_20xx() (see Edition hygiene) or the Session::edition. This will implicitly depend on the stability of the edition itself to indicate that your feature is available.
  • Place your new behavior behind a feature gate.

It may be sufficient to only check the current edition for relatively simple changes. However, for larger language changes, you should consider creating a feature gate. There are several benefits to using a feature gate:

  • A feature gate makes it easier to work on and experiment with a new feature.
  • It makes the intent clear when the #![feature(…)] attribute is used that your new feature is being enabled.
  • It makes testing of editions easier so that features that are not yet complete do not interfere with testing of edition-specific features that are complete and ready.
  • It decouples the feature from an edition, which makes it easier for the team to make a deliberate decision of whether or not a feature should be added to the next edition when the feature is ready.

When a feature is complete and ready, the feature gate can be removed (and the code should just check the span or Session edition to determine if it is enabled).

There are a few different options for doing feature checks:

  • For highly experimental features, that may or may not be involved in an edition, they can implement regular feature gates like tcx.features().my_feature, and ignore editions for the time being.

  • For experimental features that might be involved in an edition, they should implement gates with tcx.features().my_feature && span.at_least_rust_20xx(). This requires the user to still specify #![feature(my_feature)], to avoid disrupting testing of other edition features which are ready and have been accepted within the edition.

  • For experimental features that have graduated to definitely be part of an edition, they should implement gates with tcx.features().my_feature || span.at_least_rust_20xx(), or just remove the feature check altogether and just check span.at_least_rust_20xx().

If you need to do the feature gating in multiple places, consider placing the check in a single function so that there will only be a single place to update. For example:

// An example from Edition 2021 disjoint closure captures.

fn enable_precise_capture(tcx: TyCtxt<'_>, span: Span) -> bool {
    tcx.features().capture_disjoint_fields || span.rust_2021()
}

See Lints and stability below for more information about how lints handle stability.

Edition parsing

For the most part, the lexer is edition-agnostic. Within StringReader, tokens can be modified based on edition-specific behavior. For example, C-String literals like c"foo" are split into multiple tokens in editions before 2021. This is also where things like reserved prefixes are handled for the 2021 edition.

Edition-specific parsing is relatively rare. One example is async fn which checks the span of the token to determine if it is the 2015 edition, and emits an error in that case. This can only be done if the syntax was already invalid.

If you need to do edition checking in the parser, you will normally want to look at the edition of the token, see Edition hygiene. In some rare cases you may instead need to check the global edition from ParseSess::edition.

Most edition-specific parsing behavior is handled with migration lints instead of in the parser. This is appropriate when there is a change in syntax (as opposed to new syntax). This allows the old syntax to continue to work on previous editions. The lint then checks for the change in behavior. On older editions, the lint pass should emit the migration lint to help with migrating to new editions. On newer editions, your code should emit a hard error with emit_err instead. For example, the deprecated start...end pattern syntax emits the ellipsis_inclusive_range_patterns lint on editions before 2021, and in 2021 is an hard error via the emit_err method.

Keywords

New keywords can be introduced across an edition boundary. This is implemented by functions like Symbol::is_used_keyword_conditional, which rely on the ordering of how the keywords are defined.

When new keywords are introduced, the keyword_idents lint should be updated so that automatic migrations can transition code that might be using the keyword as an identifier (see KeywordIdents). An alternative to consider is to implement the keyword as a weak keyword if the position it is used is sufficient to distinguish it.

An additional option to consider is the k# prefix which was introduced in RFC 3101. This allows the use of a keyword in editions before the edition where the keyword is introduced. This is currently not implemented.

Edition hygiene

Spans are marked with the edition of the crate that the span came from. See Macro hygiene in the Edition Guide for a user-centric description of what this means.

You should normally use the edition from the token span instead of looking at the global Session edition. For example, use span.edition().at_least_rust_2021() instead of sess.at_least_rust_2021(). This helps ensure that macros behave correctly when used across crates.

Lints

Lints support a few different options for interacting with editions. Lints can be future incompatible edition migration lints, which are used to support migrations to newer editions. Alternatively, lints can be edition-specific, where they change their default level starting in a specific edition.

Migration lints

Migration lints are used to migrate projects from one edition to the next. They are implemented with a MachineApplicable suggestion which will rewrite code so that it will successfully compile in both the previous and the next edition. For example, the keyword_idents lint will take identifiers that conflict with a new keyword to use the raw identifier syntax to avoid the conflict (for example changing async to r#async).

Migration lints must be declared with the FutureIncompatibilityReason::EditionError or FutureIncompatibilityReason::EditionSemanticsChange future-incompatible option in the lint declaration:

declare_lint! {
    pub KEYWORD_IDENTS,
    Allow,
    "detects edition keywords being used as an identifier",
    @future_incompatible = FutureIncompatibleInfo {
        reason: FutureIncompatibilityReason::EditionError(Edition::Edition2018),
        reference: "issue #49716 <https://github.com/rust-lang/rust/issues/49716>",
    };
}

When declared like this, the lint is automatically added to the appropriate rust-20xx-compatibility lint group. When a user runs cargo fix --edition, cargo will pass the --force-warn rust-20xx-compatibility flag to force all of these lints to appear during the edition migration. Cargo also passes --cap-lints=allow so that no other lints interfere with the edition migration.

Migration lints can be either Allow or Warn by default. If it is Allow, users usually won't see this warning unless they are doing an edition migration manually or there is a problem during the migration. Most migration lints are Allow.

If it is Warn by default, users on all editions will see this warning. Only use Warn if you think it is important for everyone to be aware of the change, and to encourage people to update their code on all editions. Beware that new warn-by-default lint that hit many projects can be very disruptive and frustrating for users. You may consider switching an Allow to Warn several years after the edition stabilizes. This will only show up for the relatively small number of stragglers who have not updated to the new edition.

Edition-specific lints

Lints can be marked so that they have a different level starting in a specific edition. In the lint declaration, use the @edition marker:

declare_lint! {
    pub SOME_LINT_NAME,
    Allow,
    "my lint description",
    @edition Edition2024 => Warn;
}

Here, SOME_LINT_NAME defaults to Allow on all editions before 2024, and then becomes Warn afterwards.

This should generally be used sparingly, as there are other options:

  • Small impact stylistic changes unrelated to an edition can just make the lint Warn on all editions. If you want people to adopt a different way to write things, then go ahead and commit to having it show up for all projects.

    Beware that if a new warn-by-default lint hits many projects, it can be very disruptive and frustrating for users.

  • Change the new style to be a hard error in the new edition, and use a migration lint to automatically convert projects to the new style. For example, ellipsis_inclusive_range_patterns is a hard error in 2021, and warns in all previous editions.

    Beware that these cannot be added after the edition stabilizes.

  • Migration lints can also change over time. For example, the migration lint can start out as Allow by default. For people performing the migration, they will automatically get updated to the new code. Then, after some years, the lint can be made to Warn in previous editions.

    For example anonymous_parameters was a 2018 Edition migration lint (and a hard-error in 2018) that was Allow by default in previous editions. Then, three years later, it was changed to Warn for all previous editions, so that all users got a warning that the style was being phased out. If this was a warning from the start, it would have impacted many projects and be very disruptive. By making it part of the edition, most users eventually updated to the new edition and were handled by the migration. Switching to Warn only impacted a few stragglers who did not update.

Lints and stability

Lints can be marked as being unstable, which can be helpful when developing a new edition feature, and you want to test out a migration lint. The feature gate can be specified in the lint's declaration like this:

declare_lint! {
    pub SOME_LINT_NAME,
    Allow,
    "my cool lint",
    @feature_gate = sym::my_feature_name;
}

Then, the lint will only fire if the user has the appropriate #![feature(my_feature_name)]. Just beware that when it comes time to do crater runs testing the migration that the feature gate will need to be removed.

Alternatively, you can implement an allow-by-default migration lint for an upcoming unstable edition without a feature gate. Although users may technically be able to enable the lint before the edition is stabilized, most will not notice the new lint exists, and it should not disrupt anything or cause any breakage.

Idiom lints

In the 2018 edition, there was a concept of "idiom lints" under the rust-2018-idioms lint group. The concept was to have new idiomatic styles under a different lint group separate from the forced migrations under the rust-2018-compatibility lint group, giving some flexibility as to how people opt-in to certain edition changes.

Overall this approach did not seem to work very well, and it is unlikely that we will use the idiom groups in the future.

Standard library changes

Preludes

Each edition comes with a specific prelude of the standard library. These are implemented as regular modules in core::prelude and std::prelude. New items can be added to the prelude, just beware that this can conflict with user's pre-existing code. Usually a migration lint should be used to migrate existing code to avoid the conflict. For example, rust_2021_prelude_collisions is used to handle the collisions with the new traits in 2021.

Customized language behavior

Usually it is not possible to make breaking changes to the standard library. In some rare cases, the teams may decide that the behavior change is important enough to break this rule. The downside is that this requires special handling in the compiler to be able to distinguish when the old and new signatures or behaviors should be used.

One example is the change in method resolution for into_iter() of arrays. This was implemented with the #[rustc_skip_array_during_method_dispatch] attribute on the IntoIterator trait which then tells the compiler to consider an alternate trait resolution choice based on the edition.

Another example is the panic! macro changes. This required defining multiple panic macros, and having the built-in panic macro implementation determine the appropriate way to expand it. This also included the non_fmt_panics migration lint to adjust old code to the new form, which required the rustc_diagnostic_item attribute to detect the usage of the panic macro.

In general it is recommended to avoid these special cases except for very high value situations.