Language removals in editions


#1

Hello

Maybe a bit odd question, but… I’m observing how there are proposals to add new keywords, operators, etc, into the language. And I also code in C++, which very much suffers from language bloat (I think the bloat of the language is it’s biggest problem, too many features interacting with each other).

So, is there a defense against that? Is there a mechanism, possibly through the editions, to remove things from the language? If so, are there any planned removals or does anyone know of something that they would like to see gone from the language?


#2

Yes, this is exactly the purpose of editions.

Rust has strong backwards-compatibility guarantees. This means that in regular new releases of the language, no breaking changes are allowed. Any improvements to the language that come with a new version of Rust have to not break any existing code.

However, editions are the major milestones for the language. They can introduce breaking changes. However, they are opt-in. All future compiler versions can compile code in both the new and the older editions. Old code continues to compile in the old edition compatibility mode of the compiler. Only code that explicitly opts in to using a newer edition will get the breaking changes.

However, to be specific, editions are not allowed to break just anything. They are allowed to turn compiler warnings into errors, and that’s it.

This means that as new features come to Rust and some old syntax is considered obsolete, a warning can be added to the current edition of Rust. This means that people who write code using the obsolete syntax will see a warning encouraging them to learn how to do things the new way, but their code will still compile.

In the new edition, that deprecation warning can be turned into an error.

This allows people to have a gradual transition from the old edition into the new. They can keep using the old edition with the latest versions of the compiler (as newer editions are opt-in and old editions are still supported by new compiler versions) and they will only see warnings. They can work on fixing those warnings, or they can hide them to not get in the way and stay on the old edition. When they fix all the warnings, they can just change the compiler flag and start using the new edition to get the latest and greatest in Rust. When they switch over to the new edition, any such warnings turn into errors.

However, when creating a new project, Cargo will default to the newest edition. New projects should be in the new edition from the start.

Also, projects in the new edition are fully compatible with libraries written in the old edition, so using the new rust doesn’t stop you from using any of the existing old libraries.

Overall, the editions system is a fairly clean and elegant way for Rust to rid itself of legacy baggage every once in a while and avoid ending up being like C++.

An example of such a change is the new dyn Trait syntax for trait objects. In Rust 2018, the syntax for trait-generic code will be impl Trait vs. dyn Trait, for static (monomorphisation) vs. dynamic (vtables / trait objects) dispatch, respectively. This is to make the syntax clearer. The old bare Trait syntax for trait objects will get a deprecation warning when compiling in Rust2015 mode and will be a syntax error when compiling in Rust2018 mode.

There are also other changes to the language that deprecate and obsolete old syntax. Similarly, you can keep compiling old code with the latest Rust compilers and get warnings, but if you pass the Rust2018 edition flag to the compiler, you will get errors and it will not compile.


#3

I think there should always be a discussion for each RFC (that extends the language) on how to add the feature as conservatively as possible by reusing whatever constructs are already in the language.

There’s a balance to strike, of course. A while back, for example, I suggested that we don’t really need try-blocks as a language construct because the same mechanism can be implemented by reusing block labels – a much smaller and simpler change to the language. Now, the ergonomics would probably suffer comparatively with that design, but there was a constructive discussion.

I’d like to see more focused discussions on the aspect of how to grow the language minimally. Perhaps that should be a mandatory point in the RFC process.

  • reuse existing constructs as much as possible
  • avoid new syntax if possible
  • describe the proposed change in possible levels of adoption
  • describe the proposed change’s overlap, orthogonality and interactions with other constructs
  • prefer const fn or macro to keyword
  • prefer crates.io to std (this I think we do well, btw)
  • prefer impl Trait to standardising on concrete types

#4

We badly need this. The best way to avoid feature creep is to not allow Everyone’s Every Favorite Niche Feature Ever™ into the language in the first place. Once a feature is stabilized, there’s basically no way back. Users of the feature are going to be very upset if it ever comes to its removal, so it’s realistically not going to happen, or only very rarely. Therefore, we should be much more conservative (in terms of setting the bar higher for individual features to be added to the language) upfront instead.

Your comment very accurately highlights how exactly we can achieve this without much need to compromise, too. Before making a construct into a core language construct, it should go through the “fn / macro -> 3rd-party crate -> standard library -> repurpose existing syntax sensibly” pipeline, in this order.


#5

Keep in mind that editions have strict limitations on what kind of changes they can do, because crates using different editions can freely interact. Basically, only surface syntax can change, all the types, traits, function signatures, etc. must keep making sense in the new edition. This effectively means that editions can’t remove anything from the type/trait system and other parts of the language (unless it’s a trivial desugaring to a new feature that is 100% backwards compatible). To give two (artificial) example, editions can’t remove the concept of inherent methods or associated constants, even if we would eventually decide it would be better to remove those in favor of extension traits and associated const fns respectively.


#6

