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>
This commit is contained in:
Paul Berry 2022-08-29 03:27:53 +00:00 committed by Commit Bot
parent ac86c134e9
commit bd2d261bc6
6 changed files with 453 additions and 149 deletions

View file

@ -530,6 +530,17 @@ abstract class FlowAnalysis<Node extends Object, Statement extends Node,
@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 @@ abstract class FlowAnalysis<Node extends Object, Statement extends Node,
/// 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 @@ abstract class FlowAnalysis<Node extends Object, Statement extends Node,
/// 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);
@ -1172,6 +1202,18 @@ class FlowAnalysisDebug<Node extends Object, Statement extends Node,
isQuery: true);
}
@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)',
@ -1184,6 +1226,18 @@ class FlowAnalysisDebug<Node extends Object, Statement extends Node,
() => _wrapped.switchStatement_end(isExhaustive));
}
@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)',
@ -3685,6 +3739,22 @@ class _FlowAnalysisImpl<Node extends Object, Statement extends Node,
SsaNode<Type>? ssaNodeForTesting(Variable variable) => _current
.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 =
@ -3714,6 +3784,21 @@ class _FlowAnalysisImpl<Node extends Object, Statement extends Node,
_current = breakState.unsplit();
}
@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();
@ -4514,12 +4599,24 @@ class _LegacyTypePromotion<Node extends Object, Statement extends Node,
throw new StateError('ssaNodeForTesting requires null-aware flow analysis');
}
@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) {}
@ -4767,6 +4864,14 @@ class _SimpleStatementContext<Type extends Object>
'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).

View file

@ -14,29 +14,36 @@ class ExpressionCaseInfo<Expression, Node> {
/// 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 @@ class StatementCaseInfo<Statement, Node> {
StatementCaseInfo(
{required this.node,
required this.hasLabel,
this.labels = const [],
required this.pattern,
this.when,
required this.body});
}
@ -70,6 +78,9 @@ class StatementCaseInfo<Statement, Node> {
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 @@ mixin TypeAnalyzer<Node extends Object, Statement extends Node,
/// - [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 @@ mixin TypeAnalyzer<Node extends Object, Statement extends Node,
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 @@ mixin TypeAnalyzer<Node extends Object, Statement extends Node,
/// 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 @@ mixin TypeAnalyzer<Node extends Object, Statement extends Node,
/// 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 @@ mixin TypeAnalyzer<Node extends Object, Statement extends Node,
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();

View file

@ -1628,10 +1628,11 @@ main() {
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 @@ main() {
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 @@ main() {
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 @@ main() {
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 @@ main() {
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 @@ main() {
h.run([
switchExpr(expr('int'), [
caseExpr(x.pattern(type: 'int?'),
checkNotPromoted(x).thenExpr(nullLiteral)),
body: checkNotPromoted(x).thenExpr(nullLiteral)),
]).stmt,
]);
});
@ -1962,10 +1964,10 @@ main() {
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 @@ main() {
switch_(
expr('int'),
[
case_(x.pattern(type: 'int?'), [
case_(x.pattern(type: 'int?'), body: [
checkNotPromoted(x),
]),
],
@ -1993,6 +1995,31 @@ main() {
]);
});
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 @@ main() {
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 @@ main() {
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 @@ main() {
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 @@ main() {
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 @@ main() {
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 @@ main() {
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 @@ main() {
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 @@ main() {
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 @@ main() {
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');

View file

@ -28,14 +28,15 @@ Statement block(List<Statement> statements) => new _Block(statements);
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 @@ Statement declare(Var variable,
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 @@ Statement if_(Expression condition, List<Statement> ifTrue,
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,
@ -279,13 +274,19 @@ class ExpressionCase extends Node
@override
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 Harness
}
}
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 @@ abstract class Statement extends Node {
/// 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 @@ class StatementCase extends Node implements StatementCaseInfo<Statement, Node> {
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 _BooleanLiteral extends Literal {
}
class _Break extends Statement {
final LabeledStatement? target;
final Label? target;
_Break(this.target);
@ -957,7 +968,7 @@ class _Break extends Statement {
@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 _Is extends Expression {
}
}
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 @@ class _MiniAstTypeAnalyzer
final _irBuilder = MiniIrBuilder();
@override
late final Type boolType = Type('bool');
@override
@ -2083,8 +2116,16 @@ class _MiniAstTypeAnalyzer
}
@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

View file

@ -90,7 +90,7 @@ main() {
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 @@ main() {
test('scrutinee expression context', () {
h.run([
switchExpr(expr('int').checkContext('?'), [
defaultExpr(intLiteral(0)),
defaultExpr(body: intLiteral(0)),
]).inContext('num'),
]);
});
@ -106,7 +106,7 @@ main() {
test('body expression context', () {
h.run([
switchExpr(expr('int'), [
defaultExpr(nullLiteral.checkContext('C?')),
defaultExpr(body: nullLiteral.checkContext('C?')),
]).inContext('C?'),
]);
});
@ -114,11 +114,29 @@ main() {
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 @@ main() {
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 @@ main() {
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 @@ main() {
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 @@ main() {
switch_(
expr('int').checkContext('?'),
[
case_(intLiteral(0).pattern, [
case_(intLiteral(0).pattern, body: [
break_(),
]),
],
@ -193,37 +211,43 @@ main() {
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 @@ main() {
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 @@ main() {
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 @@ main() {
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 @@ main() {
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 @@ main() {
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({

View file

@ -1225,6 +1225,7 @@ sh
sha
shadowed
shallow
shares
shas
shelf
shifts