mirror of
https://github.com/dart-lang/sdk
synced 2024-10-04 16:44:59 +00:00
Add doc explaining how language versioning and experiments work.
Change-Id: I9c30bcda07321065ae215c2d3ee5029ed66e587e Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/138680 Auto-Submit: Bob Nystrom <rnystrom@google.com> Reviewed-by: Leaf Petersen <leafp@google.com> Commit-Queue: Bob Nystrom <rnystrom@google.com>
This commit is contained in:
parent
7f649e9ebc
commit
0def426463
363
docs/process/language-versions-and-experiments.md
Normal file
363
docs/process/language-versions-and-experiments.md
Normal file
|
@ -0,0 +1,363 @@
|
|||
# Language Versioning and Experiments
|
||||
|
||||
This document explains our model for how to language versioning and experiment
|
||||
flags interact, and the processes we use to work with them. The key principles:
|
||||
|
||||
* Every major and minor (".0") release of the SDK creates a new language
|
||||
version. A shipped language version's semantics are entirely fixed by how it
|
||||
behaves in that version of the SDK.
|
||||
|
||||
* At any point in time, there is an "in-progress" language version with a
|
||||
family of related languages, one for each combination of experiments.
|
||||
|
||||
* Language versioning, experiment flags, and other magic for handling the core
|
||||
libraries and special packages like Flutter all boil down to mechanisms to
|
||||
select which of these many languages a given library wants to target.
|
||||
|
||||
## Language Versions
|
||||
|
||||
There is an ordered series of shipped language versions, 2.5, 2.6, etc. Each one
|
||||
is a different language. They may be mutually incompatible (in practice they are
|
||||
mostly compatible). Each Dart library targets—is "written in"—a
|
||||
*single* version of the language. (We'll get to how they target one soon.)
|
||||
Programs may contain libraries written in a variety of languages, and a Dart SDK
|
||||
supports multiple different Dart versions simultaneously.
|
||||
|
||||
Each time we ship a major or minor stable release of the SDK, the corresponding
|
||||
language version gets carved in stone. The day we shipped Dart 2.5.0, we
|
||||
henceforth and forevermore declared that there is only one Dart 2.5, and it
|
||||
refers to the first Dart version that supports the "constant update" changes.
|
||||
|
||||
As of today, the 2.5, 2.6, and 2.7 language versions are all locked down.
|
||||
|
||||
Patch releases, like 2.5.1, do not introduce new language versions. Both Dart
|
||||
SDK 2.5.0 and 2.5.1 contain language version 2.5. This implies that we cannot
|
||||
ship breaking language changes in patch releases. Doing so would spontaneously
|
||||
break any user whose library already targeted that language version.
|
||||
|
||||
### "In-progress" version
|
||||
|
||||
At any point in time, there is also an **in-progress language version.** It
|
||||
corresponds to the current dev build or (equivalently) the next stable version
|
||||
to be released. Today's current in-progress language version is 2.8 because we
|
||||
have shipped 2.7.1 and have not yet shipped 2.8.0.
|
||||
|
||||
Unlike the previous stable releases, the in-progress version is not carved in
|
||||
stone. As we develop the SDK, its behavior may change.
|
||||
|
||||
### Experimental languages
|
||||
|
||||
The in-progress version is not alone. Hanging off it are a family of sibling
|
||||
**experimental languages.** Each one corresponds to a specific combination of
|
||||
experiment flags. While there is only one Dart 2.7, there are several Dart 2.8s:
|
||||
|
||||
* "2.8": The Dart you get right now on bleeding edge with no experiments enabled.
|
||||
|
||||
* "2.8+non-nullable": The same but with the "non-nullable" experiment enabled.
|
||||
|
||||
* "2.8+variance": Likewise but with "variance" instead.
|
||||
|
||||
* "2.8+non-nullable+variance": Both "non-nullable" and "variance" experiments.
|
||||
|
||||
All of these languages exist simultaneously and in parallel. Dart 2.6, Dart 2.7,
|
||||
Dart 2.8, and Dart 2.8+non-nullable, etc. all *are* in some sense. They have
|
||||
(sometimes incomplete) specs. There are tools that implement them. Think of each
|
||||
as a different language with its own name, syntax, and semantics.
|
||||
|
||||
Don't think of an experiment flag as "turning on a feature". The feature is
|
||||
there, it's just in some other language. The only question is which libraries
|
||||
want to go over there and use it.
|
||||
|
||||
You can visualize the space of different flavors of Dart something like this:
|
||||
|
||||
```
|
||||
┌──────── shipped ─────────┐ ┌─ in-progress ─────────────┐
|
||||
older... ┄─ 2.5 ─ 2.6 ─ 2.7 ─ 2.8
|
||||
│
|
||||
┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ 2.8+non-nullable
|
||||
╎ ╎ │
|
||||
╎ no ╎ 2.8+variance
|
||||
╎ languages ╎ │
|
||||
╎ here... ╎ 2.8+triple-shift
|
||||
╎ ╎ │
|
||||
╎ ╎ 2.8+non-nullable+variance
|
||||
└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ │
|
||||
┆
|
||||
other experiment combinations...
|
||||
```
|
||||
|
||||
There is a line of numeric languages for all of the shipped stable versions of
|
||||
Dart, receding back into history. Then there is a single in-progress version and
|
||||
next to it are all of the various combinations of experiments. There are no
|
||||
other languages. In particular, there are no combinations of "shipped version +
|
||||
experiment". **You cannot "enable an experiment" in a library targeting a shipped
|
||||
version of the language.**
|
||||
|
||||
## Selecting a Language
|
||||
|
||||
This is the fundamental concept: there are a variety of different Dart languages
|
||||
for various versions and combinations of experimental in-progress features.
|
||||
Everything else is just a mechanism for users to select *which* of these
|
||||
languages they use.
|
||||
|
||||
The first part to picking a language for a library is picking the numeric part.
|
||||
The language versioning spec defines how that works. For completeness' sake, the
|
||||
basic rules are (in priority order):
|
||||
|
||||
1. A comment like `// @dart=2.5` selects that (numeric) version for the library.
|
||||
|
||||
2. For other libraries in a package, the "package_config.json" file specifies
|
||||
their language version. This file is generated by pub from the SDK
|
||||
constraints in the pubspecs of the various packages the user is using.
|
||||
|
||||
3. If a package does not specify an SDK constraint, then pub doesn't put a
|
||||
language version in the "package_config.json" for that file. In that case,
|
||||
libraries in that package default to the "current SDK" version.
|
||||
|
||||
4. Likewise, any library not part of a package defaults to the "current SDK"
|
||||
version.
|
||||
|
||||
### SDK version → Language version
|
||||
|
||||
The "current SDK version" is generally the semver version you get when you run
|
||||
one of the various Dart tools with `--version`.
|
||||
|
||||
To convert that three-component semver SDK version to a major.minor language
|
||||
version, use this rule: **The language version of an SDK is the major and minor
|
||||
version of the version reported by tools in that SDK.** This means that:
|
||||
|
||||
* Stable releases of the Dart SDK have the language version you expect: Dart
|
||||
2.5.3's language version is 2.5.
|
||||
|
||||
* Dev and bleeding edge releases have the language version of the upcoming
|
||||
stable release. On my machine today, `dart --version` reports
|
||||
"2.8.0-edge.a38…", which means its language version is "2.8". In other
|
||||
words, **the default language version of non-stable versions of the SDK is
|
||||
the in-progress language.**
|
||||
|
||||
Internally in the Dart SDK repo, the source of truth for the SDK version number
|
||||
is `tools/VERSION`. That file gets updated during the release process when
|
||||
various branches are cut and releases shipped. We could calculate the language
|
||||
version from that using the above rule, but we're worried that that means the
|
||||
language version could change inadvertently as a consequence of release
|
||||
administrivia. Instead, the repo stores the language version explicitly in
|
||||
`tools/experimental_features.yaml`.
|
||||
|
||||
In theory this means the SDK's reported version can get out of sync with its
|
||||
language version. In practice, slippage should be rare and only visible to users
|
||||
building the Dart SDK on bleeding edge.
|
||||
|
||||
### SDK constraint → Language version
|
||||
|
||||
The rule to convert an SDK constraint to a language is: **The default language
|
||||
version used by a package is the language version of its SDK constraint's
|
||||
minimum version.** Thus the following SDK constraints yield these language
|
||||
versions:
|
||||
|
||||
* `>=2.6.0 <3.0.0` → 2.6
|
||||
|
||||
* `>=2.6.3 <3.0.0` → 2.6 (still on 2.6)
|
||||
|
||||
* `>=2.7.1 <3.0.0` → 2.7 (still on 2.7)
|
||||
|
||||
* `>=2.8.0-dev.1 <3.0.0` → (2.8, in-progress version)
|
||||
|
||||
This rule lets users target language versions that exist only in dev releases.
|
||||
It also lets them use a patch version as their minimum version in order to get
|
||||
bug fixes or core library changes.
|
||||
|
||||
## Experiment Flags
|
||||
|
||||
Language versioning and the above section cover cases where you just want your
|
||||
library to get onto a specific numeric language version like 2.7, even including
|
||||
the current in-progress version 2.8. But what if you want to play with some
|
||||
experimental in-progress features? For that, you need to get onto one of the
|
||||
experimental sibling languages of the in-progress version. You get there by
|
||||
passing experiment flags to the various tools (and in their
|
||||
`analysis_options.yaml` file).
|
||||
|
||||
This is *all* the experiment flags do. Passing a set of experiment flags to a
|
||||
Dart tool means **Treat every user library using the in-progress language as
|
||||
using the given experimental language instead.**
|
||||
|
||||
"User library" means libraries authored by normal Dart users outside of the Dart
|
||||
and Flutter teams who may have some special powers described below.
|
||||
|
||||
"Using the in-progress language" means this rule only comes into play for the
|
||||
in-progress language. Today, passing the "non-nullable" flag shunts every user
|
||||
library targeting 2.8 over to 2.8+non-nullable, but has no effect on any library
|
||||
targeting 2.7 or older. *There is no such thing as 2.7+non-nullable.* The day
|
||||
2.7.0 shipped, 2.7 got locked down and all of the experimental versions
|
||||
surrounding it evaporated, to be replaced by a new set of experimental languages
|
||||
surrounding the new in-progress version 2.8.
|
||||
|
||||
Shipping a version of the SDK and language does not imply that all experiment
|
||||
flags that exist at that point automatically get turned on in that version. Many
|
||||
language changes gated behind experiment flags float through several releases
|
||||
before finally becoming ready to ship. The "non-nullable" and "variance"
|
||||
experiments existed before we shipped 2.7.0 and still exist today.
|
||||
|
||||
When a new version of the SDK is released, unless an experimental feature is
|
||||
deliberately "shipped" (meaning the behavior is turned on by default and the
|
||||
flag goes away), the flag simply carries forward as an experimental feature in
|
||||
the next in-progress version. So the day we shipped Dart 2.7.0, "variance"
|
||||
ceased to be a flag that affects Dart 2.7 and instead became a flag that affects
|
||||
Dart 2.8.
|
||||
|
||||
### Experiment flags are global across all user libraries
|
||||
|
||||
Note that passing an experiment flag moves *all* in-progress version user
|
||||
libraries onto that experimental language. If you pass "non-nullable", all of
|
||||
your 2.8 libraries *and every 2.8 library in every package you use* instantly
|
||||
starts targeting 2.8+non-nullable. We support mixed-mode Dart programs
|
||||
consisting of libraries using a variety of *shipped* versions like 2.7 and 2.6.
|
||||
You can even mix them with *one* in-progress version like 2.8 or
|
||||
2.8+non-nullable.
|
||||
|
||||
We do *not* support user programs that are arbitrary combinations of
|
||||
*experimental* languages. We don't want to have to define or implement what it
|
||||
means to have, for example, a 2.8+variance library importing a 2.8+non-nullable
|
||||
library and extending a generic class from it. Combinations of combinations is a
|
||||
path to madness.
|
||||
|
||||
We may internally allow some mixture to occur because of things like core
|
||||
libraries (see below), but that's because we can carefully control what code is
|
||||
in that weird state. We do not want to let *users* write programs that mix
|
||||
different experimental languages. If you have some user library that you don't
|
||||
want to be affected by an experiment you are playing with, make sure that
|
||||
library is not on the in-progress version.
|
||||
|
||||
### SDK core libraries and other special friends
|
||||
|
||||
Experiment flags are *a* way to shift a library from the in-progress version
|
||||
over to one of its experimental siblings, but not the only way. Remember, all
|
||||
experimental flavors of the in-progress version exist simultaneously. Experiment
|
||||
flags are primarily intended to let *users* opt their libraries into one of
|
||||
those experimental languages.
|
||||
|
||||
We on the Dart team have our own special powers. The migrated SDK core libraries
|
||||
do not need the user to pass any experiment flag to move them into
|
||||
2.8+non-nullable. Our tools know to do that automatically when compiling those
|
||||
particular libs. Likewise, when Flutter (and a couple of packages like
|
||||
vector_math that it exports from its API) migrate, we can also use whitelists or
|
||||
other special sauce to move them into 2.8+non-nullable.
|
||||
|
||||
However, all those libraries do need to take care to select the right *numeric*
|
||||
version. Because, again, there is no such thing as 2.7+non-nullable. So if, say,
|
||||
a core lib doesn't get marked as 2.8, it ain't gonna be 2.8+non-nullable. Dart's
|
||||
"language versioning" support is how libraries do that.
|
||||
|
||||
## Using Null Safety
|
||||
|
||||
OK, so let's put that all together to see how someone goes about being able to
|
||||
use `?` and `late` in their library today.
|
||||
|
||||
1. You must be running on a dev or bleeding edge build of the SDK. No stable
|
||||
release of Dart has support for any language later than 2.7.
|
||||
|
||||
2. Tell Dart that your libraries should be treated as 2.8. In the core libs,
|
||||
we've been using the version comments and/or some hardcoding. In a package,
|
||||
you can set the SDK constraint to:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
sdk: >=2.8.0-dev.0 <2.8.0
|
||||
```
|
||||
|
||||
*SDK min constraint:* You can require a higher dev release if you want. The
|
||||
important part is that the minimum is at least *a dev version of 2.8.0*. You
|
||||
could also omit the SDK constraint completely. That works OK for application
|
||||
packages but not for library packages since pub will not let you publish a
|
||||
package without an SDK constraint.
|
||||
|
||||
SDK max constraint: The relatively low max version gives us some wiggle room
|
||||
to break things before 2.8.0. I don't know if it's wise to claim that a
|
||||
package we publish right now will keep working all the way through, say,
|
||||
3.0.0. It's not strictly necessary. Everything in this doc still works if
|
||||
you do <3.0.0
|
||||
|
||||
3. Tell your Dart compiler to shunt all version 2.8 user libraries over to
|
||||
2.8+non-nullable by passing `--enable-experiment=non-nullable` when you
|
||||
invoke the tool. Put something similar in your `analysis_options.yaml` file
|
||||
for IDE goodness. See the [experimental flags doc][] for details.
|
||||
|
||||
That's it. Now you have a library that Dart tools know targets 2.8+non-nullable,
|
||||
at least today.
|
||||
|
||||
[experimental flags doc]: https://github.com/dart-lang/sdk/blob/master/docs/process/experimental-flags.md
|
||||
|
||||
### 2.8.0 ships without null safety
|
||||
|
||||
Let's say we ship Dart 2.8.0 stable and it does not include stable support for null safety. That means null safety is still behind the "non-nullable" flag. What happens?
|
||||
|
||||
The day this happens, 2.8 is no longer an "in-progress" language version. It has
|
||||
become carved in stone and that language version refers to exactly the behavior
|
||||
shipped by that SDK. All of the 2.8 experimental versions disappear. The
|
||||
experiment flags no longer affect libraries using language 2.8. Instead, at that
|
||||
exact same moment, a new 2.9 in-progress version appears. Any flags that we
|
||||
didn't ship and carried forward now apply to that. So there is 2.9+non-nullable,
|
||||
2.9+variance, etc.
|
||||
|
||||
This closing of 2.8 and opening of 2.9 implies several things:
|
||||
|
||||
* **Any library targeting 2.8 and using null safety features needs to have its
|
||||
language version changed to target 2.9.** This can mean changing a ``//
|
||||
@dart=2.8` comment or bumping the minimum SDK constraint in the pubspec.
|
||||
|
||||
* **In the 2.8.0 stable SDK that we just shipped, the language version for the
|
||||
core libraries must be 2.9.** They must be because they use null safety
|
||||
features, which can no longer be enabled for 2.8 libraries. This seems
|
||||
weird. How can 2.8.0 support a *future* version of Dart? The reality is that
|
||||
2.8.0 has secret *internal* support for some subset of 2.9 that we know the
|
||||
core libraries happen to fit within. It's an implementation detail that
|
||||
those core libraries happen to use capabilities within the SDK that we don't
|
||||
expose externally yet. It's strange, but I think should be OK.
|
||||
|
||||
* **Any packages needed by Flutter for users to play with null safety after
|
||||
2.8.0 ships need to have min SDK constraints above 2.8.0.** They need to get
|
||||
to language level 2.9, and the only way to do that is with a constraint that
|
||||
excludes 2.8.0. But if users are running on Dart 2.8.0 stable, Pub won't
|
||||
select any of those packages because 2.8.0 is outside of their SDK
|
||||
constraint!
|
||||
|
||||
This is a real problem, a consequence of using SDK constraints to control
|
||||
*both* language version and package resolution. To address this, shortly after
|
||||
shipping the stable build, we will also ship a Dart 2.9.0-dev.0 dev release
|
||||
and roll that into Flutter's dev channel. Anyone who wants to experiment
|
||||
with non-nullability needs to be on the dev channel. Null-safety is still an
|
||||
experimental feature, and the point of stable releases is to be, well,
|
||||
stable. If you want to experiment with experimental features, get yourself
|
||||
on dev channel.
|
||||
|
||||
### 2.10.0 ships with null safety
|
||||
|
||||
Then let's say we finally ship null safety officially in 2.10.0. Every package
|
||||
out there playing with the null safety experiment has already revved its minimum
|
||||
SDK constraint to something like `>=2.10.0-dev.0`. What happens next?
|
||||
|
||||
* **If the SDK constraint is like `>=2.10.0-dev.0 <2.10.0`, they just need to
|
||||
raise that to include 2.10.0.** This path is the safe choice because it's
|
||||
risky to assume the next stable release will be compatible with preceding
|
||||
in-progress dev builds. The point of dev builds is that they are in flux. So
|
||||
most packages using null safety should be in this state. Once we ship null
|
||||
safety, we just need to raise their max SDK constraints to include 2.10.0
|
||||
after verifying that the package still works with the stable release.
|
||||
|
||||
* **If the SDK constraint is like `>=2.10.0-dev.0 <3.0.0` the package author
|
||||
has nothing to do.** The package claims to support both the previous dev
|
||||
versions of null safety and the shipped stable version. A constraint like
|
||||
this is dubious because we reserve the right to make arbitrary breaking
|
||||
changes to features that are gated behind experiment flags. But if the
|
||||
package author is confident that no breakage has or will happen (likely
|
||||
because said "author" is a member of the Dart team), a wide constraint like
|
||||
this *can* be reasonable. And, in that case the package keeps working. It's
|
||||
already on language 2.10 and 2.10 now supports null safety out of the box.
|
||||
There's nothing to do.
|
||||
|
||||
* **If the SDK constraint is like `>=2.8.0 <3.0.0`, the package is on a
|
||||
previous "legacy" language version.** The package has "opted out of NNBD"
|
||||
and keeps working like it did before.
|
||||
|
||||
There's nothing special about "2.10.0" in this scenario. Whenever we choose to
|
||||
ship an experimental feature, in whatever version, this is how it should play
|
||||
out regarding packages.
|
Loading…
Reference in a new issue