Skip to content

Latest commit

 

History

History
520 lines (436 loc) · 26.7 KB

3120-preview-features.md

File metadata and controls

520 lines (436 loc) · 26.7 KB

Summary

Making select unstable features available on stable Rust behind a preview flag.

Motivation

Most Rust features start their lives as unstable, which restricts them to only be available on the nightly release channel. This choice was made early on to reinforce Rust's commitment to stability as a deliverable. Once a feature is available on nightly, the hope is that it attracts developers who want the feature and are willing to restrict their crate's to only compiling on nightly. Those developers try out the feature, report problems, and thus improve the design, implementation, and stability of the feature. Eventually, once (if) the feature has seen sufficient evidence of its correctness, completeness, and usefulness, it is stabilized, which makes the feature available on the beta release channel, and eventually stable itself. This process generally serves Rust well -- it ensures that only features whose design and implementation have been battle-tested end up on stable.

The main factor that determines if, and how quickly, a feature is stabilized, is the evidence-gathering process. For highly desired features like const generics or generic associated types, there is no shortage of users willing to try out the feature and provide experience reports with using it. That, in turn, provides more feedback for honing its design and implementation in the early phases, and provides more evidence for maturity and usefulness in the later phases. That evidence is ultimately what is needed to build the confidence needed to stabilize a feature, so such features have a robust path to stabilization. Less highly desired features get less attention, and may spend much longer before the get stabilized if at all. This is, arguably, the process working as designed -- features that users want get prioritized.

There is, however, a class of features that suffers under this process -- those that impact a large number of users without impacting a large number of Rust developers. Consider a feature like Cargo's -Zstrip, which strips debug symbols from binaries and thus significantly reduces their size. It's likely not a feature a large number of individual Rust developers care about, but once it's made available, developers who disseminate programs written in Rust can much more easily distribute smaller binaries to all of their users, who may in turn have a better experience from the faster downloads. Another example is -Zpatch-in-config, which allow non-Cargo build systems to inject [patch] sections into Rust crates when they're built. Few individual developers need this feature, but the developers of those build systems may be able to use that feature to significantly improve the experience of working with first-party Rust dependencies under their build tool, which may in turn improve the user experience of large number of Rust developers that in turn rely on that build system.

The reason this class of features is underserved by the current process is two-fold. First, since few developers need them directly, the perceived need for the feature is less than that of a high-profile feature like const generics, even if the actual impact of the feature may be comparable. Second, and more importantly, users who are in the position to make use of such high-impact changes are often hesitant to abandon the stability guarantees of Rust's stable channel. While Rust's nightly releases are generally quite stable, and flags like -Zallow-features exist to limit the unstableness of the nightly compiler, at such large scale even the slight risk from switching to nightly is often unacceptable. For this reason, the developers who could make use of these high-impact features may be unable to actually use those features until they're stable (see rationale and alternatives for alternatives). But since stabilization requires evidence of impact and maturity, these features can get caught in a Catch-22 situation where evidence won't be provided until they're stabilized, and they can't be stabilized without evidence.

This RFC proposes a mechanism for improving this situation for a subset of such features without incurring the wide-spread breakage potential and ecosystem fragility that Rust's stability guarantee is intended to guard against. It exploits the fact that some of these underserved high-impact features are only needed in contained contexts where any breakage would be entirely localized, and in those cases it is acceptable to allow selective opting out of the stability guarantees.

Guide-level explanation

Most unstable features are only available on the nightly release channel. This is so that unstable features can continue to change as their design and implementation evolve, or even be removed, without breaking the backwards compatibility guarantees that Rust guarantees in its stable releases. However, certain unstable features may be available as preview features on Rust's beta and stable release channels to gather feedback on the feature from more users (such as those who cannot use nightly). You can enable a preview feature using the --enable-preview=some-feature option, and do not have to be on a nightly version of the compiler to do so.

The idea behind preview features is to solicit feedback on features that can have a high impact even if enabled in just one place, and therefore will not generally cause widespread breakage if they are later removed. Thus, a feature that a crate and all of its dependencies must make use of to be impactful is unlikely to be made available as preview, whereas a feature that is hugely impactful when used in a crate that builds a binary might.

Features are only available in preview for a relatively short time window (usually a bit longer than one release cycle); once the window passes, a feature will either be stabilized or return to nightly-only. During the preview period, the hope is to gather enough evidence from real-world usage to support stabilization, so if you do find value in a preview feature, please remember to contribute back a report of your experience!

