RFC: Evolving Rust through Epochs

There has been a long-standing question around Rust's evolution: will there ever be a Rust 2.0 in the semver sense?
This RFC gives the answer: certainly not in the foreseeable future, and probably not ever.
Instead, this RFC proposes epochs, a mechanism for language evolution without breakage, which fits neatly into Rust's existing train and release channel process. It's an attempt to provide the next stage of our core principle of stability without stagnation, inspired in part by similar mechanisms in languages like C++ and Java.
With epochs, it becomes possible to do things like introduce new keywords without breaking existing code or splitting the ecosystem. Each crate specifies the epoch it fits within (a bit like "C++11" or "C++14"), and the compiler can cope with multiple epochs being used throughout a dependency graph. Thus we still guarantee that your code will keep compiling on the latest stable release (modulo the usual caveats), while making it possible to evolve the language in some new ways via explicit opt-in.
Rendered
Update: there's a Request for Explanation podcast episode about this RFC, which is a good way to quickly get up to speed!

People may be interested to review a recent draft from the C++ world, C++ Stability, Velocity, and Deployment Plans, which tries to clarify plans around C++'s evolution and has many similarities with this RFC.

I have long been advocating for a "never Rust 2.0", and I see this RFC as reaffirming our commitment to stability.
I can't tell the future, of course, but for me, this RFC is the nail in the ⚰ . No Rust 2.0! ❣

I came to Rust because it was stable, and value stability greatly. If this RFC means that I don't have to suffer from the new module system but can continue to use the current, magnificent one, I'm a great supporter!

I don't have to suffer from the new module system but can continue to use the current, magnificent one,

To be clear, there isn't a module system proposal yet 😉

(amusingly, I was just complaining about the lack of this exact feature and philosophy in the OPLSS chat)

ATCs, impl Trait, and specialization all becoming available […] 2018

Be careful what things you put in the same sentence, it could sound like a promise ;)

Will all newly-stabilized features only go into the current epoch?  There could be some new things that would be perfectly fine on the older epochs too.  My gut says to only push forward, but I could imagine some cases might be harder to try and isolate their implementations.
Is it a semver-breaking change for a crate to start requiring a new epoch?  Bumping rustc requirements at all was controversial in #1619.
Will cargo new always default to the newest epoch right away?

Rust compilers are expected to support multiple epochs, and a crate dependency graph may involve several different epochs simultaneously. Thus, epochs do not split the ecosystem nor do they break existing code.

existing deprecations may turn into hard errors ... This is the only kind of change a new epoch can make.

the RFC proposes to limit epochal changes to be "superficial", i.e. occurring purely in the early front-end stages of the compiler

That answers every single question and concern I had about epochs in exactly the way I hoped they'd be answered, and even the parts of this RFC that I was not expecting (e.g. epoch previews) make perfect sense. +1000. Let's do this.
Though it might be worth explicitly stating that the pretty graph with green and orange bars is not committing us to actually creating those specific epochs at those specific future dates.

Evidence of @cmr lamenting this missing feature.

@cuviper The cargo question is answered explicitly in the RFC:

cargo new will produce a Cargo.toml with the latest epoch value, including -preview epochs when applicable.

The semver one less explicitly, but I believe it's very strongly implied by this quote that there is no such thing as "requiring" an epoch and merely using a new epoch in your crate won't break any client code:

To be crystal clear: Rust compilers are expected to support multiple epochs, and a crate dependency graph may involve several different epochs simultaneously. Thus, epochs do not split the ecosystem nor do they break existing code.

But it wouldn't hurt to be even more crystal clear and say "there is no such thing as 'requiring' a minimum epoch of client code, only requiring minimum rustc/cargo versions" assuming I am interpreting this right.

@cuviper

Will all newly-stabilized features only go into the current epoch?  There could be some new things that would be perfectly fine on the older epochs too.  My gut says to only push forward, but I could imagine some cases might be harder to try and isolate their implementations.

Ooh, that is a very interesting question that the RFC doesn't address at all. I pretty much agree with both of your sentiments here, and hence suspect we won't have a hard rule, but rather a default tendency. In particular, the benefits of collecting bundles of features together under an umbrella name are decreased somewhat if some of them become available in older epochs.

Is it a semver-breaking change for a crate to start requiring a new epoch?  Bumping rustc requirements at all was controversial in #1619.

Another good question. I personally would like to see this issue addressed through LTS releases, which could act as a point of coordination for users wanting to be conservative with their upgrades. I don't think epochs introduce anything particularly new here.
In particular, "requiring" a new epoch means nothing more than requiring a new compiler version, so it reduces to the same question we have today (which we don't have a good answer for yet.)

Will cargo new always default to the newest epoch right away?

Yes. Part of the benefit of keeping the rapid release model at the center here is that we should be able to avoid the usual pitfalls of a "X.0" release.

Right, a new epoch requires a new compiler, so yes it's basically the same as bumping rustc generally.
One more - If a 2018+ crate wants to use a 2015 crate with catch in its API, are they just out of luck?

To be clear, there isn't a module system proposal yet

@steveklabnik yes, that's right. The proposals I saw didn't change much in the past few weeks  so I'd be suprised if it turned out much different from what was proposed initially on irlo months ago. I don't want to go into the details, as continuing this discussion seems not appropriate for this thread. I only wanted to give motivation for why I like this RFC.
The general point I wanted to make is that I clearly don't want to adapt my code to entirely new concepts/systems every 2 years. Knowing that the proponents of such changes will get them through one way or another, I really do prefer if they were done cleanly (this also helps the feature itself!) and done in newer "epochs" of Rust.
The biggest risk of going with this RFC is I think that it may be misunderstood by people that Rust's stability promise is broken, and making people distrust the promises made by the language, turning them away. To emphasize that epochs are not about breakage but about allowing future evolution, I would really like if it isn't just possible but also accepted and not a sign of bad style to stay with your evolving codebase on an older epoch.

In particular, the benefits of collecting bundles of features together under an umbrella name are decreased somewhat if some of them become available in older epochs.

I suspect the most useful "bundles of features" are ones where some of the features don't come into their own until the rest of the bundle is also stabilized. Like how impl Trait is useful today, but practically mandatory when futures and async/await are involved. Or how CTFE and const fn aren't that useful without each other. So in that sense I don't think stabilizing some "non-breaking" features will weaken a good "bundle".

One more - If a 2018+ crate wants to use a 2015 crate with catch in its API, are they just out of luck?

I guess there is a limit to the compatibility this RFC can provide?

Code that compiles without warnings on the previous epoch (under the latest compiler release) will compile without warnings or errors on the next epoch

As written, I think this excludes ever adding new warnings. You may want to weaken this to "without errors on the next epoch" (but not necessarily without warnings), or "without warnings or errors in the first compiler version that supports the next epoch".
Even then, deprecated stuff cannot be removed or repurposed until two epochs later. This is nice for stability, but maybe a bit heavy for language evolution flexibility. If it is intended, the RFC should mention this.

@cuviper

One more - If a 2018+ crate wants to use a 2015 crate with catch in its API, are they just out of luck?

Another good one :-)
There are two ways I can see this going:

We can introduce some mechanism for referring to identifiers named as keywords. It might be enough to allow use of keywords within renamed imports, e.g. use foo::catch as catch_;. We'd need to support method renaming, however.

Worst case, you can write a shim crate that uses epoch 2015 but re-exports renamed APIs. That can get tricky with traits, though...

@SimonSapin

Code that compiles without warnings on the previous epoch (under the latest compiler release) will compile without warnings or errors on the next epoch

As written, I think this excludes ever adding new warnings. You may want to weaken this to "without errors on the next epoch" (but not necessarily without warnings), or "without warnings or errors in the first compiler version that supports the next epoch".

Good point. It's probably enough to say "without errors"

Even then, deprecated stuff cannot be removed or repurposed until two epochs later. This is nice for stability, but maybe a bit heavy for language evolution flexibility. If it is intended, the RFC should mention this.

I don't quite follow this point. Examples like the catch keyword involve deprecation in one epoch, then immediate removal/replacement in the next. I don't see how that violates the rule above.

@aturon A third option would be adding #[cfg(epoch >= 2018)], which would allow crate authors affected by such unfortunate corner cases to swap their catch method for do_catch or whatever else is needed to support >=2018 clients while still staying on the old epoch for the most part.
My guess is we don't actually want to do that, as I can easily imagine overuse of it in a sufficiently large dependency graph leading to compatibility nightmares pretty quickly, and potentially encouraging "people can just cfg it" arguments for epoch changes we would otherwise reject as to obreaking. But it seems worth mentioning.

Update: per discussion with @Manishearth, I've changed the RFC to default to the 2015 epoch when no flag is given to rustc, since that's more consistent with our general stability story. @nikomatsakis seemed to prefer the latest epoch, but I'll let him argue that point.

Do macros operate with the epoch in their crate of origin?  I guess they must, but there could be some hairy interactions with stuff that's only legal in the epoch that instantiated it. e.g. oldvec![new-expr]

@cuviper
Procedural macros, in particular, are going to be "interesting" since we can stitch together token streams potentially coming from a mixture of sources.
I haven't thought extensively about this, but I think the ideal behavior would involve some kind of "coloring" of token trees with their epoch of origin. That works well for keywords, since we make the distinction at the lexer level and don't otherwise mind if an identifier happens to have the same name as a keyword.
Other concerns, such as changes to type inference, would likely be much harder to provide a "coloring" strategy for.
I suspect the best we can do is: any time we make an epoch-dependent change, the RFC should contain a section laying out the plan for handling (procedural) macros.

Even then, deprecated stuff cannot be removed or repurposed until two epochs later.

I don't quite follow this point. Examples like the catch keyword involve deprecation in one epoch, then immediate removal/replacement in the next. I don't see how that violates the rule above.

Let’s assume that catch didn’t make it for 2018. let catch = 4; was not a warning in epoch 2015, so the bit of RFC text quoted earlier forbids making it a warning in epoch 2018. We’d have to wait for the next one, let’s call it 2020, to deprecate catch as an identifier and then yet another cycle to be able to make it a keyword in 2022.
So I think we want to drop the "code with warning in one epoch emits no warning in the next" rule. (Reduce it to "no hard error in the next".)

@SimonSapin ah I see, it was based on reading the literal words I wrote, as opposed to what I had in my head :-)
I've pushed a fix.

Just for clarity, this will fully replace the current pattern of first linting and then turning that lint into error by default then into a hard error, which is being done right now (also has a tag) for smaller breaking changes?

How will multi epoch transitions work, especially in relation to the example of changing the Trait syntax? If for example someone writes a program for a company in current Rust, the program's code isn't touched much for a couple of years till lets say mid 2020 and the programmer decides they want upgrade the code. If they simply added epoch = "2020" to Cargo.toml couldn't they have a program that works in both 2015 Rust and 2020 Rust that behaves differently?
Similarly how will the rustfix handle transitioning from arbitrary epochs to the latest? Does it essentially run older versions of itself till it's just at the last old epoch and then run the new changes?

Presumably rustfix, like rustc, will need to know about all older epochs. It would fix from 2015 to 2020 by going through 2018, at least internally.

Also what if there is a case where the no need for an epoch on year there should be one? For example, it was decided to spend the year refining existing features and adding features that have no stability impact? I don't know if that'd ever be possible, but would a "empty" epoch be released or would it be simply skipped?

I think it would be pushed to the next year, or whenever it becomes useful. This is why the "preview" is 2015-next (with the previous number), not 2018-pre (which would put a deadline at December 2018).

A slight tangent to this PR: if we are not going to follow semver's version breaking changes and instead adopt epochs, should we just simply drop the "1.x" prefixes on rustc's versions?

Loose Notes:

The core proposed machinery is good and I think it should exist.

