- 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
versusTrait
);
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 namecatch
. Both ROOT and B use this function.A
implements some scheme which renamescatch
to_catch
, whencatch_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 oldcatch
function. This should work – which implies two differentA
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 currentimpl Trait
proposal, old unsized behaviour is accessible viadyn 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 enablingtrait_shorthand
feature first. -
catch
keyword. Introducingcatch
blocks ran into syntax ambiguities with struct initializers. Makingcatch
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.