Skip to content

Commit c504a1a

Browse files
committed
[patterns] Add prototype exhaustiveness checker.
1 parent eba4849 commit c504a1a

19 files changed

+2145
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This is a prototype implementation of the exhaustiveness checking algorithm.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
include: package:lints/recommended.yaml
2+
linter:
3+
rules:
4+
- avoid_null_checks_in_equality_operators
5+
- avoid_unused_constructor_parameters
6+
- await_only_futures
7+
- camel_case_types
8+
- cancel_subscriptions
9+
- comment_references
10+
- constant_identifier_names
11+
- control_flow_in_finally
12+
- directives_ordering
13+
- empty_statements
14+
- hash_and_equals
15+
- implementation_imports
16+
- iterable_contains_unrelated_type
17+
- library_names
18+
- library_prefixes
19+
- list_remove_unrelated_type
20+
- non_constant_identifier_names
21+
- overridden_fields
22+
- package_api_docs
23+
- package_names
24+
- package_prefixed_library_names
25+
- prefer_final_fields
26+
- prefer_generic_function_type_aliases
27+
- prefer_typing_uninitialized_variables
28+
- test_types_in_equals
29+
- throw_in_finally
30+
- unnecessary_brace_in_string_interps
31+
- unrelated_type_equality_checks
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
import 'space.dart';
5+
6+
/// Returns `true` if [left] and [right] are equivalent spaces.
7+
///
8+
/// Equality is defined purely structurally/syntactically.
9+
bool equal(Space left, Space right) {
10+
if (left == right) return true;
11+
12+
// Empty is only equal to itself (and will get caught by the previous check).
13+
if (left == Space.empty) return false;
14+
if (right == Space.empty) return false;
15+
16+
if (left is UnionSpace && right is UnionSpace) {
17+
return _equalUnions(left, right);
18+
}
19+
20+
if (left is ExtractSpace && right is ExtractSpace) {
21+
return _equalExtracts(left, right);
22+
}
23+
24+
// If we get here, one is a union and one is an extract.
25+
return false;
26+
}
27+
28+
/// Returns `true` if [left] and [right] contain equal arms in any order.
29+
///
30+
/// Assumes that all duplicates have already been removed from each union.
31+
bool _equalUnions(UnionSpace left, UnionSpace right) {
32+
if (left.arms.length != right.arms.length) return false;
33+
34+
/// For each left arm, should find an equal right arm.
35+
for (var leftArm in left.arms) {
36+
var found = false;
37+
for (var rightArm in right.arms) {
38+
if (equal(leftArm, rightArm)) {
39+
found = true;
40+
break;
41+
}
42+
}
43+
if (!found) return false;
44+
}
45+
46+
return true;
47+
}
48+
49+
/// Returns `true` if [left] and [right] have the same type and the same fields
50+
/// with equal subspaces.
51+
bool _equalExtracts(ExtractSpace left, ExtractSpace right) {
52+
// Must have the same type.
53+
if (left.type != right.type) return false;
54+
55+
// And the same fields.
56+
var fields = {...left.fields.keys, ...right.fields.keys};
57+
if (left.fields.length != fields.length) return false;
58+
if (right.fields.length != fields.length) return false;
59+
60+
for (var field in fields) {
61+
if (!equal(left.fields[field]!, right.fields[field]!)) return false;
62+
}
63+
64+
return true;
65+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
import 'space.dart';
5+
import 'static_type.dart';
6+
import 'subtract.dart';
7+
8+
/// Returns `true` if [cases] exhaustively covers all possible values of
9+
/// [value].
10+
///
11+
/// This is defined simply in terms of subtraction and unions: [cases] is a
12+
/// union space, and it's exhaustive if subtracting it from [value] leaves
13+
/// nothing.
14+
bool isExhaustive(Space value, List<Space> cases) {
15+
return subtract(value, Space.union(cases)) == Space.empty;
16+
}
17+
18+
/// Checks the [cases] representing a series of switch cases to see if they
19+
/// exhaustively cover all possible values of the matched [valueType]. Also
20+
/// checks to see if any case can't be matched because it's covered by previous
21+
/// cases.
22+
///
23+
/// Returns a string containing any unreachable case or non-exhaustive match
24+
/// errors. Returns an empty string if all cases are reachable and the cases
25+
/// are exhaustive.
26+
String reportErrors(StaticType valueType, List<Space> cases) {
27+
var errors = <String>[];
28+
29+
var remaining = Space(valueType);
30+
for (var i = 0; i < cases.length; i++) {
31+
// See if this case is covered by previous ones.
32+
if (i > 0) {
33+
var previous = Space.union(cases.sublist(0, i));
34+
if (subtract(cases[i], previous) == Space.empty) {
35+
errors.add('Case #${i + 1} ${cases[i]} is covered by $previous.');
36+
}
37+
}
38+
39+
remaining = subtract(remaining, cases[i]);
40+
}
41+
42+
if (remaining != Space.empty) {
43+
errors.add(
44+
'$valueType is not exhaustively matched by ${Space.union(cases)}.');
45+
}
46+
47+
return errors.join('\n');
48+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
import 'package:exhaustiveness_prototype/static_type.dart';
5+
6+
import 'space.dart';
7+
8+
/// Calculates the intersection of [left] and [right].
9+
///
10+
/// This is used to tell if two field spaces on a pair of spaces being
11+
/// subtracted have no common values.
12+
Space intersect(Space left, Space right) {
13+
// The intersection with an empty space is always empty.
14+
if (left == Space.empty) return Space.empty;
15+
if (right == Space.empty) return Space.empty;
16+
17+
// The intersection of a union is the union of the intersections of its arms.
18+
if (left is UnionSpace) {
19+
return Space.union(left.arms.map((arm) => intersect(arm, right)).toList());
20+
}
21+
22+
if (right is UnionSpace) {
23+
return Space.union(right.arms.map((arm) => intersect(left, arm)).toList());
24+
}
25+
26+
// Otherwise, we're intersecting two [ExtractSpaces].
27+
return _intersectExtracts(left as ExtractSpace, right as ExtractSpace);
28+
}
29+
30+
/// Returns the interaction of extract spaces [left] and [right].
31+
Space _intersectExtracts(ExtractSpace left, ExtractSpace right) {
32+
var type = intersectTypes(left.type, right.type);
33+
34+
// If the types are disjoint, the intersection is empty.
35+
if (type == null) return Space.empty;
36+
37+
// Recursively intersect the fields.
38+
var fieldNames = {...left.fields.keys, ...right.fields.keys}.toList();
39+
40+
// Sorting isn't needed for correctness, just to make the tests less brittle.
41+
fieldNames.sort();
42+
43+
var fields = <String, Space>{};
44+
for (var name in fieldNames) {
45+
var field = _intersectFields(left.fields[name], right.fields[name]);
46+
47+
// If the fields are disjoint, then the entire space will have no values.
48+
if (field == Space.empty) return Space.empty;
49+
fields[name] = field;
50+
}
51+
52+
return Space(type, fields);
53+
}
54+
55+
Space _intersectFields(Space? left, Space? right) {
56+
if (left == null) return right!;
57+
if (right == null) return left;
58+
return intersect(left, right);
59+
}
60+
61+
/// Returns the intersection of two static types [left] and [right].
62+
///
63+
/// Returns `null` if the intersection is empty.
64+
StaticType? intersectTypes(StaticType left, StaticType right) {
65+
// If one type is a subtype, the subtype is the intersection.
66+
if (left.isSubtypeOf(right)) return left;
67+
if (right.isSubtypeOf(left)) return right;
68+
69+
// If we allow sealed types to share subtypes, then this will need to be more
70+
// sophisticated. Here:
71+
//
72+
// (A) (B)
73+
// / \ / \
74+
// C D E
75+
//
76+
// The intersection of A and B should be D. Here:
77+
//
78+
// (A) (B)
79+
// | \ / |
80+
// |\ \/ /|
81+
// | \/\/ |
82+
// C D E F
83+
//
84+
// It should be D, E.
85+
86+
// Unrelated types.
87+
return null;
88+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
import 'package:exhaustiveness_prototype/equal.dart';
5+
6+
import 'static_type.dart';
7+
8+
// TODO: List spaces.
9+
10+
abstract class Space {
11+
static final empty = _EmptySpace._();
12+
13+
factory Space.union(List<Space> arms) {
14+
// Simplify the arms if possible.
15+
var allArms = <Space>[];
16+
17+
void addSpace(Space space) {
18+
// Discard duplicate arms. Duplicates can appear when working through a
19+
// series of cases that destructure multiple fields with different types.
20+
// Discarding the duplicates isn't necessary for correctness (a union with
21+
// redundant arms contains the same set of values), but improves
22+
// performance greatly. In the "sealed subtypes large T with all cases"
23+
// test, you end up with a union containing 2520 arms, 2488 are
24+
// duplicates. With this check, the largest union has only 5 arms.
25+
//
26+
// This is O(n^2) since we define only equality on spaces, but a real
27+
// implementation would likely define hash code too and then simply
28+
// create a hash set to merge duplicates in O(n) time.
29+
for (var existing in allArms) {
30+
if (equal(existing, space)) return;
31+
}
32+
33+
allArms.add(space);
34+
}
35+
36+
for (var space in arms) {
37+
// Discard empty arms.
38+
if (space == empty) continue;
39+
40+
// Flatten unions. We don't need to flatten recursively since we always
41+
// go through this constructor to create unions. A UnionSpace will never
42+
// contain UnionSpaces.
43+
if (space is UnionSpace) {
44+
for (var arm in space.arms) addSpace(arm);
45+
} else {
46+
addSpace(space);
47+
}
48+
}
49+
50+
if (allArms.isEmpty) return empty;
51+
if (allArms.length == 1) return allArms.first;
52+
return UnionSpace._(allArms);
53+
}
54+
55+
factory Space(StaticType type, [Map<String, Space> fields = const {}]) =>
56+
ExtractSpace._(type, fields);
57+
58+
factory Space.record([Map<String, Space> fields = const {}]) =>
59+
Space(StaticType.top, fields);
60+
61+
Space._();
62+
63+
/// An untyped record space with no fields matches all values and thus isn't
64+
/// very useful.
65+
bool get isTop => false;
66+
}
67+
68+
/// A union of spaces. The space A|B contains all of the values of A and B.
69+
class UnionSpace extends Space {
70+
final List<Space> arms;
71+
72+
UnionSpace._(this.arms) : super._() {
73+
assert(arms.length > 1);
74+
}
75+
76+
@override
77+
String toString() => arms.join('|');
78+
}
79+
80+
/// The main space for matching types and destructuring.
81+
///
82+
/// It has a type which determines the type of values it contains. The type may
83+
/// be [StaticType.top] to indicate that it doesn't filter by type.
84+
///
85+
/// It may also contain zero or more named fields. The space then only contains
86+
/// values where the field values are contained by the corresponding field
87+
/// spaces.
88+
class ExtractSpace extends Space {
89+
/// The type of values the space matches.
90+
final StaticType type;
91+
92+
/// Any field subspaces the space matches.
93+
final Map<String, Space> fields;
94+
95+
ExtractSpace._(this.type, [this.fields = const {}]) : super._();
96+
97+
/// An [ExtractSpace] with no type and no fields contains all values.
98+
@override
99+
bool get isTop => type == StaticType.top && fields.isEmpty;
100+
101+
@override
102+
String toString() {
103+
if (fields.isEmpty && type == StaticType.top) return '()';
104+
105+
// If there are no fields, just show the type.
106+
if (fields.isEmpty) return type.name;
107+
108+
var buffer = StringBuffer();
109+
110+
// We model a bare record pattern by treating it like an extractor on top.
111+
if (type != StaticType.top) buffer.write(type.name);
112+
113+
buffer.write('(');
114+
var first = true;
115+
116+
// Positional fields have stringified number names.
117+
for (var i = 0;; i++) {
118+
var pattern = fields[i.toString()];
119+
if (pattern == null) break;
120+
121+
if (!first) buffer.write(', ');
122+
buffer.write(pattern);
123+
first = false;
124+
}
125+
126+
fields.forEach((name, pattern) {
127+
// Skip positional fields.
128+
if (int.tryParse(name) != null) return;
129+
130+
if (!first) buffer.write(', ');
131+
buffer.write('$name: $pattern');
132+
first = false;
133+
});
134+
135+
buffer.write(')');
136+
137+
return buffer.toString();
138+
}
139+
}
140+
141+
/// The uninhabited space.
142+
class _EmptySpace extends Space {
143+
_EmptySpace._() : super._();
144+
145+
@override
146+
String toString() => '∅';
147+
}

0 commit comments

Comments
 (0)