This is a huge RFC, and so I wanted to make sure you’re all aware:
Why introduce this whole new concept of epochs? Seems to me you could achieve much of the same by using new major versions, and having
rustc commit to supporting code written for previous major versions of the language as well.
Basically, you could almost take the current RFC and replace every occurrence of epoch with major version. For example, the “Basic Idea” section from the RFC becomes:
A major version represents a multi-year accumulation of features, improvements, and idiom shifts for Rust. It covers the language, core libraries, core tooling, and core documentation.
Each crate declares a major version in its
Cargo.toml(or, if not, is assumed to have major version 1):
major_version = "2". Thus, new major versions are opt in, and the dependencies of a crate may use older or newer major versions than the crate itself.
To be crystal clear: Rust compilers are expected to support multiple major versions, and a crate dependency graph may involve several different major versions simultaneously. Thus, major versions do not split the ecosystem nor do they break existing code.
So yes, major versions would involve backwards incompatible changes but the tooling would be designed to deal with this, and as a result nothing would break.
This is not what most people think of as major versions, though. Basically, the danger here is that people will think we’ve dropped our stability promises, even if we haven’t. Major versions communicate that we have done so.
As far as I’ve understood the proposal, it is what most people think of as major versions: A new release for the language, that has syntactic or semantic changes that require assigning a new identifier. In fact rust does not make a stability promise, but a compatibility promise. That means that there will be (and already is) code using deprecated features, but never code using incompatible features.
Therefore, I believe that the planned epochs should be named just like new major versions in the semver way. The crucial difference to semver will be the compatibility, but that can not be communicated by replacing “major version” with “epoch”
A significant difference is that the core of Rust cannot change; this is basically limited to the parser only. Most people expect a 2.0 to be significantly more breakage than “hey here’s a new keyword and so you may need to rename a few identifiers.”
I have to agree with @konstin. Coming from an environment using the “corporate” languages, I would expect a major language version to contain new features and few, if any, breaking changes at the language level. Java, for example, has gone 8 major versions with the only breaking changes being the introduction of three keywords. C#, meanwhile, is on version 7 with the only breaking changes being two small bug fixes in C# 5.
In addition to that I expect bigger breakages to happen in rust. Without them rust would have to stick forever with decisions made with today’s knowledge, resulting in problems such as the unsoundness of java’s type system. One example is the often criticized module system, which might be overhauled one day requiring bigger refactorings for libraries adapting to a new format.
Another rather small but more imminent example are macros 2.0, which are imho the best real world application for the principle of compatibility. Once macros 2.0 lands in stable rust, the ecosystem will be split in two kinds of crates: Those still using the old 1.0
macro_rules styles and those using the new
macro keyword of macros 2.0. While they are technically two totally different versions, they do not break the compatibility with older or newer libraries.
This is not the plan of the core team, at least.
We have plans to do this without breaking the older module system.
This also will not be backwards-incompatible.
Perhaps we could borrow the versioning scheme used by the C and C++ standards: “Rust language specification, YEAR”. I think that would set the right expectations in terms of both the kind of changes expected and the continued availability of support for older specs.
I see that the RFC already uses years for the numbering scheme, so this is pretty much just a matter of terminology.
Before discussing the details of Epochs management in this thread I think it’s essential to have an as much complete as possible list of things we wish to fix/change now in Rust that can’t be changed in the normal way. Then if the amount of the changes that can be done in the Epoch is too much small compared to the whole list, then the idea of Epoch could be redesigned.
I also think epochs don’t need to have a regular cadence, they should be more need-driven and irregularly spaced.
I just looked at this thread (raw identifiers). If the solution that’s presented there is actually necessary, we probably want to prevent the spread of future-keyword-containing-identifiers before disallowing them in the language itself. Maybe crates.io could reject new crates if they trigger warnings about incompatibility with future epochs?
I’m not sure… it’s what I would expect from most tools That is, version X of the tool should be able to work with files in format X, but also in all previous versions. For a tool like a compiler, I would say it’s particularly important to keep compatibility so you can compile your 5 or 15 year old source code with a current compiler.
As I read it, this is basically also what the epoch proposal says: you can keep your code on epoch 2015 and still expect it to compile with newer compilers. So whatever new features epochs 2018, 2020, etc will bring, the compilers will keep the support for epoch 2015.
Isn’t using years as an epoch will be forcing us to push as many changes as possible at one release? and changes that did not make it into this one will be forced to be delayed until the next year?
I suggest using target rust version as epoch, as it will be more flexible, and it may be confusing to have multiple incompatible versioning, and because of that rust versions will not be communicating incompatibility, but the catch is that it may be communicating seamless upgradability, too, which may not be the case.
My understanding of the epoch system is they do provide seamless upgradability, because you need to opt into the next epoch.
This merits having two versions.
For example, Rust 1.123.0 might support both compiling epoch 2015 and epoch 2018 code. All code written since 1.0 will be 2015, unless explicitly opting in to 2018. It might generate some warnings for things that might be incompatible in the future.
What I mean by seamless upgradability is that when I write code against Rust 1.0, I expect it to compile using Rust 1.23 without changing it, but I won’t expect a code written against 2015 epoch to compile against 2018 without rewriting.
The point I am concerned about is that using year-based epoch may not end as flexible as other versioning techniques can be, and this was just a suggestion. Another one that comes to my mind is using
year-month epoch instead.
A target version is inflexible, as each target version has one specific day on which it’s released. A year, on the other hand, has many releases. A year is much more flexible than a specific release.
C++ uses years, but it’s labeled after it’s released, so I don’t think they are technically limited to “this needs to all get in by year X”, even though it does seem like ever since C++11 they are on a 3 year cadence.
An example of this is that C++11 was originally expected to come out before 2010, hence the C++0x name, but slipped to 2011.
I guess my point is that as long as you don’t need to release two epochs in the same year, having a year-named epochs that are tagged when the epoch is released seem just as flexible.
Epochs should only be happening at intervals considerably longer than a year. C++ is doing something very similar at three-year intervals, and that’s still too fast, they’re talking about C++20 already and I don’t even think I have a fully C++11-compliant compiler+library on this computer! I’m actually inclined to doubt a fully C++11-compliant compiler+library exists! The right cadence for epochs is something like once a decade.
Yes, that means some changes have to wait a very long time to get done. That’s a Good Thing for everyone trying to use the language, it means it’s a stable foundation.
But most changes don’t need an epoch. Even huge stuff like NLL doesn’t need one, as I currently understand it anyway.
Sometimes, the need for epochs is caused by things that are improbable to mess with more than a tiny fraction of the ecosystem’s code, an example would be making
if let true = p && q bind as
if (let true = p) && q - for that example, there are zero usages in crates.io, but it is a breaking change. These things can be easily fixed by a
rustfix tool that lets you transition between epochs without any manual labour. That could entail that the three year interval is too long rather than too short.
Although it’s impossible to prove any of this, during the Great Epoch Debate of 2017 I quickly formed the opinion that:
- the list of “epoch-worthy changes” we currently know about is very small (i.e., single digits), so probably all of them will be in the next epoch
- it’s extremely unlikely that we’re going to discover any additional epoch-worthy changes with the current feature set of Rust
- we won’t know what, if any, additional epoch-worthy changes there are until we stabilize new features
So my opinion on the proper epoch-as-breaking-change-batch cadence is simply: There isn’t one. It’s entirely possible we don’t discover any epoch-worthy changes for a decade or more. Maybe we already fixed them all. Or maybe something terrible will happen the day stable specialization ships and we’ll “need” an epoch next year.
Of course, epoch-as-marketing-nexus cadence is an entirely different subject, so we may end up doing a regular cadence without having any breaking changes to ship in most epochs. In fact, that’d probably be the ideal outcome.