Since preview features may not be stabilized after the preview period ends, users of preview features should be prepared to stop using the feature again in the future. This is another reason why only features that are useful at the "end" of the compilation pipeline are likely to be considered for preview.

Reference-level explanation

Process

To make an unstable feature available under preview, an FCP should take place for the responsible team. The customary waiting period is not required for preview features. When evaluating whether to allow a feature for preview, the team members should evaluate whether the feature:

  1. Has wide, fan-out impact. That is, if the feature is enabled in one place (e.g., a single build system or a single crate) would meaningfully and positively impact a large number of developers. "Large" is subjective of course, but 100+ developers is a good place to start.
  2. Has a stable implementation. The feature is unlikely to see significant changes before landing, especially in its interface. This could be because it is simple enough that this is trivial to determine, or it can be because the feature has gone through enough revisions that it's reasonable to conclude. This is important because if that's not the case, chances are it's best for the feature to be tested on nightly for the time being anyway.
  3. Does not cascade. The feature is impactful when enabled only at the "top level", be it at a crate with no dependents or in an external system. This is an important condition, as features that do not meet this requirement very quickly start violating Rust's stability guarantees.

Preview features are always approved for only a limited period of time, usually until the end of the next release cycle. This process is automated -- when a feature is marked for preview, it is also given an expiry, and any build after the listed expiry will treat the preview as inactive.

Over the course of the preview period, the hope is that users provide experience reports for the feature on stable, which can then serve as evidence supporting stabilization. Once a feature enters preview, teams should strive to make a stabilization decision by the time the preview period ends. If the decision is to stabilize the feature, the preview window should be extended until the next beta release that includes the stabilization so that users experience no gap in support.

In Cargo

Cargo traditionally exposes unstable features through the -Z flag, though the [unstable] table in .cargo/config.toml can also be used. Through these, users can enable new syntax in Cargo.toml, experimental command-line options, and other opt-in behavior changes that may not have an external interface.

This RFC proposes that cargo accept a new command-line flag, --enable-preview, and a new section in .cargo/config.toml: [enable-preview]. These are both available on beta/stable, and allow the user to list unstable features by name, or command-line options by their option. Passing in this flag makes cargo pretend that the corresponding feature/option is stable. That is, --enable-preview=im-a-teapot is equivalent to -Zim-a-teapot, and --enable-preview=--out-dir is equivalent to -Zunstable-options with the restriction that only the unstable option --out-dir is permitted.

