- Feature Name: (fill me in with a unique ident, my_awesome_feature)
- Start Date: (fill me in with today’s date, YYYY-MM-DD)
- RFC PR: (leave this empty)
- Rust Issue: (leave this empty)
Summary
This RFC proposes a concept of stable #[feature]s, which allow implementing backwards
incompatible changes.
Motivation
Rust wants to be able to introduce breaking changes without necessarily releasing the 2.0 version
of the language (yet). We already use a system of deprecation warnings which eventually become hard
errors to remove features we want.
This approach, however, is only appropriate for breaking changes where the “old way” becomes
invalid and an alternative approach is introduced. It is not appropriate if it is desired to change
semantics of some code altogether.
Notable examples of such code could be:
- Introducing new keywords (e.g.
union, catch);
- Changing semantics of some syntax (e.g.
impl Trait versus Trait);
Detailed design
Before proposing something concrete lets investigate what the other languages do to enable breaking
language changes.
Case Study
Python
Python has a mechanism to “import” new features (from __future__ import ...). These were used
extensively to implement libraries in a way that’s compatible with both Python 2 and Python 3 at
the same time. In Python 3 most of these imports became a no-op.
It wasn’t widely used before the release of 3-series of Python.
Haskell
{#- LANGUAGE -#} pragma is very similar to Rust’s #[feature]. Due to this pragma, Haskell
code can take advantage of advanced features such as GADTs, type families and template haskell
without necessarily having these features specified in the base Haskell language (The Haskell
Language Reports). Note, that LANGUAGE pragma is used more as a way to enable extra
super-user-like features, rather than as a way to introduce breaking changes to the language.
Across major revisions of language specification (from Haskell 98 to Haskell 2010) only a
very small number of the LANGUAGE features got enabled by default in the standard.
Stable opt-in semantic-changing features
The primary component of this RFC is introducing stable #![feature(name)] annotations. These
annotations would have behaviour very similar to our current unstable feature attributes; the only
real difference is being able to use these in stable compilers. The lifetime of such stable
attribute is expected to be as follows:
- A semantic-changing feature gets implemented, is available behind unstable
#![feature(name)]
(same as currently);
- The feature gets “stabilised” and follows all the usual stability guarantees. The alternative
behaviour must be enabled with
#![feature(name)] before use;
- At eventual release of the next major language version (see Releasing Rust 2.0) these
features are considered for being enabled by default.
This approach has both the benefit of allowing early adopters to use nice, but breaking features
and allows people to write forward-compatible code as well (i.e. code which would work on both
Rust 1.x and Rust 2.0).
Incompatible library changes
There’s a question of the language changes which impact the libraries. Most notable of these
changes is introduction of the new keywords.
Making union/catch a keyword (an immediate use-case described above) would involve changes to
the libraries as well. The libraries could use these words in their public APIs: methods, free
standing functions and so on. In fact the standard library itself exports a few methods with the
name union.
Presumably, as a part of an RFC introducing a new keyword renamings for these methods would be
proposed. However, in that case the feature cannot be implemented before the public API is
fully renamed (i.e. the public API with the old name is entirely gone).
This demonstrates that the libraries need an ability to react to the presence of the feature
somehow. It is a problem with many tricky corner cases, however. For example:
Crate ROOT depends on A and B, B depends on A as well. A exports a function with name catch.
Both ROOT and B use this function. A implements some scheme which renames catch to _catch,
when catch_keyword feature is enabled. ROOT enables the feature and replaces all calls with
_catch. B, however, hasn’t enabled this feature yet and still uses the old catch function. This
should work – which implies two different A crates in the dependency graph(?).
To solve this something along the lines of #[cfg(feature(something))] that gets evaluated on
import of a library would have to be implemented.
[Author note: no concrete implementation ideas currently]
Guidelines
The stable features shall not be abused to introduce unnecessary breaking changes. In short, the
stable features introduced by this RFC should only be used as a last effort – when a strongly
desired design or syntax cannot be introduced or changed in a backwards compatible way. A good
indicator of stable features being misused would be a lack of (heated) discussion exploring
alternative syntaxes for some feature.
(Sidenote: technically if a feature could be implemented with a contextual keyword, then the
current wording suggests the contextual keyword approach ought to be preferred, but to me personally
an option to get rid of these contextual keywords is a strong motivation for this RFC, as I find
them to be the nastiest hack ever invented)
Some immediate use cases for this could be:
-
Trait now has the behaviour of current impl Trait proposal, old unsized behaviour is
accessible via dyn Trait (new keyword);
Current behaviour of Trait is seldom used and hides the costs of dynamic dispatch, while the
0-cost sugar cannot use the most sensible syntax for it. The feature would be inaccessible
without enabling trait_shorthand feature first.
-
catch keyword. Introducing catch blocks ran into syntax ambiguities with struct initializers.
Making catch a keyword is the only fix (not even contextual keywords would help), but it cannot
be done backward-compatibly.
Releasing Rust 2.0
With the scheme as above in place, it becomes significantly easier to implement and release a
breaking version of the Rust language. The #[feature] opt-ins allow users to test the feature for
major warts, helps teams to probe the desirability for a particular feature, and in would signal
the general direction the 2.0 version should go towards.
Ideally, releasing 2.0 would then become switching a number of these features to enabled by default
and changing the version number (maybe also adding a flag similar to --language=rust1, which
would switch these features back off).
Forward compatibility
When considering a breaking release, the forward compatibility is not to be underestimated. Python
3 example shows clearly that people will be using both the old and the new version of the
language thus fragmenting the ecosystem and some libraries will not be ported to the new version of
the language. It is important to make it easy to port these libraries.
While python has a tool to do the automatic conversion, it turned out to not be the
ideal solution, primarily because it couldn’t handle converting all the code bases correctly. It
also generates code that is only valid on python 3 and not both versions, which prompted people to
do the “porting” manually anyway.
If the ideal scheme outlined in Releasing Rust 2.0 is followed, then porting the codebase
would simply involve adding all the features which got enabled by default to the top of the library
file and fixing up the code (could be done automatically to at least some degree). Not only
libraries ported this way would be forward compatible, but the library authors would have an easy
way to track the porting progress as well.
How We Teach This
…snip…
Drawbacks
It is possible that eventually wanting to use a full subset of Rust would involve adding a large
number of #[feature(...)] to the lib.rs. (counter argument: it is not a big problem in Haskell)
People learning would have to learn the base language and then in addition each of the “features” they encounter along the way. This is both good and bad. In haskellandia stuff like GADTs are not trivial to understand, and so on one hand people avoid learning what they most likely will not need when starting out. On the other hand, it is much harder to go back to understand what GADTs are when you encounter them in the wild while working on your own stuff with possible time constraints.
Alternatives
No alternatives have been investigated yet.
Unresolved questions
How to handle libraries? (see section Incompatible Library Changes)
end of rfc text
Note that this design is not as fleshed out as I would like it to be, but some recent discussion on #rust-lang IRC channel prompted me to write these ideas down and start a more public and lasting discussion this way.
cc @aturon @nikomatsakis @withoutboats