Shared type analysis: add support for `when` clauses and fix label support.
Support for `when` clauses requires flow analysis integration, so that
`when` clauses can promote variables, e.g.:
f(int x, String? y) {
switch (x) {
case 0 when y != null:
// y is known to be non-null here
}
}
Support for labels in switch statements had a small flaw: we weren't
reporting an error in the case where a label shared a case body with a
pattern that tried to bind a variable, e.g.:
f(int x) {
switch (x) {
L: // Error: does not mind the variable `y`
case var y:
...
}
}
Change-Id: I0b2bb4721a6b3a8f7898df682b24b75ddb6e44ae
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/256605
Commit-Queue: Paul Berry <paulberry@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
diff --git a/pkg/_fe_analyzer_shared/lib/src/flow_analysis/flow_analysis.dart b/pkg/_fe_analyzer_shared/lib/src/flow_analysis/flow_analysis.dart
index f7d88b7..e315ad9 100644
--- a/pkg/_fe_analyzer_shared/lib/src/flow_analysis/flow_analysis.dart
+++ b/pkg/_fe_analyzer_shared/lib/src/flow_analysis/flow_analysis.dart
@@ -530,6 +530,17 @@
@visibleForTesting
SsaNode<Type>? ssaNodeForTesting(Variable variable);
+ /// Call this method just after visiting a `when` part of a case clause. See
+ /// [switchStatement_expressionEnd] for details.
+ ///
+ /// [when] should be the expression following the `when` keyword.
+ void switchStatement_afterWhen(Expression when);
+
+ /// Call this method just before visiting a sequence of two or more `case` or
+ /// `default` clauses that share a body. See [switchStatement_expressionEnd]
+ /// for details.`
+ void switchStatement_beginAlternatives();
+
/// Call this method just before visiting one of the cases in the body of a
/// switch statement. See [switchStatement_expressionEnd] for details.
///
@@ -547,6 +558,16 @@
/// were listed in cases.
void switchStatement_end(bool isExhaustive);
+ /// Call this method just after visiting a `case` or `default` clause, if it
+ /// shares a body with at least one other `case` or `default` clause. See
+ /// [switchStatement_expressionEnd] for details.`
+ void switchStatement_endAlternative();
+
+ /// Call this method just after visiting a sequence of two or more `case` or
+ /// `default` clauses that share a body. See [switchStatement_expressionEnd]
+ /// for details.`
+ void switchStatement_endAlternatives();
+
/// Call this method just after visiting the expression part of a switch
/// statement or expression. [switchStatement] should be the switch statement
/// itself (or `null` if this is a switch expression).
@@ -554,9 +575,18 @@
/// The order of visiting a switch statement should be:
/// - Visit the switch expression.
/// - Call [switchStatement_expressionEnd].
- /// - For each switch case (including the default case, if any):
+ /// - For each case body:
/// - Call [switchStatement_beginCase].
- /// - Visit the case.
+ /// - If there is more than one `case` or `default` clause associated with
+ /// this case body, call [switchStatement_beginAlternatives].
+ /// - For each `case` or `default` clause associated with this case body:
+ /// - If a `when` clause is present, visit it and then call
+ /// [switchStatement_afterWhen].
+ /// - If there is more than one `case` or `default` clause associated with
+ /// this case body, call [switchStatement_endAlternative].
+ /// - If there is more than one `case` or `default` clause associated with
+ /// this case body, call [switchStatement_endAlternatives].
+ /// - Visit the case body.
/// - Call [switchStatement_end].
void switchStatement_expressionEnd(Statement? switchStatement);
@@ -1173,6 +1203,18 @@
}
@override
+ void switchStatement_afterWhen(Expression when) {
+ _wrap('switchStatement_afterWhen($when)',
+ () => _wrapped.switchStatement_afterWhen(when));
+ }
+
+ @override
+ void switchStatement_beginAlternatives() {
+ _wrap('switchStatement_beginAlternatives()',
+ () => _wrapped.switchStatement_beginAlternatives());
+ }
+
+ @override
void switchStatement_beginCase(bool hasLabel, Statement? node) {
_wrap('switchStatement_beginCase($hasLabel, $node)',
() => _wrapped.switchStatement_beginCase(hasLabel, node));
@@ -1185,6 +1227,18 @@
}
@override
+ void switchStatement_endAlternative() {
+ _wrap('switchStatement_endAlternative()',
+ () => _wrapped.switchStatement_endAlternative());
+ }
+
+ @override
+ void switchStatement_endAlternatives() {
+ _wrap('switchStatement_endAlternatives()',
+ () => _wrapped.switchStatement_endAlternatives());
+ }
+
+ @override
void switchStatement_expressionEnd(Statement? switchStatement) {
_wrap('switchStatement_expressionEnd($switchStatement)',
() => _wrapped.switchStatement_expressionEnd(switchStatement));
@@ -3686,6 +3740,22 @@
.variableInfo[promotionKeyStore.keyForVariable(variable)]?.ssaNode;
@override
+ void switchStatement_afterWhen(Expression when) {
+ ExpressionInfo<Type>? expressionInfo = _getExpressionInfo(when);
+ if (expressionInfo != null) {
+ _current = expressionInfo.ifTrue;
+ }
+ }
+
+ @override
+ void switchStatement_beginAlternatives() {
+ _current = _current.split();
+ _SwitchAlternativesContext<Type> context =
+ new _SwitchAlternativesContext<Type>(_current);
+ _stack.add(context);
+ }
+
+ @override
void switchStatement_beginCase(bool hasLabel, Statement? node) {
_SimpleStatementContext<Type> context =
_stack.last as _SimpleStatementContext<Type>;
@@ -3715,6 +3785,21 @@
}
@override
+ void switchStatement_endAlternative() {
+ _SwitchAlternativesContext<Type> context =
+ _stack.last as _SwitchAlternativesContext<Type>;
+ context._combinedModel = _join(context._combinedModel, _current);
+ _current = context._previous;
+ }
+
+ @override
+ void switchStatement_endAlternatives() {
+ _SwitchAlternativesContext<Type> context =
+ _stack.removeLast() as _SwitchAlternativesContext<Type>;
+ _current = context._combinedModel!.unsplit();
+ }
+
+ @override
void switchStatement_expressionEnd(Statement? switchStatement) {
_current = _current.split();
_SimpleStatementContext<Type> context =
@@ -4515,12 +4600,24 @@
}
@override
+ void switchStatement_afterWhen(Expression when) {}
+
+ @override
+ void switchStatement_beginAlternatives() {}
+
+ @override
void switchStatement_beginCase(bool hasLabel, Statement? node) {}
@override
void switchStatement_end(bool isExhaustive) {}
@override
+ void switchStatement_endAlternative() {}
+
+ @override
+ void switchStatement_endAlternatives() {}
+
+ @override
void switchStatement_expressionEnd(Statement? switchStatement) {}
@override
@@ -4767,6 +4864,14 @@
'checkpoint: $_checkpoint)';
}
+class _SwitchAlternativesContext<Type extends Object> extends _FlowContext {
+ final FlowModel<Type> _previous;
+
+ FlowModel<Type>? _combinedModel;
+
+ _SwitchAlternativesContext(this._previous);
+}
+
/// Specialization of [ExpressionInfo] for the case where the information we
/// have about the expression is trivial (meaning we know by construction that
/// the expression's [after], [ifTrue], and [ifFalse] models are all the same).
diff --git a/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer.dart b/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer.dart
index cdec2a3..a84e2dc 100644
--- a/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer.dart
+++ b/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer.dart
@@ -14,29 +14,36 @@
/// For a `case` clause, the case pattern. For a `default` clause, `null`.
final Node? pattern;
+ /// For a `case` clause that has a `when` part, the expression following
+ /// `when`. Otherwise `null`.
+ final Expression? when;
+
/// The body of the `case` or `default` clause.
final Expression body;
- ExpressionCaseInfo(this.pattern, this.body);
+ ExpressionCaseInfo({required this.pattern, this.when, required this.body});
}
/// Information supplied by the client to [TypeAnalyzer.analyzeSwitchStatement]
/// about an individual `case` or `default` clause.
///
/// The client is free to `implement` or `extend` this class.
-class StatementCaseInfo<Statement, Node> {
+class StatementCaseInfo<Statement, Expression, Node> {
/// The AST node for this `case` or `default` clause. This is used for error
/// reporting, in case errors arise from mismatch among the variables bound by
/// various cases that share a body.
final Node node;
- /// Indicates whether this `case` or `default` clause is preceded by one or
- /// more `goto` labels.
- final bool hasLabel;
+ /// The labels preceding this `case` or `default` clause, if any.
+ final List<Node> labels;
/// For a `case` clause, the case pattern. For a `default` clause, `null`.
final Node? pattern;
+ /// For a `case` clause that has a `when` part, the expression following
+ /// `when`. Otherwise `null`.
+ final Expression? when;
+
/// The statements following this `case` or `default` clause. If this list is
/// empty, and this is not the last `case` or `default` clause, this clause
/// will be considered to share a body with the `case` or `default` clause
@@ -45,8 +52,9 @@
StatementCaseInfo(
{required this.node,
- required this.hasLabel,
+ this.labels = const [],
required this.pattern,
+ this.when,
required this.body});
}
@@ -70,6 +78,9 @@
mixin TypeAnalyzer<Node extends Object, Statement extends Node,
Expression extends Node, Variable extends Object, Type extends Object>
implements VariableBindingCallbacks<Node, Variable, Type> {
+ /// Returns the type `bool`.
+ Type get boolType;
+
/// Returns the type `double`.
Type get doubleType;
@@ -175,6 +186,8 @@
/// - [analyzeExpression]
/// - For each `case` or `default` clause:
/// - [dispatchPattern] if this is a `case` clause
+ /// - [analyzeExpression] if this is a `case` clause with a `when` part
+ /// - [handleCaseHead] if this is a `case` clause
/// - [handleDefault] if this is a `default` clause
/// - [handleCase_afterCaseHeads]
/// - [analyzeExpression]
@@ -196,10 +209,17 @@
if (pattern != null) {
dispatchPattern(pattern)
.match(expressionType, bindings, isFinal: true, isLate: false);
+ Expression? when = caseInfo.when;
+ bool hasWhen = when != null;
+ if (hasWhen) {
+ analyzeExpression(when, boolType);
+ flow?.switchStatement_afterWhen(when);
+ }
+ handleCaseHead(hasWhen: hasWhen);
} else {
handleDefault();
}
- handleCase_afterCaseHeads(1);
+ handleCase_afterCaseHeads(const [], 1);
Type type = analyzeExpression(caseInfo.body, context);
if (lubType == null) {
lubType = type;
@@ -217,8 +237,11 @@
/// Invokes the following [TypeAnalyzer] methods (in order):
/// - [dispatchExpression]
/// - For each `case` or `default` body:
- /// - [dispatchPattern] for each `case` pattern associated with the body
- /// - [handleDefault] if a `default` clause is associated with the body
+ /// - For each `case` or `default` clause associated with the body:
+ /// - [dispatchPattern] if this is a `case` clause
+ /// - [analyzeExpression] if this is a `case` clause with a `when` part
+ /// - [handleCaseHead] if this is a `case` clause
+ /// - [handleDefault] if this is a `default` clause
/// - [handleCase_afterCaseHeads]
/// - [dispatchStatement] for each statement in the body
/// - [finishStatementCase]
@@ -228,42 +251,65 @@
/// length of [cases] because a case with no statements get merged into the
/// case that follows).
int analyzeSwitchStatement(Statement node, Expression scrutinee,
- List<StatementCaseInfo<Statement, Node>> cases) {
+ List<StatementCaseInfo<Statement, Expression, Node>> cases) {
Type expressionType = analyzeExpression(scrutinee, unknownType);
flow?.switchStatement_expressionEnd(node);
- bool hasLabel = false;
- List<StatementCaseInfo<Statement, Node>>? casesInThisExecutionPath;
+ List<Node> labels = [];
+ List<StatementCaseInfo<Statement, Expression, Node>>?
+ casesInThisExecutionPath;
int numExecutionPaths = 0;
for (int i = 0; i < cases.length; i++) {
- StatementCaseInfo<Statement, Node> caseInfo = cases[i];
- hasLabel = hasLabel || caseInfo.hasLabel;
+ StatementCaseInfo<Statement, Expression, Node> caseInfo = cases[i];
+ labels.addAll(caseInfo.labels);
(casesInThisExecutionPath ??= []).add(caseInfo);
if (i == cases.length - 1 || caseInfo.body.isNotEmpty) {
numExecutionPaths++;
- flow?.switchStatement_beginCase(hasLabel, node);
+ flow?.switchStatement_beginCase(labels.isNotEmpty, node);
VariableBindings<Node, Variable, Type> bindings =
new VariableBindings(this);
bindings.startAlternatives();
- for (int i = 0; i < casesInThisExecutionPath.length; i++) {
- StatementCaseInfo<Statement, Node> caseInfo =
+ // Labels count as empty patterns for the purposes of bindings.
+ for (Node label in labels) {
+ bindings.startAlternative(label);
+ bindings.finishAlternative();
+ }
+ int numCasesInThisExecutionPath = casesInThisExecutionPath.length;
+ if (numCasesInThisExecutionPath > 1) {
+ flow?.switchStatement_beginAlternatives();
+ }
+ for (int i = 0; i < numCasesInThisExecutionPath; i++) {
+ StatementCaseInfo<Statement, Expression, Node> caseInfo =
casesInThisExecutionPath[i];
bindings.startAlternative(caseInfo.node);
Node? pattern = caseInfo.pattern;
if (pattern != null) {
dispatchPattern(pattern)
.match(expressionType, bindings, isFinal: true, isLate: false);
+ Expression? when = caseInfo.when;
+ bool hasWhen = when != null;
+ if (hasWhen) {
+ analyzeExpression(when, boolType);
+ flow?.switchStatement_afterWhen(when);
+ }
+ handleCaseHead(hasWhen: hasWhen);
} else {
handleDefault();
}
bindings.finishAlternative();
+ if (numCasesInThisExecutionPath > 1) {
+ flow?.switchStatement_endAlternative();
+ }
}
bindings.finishAlternatives();
- handleCase_afterCaseHeads(casesInThisExecutionPath.length);
+ if (numCasesInThisExecutionPath > 1) {
+ flow?.switchStatement_endAlternatives();
+ }
+ handleCase_afterCaseHeads(labels, numCasesInThisExecutionPath);
for (Statement statement in caseInfo.body) {
dispatchStatement(statement);
}
finishStatementCase(node, i, caseInfo.body.length);
- hasLabel = false;
+ labels.clear();
casesInThisExecutionPath = null;
}
}
@@ -334,7 +380,10 @@
void finishStatementCase(Statement node, int caseIndex, int numStatements);
/// See [analyzeSwitchStatement] and [analyzeSwitchExpression].
- void handleCase_afterCaseHeads(int numHeads);
+ void handleCase_afterCaseHeads(List<Node> labels, int numHeads);
+
+ /// See [analyzeSwitchStatement] and [analyzeSwitchExpression].
+ void handleCaseHead({required bool hasWhen});
/// See [analyzeConstOrLiteralPattern].
void handleConstOrLiteralPattern();
diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart b/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart
index 35d1050..7a806c4 100644
--- a/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart
+++ b/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart
@@ -1628,10 +1628,11 @@
test('labeledBlock without break', () {
var x = Var('x');
+ var l = Label('l');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
if_(x.expr.isNot('int'), [
- labeled((_) => return_()),
+ l.thenStmt(return_()),
]),
checkPromoted(x, 'int'),
]);
@@ -1639,15 +1640,16 @@
test('labeledBlock with break joins', () {
var x = Var('x');
+ var l = Label('l');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
if_(x.expr.isNot('int'), [
- labeled((t) => block([
- if_(expr('bool'), [
- break_(t),
- ]),
- return_(),
- ])),
+ l.thenStmt(block([
+ if_(expr('bool'), [
+ break_(l),
+ ]),
+ return_(),
+ ])),
]),
checkNotPromoted(x),
]);
@@ -1914,8 +1916,8 @@
h.run([
switchExpr(throw_(expr('C')), [
caseExpr(intLiteral(0).pattern,
- checkReachable(false).thenExpr(intLiteral(1))),
- defaultExpr(checkReachable(false).thenExpr(intLiteral(2))),
+ body: checkReachable(false).thenExpr(intLiteral(1))),
+ defaultExpr(body: checkReachable(false).thenExpr(intLiteral(2))),
]).stmt,
checkReachable(false),
]);
@@ -1924,8 +1926,8 @@
test('switchExpression throw in case body has isolated effect', () {
h.run([
switchExpr(expr('int'), [
- caseExpr(intLiteral(0).pattern, throw_(expr('C'))),
- defaultExpr(checkReachable(true).thenExpr(intLiteral(2))),
+ caseExpr(intLiteral(0).pattern, body: throw_(expr('C'))),
+ defaultExpr(body: checkReachable(true).thenExpr(intLiteral(2))),
]).stmt,
checkReachable(true),
]);
@@ -1934,8 +1936,8 @@
test('switchExpression throw in all case bodies affects flow after', () {
h.run([
switchExpr(expr('int'), [
- caseExpr(intLiteral(0).pattern, throw_(expr('C'))),
- defaultExpr(throw_(expr('C'))),
+ caseExpr(intLiteral(0).pattern, body: throw_(expr('C'))),
+ defaultExpr(body: throw_(expr('C'))),
]).stmt,
checkReachable(false),
]);
@@ -1952,7 +1954,7 @@
h.run([
switchExpr(expr('int'), [
caseExpr(x.pattern(type: 'int?'),
- checkNotPromoted(x).thenExpr(nullLiteral)),
+ body: checkNotPromoted(x).thenExpr(nullLiteral)),
]).stmt,
]);
});
@@ -1962,10 +1964,10 @@
switch_(
throw_(expr('C')),
[
- case_(intLiteral(0).pattern, [
+ case_(intLiteral(0).pattern, body: [
checkReachable(false),
]),
- case_(intLiteral(1).pattern, [
+ case_(intLiteral(1).pattern, body: [
checkReachable(false),
]),
],
@@ -1985,7 +1987,7 @@
switch_(
expr('int'),
[
- case_(x.pattern(type: 'int?'), [
+ case_(x.pattern(type: 'int?'), body: [
checkNotPromoted(x),
]),
],
@@ -1993,6 +1995,31 @@
]);
});
+ test('switchStatement_afterWhen() promotes', () {
+ var x = Var('x');
+ h.run([
+ switch_(
+ expr('num'),
+ [
+ case_(x.pattern(), when: x.expr.is_('int'), body: [
+ checkPromoted(x, 'int'),
+ ]),
+ ],
+ isExhaustive: true),
+ ]);
+ });
+
+ test('switchStatement_afterWhen() called for switch expressions', () {
+ var x = Var('x');
+ h.run([
+ switchExpr(expr('num'), [
+ caseExpr(x.pattern(),
+ when: x.expr.is_('int'),
+ body: checkPromoted(x, 'int').thenExpr(expr('String'))),
+ ]).stmt,
+ ]);
+ });
+
test('switchStatement_beginCase(false) restores previous promotions', () {
var x = Var('x');
h.run([
@@ -2001,12 +2028,12 @@
switch_(
expr('int'),
[
- case_(intLiteral(0).pattern, [
+ case_(intLiteral(0).pattern, body: [
checkPromoted(x, 'int'),
x.write(expr('int?')).stmt,
checkNotPromoted(x),
]),
- case_(intLiteral(1).pattern, [
+ case_(intLiteral(1).pattern, body: [
checkPromoted(x, 'int'),
x.write(expr('int?')).stmt,
checkNotPromoted(x),
@@ -2024,7 +2051,7 @@
switch_(
expr('int'),
[
- case_(intLiteral(0).pattern, [
+ case_(intLiteral(0).pattern, body: [
checkPromoted(x, 'int'),
x.write(expr('int?')).stmt,
checkNotPromoted(x),
@@ -2043,7 +2070,7 @@
switch_(
expr('int'),
[
- case_(intLiteral(0).pattern, [
+ case_(intLiteral(0).pattern, body: [
checkPromoted(x, 'int'),
localFunction([
x.write(expr('int?')).stmt,
@@ -2057,6 +2084,7 @@
test('switchStatement_beginCase(true) un-promotes', () {
var x = Var('x');
+ var l = Label('l');
late SsaNode<Type> ssaBeforeSwitch;
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
@@ -2067,16 +2095,13 @@
getSsaNodes((nodes) => ssaBeforeSwitch = nodes[x]!),
])),
[
- case_(
- intLiteral(0).pattern,
- [
- checkNotPromoted(x),
- getSsaNodes(
- (nodes) => expect(nodes[x], isNot(ssaBeforeSwitch))),
- x.write(expr('int?')).stmt,
- checkNotPromoted(x),
- ],
- hasLabel: true),
+ l.thenCase(case_(intLiteral(0).pattern, body: [
+ checkNotPromoted(x),
+ getSsaNodes(
+ (nodes) => expect(nodes[x], isNot(ssaBeforeSwitch))),
+ x.write(expr('int?')).stmt,
+ checkNotPromoted(x),
+ ])),
],
isExhaustive: false),
]);
@@ -2084,23 +2109,21 @@
test('switchStatement_beginCase(true) handles write captures in cases', () {
var x = Var('x');
+ var l = Label('l');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
x.expr.as_('int').stmt,
switch_(
expr('int'),
[
- case_(
- intLiteral(0).pattern,
- [
- x.expr.as_('int').stmt,
- checkNotPromoted(x),
- localFunction([
- x.write(expr('int?')).stmt,
- ]),
- checkNotPromoted(x),
- ],
- hasLabel: true),
+ l.thenCase(case_(intLiteral(0).pattern, body: [
+ x.expr.as_('int').stmt,
+ checkNotPromoted(x),
+ localFunction([
+ x.write(expr('int?')).stmt,
+ ]),
+ checkNotPromoted(x),
+ ])),
],
isExhaustive: false),
]);
@@ -2119,7 +2142,7 @@
switch_(
expr('int'),
[
- case_(intLiteral(0).pattern, [
+ case_(intLiteral(0).pattern, body: [
x.expr.as_('int').stmt,
y.write(expr('int?')).stmt,
break_(),
@@ -2148,13 +2171,13 @@
switch_(
expr('int'),
[
- case_(intLiteral(0).pattern, [
+ case_(intLiteral(0).pattern, body: [
w.expr.as_('int').stmt,
y.expr.as_('int').stmt,
x.write(expr('int?')).stmt,
break_(),
]),
- default_([
+ default_(body: [
w.expr.as_('int').stmt,
x.expr.as_('int').stmt,
y.write(expr('int?')).stmt,
@@ -2176,17 +2199,41 @@
switch_(
expr('int'),
[
- case_(intLiteral(0).pattern, [
+ case_(intLiteral(0).pattern, body: [
x.expr.as_('int').stmt,
break_(),
]),
- default_([]),
+ default_(body: []),
],
isExhaustive: true),
checkNotPromoted(x),
]);
});
+ test('switchStatement_endAlternative() joins branches', () {
+ var x = Var('x');
+ var y = Var('y');
+ var z = Var('z');
+ h.run([
+ declare(y, type: 'num'),
+ declare(z, type: 'num'),
+ switch_(
+ expr('num'),
+ [
+ case_(x.pattern(),
+ when: x.expr.is_('int').and(y.expr.is_('int')), body: []),
+ case_(x.pattern(),
+ when: y.expr.is_('int').and(z.expr.is_('int')),
+ body: [
+ checkNotPromoted(x),
+ checkPromoted(y, 'int'),
+ checkNotPromoted(z),
+ ]),
+ ],
+ isExhaustive: true),
+ ]);
+ });
+
test('tryCatchStatement_bodyEnd() restores pre-try state', () {
var x = Var('x');
var y = Var('y');
diff --git a/pkg/_fe_analyzer_shared/test/mini_ast.dart b/pkg/_fe_analyzer_shared/test/mini_ast.dart
index 5fe88dd..5cbd243 100644
--- a/pkg/_fe_analyzer_shared/test/mini_ast.dart
+++ b/pkg/_fe_analyzer_shared/test/mini_ast.dart
@@ -28,14 +28,15 @@
Expression booleanLiteral(bool value) => _BooleanLiteral(value);
-Statement break_([LabeledStatement? target]) => new _Break(target);
+Statement break_([Label? target]) => new _Break(target);
-StatementCase case_(Pattern pattern, List<Statement> body,
- {bool hasLabel = false}) =>
- StatementCase._(hasLabel, pattern, _Block(body));
+StatementCase case_(Pattern pattern,
+ {Expression? when, required List<Statement> body}) =>
+ StatementCase._(pattern, when, _Block(body));
-ExpressionCase caseExpr(Pattern pattern, Expression expression) =>
- ExpressionCase._(pattern, expression);
+ExpressionCase caseExpr(Pattern pattern,
+ {Expression? when, required Expression body}) =>
+ ExpressionCase._(pattern, when, body);
/// Creates a pseudo-statement whose function is to verify that flow analysis
/// considers [variable]'s assigned state to be [expectedAssignedState].
@@ -77,11 +78,11 @@
isLate: isLate,
isFinal: isFinal);
-StatementCase default_(List<Statement> body, {bool hasLabel = false}) =>
- StatementCase._(hasLabel, null, _Block(body));
+StatementCase default_({required List<Statement> body}) =>
+ StatementCase._(null, null, _Block(body));
-ExpressionCase defaultExpr(Expression expression) =>
- ExpressionCase._(null, expression);
+ExpressionCase defaultExpr({required Expression body}) =>
+ ExpressionCase._(null, null, body);
Statement do_(List<Statement> body, Expression condition) =>
_Do(block(body), condition);
@@ -146,12 +147,6 @@
Literal intLiteral(int value, {bool? expectConversionToDouble}) =>
new _IntLiteral(value, expectConversionToDouble: expectConversionToDouble);
-Statement labeled(Statement Function(LabeledStatement) callback) {
- var labeledStatement = LabeledStatement._();
- labeledStatement._body = callback(labeledStatement);
- return labeledStatement;
-}
-
Statement localFunction(List<Statement> body) => _LocalFunction(block(body));
Statement match(Pattern pattern, Expression initializer,
@@ -280,12 +275,18 @@
final Pattern? pattern;
@override
+ final Expression? when;
+
+ @override
final Expression body;
- ExpressionCase._(this.pattern, this.body) : super._();
+ ExpressionCase._(this.pattern, this.when, this.body) : super._();
- String toString() =>
- [pattern == null ? 'default:' : 'case $pattern:', '$body'].join(' ');
+ String toString() => [
+ pattern == null ? 'default' : 'case $pattern',
+ if (when != null) ' when $when',
+ ': $body'
+ ].join('');
void _preVisit(AssignedVariables<Node, Var> assignedVariables) {
pattern?.preVisit(assignedVariables);
@@ -642,23 +643,29 @@
}
}
-class LabeledStatement extends Statement {
- late final Statement _body;
+class Label extends Node {
+ final String _name;
- LabeledStatement._();
+ late final Node _binding;
- @override
- void preVisit(AssignedVariables<Node, Var> assignedVariables) {
- _body.preVisit(assignedVariables);
+ Label(this._name) : super._();
+
+ StatementCase thenCase(StatementCase case_) {
+ case_.labels.insert(0, this);
+ return case_;
+ }
+
+ Statement thenStmt(Statement statement) {
+ if (statement is! _LabeledStatement) {
+ statement = _LabeledStatement(statement);
+ }
+ statement._labels.insert(0, this);
+ _binding = statement;
+ return statement;
}
@override
- String toString() => 'labeled: $_body';
-
- @override
- void visit(Harness h) {
- h.typeAnalyzer.analyzeLabeledStatement(this, _body);
- }
+ String toString() => _name;
}
abstract class Literal extends Expression {
@@ -765,16 +772,20 @@
/// Representation of a single case clause in a switch statement. Use [case_]
/// to create instances of this class.
-class StatementCase extends Node implements StatementCaseInfo<Statement, Node> {
+class StatementCase extends Node
+ implements StatementCaseInfo<Statement, Expression, Node> {
@override
- final bool hasLabel;
+ final List<Label> labels = [];
@override
final Pattern? pattern;
+ @override
+ final Expression? when;
+
final _Block _statements;
- StatementCase._(this.hasLabel, this.pattern, this._statements) : super._();
+ StatementCase._(this.pattern, this.when, this._statements) : super._();
@override
List<Statement> get body => _statements.statements;
@@ -783,7 +794,7 @@
Node get node => this;
String toString() => [
- if (hasLabel) '<label>:',
+ for (var label in labels) '$label:',
pattern == null ? 'default:' : 'case $pattern:',
...body
].join(' ');
@@ -945,7 +956,7 @@
}
class _Break extends Statement {
- final LabeledStatement? target;
+ final Label? target;
_Break(this.target);
@@ -957,7 +968,7 @@
@override
void visit(Harness h) {
- h.typeAnalyzer.analyzeBreakStatement(target);
+ h.typeAnalyzer.analyzeBreakStatement(target?._binding as Statement?);
h.irBuilder.apply('break', 0);
}
}
@@ -1619,6 +1630,27 @@
}
}
+class _LabeledStatement extends Statement {
+ final List<Label> _labels = [];
+
+ final Statement _body;
+
+ _LabeledStatement(this._body);
+
+ @override
+ void preVisit(AssignedVariables<Node, Var> assignedVariables) {
+ _body.preVisit(assignedVariables);
+ }
+
+ @override
+ String toString() => [..._labels, _body].join(': ');
+
+ @override
+ void visit(Harness h) {
+ h.typeAnalyzer.analyzeLabeledStatement(this, _body);
+ }
+}
+
class _LocalFunction extends Statement {
final Statement body;
@@ -1752,6 +1784,7 @@
final _irBuilder = MiniIrBuilder();
+ @override
late final Type boolType = Type('bool');
@override
@@ -2083,8 +2116,16 @@
}
@override
- void handleCase_afterCaseHeads(int numHeads) {
- _irBuilder.apply('heads', numHeads);
+ void handleCase_afterCaseHeads(List<Node> labels, int numHeads) {
+ for (var label in labels) {
+ _irBuilder.atom((label as Label)._name);
+ }
+ _irBuilder.apply('heads', numHeads + labels.length);
+ }
+
+ @override
+ void handleCaseHead({required bool hasWhen}) {
+ _irBuilder.apply('head', hasWhen ? 2 : 1);
}
@override
diff --git a/pkg/_fe_analyzer_shared/test/type_inference/type_inference_test.dart b/pkg/_fe_analyzer_shared/test/type_inference/type_inference_test.dart
index c539b43..241f8f5e 100644
--- a/pkg/_fe_analyzer_shared/test/type_inference/type_inference_test.dart
+++ b/pkg/_fe_analyzer_shared/test/type_inference/type_inference_test.dart
@@ -90,7 +90,7 @@
test('IR', () {
h.run([
switchExpr(expr('int'), [
- defaultExpr(intLiteral(0)),
+ defaultExpr(body: intLiteral(0)),
]).checkIr('switchExpr(expr(int), case(heads(default), 0))').stmt,
]);
});
@@ -98,7 +98,7 @@
test('scrutinee expression context', () {
h.run([
switchExpr(expr('int').checkContext('?'), [
- defaultExpr(intLiteral(0)),
+ defaultExpr(body: intLiteral(0)),
]).inContext('num'),
]);
});
@@ -106,7 +106,7 @@
test('body expression context', () {
h.run([
switchExpr(expr('int'), [
- defaultExpr(nullLiteral.checkContext('C?')),
+ defaultExpr(body: nullLiteral.checkContext('C?')),
]).inContext('C?'),
]);
});
@@ -114,11 +114,29 @@
test('least upper bound behavior', () {
h.run([
switchExpr(expr('int'), [
- caseExpr(intLiteral(0).pattern, expr('int')),
- defaultExpr(expr('double')),
+ caseExpr(intLiteral(0).pattern, body: expr('int')),
+ defaultExpr(body: expr('double')),
]).checkType('num').stmt
]);
});
+
+ test('when clause', () {
+ var i = Var('i');
+ h.run([
+ switchExpr(expr('int'), [
+ caseExpr(i.pattern(),
+ when: i.expr
+ .checkType('int')
+ .eq(expr('num'))
+ .checkContext('bool'),
+ body: expr('String')),
+ ])
+ .checkIr('switchExpr(expr(int), '
+ 'case(heads(head(varPattern(i, int), ==(i, expr(num)))), '
+ 'expr(String)))')
+ .stmt,
+ ]);
+ });
});
});
@@ -129,13 +147,13 @@
switch_(
expr('int').checkContext('?'),
[
- case_(intLiteral(0).pattern, [
+ case_(intLiteral(0).pattern, body: [
break_(),
]),
],
isExhaustive: false)
.checkIr('switch(expr(int), '
- 'case(heads(const(0)), block(break())))'),
+ 'case(heads(head(const(0))), block(break())))'),
]);
});
@@ -146,13 +164,13 @@
switch_(
expr('int').checkContext('?'),
[
- case_(x.pattern(), [
+ case_(x.pattern(), body: [
break_(),
]),
],
isExhaustive: false)
.checkIr('switch(expr(int), '
- 'case(heads(varPattern(x, int)), '
+ 'case(heads(head(varPattern(x, int))), '
'block(break())))'),
]);
});
@@ -163,13 +181,13 @@
switch_(
expr('int').checkContext('?'),
[
- case_(x.pattern(type: 'num'), [
+ case_(x.pattern(type: 'num'), body: [
break_(),
]),
],
isExhaustive: false)
.checkIr('switch(expr(int), '
- 'case(heads(varPattern(x, num)), '
+ 'case(heads(head(varPattern(x, num))), '
'block(break())))'),
]);
});
@@ -180,7 +198,7 @@
switch_(
expr('int').checkContext('?'),
[
- case_(intLiteral(0).pattern, [
+ case_(intLiteral(0).pattern, body: [
break_(),
]),
],
@@ -193,37 +211,43 @@
switch_(
expr('int'),
[
- case_(intLiteral(0).pattern, []),
- case_(intLiteral(1).pattern, [
+ case_(intLiteral(0).pattern, body: []),
+ case_(intLiteral(1).pattern, body: [
break_(),
]),
],
isExhaustive: false)
.checkIr('switch(expr(int), '
- 'case(heads(const(0), const(1)), block(break())))'),
+ 'case(heads(head(const(0)), head(const(1))), '
+ 'block(break())))'),
]);
});
test('merge labels', () {
var x = Var('x');
+ var l = Label('l');
h.run([
declare(x, type: 'int?', initializer: expr('int?')),
x.expr.as_('int').stmt,
switch_(
- expr('int'),
- [
- case_(intLiteral(0).pattern, [], hasLabel: true),
- case_(intLiteral(1).pattern, [
- x.expr.checkType('int?').stmt,
- break_(),
- ]),
- case_(intLiteral(2).pattern, [
- x.expr.checkType('int').stmt,
- x.write(nullLiteral).stmt,
- continue_(),
- ])
- ],
- isExhaustive: false),
+ expr('int'),
+ [
+ l.thenCase(case_(intLiteral(0).pattern, body: [])),
+ case_(intLiteral(1).pattern, body: [
+ x.expr.checkType('int?').stmt,
+ break_(),
+ ]),
+ case_(intLiteral(2).pattern, body: [
+ x.expr.checkType('int').stmt,
+ x.write(nullLiteral).stmt,
+ continue_(),
+ ])
+ ],
+ isExhaustive: false)
+ .checkIr('switch(expr(int), '
+ 'case(heads(head(const(0)), head(const(1)), l), '
+ 'block(x, break())), '
+ 'case(heads(head(const(2))), block(x, null, continue())))'),
]);
});
@@ -232,15 +256,37 @@
switch_(
expr('int'),
[
- case_(intLiteral(0).pattern, [
+ case_(intLiteral(0).pattern, body: [
break_(),
]),
- case_(intLiteral(1).pattern, []),
+ case_(intLiteral(1).pattern, body: []),
],
isExhaustive: false)
.checkIr('switch(expr(int), '
- 'case(heads(const(0)), block(break())), '
- 'case(heads(const(1)), block()))'),
+ 'case(heads(head(const(0))), block(break())), '
+ 'case(heads(head(const(1))), block()))'),
+ ]);
+ });
+
+ test('when clause', () {
+ var i = Var('i');
+ h.run([
+ switch_(
+ expr('int'),
+ [
+ case_(i.pattern(),
+ when: i.expr
+ .checkType('int')
+ .eq(expr('num'))
+ .checkContext('bool'),
+ body: [
+ break_(),
+ ]),
+ ],
+ isExhaustive: true)
+ .checkIr('switch(expr(int), '
+ 'case(heads(head(varPattern(i, int), ==(i, expr(num)))), '
+ 'block(break())))'),
]);
});
@@ -251,8 +297,8 @@
switch_(
expr('int'),
[
- case_(x.pattern(), []),
- default_([])..errorId = 'DEFAULT',
+ case_(x.pattern(), body: []),
+ default_(body: [])..errorId = 'DEFAULT',
],
isExhaustive: true)
.expectErrors({'missingMatchVar(DEFAULT, x)'}),
@@ -265,13 +311,28 @@
switch_(
expr('int'),
[
- case_(intLiteral(0).pattern, [])..errorId = 'CASE(0)',
- case_(x.pattern(), []),
+ case_(intLiteral(0).pattern, body: [])
+ ..errorId = 'CASE(0)',
+ case_(x.pattern(), body: []),
],
isExhaustive: true)
.expectErrors({'missingMatchVar(CASE(0), x)'}),
]);
});
+
+ test('label', () {
+ var x = Var('x');
+ var l = Label('l')..errorId = 'LABEL';
+ h.run([
+ switch_(
+ expr('int'),
+ [
+ l.thenCase(case_(x.pattern(), body: [])),
+ ],
+ isExhaustive: true)
+ .expectErrors({'missingMatchVar(LABEL, x)'}),
+ ]);
+ });
});
group('conflicting var:', () {
@@ -282,9 +343,9 @@
expr('num'),
[
case_(x.pattern(type: 'int')..errorId = 'PATTERN(int x)',
- []),
+ body: []),
case_(x.pattern(type: 'num')..errorId = 'PATTERN(num x)',
- []),
+ body: []),
],
isExhaustive: true)
.expectErrors({
@@ -302,9 +363,9 @@
switch_(
expr('int'),
[
- case_(x.pattern()..errorId = 'PATTERN(x)', []),
+ case_(x.pattern()..errorId = 'PATTERN(x)', body: []),
case_(x.pattern(type: 'int')..errorId = 'PATTERN(int x)',
- []),
+ body: []),
],
isExhaustive: true)
.expectErrors({
diff --git a/pkg/front_end/test/spell_checking_list_code.txt b/pkg/front_end/test/spell_checking_list_code.txt
index 012784c..c3e7532 100644
--- a/pkg/front_end/test/spell_checking_list_code.txt
+++ b/pkg/front_end/test/spell_checking_list_code.txt
@@ -1225,6 +1225,7 @@
sha
shadowed
shallow
+shares
shas
shelf
shifts