3
3
// BSD-style license that can be found in the LICENSE file.
4
4
5
5
import 'static_type.dart' ;
6
+ import 'key.dart' ;
7
+ import 'path.dart' ;
8
+ import 'profile.dart' as profile;
9
+ import 'space.dart' ;
6
10
import 'witness.dart' ;
7
11
8
12
/// Indicates whether the "fallback" exhaustiveness algorithm (based on flow
@@ -14,6 +18,255 @@ import 'witness.dart';
14
18
/// exhaustiveness algorithm) when it is no longer needed.
15
19
bool useFallbackExhaustivenessAlgorithm = true ;
16
20
21
+ /// Returns `true` if [caseSpaces] exhaustively covers all possible values of
22
+ /// [valueSpace] .
23
+ bool isExhaustive (Space valueSpace, List <Space > caseSpaces) {
24
+ return checkExhaustiveness (valueSpace, caseSpaces) == null ;
25
+ }
26
+
27
+ /// Checks the [cases] representing a series of switch cases to see if they
28
+ /// exhaustively cover all possible values of the matched [valueType] . Also
29
+ /// checks to see if any case can't be matched because it's covered by previous
30
+ /// cases.
31
+ ///
32
+ /// Returns a list of any unreachable case or non-exhaustive match errors.
33
+ /// Returns an empty list if all cases are reachable and the cases are
34
+ /// exhaustive.
35
+ List <ExhaustivenessError > reportErrors (
36
+ StaticType valueType, List <Space > cases) {
37
+ List <ExhaustivenessError > errors = < ExhaustivenessError > [];
38
+
39
+ Space valuePattern = new Space (const Path .root (), valueType);
40
+ List <List <Space >> caseRows = cases.map ((space) => [space]).toList ();
41
+
42
+ for (int i = 1 ; i < caseRows.length; i++ ) {
43
+ // See if this case is covered by previous ones.
44
+ if (_unmatched (caseRows.sublist (0 , i), caseRows[i]) == null ) {
45
+ errors.add (new UnreachableCaseError (valueType, cases, i));
46
+ }
47
+ }
48
+
49
+ Witness ? witness = _unmatched (caseRows, [valuePattern]);
50
+ if (witness != null ) {
51
+ errors.add (new NonExhaustiveError (valueType, cases, witness));
52
+ }
53
+
54
+ return errors;
55
+ }
56
+
57
+ /// Determines if [cases] is exhaustive over all values contained by
58
+ /// [valueSpace] . If so, returns `null` . Otherwise, returns a string describing
59
+ /// an example of one value that isn't matched by anything in [cases] .
60
+ Witness ? checkExhaustiveness (Space valueSpace, List <Space > cases) {
61
+ // TODO(johnniwinther): Perform reachability checking.
62
+ List <List <Space >> caseRows = cases.map ((space) => [space]).toList ();
63
+
64
+ Witness ? witness = _unmatched (caseRows, [valueSpace]);
65
+
66
+ // Uncomment this to have it print out the witness for non-exhaustive matches.
67
+ // if (witness != null) print(witness);
68
+
69
+ return witness;
70
+ }
71
+
72
+ /// Tries to find a pattern containing at least one value matched by
73
+ /// [valuePatterns] that is not matched by any of the patterns in [caseRows] .
74
+ ///
75
+ /// If found, returns it. This is a witness example showing that [caseRows] is
76
+ /// not exhaustive over all values in [valuePatterns] . If it returns `null` ,
77
+ /// then [caseRows] exhaustively covers [valuePatterns] .
78
+ Witness ? _unmatched (List <List <Space >> caseRows, List <Space > valuePatterns,
79
+ [List <Predicate > witnessPredicates = const []]) {
80
+ assert (caseRows.every ((element) => element.length == valuePatterns.length),
81
+ "Value patterns: $valuePatterns , case rows: $caseRows ." );
82
+ profile.count ('_unmatched' );
83
+ // If there are no more columns, then we've tested all the predicates we have
84
+ // to test.
85
+ if (valuePatterns.isEmpty) {
86
+ // If there are still any rows left, then it means every remaining value
87
+ // will go to one of those rows' bodies, so we have successfully matched.
88
+ if (caseRows.isNotEmpty) return null ;
89
+
90
+ // If we ran out of rows too, then it means [witnessPredicates] is now a
91
+ // complete description of at least one value that slipped past all the
92
+ // rows.
93
+ return new Witness (witnessPredicates);
94
+ }
95
+
96
+ // Look down the first column of tests.
97
+ Space firstValuePatterns = valuePatterns[0 ];
98
+
99
+ Set <Key > keysOfInterest = {};
100
+ for (List <Space > caseRow in caseRows) {
101
+ for (SingleSpace singleSpace in caseRow.first.singleSpaces) {
102
+ keysOfInterest.addAll (singleSpace.additionalFields.keys);
103
+ }
104
+ }
105
+ for (SingleSpace firstValuePattern in firstValuePatterns.singleSpaces) {
106
+ // TODO(johnniwinther): Right now, this brute force expands all subtypes of
107
+ // sealed types and considers them individually. It would be faster to look
108
+ // at the types of the patterns in the first column of each row and only
109
+ // expand subtypes that are actually tested.
110
+ // Split the type into its sealed subtypes and consider each one separately.
111
+ // This enables it to filter rows more effectively.
112
+ List <StaticType > subtypes =
113
+ expandSealedSubtypes (firstValuePattern.type, keysOfInterest);
114
+ for (StaticType subtype in subtypes) {
115
+ Witness ? result = _filterByType (subtype, caseRows, firstValuePattern,
116
+ valuePatterns, witnessPredicates, firstValuePatterns.path);
117
+
118
+ // If we found a witness for a subtype that no rows match, then we can
119
+ // stop. There may be others but we don't need to find more.
120
+ if (result != null ) return result;
121
+ }
122
+ }
123
+
124
+ // If we get here, no subtype yielded a witness, so we must have matched
125
+ // everything.
126
+ return null ;
127
+ }
128
+
129
+ Witness ? _filterByType (
130
+ StaticType type,
131
+ List <List <Space >> caseRows,
132
+ SingleSpace firstSingleSpaceValue,
133
+ List <Space > valueSpaces,
134
+ List <Predicate > witnessPredicates,
135
+ Path path) {
136
+ profile.count ('_filterByType' );
137
+ // Extend the witness with the type we're matching.
138
+ List <Predicate > extendedWitness = [
139
+ ...witnessPredicates,
140
+ new Predicate (path, type)
141
+ ];
142
+
143
+ // 1) Discard any rows that might not match because the column's type isn't a
144
+ // subtype of the value's type. We only keep rows that *must* match because a
145
+ // row that could potentially fail to match will not help us prove
146
+ // exhaustiveness.
147
+ //
148
+ // 2) Expand any unions in the first column. This can (deliberately) produce
149
+ // duplicate rows in remainingRows.
150
+ List <SingleSpace > remainingRowFirstSingleSpaces = [];
151
+ List <List <Space >> remainingRows = [];
152
+ for (List <Space > row in caseRows) {
153
+ Space firstSpace = row[0 ];
154
+
155
+ for (SingleSpace firstSingleSpace in firstSpace.singleSpaces) {
156
+ // If the row's type is a supertype of the value pattern's type then it
157
+ // must match.
158
+ if (type.isSubtypeOf (firstSingleSpace.type)) {
159
+ remainingRowFirstSingleSpaces.add (firstSingleSpace);
160
+ remainingRows.add (row);
161
+ }
162
+ }
163
+ }
164
+
165
+ // We have now filtered by the type test of the first column of patterns, but
166
+ // some of those may also have field subpatterns. If so, lift those out so we
167
+ // can recurse into them.
168
+ Set <String > fieldNames = {
169
+ ...firstSingleSpaceValue.fields.keys,
170
+ for (SingleSpace firstPattern in remainingRowFirstSingleSpaces)
171
+ ...firstPattern.fields.keys
172
+ };
173
+
174
+ Set <Key > additionalFieldKeys = {
175
+ ...firstSingleSpaceValue.additionalFields.keys,
176
+ for (SingleSpace firstPattern in remainingRowFirstSingleSpaces)
177
+ ...firstPattern.additionalFields.keys
178
+ };
179
+
180
+ // Sorting isn't necessary, but makes the behavior deterministic.
181
+ List <String > sortedFieldNames = fieldNames.toList ()..sort ();
182
+ List <Key > sortedAdditionalFieldKeys = additionalFieldKeys.toList ()..sort ();
183
+
184
+ // Remove the first column from the value list and replace it with any
185
+ // expanded fields.
186
+ valueSpaces = [
187
+ ..._expandFields (sortedFieldNames, sortedAdditionalFieldKeys,
188
+ firstSingleSpaceValue, type, path),
189
+ ...valueSpaces.skip (1 )
190
+ ];
191
+
192
+ // Remove the first column from each row and replace it with any expanded
193
+ // fields.
194
+ for (int i = 0 ; i < remainingRows.length; i++ ) {
195
+ remainingRows[i] = [
196
+ ..._expandFields (
197
+ sortedFieldNames,
198
+ sortedAdditionalFieldKeys,
199
+ remainingRowFirstSingleSpaces[i],
200
+ remainingRowFirstSingleSpaces[i].type,
201
+ path),
202
+ ...remainingRows[i].skip (1 )
203
+ ];
204
+ }
205
+
206
+ // Proceed to the next column.
207
+ return _unmatched (remainingRows, valueSpaces, extendedWitness);
208
+ }
209
+
210
+ /// Given a list of [fieldNames] and [additionalFieldKeys] , and a [singleSpace] ,
211
+ /// generates a list of single spaces, one for each named field and additional
212
+ /// field key.
213
+ ///
214
+ /// When [singleSpace] contains a field with that name or an additional field
215
+ /// with the key, extracts it into the resulting list. Otherwise, the
216
+ /// [singleSpace] doesn't care about that field, so inserts a default [Space]
217
+ /// that matches all values for the field.
218
+ ///
219
+ /// In other words, this unpacks a set of fields so that the main algorithm can
220
+ /// add them to the worklist.
221
+ List <Space > _expandFields (
222
+ List <String > fieldNames,
223
+ List <Key > additionalFieldKeys,
224
+ SingleSpace singleSpace,
225
+ StaticType type,
226
+ Path path) {
227
+ profile.count ('_expandFields' );
228
+ List <Space > result = < Space > [];
229
+ for (String fieldName in fieldNames) {
230
+ Space ? field = singleSpace.fields[fieldName];
231
+ if (field != null ) {
232
+ result.add (field);
233
+ } else {
234
+ // This pattern doesn't test this field, so add a pattern for the
235
+ // field that matches all values. This way the columns stay aligned.
236
+ result.add (new Space (path.add (fieldName),
237
+ type.fields[fieldName] ?? StaticType .nullableObject));
238
+ }
239
+ }
240
+ for (Key key in additionalFieldKeys) {
241
+ Space ? field = singleSpace.additionalFields[key];
242
+ if (field != null ) {
243
+ result.add (field);
244
+ } else {
245
+ // This pattern doesn't test this field, so add a pattern for the
246
+ // field that matches all values. This way the columns stay aligned.
247
+ result.add (new Space (path.add (key.name),
248
+ type.getAdditionalField (key) ?? StaticType .nullableObject));
249
+ }
250
+ }
251
+ return result;
252
+ }
253
+
254
+ /// Recursively expands [type] with its subtypes if its sealed.
255
+ ///
256
+ /// Otherwise, just returns [type] .
257
+ List <StaticType > expandSealedSubtypes (
258
+ StaticType type, Set <Key > keysOfInterest) {
259
+ profile.count ('expandSealedSubtypes' );
260
+ if (! type.isSealed) {
261
+ return [type];
262
+ } else {
263
+ return {
264
+ for (StaticType subtype in type.getSubtypes (keysOfInterest))
265
+ ...expandSealedSubtypes (subtype, keysOfInterest)
266
+ }.toList ();
267
+ }
268
+ }
269
+
17
270
class ExhaustivenessError {}
18
271
19
272
class NonExhaustiveError implements ExhaustivenessError {
0 commit comments