RFC 3085: 2021 edition

core (roadmap)

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:

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:

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:

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.

Tooling workflow

The expected workflow for upgrading to a new edition is as follows:

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:

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:

Some examples of changes editions cannot make:

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.

Metrics for a successful edition

The following metrics are an attempt to quantify some of the goals we have for editions.

Easy migration

Adoption

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

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.