- Feature Name: edition_2021
- Start Date: 2021-02-19
- RFC PR: rust-lang/rfcs#3085
- Rust Issue: rust-lang/rust#85811
Summary
This RFC describes the plan for the 2021 Edition. It supersedes RFC 2052. The proposed 2021 Edition is intentionally more limited than the 2018 Edition. Rather than representing a major marketing push, the 2021 Edition represents a chance for us to introduce changes that would otherwise be backwards incompatible. It is meant to be marketed in many ways as something closer to “just another release”.
Key points include:
- Editions are used to introduce changes into the language that would otherwise have the potential to break existing code, such as the introduction of a new keyword.
- Editions are never allowed to split the ecosystem. We only permit changes that still allow crates in different editions to interoperate.
- Editions are named after the year in which they occur (e.g., Rust 2015, Rust 2018, Rust 2021).
- When we release a new edition, we also release tooling to automate the migration of crates. Some manual work may be required but that should be uncommon.
- The nightly toolchain offers “preview” access to upcoming editions, so that we can land work that targets future editions at any time.
- We maintain an Edition Migration Guide that offers guidance on how to migrate to the next edition.
- Whenever possible, new features should be made to work across all editions.
This RFC is meant to establish the high-level purpose of an edition and to describe how the RFC feels to end users. It intentionally avoids detailed policy discussions, which are deferred to the appropriate subteams (compiler, lang, dev-tools, etc) to figure out.
Motivation
The plan for editions was laid out in RFC 2052 and Rust had its first edition in 2018. This effort was in many ways a success but also resulted in a lot of stress on the Rust organization as a whole. This RFC proposes a different model for the 2021 Edition. Depending on our experience, we may opt to continue with this model in the future, or we may wish to make changes for future editions.
The following sections describe various “guiding principles” concerning editions. They are ordered with a loose notion of priority, starting with the most important and ending with the least. (This may be relevant in case of conflicting principles.)
Editions enable “stability without stagnation”
The release of Rust 1.0 established “stability without stagnation” as a core Rust deliverable. Ever since the 1.0 release, the rule for Rust has been that once a feature has been released on stable, we are committed to supporting that feature for all future releases.
There are times, however, when it is useful to be able to make small changes in the surface syntax of Rust that would not otherwise be backwards compatible. The most obvious example is introducing a new keyword, which would invalidate existing names for variables and so forth. Even when such changes do not “feel” backwards incompatible, they still have the potential to break existing code. If we were to make such changes, people would quickly find that existing programs stopped compiling.
Editions are the mechanism we use to square this circle. When we wish to release a feature that would otherwise be backwards incompatible, we do so as part of a new Rust edition. Editions are opt-in, and so existing crates do not see these changes until they explicitly migrate over to the new edition. New crates created by cargo always default to the most recent edition.
Guide-level explanation
The following sections define the goals and design principles that we will use for the 2021 edition, and potentially for future editions. The ordering is significant, with earlier sections taking precedence in the case of a conflict. (For example, it’s more important that users are able to control when they adopt the new edition than it is for the edition to be rapidly adopted.)
Editions do not split the ecosystem
The most important rule for editions is that crates in one edition can interoperate seamlessly with crates compiled in other editions. This ensures that the decision to migrate to a newer edition is a “private one” that the crate can make without affecting others, apart from the fact that it affects the version of rustc that is required, akin to making use of any new feature.
The requirement for crate interoperability implies some limits on the kinds of changes that we can make in an edition. In general, changes that occur in an edition tend to be “skin deep”. All Rust code, regardless of edition, is ultimately compiled to the same internal representation within the compiler.
Edition migration is easy and largely automated
Our goal is to make it easy for crates to upgrade to newer editions. Whenever we release a new edition, we also release tooling to automate the migration. The tooling is not necessarily perfect: it may not cover all corner cases, and manual changes may still be required. The tooling tries hard to avoid changes to semantics that could affect the correctness or performance of the code.
In addition to tooling, we also maintain an Edition Migration Guide that covers the changes that are part of an edition. This guide will describe the change and give pointers to where people can learn more about it. It will also cover any corner cases or details that people should be aware of. The guide serves both as an overview of the edition, but also as a quick troubleshooting reference if people encounter problems with the automated tooling.
Users control when they adopt the new edition
Part of making edition migration easy is ensuring that users can choose when they wish to upgrade. We recognize that many users, particularly production users, will need to schedule time to manage an Edition upgrade as part of their overall development cycle.
We also want to enable upgrading to be done in a gradual fashion, meaning that it is possible to migrate a crate module-by-module in separate steps, with the final move to the new edition happening only when all modules are migrated. This is done by ensuring that there is always an “intersection” that is compatible with both the edition N and its successor N+1.
Rust should feel like “one language”
Editions introduce backwards incompatible changes to Rust, which in turn introduces the risk that Rust begins to feel like a language with many dialects. We want to avoid the experience that people come to a Rust project and feel unsure about what a given piece of code means or what kinds of features they can use. This is why we prefer year-based editions (e.g., Rust 2018, Rust 2021) that group together a number of changes, rather than fine-grained opt-in; year-based editions can be succinctly described, and ensure that when you go to a codebase, it is relatively easy to determine what set of features is available.
Uniform behavior across editions
Pursuant to the goal of having Rust feel like “one language”, we generally prefer uniform behavior across all editions of Rust, so long as it can be achieved without compromising other design goals. This means for example that if we add a new feature that is backwards compatible, it should be made available in all editions. Similarly, if we deprecate a pattern that we think is problematic, it should be deprecated across all editions.
Having uniform behavior across editions has several benefits:
- Reducing technical debt: the compiler is simpler if it has to consider fewer cases.
- Minimizing cognitive overload: we want to avoid users having to think too much about what edition they are in. We would prefer that all Rust code feels the same, no matter what edition it is using, although we are willing to compromise on this principle if the benefits are large.
- Even when changes alter core parts of Rust, we are often able to introduce parts of those changes across all editions, which helps us to achieve more uniformity. For example, the
crate::foo::bar
paths introduced in Rust 2018 are also available on Rust 2015, since that syntax had no meaning before.
- Even when changes alter core parts of Rust, we are often able to introduce parts of those changes across all editions, which helps us to achieve more uniformity. For example, the
Editions are meant to be adopted
Our goal is to see all Rust users adopt the new edition, just as we would generally prefer for people to move to the “latest and greatest” ways of using Rust once they are available. Pursuant to the previously stated principles, we don’t force the edition on our users, but we do feel free to encourage adoption of the edition through other means. For example, the default edition for new projects will always be the newest one. When designing features, we are generally willing to require edition adoption to gain access to the full feature or the most ergonomic forms of the feature.
Reference-level explanation
Edition cadence not specified
This RFC explicitly does not attempt to specify the cadence for releasing editions. Historically, they have occurred every 3 years, if one considers Rust 1.0 (released in 2015) to be the “original edition”. Our expectation is that after Rust 2021 is shipped, we will use the experiences from Rust 2018 and Rust 2021 to review how editions are working and potentially establish a cadence for future editions (or perhaps make other changes to the edition system).
There are various constraints on the timing of editions that have been identified over time:
- Even if it’s automated, migrating to a new edition can require a significant investment of time, particularly for production users with many crates. We don’t wish to release editions at a rate that makes it hard for such users to keep up.
- At the same time, we don’t wish to wait too long in between editions. We want to maintain the spirit of Rust’s train model, so that folks don’t feel a “rush” to get work done by any particular deadline.
- Having a generally known time when editions will be released is helpful in planning feature development.
- We do not wish to release an empty edition. Sometimes there simply won’t be any backwards incompatible changes and thus no reason to issue an edition.
Definition: migration
Migrations are the “breaking changes” introduced by the edition; of course, since editions are opt-in, existing code does not actually break.
Definition: migration lint
Migrations typically come with one or more migration lints. Each lint warns about code that will need to change in order to move to the new edition. The lints typically contain “suggestions”, which is a diff that can be applied to make the code work in the new edition. In some cases, the rewrite may be too complex for the lint to make a suggestion. In addition, some suggestions are marked as “not machine applicable”, if they are not known to be semantics preserving.
Default edition for new projects
The default edition for new projects created within cargo or other tooling will be 2021.
RFCs that propose edition changes
RFCs that propose migrations should include details about how the migration between editions will work. This migration should be described in sufficient detail that the relevant teams can assess the feasibility of the migration. It will often make sense to consult the compiler team on this question specifically.
- To perform this evaluation, an RFC proposing a migration should enumerate the situations that will no longer compile with the change.
- For each such situation, it should describe what suggestions will be made to modify the user’s code such that it works on both the old and new edition. Optionally, the description may include “idiom lints” that run only in the new edition in order to make the code more idiomatic.
- Alternatively, if that scenario is deemed unlikely, the RFC can state that this sort of code will not be automatically ported between editions. It should then describe if it is possible to at least issue a warning that the code will no longer work or will change meaning in the new edition.
Tooling workflow
The expected workflow for upgrading to a new edition is as follows:
- Prepare to upgrade: Run
cargo fix --edition
: This will automatically prepare your code to be upgraded to next edition by applying any suggestions from the migration lints.- For example, if your code is in Rust 2018, this will prepare you to move to the 2021 edition.
- Note that
cargo fix
does not actually move your code to the new edition. Instead, it produces code that works in both the old and the new edition.- This allows people to upgrade and fix things in a “piecemeal” fashion. Because of this, however, the code produced by these suggestions is not always the most idiomatic, as it is not able to take advantage of features from the new edition.
- The expectation is that
cargo fix
should be able to fix the majority of crates out there, but it is not required that the tooling is able to handle every case. As long as code does not occur frequently in the wild, it is acceptable for the automated suggestions to be inapplicable to edge cases. The metrics section in this RFC includes some guidelines for how to measure this.
- Upgrade: Edit your cargo.toml to include
edition = "2021"
instead of the older edition.- The code should still build, but it may be necessary to make some fixes or other changes.
- Cleanup: After upgrading, you should run
cargo fix
again. This second step will apply any remaining changes that are only possible in the new edition.- In some cases, these lints may be cleaning up “non-idiomatic” code produced by a migration.
- Manual changes: As the final step, resolve any remaining lints or errors manually (hopefully these will be quite limited). Idiom lints, in particular, may not have automated suggestions, though you can always add an
#![allow]
at the crate level to silence them if you are in a hurry.
Specifying edition changes and migration plans
Any RFC or other proposal that will cause code that used to compile on edition N
no longer compiles on edition N+1
must include details of how the “current edition” for that change is determined as well a migration plan.
Determining the current edition
Although there is an “ambient” edition for the crate that is specified as part of Cargo.toml
, individual bits of code can be tied to earlier editions due to the interactions of macros. For example, if a Rust 2021 crate invokes a macro from a Rust 2018 crate, the body of that macro may be interpreted using Rust 2018 rules. For this reason, whenever an edition change is proposed, it’s important to specify the tokens from which the edition is determined.
As an example, if we are introducing a new keyword, then the edition will be taken from the keyword itself. If we were to make a change to the semantics of the +
operator, we might say that the current edition is determined from the +
token. This way, if a macro author were to write $a + $b
, then this expression would use the edition rules from the macro definition (which contained the +
token). Additions that defined in the $a
expression would use the edition from which $a
was derived.
Migration plans
These migration plans should be structured as the answer to three questions; these questions are designed to compartmentalize review and implementation.
The three questions are:
- What code patterns no longer compile (or change meaning) in the new edition?
- Which code patterns will you migrate from the old to the new edition?
- This should be some subset of the answer to the first question.
- What is your plan to migrate each code pattern?
- The plan should include automated suggestions targeting the intersection of Edition N and Edition N+1.
- The plan may also include lints specific to Edition N+1 that will cleanup the code to make it more idiomatic.
More details on these questions follow.
What code patterns no longer compile (or change meaning) in the new edition?
The RFC should list all known code patterns that are affected by the change in edition, either because they no longer compile or because their meaning changes. This should include unusual breakage that is not expected in practice.
Answering this question is not a commitment to automatically migrate every instance of these code patterns, but it is important for this research to be done during the design phase such that it can be considered when evaluating migration strategies.
Listing these code patterns allows us to make sure the migration is premised on the full set of breakages. This helps avoid confusion where people don’t recognize what code may be affected.
Which code patterns will you migrate from the old to the new edition?
The proposal should then declare what its intended scope for migration is. This may involve declaring some of the listed breakages as out of scope.
Historically we have considered macro-heavy code to be something that edition migrations can try to fix but are not expected to be successful in working on. Similarly, there may be niche breakages that the designers do not expect to crop up in practice.
Listing all this helps correctly set expectations, and also gives people a venue to discuss whether the choice of scope is correct without having to glean the scope from the design of the migrations. Furthermore, if we later discover that an out-of-scope breakage is actually somewhat common, this provides a clean separation in the proposal to work on addressing this.
What is your plan to migrate each code pattern?
The proposal should then contain a detailed design for one or more compatibility lints that migrate code such that it will compile on both editions. Such lints can be specified by listing the kinds of code they should catch, and the changes that should be programmatically made to them.
If needed, the proposal may also contain designs for idiom lints that clean up the migrated code (potentially making it compile on the new edition only).
Listing all of this helps allow reviewers to check if the lints seem feasible and whether they do indeed cover the desired code patterns. It also allows implementors to implement a concrete migration plan rather than having to come up with one on the spot.
Limits of edition changes
Edition changes cannot make arbitrary changes. First and foremost, they must always respect the constraint that crates in different editions can interoperate; this implies that the changes must be “skin deep”. In terms of the compiler implementation, what this means is that Edition can change the way that concrete Rust syntax is desugared into MIR (the compiler’s internal representation for Rust code), but doesn’t change the semantics of MIR itself (or trait matching, etc).
Some examples of changes the editions can make:
- Introducing a new keyword, as with
async-await
.- This change comes with automated tooling that rewrites identifiers using that keyword to use
r#
form.
- This change comes with automated tooling that rewrites identifiers using that keyword to use
- Changing closures so that they capture precise paths, rather than entire variables, as in RFC 2229.
- This change comes with automated tooling that modifies closures to capture entire variables when necessary.
- In MIR, closures are desugared into structs with fields, so all editions can still target the same internal representation.
- Changing the prelude for Rust code.
- Changing type inference rules.
- For example, the current
AsRef
trait allows one to writelet x: _ = y.as_ref()
and have the compiler infer the type ofx
based on the set of applicableAsRef
impls. This results in frequent breakage when new impls are added. While this is permitted by our semver rules, it is not desirable. We could theoretically introduce a change to newer editions that forbids this sort of inference and requires an explicit type ofx
, while keeping the behavior the same for older editions. Since MIR contains explicit types everywhere, there is a common IR.
- For example, the current
Some examples of changes editions cannot make:
- Loosening the coherence rules in just one edition.
- Coherence is a “zero sum” global property that states that there are no two impls that apply to the same types. This property must hold across all crates, regardless of edition, so we can’t tweak the rules in just crates in the 2021 Edition to permit impls that would be disallowed in 2018 crates. If we did so, then crates in the 2018 Edition could add the impl under the old rules, and another crate in the 2021 Edition could add the same impl per the new rules, resulting in an error.
Access to the 2021 edition on nightly
The nightly compiler will make a 2021 edition available in an unstable form. This will allow us to build and test the migrations for various features that will be part of the 2021 edition.
Warning: It is not recommended for crates, even nightly-only crates, to adopt the 2021 Edition too early. Like any unstable feature, the future editions can change without notice and crates are expected to keep up. Furthermore, the automatic migrations are designed to be used exactly once; if a crate C is migrated from Edition X to Edition Y, and then new features or migrations are added to Edition Y, the crate C may not be able to access those new migrations and the changes may have to be done manually.
Advertising and documenting the edition
The expectation is that the 2021 Edition will not be marketed as an “all encompassing event” in the same way as the 2018 Edition.
Producing an edition requires producing the following key artifacts. This list is not meant to be exhaustive; as part of executing on the 2021 Edition, we will be producing more detailed guidelines for how the process works that can be used as a template for future editions if desired.
- “What is an edition” page on the main Rust web site
- Edition migration guide
- Edition status tracking page, likely part of a general tracking page as envisioned by RFC 3037.
- Blog post announcing the edition – this will be the “highlight” part of the standard release blog post
Metrics for a successful edition
The following metrics are an attempt to quantify some of the goals we have for editions.
Easy migration
- 90% of crater-seen crates migrate without manual intervention.
- This can be measured automatically, subject to the limitations of crater (e.g., linux only).
- 90% of crates that were migrated to the newer edition could be migrated in less than 1 hour
- This includes both rustfix but also manual migration.
- This will require a survey or other forms of self-reporting.
Adoption
- 75% of Rust survey respondents in 2022 indicate that they are using Rust 2021
- If we don’t see this number, it may not be a problem, but it’d be worth investigating why.
Drawbacks
Executing an edition requires coordination. There has also been some concern that Rust is making too many changes and moving too quickly, and releasing a new edition could feed those fears. On the other hand, the fact that this edition is relatively limited in scope and that it will be marketed less aggressively also helps here. Further, continuing the regular cadence for editions has its own advantages, and helps us to make some changes (such as closure capture or format printing) that are very valuable.
Rationale and alternatives
There are several alternative designs which we could consider.
“Rallying point” editions
The Rust 2018 Edition was described in RFC 2052 as being a “rallying point” that not only introduced some migrations, but was also the target for a host of other changes such as updating the book, achieving a coherent set of new APIs, and so forth. This was helpful in many respects, but harmful in others. There was a certain amount of confusion, for example, as to whether it was necessary to upgrade to the new edition to gain access to its features (whether this confusion had other negative effects beyond confusion is unclear). It was also a stress on the organization itself to pull everything together; it worked against the train model, which is meant to ensure that we have “low stress” releases.
In contrast, the 2021 Edition is intentionally a “low key” event, which is focused exclusively on introducing some migrations, idiom lints, and other bits of work that have been underway for some time. We are not coordinating it with other unrelated changes. This is not to say that we should never do a “rallying point” release again; at this moment, though, we simply don’t have a whole host of coordinated changes in the works that we need to pull together.
Due to this change, however, one benefit of Rust 2018 may be lost. There is a certain segment of potential Rust users that may be interested in Rust but not interested enough to follow along with every release and track what has changed. For those users, a blog post that lays out all the exciting things that have happened since Rust 2018 could be enough to persuade them to give Rust a try. We can address this by releasing a retrospective that goes over the last few years of progress. We don’t have to tie this retrospective to the edition, however, and as such it is not described in this RFC.
Stop doing editions
We could simply stop doing editions altogether. However, this would mean that we are no longer able to introduce new keywords or correct language features that are widely seen as missteps, and it seems like an overreaction.
Do editions only on demand
An alternative would be to wait and only do an edition when we have a need for one – i.e., when we have some particular language change in mind. But by making editions less predictable, this would complicate the discussion of new features and changes, as it introduces more variables. Under the “train model” proposed here, the timing of the edition is a known quantity that can be taken into account when designing new features.
Feature-driven editions released when things are ready, but not on a fixed schedule
An alternative to doing editions on a schedule would be to do a feature-driven edition. Under this model, editions would be tied to a particular set of features we want to introduce, and they would be released when those features complete. This is what Ember did with its notion of editions. As part of this, Ember’s editions are given names (“Octane”) rather than being tied to years, since it is not known when the edition will be released when planning begins.
This model works well for larger, sweeping changes, such as the changes to module paths in Rust 2018, but it doesn’t work as well for smaller, more targeted changes, such as those that are being considered for Rust 2021. To take one example, RFC 2229 introduced some tweaks to how closure captures work. When that implementation is ready, it will require an edition to phase in. However, it on its own is hardly worthy of a “special edition”. It may be that this change, combined with a few others, merits an edition, but that then requires that we consider “sets of changes” rather than judging each change on its own individual merits.
Prior art
- RFC 2052 introduced Rust’s editions.
- Ember’s notion of feature-driven editions was introduced in Ember RFC 364.
- As noted in RFC 2052, C/C++ and Java compilers both have ways of specifying which version of the standard the code is expected to conform to.
- The XSLT programming language had explicit version information embedded in every program that was used to guide transitions. (Author’s note: nikomatsakis used to work on an XSLT compiler and cannot resist citing this example. nikomatsakis also discovered that there is apparently an XSLT 3.0 now. 👀)
Unresolved questions
When should we use a migration and when should we prefer to be strictly backwards compatible?
Sometimes, when designing a feature, we have a choice between using a migration or between designing the feature to be backwards compatible. For example, we might have a choice of introducing a new keyword or (ab)using an old one. The lang team is expected to produce guidelines to help guide that choice.
What is the policy on warnings and lints tied to the edition?
The lang team is expected to decide the final policy on warnings and idiom lints, along with an accompanying write-up on the rationale. This is currently under discussion in Zulip. Whatever the final policy is, however, it will respect the principles established in this RFC (e.g., minimizing user disruption, encouraging adoption).
Future possibilities
None.