I would like to emphasize that using epochs to break things is a nuclear option (which the text seems to hint at, but isn't totally clear on). I would personally prefer that we proceed with one of the stated policy alternatives -- "no RFC should qualify its acceptance on the creation of a breaking epoch". That is, every RFC must have a non-breaking version, which is the primary proposal. Breaking things should come only if there's public outcry for it. No point in breaking things for a feature our users don't end up caring much about.

I flat-out don't believe the claim that random pieces of code on the internet will start mentioning epochs. Notably, it's impossible to predict when I write something if epochs will be relevant to any particular claim. We also can't expect developers to be aware of things that changed before they ever started using Rust. Something that seemingly "always" has been true might have changed in a much earlier epoch. Also there's just lots of relatively low effort content that ends up living forever like Stack Overflow answers. The fact that epochs will always lead to a Rust-1.0 style documentation confusion is why I consider them to be nuclear.

I don't like the "regular" epochs that the RFC seems to be suggesting; I would prefer if they were reserved for "ok we actually broke something". I don't want to have muddy epochs where some of them are totally fine and some actually matter. Although epochs are "beyond" SemVer I would like to keep the spirit of it; it should be easy to tell how hard it is to upgrade.

I don't understand why we're calling the alpha releases for 2018 to be "2015-next" -- seems incredibly confusing, considering under this proposal we always know what the final release will be called?

The difference between rustc and cargo's epoch defaults seems to be suggesting that only Cargo provides support for reproducible builds? This doesn't make sense to me, can you elaborate?

I still think epochs should still be called Sonic Generations, after Rust's official mascot.

@KiChang there is value in versioning the compiler separately from the language. One could say that the "1." part of the version number becomes futile if there's never going to be 2.0, but it still essentially communicates the intent, as it's a semver-based version number: "this compiler is still 1.x, and therefore still compatible with the good old Rust!"

Do we need another concept — year-epoch? Could't simply every release of rust introduce a new epoch? And "epoch" would be simply a version of compiler, e. g.
rust_version = "1.20"

BTW, this will also fix an issue of library compatibility with older rust versions. E. g. if Vec adds function like shuffle in 1.21, program should not compile in compiler version 1.21 if epoch is 1.20. This will guarantee that code will compile with rust 1.20 (and also compilation shouldn't even start with rust 1.19). Epoch mechanism doesn't solve this issue.

A slight tangent to this RFC: if we are not going to follow semver's version breaking changes and instead adopt epochs, should we just simply drop the "1.x" prefixes on rustc's versions?

Staying at 1.x.y forever is a valid use of SemVer. Also I think it helps signal to the outside world that we’re committed to stability.
I’ve said it when 1.0.0 came and and I still think it: I’m looking forward to the release of Rust 1.100.0 as a proof that our stability story has worked.

@Gankro

I don't like the "regular" epochs that the RFC seems to be suggesting; I would prefer if they were reserved for "ok we actually broke something". I don't want to have muddy epochs where some of them are totally fine and some actually matter. Although epochs are "beyond" SemVer I would like to keep the spirit of it; it should be easy to tell how hard it is to upgrade.

If the epochs are only released with a breaking change, what makes this different from having a Rust 2.0, other than the name?

If the epochs are only released with a breaking change, what makes this different from having a Rust 2.0, other than the name?

This is a confusion I had when first introduced to this idea as well. The difference is that code written in, say, epoch 2015 Rust will always compile with every later Rust compiler, such as a hypothetical Rust 1.29 that will support epoch 2018 Rust. See the table in this section of the RFC-- if we were actually making a 2.0 Rust compiler, some of the cells in "Status of catch in epoch 2015" would say "compiler error" instead.
Please keep asking questions on this point if this still isn't clear, we very much want the difference between epochs and semver 2.0 to be clear before coming anywhere close to accepting it. ❤

Is it worth the additional complexity though? You said you were confused by this too, and the RFC acknowledges that epochs, compiler versions, the release train are kind of confusing concepts. Wouldn't it be easier to just say that Rust follows a tweaked semver where the major version indicates the epoch and is always backwards compatible? I don't think people expect breaking changes in major version changes of programming languages all too much anyway, so I'm not sure if the additional complexity of specifically naming them epochs is worth being able to say Rust follows semver entirely.

@CryZe I'd like to follow a model somewhat like C++ - new versions make breaking changes, but only minor breaking changes and preferably with lots of warning time (i.e., deprecations).

If the epochs are only released with a breaking change, what makes this different from having a Rust 2.0, other than the name?

One of the implications of what @carols10cents mentioned (new compilers still support old epochs) is that multiple epochs can be supported in a single dependency graph.

@CryZe the problem is that while a semver breaking change doesn't require there to be breakage, which a system like that essentially would amount to, it's like printing "we don't GUARANTEE you not to get sick drinking this juice" to a can of juice, with big letters. And then there's a small print: "Actually, we held ourselves to a very high standard of hygiene! Still not guarantees though!"
In other words, while technically not wrong, it's just bad marketing, and doesn't actually communicate what we intend to do with the system.

It seems odd to me to limit "preview" epochs to one-per-normal-epoch. This means that if eg. catch is deprecated as an identifier, then 2015-next is split off, making it a keyword, and therefore a hard error, now no further breaking changes can go in until the next preview release, which based on the timescales in the RFC would be around four to five years away.
Wouldn't it make more sense to either have numbered preview releases (2015-next-1, 2015-next-2) or simply do away with preview epochs altogether?
What is the point of the preview release? Does it lock down the set of deprecations that will become hard-errors? If so, why is that desirable?

I'm not sure if I expressed my point well enough. I basically just meant that instead of sticking with 1.xx, we could just encode the epoch there and have a Rust 2018.3 and a Rust 2020.2 and so on. Then while yes we kind of get away from semver, this doesn't have to have any negative influence on how people view Rust's stance towards breaking changes, and instead just encodes the latest supported epoch directly into the version, making the epoch story very obvious and it would get rid of the infinitely growing minor versions like 1.345. This RFC implies we'll never have a 2.0, so why even keep the major version of 1, when there's a valuable information to encode there now.

I agree with all of @Gankro’s points, particularly about "regular" epochs.
From the RFC:

There are two desires […]:

Changes that may require some breakage in corner cases. […]
Lack of clear "chapters" in the evolutionary story. […] To some degree, you can understand these as "marketing" releases

These two things are somewhat conflicting.
First, breaking changes are Bad.  (Any large enough project will involve crates in multiple epochs, either in their own crates or in dependencies.)
As such, cutting a new epoch to enable a new feature should be a last resort, after we’ve tried very hard to avoid it and exhausted all other options. This is why @Gankro calls it the nuclear option.

we expect [epochs] to happen every two or three years

(Emphasis added.)
Expecting to make them regularly is absolutely the wrong way to approach breaking changes.
It makes sense from a marketing point of view though: every few years we want Rust to make the news with all the improvements that came since last time. But I think this should not be conflated with a mechanism for dealing with breaking changes.
New features that are not breaking, even really nice and big ones, should not require a new epoch. And a new epoch should not be made just to celebrate a bunch of them. I think there should be a mechanism that is for breaking changes and nothing else. (And maybe another mechanism for bi-yearly-or-so marketing.)

What I don't get about @SimonSapin 's critique is that if the migrations are designed to be fully rustfixable and the epochs fully intermixable within a single dependency graph, what's the pain to deal with?

Much as I like the general direction of this proposal, I think it's worth pondering some theoretical concerns as a kind of "stress test".
Firstly, what, if anything, will prevent the complexity of the compiler and therefore the maximal compile time from tending to infinity as the epoch tends to infinity? I.e., is it feasible to support all extant epochs with a single compiler version or will compile times of certain programs become unacceptably slow? I suspect the answer depends on the modularity of the compiler.
Secondly, and possibly related to the first question, is the crate graph of a given (future) program necessarily well defined if the very constructs which define the dependencies between crates are allowed to evolve arbitrarily in line with this RFC. And, even if the graph is well defined, will computing the graph take arbitrarily long as the epoch tends to infinity?
Thirdly, since the compiler is written in Rust, how is it possible to ensure that the semantics of a given epoch do not mutate over time? I suspect this hinges on the modularity of the compiler and the decidability of its crate graph.
I guess the saving grace of this RFC is that it doesn't absolutely rule out a Rust 2.0 in the future if these theoretical concerns turn into show stoppers.

is the crate graph of a given (future) program necessarily well defined if the very constructs which define the dependencies between crates are allowed to evolve arbitrarily in line with this RFC

The crate graph is Cargo's problem, how should the epochs change that? Their point, according to the text, was to have only crate-internal effects.

since the compiler is written in Rust, how is it possible to ensure that the semantics of a given epoch do not mutate over time?

I don't see how the implementation language of the compiler would affect the semantics of the produced code?

What I don't get about @SimonSapin 's critique is that if the migrations are designed to be fully rustfixable and the epochs fully intermixable within a single dependency graph, what's the pain to deal with?

I think @Gankro explained the inherent flaw most concretely:

I flat-out don't believe the claim that random pieces of code on the internet will start mentioning epochs. Notably, it's impossible to predict when I write something if epochs will be relevant to any particular claim. We also can't expect developers to be aware of things that changed before they ever started using Rust. Something that seemingly "always" has been true might have changed in a much earlier epoch. Also there's just lots of relatively low effort content that ends up living forever like Stack Overflow answers. The fact that epochs will always lead to a Rust-1.0 style documentation confusion is why I consider them to be nuclear.

I fully agree.

Is it worth the additional complexity though? You said you were confused by this too, and the RFC acknowledges that epochs, compiler versions, the release train are kind of confusing concepts. Wouldn't it be easier to just say that Rust follows a tweaked semver where the major version indicates the epoch and is always backwards compatible with old epochs?

I think trying to shoehorn this concept into semver would be more confusing, since semver does have a spec and therefore has a shared definition most folks would assume.

I don't think people expect breaking changes in major version changes of programming languages all too much anyway

Expect? No. Fear? Yes. One in my history was the Ruby 1.8.7 to 1.9.1 upgrade... that one was particularly painful :( Also there are still some shops on C89...
(from your other comment):

This RFC implies we'll never have a 2.0, so why even keep the major version of 1, when there's a valuable information to encode there now.

Keeping the major version at 1 is valuable information: we're communicating that the public API of Rust is not ever changing in a backwards-incompatible way.

I am also concerned about the documentation side of this.
Let's say that we have a old crate in epoch 2015.
Currently the documentation code would work when it is copied into our crate, although it would have some warnings.
With these "epochs" it would be harder to use and maintain old crates, with multiple versions of examples required.

@Gankro @SimonSapin

I would like to emphasize that using epochs to break things is a nuclear option (which the text seems to hint at, but isn't totally clear on).

First, breaking changes are Bad.

As such, cutting a new epoch to enable a new feature should be a last resort, after we’ve tried very hard to avoid it and exhausted all other options.

Expecting to make them regularly is absolutely the wrong way to approach breaking changes.

It makes sense from a marketing point of view though: every few years we want Rust to make the news with all the improvements that came since last time. But I think this should not be conflated with a mechanism for dealing with breaking changes.

Respectfully, I think you may be "over-rotating" on the breakage aspect of this RFC, and thereby missing some of the big picture.
Epochs are about aggregating change, only a small fraction of which is breakage
A good way to think about this proposal is as an extension to the train model. Rapid releases are too fine-grained for humans to track easily (quick -- what came out in Rust 1.7??). Having a regular, say, two year cadence for tying everything together, putting out a new edition of the book, polishing and shipping a coherent product is incredibly valuable. Think a bit about coming together for Rust 1.0, but with a greater focus on polish and coherence. As someone who spends his time trying to look at the Rust project as a whole, this is huge.
This is the model that two of the languages with the greatest reputations for stability -- C++ and Java -- both follow, and when people talk about those language versions, they do so at "epochal" scales (like C++11, 14, and 17). And these epochs include minor, clearly publicized-in-advance breakage during transitions.
I also want to stress that the accumulation of major new features, and the resulting idiom shifts, can have much the same flavor as breakage: it can render blog posts outdated and APIs suboptimal. Deprecations and eventual removals are just one small facet of the core theme: change. It's vital that we have mechanisms to regulate and set expectations for change, which is part of why I see this RFC as an extension to the existing train/rapid-release model.
How to think about breakage
I get the sense, from your reactions, that you're seeing "cutting an epoch" as all about enabling a new feature via breakage. But I see that as the exception, not the rule. Epochs are a coarse-grained way of tracking "what chapter of Rust's development" we're talking about, which, yes, may include introducing a new keyword, but will mostly be about a number of other things not involving breakage.
I do not see epochs as an opportunity to introduce breakage freely. I see them primarily as about the above model of Rust evolution, leaving some room for small opt-in breakage that we as a community deem worthwhile.
I'd also urge you to consider the magnitude of breakage we're talking about here. Consider introducing a new keyword with:

a year+ deprecation cycle
tools like rustfix and rename-on-import to help ease the transition
opt-in to the new epoch
continuing to work just fine with old libraries that use the keyword internally as an identifer

I'm not saying this is zero cost, but when you add all these mitigations together, I think the experience is pretty livable (especially given that the opt-in allows you to take care of things on your schedule).
You need to compare this to the costs on the other side: if we want to introduce a concept like catch, forcing that concept into the small set of reserved keywords, or other such hacks, will make the language harder to learn and less pleasant to use.
In other words, as usual, there are costs on both sides, and we need to find a careful balance. This RFC is partly about giving us the tools to reduce costs around evolution.
I think it's fair to say that, since 1.0, we've carefully followed our stated stability policy (in many cases avoided making breakages that would be considered fair game); when we've failed, we've usually tried to revert or otherwise minimize the fallout. I believe we, as a community, have the discipline to ship regular epochs without over-using their potential for breakage, and we can articulate more specific guidance on the policy over time.
How we talk about epochs

I flat-out don't believe the claim that random pieces of code on the internet will start mentioning epochs.

Maybe not random pieces of code, but again, the world I'm imagining is one in which you think about "Rust 2018" as being a big event, and code thereafter as being written in "Rust 2018", much the same way as "Java 8" or "C++17". In these other languages, there are articles, blog posts, and books specifically centered on these new "epochs". Again, I see this not so much as about anticipating unknown breakage, but rather as a way of talking about "which Rust" is being discussed. By 2018, Rust 2015 should seem (rightfully!) dated. And seeing books out with "Rust 2021" or whatever in their title seems plausible to me.
It's also worth noting that because epochs are calendar years, it's pretty easy to determine at a glance whether content you're looking at might not reflect current idioms, based solely on its date.

Is there a policy for how long epoch previews should be (or how many releases it should encompass)?

@dashed

Is there a policy for how long epoch previews should be (or how many releases it should encompass)?

I didn't nail down these kinds of specifically too tightly in this first RFC, mostly because I think we'll need experience to judge (and we may want to change it over time). But I'm imagining a preview lasting for about 9-12 months.

In these other languages, there are articles, blog posts, and books specifically centered on these new "epochs".

Sure, when talking about those new features. But blog posts/stackoverflow answers addressing random questions are unlikely to say things like "you need Java 9" or "you need Ruby 1.9".

In particular, the benefits of collecting bundles of features together under an umbrella name are decreased somewhat if some of them become available in older epochs.

Indeed, which is why this proposal needs some tweaking I think. In C++ land, a lot of new(er) features are not being used because "the project" is using an older version. I think as many new features as possible should be available on earlier epochs. Only when there is no other way should you have to opt in to a new epoch. Since these are expected to be rare I'm not sure if creating the whole epoch system just for that is worthwhile.
Here's an idea which I haven't really thought out, but I'm just throwing out there: enable breakage on a per-feature basis (e.g. using #![feature(catch)]) or optionally using a date enabling all breaking features that were available at that time #![feature=20170703]. Cargo might even automatically insert today's date when starting a new crate. This allows us to maintain much greater forward as well as backward compatibility while still allowing the occasional break.

There's one sentiment in the comments above that hasn't been addressed, and that is whether we should cut epochs based on features, rather than a periodic and mechanical 2 or 3 year timescale. I would argue for the former, so that we have a consistent, feature-full story of rust whenever we enter into a new epoch.

This proposal is an excellent starting point, and I'm delighted to see that you've put this much thought into it. I ran a largish C++ project during the 0x - 11 - 14 transition period, and have worked on fairly complex Haskell systems using a wide array of language feature switches; this proposal covers basically all my concerns from those perspectives.
Designating MIR as Core, with the understanding that MIR itself will continue to evolve without stability requirements so long as translations from prior epochs remain possible, seems like the right move. (Though that may be my GHC background showing.)

@golddranks If crate dependency specifications are kept in cargo's metadata and programmatic equivalents are not allowed into Rust, then I agree the crate graph will continue to be well-defined and so the epoch of a program will be decidable.
I guess the issue of the compiler being written in Rust is just a special case of the semantics of any Rust program mutating over time as new epochs are added. If the compiler produces the same, or at least equivalent, code for a given crate when new epochs are added, it shouldn't be a problem.

@carols10cents

This is a confusion I had when first introduced to this idea as well. The difference is that code written in, say, epoch 2015 Rust will always compile with every later Rust compiler, such as a hypothetical Rust 1.29 that will support epoch 2018 Rust

Why can't a Rust 2.0 compiler support compiling Rust 1.0 code, and linking between 2.0 and 1.0 crates?

I still hope there will be Rust 2.0 versions in the future. Compared with Epochs I like LTS (Long Term Support), in this long-term Support range, we guarantee compatibility. The next LTS version, we can break some old things. I hope that rust will brave the baggage of history, not like c + +. I also don't want the compiler's implementation to be more and more complicated. I hope it's a lean and robust language.

@jethrogb,

In C++ land, a lot of new(er) features are not being used because "the project" is using an older language version. Speaking as a production user, I'm much more likely to start using new features if I don't have to convert my entire codebase at once.

I believe, the main cause of such reasoning in case of C++ is due to a large and fragmented ecosystem. For example, at my work we're forced to use ancient GCC that barely supports tr1, solely because compiler is provided by hardware vendor as part of SDK.
So the lock-in happens because of the environment, not because of language features. Hopefully, Rust ecosystem will stay unfragmented forever, so that would not be a problem.
Unfortunately, "2018 crate using 2015 crate with catch in it's API" remains a problem even in case of a single compiler that rule 'em all.

I am still unclear on how could we add a breaking change, like a new keyword, using epochs without having to update semver

Do you mean, by "updating semver", updating compiler semver or updating the semver of the crate that contains the code?
Because if you mean the compiler semver, it's not a breaking change from the viewpoint of the compiler. The compiler, even after adding a support for a new epoch, still supports compiling that crate, and even supports linking it so it won't break the building of that crate nor does it break the whole build. Where do you see the breaking change there?

A good way to think about this proposal is as an extension to the train model. Rapid releases are too fine-grained for humans to track easily (quick -- what came out in Rust 1.7??). Having a regular, say, two year cadence for tying everything together, putting out a new edition of the book, polishing and shipping a coherent product is incredibly valuable.

I wonder: Is it really?
This RFC acknowledges that Rust's frequent releases are a good thing - rightfully so! It is a very noticeable difference to the approach taken by most big mainstream languages (Java, C++) where new features only ever arrive with new major versions (aka epochs or whatever you want to call them) and I believe it keeps the language alive in the sense that it is always growing and improving, never completely frozen for prolonged periods of time (years).
Even though I don't remember the exact version number, I do know that I started using ? the version it was stabilized - to name just one example. What I'm trying to say is that I really can't imagine features just sitting there, available but unused, waiting for the next epoch - nor would I want it to be that way. And once features are stable, they usually are nice and polished, I consider that one of the train model's cornerstones.
And from that perspective, what does an epoch really mean apart from fact that it may break things? All that remains is some kind of status report: "During the last epoch, these new features were stabilized and are in use now."
To me this seems a lot like an attempt to map Rust's current release cycle onto the well-known one from C++ and while it's certainly a great inspiration, I can't help feeling that we might be better off embracing what we have right now instead - a constantly evolving language. In other words, I don't really see epochs pulling their weight unless there are breaking changes.

One more - If a 2018+ crate wants to use a 2015 crate with catch in its API, are they just out of luck?

C# deals with this by allowing you to do @class even though class is a reserved keyword, https://msdn.microsoft.com/en-us/library/aa664670(v=vs.71).aspx
ReactJS deals with this by slightly renaming things, e.g. for to htmlFor, class to className. https://facebook.github.io/react/docs/dom-elements.html

Could we add a raw escape for identifiers?  Something like r#catch which always parses as an identifier, no matter what keywords exist.  It doesn't have to be pretty, as this should be an uncommon case, but we'll at least have some means for newer epochs to use that hypothetical old crate with catch in the API.

@golddranks

Do you mean, by "updating semver", updating compiler semver or updating the semver of the crate that contains the code?

Updating the semver of the language, which is the only thing we should be talking here about.
This is about Rust, the language, and which version of it we are writting code in. Making a breaking change, requires a major semver bump of the language. Whether rustc, cargo, ... can compiler different language version with/without annotating the source code or the crate, that's a completely different issue.
Consider the catch example. It says that because we have fixed the version of the Rust language used on the example, then "this code will compile on all Rust versions" (it should say, on all rustc versions). Then, as you increase the version of the language you are using, the code will fail to compile. Why? Because we introduced a breaking change in the language.
So IMO, while we can keep rustc versions in 1.xx.yy because we never want to break the backwards compatibility of rustc, we should still be honest an admit that we made a backwards incompatible change in Rust, the language. Just because one can compile Rust 2.0 on rustc 1.50.3 does not mean that the change did not happen.

Unless I missed it, the RFC does not address the tensions between fixing compiler bugs and compatibility. Maybe this is already well known therefore out of scope?
As a concrete example, if we discover Undefined Behavior which happens to compile in a way people are relying on, do we fix it thus breaking their programs behavior? If the exhibiting programs "should" not have been accepted, can we stop accepting them? The latter option (but not the former, strangely) clashes with guarantees the RFC makes.
I for one am for prioritizing bugs over stability, but this is easier if the messaging clarifies your stance in advance.

I like the idea ok epoch, but It seams to me that they should be tied to the major compiler number. I agree that big version numbers are good for advertising, but if there is no clear reference between the compiler version number and the supported epoch, this will turn out to be a mess instead.
Allowing new backward incompatibles syntax is a breaking change that require increasing the major version number. Ok there is a great compatibility feature that is on by default so existing code won't break, but the the major version number should reflect the general Rust direction. And it seems to me you intend most of the people to switch to the new syntax at least for future crates.
More over, forever increasing minor version in just untenable. Java did it until they had to change JavaSE 1.5 to JavaSE 5.0, because even if old syntax could sill be compiled, having strong changes without incrementing the major number did not make sense to anyone. It's even worse with Linux : forever incrementing the last version number was giving a huge feeling of stagnation, so they had to choose to randomly increment the major version number from time to time.

I like the proposal in general, but still have a sour feeling about not bumping the major version on newer epochs.
The problem is that, when talking about Rust, we're actually talking about two things: The language and the toolchain.

There has been a long-standing question around Rust's evolution: will there ever be a Rust 2.0 in the semver sense?

Newer epochs are effectively wanted to make backwards incompatible breaking changes to Rust the language and would thus warrant a major version increase.
Rust the toolchain however would target multiple versions of Rust the language and thus tries to prevent breaking changes so it won't need to increase it's major version. The only time a major version bump would be needed if, at some point in the far away future, an some support for ancient epochs would be dropped.
So the real question is, are we talking about the semver version of Rust the toolchain or Rust the language. I suspect most maintainers look at Rust as the former, whilst most users/outsiders look at Rust as the latter.
In the world of C and C++ there is a much clearer separation between the language and the different implementations (GCC, Clang, MSVC). This is also the reason why the Perl 6 people differentiate between Perl 6 (the language) and Rakudo (the canonical implementation).
I'm not suggesting we rename one or the other, but perhaps it would be a good idea to start explicitly versioning them both. Personally I'd like to see Rust the language follow semver versioning instead of timestamped epochs (so minor backwards compatible changes to the language can also be explicitly versioned) and reading the semver spec I think it's perfectly reasonable to have major versions not be monotonically increasing (so 2015.1.2 followed by 2018.0.0 is correct semver).
But this explicit versioning would mostly make sense as soon as there are multiple implementations and these implementations can explicitly state which versions, major and minor, of Rust the language they support. So perhaps not something for now, but I think worth considering.

One thing I am a bit unclear on:

Can new breaking changes show up in 2015-next after it ships but before 2018 ships? If so, how can it be stable?
Or do all breaking changes for the 2018 epoch need to be final before the 2015-next ships? If so, what is the point of the preview?

Is there a reason the epoch is something we can specify as an attribute on the main module of a crate? Having it be a property you pass to rustc means that it's disassociated with the code, and is another thing that manual users of rustc have to add to every invocation.

@jethrogb Having rustc support multiple epochs at the same time should make it straight forward to upgrade your project if it is split into crates as far as I can tell. That makes it easier to upgrade single crates at a time, or just writing new crates in new epochs. If one would want to upgrade of course :)
I generally really like this, but I do agree with @gnzlbg and @internetionals that it creates some confusion about how we version the language and the tooling. If an epoch has a breaking change, then it is a effectively a bump of the major version of Rust, except that rustc doesn't need a major bump to handle it.

@gnzlbg Ah, in that case, I pretty much agree with you; I thought that by "updating semver" you referred the compiler semver since that is pretty much the only thing that has been relevant this far. You seem to think that there is also Rust-the-language semver which has just been updated in lockstep with the compiler, which makes sense; I always thought that there is only implicit Rust-the-language version, and it hasn't had a proper version number of it's own (yet). From your standpoint I understand, that it's weird and unnecessary to have both lang semver AND epochs. On the others hand, I took this proposal as a first step of having versioning in the language at all.

I too would like to see Rust 2.0 approach instead of epochs. I understand marketing reasons behind trying to hide breaking changes or idioms evolution, and to endorse language change, but personally for me epochs are less honest way of handling potential breaking changes and significantly more confusing in result.
About coarse grained updates and endorsement of big changes: I think it can be solved by making say each 10th release of Rust something alike to LTS. So no need for epochs here.
So lets imagine we accumulated enough proposals which require breaking changes. It could be language features, new keywords, reorganization of the standard library. (e.g. introduction of PAL) It will be much easier both technically and mentally to use 2.0 to bundle them. Now we want to compile 1.0 crate, instead of keeping all deprecated features in the 2.0 we will be able to use both 1.0 and 2.0, interoperability between which will be handled by 2.0 tooling. And long needed feature of marking minimal required Rust version for the crate will be perfect fit here.
Also this approach will allow to naturally implement new proposals both into 1.0 and 2.0 branches of the compiler. And thanks to the tooling transition between major versions and fragmentation of the crate ecosystem should be much less painful compared for example to Python case. Obviously you will not be able to use crates which require Rust 2.0 while being on 1.0, but it the same story with 1.x releases.
So to sum up: I propose instead of keeping all compatibility luggage to try to make compilers with different major versions work with each other seamlessly and effortlessly for the users.

@newpavlov That would require backporting performance and bug fixes to older compilers since some crates will be stuck in 1.0 forever once they stop being maintained. This means we'll see things like 1.18.4 being released several years from now to improve compilation speed to support crates written today.
You'll need to have a future version of rustc and a past version of rustc both installed to compile a project with older dependencies.

@iopq
Yes, the same story with LTS releases inside one major version. So we'll need to release something like 1.20.6, while having 1.28.0 as a last version, if there is significant bug fixes or speedups.
And yes to the second part too, you will need both old rustc and stdlib if you want to compile Rust 1.0 crates. But I think it's an advantage, as instead of bundling everything into one compiler you make clear distinction between them. Also on a scale of 5-10 years I think it will help with replacement of abandoned crates with the forks to remove Rust 1.0 dependency, which will help with the evolution of the ecosystem.

Will rustc/cargo suggest upgrading to a newer epoch once all the deprecation warnings have been resolved (or if there weren't any in the first place)?
Also, I think this point by @Aaronepower  was overlooked:

If for example someone writes a program for a company in current Rust, the program's code isn't touched much for a couple of years till lets say mid 2020 and the programmer decides they want upgrade the code. If they simply added epoch = "2020" to Cargo.toml couldn't they have a program that works in both 2015 Rust and 2020 Rust that behaves differently?

Will rustc/cargo suggest upgrading to a newer epoch once all the deprecation warnings have been resolved (or if there weren't any in the first place)?

Some people might need to keep compatibility with older rustc toolchains, so if newer toolchains suggest this, there should be a backwards compatible way of disabling this suggestion without breaking thing in old toolchains.

My main concern is the psychology of the whole thing. After the Python 2/3 split, this sort of talk makes me uneasy, even if there's a guarantee that crates using the old and crates using the new will always be able to coexist within the same binary.
This could easily be seen as Rust trying to language-lawyer its way around the stability promise, which is something it can ill afford at this point in its lifecycle.

Some people might need to keep compatibility with older rustc toolchains

I believe the whole idea behind Rust's focus on stability is for people not to have to use (and therefore support) older rustc versions, at the cost of the rustc developers having to support older code. But I've seen some crates do this, so what am I missing?

@ssokolow

Programs written in the newer Rust 1.15.0 language do not compile with older toolchains like rustc 1.0.0.
Programs written in the older Rust 1.0.0 language do compile with newer toolchains like rustc 1.15.0 (modulo bugs)

Major versions bumps to Rust, the language, do not imply that program written with older Rust language version won't be able to be compiled with newer toolchains supporting new language versions and/or linked against crates compiled with newer toolchains. That is, we can have our cake and eat it too as long as the toolchain still supports seamlessly using older Rust code.

This could easily be seen as Rust trying to language-lawyer its way around the stability promise, which is something it can ill afford at this point in its lifecycle.

Stability just means that old code will compile with newer toolchain, nothing more, nothing less. This proposal basically states that we can evolve Rust, the language, as long as we ensure stability.

@gnzlbg Don't convince me, convince the world.
"If you have to clarify, you've already lost because the vast majority of people will never contact you" is one of the number-1 things any good marketing or PR course will teach you.
That is what worries me. Rust is my favourite language by far and I don't want to see interest and adoption backsliding.

@bugaevc

I believe the whole idea behind Rust's focus on stability is for people not to have to use (and therefore support) older rustc versions, at the cost of the rustc developers having to support older code. But I've seen some crates do this, so what am I missing?

Microsoft ships Windows 23 with their own toolchain supporting Rust 1.123. You need to use this toolchain to target Windows 23, machines with Windows 23 live for 10 years, and Microsoft never updates this toolchain. If you want to program for Windows 23, you need to use Rust 1.123.
What I'm saying is that if I compile my Rust 1.123 program on Linux using a modern rustc 1.250, then I will get lots of deprecation warnings because there are new things that I cannot use if I want to target Windows 23, which I want to target. That's all right, but I want a way of turning those warnings off, or to tell rustc that this program needs to be in strict Rust 1.123 mode and that checks that I don't use anything outside that subset.
That allows me to develop in a new shiny machine, and target an old legacy system.

Microsoft ships Windows 23 with their own toolchain supporting Rust 1.123. You need to use this toolchain to target Windows 23, machines with Windows 23 live for 10 years, and Microsoft never updates this toolchain. If you want to program for Windows 23, you need to use Rust 1.123.

That's an interesting hypothetical situation... By the way, this RFC could use a mention about how this affects other Rust compilers (including future and non-friendly third-party ones). Is, for example, MIR an integral part of the Rust language or a rustc implementation detail (it's called internal representation after all) -- what if other compilers (e.g. the GCC Rust frontend) don't use MIR?
But yeah, there needs to be a way to turn off specific warnings and warning groups. Maybe there is one already, but thankfully I never had any reason to use it, whereas in the C world -Wno-some-warning is omnipresent.

I feel that the RFC is conflating multiple concerns. I can discern at least four different aspects that (ideally) should be separately discussed:

Cargo and rustc should be able to receive the required epoch for each crate and/or source file.
Cargo and rustc should be able to use multiple epochs in the linking phase.
The epoch can introduce changes that would be normally considered breaking, but they should be limited to (so to say) "the front-end translation".
The epoch will be used as a public name for describing a particular set of features, much like C++11 or C++14.

I believe that the aspect 1 and 4 are ultimately the way to go (and agree to @SimonSapin that the major version should not change for the purpose of semantic versioning), but I'm fine with not having the aspect 2, and very frustrated with the aspect 3.
The aspect 4 alone is a big paradigm shift in the entire Rust ecosystem: essentially the epoch will be the major version for the Rust language, except that it won't break existing code base. The aspect 1 can be seen as a way to empower new epochs: we want to make people want more recent epochs. The aspect 2 is an important, but not mandatory, way to reduce the friction in the aspect 1. But I cannot see this level of justification for the aspect 3---all listed concrete examples are too weak evidences for such a big change (at least for me):

The catch { ... } syntax conflicting with the struct literal should be regarded as a (minor) failure in the RFC process (AFAIK it was first noticed well after the original discussion), not as a reason to introduce a breaking change in this fashion. Many RFCs tend to avoid such a syntax conflict for the very reason, and as #2033 shows we have a suboptimal but last resort syntax in the form of macros.

It should be possible for Cargo to disable implicit module declarations when the conflict is expected (along with proper warnings before rustc failing). Not supporting explicit module declarations will be breaking changes anyway, so this does not constitue the needs for epochs---this merely shows the needs for breaking changes. And again, the RFC should be there for the actual discussion :-)

While technically possible, I feel repurposing syntax across epochs seems a very clever cheat for the semantic versioning.

Minor type inference changes are already considered minor changes in #1122, I believe.

In the other words, I think the spirit of the RFC is sane, but the implementation detail is very fragile. If my judgement is correct, the current set of limitations will mainly allow for syntactic changes that will be more "intuitive" or "easy", but not much else. In order to get the language evolution via epochs traction we need more detailed and attractive story: for example, the standard library changes can be very important. Otherwise we may not want the aspect 3 as it will only provide marginal benefits at much confusion.

@bugaevc

That's an interesting hypothetical situation...

If you replace Rust with C++ the situation is not hypothetical anymore. This is how compiling C++ code for Windows actually works... I wish I had made it up...

I very much like the idea of handling older rust by mapping it to later Rust and never breaking anything.
However, I feel that one obvious alternative is missing.
What if we make sure that Rust 2.0, 3.0, etc is able to compile 1.0 code?
Essentially, leave the current RFC proposal as-is, but replace "epoch" with a "major Rust version". Crates that want to use Rust 2.0 - only features must say "2.0" in their crate definition, much the same way we intend to do with epochs.
I understand that we want another terminology because people generally assume that new major version means breakage, but this is an alternative that I feel has to be mentioned and considered.

I also like the idea of just using new major versions. Aside from the benefits already mentioned, using years is problematic because you can't refer to future versions easily. (Think of C++0x which took so long to materialise that it eventually became C++11!)
This RFC seems to recognise that to some extent (by using <year>-next to allow referring to one epoch in the future) but it would be much simpler to just use the existing semver system, and "next" versions can be referred to in standard semver notation, like 2.0.1-alpha1.

Is, for example, MIR an integral part of the Rust language or a rustc implementation detail (it's called internal representation after all)

@bugaevc I'm pretty sure IR stands for "intermediate representation", not "internal representation".

@jplatte oh, right, sorry about that. Still, is it an implementation detail or a part of the language?

Should rustc default to the latest epoch instead?

FWIW

In GCC 5 the default mode for C is -std=gnu11 instead of -std=gnu89
In GCC 6 the default mode for C++ is -std=gnu++14 instead of -std=gnu++98

So there is some precedence for eventually making new "epochs" the default for the compiler. Though that seems like a bit of an implementation detail of the compiler itself.
I'll echo what others have said in that we often talk about Rust the language and rustc interchangeably because there is currently only one compiler, but perhaps we should make a bigger effort to separate out those two concerns.

@bugaevc It's an implementation detail.
@johnthagen Making the default be the latest epoch means that it's a breaking change to upgrade rustc if you don't specify your epoch because your identifier could become a keyword in a future epoch - though very remote possibility for any single crate, in aggregate it's almost guaranteed.

@Havvy Agreed, it seems like Rust is proposing more breaking changes than C++ has (adding a new catch keyword as opposed to, for example, repurposing the auto keyword in C++'s case). Perhaps setting the default to C++14 isn't as equivalent to what Rust is proposing.
I guess I wasn't proposing that the default actually be the latest epoch, but perhaps that over time you could bump it from 2015 to something else and there is some precedent for doing this.

Three questions about this RFC:

If an epoch is supposed to label a paradigm shift and the compiler has not unlimited support for all epochs, won't this put back-pressure on the developers to upgrade on newer epochs? We are not going to support infinite number of epochs aren't we? If we are not, then let the paradigm shift decision to the developer.
Is this epoch approach not cementing the monoculture of having a single rust ecosystem from compiler to tooling? Maybe this will limit the chance to get other compiler implementations that are more constrained of resources and maintainers, e.g. embedded systems.
Furthermore, do we have a paradigm shift in our current nightly features on-maturing portfolio that could be declarative enough for a new Rust epoch beyond impl trait or am i missing something?

Summary: I would prefer to have ONE standard Rust version
I would like to add clarification how exactly epoch is a better than bumping major version?
epoch seems to be just hack around semver.
It makes a little sense to introduce new meta variable into compiler when major version could server the same purpose.
Second: if epoch are going to happen often, then it is going to fragment Rust ecosystem significantly. Do you expect every library to keep their crates updated with epoches? I doubt that will be possible.
Eventually we'll have many many epoch and each of them has own extensions.
I don't want to remember each and every epoch extension in order to read Rust code.
Breaking changes each two-three year is definitely awful idea.
Current state of Rust is definitely not stagnant, and epoch is just the same major version bump.
Do we really think so badly of Rust 2.0? If so then just add possibility to support older versions.
There is definitely no need to add new meta variables to Rust compiler when you already have more than enough.
Current evolution seems appropriate for Rust.
Are there many examples of serious breaking changes we would like to see?
If there are: then accumulate them and release 2.0 in maybe 5 years.
Rust 2.0 is less harmful than having breaking changes each few years

I think calling these epochs is just tiptoeing around it. It feels a bit hypocritical and adds needless complexity. Please, let's just stick to semver and bump the major version number. It's honest and a lot clearer for every one.
I like the backward compatibility story and the plan for the compiler to support it, but I also think that we should not be too greedy or ambitious. It's a lot of complexity and technical debt that builds over time. We should be reasonable over how much of it Rust can take on.
Another drawback of this RFC is that some breaking changes can never be made.
We could compromise and maintain a limited backward compatibility. A new major release would deprecate the previous one and introduce breaking changes but maintain backward compatibility, exactly as proposed here, but promise to do so for x years/y releases.
This would give us the time to write and mature tools and migration stories to help people and companies move on. Eventually the promise runs out and at the next major release, we can decide to drop it or commit to support for one more major version. This goes on for as long as the burden is acceptable and backward compatibility with the next major release is possible.
This would give us a good stability story and alleviate fears of splitting the ecosystem while avoiding the caveats of C++ forever™ backward compatibility and leaving the door open for any evolution in the long run.

I strongly dislike this RFC. Ultimately this sounds like just having semver-major changes with a different name, and calling it OK because the compiler can continue to compile/link old versions. I do strongly appreciate the focus on limiting breaking changes to crate-local, but I don't think we need to introduce fancy marketing for that.
One question that I don't think has been sufficiently addressed is what happens with new features on old epochs that could be added backwards compatibly. For example, could I expect that every feature from the 2018 epoch except for catch will be useable on the 2015 epoch? What about features from the 2021 epoch? How are we going to deal with the increased complexity of every release having to discuss which features affect which epochs?
If new features won't continue to be added to old epochs as they're stabilized, then we should just call this what it is -- a semver major version bump with solid tooling support. Being able to link across multiple versions is great, but saying that there is no breakage would be a lie. This scenario would mean that Rust has failed its promise of stability without stagnation, as I have to deal with breaking changes in order to get new features which... sounds pretty much like normal version bumps.

Awesome! I would place myself in @steveklabnik's "never 2.0" camp from the stance of someone who's nudging people towards adoption but still (2+ years after 1.0) explaining Rust's stability promises to colleagues and acquaintances. But it'd be nice to have an escape hatch for the very conservative examples this RFC mentions. I think some excellent questions have been raised in this thread about a number of details (@sgrif right above me, for example), but in general I think this seems like a positive direction to explore.
One specific point that I'm curious about: the RFC currently states that rustc will accept an epoch flag but that the source of truth will be a crate's Cargo.toml. Does this satisfy the requirements of people who are working to support Rust in other build systems like Bazel and Buck? Does it satisfy the requirements of people packaging crates for distribution? I'm not sure who the right people to ping are, but it seems to me that it would be good to get feedback from those who are supporting and maintaining non-Cargo tooling around Rust.

One specific point that I'm curious about: the RFC currently states that rustc will accept an epoch flag but that the source of truth will be a crate's Cargo.toml. Does this satisfy the requirements of people who are working to support Rust in other build systems like Bazel and Buck? Does it satisfy the requirements of people packaging crates for distribution? I'm not sure who the right people to ping are, but it seems to me that it would be good to get feedback from those who are supporting and maintaining non-Cargo tooling around Rust (EDIT: given that this RFC would affect every Rust application within a couple of years, not a niche configuration option).

@ignatenkobrain is packaging Rust crates for Fedora.
@pvalkone and @t6 are working on packaging for FreeBSD.
@joshtriplett is working on packaging for Debian.
@dstcruz has packaged things for Chocolatety (Windows).
@x4121 has packaged Rust applications for Ubuntu.
@svenstaro has packaged Rust applications for Archlinux.

@BurntSushi And not to forget NixOS packaging crates rust-packages.nix :)

For openSUSE you can check the .changelog in the rust repo

This RFC looks great to me; it addresses all the concerns I'd had when I'd previously heard about the idea of "Rust 2.0".
I do have the same concerns @cuviper did about macros; @aturon, would you consider adding some explicit text to the RFC saying that's an issue that needs to be addressed, and hypothesizing how it might be?  Important cases include "new macro in old code", "old macro in new code", and "old/new procedural macro processing old/new code", all of which can involve nesting of several levels of epochs. We don't want to end up with a world where the compatibility story says "except for macros".
Apart from that: I don't see any obvious issues that would make this problematic for distro packaging. We already would have to package new rustc and new cargo before packaging crates that depend on them; this makes that more well specified. It'll be easy enough to parse an epoch field from Cargo.toml and use that when generating packaging to specify versioned dependencies.

@aturon at least superficially, this proposal seems a lot like how Javascript/Babel.js works. There is a "core" Javascript and a set of transformations which can be opted into.
Instead of being limited to "epochs", can rustc have a similar API to Babel, and specify a set of transformations, and a set of higher level "official presets".
This does two things: 1. Gives the Rust compiler devs a working model to follow. 2. Allows developers to pick and choose transformations piece-meal.
Example: There is some official preset rust-2015, and an upcoming official preset rust-2018. 2018 includes [async_await, dyn_impl]. My crate uses bare traits a lot, so I cannot use dyn_impl without effort and therefore cannot userust-2018 just yet, but I'd love to start using async_await. It would be nice to be able to specify, transformations: [rust-2015, async_await] then later move to transformations: [rust-2018]

Question: does a new stable feature lands for all epochs? For example, if an hypothetical HKT is stabilized Rust 1.43, would it be available in Rust 2015, Rust 2018 and Rust 2020?
For me it's obvious it should; otherwise, a project that keeps rustc updated but opts for keeping the Rust 2015 epoch will be locked out of a significant part of the ecosystem (as new versions of libraries change their API to adopt new language features).
Further than that, I think that even epoch-needing, backwards incompatible features (like changes to inference) should be available to older epochs with an opt-in stable #[feature], so that a project can convert to a new epoch gradually, feature by feature. This echoes @yazaddaruvala's comparison to Javascript mechanisms for language evolution.

Question: In Cargo.toml, does epoch sit under the package table, or on the root of the toml?
The current wording sounds like it's the latter, but I'm guessing it's intended to be the former, which also allows a workspace to host packages with different epochs, that's necessary for gradual upgrades in larger projects.

Under Alternatives to epochs:

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:

Lack of clarity around ecosystem compatibility. If we allow both 1.0 and 2.0 crates to interoperate, we arrive at something like this RFC. If we don't, we risk splitting the ecosystem, which is extremely dangerous.

What does interoperate mean here?
To me, it sounds like it means rust=1 code being compile by rust=2 compiler, the same way as a epoch=2018 compiler would work with epoch=2015 code.

Likely significant blowback based on abandoning stability as a core principle of Rust. Even if we provide a perfect rustfix, the message is significantly muddied.

If a rust=2 compiler can compile rust=1 code in a compatible mode (the same way most C++ compilers work), why would it be abandoning?

Much greater temptation to make sweeping changes, and continuous litigation over what those changes should be.

I don't understand how this is different from the epoch approach. Is it something that has came up in previous discussions/RFCs? Could you please elaborate for someone who doesn't know the background?

So as I understand it, this will:

make it so that crate authors who abandon their crates can point at this as a reason to never fix anything again. Therefore increasing the number of abandoned crates.
encourage crate authors to not adopt newer API designs that are enabled by new features, thereby making your code much more cumbersome and annoying to write.
make the rust compiler significantly more complicated for the benefit of people who have abandoned the language anyway.
increase the amount of dependency hell because some garbage dependency was abandoned and therefore your indirect dependencies are more often incompatible with newer versions of your direct dependencies.

Given that understanding:
If this does get forced on us, which I'm very much against, then hopefully we will also get corresponding features in cargo to blacklist crates that are known to be abandoned (or which are worthless on our platform like how everything seems to pull in win32 crates) and set a minimum epoch that crates must be coded against in order to be included.
Basically all I ask is that if you make allowances for people to abandon their crates then give us tools to avoid using them. "You only pay for what you use" only has value if you know you are using something.

I'm in favour of this. But I'm wondering what this means for our toolchains, particularly nightly where unstable features hang out.
Will nightly unstable features be available to any epoch? Or just the latest?

There seems to be a lot of confusion about what semver major versions imply and what "epochs" imply, so it's hard to tell when we're actually disagreeing on the meat of the RFC or just disagreeing over whether "epoch" is the right term for it. For the purposes of this discussion, let's say that there are two strategies for dealing with a breaking change: A "hard cutoff" versus a "feature toggle".
A hard cutoff means:

At some arbitrary point in time the Rust compiler stops supporting some existing functionality (let catch = 10;), and starts supporting some new functionality in its place (catch { ... })
No version of the Rust compiler supports both the old and new functionality at the same time. This implies your dependency graph must contain only code using the old functionality, or only code using the new functionality, with no mixing whatsoever.
This can be mitigated by compiler feature detection, which in Rust probably means a #[cfg(...)], allowing a crate to "manually" support all compiler versions.

A feature toggle means:

At some arbitrary point in time the Rust compiler gains a flag that tells it whether to only accept some existing functionality (let catch = 10;) or to only accept some new functionality in its place (catch { ... }).
Every version of the Rust compiler after that arbitrary point in time supports both the old and new functionality, just not both in the same crate. This implies your dependency graph may contain both old code and new code without issues. No ecosystem splitting.
This flag applies at multiple granularities, presumably including command line arguments and a per-crate setting, in order to make a "mixed dependency graph" workable in practice.

Assuming I read it right, this RFC proposes "epochs" as a feature toggle. In the Alternatives section, this RFC describes a "semver major bump" as a hard cutoff.
Many of the comments made in this thread appear to be agreeing that we should do a feature toggle, but proposing that we present it as a semver major bump instead of presenting it as epochs. Many other comments appear to be confused that the RFC talks about a semver major bump as if it was a hard cutoff, while they feel a semver major bump typically implies a feature toggle. Still other comments seem to be assuming that epochs are a hard cutoff and ask how that's any different from semver major bumps (to be honest, I'm not sure if this third group actually read the RFC, but let's assume they did).
To me, a semver major bump always implies a hard cutoff unless explicitly stated otherwise (regardless of whether it's the "language" version or the rustc version). I believe "epochs" is merely being proposed as a formal term for the practice of feature toggling a breaking change since we don't really have a term for that (despite it being the de facto practice in C++). I'm quite surprised to learn that there's such a wide gap in interpretation of semver major versions, and this tells me that the RFC is already "muddying our stability story" more than I thought it would.

And with that out of the way, now I can make my actual point.
As this conversation unfolds I am becoming concerned about the intended frequency and scope of the proposed "epochs". Despite my enthusiasm in my initial comments, it's becoming clear that there is a pretty big gap between how I interpreted the RFC and how its author intended it.
An oversimplified version of what I was imagining: Later this year, as part of the current epoch "2015", we get impl Trait, dyn Trait, #[async] and await!() stabilized. Then epoch "2015-next" (or 2018, or 2.0, or whatever) becomes the one where we change what "bare Trait" means, add async/await as keywords, add catch keyword and syntax, and do whatever slightly-breaking coercion/inference tweaks we managed to agree on. All non-breaking future features simply get stabilized for all epochs in some future compiler version, including specialization, const fn, CTFE, const generics, virtual structs, higher-kinded types, dependent typing or whatever we end up actually doing. Thus, when we release Rust 1.99 and I upgrade to that compiler version, I can write code against epoch "2015" that doesn't get to use catch/async/await keywords but can still do higher-kinded compile-time-evaluated shenanigans with #[async] and await!() if I really want to. And if I then choose to flip the epoch setting, only the bits of code that actually use an "epochified feature" like async/await or catch would need to change. There might not ever be a third epoch in the long run if we never manage to come up with another good reason to make a breaking syntax change.
Obviously, that's not what @aturon was describing in https://github.com/rust-lang/rfcs/pull/2052#issuecomment-312774552, in particular "Epochs are about aggregating change, only a small fraction of which is breakage". I thought the primary purpose of epochs was going to be aggregating breaking changes, and all the other fuzzy suggestions around marketing eras of the language were strictly optional secondary concerns that might piggyback on the breaking changes for convenience and timing reasons.
That's not to say I necessarily disagree with the idea of epochs as a primarily marketing proposal. It is true that non-breaking changes still introduce a lot of ecosystem churn as idioms evolve which is worth addressing somehow, and having a new edition of The Book for each epoch to codify these new features and idioms seems particularly appealing. But if this is the primary goal, then this RFC feels like an awful lot of technical machinery for a lot of vague non-technical benefits, and I'm skeptical this is the correct way to try and achieve those benefits.
My current gut feeling is that I'd rather keep "marketing epochs" separate from "breaking syntax change epochs". It makes sense to commit to making a marketing epoch happen "regularly". It makes no sense at all to commit to making breaking syntax changes "regularly" (obviously that's not what the RFC proposes, but it's an example of why lumping these two things together doesn't necessarily make sense).

To avoid the issue of publishing a crate in epoch "2015-next", later to discover that a new keyword needs to be reserved possibly breaking that crate, we do all the breaking changes in 2018 in 2015-next at once (upgrading warnings to hard errors). What would the gap in time between knowing the breaking changes we want to make, and them stabilizing? That is, is 2015-next stable for a few months at most before epoch 2018 is finalized?

@Ixrec

No version of the Rust compiler supports both the old and new functionality at the same time. This implies your dependency graph must contain only code using the old functionality, or only code using the new functionality, with no mixing whatsoever.

I think you forget potential middle ground in your classification, having "hard cutoff" compilers which work together through tooling. Yes, it will require stable ABI to work (which is needed even without the proposed approach), but we don't have to ship Rust 2.0 tomorrow, so plenty of time to work on that.

"TazeTSchnitzel" on HN had a good insight into a potential problem:

If "catch" becomes a keyword in a future epoch, how does code in that new epoch call functions with that name defined in libraries from previous epochs? Does Rust support stropping?

Thsi thread grew way too fast. As at least https://github.com/rust-lang/rfcs/pull/2052 mentions, it is silly to not have minor versions for the langauge, which needs it, and do for the tools, which probably don't. I have other stylistic criticisms, but I will not mention them because by far the most important change we NEED is a richer notion of compatability for language versions.

@joshtriplett an option is renaming symbols on import (ctrl+f rename on this thread).
This should probably be added to the RFC (to explain how it's at all possible to add a keyword without breaking compatibility with the existing crate ecosystem)

@joshtriplett

If "catch" becomes a keyword in a future epoch, how does code in that new epoch call functions with that name defined in libraries from previous epochs? Does Rust support stropping?

I brought that up too, and @aturon replied, as did a few other people.  I didn't know the term stropping, but that's what I was getting at with the idea of escaping it.

I'd rather we use use and make aliases than have stropping in there.

This looks uncomfortably lot like IE's X-UA-Compatible meta tag, which Edge abandoned.
I doubt the complexity proposed here is worth the benefit of being able to change some things.
To the extent there are super-useful breaking changes, I'd like to see an analysis why epochs would be a more appropriate trigger than opting into specific breaking features on a per-crate basis.
Not having seen such an analysis, based on Web experience and observation from X-UA-Compatible, I'd be inclined to go with per-feature opt-ins for things that have to be opt-in due to breakage otherwise instead of bundling stuff behind a time/release-oriented trigger.

Love the mechanics of this RFC. Crate compatibility across epochs is essential.
Not so keen on the naming, but it's okay. Personally I don't care much for semver and think we should just call the next "version" Rust 2.0 instead of Rust 1.27 epoch 2018 or whatever. (Compare Linux 3.0 "because 2.x had been around a long time" and Firefox now on what, 54.0?)

https://news.ycombinator.com/item?id=14700516 had the great idea of "super-major" versions to denote interoprability after desugaring. That means we retroactively:

Rename the current version to 1.1.x.0
Next epoch would instead be 1.2.x.0
<IP_ADDRESS> will probably never happen.

I can't really see what the distinction is between "you have to change a setting to enable epoch 2018 which has some breaking changes but is compatible with code from epoch 2015" and "you have to change a setting to enable Rust 2.0 which has some breaking changes but is compatible with code from Rust 1.x". The whole reason for semver is so you can clearly and unambiguously communicate when breaking changes happen - why are we now walking back from that in favour of a year-based major version number?

@Ixrec

Still other comments seem to be assuming that epochs are a hard cutoff and ask how that's any different from semver major bumps (to be honest, I'm not sure if this third group actually read the RFC, but let's assume they did).

I am in this group and feel a bit insulted from your passive-aggressive comment, but I'll bite, @aturon mentions in the RFC:

will there ever be a Rust 2.0 in the semver sense? This RFC gives the answer: certainly not in the foreseeable future, and probably not ever.

To be crystal clear: Rust compilers are expected to support multiple epochs

The proposal is much akin to C++ standards,

which to me means that:

Epochs do not require a semver major version bump of anything, not the language, the implementation, etc.
Rust compilers must not support all epochs, so either old code will stop working or he is only talking about the rustc + cargo implementation.
the C++ standards are backward incompatible and no major compiler supports most of them (e.g. none of the official compilers for the major platforms Windows/MacOS/Linux supports C++03).

I couldn't find anywhere in this RFC a mention of what exactly is this RFC talking about. Is it talking about Rust, the language? Is it talking about rustc + cargo? Since you seem to be completely sure about what this RFC is talking about, maybe you can point me to the place in the text where this is explained?
Otherwise I consider the lack of this information a fundamental flaw in the RFC.
If I interpret the RFC as "talking about Rust, the language" then it is proposing to introduce breaking changes in the language spec without following semver (note that the mentioned C++ specs are backward incompatible specs).
If it is talking about rustc + cargo as one implementation, then it is proposing that:

proposing that Rust, the language, can break backward compatibility in some cases
promising that the ecosystem of those using rustc + cargo won't break

In both cases, this RFC introduces under which circumstances we are allowed to break backward compatibility of Rust, the language, in an extremely obscure manner due to the introduction of epochs, something specific to rustc + cargo, that is also left a bit hanging because not all Rust implementations are required to support all epochs.
In my opinion, discussing when are we allowed to break backward compatibility of Rust, the language, is an extremely important and serious topic. Whether the ecosystem/toolchains can handle these transparently is an orthogonal topic.
IMO it would be better to close this RFC and to submit an RFC discussing that puts on front and discusses more concretely how to evolve Rust, the language, and also discusses what are the conditions that backwards incompatible changes must satisfy, how to group backwards incompatible changes (epochs, cargo.toml, compiler flags), and maybe add some more concrete/hard wording (e.g. a backward incompatible change can only be merged if at least one compiler + ecosystem has shown that this change can be introduced without breaking the ecosystem, or all conforming Rust 2.x implementation must also be conforming Rust 1.y implementations) an examples (e.g. how would this use syntax word when importing a function from an incompatible Rust language version that in the current version clashes against a keyword). In particular, "epochs" look to me like almost an implementation detail of something that is much bigger. Without this bigger context, it is hard to discuss epochs at all.

Another point about stability: should the compiler warn about all deprecations?. Imagine, for example, that project X must target Rust 2015 (don't ask, there are many reasons), but you have the Rust 2018 tool-chain installed (because it is faster). Do you want to see deprecation warnings?

identifiers becoming keywords: easy to solve, but technically you don't need to
std library features with existing replacement: you could update your code (okay, but doesn't leave you the impression that Rust 2015 is stable)
std library features without existing replacement (e.g. altered features): you can't update

You could of course add an option to Cargo.toml disabling deprecations in Rust 2018, but then you run the risk that the Rust 2015 tool-set doesn't like the new option.
Overall, I feel that only things you shouldn't do should be warned about. If Rust 2015 is supposed to be a stable, valid target in ten years time, you should be able to use 2015 features without getting nagged about using replacements instead, hence no deprecation warnings.

Having read quite a few comments here, I've have made some significant changes to the RFC (will make a PR):

Use major/minor versions instead of epochs. This is really just marketing, but I've seen a lot of people in support of using version numbers; I think either is workable. The change is mostly straightforward, aside from talk of "epoch years".
Make a distinction between language and toolset versions
Add motivation / use cases
Make deprecations opt-in. This is really just my POV (see previous post).
Made previews opt-in
Clarified that previews are not stable

You can read my changes here. I have [marked] most of the changes; mainly it's the first few sections and last two.

@gnzlbg Here's how I understand it:

Epochs are a versioning scheme for the language
A new epoch is a breaking change.
That scares people, and it shouldn't. I am with the group that thinks this year-based stuff is silly, but I very much agree with those whose opinions crafted this RFC that the breakage being proposed here is quite inconsequential and harmless.

Don't get me wrong. I think we can break the language often and get away
with it with tooling.
But I am more in the camp of "I'd like to just bump semver major version
every single time we make a breaking change" because I think that is
simpler and makes easy to follow in retrospective what changed when.
My only concern is that this is actually what we are discussing here, but
this RFC "hides" it away, and IMO it shouldn't.
We should explicitly address under which conditions will it be allowed to
introduce a breaking change, and what the process will look like. If
tooling cannot make the breaking change "seamless" then we should just have
way higher standards for those kinds of breaking changes. But when tooling
allows us "not to care" about breaking changes, then im in the camp of "if
they are worth it, let's just do it".
On Wed 5. Jul 2017 at 22:36, John Ericson<EMAIL_ADDRESS>wrote:

@gnzlbg https://github.com/gnzlbg Here's how I understand it:

Epochs are a versioning scheme for the language
A new epoch is a breaking change.
That scares people, and it shouldn't. I am with the group that
thinks this year-based stuff is silly, but I very much agree with those
whose opinions crafted this RFC that the breakage being proposed here is
quite inconsequential and harmless.

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/rust-lang/rfcs/pull/2052#issuecomment-313219366, or mute
the thread
https://github.com/notifications/unsubscribe-auth/AA3NpkvUeiTMx6EVvvpEaOLWuubVVc1sks5sK_PGgaJpZM4OMt6b
.

@gnzlbg That sounds basically right to me.

@Ericson2314 I agree that it's fine if we're breaking the language, and it's not that scary for us to do so. But if that's the case, we shouldn't need to try to lie about what we're doing by hiding it behind a bunch of marketing.

I'm very much in favour of the changes @dhardy presented.

@gnzlbg

My only concern is that this is actually what we are discussing here, but this RFC "hides" it away, and IMO it shouldn't.

What if all new Rust releases are available for all epochs? (like a Rust 1.43 available in Rust 2015, 2018 and 2020 epochs, as my question above implies).
In this case, epochs are the best framing: it doesn't makes sense to have a Rust 3.2 instead of 1.43, since Rust 3 would only be equivalent to a single epoch, Rust 2020. And it would be too confusing to keep releasing Rust 1.x and Rust 2.x versions that are identical to Rust 3.x (otherwise you would need to point out that Rust 1.43 is basically Rust 3.2 minus crate-local breakage).
I think it's fundamental to guarantee that nobody has to upgrade from Rust 2015 to benefit from keeping the compiler up to date with the latest Rust version. That is, you don't need to embrace breaking changes just to run the latest Rust. You can migrate your code at your own pace.

Thanks for creating this @aturon and dealing with what is sure to be a massive thread.

After reading through them I feel that @dhardy's changes are a step in the right direction.
However, I feel the terms "Rust version" and "Rust 2.0" cause confusion and create strawmen, since many instinctively read these as referring to the version of rustc.  Perhaps we can just cut to the chase and refer to it as Lang 2.0?

Thanks all for the in-depth commentary so far! I had a chance to talk through some of the responses with the core team today as well, which raised yet more questions.
There are far too many new comments for me to try to reply point-by-point, so I'm instead going to try to tease out the major threads and give some further thoughts.
"Just use major versions!" aka "epochs are an elaborate way of hiding breakage"
I'm definitely sympathetic to this point of view -- much earlier versions of the proposal did just that.
The bottom line to me is keeping our promise: you should always be able to upgrade to a new version of the compiler without hassle. Allowing your dependencies to use different versions of Rust than you do is key to making that happen. That mechanism is the heart of this proposal.
But @sgrif and @gnzlbg, amongst others, have pointed out that upgrading the compiler and upgrading the "language" (here, epoch) are two different things, and new language versions involve breakage. The point of the proposal is to let the two vary independently, so that you're not forced into breakage.
The thinking behind the proposed versioning scheme is then, as with any versioning scheme, primarily about communication:

Keeping rustc on the 1.X series in perpetuity provides clear messaging that upgrading the compiler, alone, does not entail breakage.

Using years for epochs helps tie them to roadmaps, makes it easy to tie them back to dates (and helps indicate just how recent the version is). It has precedent in C++, of course, but you can also imagine adding ".0.0" if you want to see it as semver.

By contrast, using semver for both the language and the compiler, but then having them vary (if we keep the compiler forever in 1.X), seems quite confusing.

Also, any announcement of "Rust 2.0" will take a huge amount of messaging to avert panic that the language is undergoing significant breaking changes. Qualifying it to say "but the breakage is super minor and the compiler will support old versions" helps but can easily get lost in the noise.

If we do go the semver route, I personally would prefer to use the same version for the compiler and the language itself, and just think of the compiler as having a "1.x compatibility mode" etc.
TL;DR: yes, the versioning scheme is in a sense "marketing", but all version schemes are about communication and marketing; we need to look carefully at the tradeoffs.
Why not fine-grained feature flags?
Another option would be to, in essence, use feature flags on the stable compiler for features that involve breakage, giving you a fine-grained way to opt-in (which could potentially be applied per-file).
There's a lot of appeal there, partly because it further decreases the pain of upgrading!
But there are some downsides as well:

These flags will accumulate over time, and it'd be best for code not to have to start with turning them on (as is commonly the case with, for example, Haskell code). So we'd probably want some epoch-like mechanism for saying "please turn on all the features shipped by 2018" or some such.

Allowing fine-grained opt-ins means that, in a sense, we have an exponential number of language dialects, which causes headaches for testing the compiler and communicating about code. If the total number stayed quite small, though, this might be manageable.

Fine-grained flags lose the "rallying point" aka "marketing release" aspect of the proposal, which I'll talk more about below.

You're conflating breakage and "marketing" releases
A major theme of the comments here and elsewhere is to point out that the two explicit goals of this RFC -- a smooth story for breaking changes, and a story for "marketing" releases -- may be better served by separate mechanisms. In particular, there's discomfort with expecting regular opt-in breakage. (Though I'd point out that we do fairly regularly make deprecations, and I'd expect even if we do an epoch release every three years, there will always be some deprecations to clear out.)
It's definitely true that this RFC proposes a single mechanism for serving two rather different goals! I personally saw that as a simplification (and, moreover, don't see a great way to separate them), but rather than defend that choice, I want to take a step back and talk the goals I think we should try to address. Then maybe we can collectively find a better way to address them!
What I personally want
To restate the core motivations, here's what I'm hoping we can achieve:
A means of introducing small breaking changes independently of compiler version, i.e. via an opt-in.
I think the RFC proposes at least a plausible mechanism for doing this. There are a lot of details we could consider changing, but most commentary suggests that the core mechanism at least sounds reasonable.
A form of rallying releases -- a term I prefer over "marketing releases".
I think the roadmap process has been successful so far, but we're missing a clear rallying point where all the work comes together into a coherent product:

For example, new stable features are currently added as an appendix to the book, but at some point we'll want to undergo a deep revision to update with changing idioms. Ideally, this new text would coincide with the library ecosystem adapting to the new idioms, with tooling fully supporting the new features, etc. Laying out a specific timeframe for pulling all of this together helps build excitement and momentum within the community, and helps us avoid wallowing in perfectionism for too long by remembering that we have to ship (even though shipping is done incrementally due to rapid releases).

The rapid release process is great for avoiding rushing, and for getting features into people's hands quickly, but it means that everything is a moving target. Producing something really polished and coherent -- not to mention publishing books on paper, etc -- requires coordination at a coarser grain.

As the RFC argues, new features (whether breaking or not) lead to shifts in idioms, and it's important to have a way to talk about those shifts at a coarse grain, since in many cases features are intended to work in concert for building APIs.

And yes, the above also leads to marketing benefits, since it gives us a more clear-cut point to say "The next major step in Rust's evolution has occurred; come take another look!" Tying together the work across the spectrum of docs, libraries, tools, the compiler, and the language into a coherent such release makes that marketing much more effective.

There was some push back from the core team on some of the logistics here, though. For one, the rapid release model means that, for people using Rust seriously, it's not enough to talk purely in terms of epoch -- you need the compiler version as well. Moreover, several people echoed the concerns in this thread that we want regular rallying points, but not necessarily regular breaking changes, and epochs may conflate the two.
Where to go from here?
In hindsight, it seems quite possible that the core team (who has been iterating on versions of this idea for some time) has over-rotated on fears around messaging a 2.0 release, and over-designed as a result.
I think that @dhardy's strawman revision is a very plausible alternative that addresses both my goals, and the concerns of many on this thread. To recap:

We plan on producing a new major version of Rust regularly, say every 2-3 years for the foreseeable future (so we imagine a 3.0, 4.0, etc). These are rallying releases in the sense described above, and fit into the roadmap process as explained in the RFC.

These major releases may include breaking changes.

However, the compiler ships with a "compatibility" mode for all prior major versions, expressed through Cargo.toml or a compiler flag.
We sing this from the hilltops so that people understand that old code continues to work, and that you can mix and match versions across your dependency graph.

To be clear, these changes are essentially "just" a renaming of epochs to major versions; all the mechanics are the same.
This addresses earlier concerns:

It makes clear that the language is indeed breaking, but we make very clear that there's a compat layer. (It's also natural for that compat layer to allow access to new features that are back-compat).

It gives us a single version number scheme, where the major version is the "epoch" and the minor is the "how far in the epoch you are".

It feels relatively natural to put out a new major release on a regular cadence, with or without breaking changes.

The main downside is one of clear messaging around breakage, as far as I can see.
What do you all think?

@aturon it sounds like no one is really against having a minor version for "how far in the epoch you are", as you put it. Perhaps that should sooner-rather-than later be added to the RFC as-is to decouple that important difference in compatibility information from bikeshedding about isomorphic marketing schemes?

@aturon I think the idea of rallying releases is great. It’s similar to what Firefox is doing for 57. (Just to point out that other people came to similar conclusions about rapid releases.)
But rallying releases don’t need to be more than purely social. (Which is absolutely not a negative word.) A "big" release where many features come together and that makes the news doesn’t need to be technically more "special" from the point of view or Cargo or rustc than the one that came six weeks earlier.
We can talk about idoms of modern Rust in 2018 without using the same terminology as the Cargo config key that opts into breaking changes introduced in 2018. The two are not necessarily related in the first place, since (hopefully) only a minority of new features will involve breaking changes.

@aturon

We sing this from the hilltops so that people understand that old code continues to work, and that you can mix and match versions across your dependency graph.

But you can't in general mix and match Rust 1.x with Rust 1.y, right? Minor versions in Rust can introduce ABI changes, and those aren't breaking changes because Rust doesn't have a stable ABI.
The actual guarantee would be that the same compiler can mix Rust 1 and Rust 2 in the same build, but not necessarily Rust 1.x and 2.y for specific x and y, which is a subtler message that may get lost in the marketing.

What about the alternative that tooling allows you to compile using different rustc versions seamlessly?
For example, a Rust crate from the previous epoch uses i8::checked_add. In the future, this was replaced with i8::try_add that returns a Result (which specifies whether an underflow or an overflow occured) and i8::checked_add has been long deprecated and removed
In the current proposal, the old standard library always exists somewhere shipping with rustc in perpetuity. Why not just let you have several rustc versions compile and link against each other so that if you don't need old stuff you never have to get it.
It seems REALLY hard to support very old stuff in the current compiler, especially if this RFC gets accepted and new epochs start having more ambitious changes.

@dlight You are over thinking it. Code written in all combinations of post-1.0 Rust can be linked together. Code can only be linked together if it is compiled with the same rustc, because there is no ABI stability, ever.

I think it is important to separate Rust language versions from rustc tool versions, but I don't think we need to use semver for the language itself, and I prefer the year-based naming scheme. People are familiar with it, and the level of breakage it implies, from C++ and Javascript. It should allay fears of a Python 3.0 style split since both of those languages are very interoperable across their "epochs" within a tool version. Those languages also have several implementations, which I hope Rust will have as well when epochs start becoming relevant.
One thing I'm not a fan of in @dhardy's version is the idea of patch releases to old epochs. That has the potential to create a much bigger configuration space for the compiler (as well as other implementations), and backporting starts to feel like GNU variations on C++ versions (gnu++98 for example).

@iopq The RFC as written kind of assumes there will be no standard library breaking changes---it only talks about the compiler supporting old rust, not the standard library being able to shim itself into an older interface. There's 0 reason to mix and match compilers because of the standard library.
But, for sake of argument, let's suppose the standard library did have breaking changes. A much more lightweight solution than ABI compatability is just to allow mixing and matching standard libraries. #1133 provides some ground work for that by giving Cargo the information it needs to build standard library crates on demand. The next component would be "shim libraries" i.e. allowing one to publish crates that depend on newer versions of themselves. That in turn builds on https://github.com/rust-lang/rfcs/pull/1977 as the self-depedency must be private and we need to support it.
Just to be clear, ABI compat essentially means we can no longer make code faster in innovative unforeseeable ways, which would be a real bummer.

@aturon

As the RFC argues, new features (whether breaking or not) lead to shifts in idioms, and it's important to have a way to talk about those shifts at a coarse grain, since in many cases features are intended to work in concert for building APIs.

100% agree.

However, the compiler ships with a "compatibility" mode for all prior major versions, expressed through Cargo.toml or a compiler flag.
We sing this from the hilltops so that people understand that old code continues to work, and that you can mix and match versions across your dependency graph.

These points contradict the above, abandoned code doesn't use new idioms, it doesn't change. If it compiled with one version of the compiler it will continue to do so, there is no point in expending a massive amount of effort to fix something that isn't broken. Anyone who wants to use an old abandoned library might as well use an old compiler since they'll be prevented from writing modern code or using new optimizations by using said library anyway. No matter whether you call it epochs or semver major versions.
Why not simply use the crater project to watch crates.io and when a crate stops compiling it gets marked as obsolete and and cargo prints an error indicating that it is abandoned. Not only does this address the compatibility issue, but it should cut down on the crates that use ancient versions of their dependencies.

@rpjohnst @rpjohnst the language version is an interface not a specific implementation, so yes patch versions make no sense.
But other than that, I disagree complete. Minor versions are essential to inform users that their tools are too old to build the crate in question. If it's 2016 and my rustc supports the 2015 epoch that does not mean 2017 rust (before the example 2018 epoch) is guaranted to build. It just means there is an upgrade path that is guaranteed to not breaking any (stable) code ever.

@Ericson2314 we can backport ABI changes to older releases and release 1.18.1 when the ABI changes
Yes, you will need to get two versions of rustc, but that might be cleaner than putting everything into the current version of rustc

@iopq That's way way way harder. Like rewrite-rustc-to-use-different-llvm harder. Yuck.

@CamJN if a old library is unidiomatic, it may be annoying to use from a newer ideom, but certainly not impossible.
IMO if one explicitly specifies a language version, they probably shouldn't get deprecation warnings by default because they made an explicit choice. But if the the default language version is 1.0/2015 they definitely should get flooded with as many deprecation warnings as possible, lest there are a beginner unwittingly not opting into the latest and greatest version.

@Ericson2314 That's disingenuous, there are quite a few new features that could be added to the language that would change the idiomatic way to do things and would be completely incompatible with old code.

@CamJN Are we talking about editing old code, or writing it? Sure if your code base is 100% really old, then yes you are a major refactor away from employing modern idioms. But if you are writing new code calling an old library, there is no such barrier.
Also, to be clear, it would be very very hard to make a language-level change with that much idiomatic sway without dramatically changing the standard library also in ways that aren't proposed. In C++ this happens because everything is unsafe. That gives one a lot of head room to evolve, where old ideoms don't mix with new ones because they, as the weakest link, destroy any safety "guarantees". In Rust, we have real safety guarantees and little headroom for some "weakest link" phenomina. We would need a new axis of correctness for old code to be a fatal flaw (non-blocking for async, timing guarantees for real time, panicking etc). No one has proposed any language features like that, and if they did there would probably be some effect system to catch non-compliant code anways. /tangent

@Ericson2314 I wasn't talking about safety, just basic syntax. Just as a dumb example if Rust added function overloading or even just default values, then it would become impossible to use an old library in the new idiomatic way. And the whole point of this thread is that such features could exist otherwise there would be no need for this RFC.

@CamJN Those are both backwards compatible changes---strictly more overlapping names are valid than before. The RFC has no baring on them. I assume you meant something like my catch example where now less names are valid (but I edited in my catch example so you didn't see it).

Minor versions are essential to inform users that their tools are too old to build the crate in question.

@Ericson2314 Ah, that's true, since Rust plans to do non-breaking additions in between epochs, unlike C++ or Javascript. A toolchain couldn't truly be considered to support the full Rust 2015 language then until Rust 2018, when 2015 stops changing.

@Ericson2314 No I meant changes that would make old code unidiomatic, and impossible to use idiomatically. The fact that it would eventually stop compiling due to other changes is simply a nice way of forcing that code out of the ecosystem.

@aturon

Allowing fine-grained opt-ins means that, in a sense, we have an exponential number of language dialects, which causes headaches for testing the compiler and communicating about code. If the total number stayed quite small, though, this might be manageable.

These do not come across as issues for Javascript, so why would they be issues for Rust?
Meanwhile, from a code maintainability perspective (10-30 years out), you'll have something like 5-15 epocs. No matter what, there will be issues testing all these "compiler-internal language_extentions" together and separately (likely needing some sort of dependency resolution across sets of these language_extentions). I'd imagine that the developers of a future rustc will wish that such a framework was created early.
Additionally, given that we are just talking about AST travers/transforms, this could be the foundations of a compiler plugin framework which can allow anyone to travers/transform the AST, and share them via Cargo. There seems to be a growing convergence for language ecosystems with plugin based transpilers: gofix, Babel, Java's AOP, Java's Lombok, etc. Doing it early (i.e. starting but taking as long as it takes to do well) and doing it well, could be extremely powerful for the entire ecosystem.

Fine-grained flags lose the "rallying point" aka "marketing release" aspect of the proposal, which I'll talk more about below.

Wouldn't having a "feature flag" called 2018 which was a group of the popular fine-grained feature flags, alleviate your concerns?

These flags will accumulate over time, and it'd be best for code not to have to start with turning them on (as is commonly the case with, for example, Haskell code). So we'd probably want some epoch-like mechanism for saying "please turn on all the features shipped by 2018" or some such.

cargo init in 2018, should start projects with the feature flag group 2018

Perhaps I've been too literal for people in previous posts, so how's this sound:
I see Rusts value of stability without stagnation as a good guiding light here.
Stability, so it should be easy to keep on the most recent updates to the language; without stagnation: anything that doesn't keep up to date should be removed from the ecosystem so people don't have to deal with those stagnant projects.

@CamJN: unfortunately I don't think that is a realistic approach to take. There are many libraries that may not see frequent updates, but which are nonetheless extremely useful, and in fact work very well. If anything, libraries that have been around for a long time, and have seen significant use, are often the ones that see updates least frequently, precisely because they have been battle-tested so much. Saying that libraries are just left in the dust if they aren't constantly updated seems counter-productive to a healthy library ecosystem, as you end up prioritizing crates with high churn over long-term stable ones.

@jonhoo That kind of thinking is how we wind up with a mess like openssl as the most used crypto library in open source. Just because something is old and rarely updated doesn't imply some kind of quality. And certainly the people using a library can't be relied on to ensure the libraries correctness or usability.

@CamJN: That doesn't mean that age or stagnation is an indicator of poor quality either though. Library monopoly isn't great, that is true, but I don't think throwing out old libraries is going to help with that. If anything, you would likely just end up with many short-lived libraries, which I would argue is strictly worse than having few long-lived ones. Take something like https://github.com/jonhoo/hurdles, a crate that provides a better multi-core implementation of sync::Barrier. That crate is likely to not see many updates going forward, mostly because the implementation and API are relatively simple, and unlikely to need changing. Why should that cause the library to become unusable at some point in the future?

@CamJN
I should probably give a myself as an opposing case study:
I'm a big Firefox and Thunderbird user and I've been using (insert name of flagship Gecko browser) and (insert name of main Gecko-based mail client) since Mozilla Suite was pre-1.0. Despite loving said tools and despite loving to customize my software, I never got past initial experiments with regards to writing my own extensions back in the Mozilla Suite era.
Why? Because the prospect of babysitting maxVersion was a huge turn-off. Instead, I chose to write utilities in Python (2.x, because 3.x wasn't out yet) because I could trust that I could build up a pile of useful code, knowing that I could get it working, and then trust that it generally wouldn't require maintenance beyond slowly polishing it toward perfection as I discovered bugsduring use.
Not everyone has the time and dedication to apply "change, simply because no change means abandonment" development policies to a small number of projects and my whole reason for being interested in Rust is that it magnifies the feasibility of a "many decent projects, each requiring little maintenance once they reach a usable state" approach to development.
The psychological effect of trying to force me to chase deprecation warnings on someone else's schedule (especially for more secondary projects which are truly mothballed, rather than actively used) could very well be enough to drive me off Rust in favour of another language friendlier to people who aren't being paid for their time spent chasing a moving language.

@aturon

A means of introducing small breaking changes independently of compiler version, i.e. via an opt-in.

I want this too. The last time this discussion got big was IIRC in the dyn Trait RFC (coupled with impl Trait). There were tons of arguments saying "we can get away with breaking the language using tooling" but the consensus there was "we are never going to break the language".
AFAIK the current consensus hasn't changed, we are never going to introduce a breaking change in Rust, the language, so this whole RFC is about solving a problem that we currently don't have.
IMO the discussion about whether we want to break the language at all (under which conditions, etc.) must come before we start discussing a specific solution for releases with breaking changes. It might well be that once we have that discussion, we agree on conditions for breaking changes that make epochs impossible.

@gnzlbg C++ has (sort-of) managed to avoid breaking changes over its lifetime. I'm not convinced this is best for everyone, since e.g. a new keyword, if handled properly (with warnings on uses as an identifier), is very easy to introduce and can make the language quite a bit more ergonomic.
On the other hand, being able to select a "standard" (or "epoch" or whatever), like g++ -std=c++03, is very useful.
I also agree with @yazaddaruvala that individual feature flags could be useful. Being able to opt-in to new features like this would probably remove the need for "preview epochs".

@CamJN go read the motivations in my partial rewrite. There are plenty of reasons to continue using an old language version; many projects don't have major security implications, and those that do deserve a lot more attention than just updating to new language features.

@dhardy

C++ has (sort-of) managed to avoid breaking changes over its lifetime.

The amount of people affected by the breakage varies between C++ standards (e.g. it is larger in C++14 to C++17 than in C++98 to C++03), but each C++ standard has broken backward compatibility with previous standard (e.g. C++11 removed export template, constexpr became a non-contextual keyword so if you were using it as a variable name in e.g. a compiler AST your code would break, C++17 removes functionality from the standard library, etc.). Or in other words, valid C++ program for a given standard do not necessarily compile for the next revisions of the standard.

Okay, I suspected as much (about C++). If that's the case that most users do not want any breakage, fine. But then it's @aturon at fault, not me; whatever the RFC says about guaranteeing no breakage of existing code is wrong.

@aturon

The bottom line to me is keeping our promise: you should always be able to upgrade to a new version of the compiler without hassle.

This is super-important. I hope Rust never does a Python 3.

Keeping rustc on the 1.X series in perpetuity provides clear messaging that upgrading the compiler, alone, does not entail breakage.

FWIW, I think the "1." prefix is a mistake, since it implies the possibility of Rust doing its Python 3 aka. Rust 2.0, and I think "Rust 2.0" showing up as a concept every now and then (including in this thread!) is harmful, since it creates doubt that Rust might do a Python 3 after all.
It's now too late to drop the "1." prefix, but marketing-wise, it would be good to do what Java did: commit to never doing Rust 2.0 and hide the "1." prefix from marketing and colloquial references to rustc versions.

Using years for epochs helps tie them to roadmaps, makes it easy to tie them back to dates (and helps indicate just how recent the version is).

The implication that there is value in having an idea of how recent a feature implies that there is value is supporting old compilers. Shipping a compiler every 6 weeks is one of the great things about Rust, and I would hope the Rust community to maintain a community norm of expecting people to keep their Rust compiler fresh. That is, I think the community should set the expectation that old rustc shipped on an LTS Linux distro is for the purpose of compiling the Rust-based packages in the distro and not for doing new Rust development.

It has precedent in C++, of course, but you can also imagine adding ".0.0" if you want to see it as semver.

C and C++ standards have a very different development model compared to Rust, so it's not clear that years in C or C++ standard names is a feature as opposed to a bug when applied to Rust.

Why not fine-grained feature flags?
Another option would be to, in essence, use feature flags on the stable compiler for features that involve breakage, giving you a fine-grained way to opt-in (which could potentially be applied per-file).
There's a lot of appeal there, partly because it further decreases the pain of upgrading!
But there are some downsides as well:
These flags will accumulate over time, and it'd be best for code not to have to start with turning them on (as is commonly the case with, for example, Haskell code). So we'd probably want some epoch-like mechanism for saying "please turn on all the features shipped by 2018" or some such.

Hopefully, new features that are incompatible enough with pre-existing code to require an opt-in for a reason more substantial than claiming a new reserved word would be both rare enough that there wouldn't be a huge list of them for a given crate as well as useful enough that new code that should be using a new feature wouldn't fail to use it out of complacency of not bothering to opt in.
That is, if there's worry that new code wouldn't opt in, it pretty much means that the new feature isn't compelling enough, so maybe doing it as an incompatible change wasn't worthwhile in the first place.
OTOH, even epoch would need some opt-in boilerplate. If the expectations is for editor templates and cargo new to generate the opt-in boilerplate, they might as well generate feature-oriented opt-in boilerplate. But, again, incompatible features should be compelling enough that no one fails to opt-in out of complacency.

Allowing fine-grained opt-ins means that, in a sense, we have an exponential number of language dialects, which causes headaches for testing the compiler and communicating about code. If the total number stayed quite small, though, this might be manageable.

If the opt-ins end up being mostly of the kind of reserving an additional identifier for a new feature (i.e. the incompatibility is mainly a change to the set of reserved words), the issue of combinations isn't a real problem.
Feature combinations could become a real problem depending on the nature of future features. It might even make sense for a feature-oriented opt-in flag not to be particularly fine-grained if it makes sense to toggle two inter-dependent features as a pair.
I would much prefer Rust starting with feature-oriented opt-ins for features that would be incompatible with pre-existing code and see how it works and do coarser-grained opt-ins if real problems turn up instead of doing an X-UA-Compatible in anticipation of problematic interactions that might not actually arise. That is, I'd much prefer a "crossing the bridge when we get there" approach to problematic opt-in combination interactions.

Fine-grained flags lose the "rallying point" aka "marketing release" aspect of the proposal, which I'll talk more about below.

I think it's a mistake to couple the language compatibility mechanics to marketing--mainly because I believe that going for finer-grained opt-ins leads designers to make more of an effort try to go for compatibility without an opt-in flag compared to considering a coarse-grained opt-in as a permission to break things, so I don't want marketing concerns to lead to designers feeling they have more of a permission to break things.
It's quite possible to do marketing pushes around cool stuff in a particular Rust release or on a particular year's roadmap without there being a one-to-one mapping from marketing bundling to opt-in flag bundling.

Update: per discussion with @Manishearth, I've changed the RFC to default to the 2015 epoch when no flag is given to rustc, since that's more consistent with our general stability story. @nikomatsakis seemed to prefer the latest epoch, but I'll let him argue that point.

Is there a link to this discussion? I didn't find any comments (maybe GH is hiding them), so my apologies if I am retreating ground. That said, indeed, I would prefer for rustc to default to the newest version. In part, my argument is pragmatic: I think that for the majority of users, rustc is used to test smaller programs. In that context, having it default to the older epoch will be quite annoying. I also think there are different ways to view "consistency". I view running rustc manually as doing a "fresh, independent" compilation, and hence I feel like it should behave roughly the same as cargo new; cargo build.
For the most part, I think this is consistent with how other major compilers behave, though they are not entirely consistent on this point. I did a little digging:

GCC, for example, defaults to -std=gnu++14, if no options are given.

I did find a counterexample, in that icc seems to require a flag to enable C99 support.