Description
Dart 3.0 introduced patterns, including the following kind of construct known as an if-case statement:
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 <expression> 'case' <guardedPattern>
. 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 <expression> 'case' <guardedPattern>
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 <caseExpression>
. 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, <returnPattern>
, 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.
Here are some examples illustrating the semantics (some less trivial examples can be seen in this comment):
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
<expression> ::= ... | <caseExpression>;
<caseExpression> ::= <conditionalExpression> 'case' <guardedPattern>
('=>' <expression>)?;
<expressionWithoutCascade> ::= ... | <caseExpressionWithoutCascade>;
<caseExpressionWithoutCascade> ::=
<conditionalExpression> 'case' <guardedPatternWithoutCascade>;
<guardedPatternWithoutCascade> ::=
<pattern> ('when' <expressionWithoutCascade>)?
<returnPattern> ::= <type>? 'return' <selector>*;
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 <selector>
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 <selector>
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 <selector>
evaluates to the value () => v s1 .. skwhere
vis 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.