diff --git a/working/0546-patterns/patterns-feature-specification.md b/working/0546-patterns/patterns-feature-specification.md index f8c463b1f0..241bd42276 100644 --- a/working/0546-patterns/patterns-feature-specification.md +++ b/working/0546-patterns/patterns-feature-specification.md @@ -4,7 +4,7 @@ Author: Bob Nystrom Status: In progress -Version 1.8 (see [CHANGELOG](#CHANGELOG) at end) +Version 2.0 (see [CHANGELOG](#CHANGELOG) at end) Note: This proposal is broken into a couple of separate documents. See also [records][] and [exhaustiveness][]. @@ -24,7 +24,6 @@ of some of the most highly-voted user requests. It directly addresses: * [Extensible pattern matching](https://github.com/dart-lang/language/issues/1047) (69 👍, 23rd highest) * [JDK 12-like switch statement](https://github.com/dart-lang/language/issues/27) (79 👍, 19th highest) * [Switch expression](https://github.com/dart-lang/language/issues/307) (28 👍) -* [Type patterns](https://github.com/dart-lang/language/issues/170) (9 👍) * [Type decomposition](https://github.com/dart-lang/language/issues/169) (For comparison, the current #1 issue, [Data classes](https://github.com/dart-lang/language/issues/314) has 824 👍.) @@ -151,946 +150,960 @@ The core of this proposal is a new category of language construct called a grammar. Patterns form a third category. Like expressions and statements, patterns are often composed of other subpatterns. -The basic idea with a pattern is that it: - -* Can be tested against some value to determine if the pattern *matches*. If - not, the pattern *refutes* the value. Some kinds of patterns, called - "irrefutable patterns" always match. - -* If (and only if) a pattern does match, the pattern may bind new variables in - some scope. - -Patterns appear inside a number of other constructs in the language. This -proposal extends Dart to allow patterns in: - -* Top-level and local variable declarations. -* Static and instance field declarations. -* For loop variable declarations. -* Switch statement cases. -* A new switch expression form's cases. - -### Binding and matching patterns - -Languages with patterns have to deal with a tricky ambiguity. What does a bare -identifier inside a pattern mean? In some cases, you would like it declare a -variable with that name: +The basic ideas with patterns are: + +* Some can be tested against a value to determine if the pattern *matches* the + value. If not, the pattern *refutes* the value. Other patterns, called + *irrefutable* always match. + +* Some patterns, when they match, *destructure* the matched value by pulling + data out of it. For example, a list pattern extracts elements from the list. + A record pattern destructures fields from the record. + +* Variable patterns bind new variables to values that have been matched or + destructured. The variables are in scope in a region of code that is only + reachable when the pattern has matched. + +This gives you a compact, composable notation that lets you determine if an +object has the form you expect, extract data from it, and then execute code only +when all of that is true. + +Before introducing each pattern in detail, here is a summary with some examples: + +| Kind | Examples | +| ---- |-------- | +| [Logical-or][logicalOrPattern] | `subpattern1 \| subpattern2` | +| [Logical-and][logicalAndPattern] | `subpattern1 & subpattern2` | +| [Relational][relationalPattern] | `== expression`
`< expression` | +| [Null-check][nullCheckPattern] | `subpattern?` | +| [Null-assert][nullAssertPattern] | `subpattern!` | +| [Literal][literalPattern] | `123`, `null`, `'string'` | +| [Constant][constantPattern] | `math.pi`, `SomeClass.constant` | +| [Variable][variablePattern] | `foo`, `String str`, `_`, `int _` | +| [Cast][castPattern] | `foo as String` | +| [Grouping][groupingPattern] | `(subpattern)` | +| [List][listPattern] | `[subpattern1, subpattern2]` | +| [Map][mapPattern] | `{"key": subpattern1, someConst: subpattern2}` | +| [Record][recordPattern] | `(subpattern1, subpattern2)`
`(x: subpattern1, y: subpattern2)` | +| [Extractor][extractorPattern] | `SomeClass(x: subpattern1, y: subpattern2)` | + +[logicalOrPattern]: #logical-or-pattern +[logicalAndPattern]: #logical-and-pattern +[relationalPattern]: #relational-pattern +[nullCheckPattern]: #null-check-pattern +[nullAssertPattern]: #null-assert-pattern +[literalPattern]: #literal-pattern +[constantPattern]: #constant-pattern +[variablePattern]: #variable-pattern +[castPattern]: #cast-pattern +[groupingPattern]: #grouping-pattern +[listPattern]: #list-pattern +[mapPattern]: #map-pattern +[recordPattern]: #record-pattern +[extractorPattern]: #extractor-pattern + +Here is the overall grammar for the different kinds of patterns: + +``` +pattern ::= logicalOrPattern +patterns ::= pattern ( ',' pattern )* ','? + +logicalOrPattern ::= logicalAndPattern ( '|' logicalOrPattern )? +logicalAndPattern ::= relationalPattern ( '&' logicalAndPattern )? +relationalPattern ::= ( equalityOperator | relationalOperator) relationalExpression + | unaryPattern + +unaryPattern ::= nullCheckPattern + | nullAssertPattern + | primaryPattern + +primaryPattern ::= literalPattern + | constantPattern + | variablePattern + | castPattern + | groupingPattern + | listPattern + | mapPattern + | recordPattern + | extractorPattern +``` + +As you can see, logical-or patterns (`|`) have the lowest precedence, then +logical-and patterns (`&`), then the postfix unary null-check (`?`) and +null-assert (`!`) patterns, followed by the remaining highest precedence primary +patterns. + +The individual patterns are: + +### Logical-or pattern + +``` +logicalOrPattern ::= logicalAndPattern ( '|' logicalOrPattern )? +``` + +A pair of patterns separated by `|` matches if either of the branches match. +This can be used in a switch expression or statement to have multiple cases +share a body: ```dart -var (a, b) = (1, 2); +var isPrimary = switch (color) { + case Color.red | Color.yellow | Color.blue => true; + default => false; +}; ``` -Here, `a` and `b` declare new variables. In other cases, you would like -identifiers to be references to constants so that you can refer to a constant -value in the pattern: +Even in switch statements, which allow multiple empty cases to share a single +body, a logical-or pattern can be useful when you want multiple patterns to +share a guard: ```dart -const a = 1; -const b = 2; - -switch ([1, 2]) { - case [a, b]: print("Got 1 and 2"); +switch (shape) { + case Square(size) | Circle(size) when size > 0: + print('Non-empty symmetric shape'); + case Square() | Circle(): + print('Empty symmetric shape'); + default: + print('Asymmetric shape'); } ``` -Here, `a` and `b` are references to constants and the pattern checks to see if -the value being switched on is equivalent to a list containing those two -elements. - -This proposal [follows Swift][swift pattern] and addresses the ambiguity by -dividing patterns into two general categories *binding patterns* or *binders* -and *matching patterns* or *matchers*. (Binding patterns don't always -necessarily bind a variable, but "binder" is easier to say than "irrefutable -pattern".) Binders appear in irrefutable contexts like variable declarations -where the intent is to destructure and bind variables. Matchers appear in -contexts like switch cases where the intent is first to see if the value matches -the pattern or not and where control flow can occur when the pattern doesn't -match. - -[swift pattern]: https://docs.swift.org/swift-book/ReferenceManual/Patterns.html +A logical-or pattern does not have to appear at the top level of a pattern. It +can be nested inside a destructuring pattern: -### Grammar summary +```dart +// Matches a two-element list whose first element is 'a' or an int: +if (var ['a' | int _, c] = list) ... +``` -Before walking through them in detail, here is a short summary of the kinds of -patterns and where they can appear. There are three basic entrypoints into the -pattern grammar: +A logical-or pattern may match even if one of its branches does not. That means +that any variables in the non-matching branch would not be initialized. To avoid +problems stemming from that, the following restrictions apply: -* [`matcher`][matcher] is the refutable patterns. -* [`binder`][binder] is the irrefutable patterns. -* [`declarationBinder`][declarationBinder] is a subset of the irrefutable - patterns that make sense as the outermost pattern in a variable declaration. - Subsetting allows reasonable code like: +* It is a compile-time error if one branch contains a variable pattern whose + name or type does not exactly match a corresponding variable pattern in the + other branch. These variable patterns can appear as subpatterns anywhere in + each branch, but in total both branches must contain the same variables with + the same types. This way, variables used inside the body covered by the + pattern will always be initialized to a known type. - ```dart - var [a, b] = [1, 2]; // List. - var (a, b) = (1, 2); // Record. - var {1: a} = {1: 2}; // Map. - ``` +* If the left branch matches, the right branch is not evaluated. This + determines *which* value the variable gets if both branches would have + matched. In that case, it will always be the value from the left branch. - While avoiding strange uses like: +### Logical-and pattern - ```dart - var String str = 'redundant'; // Variable. - var str as String = 'weird'; // Cast. - var definitely! = maybe; // Null-assert. - ``` +``` +logicalAndPattern ::= relationalPattern ( '&' logicalAndPattern )? +``` -[matcher]: #refutable-patterns-matchers -[binder]: #irrefutable-patterns-binders -[declarationBinder]: #irrefutable-patterns-binders - -These entrypoints are wired into the rest of the language like so: - -* A [pattern variable declaration][] contains a - [`declarationBinder`][declarationBinder]. -* A [switch case][] contains a [`matcher`][matcher]. - -[pattern variable declaration]: #pattern-variable-declaration -[switch case]: #switch-statement - -Many kinds of patterns have both matcher (refutable) and binder (irrefutable) -forms. The table below shows examples of every specific pattern type and which -categories it appears in: - -| Type | Decl Binder? | Binder? | Matcher? | Examples | -| ---- | ------------ | ------- | -------- | -------- | -| Record | Yes | [Yes][recordBinder] | [Yes][recordMatcher] | `(subpattern1, subpattern2)`
`(x: subpattern1, y: subpattern2)` | -| List | Yes | [Yes][listBinder] | [Yes][listMatcher] | `[subpattern1, subpattern2]` | -| Map | Yes | [Yes][mapBinder] | [Yes][mapMatcher] | `{"key": subpattern}` | -| Wildcard | No | [Yes][wildcardBinder] | [Yes][wildcardMatcher] | `_` | -| Variable | No | [Yes][variableBinder] | [Yes][variableMatcher] | `foo // Binder syntax.`
`var foo // Matcher syntax.`
`String str // Works in either.` | -| Cast | No | [Yes][castBinder] | No | `foo as String` | -| Null assert | No | [Yes][nullAssertBinder] | No | `subpattern!` | -| Literal | No | No | [Yes][literalMatcher] | `123`, `null`, `'string'` | -| Constant | No | No | [Yes][constantMatcher] | `foo`, `math.pi` | -| Null check | No | No | [Yes][nullCheckMatcher] | `subpattern?` | -| Extractor | No | No | [Yes][extractMatcher] | `SomeClass(subpattern1, x: subpattern2)` | - -[recordBinder]: #record-binder -[recordMatcher]: #record-matcher -[listBinder]: #list-binder -[listMatcher]: #list-matcher -[mapBinder]: #map-binder -[mapMatcher]: #map-matcher -[wildcardBinder]: #wildcard-binder -[wildcardMatcher]: #wildcard-matcher -[variableBinder]: #variable-binder -[variableMatcher]: #variable-matcher -[castBinder]: #cast-binder -[nullAssertBinder]: #null-assert-binder -[literalMatcher]: #literal-matcher -[constantMatcher]: #constant-matcher -[nullCheckMatcher]: #null-check-matcher -[extractMatcher]: #extractor-matcher - -## Syntax - -This proposal introduces a number different pattern forms and several places in -the language where they can be used. - -Going top-down through the grammar, we start with the constructs where patterns -are allowed and then get to the patterns themselves. +A pair of patterns separated by `&` matches only if *both* subpatterns match. +Unlike logical-or patterns, the variables defined in each branch must *not* +overlap, since the logical-and pattern only matches if both branches do and +the variables in both branches will be bound. -### Pattern variable declaration +If the left branch does not match, the right branch is not evaluated. *This only +matters because patterns may invoke user-defined methods with visible side +effects.* -Most places in the language where a variable can be declared are extended to -allow a pattern, like: +### Relational pattern -```dart -var (a, [b, c]) = ("str", [1, 2]); +``` +relationalPattern ::= ( equalityOperator | relationalOperator) relationalExpression ``` -Dart's existing C-style variable declaration syntax makes it harder to -incorporate patterns. Variables can be declared just by writing their type, and -a single declaration might declare multiple variables. Fully incorporating -patterns into that could lead to confusing syntax like: +A relational pattern lets you compare the matched value to a given constant +using any of the equality or relational operators: `==`, `!=`, `<`, `>`, `<=`, +and `>=`. The pattern matches when calling the appropriate operator on the +matched value with the constant as an argument returns `true`. It is a +compile-time error if `relationalExpression` is not a valid constant expression. + +The `==` operator is sometimes useful for matching named constants when the +constant doesn't have a qualified name: ```dart -(int, String) (n, s) = (1, "str"); -final (a, b) = (1, 2), c = 3, (d, e); +void test(int value) { + const magic = 123; + switch (value) { + case == magic: print('Got the magic number'); + default: print('Not the magic number'); + } +} ``` -To avoid this weirdness, patterns only occur in variable declarations that begin -with a `var` or `final` keyword. Also, a variable declaration using a pattern -can only have a single declaration "section". No comma-separated multiple -declarations like: +The comparison operators are useful for matching on numeric ranges, especially +when combined with `&`: ```dart -var a = 1, b = 2; +String asciiCharType(int char) { + const space = 32; + const zero = 48; + const nine = 57; + + return switch (char) { + case < space => 'control'; + case == space => 'space'; + case > space & < zero => 'punctuation'; + case >= zero & <= nine => 'digit'; + // Etc... + } +} ``` -Also, declarations with patterns must have an initializer. This is not a -limitation since the point of using a pattern in a variable declaration is to -immediately destructure the initialized value. - -Add these new rules: +### Null-check pattern ``` -patternDeclaration ::= - | patternDeclarator declarationBinder '=' expression - -patternDeclarator ::= 'late'? ( 'final' | 'var' ) +nullCheckPattern ::= unaryPattern '?' ``` -**TODO: Should we support destructuring in `const` declarations?** - -And incorporate the new rules into these existing rules: +A null-check pattern matches if the value is not null, and then matches the +inner pattern against that same value. Because of how type inference flows +through patterns, this also provides a terse way to bind a variable whose type +is the non-nullable base type of the nullable value being matched: +```dart +String? maybeString = ... +if (var s? = maybeString) { + // s has type non-nullable String here. +} ``` -topLevelDeclaration ::= - | // Existing productions... - | patternDeclaration ';' // New. -localVariableDeclaration ::= - | initializedVariableDeclaration ';' // Existing. - | patternDeclaration ';' // New. +Using `?` to match a value that is *not* null seems counterintuitive. In truth, +I have not found an ideal syntax for this. The way I think about `?` is that it +describes the test it performs. Where a list pattern tests whether the value is +a list, a `?` tests whether the value is null. However, unlike other patterns, +it matches when the value is *not* null, because matching on null isn't +useful—you could always just use a `null` literal pattern for that. -forLoopParts ::= - | // Existing productions... - | ( 'final' | 'var' ) declarationBinder 'in' expression // New. +Swift [uses the same syntax for a similar feature][swift null check]. -// Static and instance fields: -declaration ::= - | // Existing productions... - | 'static' patternDeclaration // New. - | 'covariant'? patternDeclaration // New. -``` +[swift null check]: https://docs.swift.org/swift-book/ReferenceManual/Patterns.html#ID520 -### Switch statement +**TODO: Try to come up with a better syntax. Most other patterns visually +reflect what values they *do* match, and using `?` for null-checks does the +exact *opposite*: it looks like the values it *refutes*.** -We extend switch statements to allow patterns in cases: +### Null-assert pattern ``` -switchStatement ::= 'switch' '(' expression ')' '{' switchCase* defaultCase? '}' -switchCase ::= label* caseHead ':' statements -caseHead ::= 'case' matcher caseGuard? -caseGuard ::= 'if' '(' expression ')' +nullAssertPattern ::= unaryPattern '!' ``` -Allowing patterns in cases significantly increases the expressiveness of what -properties a case can verify, including executing arbitrary user-defined code. -This implies that the order that cases are checked is now potentially -user-visible and an implementation must execute the *first* case that matches. - -#### Guard clauses +A null-assert pattern is similar to a null-check pattern in that it permits +non-null values to flow through. But a null-assert *throws* if the matched +value is null. It lets you forcibly *assert* that you know a value shouldn't +be null, much like the corresponding `!` null-assert expression. -We also allow an optional *guard clause* to appear after a case. This enables -a switch case to evaluate a secondary arbitrary predicate: +This lets you eliminate null in variable declarations where a refutable pattern +isn't allowed: ```dart -switch (obj) { - case [var a, var b] if (a > b): - print("First element greater"); -} +(int?, int?) position = ... + +// We know if we get here that the coordinates should be present: +var (x!, y!) = position; ``` -This is useful because if the guard evaluates to false then execution proceeds -to the next case, instead of exiting the entire switch like it would if you -had nested an `if` statement inside the switch case. +Or where you don't want null to be silently treated as a match failure, as in: -#### Implicit break +```dart +List row = ... -A long-running annoyance with switch statements is the mandatory `break` -statements at the end of each case body. Dart does not allow fallthrough, so -these `break` statements have no real effect. They exist so that Dart code does -not *appear* to be doing fallthrough to users coming from languages like C that -do allow it. That is a high syntactic tax for limited benefit. +// If the first column is 'user', we expect to have a name after it. +if (var ['user', name!] = row) { + // name is a non-nullable string here. +} +``` -I inspected the 25,014 switch cases in the most recent 1,000 packages on pub -(10,599,303 LOC). 26.40% of the statements in them are `break`. 28.960% of the -cases contain only a *single* statement followed by a `break`. This means -`break` is a fairly large fraction of the statements in all switches for -marginal benefit. +### Literal pattern -Therefore, this proposal removes the requirement that each non-empty case body -definitely exit. Instead, a non-empty case body implicitly jumps to the end of -the switch after completion. From the spec, remove: +``` +literalPattern ::= booleanLiteral | nullLiteral | numericLiteral | stringLiteral -> If *s* is a non-empty block statement, let *s* instead be the last statement -of the block statement. It is a compile-time error if *s* is not a `break`, -`continue`, `rethrow` or `return` statement or an expression statement where the -expression is a `throw` expression. +``` -*This is now valid code that prints "one":* +A literal pattern determines if the value is equivalent to the given literal +value. There are no list and map *literal* patterns since there are actual list +and map patterns. + +**Breaking change**: Using patterns in switch cases means that a list or map +literal in a switch case is now interpreted as a list or map pattern which +destructures its elements at runtime. Before, it was simply treated as identity +comparison. ```dart -switch (1) { - case 1: - print("one"); - case 2: - print("two"); +const a = 1; +const b = 2; +var obj = [1, 2]; // Not const. + +switch (obj) { + case [a, b]: print("match"); break; + default: print("no match"); } ``` -Empty cases continue to fallthrough to the next case as before: +In Dart today, this prints "no match". With this proposal, it changes to +"match". However, looking at the 22,943 switch cases in 1,000 pub packages +(10,599,303 lines in 34,917 files), I found zero case expressions that were +collection literals. -*This prints "one or two":* +### Constant pattern -```dart -switch (1) { - case 1: - case 2: - print("one or two"); -} +``` +constantPattern ::= qualifiedName ``` -### Switch expression +Like literal patterns, a named constant pattern determines if the matched value +is equal to the constant's value. -When you want an `if` statement in an expression context, you can use a -conditional expression (`?:`). There is no expression form for multi-way -branching, so we define a new switch expression. It takes code like this: +Only qualified names can be used as constant patterns. That includes prefixed +constants like `some_library.aConstant`, static constants on classes like +`SomeClass.aConstant`, and prefixed static constants like +`some_library.SomeClass.aConstant`. It does *not* allow references to named +constants that are simple identifiers. Those are ambiguous with variable +patterns and the language resolves the ambiguity by treating it as a variable +pattern: ```dart -Color shiftHue(Color color) { - switch (color) { - case Color.red: - return Color.orange; - case Color.orange: - return Color.yellow; - case Color.yellow: - return Color.green; - case Color.green: - return Color.blue; - case Color.blue: - return Color.purple; - case Color.purple: - return Color.red; +void test() { + const localConstant = 1; + switch (2) { + case localConstant: print('matched!'); + default: print('unmatched'); } } ``` -And turns it into: +This prints "matched!" because `localConstant` in the case is interpreted as a +*variable* pattern that matches any value and binds the value to a new variable +with that name. (We should have a lint that warns if you have a variable pattern +whose name is the same as a surrounding constant, because that likely indicates +a mistake.) -```dart -Color shiftHue(Color color) { - return switch (color) { - case Color.red => Color.orange; - case Color.orange => Color.yellow; - case Color.yellow => Color.green; - case Color.green => Color.blue; - case Color.blue => Color.purple; - case Color.purple => Color.red; - }; -} -``` - -The grammar is: +**Breaking change:** This is a breaking change for simple identifiers that +appear in existing switch cases. Fortunately, it turns out that most switch +cases are not simple named constants. I analyzed a large corpus of Pub packages +and Flutter apps (13M+ lines in 61,346 files): ``` -primary ::= thisExpression - | // Existing productions... - | switchExpression - -switchExpression ::= 'switch' '(' expression ')' '{' - switchExpressionCase* defaultExpressionCase? '}' -switchExpressionCase ::= caseHead '=>' expression ';' -defaultExpressionCase ::= 'default' '=>' expression ';' + -- Case (81469 total) -- + 43469 ( 53.356%): literal =================== + 34960 ( 42.912%): prefixed.identifier =============== + 2855 ( 3.504%): identifier == + 171 ( 0.210%): prefixed.property.access = + 7 ( 0.009%): SymbolLiteralImpl = + 4 ( 0.005%): MethodInvocationImpl = (const ctor calls) + 3 ( 0.004%): FunctionReferenceImpl = (type literals) ``` -**TODO: This does not allow multiple cases to share an expression like empty -cases in a switch statement can share a set of statements. Can we support -that?** +Named constants are common in switch cases, but most of them are *qualified* +identifiers like `SomeEnum.value` or `prefix.aConstant`. Switches using a simple +identifier are only 3.5% of the cases. Most come from just a couple of packages +and most of those are from using the charcode package. Changing those to use an +import prefix would eliminate most of that already small 3.5%. -Slotting into `primary` means it can be used anywhere any expression can appear -even as operands to unary and binary operators. Many of these uses are ugly, but -not any more problematic than using a collection literal in the same context -since a `switch` expression is always delimited by a `switch` and `}`. - -Making it high precedence allows useful patterns like: +In rare cases where you do need a pattern to refer to a named constant with a +simple identifier name, you can use an explicit `==` pattern: ```dart -await switch (n) { - case 1 => aFuture; - case 2 => anotherFuture; -}; - -var x = switch (n) { - case 1 => obj; - case 2 => another; -}.someMethod(); +void test() { + const localConstant = 1; + switch (2) { + case == localConstant: print('matched!'); + default: print('unmatched'); + } +} ``` -Over half of the switch cases in a large corpus of packages contain either a -single return statement or an assignment followed by a break so there is some -evidence this will be useful. +This prints "unmatched". -#### Expression statement ambiguity +**TODO: We should add a lint that warns if a variable pattern shadows an +in-scope constant since it's likely a mistaken attempt to match the constant.** -Thanks to expression statements, a switch expression could appear in the same -position as a switch statement. This isn't technically ambiguous, but requires -unbounded lookahead to tell if a switch in statement position is a statement or -expression. - -```dart -main() { - switch (some(extremely, long, expression, here)) { - case Some(Quite(var long, var pattern)) => expression(); - }; +### Variable pattern - switch (some(extremely, long, expression, here)) { - case Some(Quite(var long, var pattern)) : statement(); - } -} +``` +variablePattern ::= type? identifier ``` -To avoid that, we disallow a switch expression from appearing at the beginning -of an expression statement. This is similar to existing restrictions on map -literals appearing in expression statements. In the rare case where a user -really wants one there, they can parenthesize it. +A variable pattern binds the matched value to a new variable. These usually +occur as subpatterns of a destructuring pattern in order to capture a +destructured value. -**TODO: If we change switch expressions [to use `:` instead of `=>`][2126] then -there will be an actual ambiguity. In that case, reword the above section.** +```dart +var (a, b) = (1, 2); +``` -[2126]: https://github.com/dart-lang/language/issues/2126 +Here, `a` and `b` are variable patterns and end up bound to `1` and `2`, +respectively. -### If-case statement - -Often you want to conditionally match and destructure some data, but you only -want to test a value against a single pattern. You can use a `switch` statement -for that, but it's pretty verbose: +The pattern may also have a type annotation in order to only match values of the +specified type. If the type annotation is omitted, the variable's type is +inferred and the pattern matches all values. ```dart -switch (json) { - case [int x, int y]: - return Point(x, y); +if ((int x, String s) = record) { + print('First field is int $x and second is String $s.'); } ``` -We can make simple uses like this a little cleaner by introducing an if-like -form similar to [if-case in Swift][]: +#### Wildcards -[if-case in swift]: https://useyourloaf.com/blog/swift-if-case-let/ +If the variable's name is `_`, it doesn't bind any variable. This "wildcard" +name is useful as a placeholder in places where you need a subpattern in order +to destructure later positional values: ```dart -if (json case [int x, int y]) return Point(x, y); +var list = [1, 2, 3]; +var [_, two, _] = list; ``` -It may have an else branch as well: +The `_` identifier can also be used with a type annotation when you want to test +a value's type but not bind the value to a name: ```dart -if (json case [int x, int y]) { - print('Was coordinate array $x,$y'); -} else { - throw FormatException('Invalid JSON.'); +if ((int _, String _) = record) { + print('First field is int and second is String.'); } ``` -The grammar is: +### Cast pattern ``` -ifCaseStatement ::= 'if' '(' expression 'case' matcher ')' - statement ('else' statement)? +castPattern ::= identifier 'as' type ``` -The `expression` is evaluated and matched against `matcher`. If the pattern -matches, then the then branch is executed with any variables the pattern -defines in scope. Otherwise, the else branch is executed if there is one. - -Unlike `switch`, this form doesn't allow a guard clause. Guards are important in -switch cases because, unlike nesting an if statement *inside* the switch case, a -failed guard will continue to try later cases in the switch. That is less -important here since the only other case is the else branch. +A cast pattern is similar to a variable pattern in that it binds a new variable +to the matched value with a given type. But where a variable pattern is +*refuted* if the value doesn't have that type, a cast pattern *throws*. Like the +null-assert pattern, this lets you forcibly assert the expected type of some +destructured value. This isn't useful as the outermost pattern in a declaration +since you can always move the `as` to the initializer expression: -**TODO: Consider allowing guard clauses here. That probably necessitates -changing guard clauses to use a keyword other than `if` since `if` nested inside -an `if` condition looks pretty strange.** - -### Irrefutable patterns ("binders") +```dart +num n = 1; +var i as int = n; // Instead of this... +var i = n as int; // ...do this. +``` -Binders are the subset of patterns whose aim is to define new variables in some -scope. A binder can never be refuted. To avoid ambiguity with existing variable -declaration syntax, the outermost pattern where a binding pattern is allowed is -somewhat restricted: +But when destructuring, there is no place in the initializer to insert the cast. +This pattern lets you insert the cast as values are being pulled out by the +pattern: +```dart +(num, Object) record = (1, "s"); +var (i as int, s as String) = record; ``` -declarationBinder ::= -| listBinder -| mapBinder -| recordBinder -``` - -**TODO: Allow extractBinder patterns here if we support irrefutable user-defined -extractors.** -This means that the outer pattern is always some sort of destructuring pattern -that contains subpatterns. Once nested inside a surrounding binder pattern, you -have access to all of the binders: +### Grouping pattern ``` -binder -| declarationBinder -| wildcardBinder -| variableBinder -| castBinder -| nullAssertBinder - -binders ::= binder ( ',' binder )* ','? +groupingPattern ::= '(' pattern ')' ``` -#### Type argument binder +Like parenthesized expressions, parentheses in a pattern let you control pattern +precedence and insert a lower precedence pattern where a higher precedence one +is expected. -Certain places in a pattern where a type argument is expected also allow you to -declare a type parameter variable to destructure and capture a type argument -from the runtime type of the matched object: +### List pattern ``` -typeOrBinder ::= typeWithBinder | typePattern - -typeWithBinder ::= - 'void' - | 'Function' '?'? - | typeName typeArgumentsOrBinders? '?'? - | recordTypeWithBinder - -typeOrBinders ::= typeOrBinder (',' typeOrBinder)* +listPattern ::= typeArguments? '[' patterns ']' +``` -typeArgumentsOrBinders ::= '<' typeOrBinders '>' +A list pattern matches an object that implements `List` and extracts elements by +position from it. -typePattern ::= 'final' identifier +It is a compile-time error if `typeArguments` is present and has more than one +type argument. -// This the same as `recordType` and its related rules, but with `type` -// replaced with `typeOrBinder`. -recordTypeWithBinder ::= '(' recordTypeFieldsWithBinder ','? ')' - | '(' ( recordTypeFieldsWithBinder ',' )? - recordTypeNamedFieldsWithBinder ')' - | recordTypeNamedFieldsWithBinder +**TODO: Allow a `...` element in order to match suffixes or ignore extra +elements. Allow capturing the rest in a variable.** -recordTypeFieldsWithBinder ::= typeOrBinder ( ',' typeOrBinder )* +### Map pattern -recordTypeNamedFieldsWithBinder ::= '{' recordTypeNamedFieldWithBinder - ( ',' recordTypeNamedFieldWithBinder )* ','? '}' -recordTypeNamedFieldWithBinder ::= typeOrBinder identifier +``` +mapPattern ::= typeArguments? '{' mapPatternEntries '}' +mapPatternEntries ::= mapPatternEntry ( ',' mapPatternEntry )* ','? +mapPatternEntry ::= expression ':' pattern ``` -**TODO: Can type patterns have bounds?** +A map pattern matches values that implement `Map` and accesses values by key +from it. -The `typeOrBinder` rule is similar to the existing `type` grammar rule, but also -allows `final` followed by an identifier to declare a type variable. It allows -this at the top level of the rule and anywhere a type argument may appear inside -a nested type. For example: +It is a compile-time error if: -```dart -switch (object) { - case List: ... - case Map: ... - case Set>: ... -} -``` +* `typeArguments` is present and there are more or fewer than two type + arguments. -**TODO: Do we want to support function types? If so, how do we handle -first-class generic function types?** +* Any of the entry key expressions are not constant expressions. -#### List binder +* If any two keys in the map both have primitive `==` methods, then it is a + compile-time error if they are equal according to their `==` operator. *In + cases where keys have types whose equality can be checked at compile time, + we report errors if there are redundant keys. But we don't require the keys + to have primitive equality for flexibility. In map patterns where the keys + don't have primitive equality, it is possible to have redundant keys and the + compiler won't detect it.* -A list binder extracts elements by position from objects that implement `List`. +### Record pattern ``` -listBinder ::= ('<' typeOrBinder '>' )? '[' binders ']' +recordPattern ::= '(' patternFields ')' +patternFields ::= patternField ( ',' patternField )* ','? +patternField ::= ( identifier? ':' )? pattern ``` -**TODO: Allow a `...` element in order to match suffixes or ignore extra -elements. Allow capturing the rest in a variable.** +A record pattern matches a record object and destructures its fields. If the +value isn't a record with the same shape as the pattern, then the match fails. +Otherwise, the field subpatterns are matched against the corresponding fields in +the record. -#### Map binder +Field subpatterns can be in one of three forms: -A map binder access values by key from objects that implement `Map`. +* A bare `pattern` destructures the corresponding positional field from the + record and matches it against `pattern`. -``` -mapBinder ::= mapTypeArguments? '{' mapBinderEntries '}' +* A `identifier: pattern` destructures the named field with the name + `identifier` and matches it against `pattern`. -mapTypeArguments ::= '<' typeOrBinder ',' typeOrBinder '>' +* A `: pattern` is a named field with the name omitted. When destructuring + named fields, it's very common to want to bind the resulting value to a + variable with the same name. As a convenience, the identifier can be omitted + on a named field. In that case, the name is inferred from `pattern`. The + subpattern must be a variable pattern or cast pattern, which may be wrapped + in any number of null-check or null-assert patterns. -mapBinderEntries ::= mapBinderEntry ( ',' mapBinderEntry )* ','? -mapBinderEntry ::= expression ':' binder -``` + The field name is then inferred from the name in the variable or cast + pattern. These pairs of patterns are each equivalent: -If it is a compile-time error if any of the entry key expressions are not -constant expressions. It is a compile-time error if any of the entry key -expressions evaluate to equivalent values. + ```dart + // Variable: + (untyped: untyped, typed: int typed) + (:untyped, :int typed) -#### Record binder + // Null-check and null-assert: + (checked: checked?, asserted: asserted!) + (:checked?, :asserted!) -A record pattern destructures fields from records and objects. + // Cast: + (field: field as int) + (:field as int) + ``` -``` -recordBinder ::= '(' recordFieldBinders ')' +### Extractor pattern -recordFieldBinders ::= recordFieldBinder ( ',' recordFieldBinder )* ','? -recordFieldBinder ::= ( identifier ':' )? binder - | identifier ':' +``` +extractorPattern ::= extractorName typeArguments? '(' patternFields? ')' +extractorName ::= typeIdentifier | qualifiedName ``` -Each field is either a binder which destructures a positional field, or a binder -prefixed with an identifier and `:` which destructures a named field. - -Each named field in the record pattern is matched by calling a corresponding -getter on the matched object. A record pattern can be applied to an object of -any type with any set of named field patterns as long as the object being -matched has getters with those names. (Positional fields, however, can only be -matched by record values.) +An extractor matches values of a given named type and then extracts values from +it by calling getters on the value. Extractor patterns let users destructure +data from arbitrary objects using the getters the object's class already +exposes. -When destructuring named fields, it's common to want to bind the resulting value -to a variable with the same name. As a convenience, the binder can be omitted on -a named field. In that case, the field implicitly contains a variable binder -subpattern with the same name. These are equivalent: +This pattern is particularly useful for writing code in an algebraic datatype +style. For example: ```dart -var (first: first, second: second) = (first: 1, second: 2); -var (first:, second:) = (first: 1, second: 2); +class Rect { + final double width, height; + + Rect(this.width, this.height); +} + +display(Object obj) { + switch (obj) { + case Rect(width: w, height: h): print('Rect $w x $h'); + default: print(obj); + } +} ``` -**TODO: Allow a `...` element in order to ignore some positional fields while -capturing the suffix.** +It is a compile-time error if: -#### Wildcard binder +* `extractorName` does not refer to a type. -A wildcard binder pattern does nothing. +* A type argument list is present and does not match the arity of the type of + `extractorName`. -``` -wildcardBinder ::= "_" -``` +* A `patternField` is of the form `pattern`. Positional fields aren't allowed. -It's useful in places where you need a subpattern in order to destructure later -positional values: +As with record patterns, the getter name can be omitted in which case it is +inferred from the variable or cast pattern in the field subpattern, which may be +wrapped in null-check or null-assert patterns. The previous example could be +written like: +```dart +display(Object obj) { + switch (obj) { + case Rect(:width, :height): print('Rect $width x $height'); + default: print(obj); + } +} ``` -var list = [1, 2, 3]; -var [_, two, _] = list; -``` - -#### Variable binder -A variable binding pattern matches the value and binds it to a new variable. -These often occur as subpatterns of a destructuring pattern in order to capture -a destructured value. +## Pattern uses -``` -variableBinder ::= typeWithBinder? identifier -``` +Patterns are woven into the larger language in a few ways: -#### Cast binder +### Pattern variable declaration -A cast pattern explicitly casts the matched value to the expected type. +Places in the language where a local variable can be declared are extended to +allow a pattern, like: -``` -castBinder ::= identifier "as" type +```dart +var (a, [b, c]) = ("str", [1, 2]); ``` -This is not a type *test* that causes a match failure if the value isn't of the -tested type. This pattern can be used in irrefutable contexts to forcibly assert -the expected type of some destructured value. This isn't useful as the outermost -pattern in a declaration since you can always move the `as` to the initializer -expression: +Dart's existing C-style variable declaration syntax makes it harder to +incorporate patterns. Variables can be declared just by writing their type, and +a single declaration might declare multiple variables. Fully incorporating +patterns into that could lead to confusing syntax like: ```dart -num n = 1; -var i as int = n; // Instead of this... -var i = n as int; // ...do this. +// Not allowed: +(int, String) (n, s) = (1, "str"); +final (a, b) = (1, 2), c = 3, (d, e); ``` -But when destructuring, there is no place in the initializer to insert the cast. -This pattern lets you insert the cast as values are being pulled out by the -pattern: +To avoid this weirdness, patterns only occur in variable declarations that begin +with a `var` or `final` keyword. Also, a variable declaration using a pattern +can only have a single declaration "section". No comma-separated multiple +declarations like: ```dart -(num, Object) record = (1, "s"); -var (i as int, s as String) = record; +// Not allowed: +var [a] = [1], (b, c) = (2, 3); ``` -#### Null-assert binder +Declarations with patterns must have an initializer. This is not a limitation +since the point of using a pattern in a variable declaration is to match it +against the initializer's value. + +Add this new rule: ``` -nullAssertBinder ::= binder '!' +patternDeclaration ::= ( 'final' | 'var' ) pattern '=' expression ``` -When the type being matched or destructured is nullable and you want to assert -that the value shouldn't be null, you can use a cast pattern, but that can be -verbose if the underlying type name is long: +It is a compile-time error if the outermost pattern in a `patternDeclaration` +is not one of: -```dart -(String, Map?>) data = ... -var (name, timeStamps as Map>) = data; -``` +* [`groupingPattern`][groupingPattern] +* [`listPattern`][listPattern] +* [`mapPattern`][mapPattern] +* [`recordPattern`][recordPattern] +* [`extractorPattern`][extractorPattern] +* [`nullCheckPattern`][nullCheckPattern] -To make that easier, similar to the null-assert expression, a null-assert binder -pattern forcibly casts the matched value to its non-nullable type. If the value -is null, a runtime exception is thrown: +This allows useful code like: ```dart -(String, Map?>) data = ... -var (name, timeStamps!) = data; +var [a, b] = [1, 2]; // List. +var {1: a} = {1: 2}; // Map. +var (a, b, x: x) = (1, 2, x: 3); // Record. +var Point(x: x, y: y) = Point(1, 2); // Extractor. +if (var string? = nullableString) ... // Null-check. ``` -### Refutable patterns ("matchers") - -Refutable patterns determine if the value in question matches or meets some -predicate. This answer is used to select appropriate control flow in the -surrounding construct. Matchers can only appear in a context where control flow -can naturally handle the pattern failing to match. +But excludes other kinds of patterns to prohibit weird code like: -``` -matcher ::= - | literalMatcher - | constantMatcher - | wildcardMatcher - | listMatcher - | mapMatcher - | recordMatcher - | variableMatcher - | extractMatcher - | nullCheckMatcher +```dart +// Not allowed: +var String str = 'redundant'; // Variable. +var str as String = 'weird'; // Cast. +var definitely! = maybe; // Null-assert. +var (pointless) = 'parentheses'; // Grouping. ``` -#### Literal matcher +**TODO: Should we support destructuring in `const` declarations?** -A literal pattern determines if the value is equivalent to the given literal -value. +This new rule is incorporated into the existing rules for declaring variables +like so: ``` -literalMatcher ::= - | booleanLiteral - | nullLiteral - | numericLiteral - | stringLiteral -``` - -Note that list and map literals are not in here. Instead there are list and map -*patterns*. - -**Breaking change**: Using matcher patterns in switch cases means that a list or -map literal in a switch case is now interpreted as a list or map pattern which -destructures its elements at runtime. Before, it was simply treated as identity -comparison. - -```dart -const a = 1; -const b = 2; -var obj = [1, 2]; // Not const. +localVariableDeclaration ::= + | initializedVariableDeclaration ';' // Existing. + | patternDeclaration ';' // New. -switch (obj) { - case [a, b]: print("match"); break; - default: print("no match"); -} +forLoopParts ::= + | // Existing productions... + | patternDeclaration 'in' expression // New. ``` -In Dart today, this prints "no match". With this proposal, it changes to -"match". However, looking at the 22,943 switch cases in 1,000 pub packages -(10,599,303 lines in 34,917 files), I found zero case expressions that were -collection literals. +*This could potentially be extended to allow patterns in top-level variables and +static fields but laziness makes that more complex. We could support patterns in +instance field declarations, but constructor initializer lists make that harder. +Parameter lists are a natural place to allow patterns, but the existing grammar +complexity of parameter lists—optional parameters, named parameters, +required parameters, default values, etc.—make that very hard. For the +initial proposal, we focus on patterns only in variables with local scope.* -#### Constant matcher +### Switch statement -Like literals, references to constants determine if the matched value is equal -to the constant's value. +We extend switch statements to allow patterns in cases: ``` -constantMatcher ::= qualified ( "." identifier )? +switchStatement ::= 'switch' '(' expression ')' '{' switchCase* defaultCase? '}' +switchCase ::= label* caseHead ':' statements +caseHead ::= 'case' pattern ( 'when' expression )? ``` -The expression is syntactically restricted to be either: +Allowing patterns in cases significantly increases the expressiveness of what +properties a case can verify, including executing arbitrary user-defined code. +This implies that the order that cases are checked is now potentially +user-visible and an implementation must execute the *first* case that matches. -* **A bare identifier.** In this case, the identifier must resolve to a - constant declaration in scope. +#### Guard clause -* **A prefixed or qualified identifier.** In other words, `a.b`. It must - resolve to either a top level constant imported from a library with a - prefix, a static constant in a class, or an enum case. +We also allow an optional *guard clause* to appear after a case. This enables a +switch case to evaluate an arbitrary predicate after matching. Guards are useful +because when the predicate evaluates to false, execution proceeds to the next +case instead of exiting the entire switch like it would if you nested an `if` +statement inside the switch case's body: -* **A prefixed qualified identifier.** Like `a.B.c`. It must resolve to an - enum case on an enum type that was imported with a prefix. +```dart +var pair = (1, 2); -To avoid ambiguity with wildcard matchers, the identifier cannot be `_`. +// This prints nothing: +switch (pair) { + case (a, b): + if (a > b) print('First element greater'); + break; + case (a, b): + print('Other order'); + break; +} -**TODO: Do we want to allow other kinds of constant expressions like `1 + 2`?** +// This prints "Other order": +switch (pair) { + case (a, b) when a > b: + print('First element greater'); + break; + case (a, b): + print('Other order'); + break; +} +``` -#### Wildcard matcher +**TODO: Consider using `if (...)` instead of `when ...` for guards so that +we don't need a contextual keyword.** -A wildcard pattern always matches. +#### Implicit break -``` -wildcardMatcher ::= "_" -``` +A long-running annoyance with switch statements is the mandatory `break` +statements at the end of each case body. Dart does not allow fallthrough, so +these `break` statements have no real effect. They exist so that Dart code does +not *appear* to be doing fallthrough to users coming from languages like C that +do allow it. That is a high syntactic tax for limited benefit. -**TODO: Consider giving this an optional type annotation to enable matching a -value of a specific type without binding it to a variable.** +I inspected the 25,014 switch cases in the most recent 1,000 packages on pub +(10,599,303 LOC). 26.40% of the statements in them are `break`. 28.960% of the +cases contain only a *single* statement followed by a `break`. This means +`break` is a fairly large fraction of the statements in all switches even though +it does nothing. -This is useful in places where a subpattern is required but you always want it -to succeed. It can function as a "default" pattern for the last case in a -pattern matching statement. +Therefore, this proposal removes the requirement that each non-empty case body +definitely exit. Instead, a non-empty case body implicitly jumps to the end of +the switch after completion. From the spec, remove: -#### List matcher +> If *s* is a non-empty block statement, let *s* instead be the last statement +> of the block statement. It is a compile-time error if *s* is not a `break`, +> `continue`, `rethrow` or `return` statement or an expression statement where +> the expression is a `throw` expression. -Matches objects of type `List` with the right length and destructures their -elements. +This is now valid code that prints "one": -``` -listMatcher ::= ('<' typeOrBinder '>' )? '[' matchers ']' +```dart +switch (1) { + case 1: + print("one"); + case 2: + print("two"); +} ``` -**TODO: Allow a `...` element in order to match suffixes or ignore extra -elements. Allow capturing the rest in a variable.** +Empty cases continue to fallthrough to the next case as before. This prints "one +or two": -#### Map matcher +```dart +switch (1) { + case 1: + case 2: + print("one or two"); +} +``` -Matches objects of type `Map` and destructures their entries. +### Switch expression -``` -mapMatcher ::= mapTypeArguments? '{' mapMatcherEntries '}' +When you want an `if` statement in an expression context, you can use a +conditional expression (`?:`). There is no expression form for multi-way +branching, so we define a new switch expression. It takes code like this: -mapMatcherEntries ::= mapMatcherEntry ( ',' mapMatcherEntry )* ','? -mapMatcherEntry ::= expression ':' matcher +```dart +Color shiftHue(Color color) { + switch (color) { + case Color.red: + return Color.orange; + case Color.orange: + return Color.yellow; + case Color.yellow: + return Color.green; + case Color.green: + return Color.blue; + case Color.blue: + return Color.purple; + case Color.purple: + return Color.red; + } +} ``` -If it is a compile-time error if any of the entry key expressions are not -constant expressions. It is a compile-time error if any of the entry key -expressions evaluate to equivalent values. +And turns it into: -#### Record matcher +```dart +Color shiftHue(Color color) { + return switch (color) { + case Color.red => Color.orange; + case Color.orange => Color.yellow; + case Color.yellow => Color.green; + case Color.green => Color.blue; + case Color.blue => Color.purple; + case Color.purple => Color.red; + }; +} +``` -Destructures fields from records and objects. +The grammar is: ``` -recordMatcher ::= '(' recordFieldMatchers ')' +primary ::= // Existing productions... + | switchExpression -recordFieldMatchers ::= recordFieldMatcher ( ',' recordFieldMatcher )* ','? -recordFieldMatcher ::= ( identifier ':' )? matcher - | identifier ':' +switchExpression ::= 'switch' '(' expression ')' '{' + switchExpressionCase* defaultExpressionCase? '}' +switchExpressionCase ::= caseHead '=>' expression ';' +defaultExpressionCase ::= 'default' '=>' expression ';' ``` -Each field is either a positional matcher which destructures a positional field, -or a matcher prefixed with an identifier and `:` which destructures a named -field. +Slotting into `primary` means it can be used anywhere any expression can appear, +even as operands to unary and binary operators. Many of these uses are ugly, but +not any more problematic than using a collection literal in the same context +since a `switch` expression is always delimited by a `switch` and `}`. -As with record binders, a named field without a matcher is implicitly treated as -containing a variable matcher with the same name as the field. The variable is -always `final`. These cases are equivalent: +Making it high precedence allows useful patterns like: ```dart -switch (obj) { - case (first: final first, second: final second): ... - case (first:, second:): ... -} -``` - -**TODO: Add a `...` syntax to allow ignoring positional fields?** - -#### Variable matcher - -A variable matcher lets a matching pattern also perform variable binding. +await switch (n) { + case 1 => aFuture; + case 2 => anotherFuture; + default => otherwiseFuture; +}; -``` -variableMatcher ::= ( 'final' | 'var' | 'final'? typeWithBinder ) identifier +var x = switch (n) { + case 1 => obj; + case 2 => another; + default => otherwise; +}.someMethod(); ``` -By using variable matchers as subpatterns of a larger matched pattern, a single -composite pattern can validate some condition and then bind one or more -variables only when that condition holds. - -A variable pattern can also have a type annotation in order to only match values -of the specified type. +Over half of the switch cases in a large corpus of packages contain either a +single return statement or an assignment followed by a break so there is some +evidence this will be useful. -#### Extractor matcher +#### Expression statement ambiguity -An extractor combines a type test and record destructuring. It matches if the -object has the named type. If so, it then uses the following record pattern to -destructure fields on the value as that type. This pattern is particularly -useful for writing code in an algebraic datatype style. For example: +Thanks to expression statements, a switch expression could appear in the same +position as a switch statement. This isn't technically ambiguous, but requires +unbounded lookahead to tell if a switch in statement position is a statement or +expression. ```dart -class Rect { - final double width, height; - - Rect(this.width, this.height); -} +main() { + switch (some(extremely, long, expression, here)) { + case Some(Quite(var long, var pattern)) => expression(); + }; -display(Object obj) { - switch (obj) { - case Rect(width:, height:): - print('Rect $width x $height'); - case _: - print(obj); + switch (some(extremely, long, expression, here)) { + case Some(Quite(var long, var pattern)) : statement(); } } ``` -You can also use an extractor to both match an enum value and destructure -fields from it: +To avoid that, we disallow a switch expression from appearing at the beginning +of an expression statement. This is similar to existing restrictions on map +literals appearing in expression statements. In the rare case where a user +really wants one there, they can parenthesize it. + +**TODO: If we change switch expressions [to use `:` instead of `=>`][2126] then +there will be an actual ambiguity. In that case, reword the above section.** -```dart -enum Severity { - error(1, "Error"), - warning(2, "Warning"); +[2126]: https://github.com/dart-lang/language/issues/2126 - Severity(this.level, this.prefix); +### Pattern-if statement - final int level; - final String prefix; -} +Often you want to conditionally match and destructure some data, but you only +want to test a value against a single pattern. You can use a `switch` statement +for that, but it's pretty verbose: -log(Severity severity, String message) { - switch (severity) { - case Severity.error(prefix: final prefix): - print('!! $prefix !! $message'.toUppercase()); - case Severity.warning(prefix: final prefix): - print('$prefix: $message'); - } +```dart +switch (json) { + case [int x, int y]: + return Point(x, y); } ``` -The grammar is: +We can make simple uses like this a little cleaner by allowing a pattern +variable declaration in place of an if condition: +```dart +if (var [int x, int y] = json) return Point(x, y); ``` -extractMatcher ::= extractName typeArgumentsOrBinders? "(" recordFieldMatchers ")" -extractName ::= typeIdentifier | qualifiedName -``` - -It requires the type to be a named type. If you want to use an extractor with a -function type, you can use a typedef. -It is a compile-time error if `extractName` does not refer to a type or enum -value. It is a compile-time error if a type argument list is present and does -not match the arity of the type of `extractName`. - -As with record matchers, a named field without a matcher is implicitly treated -as containing a variable matcher with the same name as the field. The variable -is always `final`. The previous example could be written like: +It may have an else branch as well: ```dart -log(Severity severity, String message) { - switch (severity) { - case Severity.error(prefix:): - print('!! $prefix !! $message'.toUppercase()); - case Severity.warning(prefix:): - print('$prefix: $message'); - } +if (var [int x, int y] = json) { + print('Was coordinate array $x,$y'); +} else { + throw FormatException('Invalid JSON.'); } ``` -#### Null-check matcher - -Similar to the null-assert binder, a null-check matcher provides a nicer syntax -for working with nullable values. Where a null-assert binder *throws* if the -matched value is null, a null-check matcher simply fails the match. To highlight -the difference, it uses a gentler `?` syntax, like the [similar feature in -Swift][swift null check]: - -[swift null check]: https://docs.swift.org/swift-book/ReferenceManual/Patterns.html#ID520 +We replace the existing `ifStatement` rule with: ``` -nullCheckMatcher ::= matcher '?' +ifStatement ::= 'if' '(' ifCondition ')' statement ('else' statement)? + +ifCondition ::= expression // Existing if statement condition. + | patternDeclaration + | type identifier '=' expression ``` -A null-check pattern matches if the value is not null, and then matches the -inner pattern against that same value. Because of how type inference flows -through patterns, this also provides a terse way to bind a variable whose type -is the non-nullable base type of the nullable value being matched: +**TODO: Allow patterns in if elements too.** + +When the `ifCondition` is an `expression`, it behaves as it does today. If the +condition is a `patternDeclaration`, then the expression is evaluated and +matched against the pattern. If it matches, then branch is executed with any +variables the pattern defines in scope. Otherwise, the else branch is executed +if there is one. The third form of `ifCondition` allows simple typed variable +declarations inside the condition: ```dart -String? maybeString = ... -if (maybeString case var s?) { - // s has type String here. -} +num n = ... +if (int i = n) print('$n is an integer $i'); ``` +This behaves like a typed variable pattern. *We don't allow a typed variable +pattern to appear in `patternDeclaration` to avoid a redundant `var int x` +syntax.* + +Unlike `switch`, the pattern-if statement doesn't allow a guard clause. Guards +are important in switch cases because, unlike nesting an if statement *inside* +the switch case, a failed guard will continue to try later cases in the switch. +That is less important here since the only other case is the else branch. + ## Static semantics ### Type inference @@ -1192,16 +1205,13 @@ To orchestrate this, type inference on patterns proceeds in three phases: holes in the type schema. When that completes, we now have a full static type for the pattern and all of its subpatterns. -The full process only comes into play for pattern variable declarations. For -switch case, and if-case statements, there is no downwards inference from -pattern to value and the first step is skipped. Instead, the type of the matched -value is inferred with no downwards context type and we jump straight to -inferring the types of the case patterns from that context type. *The intent of -a matcher pattern is to query the type of the matched value, so it would be -strange if that query affected the value expression.* - -When calculating the context type schema or static type of a pattern, any -occurrence of `typePattern` in a type is treated as `Object?`. +The full process only comes into play for pattern variable declarations and +pattern-if statements. For switch cases, there is a separate pattern for each +case and deciding how to unify those into a single downwards inference context +for the value could be challenging. It's not clear that doing that would even be +helpful for users. Instead, for switch statements and expressions, the type of +the matched value is inferred with no downwards context type and we jump +straight to inferring the types of the case patterns from that context type. #### Pattern context type schema @@ -1218,8 +1228,10 @@ Patterns extend this behavior: var (List list, [a]) = ([], [1]); // Infer ([], []). ``` -To support this, every pattern has a context type schema. This is a type -*schema* because there may be holes in the type: +To support this, every pattern has a context type schema which is used as the +downwards inference context on the matched value expression in pattern variable +declarations and pattern-if statements. This is a type *schema* because there +may be holes in the type: ```dart var (a, int b) = ... // Schema is `(?, int)`. @@ -1227,145 +1239,192 @@ var (a, int b) = ... // Schema is `(?, int)`. The context type schema for a pattern `p` is: -* **List binder or matcher**: A type schema `List` where: - * If `p` has a type argument, then `E` is the type argument. - * Else `E` is the greatest lower bound of the type schemas of all element - subpatterns. +* **Logical-or**: The least upper bound of the context type schemas of the + branches. -* **Map binder or matcher**: A type schema `Map` where: - * If `p` has type arguments then `K`, and `V` are those type arguments. - * Else `K` is the least upper bound of the types of all key expressions - and `V` is the greatest lower bound of the context type schemas of all - value subpatterns. +* **Logical-and**: The greatest lower bound of the context type schemas of the + branches. -* **Record binder or matcher**: A record type scheme with positional and named - fields corresponding to the type schemas of the corresponding field - subpatterns. If a named field uses the shorthand syntax to infer a variable - subpattern with the same name as the field, then the type schema is `?` for - that field. + **TODO: Figure out if LUB and GLB are defined for type schemas.** - *Note that the type schema will be a record type even when the matched value - type isn't a record, as in:* +* **Null-check** or **null-assert**: A context type schema `E?` where `E` is + the context type schema of the inner pattern. *For example:* ```dart - var p = Point(x: 1, y: 2); - var (x: x, y: y) = p; + var [[int x]!] = [[]]; // Infers List?> for the list literal. ``` - *The record type schema inferred from the pattern may be used to infer - record field types on the initialized value if the value is a record - literal, and may otherwise be captured. But if the initializer type isn't a - record, then the record type context schema inferred from the pattern ends - up being ignored.* +* **Literal** or **constant**: The context type schema is the static type of + the pattern's constant value expression. -* **Variable matcher**: - * If `p` has a type annotation, the context type schema is `Object?`. - *It is not the annotated type because a variable matching pattern can - be used to downcast from any other type.* - * Else it is `?`. +* **Variable**: -* **Cast binder**, **variable binder**, **wildcard matcher**, or **extractor - matcher**: The context type schema is `Object?`. + 1. If `p` has no type annotation, the context type schema is `?`. + *This lets us potentially infer the variable's type from the matched + value.* - **TODO: Should type arguments on an extractor create a type argument - constraint?** + 2. Else the context type schema is the annotated type. *When a typed + variable pattern is used in a destructuring variable declaration, we + do push the type over to the value for inference, as in:* -* **Null-assert binder** or **null-check matcher**: A type schema `E?` where - `E` is the type schema of the inner pattern. *For example:* + ```dart + var (items: List x) = (items: []); + // ^- Infers List. + ``` - ```dart - var [[int x]!] = [[]]; // Infers List?> for the list literal. - ``` +* **Relational** or **cast**: The context type schema is `Object?`. -* **Literal matcher** or **constant matcher**: The context type schema is the - static type of the pattern's constant value expression. +* **Grouping**: The context type schema of the inner subpattern. -*We use the greatest lower bound for list elements and map values to ensure that -the outer collection type has a precise enough type to ensure that any typed -field subpatterns do not need to downcast:* +* **List**: A context type schema `List` where: -```dart -var [int a, num b] = [1, 2]; -``` + 1. If `p` has a type argument, then `E` is the type argument. + + 2. Else `E` is the greatest lower bound of the type schemas of all element + subpatterns. *We use the greatest lower bound to ensure that the outer + collection type has a precise enough type to ensure that any typed field + subpatterns do not need to downcast:* + + ```dart + var [int a, num b] = [1, 2]; + ``` + + *Here, the GLB of `int` and `num` is `int`, which ensures that neither + `int a` nor `num b` need to downcast their respective fields.* + +* **Map**: A type schema `Map` where: -*Here, the GLB of `int` and `num` is `int`, which ensures that neither `int a` -nor `num b` need to downcast their respective fields.* + 1. If `p` has type arguments then `K`, and `V` are those type arguments. + + 2. Else `K` is the least upper bound of the types of all key expressions + and `V` is the greatest lower bound of the context type schemas of all + value subpatterns. + +* **Record**: A record type schema with positional and named fields + corresponding to the type schemas of the corresponding field subpatterns. + +* **Extractor**: The type the extractor name resolves to. *This lets inference + fill in type arguments in the value based on the extractor's type arguments, + as in:* + + ```dart + var Foo() = Foo(); + // ^-- Infer Foo. + ``` -#### Pattern static type +#### Type checking and pattern static type Once the value a pattern is matched against has a static type (which means downwards inference on it using the pattern's context type schema is complete), -we can calculate the static type of the pattern. - -The value's static type is used to do upwards type inference on the pattern for -patterns in variable declarations and switches. Also, a pattern's static type -may be used for "downwards" ("inwards"?) inference of a pattern's subpatterns -in the same way that a collection literal's type argument is used for inference -on the collection's elements. +we can type check the pattern. We also calculate a static type for the patterns +that only match certain types: null-check, variable, list, map, record, and +extractor. Some examples and the corresponding pattern static types: ```dart var [a, b] = [1, 2]; // List (and compile error). var [a, b] = [1, 2]; // List, a is num, b is num. -var [int a, b] = [1, 2]; // List. +var [int a, b] = [1, 2]; // List. ``` -Putting this together, it means the process of completely inferring the types of -a construct using patterns works like: +To type check a pattern `p` being matched against a value of type `M`: + +* **Logical-or** and **logical-and**: Type check each branch using `M` as the + matched value type. + +* **Relational**: If the operator is a comparison (`<`, `<=`, `>`, or `>=`), + then it is a compile-time error if `M` does not define that operator, if the + type of the constant in the relational pattern is not a subtype of the + operator's parameter type, or if the operator's return type is not `bool`. + *The `==` and `!=` operators are valid for all pairs of types.* + +* **Null-check** or **null-assert**: + + 1. Let `N` be [**NonNull**][nonnull](`M`). + + 2. Type-check the subpattern using `N` as the matched value type. + + 3. If `p` is a null-check pattern, then the static type of `p` is `N`. + + [nonnull]: https://github.com/dart-lang/language/blob/master/accepted/2.12/nnbd/feature-specification.md#null-promotion + +* **Literal** or **constant**: Type check the pattern's value expression in + context type `M`. *The context type comes into play for things like type + arguments and int-to-double:* + + ```dart + double d = 1.0; + switch (d) { + case 1: ... + } + ``` + + *Here, the `1` literal pattern in the case is inferred in a context type of + `double` to be `1.0` and so does match.* + +* **Variable**: + + 1. If the variable has a type annotation, the type of `p` is that type. + + 2. Else the type of `p` is `M`. *This means that an untyped variable + pattern can have its type indirectly inferred from the type of a + superpattern:* + + ```dart + var <(num, Object)>[(a, b)] = [(1, true)]; // a is num, b is Object. + ``` + + *The pattern's context type schema is `List<(num, Object>)`. Downwards + inference uses that to infer `List<(num, Object>)` for the initializer. + That inferred type is then destructured and used to infer `num` for `a` + and `Object` for `b`.* -1. Calculate the context type schema of the pattern. -2. Use that in downwards inference to calculate the type of the matched value. -3. Use that to calculate the static type of the pattern. +* **Cast**: Nothing to do. -The static type of a pattern `p` being matched against a value of type `M` is: +* **Grouping**: Type-check the inner subpattern using `M` as the matched value + type. -* **List binder or matcher**: +* **List**: 1. Calculate the value's element type `E`: + 1. If `M` implements `List` for some `T` then `E` is `T`. + 2. Else if `M` is `dynamic` then `E` is `dynamic`. - 3. Else compile-time error. *It is an error to destructure a non-list - value with a list pattern.* - 2. Calculate the static types of each element subpattern using `E` as the - matched value type. *Note that we calculate a single element type and - use it for all subpatterns. In:* + 3. Else `E` is `Object?`. + + 2. Type-check each element subpattern using `E` as the matched value type. + *Note that we calculate a single element type and use it for all + subpatterns. In:* ```dart - var [a, b] = [1, bool]; + var [a, b] = [1, 2.3]; ``` - *both `a` and `b` use `Object` as their matched value type.* + *both `a` and `b` use `num` as their matched value type.* 3. The static type of `p` is `List` where: + 1. If `p` has a type argument, `S` is that type. *If the list pattern has an explicit type argument, that wins.* - 2. Else if the greatest lower bound of the types of the element - subpatterns is not `?`, then `S` is that type. *Otherwise, if we - can infer a type bottom-up from the from the subpatterns, use that.* - 3. Else `S` is `E`. *Otherwise, infer the type from the matched value.* - 4. It is a compile-time error if the list pattern is a binder and any - element subpattern's type is not a supertype of `S`. *This ensures an - element binder subpattern does not need to downcast an element from the - matched value. For example:* - - ```dart - var [int i] = [1.2]; // Compile-time error. - ``` + 2. Else `S` is `E`. *Otherwise, infer the type from the matched value.* -* **Map binder or matcher**: +* **Map**: 1. Calculate the value's entry key type `K` and value type `V`: + 1. If `M` implements `Map` for some `K` and `V` then use those. + 2. Else if `M` is `dynamic` then `K` and `V` are `dynamic`. - 3. Else compile-time error. *It is an error to destructure a non-map - value with a map pattern.* - 2. Calculate the static types of each value subpattern using `V` as the - matched value type. *Like lists, we calculate a single value type and - use it for all value subpatterns:* + 3. Else `K` and `V` are `Object?`. + + 2. Type-check each value subpattern using `V` as the matched value type. + *Like lists, we calculate a single value type and use it for all value + subpatterns:* ```dart var {1: a, 2: b} = {1: "str", 2: bool}; @@ -1374,190 +1433,165 @@ The static type of a pattern `p` being matched against a value of type `M` is: *Here, both `a` and `b` use `Object` as the matched value type.* 3. The static type of `p` is `Map` where: + 1. If `p` has type arguments, `L` and `W` are those type arguments. *If the map pattern is explicitly typed, that wins.* - 2. Else `L` is the least upper bound of the types of all key - expressions. If the greatest lower bound of all value subpattern - types is not `?` then `W` is that type. Otherwise `W` is `V`. - 4. It is a compile-time error if the map pattern is a binder and any value - subpattern's type is not a supertype of `W`. *This ensures a value - binder subpattern does not need to downcast an entry from the matched - value. For example:* + 2. Else `L` is `K` and `W` is `V`. - ```dart - var {1: String s} = {1: false}; // Compile-time error. - ``` +* **Record**: -* **Record binder or matcher**: + 1. Type-check each of `f`'s positional field subpatterns using the + corresponding positional field type on `M` as the matched value type or + `Object?` if `M` is not a record type with the corresponding field. *The + field subpattern will only be matched at runtime if the value does turn + out to be a record with the right shape where the field is present, so + it's safe to just assume the field exists when type checking here.* - 1. Calculate the static types of the field subpatterns: + 2. Type check each of `f`'s named field subpatterns using the type of the + corresponding named field on `M` as the matched value type or `Object?` + if `M` is not a record type with the corresponding field. - 1. Calculate the type of each of `f`'s positional field subpatterns - using the corresponding positional field type on `M` as the matched - value type. It is a compile-time error if there are positional - fields, `M` is not `dynamic`, and `M` is not a record with the same - number of positional fields. + 3. The static type of `p` is a record type with the same shape as `p` and + `Object?` for all fields. *If the matched value's type is `dynamic` or + some record supertype like `Object`, then the record pattern should + match any record with the right shape and then delegate to its field + subpatterns to ensure that the fields match.* - 1. Calculate the type of each of `f`'s named field subpatterns using - the return type of the getter on `M` with the same name as the field - as the matched value type. If `M` is `dynamic`, then use `dynamic` - as the matched value type. It is a compile-time error if `M` is not - `dynamic` and does not have a getter whose name matches the - subpattern's field name. +* **Extractor**: - If a named field uses the shorthand syntax to infer a variable - subpattern with the same name as the field, then calculate the - static type using that inferred variable pattern. + 1. Resolve the extractor name to a type `X`. It is a compile-time error if + the name does not refer to a type. Apply downwards inference from `M` + to infer type arguments for `X` if needed. - 1. If `M` is a record type (of any shape) or `dynamic`, then the static - type of `p` is a record type whose fields are the fields of `p` with the - types of the corresponding subpatterns of `p`. *If the matched value is - a record, then we want to ensure that the record pattern has the same - shape. We infer a record type from the pattern's fields, then it will be - a compile-time error if that inferred record type is not a subtype of - the matched value's record type.* + 1. Type-check each of `f`'s field subpatterns using the type of the getter + on `X` with the same name as the field as the matched value type. It is + a compile-time error if `X` does not have a getter whose name matches + the subpattern's field name. - 2. Otherwise, the static type of `p` is `Object?`. *Record patterns are - structural and can be applied to values of types that aren't records as - long as the type has the right getters.* + 2. The static type of `p` is `X`. - 3. It is a compile-time error if `p` has positional fields and `M` is not - a record type or `dynamic`. *Only records support positional field - destructuring.* +It is a compile-time error if the type of an expression in a guard clause is not +`bool` or `dynamic`. -* **Variable binder**: +## Refutable and irrefutable patterns - 1. If the variable has a type annotation, the type of `p` is that type. +Patterns appear inside a number of other constructs in the language. This +proposal extends Dart to allow patterns in: - 2. Else the type of `p` is `M`. *This means that an untyped variable - pattern can have its type indirectly inferred from the type of a - superpattern:* +* Local variable declarations. +* For loop variable declarations. +* Switch statement cases. +* A new switch expression form's cases. +* A new pattern-if statement. - ```dart - var <(num, Object)>[(a, b)] = [(1, true)]; // a is num, b is Object. - ``` +When a pattern appears in a switch case, any variables bound by the pattern are +only in scope in that case's body. If the pattern fails to match, the case body +is skipped. This ensures that the variables can't be used when the pattern +failed to match and they have no defined value. Likewise, the variables bound by +a pattern-if statement's pattern are only in scope in the then branch. That +branch is skipped if the pattern fails to match. - *The pattern's context type schema is `List<(num, Object>)`. Downwards - inference uses that to infer `List<(num, Object>)` for the initializer. - That inferred type is then destructured and used to infer `num` for `a` - and `Object` for `b`.* +The other places patterns can appear are various kinds of variable declarations, +like: -* **Cast binder**, **wildcard binder**, or **wildcard matcher**: The static - type of `p` is `Object?`. *Wildcards accept all types. Casts exist to check - types at runtime, so statically accept all types.* +```dart +main() { + var (a, b) = (1, 2); + print(a + b); +} +``` -* **Null-assert binder** or **null-check matcher**: +Variable declarations have no natural control flow attached to them, so what +happens if the pattern fails to match? What happens when `a` is printed in the +example above? - 1. If `M` is `N?` for some type `N` then calculate the static type `q` of - the inner pattern using `N` as the matched value type. Otherwise, - calculate `q` using `M` as the matched value type. *A null-assert or - null-check pattern removes the nullability of the type it matches - against.* +To avoid that, we restrict which patterns can be used in variable declarations. +Only *irrefutable* patterns that never fail to match are allowed in contexts +where match failure can't be handled. For example, this is an error: - ```dart - var [x!] = []; // x is int. - ``` +```dart +main() { + var (== 2, == 3) = (1, 2); +} +``` - 2. The static type of `p` is `q?`. *The intent of `!` and `?` is only to - remove nullability and not cast from an arbitrary type, so they accept a - value of its nullable base type, and not simply `Object?`.* +We define an *irrefutable context* as the pattern in a +`localVariableDeclaration`, `forLoopParts` or its subpatterns. A *refutable +context* is the pattern in a `caseHead` or `ifCondition` or its subpatterns. -* **Literal matcher** or **constant matcher**: The static type of `p` is the - static type of the pattern's value expression. +Refutability is not just a property of the pattern itself. It also depends on +the static type of the value being matched. Consider: -* **Extractor matcher**: +```dart +irrefutable((int, int) obj) { + var (a, b) = obj; +} - 1. Resolve the extractor name to declaration `X`. It is a compile-time - error if `X` does not refer to a type. +refutable(Object obj) { + var (a, b) = obj; +} +``` - 2. Calculate the static types of each field subpattern as if `p` were a - record pattern using `X` as `M`. *An extractor pattern matches a type - then recurses into it, so we infer the fields of the subpatterns using - that matched type.* +In the first function, the `(a, b)` pattern will always successfully destructure +the record because `obj` is known to be a record type of the right shape. But in +the second function, `obj` may fail to match because the value may not be a +record. *This implies that we can't determine whether a pattern in a variable +declaration is incorrectly refutable until after type checking.* - 2. The static type of `p` is `Object?`. *Extractors exist to check types at - runtime, so statically accept all types.* +Refutability of a pattern `p` matching a value of type `v` is: -It is a compile-time error if `M` is not a subtype of `p`. +* **Logical-or**, **logical-and**, **grouping**, **null-assert**, or **cast**: + Always irrefutable (though may contain refutable subpatterns). -It is a compile-time error if the type of an expression in a guard clause is not -`bool` or `dynamic`. +* **Relational**, **literal**, or **constant**: Always refutable. -### Variables and scope +* **Null-check**, **variable**, **list**, **map**, **record**, or + **extractor**: Irrefutable if `v` is assignable to the static type of `p`. + *If `p` is a variable pattern with no type annotation, the type is inferred + from `v`, so it is never refutable.* -Patterns often exist to introduce new bindings. Type patterns introduce type -variables and other patterns introduce normal variables. +It is a compile-time error if a refutable pattern appears in an irrefutable +context, either as the outermost pattern or a subpattern. *This means that the +explicit predicate patterns like constants and literals can never appear in +pattern variable declarations. The patterns that do type tests directly or +implicitly can appear in variable declarations only if the tested type is a +supertype of the value type. In other words, any pattern that needs to +"downcast" to match is refutable.* -Consistent with wildcard patterns, any time a pattern contains an identifier -that would introduce a binding, no binding is created if the identifier is `_`. +### Variables and scope -*We always treat `_` as non-binding in patterns. It's sometimes useful to have -patterns that aren't wildcards but still don't want to bind. For example, a -variable matcher with a type annotation and `_` as its name is a useful pattern -for testing the type of a value.* +Patterns often exist to introduce new variable bindings. A "wildcard" identifier +named `_` in a variable or cast pattern never introduces a binding. The variables a patterns binds depend on what kind of pattern it is: -* **Type pattern**: Type argument patterns (i.e. `typePattern` in the grammar) - that appear anywhere in some other pattern introduce new *type* variables - whose name is the type pattern's identifier. Type variables are always - final. - -* **List binder or matcher**, **map binder or matcher**, or **record binder or - matcher**: These do not introduce variables themselves but may contain type - patterns and subpatterns that do. A named record field with no subpattern - implicitly defines a variable with the same name as the field. If the - pattern is a matcher, the variable is `final`. +* **Logical-or**: Does not introduce variables but may contain subpatterns + that do. If it a compile-time error if the two subpatterns do not introduce + the same variables with the same names and types. -* **Literal matcher**, **constant matcher**, or **wildcard binder or - matcher**: These do not introduce any variables. +* **Logical-and**, **null-check**, **null-assert**, **grouping**, **list**, + **map**, **record**, or **extractor**: These do not introduce variables + themselves but may contain subpatterns that do. -* **Variable binder**: May contain type argument patterns. Introduces a - variable whose name is the pattern's identifier. The variable is final if - the surrounding pattern variable declaration or declaration matcher has a - `final` modifier. The variable is late if it is inside a pattern variable - declaration marked `late`. +* **Relational**, **literal**, or **constant**: These do not introduce any + variables. -* **Variable matcher**: May contain type argument patterns. Introduces a +* **Variable** or **cast**: May contain type argument patterns. Introduces a variable whose name is the pattern's identifier. The variable is final if - the pattern has a `final` modifier, otherwise it is assignable *(annotated - with `var` or just a type annotation)*. The variable is never late. - -* **Cast binder**: Introduces a variable whose name is the pattern's - identifier. The variable is final if the surrounding pattern variable - declaration or declaration matcher has a `final` modifier. The variable is - late if it is inside a pattern variable declaration marked `late`. - -* **Null-assert binder** or **null-check matcher**: Introduces all of the - variables of its subpattern. - -* **Extractor matcher**: May contain type argument patterns and introduces all - of the variables of its subpatterns. A named field with no subpattern - implicitly defines a `final` variable with the same name as the field. - -All variables (except for type variables) declared in an instance field pattern -variable declaration are covariant if the pattern variable declaration is marked -`covariant`. Variables declared in a field pattern declaration define getters -on the surrounding class and setters if the field pattern declaration is not -`final`. + the surrounding pattern variable declaration has a `final` modifier. The scope where a pattern's variables are declared depends on the construct that contains the pattern: -* **Top-level pattern variable declaration**: The top-level library scope. * **Local pattern variable declaration**: The rest of the block following the declaration. * **For loop pattern variable declaration**: The body of the loop and the condition and increment clauses in a C-style for loop. -* **Static field pattern variable declaration**: The static scope of the - enclosing class. -* **Instance field pattern variable declaration**: The instance scope of the - enclosing class. * **Switch statement case**: The guard clause and the statements of the subsequent non-empty case body. * **Switch expression case**: The guard clause and the case expression. -* **If-case statement**: The then statement. +* **Pattern-if statement**: The then statement. Multiple switch case patterns may share the same variable scope if their case bodies are empty: @@ -1580,7 +1614,7 @@ body do not all define the exact same variables with the exact same types. *Aside from this special case, note that since all variables declared by a pattern and its subpattern go into the same scope, it is an error if two -subpatterns declare a variable with the same name.* +subpatterns declare a variable with the same name, unless the name is `_`.* ### Type promotion @@ -1589,14 +1623,12 @@ type.** ### Exhaustiveness and reachability -A switch is *exhaustive* if all possible values of the matched value's type -will definitely match at least one case, or there is a default case. Dart -currently shows a warning if a switch statement on an enum type does not have -cases for all enum values (or a default). - -This is helpful for code maintainance: when you add a new value to an enum -type, the language shows you every switch statement that may need a new case -to handle it. +A switch is *exhaustive* if all possible values of the matched value's type will +definitely match at least one case, or there is a default case. Dart currently +shows a warning if a switch statement on an enum type does not have cases for +all enum values (or a default). This is helpful for code maintainance: when you +add a new value to an enum type, the language shows you every switch statement +that may need a new case to handle it. This checking is even more important with this proposal. Exhaustiveness checking is a key part of maintaining code written in an algebraic datatype style. It's @@ -1667,11 +1699,11 @@ behavior. expression after it and yield that as the result of the entire switch expression. -#### If-case statement +#### Pattern-if statement 1. Evaluate the `expression` producing `v`. -2. Match the `matcher` pattern against `v`. +2. Match the `pattern` against `v`. 3. If the match succeeds, evaluate the then `statement`. Otherwise, if there is an `else` clause, evaluate the else `statement`. @@ -1682,162 +1714,271 @@ At runtime, a pattern is matched against a value. This determines whether or not the match *fails* and the pattern *refutes* the value. If the match succeeds, the pattern may also *destructure* data from the object or *bind* variables. -Most refutable patterns (matchers) are syntactically restricted to only appear -in a context where refutation is meaningful and control flow can occur. If the -pattern refutes the value, then no code where any variables defined by the -pattern are in scope will be executed. Specifically, if a pattern in a switch -case is refuted, execution proceeds to the next case. - -If a pattern match failure occurs in pattern variable declaration, a runtime -exception is thrown. *(This can happen, for example, when matching against a -variable of type `dynamic`.)* +Refutable patterns usually occur in a context where match refutation causes +execution to skip over the body of code where any variables bound by the pattern +are in scope. If a pattern match failure occurs in irrefutable context, a +runtime exception is thrown. *This can happen when matching against a value of +type `dynamic`, or when a list pattern in a variable declaration is matched +against a list of a different length.* To match a pattern `p` against a value `v`: -* **Type pattern**: Always matches. Binds the corresponding type argument of - the runtime type of `v` to the pattern's type variable. +* **Logical-or**: -* **List binder or matcher**: + 1. Match the left subpattern against `v`. If it matches, the logical-or + match succeeds. - 1. If `v` does not implement `List` for some `T`, then the match fails. - *This may happen at runtime if `v` has static type `dynamic`.* + 2. Otherwise, match the right subpattern against `v` and succeed if it + matches. - 2. If the length of the list determined by calling `length` is not equal to - the number of subpatterns, then the match fails. +* **Logical-and**: - 3. Otherwise, extracts a series of elements from `v` using `[]` and matches - them against the corresponding subpatterns. The match succeeds if all - subpatterns match. + 1. Match the left subpattern against `v`. If the match fails, the + logical-and match fails. -* **Map binder or matcher**: + 2. Otherwise, match the right subpattern against `v` and succeed if it + matches. - 1. If the value's type does not implement `Map` for some `K` and `V`, - then the match fails. Otherwise, tests the entry patterns: +* **Relational**: - 2. For each `mapBinderEntry` or `mapMatcherEntry`: + 1. Evaluate the right-hand constant expression to `c`. - 1. Evaluate key `expression` to `key` and call `containsKey()` on - the value. If this returns `false`, the map does not match. + 2. A `== c` pattern matches if `v == c` evaluates to true. *This takes into + account the built-in semantics that `null` is only equal to `null`.* - 3. Otherwise, evaluate `v[key]` and match the resulting value against - this entry's value subpattern. If it does not match, the map does - not match. + 3. A `!= c` pattern matches if `v == e` evaluates to false. *This takes + into account the built-in semantics that `null` is not equal to anything + but `null`.* - 3. If all entries match, the map matches. + 4. For any other operator, the pattern matches if calling the operator + method of the same name on the matched value, with `c` as the argument + returns true. - *Note that, unlike with lists, a matched map may have additional entries - that are not checked by the pattern.* +* **Null-check**: -* **Record matcher or binder**: + 1. If `v` is null then the match fails. - 1. If `p` has positional fields and `v` has type `dynamic`, then throw a - runtime exception if `v` is not an instance of the inferred record type - of `p`. *Edge case: Record patterns with positional fields can only - match record objects. Normally we statically ensure that a record - pattern with positional fields can only match a value known to be a - record type. But `dynamic` evades that check, so check here. We only do - this if the pattern has positional fields in order to allow record - patterns with only named fields to be used to call arbitrary getters on - values of type `dynamic`.* + 2. Otherwise, match the inner pattern against `v`. - 2. For each field `f` in `p`, in source order: +* **Null-assert**: - 1. If `f` is positional, then destructure the corresponding positional - field from record `v` to get result `r`. + 1. If `v` is null then throw a runtime exception. *Note that we throw even + if this appears in a refutable context. The intent of this pattern is to + assert that a value *must* not be null.* - 2. Otherwise (`f` is named), call the getter with the same name as `f` - on `v` to get result `r`. *If `v` has type `dynamic`, this getter - call may throw a NoSuchMethodError, which we allow to propagate - instead of treating that as a match failure.* + 2. Otherwise, match the inner pattern against `v`. - 3. Match the subpattern of `f` against `r`. If the match fails, the - record match fails. If `f` is a named field using the shorthand - syntax that that infers an implicit variable subpattern from the - field's name, match `r` against that inferred variable subpattern. +* **Literal** or **constant**: The pattern matches if `o == v` evaluates to + `true` where `o` is the pattern's value. - 3. If all field subpatterns match, the record pattern matches. + **TODO: Should this be `v == o`?** -* **Variable binder or matcher**: +* **Variable**: - 1. If `v` is not a subtype of `p` then the match fails. *This is a - deliberate failure when using a typed variable pattern in a switch in - order to test a value's type. In a binder, this can only occur on a - failed downcast from `dynamic` and becomes a runtime exception.* + 1. If `v` is not a subtype of `p` then the match fails. 2. Otherwise, bind the variable's identifier to `v` and the match succeeds. -* **Cast binder**: +* **Cast**: 1. If `v` is not a subtype of `p` then throw a runtime exception. *Note that we throw even if this appears in a refutable context. The intent of this pattern is to assert that a value *must* have some type.* - 2. Otherwise, bind the variable's identifier to `v`. The match always - succeeds (if it didn't throw). + 2. Otherwise, bind the variable's identifier to `v` and the match succeeds. -* **Null-assert binder**: +* **Grouping**: Match the subpattern against `v` and succeed if it matches. - 1. If `v` is null then throw a runtime exception. *Note that we throw even - if this appears in a refutable context. The intent of this pattern is to - assert that a value *must* not be null.* +* **List**: - 2. Otherwise, match the inner pattern against `v`. + 1. If `v` is not a subtype of `p` then the match fails. *The list pattern's + type will be `List` for some `T` determined either by the pattern's + explicit type argument or inferred from the matched value type.* -* **Literal matcher** or **constant matcher**: The pattern matches if `o == v` - evaluates to `true` where `o` is the pattern's value. + 2. If the length of the list determined by calling `length` is not equal to + the number of subpatterns, then the match fails. *This match failure + becomes a runtime exception if the list pattern is in a variable + declaration.* - **TODO: Should this be `v == o`?** + 3. Otherwise, for each element subpattern, in source order: -* **Wildcard binder or matcher**: Always succeeds. + 1. Extract the element value `e` by calling `[]` on `v` with an + appropriate integer index. -* **Extractor matcher**: + 2. Match `e` against the element subpattern. - 1. If `v` is not a subtype of the extractor pattern's type, then the - match fails. + 4. The match succeeds if all subpatterns match. - 2. If the extractor pattern refers to an enum value and `v` is not that - value, then the match fails. +* **Map**: - 3. Otherwise, match `v` against the subpatterns of `p` as if it were a - record pattern. + 1. If `v` is not a subtype of `p` then the match fails. *The map pattern's + type will be `Map` for some `K` and `V` determined either by the + pattern's explicit type arguments or inferred from the matched value + type.* -* **Null-check matcher**: + 2. Otherwise, for each entry in `p`: - 1. If `v` is null then the match fails. + 1. Evaluate the key `expression` to `k` and call `containsKey()` on the + value. If this returns `false`, the map does not match. - 2. Otherwise, match the inner pattern against `v`. + 2. Otherwise, evaluate `v[k]` and match the resulting value against + this entry's value subpattern. If it does not match, the map does + not match. + + 3. The match succeeds if all entry subpatterns match. + + *Note that, unlike with lists, a matched map may have additional entries + that are not checked by the pattern.* + +* **Record**: + + 1. If `v` is not a record with the same type as `p`, then the match fails. + + 2. For each field `f` in `p`, in source order: + + 1. Access the corresponding field in record `v` as `r`. + + 2. Match the subpattern of `f` against `r`. If the match fails, the + record match fails. + + 3. The match succeeds if all field subpatterns match. + +* **Extractor**: + + 1. If `v` is not a subtype of `p` then the match fails. + + 3. Otherwise, for each field `f` in `p`: + + 1. Call the getter with the same name as `f` on `v` to a result `r`. + + 2. Match the subpattern of `f` against `r`. If the match fails, the + extractor match fails. + + 3. The match succeeds if all field subpatterns match. **TODO: Update to specify that the result of operations can be cached across cases. See: https://github.com/dart-lang/language/issues/2107** -### Late and static variables in pattern declaration +## Severability -If a pattern variable declaration is marked `late` or a static variable -declaration has a pattern, then all variables declared by the pattern are late. -Evaluation of the initializer expression is deferred until any variable in the -pattern is accessed. When that occurs, the initializer is evaluated and all -pattern destructuring occurs and all variables become initialized. +This proposal, along with the records and exhaustiveness documents it depends +on, is a lot of new language work. There is new syntax to parse, new type +checking and inference features (including quite complex exhaustiveness +checking), a new kind of object that needs a runtime representation and runtime +type, and new imperative behavior. -*If you touch *any* of the variables, they *all* get initialized:* +It might be too much to fit into a single Dart release. However, it isn't +necessary to ship every corner of these proposals all at once. If needed for +scheduling reasons, we could stage it across several releases. -```dart -int say(int n) { - print(n); - return n; -} +Here is one way it could be broken down into separate pieces: -main() { - late var (a, b) = (say(1), say(2)); - a; - print("here"); - b; -} -``` +* **Records and destructuring.** Record expressions and record types are one + of the most-desired aspects of this proposal. Currently, there is no + expression syntax for accessing positional fields from a record. That means + we need destructuring. So, at a minimum: + + * Record expressions and types + * Pattern variable declarations + * Record patterns + * Variable patterns + + This would not include any refutable patterns, so doesn't need the changes + to allow patterns in switches. + +* **Collection destructuring.** A minor extension of the above is to also + allow destructuring the other built-in aggregate types: + + * List patterns + * Map patterns + +* **Extractors.** I don't want patterns to feel like we're duct taping a + functional feature onto an object-oriented language. To integrate it more + gracefully means destructuring user-defined types too, so adding: + + * Extractor patterns + +* **Refutable patterns.** The next big step is patterns that don't just + destructure but *match*. The bare minimum refutable patterns and features + are: + + * Patterns in switch statement cases + * Switch case guards + * Exhaustiveness checking + * Literal patterns + * Constant patterns + * Relational patterns (at least `==`) + + The only critical relational pattern is `==` because once we allow patterns + in switch cases, we lose the ability to have a bare identifier constant in + a switch case. + +* **Type testing patterns.** The other type-based patterns aren't critical but + do make patterns more convenient and useful: + + * Null-check patterns + * Null-assert patterns + * Cast patterns -*This prints "1", "2", "here".* +* **Control flow.** Switch statements are heavyweight. If we want to make + refutable patterns more useful, we eventually want: + + * Switch expressions + * Pattern-if statements + +* **Logical patterns.** If we're going to add `==` patterns, we may as well + support other Boolean infix operators. And if we're going to support the + comparison operators, then `&` is useful for numeric ranges. It's weird to + have `&` without `|` so we may as well do that too (and it's useful for + switch expressions). Once we have infix patterns precedence comes into play, + so we need parentheses to control it: + + * Relational patterns (other than `==`) + * Logical-or patterns + * Logical-and patterns + * Grouping patterns ## Changelog +### 2.0 + +Major redesign of the syntax and minor redesign of the semantics. + +- Unify binder and matcher patterns into a single grammar. Refutable patterns + are still prohibited outside of contexts where failure can be handled using + control flow, but the grammar is unified and more patterns can be used in + the other context. For example, null-assert patterns can be used in switch + cases. + +- Always treat simple identifiers as variables in patterns, even in switch + cases. + +- Change the `if (expr case pattern)` syntax to `if (var pattern = expr)`. + +- Change the guard syntax to `when expr`. + +- Record patterns match only record objects. Extractor patterns (which can + now be used in variable declarations) are the only way to call getters on + abitrary objects. + +- New patterns for relational operators, `|`, `&`, and `(...)`. Set up a + precedence hierarchy for patterns. + +- Get rid of explicit wildcard patterns since they're redundant with untyped + variable patterns named `_`. + +- Don't allow extractor patterns to match enum values. (It doesn't seem that + well motivated and could be added later if useful.) + +- Remove support for `late` pattern variable declarations, patterns in + top-level variables, and patterns in fields. The semantics get pretty weird + and it's not clear that they're worth it. + +- Change the static typing rules significantly in a number of ways. + +- Remove type patterns. They aren't fully baked, are pretty complex, and don't + seem critical right now. We can always add them as a later extension. + ### 1.8 - Remove declaration matcher from the proposal. It's only a syntactic sugar