owned this note
owned this note
Published
Linked with GitHub
# RFC 2021 Edition
- Feature Name: (fill me in with a unique ident, `my_awesome_feature`)
- Start Date: (fill me in with today's date, YYYY-MM-DD)
- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000)
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000)
# Summary
[summary]: #summary
This RFC describes the plan for the 2021 Edition. It supercedes [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 the rare case.
* 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.
[RFC 2052]: https://github.com/rust-lang/rfcs/blob/master/text/2052-epochs.md
# Motivation
[motivation]: #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 some difficult lessons. 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.
## Editions enable "stability without stagnation"
The release of Rust 1.0 established ["stability without stagnation"](https://blog.rust-lang.org/2014/10/30/Stability.html) 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.
# Guide-level explanation
[guide-level-explanation]: #guide-level-explanation
The follow 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 a edition tend to be "skin deep". All Rust code, regardless of edition, is ultimately compiled to the same internal IR 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.
## 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. We try primarily to achieve this by "soft means", such as changing the default of cargo to the latest edition, or making new features that entice people to move. However, we may sometimes opt to issue warnings or deprecations where the best fix involves moving to a new edition.
## 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 succintly 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
Persuant 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.
# Reference-level explanation
[reference-level-explanation]: #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, except of course that since editions are opt-in, no code actually breaks.
## 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.
## 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 produces 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.
## 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.
* 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
* 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 write `let x: _ = y.as_ref()` and have the compiler infer the type of `x` based on the set of applicable `AsRef` 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 of `x`, while keeping the behavior the same for older editions. Since MIR contains explicit types everywhere, there is a common IR.
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.
[RFC 2229]: https://github.com/rust-lang/rfcs/pull/2229
## 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](https://github.com/rust-lang/rfcs/pull/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 migraton.
* 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
[drawbacks]: #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 narratives. 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
[rationale-and-alternatives]: #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](https://emberjs.com/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
[prior-art]: #prior-art
- [RFC 2052] introduced Rust's editions.
- Ember's notion of feature-driven editions were introduced in [Ember RFC 364](https://github.com/emberjs/rfcs/blob/master/text/0364-roadmap-2018.md).
- 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](https://www.w3.org/TR/xslt-30/) 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
[unresolved-questions]: #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
[future-possibilities]: #future-possibilities
None.