Skip to content

Commit 9698e72

Browse files
johnniwintherCommit Queue
authored and
Commit Queue
committed
[_fe_analyzer_shared] Split exhaustiveness implementation into smaller files
Change-Id: I08e098b978480fe72a3273ac3331bef9745a0bb6 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/287761 Commit-Queue: Johnni Winther <johnniwinther@google.com> Reviewed-by: Paul Berry <paulberry@google.com>
1 parent 08c492f commit 9698e72

30 files changed

+2001
-1915
lines changed

pkg/_fe_analyzer_shared/benchmark/exhaustiveness/large_fields_call_counts.dart

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'package:_fe_analyzer_shared/src/exhaustiveness/exhaustive.dart';
6+
import 'package:_fe_analyzer_shared/src/exhaustiveness/path.dart';
57
import 'package:_fe_analyzer_shared/src/exhaustiveness/profile.dart' as profile;
68
import 'package:_fe_analyzer_shared/src/exhaustiveness/static_type.dart';
7-
import 'package:_fe_analyzer_shared/src/exhaustiveness/witness.dart';
9+
import 'package:_fe_analyzer_shared/src/exhaustiveness/space.dart';
810

911
import '../../test/exhaustiveness/env.dart';
1012
import '../../test/exhaustiveness/utils.dart';

pkg/_fe_analyzer_shared/benchmark/exhaustiveness/large_fields_timed.dart

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
import 'dart:math';
66

7+
import 'package:_fe_analyzer_shared/src/exhaustiveness/exhaustive.dart';
8+
import 'package:_fe_analyzer_shared/src/exhaustiveness/path.dart';
9+
import 'package:_fe_analyzer_shared/src/exhaustiveness/space.dart';
710
import 'package:_fe_analyzer_shared/src/exhaustiveness/static_type.dart';
8-
import 'package:_fe_analyzer_shared/src/exhaustiveness/witness.dart';
911

1012
import '../../test/exhaustiveness/env.dart';
1113
import '../../test/exhaustiveness/utils.dart';

pkg/_fe_analyzer_shared/lib/src/exhaustiveness/exhaustive.dart

+253
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'static_type.dart';
6+
import 'key.dart';
7+
import 'path.dart';
8+
import 'profile.dart' as profile;
9+
import 'space.dart';
610
import 'witness.dart';
711

812
/// Indicates whether the "fallback" exhaustiveness algorithm (based on flow
@@ -14,6 +18,255 @@ import 'witness.dart';
1418
/// exhaustiveness algorithm) when it is no longer needed.
1519
bool useFallbackExhaustivenessAlgorithm = true;
1620

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+
17270
class ExhaustivenessError {}
18271

19272
class NonExhaustiveError implements ExhaustivenessError {

0 commit comments

Comments
 (0)