From 380a505b0d608244df5d0da38961ab1134a0c0c0 Mon Sep 17 00:00:00 2001 From: Paul Berry Date: Tue, 20 Sep 2022 17:15:15 +0000 Subject: [PATCH] Shared type analysis: add more pattern types. Support for the following pattern types is added to the (as yet unused) shared type analysis prototype: - Cast patterns - List patterns - Logical-and patterns - Logical-or patterns - Null-assert patterns - Null-check patterns - Wildcard patterns Change-Id: I923df94b5deef925ca94e6ff0c8eac0493f69c1c Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/257602 Reviewed-by: Chloe Stefantsova Commit-Queue: Paul Berry Reviewed-by: Konstantin Shcheglov --- .../lib/src/type_inference/type_analyzer.dart | 330 +++++++- .../src/type_inference/type_operations.dart | 11 + pkg/_fe_analyzer_shared/test/mini_ast.dart | 253 +++++- .../type_inference/type_inference_test.dart | 727 +++++++++++++++++- 4 files changed, 1292 insertions(+), 29 deletions(-) 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 e5a435d2452..5ad4d34f74a 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 @@ -150,6 +150,9 @@ mixin TypeAnalyzer analyzeCastPattern( + Node node, Node innerPattern, Type type) { + return new _CastPatternDispatchResult( + this, node, dispatchPattern(innerPattern), type); + } + /// Analyzes a constant pattern. [node] is the pattern itself, and /// [expression] is the constant expression. Depending on the client's /// representation, [node] and [expression] might or might not be identical. @@ -300,6 +313,47 @@ mixin TypeAnalyzer analyzeListPattern( + Node node, + {Type? elementType, + required List elements}) { + return new _ListPatternDispatchResult( + this, + node, + elementType, + [for (Node element in elements) dispatchPattern(element)]); + } + + /// Analyzes a logical-or or logical-and pattern. [node] is the pattern + /// itself, and [lhs] and [rhs] are the left and right sides of the `|` or `&` + /// operator. [isAnd] indicates whether [node] is a logical-or or a + /// logical-and. + /// + /// Stack effect: none. + PatternDispatchResult analyzeLogicalPattern( + Node node, Node lhs, Node rhs, + {required bool isAnd}) { + return new _LogicalPatternDispatchResult( + this, node, dispatchPattern(lhs), dispatchPattern(rhs), isAnd); + } + + /// Analyzes a null-check or null-assert pattern. [node] is the pattern + /// itself, [innerPattern] is the sub-pattern, and [isAssert] indicates + /// whether this is a null-check or a null-assert pattern. + /// + /// Stack effect: none. + PatternDispatchResult + analyzeNullCheckOrAssertPattern(Node node, Node innerPattern, + {required bool isAssert}) { + return new _NullCheckOrAssertPatternDispatchResult( + this, node, dispatchPattern(innerPattern), isAssert); + } + /// Analyzes an expression of the form `switch (expression) { cases }`. /// /// Stack effect: pushes (Expression, n * ExpressionCase), where n is the @@ -489,9 +543,12 @@ mixin TypeAnalyzer - analyzeVariablePattern(Node node, Variable variable, Type? declaredType, + analyzeVariablePattern(Node node, Variable? variable, Type? declaredType, {required bool isFinal}) { return new _VariablePatternDispatchResult( this, node, variable, declaredType, isFinal); @@ -585,6 +642,14 @@ mixin TypeAnalyzer { Type get staticType => _latestStaticType; } +/// Specialization of [PatternDispatchResult] returned by +/// [TypeAnalyzer.analyzeCastPattern] +class _CastPatternDispatchResult + extends _PatternDispatchResultImpl { + final PatternDispatchResult _innerPattern; + + final Type _type; + + _CastPatternDispatchResult( + super._typeAnalyzer, super.node, this._innerPattern, this._type); + + @override + Type get typeSchema => _typeAnalyzer.objectQuestionType; + + @override + void match( + Type matchedType, + Map> typeInfos, + MatchContext context) { + _innerPattern.match(_type, typeInfos, context); + // Stack: (Pattern) + _typeAnalyzer.handleCastPattern(node, matchedType: matchedType); + // Stack: (Pattern) + } +} + /// Specialization of [PatternDispatchResult] returned by /// [TypeAnalyzer.analyzeConstantPattern] class _ConstantPatternDispatchResult + extends _PatternDispatchResultImpl { + final Type? _elementType; + + final List> _elements; + + _ListPatternDispatchResult( + super.typeAnalyzer, super._node, this._elementType, this._elements); + + @override + Type get typeSchema { + Type? elementType = _elementType; + if (elementType == null) { + if (_elements.isEmpty) { + return _typeAnalyzer.objectQuestionType; + } + elementType = _elements[0].typeSchema; + for (int i = 1; i < _elements.length; i++) { + elementType = _typeAnalyzer.typeOperations + .glb(elementType!, _elements[i].typeSchema); + } + } + return _typeAnalyzer.listType(elementType!); + } + + @override + void match( + Type matchedType, + Map> typeInfos, + MatchContext context) { + // Stack: () + Type? elementType = _typeAnalyzer.typeOperations.matchListType(matchedType); + if (elementType == null) { + if (_typeAnalyzer.typeOperations.isDynamic(matchedType)) { + elementType = _typeAnalyzer.dynamicType; + } else { + elementType = _typeAnalyzer.objectQuestionType; + } + } + for (PatternDispatchResult element + in _elements) { + element.match(elementType, typeInfos, context); + } + // Stack: (n * Pattern) where n = _elements.length + Type? requiredType = _typeAnalyzer.listType(_elementType ?? elementType); + Node? irrefutableContext = context.irrefutableContext; + if (irrefutableContext != null && + !_typeAnalyzer.typeOperations + .isAssignableTo(matchedType, requiredType)) { + _typeAnalyzer.errors?.patternTypeMismatchInIrrefutableContext( + pattern: node, + context: irrefutableContext, + matchedType: matchedType, + requiredType: requiredType); + } + _typeAnalyzer.handleListPattern(node, _elements.length, + matchedType: matchedType, requiredType: requiredType); + // Stack: (Pattern) + } +} + +/// Specialization of [PatternDispatchResult] returned by +/// [TypeAnalyzer.analyzeLogicalPattern] +class _LogicalPatternDispatchResult + extends _PatternDispatchResultImpl { + final PatternDispatchResult _lhs; + + final PatternDispatchResult _rhs; + + final bool _isAnd; + + _LogicalPatternDispatchResult( + super._typeAnalyzer, super.node, this._lhs, this._rhs, this._isAnd); + + @override + Type get typeSchema { + if (_isAnd) { + return _typeAnalyzer.typeOperations.glb(_lhs.typeSchema, _rhs.typeSchema); + } else { + // Logical-or patterns are only allowed in refutable contexts, and + // refutable contexts don't propagate a type schema into the scrutinee. + // So this code path is only reachable if the user's code contains errors. + _typeAnalyzer.errors?.assertInErrorRecovery(); + return _typeAnalyzer.unknownType; + } + } + + @override + void match( + Type matchedType, + Map> typeInfos, + MatchContext context) { + // Stack: () + if (!_isAnd) { + Node? irrefutableContext = context.irrefutableContext; + if (irrefutableContext != null) { + _typeAnalyzer.errors + ?.refutablePatternInIrrefutableContext(node, irrefutableContext); + // Avoid cascading errors + context = context.makeRefutable(); + } + } + _lhs.match(matchedType, typeInfos, context); + // Stack: (Pattern left) + _rhs.match(matchedType, typeInfos, context); + // Stack: (Pattern left, Pattern right) + _typeAnalyzer.handleLogicalPattern(node, + isAnd: _isAnd, matchedType: matchedType); + // Stack: (Pattern) + } +} + +/// Specialization of [PatternDispatchResult] returned by +/// [TypeAnalyzer.analyzeNullCheckOrAssertPattern] +class _NullCheckOrAssertPatternDispatchResult + extends _PatternDispatchResultImpl { + final PatternDispatchResult _innerPattern; + + final bool _isAssert; + + _NullCheckOrAssertPatternDispatchResult( + super._typeAnalyzer, super.node, this._innerPattern, this._isAssert); + + @override + Type get typeSchema { + if (_isAssert) { + return _typeAnalyzer.typeOperations + .makeNullable(_innerPattern.typeSchema); + } else { + // Null-check patterns are only allowed in refutable contexts, and + // refutable contexts don't propagate a type schema into the scrutinee. + // So this code path is only reachable if the user's code contains errors. + _typeAnalyzer.errors?.assertInErrorRecovery(); + return _typeAnalyzer.unknownType; + } + } + + @override + void match( + Type matchedType, + Map> typeInfos, + MatchContext context) { + // Stack: () + Type innerMatchedType = + _typeAnalyzer.typeOperations.promoteToNonNull(matchedType); + Node? irrefutableContext = context.irrefutableContext; + if (irrefutableContext != null && !_isAssert) { + _typeAnalyzer.errors + ?.refutablePatternInIrrefutableContext(node, irrefutableContext); + // Avoid cascading errors + context = context.makeRefutable(); + } + _innerPattern.match(innerMatchedType, typeInfos, context); + // Stack: (Pattern) + _typeAnalyzer.handleNullCheckOrAssertPattern(node, + matchedType: matchedType, isAssert: _isAssert); + // Stack: (Pattern) + } +} + /// Common base class for all specializations of [PatternDispatchResult] /// returned by methods in [TypeAnalyzer]. abstract class _PatternDispatchResultImpl extends _PatternDispatchResultImpl { - final Variable _variable; + final Variable? _variable; final Type? _declaredType; @@ -967,22 +1260,25 @@ class _VariablePatternDispatchResult { /// TODO(paulberry): once the analyzer and front end both use [TypeAnalyzer], /// combine this mixin with [TypeOperations]. mixin TypeOperations2 implements TypeOperations { + /// Computes the greatest lower bound of [type1] and [type2]. + Type glb(Type type1, Type type2); + /// Returns `true` if [fromType] is assignable to [toType]. bool isAssignableTo(Type fromType, Type toType); @@ -92,4 +95,12 @@ mixin TypeOperations2 implements TypeOperations { /// Computes the least upper bound of [type1] and [type2]. Type lub(Type type1, Type type2); + + /// Computes the nullable form of [type], in other words the least upper bound + /// of [type] and `Null`. + Type makeNullable(Type type); + + /// If [type] is a subtype of the type `List` for some `T`, returns the + /// type `T`. Otherwise returns `null`. + Type? matchListType(Type type); } diff --git a/pkg/_fe_analyzer_shared/test/mini_ast.dart b/pkg/_fe_analyzer_shared/test/mini_ast.dart index b2cafa574c0..aae37d411d1 100644 --- a/pkg/_fe_analyzer_shared/test/mini_ast.dart +++ b/pkg/_fe_analyzer_shared/test/mini_ast.dart @@ -204,6 +204,10 @@ Expression intLiteral(int value, {bool? expectConversionToDouble}) => expectConversionToDouble: expectConversionToDouble, location: computeLocation()); +Pattern listPattern(List elements, {String? elementType}) => + _ListPattern(elementType == null ? null : Type(elementType), elements, + location: computeLocation()); + Statement localFunction(List body) { var location = computeLocation(); return _LocalFunction(_Block(body, location: location), location: location); @@ -252,6 +256,11 @@ Statement while_(Expression condition, List body) { location: location); } +Pattern wildcard( + {String? type, String? expectInferredType, bool isFinal = false}) => + _VariablePattern(type == null ? null : Type(type), null, expectInferredType, + isFinal: isFinal, location: computeLocation()); + mixin CaseHead implements CaseHeads, Node { @override List get _caseHeads => [this]; @@ -449,6 +458,8 @@ class Harness 'double <: int?': false, 'double <: String': false, 'dynamic <: int': false, + 'dynamic <: Null': false, + 'dynamic <: Object': false, 'int <: bool': false, 'int <: double': false, 'int <: double?': false, @@ -472,11 +483,14 @@ class Harness 'int? <: num?': true, 'int? <: Object': false, 'int? <: Object?': true, + 'List <: Object': true, 'Never <: Object?': true, + 'Null <: double?': true, 'Null <: int': false, 'Null <: Object': false, 'Null <: Object?': true, 'Null <: dynamic': true, + 'num <: double': false, 'num <: int': false, 'num <: Iterable': false, 'num <: List': false, @@ -500,6 +514,7 @@ class Harness 'List <: int': false, 'List <: Iterable': true, 'List <: Object': true, + 'List <: List': true, 'Never <: int': true, 'Never <: int?': true, 'Never <: Null': true, @@ -511,6 +526,7 @@ class Harness 'Object <: int': false, 'Object <: int?': false, 'Object <: List': false, + 'Object <: List': false, 'Object <: Null': false, 'Object <: num': false, 'Object <: num?': false, @@ -522,6 +538,7 @@ class Harness 'Object? <: Null': false, 'String <: int': false, 'String <: int?': false, + 'String <: List': false, 'String <: num': false, 'String <: num?': false, 'String <: Object': true, @@ -568,9 +585,21 @@ class Harness 'num* - Object': Type('Never'), }; + static final Map _coreGlbs = { + 'double, int': Type('Never'), + 'double?, int?': Type('Null'), + 'int?, num': Type('int'), + }; + static final Map _coreLubs = { 'double, int': Type('num'), + 'double?, int?': Type('num?'), + 'int, num': Type('num'), 'Never, int': Type('int'), + 'Null, int': Type('int?'), + '?, int': Type('int'), + '?, List': Type('List'), + '?, Null': Type('Null'), }; bool _started = false; @@ -587,6 +616,8 @@ class Harness final Map _factorResults = Map.of(_coreFactors); + final Map _glbs = Map.of(_coreGlbs); + final Map _lubs = Map.of(_coreLubs); final Map _members = {}; @@ -688,6 +719,15 @@ class Harness return _members[query] ?? fail('Unknown member query: $query'); } + @override + Type glb(Type type1, Type type2) { + if (type1.type == type2.type) return type1; + var typeNames = [type1.type, type2.type]; + typeNames.sort(); + var query = typeNames.join(', '); + return _glbs[query] ?? fail('Unknown glb query: $query'); + } + @override bool isAssignableTo(Type fromType, Type toType) { if (legacy && isSubtypeOf(toType, fromType)) return true; @@ -728,6 +768,19 @@ class Harness return _lubs[query] ?? fail('Unknown lub query: $query'); } + @override + Type makeNullable(Type type) => lub(type, Type('Null')); + + @override + Type? matchListType(Type type) { + if (type is NonFunctionType) { + if (type.args.length == 1) { + return type.args[0]; + } + } + return null; + } + @override Type promoteToNonNull(Type type) { if (type.type.endsWith('?')) { @@ -894,12 +947,27 @@ class Node { abstract class Pattern extends Node with CaseHead, CaseHeads { Pattern._({required super.location}) : super._(); + Pattern get nullAssert => + _NullCheckOrAssertPattern(this, true, location: computeLocation()); + + Pattern get nullCheck => + _NullCheckOrAssertPattern(this, false, location: computeLocation()); + @override Expression? get _guard => null; @override Pattern? get _pattern => this; + Pattern and(Pattern other) => + _LogicalPattern(this, other, isAnd: true, location: computeLocation()); + + Pattern as_(String type) => + new _CastPattern(this, Type(type), location: computeLocation()); + + Pattern or(Pattern other) => + _LogicalPattern(this, other, isAnd: false, location: computeLocation()); + void preVisit( PreVisitor visitor, VariableBinder variableBinder); @@ -1167,6 +1235,30 @@ class _CaseHeads with CaseHeads { _CaseHeads(this._caseHeads, this._labels); } +class _CastPattern extends Pattern { + final Pattern _inner; + + final Type _type; + + _CastPattern(this._inner, this._type, {required super.location}) : super._(); + + @override + void preVisit( + PreVisitor visitor, VariableBinder variableBinder) { + _inner.preVisit(visitor, variableBinder); + } + + @override + PatternDispatchResult visit(Harness h) { + return h.typeAnalyzer.analyzeCastPattern(this, _inner, _type); + } + + @override + String _debugString({required bool needsKeywordOrType}) => + '${_inner._debugString(needsKeywordOrType: needsKeywordOrType)} as ' + '${_type.type}'; +} + /// Representation of a single catch clause in a try/catch statement. Use /// [catch_] to create instances of this class. class _CatchClause { @@ -1490,7 +1582,7 @@ class _Declare extends Statement { if (initializer == null) { var pattern = this.pattern as _VariablePattern; var staticType = h.typeAnalyzer.analyzeUninitializedVariableDeclaration( - this, pattern.variable, pattern.declaredType, + this, pattern.variable!, pattern.declaredType, isFinal: isFinal, isLate: isLate); h.typeAnalyzer.handleVariablePattern(pattern, matchedType: staticType, staticType: staticType); @@ -1937,6 +2029,38 @@ class _LabeledStatement extends Statement { } } +class _ListPattern extends Pattern { + final Type? _elementType; + + final List _elements; + + _ListPattern(this._elementType, this._elements, {required super.location}) + : super._(); + + @override + void preVisit( + PreVisitor visitor, VariableBinder variableBinder) { + for (var element in _elements) { + element.preVisit(visitor, variableBinder); + } + } + + @override + PatternDispatchResult visit(Harness h) { + return h.typeAnalyzer.analyzeListPattern(this, + elementType: _elementType, elements: _elements); + } + + @override + String _debugString({required bool needsKeywordOrType}) { + var elements = [ + for (var element in _elements) + element._debugString(needsKeywordOrType: needsKeywordOrType) + ]; + return '[${elements.join(', ')}]'; + } +} + class _LocalFunction extends Statement { final Statement body; @@ -1991,6 +2115,49 @@ class _Logical extends Expression { } } +class _LogicalPattern extends Pattern { + final Pattern _lhs; + + final Pattern _rhs; + + final bool isAnd; + + _LogicalPattern(this._lhs, this._rhs, + {required this.isAnd, required super.location}) + : super._(); + + @override + void preVisit( + PreVisitor visitor, VariableBinder variableBinder) { + if (!isAnd) { + variableBinder.startAlternatives(); + variableBinder.startAlternative(_lhs); + } + _lhs.preVisit(visitor, variableBinder); + if (!isAnd) { + variableBinder.finishAlternative(); + variableBinder.startAlternative(_rhs); + } + _rhs.preVisit(visitor, variableBinder); + if (!isAnd) { + variableBinder.finishAlternative(); + variableBinder.finishAlternatives(); + } + } + + @override + PatternDispatchResult visit(Harness h) { + return h.typeAnalyzer.analyzeLogicalPattern(this, _lhs, _rhs, isAnd: isAnd); + } + + @override + _debugString({required bool needsKeywordOrType}) => [ + _lhs._debugString(needsKeywordOrType: false), + isAnd ? '&' : '|', + _rhs._debugString(needsKeywordOrType: false) + ].join(' '); +} + /// Enum representing the different ways an [LValue] might be used. enum _LValueDisposition { /// The [LValue] is being read from only, not written to. This happens if it @@ -2143,6 +2310,9 @@ class _MiniAstTypeAnalyzer late final Type nullType = Type('Null'); + @override + late final Type objectQuestionType = Type('Object?'); + @override late final Type unknownType = Type('?'); @@ -2484,6 +2654,16 @@ class _MiniAstTypeAnalyzer location: node.location); } + @override + void handleCastPattern(covariant _CastPattern node, + {required Type matchedType}) { + _irBuilder.atom(node._type.type, Kind.type, location: node.location); + _irBuilder.atom(matchedType.type, Kind.type, location: node.location); + _irBuilder.apply( + 'castPattern', [Kind.pattern, Kind.type, Kind.type], Kind.pattern, + names: ['matchedType'], location: node.location); + } + @override void handleConstantPattern(Node node, {required Type matchedType}) { _irBuilder.atom(matchedType.type, Kind.type, location: node.location); @@ -2496,6 +2676,28 @@ class _MiniAstTypeAnalyzer _irBuilder.atom('default', Kind.caseHead, location: node.location); } + @override + void handleListPattern(Node node, int numElements, + {required Type matchedType, required Type requiredType}) { + _irBuilder.atom(matchedType.type, Kind.type, location: node.location); + _irBuilder.atom(requiredType.type, Kind.type, location: node.location); + _irBuilder.apply( + 'listPattern', + [...List.filled(numElements, Kind.pattern), Kind.type, Kind.type], + Kind.pattern, + names: ['matchedType', 'requiredType'], + location: node.location); + } + + @override + void handleLogicalPattern(Node node, + {required bool isAnd, required Type matchedType}) { + _irBuilder.atom(matchedType.type, Kind.type, location: node.location); + _irBuilder.apply(isAnd ? 'logicalAndPattern' : 'logicalOrPattern', + [Kind.pattern, Kind.pattern, Kind.type], Kind.pattern, + names: ['matchedType'], location: node.location); + } + void handleNoCondition(Node node) { _irBuilder.atom('true', Kind.expression, location: node.location); } @@ -2518,13 +2720,23 @@ class _MiniAstTypeAnalyzer _irBuilder.atom('noop', Kind.statement, location: node.location); } + @override + void handleNullCheckOrAssertPattern(Node node, + {required Type matchedType, required bool isAssert}) { + _irBuilder.atom(matchedType.type, Kind.type, location: node.location); + _irBuilder.apply(isAssert ? 'nullAssertPattern' : 'nullCheckPattern', + [Kind.pattern, Kind.type], Kind.pattern, + names: ['matchedType'], location: node.location); + } + @override void handleSwitchScrutinee(Type type) {} @override void handleVariablePattern(covariant _VariablePattern node, {required Type matchedType, required Type staticType}) { - _irBuilder.atom(node.variable.name, Kind.variable, location: node.location); + _irBuilder.atom(node.variable?.name ?? '_', Kind.variable, + location: node.location); _irBuilder.atom(matchedType.type, Kind.type, location: node.location); _irBuilder.atom(staticType.type, Kind.type, location: node.location); _irBuilder.apply( @@ -2544,6 +2756,10 @@ class _MiniAstTypeAnalyzer Type leastUpperBound(Type t1, Type t2) => _harness._lub(t1, t2); + @override + Type listType(Type elementType) => + NonFunctionType('List', args: [elementType]); + _PropertyElement lookupInterfaceMember( Node node, Type receiverType, String memberName) { return _harness.getMember(receiverType, memberName); @@ -2661,6 +2877,32 @@ class _NullAwareAccess extends Expression { } } +class _NullCheckOrAssertPattern extends Pattern { + final Pattern _inner; + + final bool _isAssert; + + _NullCheckOrAssertPattern(this._inner, this._isAssert, + {required super.location}) + : super._(); + + @override + void preVisit( + PreVisitor visitor, VariableBinder variableBinder) { + _inner.preVisit(visitor, variableBinder); + } + + @override + PatternDispatchResult visit(Harness h) { + return h.typeAnalyzer + .analyzeNullCheckOrAssertPattern(this, _inner, isAssert: _isAssert); + } + + @override + String _debugString({required bool needsKeywordOrType}) => + '${_inner._debugString(needsKeywordOrType: needsKeywordOrType)}?'; +} + class _NullLiteral extends Expression { _NullLiteral({required super.location}); @@ -3046,7 +3288,7 @@ class _TryStatement extends TryStatement { class _VariablePattern extends Pattern { final Type? declaredType; - final Var variable; + final Var? variable; final String? expectInferredType; @@ -3059,7 +3301,8 @@ class _VariablePattern extends Pattern { @override void preVisit( PreVisitor visitor, VariableBinder variableBinder) { - if (variableBinder.add(this, variable)) { + var variable = this.variable; + if (variable != null && variableBinder.add(this, variable)) { visitor._assignedVariables.declare(variable); } } @@ -3076,7 +3319,7 @@ class _VariablePattern extends Pattern { declaredType!.type else if (needsKeywordOrType) 'var', - variable.name, + variable?.name ?? '_', if (expectInferredType != null) '(expected type $expectInferredType)' ].join(' '); } 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 11b06018def..348b69dae6c 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 @@ -605,7 +605,21 @@ main() { }); test('implicit/implicit type', () { - // TODO(paulberry): need more support to be able to test this + var x = Var('x'); + h.run([ + switch_( + expr('List'), + [ + (x.pattern()..errorId = 'PATTERN(List x)').then([]), + listPattern([x.pattern()..errorId = 'PATTERN(int x)']) + .then([]), + ], + isExhaustive: true), + ], expectedErrors: { + 'inconsistentMatchVar(pattern: PATTERN(int x), type: int, ' + 'previousPattern: PATTERN(List x), ' + 'previousType: List)' + }); }); }); @@ -1097,16 +1111,13 @@ main() { }); test('illegal late pattern', () { - // TODO(paulberry): once we support some kind of irrefutable pattern - // other than a variable declaration, adjust this test so that the only - // error it expects is `patternDoesNotAllowLate`. h.run([ - (match(intLiteral(1).pattern..errorId = 'PATTERN', intLiteral(0), + (match( + listPattern([wildcard()])..errorId = 'PATTERN', expr('List'), isLate: true) ..errorId = 'CONTEXT'), ], expectedErrors: { - 'patternDoesNotAllowLate(PATTERN)', - 'refutablePatternInIrrefutableContext(PATTERN, CONTEXT)' + 'patternDoesNotAllowLate(PATTERN)' }); }); @@ -1122,6 +1133,119 @@ main() { }); group('Patterns:', () { + group('Cast:', () { + test('Type schema', () { + var x = Var('x'); + h.run([ + switch_( + expr('num'), + [ + x.pattern().as_('int').then([]), + ], + isExhaustive: true) + .checkIr('switch(expr(num), ' + 'case(heads(head(castPattern(varPattern(x, matchedType: int, ' + 'staticType: int), int, matchedType: num), true)), ' + 'block()))'), + ]); + }); + + group('Missing var:', () { + test('default', () { + var x = Var('x'); + h.run([ + switch_( + expr('int'), + [ + x.pattern().as_('int').then([]), + (default_..errorId = 'DEFAULT').then([]), + ], + isExhaustive: true), + ], expectedErrors: { + 'missingMatchVar(DEFAULT, x)' + }); + }); + + test('case', () { + var x = Var('x'); + h.run([ + switch_( + expr('int'), + [ + (intLiteral(0).pattern..errorId = 'CASE_0').then([]), + x.pattern().as_('int').then([]), + ], + isExhaustive: true), + ], expectedErrors: { + 'missingMatchVar(CASE_0, x)' + }); + }); + + test('label', () { + var x = Var('x'); + var l = Label('l')..errorId = 'LABEL'; + h.run([ + switch_( + expr('int'), + [ + l.then(x.pattern().as_('int')).then([]), + ], + isExhaustive: true), + ], expectedErrors: { + 'missingMatchVar(LABEL, x)' + }); + }); + }); + + test('conflicting var:', () { + var x = Var('x'); + h.run([ + switch_( + expr('num'), + [ + (x.pattern()..errorId = 'INT_PATTERN').as_('int').then([]), + (x.pattern()..errorId = 'NUM_PATTERN').as_('num').then([]), + ], + isExhaustive: true), + ], expectedErrors: { + 'inconsistentMatchVar(pattern: NUM_PATTERN, type: num, ' + 'previousPattern: INT_PATTERN, previousType: int)' + }); + }); + + group('Refutability:', () { + test('When matched type is a subtype of variable type', () { + var x = Var('x'); + h.run([ + match(x.pattern().as_('num'), expr('int')) + .checkIr('match(expr(int), ' + 'castPattern(varPattern(x, matchedType: num, ' + 'staticType: num), num, matchedType: int))'), + ]); + }); + + test('When matched type is dynamic', () { + var x = Var('x'); + h.run([ + match(x.pattern().as_('num'), expr('dynamic')) + .checkIr('match(expr(dynamic), ' + 'castPattern(varPattern(x, matchedType: num, ' + 'staticType: num), num, matchedType: dynamic))'), + ]); + }); + + test('When matched type is not a subtype of variable type', () { + var x = Var('x'); + h.run([ + match(x.pattern().as_('num'), expr('String')) + .checkIr('match(expr(String), ' + 'castPattern(varPattern(x, matchedType: num, ' + 'staticType: num), num, matchedType: String))'), + ]); + }); + }); + }); + group('Const or literal:', () { test('Refutability', () { h.run([ @@ -1133,6 +1257,431 @@ main() { }); }); + group('List:', () { + group('Type schema:', () { + test('Explicit element type', () { + var x = Var('x'); + h.run([ + match(listPattern([x.pattern()], elementType: 'int'), + expr('dynamic').checkContext('List')), + ]); + }); + + group('Implicit element type:', () { + test('Empty', () { + h.run([ + match(listPattern([]), expr('dynamic').checkContext('Object?')), + ]); + }); + + test('Non-empty', () { + var x = Var('x'); + var y = Var('y'); + h.run([ + match( + listPattern( + [x.pattern(type: 'int?'), y.pattern(type: 'num')]), + expr('dynamic').checkContext('List')), + ]); + }); + }); + }); + + group('Static type:', () { + test('Explicit type', () { + var x = Var('x'); + h.run([ + match(listPattern([x.pattern(type: 'num')], elementType: 'int'), + expr('dynamic')) + .checkIr('match(expr(dynamic), ' + 'listPattern(varPattern(x, matchedType: dynamic, ' + 'staticType: num), ' + 'matchedType: dynamic, requiredType: List))'), + ]); + }); + + test('Matched type is a list', () { + var x = Var('x'); + h.run([ + match(listPattern([x.pattern(expectInferredType: 'int')]), + expr('List')) + .checkIr('match(expr(List), ' + 'listPattern(varPattern(x, matchedType: int, ' + 'staticType: int), matchedType: List, ' + 'requiredType: List))'), + ]); + }); + + test('Matched type is dynamic', () { + var x = Var('x'); + h.run([ + match(listPattern([x.pattern(expectInferredType: 'dynamic')]), + expr('dynamic')) + .checkIr('match(expr(dynamic), ' + 'listPattern(varPattern(x, matchedType: dynamic, ' + 'staticType: dynamic), matchedType: dynamic, ' + 'requiredType: List))'), + ]); + }); + + test('Matched type is other', () { + var x = Var('x'); + h.run([ + switch_( + expr('Object'), + [ + listPattern([x.pattern(expectInferredType: 'Object?')]) + .then([]), + ], + isExhaustive: false) + .checkIr('switch(expr(Object), ' + 'case(heads(head(listPattern(varPattern(x, ' + 'matchedType: Object?, staticType: Object?), ' + 'matchedType: Object, requiredType: List), ' + 'true)), block()))'), + ]); + }); + }); + + group('Refutability:', () { + test('When matched type is a subtype of pattern type', () { + h.run([ + match( + listPattern([wildcard()], elementType: 'num'), + expr('List'), + ).checkIr('match(expr(List), ' + 'listPattern(varPattern(_, matchedType: int, staticType: int), ' + 'matchedType: List, requiredType: List))'), + ]); + }); + + test('When matched type is dynamic', () { + h.run([ + match(listPattern([wildcard()], elementType: 'num'), + expr('dynamic')) + .checkIr('match(expr(dynamic), ' + 'listPattern(varPattern(_, matchedType: dynamic, ' + 'staticType: dynamic), matchedType: dynamic, ' + 'requiredType: List))'), + ]); + }); + + test('When matched type is not a subtype of variable type', () { + h.run([ + (match( + listPattern([wildcard()], elementType: 'num') + ..errorId = 'PATTERN', + expr('String')) + ..errorId = 'CONTEXT'), + ], expectedErrors: { + 'patternTypeMismatchInIrrefutableContext(pattern: PATTERN, ' + 'context: CONTEXT, matchedType: String, ' + 'requiredType: List)' + }); + }); + + test('Sub-refutability', () { + h.run([ + (match( + listPattern([ + wildcard(type: 'int')..errorId = 'INT', + wildcard(type: 'double')..errorId = 'DOUBLE' + ], elementType: 'num'), + expr('List')) + ..errorId = 'CONTEXT'), + ], expectedErrors: { + 'patternTypeMismatchInIrrefutableContext(pattern: INT, ' + 'context: CONTEXT, matchedType: num, requiredType: int)', + 'patternTypeMismatchInIrrefutableContext(pattern: DOUBLE, ' + 'context: CONTEXT, matchedType: num, requiredType: double)' + }); + }); + }); + + test('Match var overlap', () { + var x = Var('x'); + h.run([ + match( + listPattern( + [x.pattern()..errorId = 'LHS', x.pattern()..errorId = 'RHS']), + expr('List')), + ], expectedErrors: { + 'matchVarOverlap(pattern: RHS, previousPattern: LHS)' + }); + }); + }); + + group('Logical-and:', () { + test('Type schema', () { + h.run([ + match(wildcard(type: 'int?').and(wildcard(type: 'double?')), + nullLiteral.checkContext('Null')) + .checkIr('match(null, ' + 'logicalAndPattern(varPattern(_, matchedType: Null, ' + 'staticType: int?), ' + 'varPattern(_, matchedType: Null, staticType: double?), ' + 'matchedType: Null))'), + ]); + }); + + test('Refutability', () { + h.run([ + (match( + (wildcard(type: 'int')..errorId = 'LHS') + .and(wildcard(type: 'double')..errorId = 'RHS'), + expr('num')) + ..errorId = 'CONTEXT'), + ], expectedErrors: { + 'patternTypeMismatchInIrrefutableContext(pattern: LHS, ' + 'context: CONTEXT, matchedType: num, requiredType: int)', + 'patternTypeMismatchInIrrefutableContext(pattern: RHS, ' + 'context: CONTEXT, matchedType: num, requiredType: double)' + }); + }); + + test('Match var overlap', () { + var x = Var('x'); + h.run([ + match( + (x.pattern()..errorId = 'LHS').and(x.pattern()..errorId = 'RHS'), + expr('int')), + ], expectedErrors: { + 'matchVarOverlap(pattern: RHS, previousPattern: LHS)' + }); + }); + }); + + group('Logical-or:', () { + test('Type schema', () { + h.run([ + (match( + wildcard(type: 'int?').or(wildcard(type: 'double?')) + ..errorId = 'PATTERN', + nullLiteral.checkContext('?'), + )..errorId = 'CONTEXT'), + ], expectedErrors: { + 'refutablePatternInIrrefutableContext(PATTERN, CONTEXT)' + }); + }); + + test('Refutability', () { + // Note: even though the logical-or contains refutable sub-patterns, we + // don't issue errors for them because they would overlap with the error + // we're issuing for the logical-or pattern as a whole. + h.run([ + (match( + wildcard(type: 'int').or(wildcard(type: 'double')) + ..errorId = 'PATTERN', + expr('num')) + ..errorId = 'CONTEXT'), + ], expectedErrors: { + 'refutablePatternInIrrefutableContext(PATTERN, CONTEXT)' + }); + }); + + test('Missing var', () { + var x = Var('x'); + h.run([ + ifCase(expr('int'), x.pattern().or(wildcard()..errorId = 'WILDCARD'), + []), + ], expectedErrors: { + 'missingMatchVar(WILDCARD, x)' + }); + }); + + group('Conflicting var:', () { + test('explicit/explicit type', () { + var x = Var('x'); + h.run([ + ifCase( + expr('int'), + (x.pattern(type: 'int')..errorId = 'PATTERN(int x)') + .or(x.pattern(type: 'num')..errorId = 'PATTERN(num x)'), + []), + ], expectedErrors: { + 'inconsistentMatchVar(pattern: PATTERN(num x), type: num, ' + 'previousPattern: PATTERN(int x), previousType: int)' + }); + }); + + test('explicit/implicit type', () { + // TODO(paulberry): not sure whether this should be treated as a + // conflict. See https://github.com/dart-lang/language/issues/2424. + var x = Var('x'); + h.run([ + ifCase( + expr('int'), + (x.pattern()..errorId = 'PATTERN(x)') + .or(x.pattern(type: 'int')..errorId = 'PATTERN(int x)'), + [], + ), + ], expectedErrors: { + 'inconsistentMatchVarExplicitness(pattern: PATTERN(int x), ' + 'previousPattern: PATTERN(x))' + }); + }); + + test('implicit/implicit type', () { + var x = Var('x'); + h.run([ + ifCase( + expr('List'), + (x.pattern()..errorId = 'PATTERN(List x)') + .or(listPattern([x.pattern()..errorId = 'PATTERN(int x)'])), + [], + ), + ], expectedErrors: { + 'inconsistentMatchVar(pattern: PATTERN(int x), type: int, ' + 'previousPattern: PATTERN(List x), ' + 'previousType: List)' + }); + }); + + group('Error recovery:', () { + test('Each type compared to previous', () { + var x = Var('x'); + h.run([ + ifCase( + expr('int'), + (x.pattern(type: 'int')..errorId = 'PATTERN1') + .or(x.pattern(type: 'num')..errorId = 'PATTERN2') + .or(x.pattern(type: 'num')..errorId = 'PATTERN3') + .or(x.pattern(type: 'int')..errorId = 'PATTERN4'), + []), + ], expectedErrors: { + 'inconsistentMatchVar(pattern: PATTERN2, type: num, ' + 'previousPattern: PATTERN1, previousType: int)', + 'inconsistentMatchVar(pattern: PATTERN4, type: int, ' + 'previousPattern: PATTERN3, previousType: num)' + }); + }); + + test('Each type explicitness compared to previous', () { + var x = Var('x'); + h.run([ + ifCase( + expr('int'), + (x.pattern(type: 'int')..errorId = 'PATTERN1') + .or(x.pattern()..errorId = 'PATTERN2') + .or(x.pattern()..errorId = 'PATTERN3') + .or(x.pattern(type: 'int')..errorId = 'PATTERN4'), + []), + ], expectedErrors: { + 'inconsistentMatchVarExplicitness(pattern: PATTERN2, ' + 'previousPattern: PATTERN1)', + 'inconsistentMatchVarExplicitness(pattern: PATTERN4, ' + 'previousPattern: PATTERN3)' + }); + }); + }); + }); + }); + + group('Null-assert:', () { + test('Type schema', () { + var x = Var('x'); + h.run([ + match(x.pattern(type: 'int').nullAssert, + expr('int').checkContext('int?')) + .checkIr('match(expr(int), ' + 'nullAssertPattern(varPattern(x, matchedType: int, ' + 'staticType: int), matchedType: int))'), + ]); + }); + + group('Refutability:', () { + test('When matched type is nullable', () { + h.run([ + match(wildcard().nullAssert, expr('int?')) + .checkIr('match(expr(int?), ' + 'nullAssertPattern(varPattern(_, matchedType: int, ' + 'staticType: int), matchedType: int?))'), + ]); + }); + + test('When matched type is non-nullable', () { + h.run([ + match(wildcard().nullAssert, expr('int')) + .checkIr('match(expr(int), ' + 'nullAssertPattern(varPattern(_, matchedType: int, ' + 'staticType: int), matchedType: int))'), + ]); + }); + + test('When matched type is dynamic', () { + h.run([ + match(wildcard().nullAssert, expr('dynamic')) + .checkIr('match(expr(dynamic), ' + 'nullAssertPattern(varPattern(_, matchedType: dynamic, ' + 'staticType: dynamic), matchedType: dynamic))'), + ]); + }); + + test('Sub-refutability', () { + h.run([ + (match((wildcard(type: 'int')..errorId = 'INT').nullAssert, + expr('num')) + ..errorId = 'CONTEXT'), + ], expectedErrors: { + 'patternTypeMismatchInIrrefutableContext(pattern: INT, ' + 'context: CONTEXT, matchedType: num, requiredType: int)' + }); + }); + }); + }); + + group('Null-check:', () { + test('Type schema', () { + var x = Var('x'); + h.run([ + (match(x.pattern(type: 'int').nullCheck..errorId = 'PATTERN', + expr('int').checkContext('?')) + ..errorId = 'CONTEXT'), + ], expectedErrors: { + 'refutablePatternInIrrefutableContext(PATTERN, CONTEXT)' + }); + }); + + group('Refutability:', () { + test('When matched type is nullable', () { + h.run([ + (match(wildcard().nullCheck..errorId = 'PATTERN', expr('int?')) + ..errorId = 'CONTEXT'), + ], expectedErrors: { + 'refutablePatternInIrrefutableContext(PATTERN, CONTEXT)' + }); + }); + + test('When matched type is non-nullable', () { + h.run([ + (match(wildcard().nullCheck..errorId = 'PATTERN', expr('int')) + ..errorId = 'CONTEXT'), + ], expectedErrors: { + 'refutablePatternInIrrefutableContext(PATTERN, CONTEXT)' + }); + }); + + test('When matched type is dynamic', () { + h.run([ + (match(wildcard().nullCheck..errorId = 'PATTERN', expr('dynamic')) + ..errorId = 'CONTEXT'), + ], expectedErrors: { + 'refutablePatternInIrrefutableContext(PATTERN, CONTEXT)' + }); + }); + + test('Sub-refutability', () { + h.run([ + (match(wildcard(type: 'int').nullCheck..errorId = 'PATTERN', + expr('num')) + ..errorId = 'CONTEXT'), + ], expectedErrors: { + 'refutablePatternInIrrefutableContext(PATTERN, CONTEXT)' + }); + }); + }); + }); + group('Variable:', () { group('Refutability:', () { test('When matched type is a subtype of variable type', () { @@ -1165,5 +1714,169 @@ main() { }); }); }); + + group('Wildcard:', () { + test('Untyped', () { + h.run([ + switch_( + expr('int'), + [ + wildcard().then([]), + ], + isExhaustive: true) + .checkIr('switch(expr(int), ' + 'case(heads(head(varPattern(_, matchedType: int, ' + 'staticType: int), true)), block()))'), + ]); + }); + + test('Typed', () { + h.run([ + switch_( + expr('num'), + [ + wildcard(type: 'int').then([]), + ], + isExhaustive: true) + .checkIr('switch(expr(num), ' + 'case(heads(head(varPattern(_, matchedType: num, ' + 'staticType: int), true)), block()))'), + ]); + }); + + group('Exempt from errors:', () { + group('Missing var:', () { + test('default', () { + h.run([ + switch_( + expr('int'), + [ + wildcard().then([]), + default_.then([]), + ], + isExhaustive: true) + .checkIr('switch(expr(int), ' + 'case(heads(head(varPattern(_, matchedType: int, ' + 'staticType: int), true), default), ' + 'block()))'), + ]); + }); + + test('case', () { + h.run([ + switch_( + expr('int'), + [ + intLiteral(0).pattern.then([]), + wildcard().then([]), + ], + isExhaustive: true) + .checkIr('switch(expr(int), ' + 'case(heads(head(const(0, matchedType: int), true), ' + 'head(varPattern(_, matchedType: int, ' + 'staticType: int), true)), block()))'), + ]); + }); + + test('label', () { + var l = Label('l'); + h.run([ + switch_( + expr('int'), + [ + l.then(wildcard()).then([]), + ], + isExhaustive: true) + .checkIr('switch(expr(int), ' + 'case(heads(head(varPattern(_, matchedType: int, ' + 'staticType: int), true), l), ' + 'block()))'), + ]); + }); + }); + + group('conflicting var:', () { + test('explicit/explicit type', () { + h.run([ + switch_( + expr('num'), + [ + wildcard(type: 'int').then([]), + wildcard(type: 'num').then([]), + ], + isExhaustive: true) + .checkIr('switch(expr(num), ' + 'case(heads(head(varPattern(_, matchedType: num, ' + 'staticType: int), true), ' + 'head(varPattern(_, matchedType: num, ' + 'staticType: num), true)), block()))'), + ]); + }); + + test('explicit/implicit type', () { + h.run([ + switch_( + expr('int'), + [ + wildcard().then([]), + wildcard(type: 'int').then([]), + ], + isExhaustive: true) + .checkIr('switch(expr(int), ' + 'case(heads(head(varPattern(_, matchedType: int, ' + 'staticType: int), true), ' + 'head(varPattern(_, matchedType: int, ' + 'staticType: int), true)), block()))'), + ]); + }); + + test('implicit/implicit type', () { + h.run([ + switch_( + expr('List'), + [ + wildcard().then([]), + listPattern([wildcard()]).then([]), + ], + isExhaustive: true) + .checkIr('switch(expr(List), ' + 'case(heads(head(varPattern(_, matchedType: List, ' + 'staticType: List), true), ' + 'head(listPattern(varPattern(_, matchedType: int, ' + 'staticType: int), matchedType: List, ' + 'requiredType: List), true)), block()))'), + ]); + }); + }); + }); + + group('Refutability:', () { + test('When matched type is a subtype of variable type', () { + h.run([ + match(wildcard(type: 'num'), expr('int')) + .checkIr('match(expr(int), ' + 'varPattern(_, matchedType: int, staticType: num))'), + ]); + }); + + test('When matched type is dynamic', () { + h.run([ + match(wildcard(type: 'num'), expr('dynamic')) + .checkIr('match(expr(dynamic), ' + 'varPattern(_, matchedType: dynamic, staticType: num))'), + ]); + }); + + test('When matched type is not a subtype of variable type', () { + h.run([ + (match(wildcard(type: 'num')..errorId = 'PATTERN', expr('String')) + ..errorId = 'CONTEXT'), + ], expectedErrors: { + 'patternTypeMismatchInIrrefutableContext(pattern: PATTERN, ' + 'context: CONTEXT, matchedType: String, requiredType: num)' + }); + }); + }); + }); }); }