Preview feature enabling is a binary setting, and does not accept values or additional options. This is because a preview should happen only when a feature is seeking stabilization, at which point its interface should be entirely stable. For example, an experimental command-line option currently exposed as -Zstrip=[none|symbols|debuginfo] could not be placed in preview, as it is not yet clear how the feature would be exposed for stabilization (it can't be with -Z).

Cargo should forward any --enable-preview features it does not recognize to rustc.

In Rust

Rust exposes unstable features in two primary ways: -Z compiler flags (like Cargo) and through #![feature] crate-level attributes. To support preview features, rustc, like cargo, will gain an --enable-preview flag on both beta and stable. That flag takes a list of features by name and command-line options by their option, and makes rustc pretend as if each passed-in feature/option is already stable.

Initial candidates

This is a list of initial candidate preview features solicited from internals.rust-lang.org:

Drawbacks

First and foremost, this addition enables users to opt out of the Rust stability guarantees on the stable release channel, a choice that was explicitly rejected in the past. Below is the rationale given, and why the mechanism proposed in this RFC may be acceptable after all.

First, as the web has shown numerous times, merely advertising instability doesn't work. Once features are in wide use it is very hard to change them -- and once features are available at all, it is very hard to prevent them from being used. Mechanisms like "vendor prefixes" on the web that were meant to support experimentation instead led to de facto standardization.

This RFC specifically targets features that need only be used in a small number of places to have a large impact, and that do not require cascading changes. Thus, any feature that makes it into preview should hopefully only be made use of in a handful of locations. Since preview periods are inherently time-limited, those early adopters are also required to be prepared for the feature leaving preview. Taken together, this should mean that exposing a feature under preview will not be equivalent to long-term stabilization.

Second, unstable features are by definition work in progress. But the beta/stable snapshots freeze the feature at scheduled points in time, while library authors will want to work with the latest version of the feature.

The RFC requires features to be specifically chosen for preview under the requirement that they have a fairly stable implementation and interface already. A feature may still end up changing while under preview, but any wide-reaching impacts should (hopefully) already have been nailed out on nightly before the feature is proposed for preview.

Finally, we simply cannot deliver stability for Rust unless we enforce it. Our promise is that, if you are using the stable release of Rust, you will never dread upgrading to the next release. If libraries could opt in to instability, then we could only keep this promise if all library authors guaranteed the same thing by supporting all three release channels simultaneously.

Once a user makes use of a preview feature, they will now experience some of that dread; the preview period will eventually end, and the feature may then be removed. But, this is something the user is aware of, and knows the timeline for, the moment they choose to opt into the preview. Furthermore, it's something that their consumers should not also be forced to opt into due to the no-cascading requirement, so undoing the change should be a local action.

Another drawback of this approach is that since stable and beta necessarily lag behind nightly, fixes and improvements to a preview feature will take a while to reach preview testers. This in turn means that testers will be testing a potentially known-buggy version of a feature, and their input may be outdated.

Rationale and alternatives

This RFC attempts to balance the need for testing of unstable features on a stable release channel against Rust's "stability as a deliverable" policy. There are many other points in the design-space:

RUSTC_BOOTSTRAP + -Zallow-features: The Rust toolchain already supports using unstable features on the stable compiler through the RUSTC_BOOSTRAP environment variable used to build the standard library with the latest stable compiler on each stable release. Users can further limit which features can be used by passing a list of features to -Zallow-features to avoid opting into all unstable features, which is particular important for build systems. We could promote this as the way to test unstable features on stable.

However, RUSTC_BOOTSTRAP is a bit of a blunt instrument -- users have little insight into how stable the features they opt into are. They also need to remember to include -Zallow-features to avoid feature creep. Furthermore, since the list of features aren't restricted, users can inadvertently opt into features that are then painful to remove later on, such as if they propagate to changes in their consumers. Preview features are essentially a list of features that the Rust maintainers have carefully decided are reasonable to test on stable, so that individual developers do not need to make that determination themselves.

Use nightly: Rust already has an avenue for testing unstable features: the nightly release channel. Since nightly releases are made, well, nightly, it contains the very latest fixes and features for unstable features, so any experience reports of bugs or poor UX are likely to be current and actionable. The nightly channel also inherently informs users that they are getting on a fast-moving train. We could tell users who want to test unstable features that they must do so on nightly, with no exceptions.

However, for larger organization using Rust, this is a non-starter. While Rust's nightly releases are generally quite stable, the frequency of stable-to-nightly regressions is much higher than that of stable-to-stable regressions. That, in turn, makes nightly a risky proposition for large-scale users for whom stability is a must. Furthermore, since nightly changes constantly, users with particularly large code bases may have a hard time finding any nightly release that has no known bugs that affect any of their potentially millions of lines of code while also supporting a particular feature they wish to test. And finally, using nightly shares the same challenge as RUSTC_BOOTSTRAP where users opt into all unstable features, rather than a carefully curated list.

Use beta: This has been suggested in the past as a compromise between allowing unstable features on stable and requiring testers to use nightly. The idea is to not allow unstable features on stable, but to allow them on the beta channel which should be more stable than nightly. Furthermore, since beta is closer to nightly, unstable features on beta would be more up to date, which should ensure that fewer experience reports are based on an outdated implementation.

However, the use of beta inherits problems from both of the preceding options. First, opting into all unstable features means that users can easily inadvertently start using features that will be hard to remove, or whose implementation is still very much in flux. And second, since beta is automatically cut from nightly, its stability varies over time -- when a beta is freshly cut, it is no more stable than nightly is. Thus, stability-sensitive users are likely to be wary of deploying beta widely. As an added challenge, the beta channel is supposed to serve as a testing ground for the next stable release. This means we want it both to be as identical as possible to the coming stable, and to encourage users to rely on its stability just as they do on that of stable. Allowing unstable features on beta but not stable violates that.

A new release channel: Since none of the current release channels are a great fit for allowing unstable features, the idea of introducing a new, fourth release channel has been proposed. We would introduce a channel that is more stable than nightly (and early betas), less stale than stable, and allows unstable features, that is specifically geared towards testing unstable features. We could even require that -Zallow-features is set on such a channel, so that users do not blindly use that channel instead of nightly and then start reporting already-fixed issues.

However, not only is a new release channel a fairly large endeavour, it's also not clear that it buys us much. If the channel is a fork of stable with unstable features enabled, then the challenge still arises that users can opt into any feature, including those that shouldn't really be considered ready for use yet. Furthermore, users would be forced to wait for a release cycle and a half for fixes to unstable features to reach them, unless we also establish a testing-beta, with all of the implications that carries. If the channel is instead a fork of nightly, well, then users can just use nightly instead. A fork off of beta splits the difference, but then means testing will vary in stability over time, just like beta does. There's also the question of whether backports should be made to such a channel, which would further increase the effort required.

Not doing this: The status quo has worked for Rust up until now -- testing happens on nightly, and that's that. And, as set out in the motivation section, that has worked well for many of Rust's features so far. However, it means that high-impact, low-visibility features end up with less testing, and thus a lower chance of being stabilized, even though those features may be highly important to big-fan-out-users, and potentially even blockers for adoption. The concern, more concretely, is that features that larger users, like organizations, external build systems, or non-Rust projects that use Rust projects highly desire, or even need, fall by the wayside, and ultimately hold back Rust's adoption in those ecosystems. Which would be a shame. Some of the trade-offs involved are discussed in depth in this internals thread on getting more testing of unstable features.

Prior art

NodeJS

NodeJS library features are marked with a "stability" indicator, but it is only used for documentation, and not for enforcement. Language features are rarer, but when support for ECMAScript modules was being tested, their opted for an explicit opt-in flag: --experimental-modules. That flag did not guarantee stable behavior, but was available on stable releases.

Java

Java has incubator modules and preview features for library features and language features respectively. The former take the form of modules under the jdk.incubator. prefix in its module name (it can also be used for tool names). Such modules are included with the standard stable JDK releases, and are explicitly renamed (requiring user action) when stabilized. The specification also states:

If an incubating API is not standardized or otherwise promoted after a small number of JDK feature releases, then it will no longer be able to incubate, and its packages and incubator module will be removed.

Which is similar to the time bound for preview features in this RFC. Furthermore:

An incubating feature need not be retained forever in the JDK Release Project in which it was introduced, nor in every release of downstream binaries derived from that Release Project. For example, an incubating feature may evolve, or even be removed, between different update releases of a JDK Release Project. Beyond this explicit statement of when evolution is permitted, this proposal deliberately provides no further guidance. Such decisions are best left to the individual feature owner.

The latter (preview features) are summarized in JEP12 as

A preview feature is a new feature of the Java language, Java Virtual Machine, or Java SE API that is fully specified, fully implemented, and yet impermanent. It is available in a JDK feature release to provoke developer feedback based on real world use; this may lead to it becoming permanent in a future Java SE Platform.

and later adds

whose design, specification, and implementation are complete, but which would benefit from a period of broad exposure and evaluation before either achieving final and permanent status in the Java SE Platform or else being refined or removed.

Which is very similar to the desire for preview features in this RFC, though with an even stronger requirement for implementation stability. The spec also suggests a timeline that resembles the one proposed in this RFC (two release cycles):

By "complete", we do not mean "100% finished", since that would imply feedback is pointless. Instead, we mean "100% finished within 12 months". This timeline reflects our experience that two rounds of previewing is the norm, i.e., preview in Java $N and $N+1 then final in $N+2.

In particular, the spec recommends that preview features are high quality, not experimental, and universally available.That is, the feature should meet the same level of "technical excellence and finesse" as a final and permanent feature, and the feature should not be experimental, risky, incomplete, or unstable. The "universally available" bit isn't very relevant in our case, and requires that all preview features are available in all Java SE implementations.

Java's Preview features are all disabled by default, and can be enabled explicitly with --enable-preview, though that flag enables the use of all preview features. This RFC instead proposes that individual features can be enabled individually.

Any use of a Java preview feature generates a non-suppressible warning.

The JEP12 section on process is also a good read.

Ruby and Python

Ruby and Python publish preview releases that may included not-yet-stabilized features. As far as I can tell, there is no formal process around opting into these beyond "run preview".

Go

Go does not appear to have any mechanism for previewing and testing language features. Library features are previewed using x modules, such as x/tools and x/net, which carry no stability guarantees.

Unresolved questions

  • How long should the preview period be?
  • What features should be chosen initially?
  • How strict should we be about adding new features?
  • How does a feature get proposed for preview in the firstplace?
  • Should we require that the API of preview features not change during the preview period?
  • Is an FCP appropriate for choosing whether to land a feature in preview?
  • Should preview features be advertised more widely? Where?
  • Should use of a preview feature give a lint warning? Should it be possible to suppress?

Future possibilities

We could consider extending preview features to cover editions as well: --enable-preview=edition2021 would enable --edition=2021 on stable/beta before we actually release the 2021 edition.

Given sufficient resources, making a feature available in preview could be extended to mean committing to backporting fixes to that feature onto stable/beta as well, so that stale feedback from stable/beta during the feedback cycle is mitigated.