RFC 2052: Editions (epochs)

core (stability | roadmap)

Summary

Rust's ecosystem, tooling, documentation, and compiler are constantly improving. To make it easier to follow development, and to provide a clear, coherent "rallying point" for this work, this RFC proposes that we declare a edition every two or three years. Editions are designated by the year in which they occur, and represent a release in which several elements come together:

Sometimes a feature we want to make available in a new edition would require backwards-incompatible changes, like introducing a new keyword. In that case, the feature is only available by explicitly opting in to the new edition. Existing code continues to compile, and crates can freely mix dependencies using different editions.

Motivation

The status quo

Today, Rust evolution happens steadily through a combination of several mechanisms:

All told, the tools work together quite nicely to allow Rust to change and grow over time, while keeping old code working (with only occasional, very minor adjustments to account for things like changes to type inference.)

What's missing

So, what's the problem?

There are a few desires that the current process doesn't have a good story for:

At the same time, the commitment to stability and rapid releases has been an incredible boon for Rust, and we don't want to give up those existing mechanisms or their benefits.

This RFC proposes editions as a mechanism we can layer on top of our existing release process, keeping its guarantees while addressing its gaps.

Detailed design

The basic idea

To make it easier to follow Rust's evolution, and to provide a clear, coherent "rallying point" for the community, the project declares a edition every two or three years. Editions are designated by the year in which they occur, and represent a release in which several elements come together:

The precise list of elements going into an edition is expected to evolve over time, as the Rust project and ecosystem grow.

Sometimes a feature we want to make available in a new edition would require backwards-incompatible changes, like introducing a new keyword. In that case, the feature is only available by explicitly opting in to the new edition. Each crate can declare an edition in its Cargo.toml like edition = "2019"; otherwise it is assumed to have edition 2015, coinciding with Rust 1.0. Thus, new editions are opt in, and the dependencies of a crate may use older or newer editions than the crate itself.

To be crystal clear: Rust compilers must support all extant editions, and a crate dependency graph may involve several different editions simultaneously. Thus, editions do not split the ecosystem nor do they break existing code.

Furthermore:

Thus, code that compiles without warnings on the previous edition (under the latest compiler release) will compile without errors on the next edition (modulo the usual caveats about type inference changes and so on).

Alternatively, you can continue working with the previous edition on new compiler releases indefinitely, but your code may not have access to new features that require new keywords and the like. New features that are backwards compatible, however, will be available on older editions.

Edition timing, stabilizations, and the roadmap process

As mentioned above, we want to retain our rapid release model, in which new features and other improvements are shipped on the stable release channel as soon as they are ready. So, to be clear, we do not hold features back until the next edition.

Rather, editions, as their name suggests, represent a point of global coherence, where documentation, tooling, the compiler, and core libraries are all fully aligned on a new set of (already stabilized!) features and other changes. This alignment can happen incrementally, but an edition signals that it has happened.

At the same time, editions serve as a rallying point for making sure this alignment work gets done in a timely fashion--and helping set scope as needed. To make this work, we use the roadmap process:

In short, editions are striking a delicate balance: they're not a cutoff for stabilization, which continues every six weeks, but they still provide a strong impetus for coming together as a community and putting together a polished product.

The preview period

There's an important tension around stabilization and editions:

Thus, at some point within an edition year, we will enable the opt-in on the stable release channel, which must include all of the hard errors that will be introduced in the next edition, but not yet all of the stabilizations (or other artifacts that go into the full edition release). This is the preview period for the edition, which ends when a release is produced that synchronizes all of the elements that go into an edition and the edition is formally announced.

A broad policy on edition changes

There are numerous reasons to limit the scope of changes for new editions, among them:

These lead to some hard and soft constraints.

Hard constraints

TL;DR: Warning-free code on edition N must compile on edition N+1 and have the same behavior.

There are only two things a new edition can do that a normal release cannot:

The second option is to be preferred whenever possible. Note that warning-free code in one edition might produce warnings in the next edition, but it should still compile successfully.

The Rust compiler supports multiple editions, but must only support a single version of "core Rust". We identify "core Rust" as being, roughly, MIR and the core trait system; this specification will be made more precise over time. The implication is that the "edition modes" boil down to keeping around multiple desugarings into this core Rust, which greatly limits the complexity and technical debt involved. Similar, core Rust encompasses the core conceptual model of the language, and this constraint guarantees that, even when working with multiple editions, those core concepts remain fixed.

Soft constraints

TL;DR: Most code with warnings on edition N should, after running rustfix, compile on edition N+1 and have the same behavior.

The core edition design avoids an ecosystem split, which is very important. But it's also important that upgrading your own code to a new edition is minimally disruptive. The basic principle is that changes that cannot be automated must be required only in a small minority of crates, and even there not require extensive work. This principle applies not just to editions, but also to cases where we'd like to make a widespread deprecation.

Note that a rustfix tool will never be perfect, because of conditional compilation and code generation. So it's important that, in the cases it inevitably fails, the manual fixes are not too onerous.

