|  | # 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 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/main/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. |