dart-lang / dart-lang/language
Case expressions and return patterns
- Dominant language
- TeX
- Stars
- 2.9k
- Forks
- 239
- Avg merge
- 2d 18h
- Merged PRs (30d)
- 14
Description
Dart 3.0 introduced patterns, including the following kind of construct known as an if-case statement:
```dart
if (json case [int x, int y]) {
print('Was coordinate array $x,$y');
} else {
throw FormatException('Invalid JSON.');
}
```
This statement has a semantics that makes it different from traditional if statements. In particular, it isn't specified as an if-statement whose condition is the expression `json case [int x, int y]`. The reason for this is that there is no expression of the form ` 'case' `. Hence, an if-case statement does not have a condition expression of type `bool`, it has an expression (of an any type), and it has a pattern, and it chooses the "then" branch if and only if the pattern matches the value of that expression. Another specialty with if-case statements is that variable declarations in the pattern are in scope in that "then" branch.
This issue claims that we might as well turn the syntax ` 'case' ` into an expression in its own right. We're generalizing it a bit, and allowing it to occur in any place where we can have an expression. The new kind of expression is known as a ``. For starters, it has static type `bool`, and it evaluates to true if and only if the match succeeds.
We say that the _result type_ of the pattern is `bool`, which is true for all the patterns that we can write today.
This issue also proposes a new kind of pattern, ``, which is used to specify a different value for the evaluation of the enclosing case expression when it matches, namely the matched value. In this case, the result type of the pattern is `M?` when the matched value type at the return pattern is `M`. The value of the pattern when the match fails is null. The return pattern also allows for invocations of methods and getters on the matched value, using syntax like `return.myMethod()`.
For example, `1 case int return` works like `switch (1) { int freshName => freshName, _ => null }` and `1 case int return.isEven` works like `switch (1) { int freshName => freshName.isEven, _ => null }`.
Here are some examples illustrating the semantics (some less trivial examples can be seen in [this comment](https://github.com/dart-lang/language/issues/4141#issuecomment-2437380099)):
```dart
void main() {
// Case expressions of type `bool`.
var b1 = 10 case > 0; // OK, sets `b1` to true.
var b2 = 10 case int j when j < 0; // OK, sets `b2` to false.
// Case expressions of other types.
var s = "Hello, world!" case String(length: > 5) && return.substring(0, 5);
var n = [1, 2.5] case [int(isEven: true) && return, _] || [_, double() && < 3.0 && return];
Object? value = (null, 3);
var i = value case (num? _, int j && return) when j.isOdd;
// It works as follows.
{ // var b1 = 10 case > 0;
bool b1;
if (10 case > 0) {
b1 = true;
} else {
b1 = false;
}
// `b1` has the value true.
}
{ // var b2 = 10 case int j when j < 0;
bool b2;
if (10 case int j when j < 0) {
b2 = true;
} else {
b2 = false;
}
// `b2` has the value false.
}
{ // var s = "Hello, world!" case String(length: > 5) && return.substring(0, 5);
String? s;
if ("Hello, world!" case String(length: > 5) && String it) {
s = it.substring(0, 5);
} else {
s = null;
}
// `s` has the value 'Hello'.
}
{ // var n = [1, 2.5] case [int(isEven: true) && return, _] || [_, double() && < 3.0 && return];
num? n;
if ([1, 2.5]
case [int(isEven: true) && final num result, _] ||
[_, double() && < 3.0 && final num result]) {
n = result;
} else {
n = null;
}
// `n` has the value 2.5.
}
{
// Object? value = (null, 3);
// var i = value case (num? _, int j && return) when j.isOdd;
Object? value = (null, 3);
int? i;
if (value case (int? _, int j) when j.isOdd) {
i = j;
} else {
i = null;
}
// `i` has the value 3.
}
}
```
## Proposal
### Syntax
```ebnf
::= ... | ;
::= 'case'
('=>' )?;
::= ... | ;
::=
'case' ;
::=
('when' )?
::= ? 'return' *;
```
### Static Analysis
With this feature, every pattern has a _result type_. The result type of every pattern which is expressible without this feature is `bool`.
The remaining patterns (which are only expressible when this feature is available) have a result type which is determined by the return patterns that occur in the pattern. They are specified below in terms of simpler patterns followed by composite ones.
The result type of a return pattern of the form `return` is the matched value type of the pattern. The result type of a return pattern of the form `T return` is `T`. The result type of a return pattern of the form `return s1 .. sk` where `sj` is derived from `` is the static type of an expression of the form `v s1 .. sk`, where `v` is a fresh variable whose type is the matched value type of the pattern. Finally, the result type of a return pattern of the form `T return s1 .. sk` is the static type of an expression of the form `v s1 .. sk`, where `v` is a fresh variable whose type is `T`.
*For example, if the matched value type for a given return pattern `P` of the form `return.substring(5).length` is `String` then the result type of `P` is `int`. This is because `v.substring(5).length` has type `int` when `v` is assumed to have type `String`.*
The result type of an object pattern that contains one field pattern with result type `R`, which is not `bool`, is `R`. It is a compile-time error if the object pattern has two or more field patterns with a result type that isn't `bool`.
It is a compile-time error if a listPattern, mapPattern, or recordPattern contains multiple elements (list elements, value patterns of the map, or pattern fields of the record) whose result type is different from `bool`. If all elements have result type `bool` then the result type of the pattern is `bool`. Otherwise, exactly one element has a result type `T` which is not `bool`, and the result type of the pattern is then `T`.
Consider a logicalAndPattern `P` of the form `P1 && P2 .. && Pn` where `Pj` has result type `T` which is not `bool`, and `Pi` has result type `bool` for all `i != j`. The result type of `P` is `T`. It is a compile-time error if a logicalAndPattern `P` of the form `P1 && P2 .. && Pn` has two or more operands `Pi` and `Pj` (where `i != j`) whose result type is not `bool`.
Consider a logicalOrPattern `P` of the form `P1 || P2 .. || Pn` where `Pi` has result type `Ti`, for `i` in `1 .. n`. A compile-time error occurs if at least one operand `Pj` has result type `bool`, and at least one operand `Pk` has a result type `T` which is not `bool`. If all operands have result type `bool` then the result type of `P` is `bool`. Otherwise, the result type of `P` is the standard upper bound of the result types `T1 .. Tn`.
Consider a parenthesizedPattern `P` of the form `(P1)`. The result type of `P` is the result type of `P1`.
Some other kinds of pattern also have the result type of their lone child: castPattern, nullCheckPattern, and nullAssertPattern.
The remaining patterns always have result type `bool`: constantPattern, variablePattern, and identifierPattern.
Assume that `e` is a case expression of the form `e1 case P` where `P` has result type `T` which is not `bool`; the static type of `e` is then `T?`. Assume that `P` has result type `bool`; the static type of `e` is then `bool`.
Assume that `e` is a case expression of the form `e1 case P => e2`. A compile-time error occurs if the result type of `P` is not `bool`. The static type of `e` is then `T?`, where `T` is the static type of `e2`.
With pre-feature patterns, it is an error if a pattern of the form `P1 || .. || Pn` declares different sets of variables in different operands `Pi` and `Pj`, with `i` and `j` in `1 .. n` and `i != j`. This is no longer an error, but it is an error to access a variable from outside `Pi` or `Pj` unless it is declared by every operand `P1 ... Pn`, with the same type and finality.
*The point is that we may well want to access different variables in return patterns: `e case A(:final x, y: return.foo(x)) || B(:final a, b: return.bar.baz(a + 1))`. For instance, a `when` clause which is shared among several patterns connected by `||` cannot use a variable like `x`, but the return pattern `return.foo(x)` _can_ use it.*
Variables which are declared by a pattern in a case expression are in the current scope of the pattern itself, and in the current scope of the guard expression, if any.
Moreover, such variables are in scope in the first branch of an `if` statement whose condition is a case expression *(just like an if-case statement today)*, and in the first branch of a conditional expression *(that is, `(e case A(:final int b)) ? b : 42` is allowed)*. Finally, such variables are in scope in the body of a `while` statement whose condition is a case expression.
### Dynamic Semantics
Evaluation of a case expression `e case P` where `P` has result type `bool` proceeds as follows: `e` is evaluated to an object `o`, and `P` is matched against `o`. If the match succeeds then the case expression evaluates to true, otherwise it evaluates to false.
Evaluation of a case expression `e case P` where `P` has result type `T` which is not `bool` proceeds as follows: `e` is evaluated to an object `o`, and `P` is matched against `o`, yielding an object `r`. If `r` is null then the case expression evaluates to null. Otherwise, `r` is a function, and the case expression then evaluates to `r()`.
A return pattern of the form `T return` evaluates to `() => v` where `v` is a fresh variable whose value is the matched value when the matched value has type `T`, otherwise it evaluates to null. A return pattern of the form `return` evaluates to `() => v` where `v` is a fresh variable whose value is the matched value (and it never fails to match).
A return pattern of the form `return s1 s2 .. sk` where `sj` is derived from `` evaluates to the value `() => v s1 s2 .. sk` where `v` is a fresh variable bound to the matched value. A return pattern of the form `T return s1 .. sk` where `sj` is derived from `` evaluates to the value () => v s1 .. sk` where `v` is a fresh variable bound to the matched value if the matched value has type `T`. If the matched value does not have type `T` then it evaluates to null.
*For example, `return.foo()` evaluates to `() => v.foo()` where `v` is the matched value.*
An object pattern that contains one field pattern with result type `R`, which is not `bool`, is evaluated by performing the type test specified by the object pattern on the matched value, yielding null if it fails, and otherwise evaluating each of the pattern fields in textual order. If every pattern field yields true, except one which yields a non-null object `r` then the object pattern evaluates to `r`. Otherwise the object pattern evaluates to null.
A listPattern, mapPattern, or recordPattern is evaluated in the corresponding manner, yielding the non-null object `r` from the element whose result type is not `bool` when all other elements yield true, and yielding null if any element yields false or null.
Consider a logicalAndPattern `P` of the form `P1 && P2 .. && Pn` where `Pj` has result type `T` which is not `bool`, and `Pi` has result type `bool` for all `i != j`. `P` is evaluated by evaluating `P1`, ..., `Pn` in that order. If every result is either true (when the result type is `bool`) or a non-null object `r` (when the result type is not `bool`), the evaluation of `P` yields `r`. Otherwise it yields null.
Consider a logicalOrPattern `P` of the form `P1 || P2 .. || Pn` where `Pi` has result type `Ti`, for `i` in `1 .. n`. The case where `Ti == bool` for all `i` has the same semantics as today. Hence, we can assume that `Ti != bool` for every `i`. Evaluation of `P` proceeds by evaluating a subset of `P1`, ..., `Pn`, in that order. As long as the the result is null, continue. If this step uses all the operands `P1` .. `Pn` then the evaluation of `P` yields null. Otherwise we evaluated some `Pj` to a non-null object `r`, in which case `P` evaluates to `r`.
Consider a parenthesizedPattern `P` of the form `(P1)`. Evaluation of `P` consists in evaluating `P1` to an object `r`, and `P` then yields `r`.
A castPattern `P` of the form `P1 as T` evaluates by evaluating `P1` to an object `r`. If `r` has a run-time type which is `T` or a subtype thereof then `P` evaluates to `r`, otherwise `P` evaluates to null.
A nullCheckPattern `P` of the form `P1?` evaluates by evaluating `P1` to an object `r`. If `r` is not null then `P` evaluates to `r`, otherwise `P` evaluates to null (that is, `P` evaluates to `r` in all cases).
A nullAssertPattern `P` of the form `P1!` evaluates by evaluating `P1` to an object `r`. If `r` is not null then `P` evaluates to `r`, otherwise the evaluation of `P` completes by throwing an exception.
The remaining patterns always have result type `bool` (constantPattern, variablePattern, and identifierPattern), and they evaluate to true if the match succeeds, otherwise they evaluate to false.
*Of course, an implementation may be able to lower patterns containing return patterns and get a behavior which isn't observably different from the semantics specified above without using any function objects, which is perfectly fine (even preferable because it is likely to be faster). However, it is important that the evaluation of a selector chain is only done in the case where the given return pattern "contributes to the successful match". If, in the end, the match fails, then we shouldn't have executed any of those selector chains. Similarly, if we have `P1 || P2` and `P1` fails but `P2` succeeds then we must execute the selector chain for `P2`, at the very end, but it is not allowed to execute the selector chain for `P1`.*
## Versions
- Nov 1, 2024: Simplified return patterns (they cannot introduce variables any more). Inspired by @tatumizer, I generalized case expressions to support an optional `=> expression` part, e.g., `41 case int() && final value => value + 1`.
- Oct 30, 2024: First version which is reasonably complete.
Contributor guide
Research direction
No implementation files or tests are named. Start with the Proposal's Syntax, Static Analysis, and Dynamic Semantics sections, then trace the listed case-expression and return-pattern examples. Done means the proposal's language-specification changes and semantics are fully reviewed and reflected in the relevant Dart language documentation or implementation.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- dart
- Domain
- compilers
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 25/100