In addition, migrations that affect a large percentage of code must be "small tweaks" (e.g. clarifying syntax), and as above, must keep the old form intact (though they can enact a deny-by-default lint on it).

These are "soft constraints" because they use terms like "small minority" and "small tweaks", which are open for interpretation. More broadly, the more disruption involved, the higher the bar for the change.

Positive examples: What edition opt-ins can do

Given those principles, let's look in more detail at a few examples of the kinds of changes edition opt-ins enable. These are just examples---this RFC doesn't entail any commitment to these language changes.

Example: new keywords

We've taken as a running example introducing new keywords, which sometimes cannot be done backwards compatibly (because a contextual keyword isn't possible). Let's see how this works out for the case of catch, assuming that we're currently in edition 2015.

To make this even more concrete, let's imagine the following (aligned with the diagram above):

Rust versionLatest available editionStatus of catch in 2015Status of catch in latest edition
1.152015Valid identifierValid identifier
1.212015Valid identifier; deprecatedValid identifier; deprecated
1.232019 (preview period)Valid identifier; deprecatedKeyword, unimplemented
1.252019 (preview period)Valid identifier; deprecatedKeyword, implemented
1.272019 (final)Valid identifier; deprecatedKeyword, implemented

Now, suppose you have the following code:

Cargo.toml:

edition = "2015"
// main.rs:

fn main() {
    let catch = "gotcha";
    println!("{}", catch);
}

Example: repurposing corner cases

A similar story plays out for more complex modifications that repurpose existing usages. For example, some suggested module system improvements deduce the module hierarchy from the filesystem. But there is a corner case today of providing both a lib.rs and a bin.rs directly at the top level, which doesn't play well with the new feature.

Using editions, we can deprecate such usage (in favor of the bin directory), then make it an error during the preview period. The module system change could then be made available (and ultimately stabilized) within the preview period, before fully shipping on the next edition.

Example: repurposing syntax

A more radical example: changing the syntax for trait objects and impl Trait. In particular, we have sometimes discussed:

Suppose we wanted to carry out such a change. We could do it over multiple steps:

Of course, this RFC isn't suggesting that such a course of action is a good one, just that it is possible to do without breakage. The policy around such changes is left as an open question.

Example: type inference changes

There are a number of details about type inference that seem suboptimal:

We may or may not be able to change these details on the existing edition. With enough effort, we could probably deprecate cases where type inference rules might change and request explicit type annotations, and then—in the new edition—tweak those rules.

Negative examples: What edition opt-ins can't do

There are also changes that editions don't help with, due to the constraints we impose. These limitations are extremely important for keeping the compiler maintainable, the language understandable, and the ecosystem compatible.

Example: changes to coherence rules

Trait coherence rules, like the "orphan" rule, provide a kind of protocol about which crates can provide which impls. It's not possible to change protocol incompatibly, because existing code will assume the current protocol and provide impls accordingly, and there's no way to work around that fact via deprecation.

More generally, this means that editions can only be used to make changes to the language that are applicable crate-locally; they cannot impose new requirements or semantics on external crates, since we want to retain compatibility with the existing ecosystem.

Example: Error trait downcasting

See rust-lang/rust#35943. Due to a silly oversight, you can’t currently downcast the “cause” of an error to introspect what it is. We can’t make the trait have stricter requirements; it would break existing impls. And there's no way to do so only in a newer edition, because we must be compatible with the older one, meaning that we cannot rely on downcasting.

This is essentially another example of a non-crate-local change.

More generally, breaking changes to the standard library are not possible.

The full mechanics

We'll wrap up with the full details of the mechanisms at play.

How We Teach This

First and foremost, if we accept this RFC, we should publicize the plan widely, including on the main Rust blog, in a style simlar to previous posts about our release policy. This will require extremely careful messaging, to make clear that editions are not about breaking Rust code, but instead primarily about putting together a globally coherent, polished product on a regular basis, while providing some opt-in ways to allow for evolution not possible today.

In addition, the book should talk about the basics from a user perspective, including:

Drawbacks

There are several drawbacks to this proposal:

These downsides are most problematic in cases that involve "breakage" if they were done without opt in. They indicate that, even if we do adopt editions, we should use them judiciously.

Alternatives

Within the basic edition structure

There was a significant amount of discussion on the RFC thread about using "2.0" rather than "2019". It's difficult to concisely summarize this discussion, but in a nutshell, some feel that 2.0 (with a guarantee of backwards compatibility) is more honest and easier to understand, while others worry that it will be misconstrued no matter how much we caveat it, and that we cannot risk Rust being perceived as unstable or risky.

Sticking with the basic idea of editions, there are a couple alternative setups that avoid "preview" editions:

Alternatives to editions

The larger alternatives include, of course, not trying to solve the problems laid out in the motivation, and instead finding creative alternatives.

The other main alternative is to issue major releases in the semver sense: Rust 2.0. This strategy could potentially be coupled with a rustfix, depending on what kinds of changes we want to allow. Downsides:

Unresolved questions