Skip to content

Commit 0cee6c5

Browse files
committed
Simplify the exhaustiveness prototype some.
Instead of fully defining intersection, just define whether the intersection is empty, which is all we need.
1 parent cdb928c commit 0cee6c5

File tree

4 files changed

+180
-77
lines changed

4 files changed

+180
-77
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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 whether the intersection of [left] and [right] is empty.
9+
///
10+
/// This is used to tell if two field spaces on a pair of spaces being
11+
/// subtracted have no common values.
12+
bool intersectEmpty(Space left, Space right) {
13+
// The intersection with an empty space is always empty.
14+
if (left == Space.empty) return true;
15+
if (right == Space.empty) return true;
16+
17+
// The intersection of a union is empty if all of the arms are.
18+
if (left is UnionSpace) {
19+
return left.arms.every((arm) => intersectEmpty(arm, right));
20+
}
21+
22+
if (right is UnionSpace) {
23+
return right.arms.every((arm) => intersectEmpty(left, arm));
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+
bool _intersectExtracts(ExtractSpace left, ExtractSpace right) {
32+
if (intersectTypes(left.type, right.type)) return true;
33+
34+
// Recursively intersect the fields.
35+
var fieldNames = {...left.fields.keys, ...right.fields.keys}.toList();
36+
for (var name in fieldNames) {
37+
// If the fields are disjoint, then the entire space will have no values.
38+
if (_intersectFields(left.fields[name], right.fields[name])) return true;
39+
}
40+
41+
return false;
42+
}
43+
44+
bool _intersectFields(Space? left, Space? right) {
45+
if (left == null) return right! == Space.empty;
46+
if (right == null) return left == Space.empty;
47+
return intersectEmpty(left, right);
48+
}
49+
50+
/// Returns true if the intersection of two static types [left] and [right] is
51+
/// empty.
52+
bool intersectTypes(StaticType left, StaticType right) {
53+
// If one type is a subtype, the subtype is the intersection.
54+
if (left.isSubtypeOf(right)) return false;
55+
if (right.isSubtypeOf(left)) return false;
56+
57+
// Unrelated types.
58+
return true;
59+
}

working/0546-patterns/exhaustiveness_prototype/lib/space.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'static_type.dart';
99

1010
abstract class Space {
1111
static final empty = _EmptySpace._();
12+
static final top = Space(StaticType.top);
1213

1314
factory Space.union(List<Space> arms) {
1415
// Simplify the arms if possible.
@@ -100,7 +101,7 @@ class ExtractSpace extends Space {
100101

101102
@override
102103
String toString() {
103-
if (fields.isEmpty && type == StaticType.top) return '()';
104+
if (isTop) return '()';
104105

105106
// If there are no fields, just show the type.
106107
if (fields.isEmpty) return type.name;

working/0546-patterns/exhaustiveness_prototype/lib/subtract.dart

Lines changed: 41 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
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.
4-
import 'package:exhaustiveness_prototype/intersect.dart';
4+
import 'package:exhaustiveness_prototype/intersect_empty.dart';
55
import 'package:exhaustiveness_prototype/static_type.dart';
66

77
import 'space.dart';
@@ -61,62 +61,55 @@ List<Space> _subtractExtractAtType(StaticType type, ExtractSpace left,
6161
// and keep all of the current fields.
6262
if (!type.isSubtypeOf(right.type)) return [Space(type, left.fields)];
6363

64+
// Infer any fields that appear in one space and not the other.
65+
var leftFields = <String, Space>{};
66+
var rightFields = <String, Space>{};
67+
for (var name in fieldNames) {
68+
// If the right space matches on a field that the left doesn't have, infer
69+
// it from the static type of the field. That contains the same set of
70+
// values as having no field at all.
71+
leftFields[name] = left.fields[name] ?? Space(type.fields[name]!);
72+
73+
// If the left matches on a field that the right doesn't have, infer top
74+
// for the right field since the right will accept any of left's values for
75+
// that field.
76+
rightFields[name] = right.fields[name] ?? Space.top;
77+
}
78+
6479
// If any pair of fields have no overlapping values, then no overall value
6580
// that matches the left space will also match the right space. So the right
6681
// space doesn't subtract anything and we keep the left space as-is.
6782
for (var name in fieldNames) {
68-
var leftField = left.fields[name];
69-
var rightField = right.fields[name];
70-
71-
if (leftField != null &&
72-
rightField != null &&
73-
intersect(leftField, rightField) == Space.empty) {
83+
if (intersectEmpty(leftFields[name]!, rightFields[name]!)) {
7484
return [Space(type, left.fields)];
7585
}
7686
}
7787

7888
// If all the right's fields strictly cover all of the left's, then the
7989
// right completely subtracts this type and nothing remains.
80-
if (_isLeftSubspace(type, fieldNames, left.fields, right.fields)) {
90+
if (_isLeftSubspace(type, fieldNames, leftFields, rightFields)) {
8191
return const [];
8292
}
8393

8494
// The right side is a supertype but its fields don't totally cover, so
85-
// handle each of them individually. This is equation 8.3 but more complex
86-
// to handle the fact that records may choose to match arbitrary subsets of
87-
// the available fields.
95+
// handle each of them individually.
8896

8997
// Walk the fields and see which ones are modified by the right-hand fields.
9098
var fixed = <String, Space>{};
9199
var changedLeft = <String, Space>{};
92100
var changedDifference = <String, Space>{};
93101
for (var name in fieldNames) {
94-
var leftField = left.fields[name];
95-
var rightField = right.fields[name];
96-
97-
// To subtract a right field, we need a baseline left field to compare
98-
// it to. If there isn't one, infer a left field that matches all values
99-
// of its type.
100-
leftField ??= Space(type.fields[name]!);
101-
102-
if (rightField == null) {
103-
// If we have a left field that constrains but aren't subtracting
104-
// anything from it, just keep the constraint.
105-
fixed[name] = leftField;
102+
var difference = subtract(leftFields[name]!, rightFields[name]!);
103+
if (difference == Space.empty) {
104+
// The right field accepts all the values that the left field accepts, so
105+
// keep the left field as it is.
106+
fixed[name] = leftFields[name]!;
107+
} else if (difference.isTop) {
108+
// If the resulting field matches everything, simply discard it since
109+
// it's equivalent to omitting the field.
106110
} else {
107-
var difference = subtract(leftField, rightField);
108-
if (difference == Space.empty) {
109-
// TODO: This comment isn't right.
110-
// The left and right fields don't overlap, so there's no need to
111-
// consider this empty case since it will match nothing.
112-
fixed[name] = leftField;
113-
} else if (difference.isTop) {
114-
// Don't bother keeping a field that matches everything.
115-
// TODO: When does this get reached?
116-
} else {
117-
changedLeft[name] = leftField;
118-
changedDifference[name] = difference;
119-
}
111+
changedLeft[name] = leftFields[name]!;
112+
changedDifference[name] = difference;
120113
}
121114
}
122115

@@ -150,23 +143,10 @@ List<Space> _subtractExtractAtType(StaticType type, ExtractSpace left,
150143
/// corresponding field in [rightFields].
151144
bool _isLeftSubspace(StaticType leftType, List<String> fieldNames,
152145
Map<String, Space> leftFields, Map<String, Space> rightFields) {
153-
// 8.1: If this type is a subtype of the right type, and all of the fields
154-
// on the left are covered by the fields on the right, then the right hand
155-
// space covers the left space entirely.
156146
for (var name in fieldNames) {
157-
var leftField = leftFields[name];
158-
var rightField = rightFields[name];
159-
160-
// If there is no right field, it definitely covers all values matched
161-
// by the left field.
162-
if (rightField == null) continue;
163-
164-
// If there is no left field, infer it from the type on the left. This is
165-
// safe to do because we know the left type is a subtype of the right and
166-
// thus the field will exist.
167-
leftField ??= Space(leftType.fields[name]!);
168-
169-
if (subtract(leftField, rightField) != Space.empty) return false;
147+
if (subtract(leftFields[name]!, rightFields[name]!) != Space.empty) {
148+
return false;
149+
}
170150
}
171151

172152
// If we get here, every field covered.
@@ -176,28 +156,13 @@ bool _isLeftSubspace(StaticType leftType, List<String> fieldNames,
176156
/// Recursively replaces [left] with a union of its sealed subtypes as long as
177157
/// doing so enables it to more precisely match against [right].
178158
List<StaticType> expandType(StaticType left, StaticType right) {
179-
// If we've reached the type, stop.
180-
if (left.isSubtypeOf(right)) return [left];
181-
182-
if (!left.isSealed) return [left];
183-
184-
// If expanding left into its subtypes won't get closer to right, then don't.
185-
if (!right.isSubtypeOf(left)) return [left];
186-
187-
// If we remove the restriction that a type can only be a direct subtype of
188-
// one sealed supertype, then the above check needs to be smarter. Consider:
189-
// (A)
190-
// / \
191-
// (B) (C)
192-
// / \ / \
193-
// D E F
194-
//
195-
// Here, `expandType(B, F)` should return [D, E, F] because even though B and
196-
// F aren't supertypes of each other, they have a shared supertype in a sealed
197-
// family.
198-
199-
// Expand [left] recursively to reach down to right.
200-
return {
201-
for (var subtype in left.subtypes) ...expandType(subtype, right),
202-
}.toList();
159+
// If [left] is a sealed supertype and [right] is in its subtype hierarchy,
160+
// then expand out the subtypes (recursively) to more precisely match [right].
161+
if (left.isSealed && left != right && right.isSubtypeOf(left)) {
162+
return {
163+
for (var subtype in left.subtypes) ...expandType(subtype, right),
164+
}.toList();
165+
}
166+
167+
return [left];
203168
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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/intersect_empty.dart';
5+
import 'package:exhaustiveness_prototype/space.dart';
6+
import 'package:exhaustiveness_prototype/static_type.dart';
7+
import 'package:test/test.dart';
8+
9+
import 'utils.dart';
10+
11+
void main() {
12+
// (A)
13+
// / \
14+
// B C
15+
var a = StaticType('A', isSealed: true);
16+
var b = StaticType('B', inherits: [a]);
17+
var c = StaticType('C', inherits: [a]);
18+
19+
Space A({StaticType? x, StaticType? y, StaticType? z}) => ty(
20+
a, {if (x != null) 'x': x, if (y != null) 'y': y, if (z != null) 'z': z});
21+
Space B({StaticType? x, StaticType? y, StaticType? z}) => ty(
22+
b, {if (x != null) 'x': x, if (y != null) 'y': y, if (z != null) 'z': z});
23+
Space C({StaticType? x, StaticType? y, StaticType? z}) => ty(
24+
c, {if (x != null) 'x': x, if (y != null) 'y': y, if (z != null) 'z': z});
25+
26+
test('records', () {
27+
expectIntersectEmpty(rec(x: a, y: a), rec(x: a, y: a), isFalse);
28+
expectIntersectEmpty(rec(x: a, y: a), rec(x: a), isFalse);
29+
expectIntersectEmpty(
30+
rec(w: a, x: a), rec(y: a, z: a), isFalse);
31+
expectIntersectEmpty(rec(w: a, x: a, y: a), rec(x: a, y: a, z: a),
32+
isFalse);
33+
});
34+
35+
test('types', () {
36+
// Note: More comprehensive tests under intersect_types_test.dart.
37+
expectIntersectEmpty(a, a, isFalse);
38+
expectIntersectEmpty(a, b, isFalse);
39+
expectIntersectEmpty(a, c, isFalse);
40+
expectIntersectEmpty(b, c, isTrue);
41+
});
42+
43+
test('field types', () {
44+
expectIntersectEmpty(rec(x: a, y: b), rec(x: b, y: a), isFalse);
45+
expectIntersectEmpty(rec(x: b), rec(x: c), isTrue);
46+
});
47+
48+
test('types and fields', () {
49+
expectIntersectEmpty(A(x: a), A(x: a), isFalse);
50+
expectIntersectEmpty(A(x: a), A(x: b), isFalse);
51+
expectIntersectEmpty(A(x: b), A(x: c), isTrue);
52+
53+
expectIntersectEmpty(A(x: a), B(x: a), isFalse);
54+
expectIntersectEmpty(A(x: a), B(x: b), isFalse);
55+
expectIntersectEmpty(A(x: b), B(x: c), isTrue);
56+
57+
expectIntersectEmpty(B(x: a), A(x: a), isFalse);
58+
expectIntersectEmpty(B(x: a), A(x: b), isFalse);
59+
expectIntersectEmpty(B(x: b), A(x: c), isTrue);
60+
61+
expectIntersectEmpty(B(x: a), B(x: a), isFalse);
62+
expectIntersectEmpty(B(x: a), B(x: b), isFalse);
63+
expectIntersectEmpty(B(x: b), B(x: c), isTrue);
64+
65+
expectIntersectEmpty(B(x: a), C(x: a), isTrue);
66+
expectIntersectEmpty(B(x: a), C(x: b), isTrue);
67+
expectIntersectEmpty(B(x: b), C(x: c), isTrue);
68+
});
69+
}
70+
71+
void expectIntersectEmpty(Object left, Object right, Matcher expected) {
72+
var leftSpace = parseSpace(left);
73+
var rightSpace = parseSpace(right);
74+
75+
// Intersection is symmetric so try both directions.
76+
expect(intersectEmpty(leftSpace, rightSpace), expected);
77+
expect(intersectEmpty(rightSpace, leftSpace), expected);
78+
}

0 commit comments

Comments
 (0)