Shared type analysis: add "if-case" support.
Change-Id: I7c956239dd050c3c07ff85508180c451a70ca8a8
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/257601
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Paul Berry <paulberry@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 fd7ecd6..2862583 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
@@ -379,7 +379,9 @@
/// Call this method after visiting the condition part of an if statement.
/// [condition] should be the if statement's condition. [ifNode] should be
/// the entire `if` statement (or the collection literal entry).
- void ifStatement_thenBegin(Expression condition, Node ifNode);
+ ///
+ /// For an if-case statement, [condition] should be `null`.
+ void ifStatement_thenBegin(Expression? condition, Node ifNode);
/// Call this method after visiting the initializer of a variable declaration,
/// or a variable pattern that is being matched (and hence being initialized
@@ -1040,7 +1042,7 @@
}
@override
- void ifStatement_thenBegin(Expression condition, Node ifNode) {
+ void ifStatement_thenBegin(Expression? condition, Node ifNode) {
_wrap('ifStatement_thenBegin($condition, $ifNode)',
() => _wrapped.ifStatement_thenBegin(condition, ifNode));
}
@@ -3530,7 +3532,7 @@
}
@override
- void ifStatement_thenBegin(Expression condition, Node ifNode) {
+ void ifStatement_thenBegin(Expression? condition, Node ifNode) {
ExpressionInfo<Type> conditionInfo = _expressionEnd(condition);
_stack.add(new _IfContext(conditionInfo));
_current = conditionInfo.ifTrue;
@@ -4010,14 +4012,14 @@
/// be the last expression that was traversed). If there is no
/// [ExpressionInfo] associated with the [expression], then a fresh
/// [ExpressionInfo] is created recording the current flow analysis state.
- ExpressionInfo<Type> _expressionEnd(Expression expression) =>
+ ExpressionInfo<Type> _expressionEnd(Expression? expression) =>
_getExpressionInfo(expression) ?? new _TrivialExpressionInfo(_current);
/// Gets the [ExpressionInfo] associated with the [expression] (which should
/// be the last expression that was traversed). If there is no
/// [ExpressionInfo] associated with the [expression], then `null` is
/// returned.
- ExpressionInfo<Type>? _getExpressionInfo(Expression expression) {
+ ExpressionInfo<Type>? _getExpressionInfo(Expression? expression) {
if (identical(expression, _expressionWithInfo)) {
ExpressionInfo<Type>? expressionInfo = _expressionInfo;
_expressionInfo = null;
@@ -4421,7 +4423,7 @@
}
@override
- void ifStatement_thenBegin(Expression condition, Node ifNode) {
+ void ifStatement_thenBegin(Expression? condition, Node ifNode) {
_conditionalOrIf_thenBegin(condition, ifNode);
}
@@ -4700,7 +4702,7 @@
_writeStackForAnd.last.add(variableKey);
}
- void _conditionalOrIf_thenBegin(Expression condition, Node node) {
+ void _conditionalOrIf_thenBegin(Expression? condition, Node node) {
_contextStack.add(new _LegacyContext<Type>(_knownTypes));
AssignedVariablesNodeInfo info = _assignedVariables.getInfoForNode(node);
Map<int, Type>? newKnownTypes;
@@ -4748,7 +4750,7 @@
/// Gets the [_LegacyExpressionInfo] associated with [expression], if any;
/// otherwise returns `null`.
- _LegacyExpressionInfo<Type>? _getExpressionInfo(Expression expression) {
+ _LegacyExpressionInfo<Type>? _getExpressionInfo(Expression? expression) {
if (identical(expression, _expressionWithInfo)) {
_LegacyExpressionInfo<Type>? expressionInfo = _expressionInfo;
_expressionInfo = null;
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 9b55ba4..e207f5d 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
@@ -194,6 +194,65 @@
return result.resolveShorting();
}
+ /// Analyzes a statement of the form `if (expression case pattern) ifTrue` or
+ /// `if (expression case pattern) ifTrue else ifFalse`.
+ ///
+ /// [node] should be the AST node for the entire statement, [expression] for
+ /// the expression, [pattern] for the pattern to match, [ifTrue] for the
+ /// "then" branch, and [ifFalse] for the "else" branch (if present).
+ ///
+ /// Stack effect: pushes (Expression scrutinee, Pattern, Expression guard,
+ /// Statement ifTrue, Statement ifFalse). If there is no `else` clause, the
+ /// representation for `ifFalse` will be pushed by [handleNoStatement]. If
+ /// there is no guard, the representation for `guard` will be pushed by
+ /// [handleNoGuard].
+ void analyzeIfCaseStatement(Statement node, Expression expression,
+ Node pattern, Expression? guard, Statement ifTrue, Statement? ifFalse) {
+ // Stack: ()
+ flow?.ifStatement_conditionBegin();
+ // Note: this follows what's proposed in
+ // https://github.com/dart-lang/language/issues/2458, which is that we use
+ // the unknown type for the type schema.
+ // TODO(paulberry): update this if necessary when that issue is closed.
+ Type initializerType = analyzeExpression(expression, unknownType);
+ // Stack: (Expression)
+ PatternDispatchResult<Node, Expression, Variable, Type>
+ patternDispatchResult = dispatchPattern(pattern);
+ Map<Variable, VariableTypeInfo<Node, Type>> typeInfos = {};
+ // TODO(paulberry): rework handling of isFinal
+ patternDispatchResult.match(initializerType, typeInfos,
+ new MatchContext(isFinal: false, topPattern: pattern));
+ // Stack: (Expression, Pattern)
+ if (guard != null) {
+ _checkGuardType(guard, analyzeExpression(guard, boolType));
+ } else {
+ handleNoGuard(node, 0);
+ }
+ // Stack: (Expression, Pattern, Guard)
+ flow?.ifStatement_thenBegin(null, node);
+ _analyzeIfCommon(node, ifTrue, ifFalse);
+ }
+
+ /// Analyzes a statement of the form `if (condition) ifTrue` or
+ /// `if (condition) ifTrue else ifFalse`.
+ ///
+ /// [node] should be the AST node for the entire statement, [condition] for
+ /// the condition expression, [ifTrue] for the "then" branch, and [ifFalse]
+ /// for the "else" branch (if present).
+ ///
+ /// Stack effect: pushes (Expression condition, Statement ifTrue, Statement
+ /// ifFalse). Note that if there is no `else` clause, the representation for
+ /// `ifFalse` will be pushed by [handleNoStatement].
+ void analyzeIfStatement(Statement node, Expression condition,
+ Statement ifTrue, Statement? ifFalse) {
+ // Stack: ()
+ flow?.ifStatement_conditionBegin();
+ analyzeExpression(condition, boolType);
+ // Stack: (Expression condition)
+ flow?.ifStatement_thenBegin(condition, node);
+ _analyzeIfCommon(node, ifTrue, ifFalse);
+ }
+
/// Analyzes a variable declaration statement of the form
/// `pattern = initializer;`.
///
@@ -562,6 +621,13 @@
/// Stack effect: pushes (Expression).
void handleNoGuard(Node node, int caseIndex);
+ /// Called when visiting a syntactic construct where there is an implicit
+ /// no-op statement. For example, this is called in place of the missing
+ /// `else` part of an `if` statement that lacks an `else` clause.
+ ///
+ /// Stack effect: pushes (Statement).
+ void handleNoStatement(Statement node);
+
/// Called after visiting the scrutinee part of a switch statement or switch
/// expression. This is a hook to allow the client to start exhaustiveness
/// analysis.
@@ -600,6 +666,25 @@
/// whose initializer expression has static type [type].
Type variableTypeFromInitializerType(Type type);
+ /// Common functionality shared by [analyzeIfStatement] and
+ /// [analyzeIfCaseStatement].
+ ///
+ /// Stack effect: pushes (Statement ifTrue, Statement ifFalse).
+ void _analyzeIfCommon(Statement node, Statement ifTrue, Statement? ifFalse) {
+ // Stack: ()
+ dispatchStatement(ifTrue);
+ // Stack: (Statement ifTrue)
+ if (ifFalse == null) {
+ handleNoStatement(node);
+ flow?.ifStatement_end(false);
+ } else {
+ flow?.ifStatement_elseBegin();
+ dispatchStatement(ifFalse);
+ flow?.ifStatement_end(true);
+ }
+ // Stack: (Statement ifTrue, Statement ifFalse)
+ }
+
void _checkGuardType(Expression expression, Type type) {
// TODO(paulberry): harmonize this with analyzer's checkForNonBoolExpression
// TODO(paulberry): spec says the type must be `bool` or `dynamic`. This
@@ -799,10 +884,9 @@
@override
Type get typeSchema {
- // Note: the type schema only matters for patterns that appear in variable
- // declarations, and variable declarations are not allowed to contain
- // constant patterns. So this code should only be reachable during error
- // recovery.
+ // Note: this follows what's proposed in
+ // https://github.com/dart-lang/language/issues/2458.
+ // TODO(paulberry): update this if necessary when that issue is closed.
_typeAnalyzer.errors?.assertInErrorRecovery();
return _typeAnalyzer.unknownType;
}
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 5907faa..e6b1c35 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
@@ -1865,6 +1865,38 @@
]);
});
+ test('ifCase splits control flow', () {
+ var x = Var('x');
+ var y = Var('y');
+ var z = Var('z');
+ var w = Var('w');
+ h.run([
+ declare(x, type: 'int'),
+ declare(y, type: 'int'),
+ declare(z, type: 'int'),
+ ifCase(expr('num'), w.pattern(type: 'int'), [
+ x.write(expr('int')).stmt,
+ y.write(expr('int')).stmt,
+ ], else_: [
+ y.write(expr('int')).stmt,
+ z.write(expr('int')).stmt,
+ ]),
+ checkAssigned(x, false),
+ checkAssigned(y, true),
+ checkAssigned(z, false),
+ ]);
+ });
+
+ test('ifCase does not promote when expression true', () {
+ var x = Var('x');
+ h.run([
+ declare(x, type: 'int?', initializer: expr('int?')),
+ ifCase(x.expr.notEq(nullLiteral), intLiteral(0).pattern, [
+ checkNotPromoted(x),
+ ]),
+ ]);
+ });
+
test('promote promotes to a subtype and sets type of interest', () {
var x = Var('x');
h.run([
diff --git a/pkg/_fe_analyzer_shared/test/mini_ast.dart b/pkg/_fe_analyzer_shared/test/mini_ast.dart
index 8da68f5..9175905 100644
--- a/pkg/_fe_analyzer_shared/test/mini_ast.dart
+++ b/pkg/_fe_analyzer_shared/test/mini_ast.dart
@@ -186,6 +186,19 @@
location: location);
}
+Statement ifCase(
+ Expression expression, CaseHead caseHead, List<Statement> ifTrue,
+ {List<Statement>? else_}) {
+ var location = computeLocation();
+ return new _IfCase(
+ expression,
+ caseHead._pattern!,
+ caseHead._guard,
+ _Block(ifTrue, location: location),
+ else_ == null ? null : _Block(else_, location: location),
+ location: location);
+}
+
Expression intLiteral(int value, {bool? expectConversionToDouble}) =>
new _IntLiteral(value,
expectConversionToDouble: expectConversionToDouble,
@@ -423,6 +436,7 @@
static const Map<String, bool> _coreSubtypes = const {
'bool <: int': false,
'bool <: Object': true,
+ 'double <: bool': false,
'double <: double?': true,
'double <: Object': true,
'double <: Object?': true,
@@ -1727,20 +1741,18 @@
}
}
-class _If extends Statement {
+class _If extends _IfBase {
final Expression condition;
- final Statement ifTrue;
- final Statement? ifFalse;
- _If(this.condition, this.ifTrue, this.ifFalse, {required super.location});
+ _If(this.condition, super.ifTrue, super.ifFalse, {required super.location});
+
+ @override
+ String get _conditionPartString => condition.toString();
@override
void preVisit(PreVisitor visitor) {
condition.preVisit(visitor);
- visitor._assignedVariables.beginNode();
- ifTrue.preVisit(visitor);
- visitor._assignedVariables.endNode(this);
- ifFalse?.preVisit(visitor);
+ super.preVisit(visitor);
}
@override
@@ -1752,6 +1764,68 @@
}
}
+abstract class _IfBase extends Statement {
+ final Statement ifTrue;
+ final Statement? ifFalse;
+
+ _IfBase(this.ifTrue, this.ifFalse, {required super.location});
+
+ String get _conditionPartString;
+
+ @override
+ void preVisit(PreVisitor visitor) {
+ visitor._assignedVariables.beginNode();
+ ifTrue.preVisit(visitor);
+ visitor._assignedVariables.endNode(this);
+ ifFalse?.preVisit(visitor);
+ }
+
+ @override
+ String toString() =>
+ 'if ($_conditionPartString) $ifTrue' +
+ (ifFalse == null ? '' : 'else $ifFalse');
+}
+
+class _IfCase extends _IfBase {
+ final Expression _expression;
+ final Pattern _pattern;
+ final Expression? _guard;
+
+ _IfCase(
+ this._expression, this._pattern, this._guard, super.ifTrue, super.ifFalse,
+ {required super.location});
+
+ @override
+ String get _conditionPartString => '$_expression case $_pattern';
+
+ @override
+ void preVisit(PreVisitor visitor) {
+ _expression.preVisit(visitor);
+ var variableBinder = VariableBinder<Node, Var, Type>(visitor);
+ _pattern.preVisit(visitor, variableBinder);
+ variableBinder.finish();
+ _guard?.preVisit(visitor);
+ super.preVisit(visitor);
+ }
+
+ @override
+ void visit(Harness h) {
+ h.typeAnalyzer.analyzeIfCaseStatement(
+ this, _expression, _pattern, _guard, ifTrue, ifFalse);
+ h.irBuilder.apply(
+ 'ifCase',
+ [
+ Kind.expression,
+ Kind.pattern,
+ Kind.expression,
+ Kind.statement,
+ Kind.statement
+ ],
+ Kind.statement,
+ location: location);
+ }
+}
+
class _IfNull extends Expression {
final Expression lhs;
final Expression rhs;
@@ -2192,22 +2266,6 @@
flow.operations.promoteToNonNull(leftType), rightType));
}
- void analyzeIfStatement(Statement node, Expression condition,
- Statement ifTrue, Statement? ifFalse) {
- flow.ifStatement_conditionBegin();
- analyzeExpression(condition, unknownType);
- flow.ifStatement_thenBegin(condition, node);
- dispatchStatement(ifTrue);
- if (ifFalse == null) {
- handleNoStatement(node);
- flow.ifStatement_end(false);
- } else {
- flow.ifStatement_elseBegin();
- dispatchStatement(ifFalse);
- flow.ifStatement_end(true);
- }
- }
-
void analyzeLabeledStatement(Statement node, Statement body) {
flow.labeledStatement_begin(node);
dispatchStatement(body);
@@ -2441,6 +2499,7 @@
_irBuilder.atom('failure', Kind.expression, location: node.location);
}
+ @override
void handleNoStatement(Node node) {
_irBuilder.atom('noop', Kind.statement, location: node.location);
}
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 3968b97..11b0601 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
@@ -176,6 +176,102 @@
});
group('Statements:', () {
+ group('If:', () {
+ test('Condition context', () {
+ h.run([
+ if_(expr('dynamic').checkContext('bool'), [
+ expr('Object').stmt,
+ ]).checkIr('if(expr(dynamic), block(stmt(expr(Object))), noop)'),
+ ]);
+ });
+
+ test('With else', () {
+ h.run([
+ if_(expr('bool'), [
+ expr('Object').stmt,
+ ], [
+ expr('String').stmt,
+ ]).checkIr('if(expr(bool), block(stmt(expr(Object))), '
+ 'block(stmt(expr(String))))'),
+ ]);
+ });
+ });
+
+ group('If-case:', () {
+ test('Type schema', () {
+ var x = Var('x');
+ h.run([
+ ifCase(expr('int').checkContext('?'), x.pattern(type: 'num'), [
+ expr('Object').stmt,
+ ]).checkIr('ifCase(expr(int), '
+ 'varPattern(x, matchedType: int, staticType: num), true, '
+ 'block(stmt(expr(Object))), noop)'),
+ ]);
+ });
+
+ test('With else', () {
+ var x = Var('x');
+ h.run([
+ ifCase(expr('num'), x.pattern(type: 'int'), [
+ expr('Object').stmt,
+ ], else_: [
+ expr('String').stmt,
+ ]).checkIr('ifCase(expr(num), '
+ 'varPattern(x, matchedType: num, staticType: int), true, '
+ 'block(stmt(expr(Object))), block(stmt(expr(String))))'),
+ ]);
+ });
+
+ test('With guard', () {
+ var x = Var('x');
+ h.run([
+ ifCase(expr('num'),
+ x.pattern(type: 'int').when(x.expr.eq(intLiteral(0))), [
+ expr('Object').stmt,
+ ]).checkIr('ifCase(expr(num), '
+ 'varPattern(x, matchedType: num, staticType: int), ==(x, 0), '
+ 'block(stmt(expr(Object))), noop)'),
+ ]);
+ });
+
+ test('Allows refutable patterns', () {
+ var x = Var('x');
+ h.run([
+ ifCase(expr('num').checkContext('?'), x.pattern(type: 'int'), [
+ expr('Object').stmt,
+ ]).checkIr('ifCase(expr(num), '
+ 'varPattern(x, matchedType: num, staticType: int), true, '
+ 'block(stmt(expr(Object))), noop)'),
+ ]);
+ });
+
+ group('Guard not assignable to bool', () {
+ test('int', () {
+ var x = Var('x');
+ h.run([
+ ifCase(expr('int'),
+ x.pattern().when(expr('int')..errorId = 'GUARD'), []),
+ ], expectedErrors: {
+ 'nonBooleanCondition(GUARD)'
+ });
+ });
+
+ test('bool', () {
+ var x = Var('x');
+ h.run([
+ ifCase(expr('int'), x.pattern().when(expr('bool')), []),
+ ], expectedErrors: {});
+ });
+
+ test('dynamic', () {
+ var x = Var('x');
+ h.run([
+ ifCase(expr('int'), x.pattern().when(expr('dynamic')), []),
+ ], expectedErrors: {});
+ });
+ });
+ });
+
group('Switch:', () {
test('Empty', () {
h.run([
diff --git a/pkg/front_end/test/spell_checking_list_code.txt b/pkg/front_end/test/spell_checking_list_code.txt
index 9babcc8..362e822 100644
--- a/pkg/front_end/test/spell_checking_list_code.txt
+++ b/pkg/front_end/test/spell_checking_list_code.txt
@@ -1180,6 +1180,7 @@
reusage
reversible
rewinds
+rework
rewrites
rewrote
rf