Could a new edition not move (for example) Vec to std::legacy::Vec and introduce a new Vec? It could even make the old one unavailable in new editions alltogether.

Same goes for things like const. The compiler would still need to support it for old editions, but it could be made unavailable, or changed to different semantics, in newer editions.


#7

Again, the key point is that crates on different editions need to be able to interoperate with each other seamlessly, as if editions weren’t a thing.

If it’s a different type, code in a crate of edition A can’t pass/receive/operate on/etc. a vector created in a crate of edition B. Even if both the old and the new Vec are available, if Vec means one type in edition A and another type in edition B, then that violates the property that warning-free code continues to work and behaves the same in the next edition. For example, if we changed Vec in the 2018 edition then the following code would would compile fine in 2015 (unless you start warning about every use of Vec, which is clearly ridiculous) but break with a type mismatch error when bumped to 2018:

extern crate crate_on_2015_edition;

fn main() {
    crate_on_2015_edition::frobnicate_vec(Vec::new());
}

The only thing that works is if the old and the new type are available under distinct names that mean the same in all editions, but you don’t need editions for that and it has all the same downsides that it always had.

(Well, one intermediate step that also could work theoretically is deprecating the existing name and introducing two distinct new names, one for the old type and one for the new type respectively. That is an incredible amount of churn though, and doesn’t really have any advantage over keeping the old type available under its old name.)

No, because then a crate on a new edition can’t implement a trait that has an associated constant, which might be needed for interacting with crates that are on the old edition. Likewise, a crate whose public interface includes a trait with an associated constant can’t go to the new edition without changing the trait definition, and thereby breaking downstream crates that are on the old edition.


#8

Even if both the old and the new Vec are available, if Vec means one type in edition A and another type in edition B, then that violates the property that warning-free code continues to work and behaves the same in the next edition.

But what timeframe does “warning free” cover? Is it the beginning of an edition? In my understanding that would imply that new keywords (for example) always require a full edition of deprecation before they can be introduced.

So, can something change when it hasn’t warned in 2015, for example?

Edit: Or would this only apply to library code, not syntax?

No, because then a crate on a new edition can’t implement a trait that has an associated constant, which might be needed for interacting with crates that are on the old edition. Likewise, a crate whose public interface includes a trait with an associated constant can’t go to the new edition without changing the trait definition, and thereby breaking downstream crates that are on the old edition.

That is true, if the guarantee is that strong indeed.


#9

You can find the details in the RFC, but they don’t matter for my point: users need some assurance that their code won’t silently change behavior from bumping the edition. Changing what Vec refers to is antithetical to that.


#10

They are also allowed to introduce new keywords. And the module system changes are indicative of that editions have been allowed to do even more.

Honestly, I think these policies are stated too vaguely to be very meaningful. We always need to, and do consider these points, but I don’t know what practical change would be made compared to now… If you are saying that we should be more conservative than we currently are, I don’t agree.

This is already the libs team’s policy AFAIK. The more important some data structure or algorithm is to the community at large (since it is used very often) and the more obvious a design is (such that there aren’t 10 different alternate competing designs) the more stronger the case for libstd inclusion imo.

Seems to me we’ve been moving away from macros (see try! and await! (the latter is temporary syntax…)).

This is problematic. -> impl Foo only allows you assume that Foo is implemented for the returned type, but not other traits such as Clone. Often, you have the situation that Bar<X> is Clone iff X is Clone. To fix this, I think ConstraintKinds is necessary but that might not work as well as we want wrt. inference.


#11

I wonder, which features would be candidates for removal?

  • non-dyn Trait getting deprecated or repurposed.
  • macros 1.0 will be redundant once macros 2.0 land.
  • there was talk of making extern crate unnecessary by a new module system or Cargo

Is there more?


#12
  • Anything in stdlib that’s deprecated becoming uncallable
  • future-compat lints moving to hard errors no longer accepting some bad code

But also I’d like to recommend again against repurposing valid syntax from one edition (especially the default one) to mean something different in a later edition.

As an example, Rust 2015 has &Trait that means &dyn Trait and warns to use &dyn Trait instead. Rust 2018 has just &dyn Trait, and &Trait is an error. In Rust 2024, it’s decided to have &Trait mean &impl Trait. Now someone upgrading from unspecified edition (2015) to 2024 has their code (though with warnings) stop warning and mean something else.

More importantly, if some old documentation sticks around, it will mention &Trait as meaning &dyn Trait which won’t be accurate anymore. In Rust 2018 &Trait will be an error suggesting that you meant &dyn Trait, but reusing that syntax is dangerous for the very reason that the “must be linted against in the previous edition (ideally in a rustfix-able manner)” rule exists.

I hope that as much as possible, the must be linted against rule extends to all previous editions, and that valid code isn’t allowed to mean two different things in two different editions – it should be the same, or the earlier ones lint and the later ones are errors, or the earlier ones are errors and it gains meaning later.


#13

No. The name cannot be reused even if the previous one is removed.