[_fe_analyzer_shared] Remove old Space model

This remove the [Space] class from the old algorithm and instead
encodes the patterns into the new model. The [Pattern] and
[Patterns] classes of the new model have been renamed to
[SingleSpace] and [Space], respectively, and a [Path] class is
added to track the path property used in the model of the new
algorithm.

Change-Id: I0c86c738807030be2f9b59f3aefb5bfcf5bbaeee
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/286501
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Johnni Winther <johnniwinther@google.com>
This commit is contained in:
Johnni Winther 2023-03-06 16:57:16 +00:00 committed by Commit Queue
parent 21bf9f4ba6
commit 12ea77b96e
16 changed files with 374 additions and 491 deletions

View file

@ -3,7 +3,6 @@
// BSD-style license that can be found in the LICENSE file.
import 'package:_fe_analyzer_shared/src/exhaustiveness/profile.dart' as profile;
import 'package:_fe_analyzer_shared/src/exhaustiveness/space.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/static_type.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/witness.dart';
@ -113,6 +112,6 @@ void main() {
void expectExhaustiveOnlyAll(StaticType type, List<Map<String, Object>> cases) {
var spaces = cases.map((c) => ty(type, c)).toList();
profile.reset();
print(isExhaustive(Space(type), spaces));
print(isExhaustive(Space(const Path.root(), type), spaces));
profile.log();
}

View file

@ -4,7 +4,6 @@
import 'dart:math';
import 'package:_fe_analyzer_shared/src/exhaustiveness/space.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/static_type.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/witness.dart';
@ -110,7 +109,7 @@ void main() {
/// Test that [cases] are exhaustive over [type] if and only if all cases are
/// included and that all subsets of the cases are not exhaustive.
void expectExhaustiveOnlyAll(StaticType type, List<Map<String, Object>> cases) {
var valueSpace = Space(type);
var valueSpace = Space(const Path.root(), type);
var caseSpaces = cases.map((c) => ty(type, c)).toList();
const trials = 100;

View file

@ -1,71 +0,0 @@
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'profile.dart' as profile;
import 'space.dart';
/// Returns `true` if [left] and [right] are equivalent spaces.
///
/// Equality is defined purely structurally/syntactically.
bool equal(Space left, Space right, String reason) {
profile.count('equal', reason);
if (identical(left, right)) return true;
// Empty is only equal to itself (and will get caught by the previous check).
if (left == Space.empty) return false;
if (right == Space.empty) return false;
if (left is UnionSpace && right is UnionSpace) {
return _equalUnions(left, right);
}
if (left is ExtractSpace && right is ExtractSpace) {
return _equalExtracts(left, right);
}
// If we get here, one is a union and one is an extract.
return false;
}
/// Returns `true` if [left] and [right] have the same type and the same fields
/// with equal subspaces.
bool _equalExtracts(ExtractSpace left, ExtractSpace right) {
// Must have the same type.
if (left.type != right.type) return false;
// And the same fields.
Set<String> fields = {...left.fields.keys, ...right.fields.keys};
if (left.fields.length != fields.length) return false;
if (right.fields.length != fields.length) return false;
for (String field in fields) {
if (!equal(left.fields[field]!, right.fields[field]!, 'recurse extract')) {
return false;
}
}
return true;
}
/// Returns `true` if [left] and [right] contain equal arms in any order.
///
/// Assumes that all duplicates have already been removed from each union.
bool _equalUnions(UnionSpace left, UnionSpace right) {
if (left.arms.length != right.arms.length) return false;
/// For each left arm, should find an equal right arm.
for (Space leftArm in left.arms) {
bool found = false;
for (Space rightArm in right.arms) {
if (equal(leftArm, rightArm, 'recurse union')) {
found = true;
break;
}
}
if (!found) return false;
}
return true;
}

View file

@ -2,9 +2,8 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'space.dart';
import 'static_type.dart';
import 'witness.dart' as witness;
import 'witness.dart';
/// Indicates whether the "fallback" exhaustiveness algorithm (based on flow
/// analysis) should be used instead of full exhaustiveness. This is a
@ -15,20 +14,6 @@ import 'witness.dart' as witness;
/// exhaustiveness algorithm) when it is no longer needed.
bool useFallbackExhaustivenessAlgorithm = true;
/// Checks the [cases] representing a series of switch cases to see if they
/// exhaustively cover all possible values of the matched [valueType]. Also
/// checks to see if any case can't be matched because it's covered by previous
/// cases.
///
/// Returns a string containing any unreachable case or non-exhaustive match
/// errors. Returns an empty string if all cases are reachable and the cases
/// are exhaustive.
List<ExhaustivenessError> reportErrors(StaticType valueType, List<Space> cases,
[List<Space>? remainingSpaces]) {
return witness.reportErrors(valueType, cases);
}
class ExhaustivenessError {}
class NonExhaustiveError implements ExhaustivenessError {
@ -42,7 +27,7 @@ class NonExhaustiveError implements ExhaustivenessError {
@override
String toString() =>
'$valueType is not exhaustively matched by ${new Space.union(cases)}.';
'$valueType is not exhaustively matched by ${cases.join('|')}.';
}
class UnreachableCaseError implements ExhaustivenessError {

View file

@ -1,156 +0,0 @@
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'equal.dart';
import 'static_type.dart';
/// The main space for matching types and destructuring.
///
/// It has a type which determines the type of values it contains. The type may
/// be [StaticType.nullableObject] to indicate that it doesn't filter by type.
///
/// It may also contain zero or more named fields. The space then only contains
/// values where the field values are contained by the corresponding field
/// spaces.
class ExtractSpace extends Space {
/// The type of values the space matches.
final StaticType type;
/// Any field subspaces the space matches.
final Map<String, Space> fields;
ExtractSpace._(this.type, [this.fields = const {}]) : super._();
/// An [ExtractSpace] with no type and no fields contains all values.
@override
bool get isTop => type == StaticType.nullableObject && fields.isEmpty;
@override
String toString() {
if (isTop) return '()';
if (this == Space.empty) return '';
if (type.isRecord) {
StringBuffer buffer = new StringBuffer();
buffer.write('(');
bool first = true;
type.fields.forEach((String name, StaticType staticType) {
if (!first) buffer.write(', ');
// TODO(johnniwinther): Ensure using Dart syntax for positional fields.
buffer.write('$name: ${fields[name] ?? staticType}');
first = false;
});
buffer.write(')');
return buffer.toString();
} else {
// If there are no fields, just show the type.
if (fields.isEmpty) return type.name;
StringBuffer buffer = new StringBuffer();
buffer.write(type.name);
buffer.write('(');
bool first = true;
fields.forEach((String name, Space space) {
if (!first) buffer.write(', ');
buffer.write('$name: $space');
first = false;
});
buffer.write(')');
return buffer.toString();
}
}
}
// TODO(paulberry, rnystrom): List spaces.
abstract class Space {
/// The uninhabited space.
static final Space empty = new Space(StaticType.neverType);
/// The space containing everything.
static final Space top = new Space(StaticType.nullableObject);
/// The space containing only `null`.
static final Space nullSpace = new Space(StaticType.nullType);
factory Space(StaticType type, [Map<String, Space> fields = const {}]) =>
new ExtractSpace._(type, fields);
factory Space.union(List<Space> arms) {
// Simplify the arms if possible.
List<ExtractSpace> allArms = <ExtractSpace>[];
void addSpace(ExtractSpace space) {
// Discard duplicate arms. Duplicates can appear when working through a
// series of cases that destructure multiple fields with different types.
// Discarding the duplicates isn't necessary for correctness (a union with
// redundant arms contains the same set of values), but improves
// performance greatly. In the "sealed subtypes large T with all cases"
// test, you end up with a union containing 2520 arms, 2488 are
// duplicates. With this check, the largest union has only 5 arms.
//
// This is O(n^2) since we define only equality on spaces, but a real
// implementation would likely define hash code too and then simply
// create a hash set to merge duplicates in O(n) time.
for (Space existing in allArms) {
if (equal(existing, space, 'dedupe union')) return;
}
allArms.add(space);
}
for (Space space in arms) {
// Discard empty arms.
if (space == empty) continue;
// Flatten unions. We don't need to flatten recursively since we always
// go through this constructor to create unions. A UnionSpace will never
// contain UnionSpaces.
if (space is UnionSpace) {
for (ExtractSpace arm in space.arms) {
addSpace(arm);
}
} else {
addSpace(space as ExtractSpace);
}
}
if (allArms.isEmpty) return empty;
if (allArms.length == 1) return allArms.first;
if (allArms.length == 2) {
if (allArms[0].type == StaticType.nullType &&
allArms[0].fields.isEmpty &&
allArms[1].fields.isEmpty) {
return new Space(allArms[1].type.nullable);
} else if (allArms[1].type == StaticType.nullType &&
allArms[1].fields.isEmpty &&
allArms[0].fields.isEmpty) {
return new Space(allArms[0].type.nullable);
}
}
return new UnionSpace._(allArms);
}
Space._();
/// An untyped record space with no fields matches all values and thus isn't
/// very useful.
bool get isTop => false;
}
/// A union of spaces. The space A|B contains all of the values of A and B.
class UnionSpace extends Space {
final List<ExtractSpace> arms;
UnionSpace._(this.arms) : super._() {
assert(arms.length > 1);
}
@override
String toString() => arms.join('|');
}

View file

@ -3,8 +3,8 @@
// BSD-style license that can be found in the LICENSE file.
import 'package:_fe_analyzer_shared/src/exhaustiveness/exhaustive.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/witness.dart';
import 'space.dart';
import 'static_type.dart';
/// Tags used for id-testing of exhaustiveness.
@ -15,11 +15,10 @@ class Tags {
static const String space = 'space';
static const String subtypes = 'subtypes';
static const String expandedSubtypes = 'expandedSubtypes';
static const String remaining = 'remaining';
}
/// Returns a textual representation for [space] used for testing.
String spaceToText(Space space) => space.toString();
String spacesToText(Space space) => space.toString();
/// Returns a textual representation for [fields] used for testing.
String fieldsToText(Map<String, StaticType> fields) {

View file

@ -4,7 +4,6 @@
import 'exhaustive.dart';
import 'profile.dart' as profile;
import 'space.dart';
import 'static_type.dart';
/// Returns `true` if [caseSpaces] exhaustively covers all possible values of
@ -22,40 +21,35 @@ bool isExhaustive(Space valueSpace, List<Space> caseSpaces) {
/// Returns an empty list if all cases are reachable and the cases are
/// exhaustive.
List<ExhaustivenessError> reportErrors(
StaticType valueType, List<Space> caseSpaces) {
StaticType valueType, List<Space> cases) {
List<ExhaustivenessError> errors = <ExhaustivenessError>[];
Pattern valuePattern = new Pattern(valueType, {}, []);
List<List<Patterns>> cases =
caseSpaces.map((space) => [_spaceToPatterns(space)]).toList();
Space valuePattern = new Space(const Path.root(), valueType);
List<List<Space>> caseRows = cases.map((space) => [space]).toList();
for (int i = 1; i < cases.length; i++) {
for (int i = 1; i < caseRows.length; i++) {
// See if this case is covered by previous ones.
if (_unmatched(cases.sublist(0, i), cases[i]) == null) {
errors.add(new UnreachableCaseError(valueType, caseSpaces, i));
if (_unmatched(caseRows.sublist(0, i), caseRows[i]) == null) {
errors.add(new UnreachableCaseError(valueType, cases, i));
}
}
String? witness = _unmatched(cases, [
new Patterns([valuePattern])
]);
String? witness = _unmatched(caseRows, [valuePattern]);
if (witness != null) {
errors.add(new NonExhaustiveError(valueType, caseSpaces, witness));
errors.add(new NonExhaustiveError(valueType, cases, witness));
}
return errors;
}
/// Determines if [caseSpaces] is exhaustive over all values contained by
/// Determines if [cases] is exhaustive over all values contained by
/// [valueSpace]. If so, returns `null`. Otherwise, returns a string describing
/// an example of one value that isn't matched by anything in [caseSpaces].
String? checkExhaustiveness(Space valueSpace, List<Space> caseSpaces) {
/// an example of one value that isn't matched by anything in [cases].
String? checkExhaustiveness(Space valueSpace, List<Space> cases) {
// TODO(johnniwinther): Perform reachability checking.
Patterns value = _spaceToPatterns(valueSpace);
List<List<Patterns>> cases =
caseSpaces.map((space) => [_spaceToPatterns(space)]).toList();
List<List<Space>> caseRows = cases.map((space) => [space]).toList();
String? witness = _unmatched(cases, [value]);
String? witness = _unmatched(caseRows, [valueSpace]);
// Uncomment this to have it print out the witness for non-exhaustive matches.
// if (witness != null) print(witness);
@ -63,40 +57,16 @@ String? checkExhaustiveness(Space valueSpace, List<Space> caseSpaces) {
return witness;
}
/// Convert the prototype's original [Space] representation to the [Patterns]
/// representation used here.
///
/// This is only a convenience to run the existing test code which creates
/// [Space]s using the new algorithm.
// TODO(johnniwinther): Encode Dart patterns directly into [Patterns].
Patterns _spaceToPatterns(Space space, [List<String> path = const []]) {
if (space is UnionSpace) {
return new Patterns(
space.arms.map((arm) => _extractSpaceToPattern(arm, path)).toList());
}
return new Patterns([_extractSpaceToPattern(space as ExtractSpace, path)]);
}
/// Convert the prototype's original [ExtractSpace] representation into the
/// [Pattern] representation used here.
Pattern _extractSpaceToPattern(ExtractSpace space,
[List<String> path = const []]) {
Map<String, Patterns> fields = {
for (String name in space.fields.keys)
name: _spaceToPatterns(space.fields[name]!, [...path, name])
};
return new Pattern(space.type, fields, path);
}
/// Tries to find a pattern containing at least one value matched by
/// [valuePatterns] that is not matched by any of the patterns in [caseRows].
///
/// If found, returns it. This is a witness example showing that [caseRows] is
/// not exhaustive over all values in [valuePatterns]. If it returns `null`,
/// then [caseRows] exhaustively covers [valuePatterns].
String? _unmatched(List<List<Patterns>> caseRows, List<Patterns> valuePatterns,
String? _unmatched(List<List<Space>> caseRows, List<Space> valuePatterns,
[List<Predicate> witnessPredicates = const []]) {
assert(caseRows.every((element) => element.length == valuePatterns.length));
assert(caseRows.every((element) => element.length == valuePatterns.length),
"Value patterns: $valuePatterns, case rows: $caseRows.");
profile.count('_unmatched');
// If there are no more columns, then we've tested all the predicates we have
// to test.
@ -112,9 +82,9 @@ String? _unmatched(List<List<Patterns>> caseRows, List<Patterns> valuePatterns,
}
// Look down the first column of tests.
Patterns firstValuePatterns = valuePatterns[0];
Space firstValuePatterns = valuePatterns[0];
for (Pattern firstValuePattern in firstValuePatterns.patterns) {
for (SingleSpace firstValuePattern in firstValuePatterns.singleSpaces) {
// TODO(johnniwinther): Right now, this brute force expands all subtypes of
// sealed types and considers them individually. It would be faster to look
// at the types of the patterns in the first column of each row and only
@ -124,7 +94,7 @@ String? _unmatched(List<List<Patterns>> caseRows, List<Patterns> valuePatterns,
List<StaticType> subtypes = expandSealedSubtypes(firstValuePattern.type);
for (StaticType subtype in subtypes) {
String? result = _filterByType(subtype, caseRows, firstValuePattern,
valuePatterns, witnessPredicates);
valuePatterns, witnessPredicates, firstValuePatterns.path);
// If we found a witness for a subtype that no rows match, then we can
// stop. There may be others but we don't need to find more.
@ -139,15 +109,16 @@ String? _unmatched(List<List<Patterns>> caseRows, List<Patterns> valuePatterns,
String? _filterByType(
StaticType type,
List<List<Patterns>> caseRows,
Pattern firstValuePattern,
List<Patterns> valuePatterns,
List<Predicate> witnessPredicates) {
List<List<Space>> caseRows,
SingleSpace firstSingleSpaceValue,
List<Space> valueSpaces,
List<Predicate> witnessPredicates,
Path path) {
profile.count('_filterByType');
// Extend the witness with the type we're matching.
List<Predicate> extendedWitness = [
...witnessPredicates,
new Predicate(firstValuePattern.path, type)
new Predicate(path, type)
];
// 1) Discard any rows that might not match because the column's type isn't a
@ -157,16 +128,16 @@ String? _filterByType(
//
// 2) Expand any unions in the first column. This can (deliberately) produce
// duplicate rows in remainingRows.
List<Pattern> remainingRowFirstPatterns = [];
List<List<Patterns>> remainingRows = [];
for (List<Patterns> row in caseRows) {
Patterns firstPatterns = row[0];
List<SingleSpace> remainingRowFirstSingleSpaces = [];
List<List<Space>> remainingRows = [];
for (List<Space> row in caseRows) {
Space firstSpace = row[0];
for (Pattern firstPattern in firstPatterns.patterns) {
for (SingleSpace firstSingleSpace in firstSpace.singleSpaces) {
// If the row's type is a supertype of the value pattern's type then it
// must match.
if (type.isSubtypeOf(firstPattern.type)) {
remainingRowFirstPatterns.add(firstPattern);
if (type.isSubtypeOf(firstSingleSpace.type)) {
remainingRowFirstSingleSpaces.add(firstSingleSpace);
remainingRows.add(row);
}
}
@ -176,8 +147,8 @@ String? _filterByType(
// some of those may also have field subpatterns. If so, lift those out so we
// can recurse into them.
Set<String> fieldNames = {
...firstValuePattern.fields.keys,
for (Pattern firstPattern in remainingRowFirstPatterns)
...firstSingleSpaceValue.fields.keys,
for (SingleSpace firstPattern in remainingRowFirstSingleSpaces)
...firstPattern.fields.keys
};
@ -186,55 +157,47 @@ String? _filterByType(
// Remove the first column from the value list and replace it with any
// expanded fields.
valuePatterns = [
..._expandFields(sorted, firstValuePattern, type),
...valuePatterns.skip(1)
valueSpaces = [
..._expandFields(sorted, firstSingleSpaceValue, type, path),
...valueSpaces.skip(1)
];
// Remove the first column from each row and replace it with any expanded
// fields.
for (int i = 0; i < remainingRows.length; i++) {
remainingRows[i] = [
..._expandFields(sorted, remainingRowFirstPatterns[i],
remainingRowFirstPatterns[i].type),
..._expandFields(sorted, remainingRowFirstSingleSpaces[i],
remainingRowFirstSingleSpaces[i].type, path),
...remainingRows[i].skip(1)
];
}
// Proceed to the next column.
return _unmatched(remainingRows, valuePatterns, extendedWitness);
return _unmatched(remainingRows, valueSpaces, extendedWitness);
}
/// Given a list of [fieldNames] and a [pattern], generates a list of patterns,
/// one for each named field.
/// Given a list of [fieldNames] and a [singleSpace], generates a list of
/// single spaces, one for each named field.
///
/// When pattern contains a field with that name, extracts it into the
/// resulting list. Otherwise, the pattern doesn't care
/// about that field, so inserts a default pattern that matches all values for
/// the field.
/// When [singleSpace] contains a field with that name, extracts it into the
/// resulting list. Otherwise, the [singleSpace] doesn't care about that field,
/// so inserts a default [Space] that matches all values for the field.
///
/// In other words, this unpacks a set of fields so that the main algorithm can
/// add them to the worklist.
List<Patterns> _expandFields(
List<String> fieldNames, Pattern pattern, StaticType type) {
List<Space> _expandFields(List<String> fieldNames, SingleSpace singleSpace,
StaticType type, Path path) {
profile.count('_expandFields');
List<Patterns> result = <Patterns>[];
List<Space> result = <Space>[];
for (String fieldName in fieldNames) {
Patterns? field = pattern.fields[fieldName];
Space? field = singleSpace.fields[fieldName];
if (field != null) {
result.add(field);
} else {
// This pattern doesn't test this field, so add a pattern for the
// field that matches all values. This way the columns stay aligned.
result.add(
new Patterns([
new Pattern(
type.fields[fieldName] ?? StaticType.nullableObject,
{},
[...pattern.path, fieldName],
)
]),
);
result.add(new Space(path.add(fieldName),
type.fields[fieldName] ?? StaticType.nullableObject));
}
}
@ -256,43 +219,143 @@ List<StaticType> expandSealedSubtypes(StaticType type) {
/// The main pattern for matching types and destructuring.
///
/// It has a type which determines the type of values it contains. The type may
/// be [StaticType.top] to indicate that it doesn't filter by type.
/// be [StaticType.nullableObject] to indicate that it doesn't filter by type.
///
/// It may also contain zero or more named fields. The pattern then only matches
/// values where the field values are matched by the corresponding field
/// patterns.
// TODO(johnniwinther): Rename this to avoid name clash with Pattern from
// dart:core.
class Pattern {
class SingleSpace {
static final SingleSpace empty = new SingleSpace(StaticType.neverType);
/// The type of values the pattern matches.
final StaticType type;
/// Any field subpatterns the pattern matches.
final Map<String, Patterns> fields;
final Map<String, Space> fields;
/// The path of getters that led from the original matched value to value
/// matched by this pattern. Used to generate a human-readable witness.
final List<String> path;
SingleSpace(this.type, {this.fields = const {}});
Pattern(this.type, this.fields, this.path);
@override
late final int hashCode = Object.hash(
type,
Object.hashAllUnordered(fields.keys),
Object.hashAllUnordered(fields.values));
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is! SingleSpace) return false;
if (type != other.type) return false;
if (fields.length != other.fields.length) return false;
if (fields.isNotEmpty) {
for (MapEntry<String, Space> entry in fields.entries) {
if (entry.value != other.fields[entry.key]) {
return false;
}
}
}
return true;
}
@override
String toString() {
return 'Pattern(type=$type,fields=$fields,path=$path)';
if (type == StaticType.nullableObject && fields.isEmpty) return '()';
if (this == StaticType.neverType && fields.isEmpty) return '';
if (type.isRecord) {
StringBuffer buffer = new StringBuffer();
buffer.write('(');
bool first = true;
type.fields.forEach((String name, StaticType staticType) {
if (!first) buffer.write(', ');
// TODO(johnniwinther): Ensure using Dart syntax for positional fields.
buffer.write('$name: ${fields[name] ?? staticType}');
first = false;
});
buffer.write(')');
return buffer.toString();
} else {
// If there are no fields, just show the type.
if (fields.isEmpty) return type.name;
StringBuffer buffer = new StringBuffer();
buffer.write(type.name);
buffer.write('(');
bool first = true;
fields.forEach((String name, Space space) {
if (!first) buffer.write(', ');
buffer.write('$name: $space');
first = false;
});
buffer.write(')');
return buffer.toString();
}
}
}
/// A union of [Pattern]s.
/// A set of runtime values encoded as a union of [SingleSpace]s.
///
/// This is used to support logical-or patterns without having to eagerly
/// expand the subpatterns in the parent context.
class Patterns {
final List<Pattern> patterns;
class Space {
/// The path of getters that led from the original matched value to value
/// matched by this pattern. Used to generate a human-readable witness.
final Path path;
Patterns(this.patterns);
final List<SingleSpace> singleSpaces;
/// Create an empty space.
Space.empty(this.path) : singleSpaces = [SingleSpace.empty];
Space(Path path, StaticType type, {Map<String, Space> fields = const {}})
: this._(path, [new SingleSpace(type, fields: fields)]);
Space._(this.path, this.singleSpaces);
Space union(Space other) {
Set<SingleSpace> singleSpacesSet = {};
for (SingleSpace singleSpace in singleSpaces) {
// Discard empty space.
if (singleSpace == SingleSpace.empty) {
continue;
}
singleSpacesSet.add(singleSpace);
}
for (SingleSpace singleSpace in other.singleSpaces) {
// Discard empty space.
if (singleSpace == SingleSpace.empty) {
continue;
}
singleSpacesSet.add(singleSpace);
}
List<SingleSpace> singleSpacesList = singleSpacesSet.toList();
if (singleSpacesSet.isEmpty) {
singleSpacesList.add(SingleSpace.empty);
} else if (singleSpacesList.length == 2) {
if (singleSpacesList[0].type == StaticType.nullType &&
singleSpacesList[0].fields.isEmpty &&
singleSpacesList[1].fields.isEmpty) {
singleSpacesList = [new SingleSpace(singleSpacesList[1].type.nullable)];
} else if (singleSpacesList[1].type == StaticType.nullType &&
singleSpacesList[1].fields.isEmpty &&
singleSpacesList[0].fields.isEmpty) {
singleSpacesList = [new SingleSpace(singleSpacesList[0].type.nullable)];
}
}
return new Space._(path, singleSpacesList);
}
@override
String toString() => 'Patterns($patterns)';
String toString() => singleSpaces.join('|');
}
/// Describes a pattern that matches the value or a field accessed from it.
@ -301,7 +364,7 @@ class Patterns {
class Predicate {
/// The path of getters that led from the original matched value to the value
/// tested by this predicate.
final List<String> path;
final Path path;
/// The type this predicate tests.
// TODO(johnniwinther): In order to model exhaustiveness on enum types,
@ -336,7 +399,7 @@ String _witnessString(List<Predicate> predicates) {
for (Predicate predicate in predicates) {
Witness here = witness;
for (String field in predicate.path) {
for (String field in predicate.path.toList()) {
here = here.fields.putIfAbsent(field, () => new Witness());
}
here.type = predicate.type;
@ -379,3 +442,82 @@ class Witness {
return sb.toString();
}
}
/// A path that describes location of a [SingleSpace] from the root of
/// enclosing [Space].
abstract class Path {
const Path();
/// Create root path.
const factory Path.root() = _Root;
/// Returns a path that adds a step by the [name] to the current path.
Path add(String name) => new _Step(this, name);
void _toList(List<String> list);
/// Returns a list of the names from the root to this path.
List<String> toList();
}
/// The root path object.
class _Root extends Path {
const _Root();
@override
void _toList(List<String> list) {}
@override
List<String> toList() => const [];
@override
int get hashCode => 1729;
@override
bool operator ==(Object other) {
return other is _Root;
}
@override
String toString() => '@';
}
/// A single step in a path that holds the [parent] pointer the [name] for the
/// step.
class _Step extends Path {
final Path parent;
final String name;
_Step(this.parent, this.name);
@override
List<String> toList() {
List<String> list = [];
_toList(list);
return list;
}
@override
void _toList(List<String> list) {
parent._toList(list);
list.add(name);
}
@override
late final int hashCode = Object.hash(parent, name);
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is _Step && name == other.name && parent == other.parent;
}
@override
String toString() {
if (parent is _Root) {
return name;
} else {
return '$parent.$name';
}
}
}

View file

@ -2,8 +2,8 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:_fe_analyzer_shared/src/exhaustiveness/space.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/static_type.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/witness.dart';
import 'package:test/test.dart';
import 'env.dart';
@ -188,10 +188,10 @@ Function(List<StaticType>, String) _makeTestFunction(
var letters = 'ABCDEF';
return (types, covered) {
var spaces = types.map((type) => Space(type)).toList();
var spaces = types.map((type) => Space(const Path.root(), type)).toList();
for (var i = 0; i < allTypes.length; i++) {
var value = Space(allTypes[i]);
var value = Space(const Path.root(), allTypes[i]);
if (covered.contains(letters[i])) {
expectExhaustive(value, spaces);
} else {

View file

@ -2,8 +2,8 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:_fe_analyzer_shared/src/exhaustiveness/exhaustive.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/static_type.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/witness.dart';
import 'package:test/test.dart';
import 'env.dart';

View file

@ -2,7 +2,6 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:_fe_analyzer_shared/src/exhaustiveness/space.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/static_type.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/witness.dart';
import 'package:test/test.dart';
@ -29,15 +28,23 @@ void expectNotExhaustive(Space value, List<Space> spaces) {
_expectExhaustive(value, spaces, false);
}
Map<String, Space> fieldsToSpace(Map<String, Object> fields) =>
fields.map((key, value) => MapEntry(key, parseSpace(value)));
Map<String, Space> fieldsToSpace(Map<String, Object> fields, Path path) =>
fields.map((key, value) => MapEntry(key, parseSpace(value, path.add(key))));
Space parseSpace(Object object) {
Space parseSpace(Object object, [Path path = const Path.root()]) {
if (object is Space) return object;
if (object == '') return Space.empty;
if (object is StaticType) return Space(object);
if (object == '') return Space(path, StaticType.neverType);
if (object is StaticType) return Space(path, object);
if (object is List<Object>) {
return Space.union(object.map(parseSpace).toList());
Space? spaces;
for (Object element in object) {
if (spaces == null) {
spaces = parseSpace(element, path);
} else {
spaces = spaces.union(parseSpace(element, path));
}
}
return spaces ?? new Space.empty(path);
}
throw ArgumentError('Invalid space $object');
}
@ -47,8 +54,9 @@ List<Space> parseSpaces(List<Object> objects) =>
objects.map(parseSpace).toList();
/// Make a [Space] with [type] and [fields].
Space ty(StaticType type, Map<String, Object> fields) =>
Space(type, fieldsToSpace(fields));
Space ty(StaticType type, Map<String, Object> fields,
[Path path = const Path.root()]) =>
Space(path, type, fields: fieldsToSpace(fields, path));
void _checkExhaustive(Space value, List<Space> spaces, bool expectation) {
var actual = isExhaustive(value, spaces);
@ -71,7 +79,7 @@ void _expectExhaustive(Space value, List<Space> spaces, bool expectation) {
/// Test that [cases] are not exhaustive over [type].
void _testCases(StaticType type, List<Object> cases, bool expectation) {
var valueSpace = Space(type);
var valueSpace = Space(const Path.root(), type);
var spaces = parseSpaces(cases);
test('$type with all cases', () {

View file

@ -5,7 +5,7 @@
import 'dart:collection';
import 'package:_fe_analyzer_shared/src/exhaustiveness/exhaustive.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/space.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/witness.dart';
import 'package:analyzer/dart/analysis/declared_variables.dart';
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/ast/ast.dart';
@ -784,10 +784,12 @@ class ConstantVerifier extends RecursiveAstVisitor<void> {
if (guardedPattern != null) {
Space space;
if (guardedPattern.whenClause != null) {
space = Space(_exhaustivenessCache.getUnknownStaticType());
space = Space(
const Path.root(), _exhaustivenessCache.getUnknownStaticType());
} else {
final pattern = guardedPattern.pattern;
space = patternConverter.convertPattern(pattern, nonNull: false);
space = patternConverter.convertPattern(pattern,
nonNull: false, path: const Path.root());
}
caseNodesWithSpace.add(caseNode);
caseSpaces.add(space);
@ -795,14 +797,10 @@ class ConstantVerifier extends RecursiveAstVisitor<void> {
}
// Prepare for recording data for testing.
List<Space>? remainingSpaces;
final exhaustivenessDataForTesting = this.exhaustivenessDataForTesting;
if (exhaustivenessDataForTesting != null) {
remainingSpaces = [];
}
// Compute and report errors.
final errors = reportErrors(scrutineeTypeEx, caseSpaces, remainingSpaces);
final errors = reportErrors(scrutineeTypeEx, caseSpaces);
if (!useFallbackExhaustivenessAlgorithm) {
for (final error in errors) {
if (error is UnreachableCaseError) {
@ -832,22 +830,12 @@ class ConstantVerifier extends RecursiveAstVisitor<void> {
}
// Record data for testing.
if (exhaustivenessDataForTesting != null && remainingSpaces != null) {
assert(remainingSpaces.isEmpty ||
remainingSpaces.length == caseSpaces.length + 1);
if (exhaustivenessDataForTesting != null) {
for (var i = 0; i < caseSpaces.length; i++) {
final caseNode = caseNodesWithSpace[i];
exhaustivenessDataForTesting.caseSpaces[caseNode] = caseSpaces[i];
if (remainingSpaces.isNotEmpty) {
exhaustivenessDataForTesting.remainingSpaces[caseNode] =
remainingSpaces[i];
}
}
exhaustivenessDataForTesting.switchScrutineeType[node] = scrutineeTypeEx;
if (remainingSpaces.isNotEmpty) {
exhaustivenessDataForTesting.remainingSpaces[node] =
remainingSpaces.last;
}
for (var error in errors) {
if (error is UnreachableCaseError) {
exhaustivenessDataForTesting.errors[caseNodesWithSpace[error.index]] =

View file

@ -3,9 +3,9 @@
// BSD-style license that can be found in the LICENSE file.
import 'package:_fe_analyzer_shared/src/exhaustiveness/exhaustive.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/space.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/static_type.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/static_types.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/witness.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
@ -291,10 +291,6 @@ class ExhaustivenessDataForTesting {
/// Map from switch case nodes to the space for its pattern/expression.
Map<AstNode, Space> caseSpaces = {};
/// Map from switch case nodes to the remaining space before the case or
/// from statement/expression nodes to the remaining space after all cases.
Map<AstNode, Space> remainingSpaces = {};
/// Map from switch statement/expression/case nodes to the error reported
/// on the node.
Map<AstNode, ExhaustivenessError> errors = {};
@ -312,11 +308,12 @@ class PatternConverter {
Space convertPattern(
DartPattern pattern, {
required bool nonNull,
required Path path,
}) {
if (pattern is DeclaredVariablePatternImpl) {
final type = pattern.declaredElement!.type;
final staticType = _asStaticType(type, nonNull: nonNull);
return Space(staticType);
return Space(path, staticType);
} else if (pattern is ObjectPattern) {
final fields = <String, Space>{};
for (final field in pattern.fields) {
@ -325,23 +322,24 @@ class PatternConverter {
// TODO(johnniwinther): How do we handle error cases?
continue;
}
fields[name] = convertPattern(field.pattern, nonNull: false);
fields[name] =
convertPattern(field.pattern, nonNull: false, path: path.add(name));
}
final type = pattern.type.typeOrThrow;
final staticType = _asStaticType(type, nonNull: nonNull);
return Space(staticType, fields);
return Space(path, staticType, fields: fields);
} else if (pattern is WildcardPattern) {
final typeNode = pattern.type;
if (typeNode == null) {
if (nonNull) {
return Space(StaticType.nonNullableObject);
return Space(path, StaticType.nonNullableObject);
} else {
return Space.top;
return Space(path, StaticType.nullableObject);
}
} else {
final type = typeNode.typeOrThrow;
final staticType = _asStaticType(type, nonNull: nonNull);
return Space(staticType);
return Space(path, staticType);
}
} else if (pattern is RecordPatternImpl) {
var index = 1;
@ -363,26 +361,26 @@ class PatternConverter {
continue;
}
}
fields[name] = convertPattern(field.pattern, nonNull: false);
fields[name] =
convertPattern(field.pattern, nonNull: false, path: path.add(name));
}
final recordType = RecordType(
positional: positional,
named: named,
nullabilitySuffix: NullabilitySuffix.none,
);
return Space(cache.getStaticType(recordType), fields);
return Space(path, cache.getStaticType(recordType), fields: fields);
} else if (pattern is LogicalOrPattern) {
return Space.union([
convertPattern(pattern.leftOperand, nonNull: nonNull),
convertPattern(pattern.rightOperand, nonNull: nonNull)
]);
return convertPattern(pattern.leftOperand, nonNull: nonNull, path: path)
.union(convertPattern(pattern.rightOperand,
nonNull: nonNull, path: path));
} else if (pattern is NullCheckPattern) {
return convertPattern(pattern.pattern, nonNull: true);
return convertPattern(pattern.pattern, nonNull: true, path: path);
} else if (pattern is ParenthesizedPattern) {
return convertPattern(pattern.pattern, nonNull: nonNull);
return convertPattern(pattern.pattern, nonNull: nonNull, path: path);
} else if (pattern is NullAssertPattern) {
Space space = convertPattern(pattern.pattern, nonNull: true);
return Space.union([space, Space.nullSpace]);
Space space = convertPattern(pattern.pattern, nonNull: true, path: path);
return space.union(Space(path, StaticType.nullType));
} else if (pattern is CastPattern ||
pattern is RelationalPattern ||
pattern is LogicalAndPattern) {
@ -390,23 +388,23 @@ class PatternConverter {
// TODO(johnniwinther): Handle `Null` aspect implicitly covered by
// [NullAssertPattern] and `as Null`.
// TODO(johnniwinther): Handle top in [AndPattern] branches.
return Space(cache.getUnknownStaticType());
return Space(path, cache.getUnknownStaticType());
} else if (pattern is ListPattern || pattern is MapPattern) {
// TODO(johnniwinther): Support list and map patterns. This not only
// requires a new interpretation of [Space] fields that handles the
// relation between concrete lengths, rest patterns with/without
// subpattern, and list/map of arbitrary size and content, but also for the
// runtime to check for lengths < 0.
return Space(cache.getUnknownStaticType());
return Space(path, cache.getUnknownStaticType());
} else if (pattern is ConstantPattern) {
final value = constantPatternValues[pattern];
if (value != null) {
return _convertConstantValue(value);
return _convertConstantValue(value, path);
}
return Space(cache.getUnknownStaticType());
return Space(path, cache.getUnknownStaticType());
}
assert(false, "Unexpected pattern $pattern (${pattern.runtimeType})");
return Space(cache.getUnknownStaticType());
return Space(path, cache.getUnknownStaticType());
}
StaticType _asStaticType(DartType type, {required bool nonNull}) {
@ -414,34 +412,37 @@ class PatternConverter {
return nonNull ? staticType.nonNullable : staticType;
}
Space _convertConstantValue(DartObjectImpl value) {
Space _convertConstantValue(DartObjectImpl value, Path path) {
final state = value.state;
if (value.isNull) {
return Space.nullSpace;
return Space(path, StaticType.nullType);
} else if (state is BoolState) {
final value = state.value;
if (value != null) {
return Space(cache.getBoolValueStaticType(value));
return Space(path, cache.getBoolValueStaticType(state.value!));
}
} else if (state is RecordState) {
final fields = <String, Space>{};
for (var index = 0; index < state.positionalFields.length; index++) {
final name = '\$${1 + index}';
final value = state.positionalFields[index];
fields['\$${1 + index}'] = _convertConstantValue(value);
fields[name] = _convertConstantValue(value, path.add(name));
}
for (final entry in state.namedFields.entries) {
fields[entry.key] = _convertConstantValue(entry.value);
final name = entry.key;
fields[name] = _convertConstantValue(entry.value, path.add(name));
}
return Space(cache.getStaticType(value.type), fields);
return Space(path, cache.getStaticType(value.type), fields: fields);
}
final type = value.type;
if (type is InterfaceType) {
final element = type.element;
if (element is EnumElement) {
return Space(cache.getEnumElementStaticType(element, value));
return Space(path, cache.getEnumElementStaticType(element, value));
}
}
return Space(cache.getUniqueStaticType(type, value, value.toString()));
return Space(
path, cache.getUniqueStaticType(type, value, value.toString()));
}
}

View file

@ -5,7 +5,6 @@
import 'dart:io';
import 'package:_fe_analyzer_shared/src/exhaustiveness/exhaustive.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/space.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/static_type.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/test_helper.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/witness.dart';
@ -84,10 +83,6 @@ class _ExhaustivenessDataExtractor extends AstDataExtractor<Features> {
}
}
}
Space? remainingSpace = _exhaustivenessData.remainingSpaces[node];
if (remainingSpace != null) {
features[Tags.remaining] = spaceToText(remainingSpace);
}
ExhaustivenessError? error = _exhaustivenessData.errors[node];
if (error != null) {
features[Tags.error] = errorToText(error);
@ -95,11 +90,7 @@ class _ExhaustivenessDataExtractor extends AstDataExtractor<Features> {
} else if (node is SwitchMember || node is SwitchExpressionCase) {
Space? caseSpace = _exhaustivenessData.caseSpaces[node];
if (caseSpace != null) {
features[Tags.space] = spaceToText(caseSpace);
}
Space? remainingSpace = _exhaustivenessData.remainingSpaces[node];
if (remainingSpace != null) {
features[Tags.remaining] = spaceToText(remainingSpace);
features[Tags.space] = spacesToText(caseSpace);
}
ExhaustivenessError? error = _exhaustivenessData.errors[node];
if (error != null) {

View file

@ -32,7 +32,7 @@ import 'package:kernel/type_algebra.dart';
import 'package:kernel/type_environment.dart';
import 'package:kernel/target/targets.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/exhaustive.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/space.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/witness.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/static_type.dart';
import '../../base/nnbd_mode.dart';
@ -833,12 +833,7 @@ class ConstantsTransformer extends RemovingTransformer {
cases.add(caseInfo.createSpace(
exhaustivenessCache, constantPatternValues, _staticTypeContext!));
}
List<Space>? remainingSpaces;
if (_exhaustivenessDataForTesting != null) {
remainingSpaces = [];
}
List<ExhaustivenessError> errors =
reportErrors(type, cases, remainingSpaces);
List<ExhaustivenessError> errors = reportErrors(type, cases);
if (!useFallbackExhaustivenessAlgorithm) {
for (ExhaustivenessError error in errors) {
if (error is UnreachableCaseError) {
@ -869,7 +864,6 @@ class ConstantsTransformer extends RemovingTransformer {
switchInfo.cases
.map((SwitchCaseInfo info) => info.fileOffset)
.toList(),
remainingSpaces!,
errors);
}

View file

@ -3,9 +3,9 @@
// BSD-style license that can be found in the LICENSE file.
import 'package:_fe_analyzer_shared/src/exhaustiveness/exhaustive.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/space.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/static_type.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/static_types.dart';
import 'package:_fe_analyzer_shared/src/exhaustiveness/witness.dart';
import 'package:front_end/src/fasta/kernel/constant_evaluator.dart';
import 'package:kernel/ast.dart';
import 'package:kernel/class_hierarchy.dart';
@ -32,11 +32,10 @@ class ExhaustivenessResult {
final StaticType scrutineeType;
final List<Space> caseSpaces;
final List<int> caseOffsets;
final List<Space> remainingSpaces;
final List<ExhaustivenessError> errors;
ExhaustivenessResult(this.scrutineeType, this.caseSpaces, this.caseOffsets,
this.remainingSpaces, this.errors);
ExhaustivenessResult(
this.scrutineeType, this.caseSpaces, this.caseOffsets, this.errors);
}
class CfeTypeOperations implements TypeOperations<DartType> {
@ -347,7 +346,8 @@ class ExpressionCaseInfo extends SwitchCaseInfo {
@override
Space createSpace(CfeExhaustivenessCache cache,
Map<Node, Constant?> constants, StaticTypeContext context) {
return convertExpressionToSpace(cache, expression, constants, context);
return convertExpressionToSpace(
cache, expression, constants, context, const Path.root());
}
}
@ -365,9 +365,11 @@ class PatternCaseInfo extends SwitchCaseInfo {
@override
Space createSpace(CfeExhaustivenessCache cache,
Map<Node, Constant?> constants, StaticTypeContext context) {
if (hasGuard) return new Space(cache.getUnknownStaticType());
if (hasGuard) {
return new Space(const Path.root(), cache.getUnknownStaticType());
}
return convertPatternToSpace(cache, pattern, constants, context,
nonNull: false);
nonNull: false, path: const Path.root());
}
}
@ -400,36 +402,38 @@ Space convertExpressionToSpace(
CfeExhaustivenessCache cache,
Expression expression,
Map<Node, Constant?> constants,
StaticTypeContext context) {
StaticTypeContext context,
Path path) {
Constant? constant = constants[expression];
return convertConstantToSpace(cache, constant, constants, context);
return convertConstantToSpace(cache, constant, constants, context,
path: path);
}
Space convertPatternToSpace(CfeExhaustivenessCache cache, Pattern pattern,
Map<Node, Constant?> constants, StaticTypeContext context,
{required bool nonNull}) {
{required bool nonNull, required Path path}) {
if (pattern is ObjectPattern) {
DartType type = pattern.objectType;
Map<String, Space> fields = {};
for (NamedPattern field in pattern.fields) {
fields[field.name] = convertPatternToSpace(
cache, field.pattern, constants, context,
nonNull: false);
nonNull: false, path: path.add(field.name));
}
StaticType staticType = cache.getStaticType(type);
if (nonNull) {
staticType = staticType.nonNullable;
}
return new Space(staticType, fields);
return new Space(path, staticType, fields: fields);
} else if (pattern is VariablePattern) {
StaticType staticType = cache.getStaticType(pattern.variable.type);
if (nonNull) {
staticType = staticType.nonNullable;
}
return new Space(staticType);
return new Space(path, staticType);
} else if (pattern is ConstantPattern) {
return convertExpressionToSpace(
cache, pattern.expression, constants, context);
cache, pattern.expression, constants, context, path);
} else if (pattern is RecordPattern) {
int index = 1;
Map<String, Space> fields = {};
@ -445,39 +449,37 @@ Space convertPatternToSpace(CfeExhaustivenessCache cache, Pattern pattern,
}
fields[name] = convertPatternToSpace(
cache, subpattern, constants, context,
nonNull: false);
nonNull: false, path: path.add(name));
}
return new Space(cache.getStaticType(pattern.type), fields);
return new Space(path, cache.getStaticType(pattern.type), fields: fields);
} else if (pattern is WildcardPattern) {
final DartType? type = pattern.type;
if (type == null) {
if (nonNull) {
return new Space(StaticType.nonNullableObject);
return new Space(path, StaticType.nonNullableObject);
} else {
return Space.top;
return new Space(path, StaticType.nullableObject);
}
} else {
StaticType staticType = cache.getStaticType(type);
if (nonNull) {
staticType = staticType.nonNullable;
}
return new Space(staticType);
return new Space(path, staticType);
}
} else if (pattern is OrPattern) {
return new Space.union([
convertPatternToSpace(cache, pattern.left, constants, context,
nonNull: nonNull),
convertPatternToSpace(cache, pattern.right, constants, context,
nonNull: nonNull)
]);
return convertPatternToSpace(cache, pattern.left, constants, context,
nonNull: nonNull, path: path)
.union(convertPatternToSpace(cache, pattern.right, constants, context,
nonNull: nonNull, path: path));
} else if (pattern is NullCheckPattern) {
return convertPatternToSpace(cache, pattern.pattern, constants, context,
nonNull: true);
nonNull: true, path: path);
} else if (pattern is NullAssertPattern) {
Space space = convertPatternToSpace(
cache, pattern.pattern, constants, context,
nonNull: true);
return new Space.union([space, Space.nullSpace]);
nonNull: true, path: path);
return space.union(new Space(path, StaticType.nullType));
} else if (pattern is CastPattern ||
pattern is InvalidPattern ||
pattern is RelationalPattern ||
@ -486,48 +488,56 @@ Space convertPatternToSpace(CfeExhaustivenessCache cache, Pattern pattern,
// TODO(johnniwinther): Handle `Null` aspect implicitly covered by
// [NullAssertPattern] and `as Null`.
// TODO(johnniwinther): Handle top in [AndPattern] branches.
return new Space(cache.getUnknownStaticType());
return new Space(path, cache.getUnknownStaticType());
} else if (pattern is ListPattern || pattern is MapPattern) {
// TODO(johnniwinther): Support list and map patterns. This not only
// requires a new interpretation of [Space] fields that handles the
// relation between concrete lengths, rest patterns with/without
// subpattern, and list/map of arbitrary size and content, but also for the
// runtime to check for lengths < 0.
return new Space(cache.getUnknownStaticType());
return new Space(path, cache.getUnknownStaticType());
}
assert(false, "Unexpected pattern $pattern (${pattern.runtimeType}).");
return new Space(cache.getUnknownStaticType());
return new Space(path, cache.getUnknownStaticType());
}
Space convertConstantToSpace(CfeExhaustivenessCache cache, Constant? constant,
Map<Node, Constant?> constants, StaticTypeContext context) {
Map<Node, Constant?> constants, StaticTypeContext context,
{required Path path}) {
if (constant != null) {
if (constant is NullConstant) {
return Space.nullSpace;
return new Space(path, StaticType.nullType);
} else if (constant is BoolConstant) {
return new Space(cache.getBoolValueStaticType(constant.value));
return new Space(path, cache.getBoolValueStaticType(constant.value));
} else if (constant is InstanceConstant && constant.classNode.isEnum) {
return new Space(
cache.getEnumElementStaticType(constant.classNode, constant));
path, cache.getEnumElementStaticType(constant.classNode, constant));
} else if (constant is RecordConstant) {
Map<String, Space> fields = {};
for (int index = 0; index < constant.positional.length; index++) {
fields['\$${index + 1}'] = convertConstantToSpace(
cache, constant.positional[index], constants, context);
String name = '\$${index + 1}';
fields[name] = convertConstantToSpace(
cache, constant.positional[index], constants, context,
path: path.add(name));
}
for (MapEntry<String, Constant> entry in constant.named.entries) {
fields[entry.key] =
convertConstantToSpace(cache, entry.value, constants, context);
String name = entry.key;
fields[name] = convertConstantToSpace(
cache, entry.value, constants, context,
path: path.add(name));
}
return new Space(cache.getStaticType(constant.recordType), fields);
return new Space(path, cache.getStaticType(constant.recordType),
fields: fields);
} else {
return new Space(cache.getUniqueStaticType(
constant.getType(context), constant, '${constant}'));
return new Space(
path,
cache.getUniqueStaticType(
constant.getType(context), constant, '${constant}'));
}
} else {
// TODO(johnniwinther): Assert that constant value is available when the
// exhaustiveness checking is complete.
return new Space(cache.getUnknownStaticType());
return new Space(path, cache.getUnknownStaticType());
}
}

View file

@ -83,9 +83,6 @@ class ExhaustivenessDataExtractor extends CfeDataExtractor<Features> {
}
features[Tags.scrutineeFields] =
fieldsToText(result.scrutineeType.fields);
if (result.remainingSpaces.isNotEmpty) {
features[Tags.remaining] = spaceToText(result.remainingSpaces.last);
}
for (ExhaustivenessError error in result.errors) {
if (error is NonExhaustiveError) {
features[Tags.error] = errorToText(error);
@ -95,10 +92,7 @@ class ExhaustivenessDataExtractor extends CfeDataExtractor<Features> {
for (int i = 0; i < result.caseSpaces.length; i++) {
int offset = result.caseOffsets[i];
Features caseFeatures = new Features();
caseFeatures[Tags.space] = spaceToText(result.caseSpaces[i]);
if (result.remainingSpaces.isNotEmpty) {
caseFeatures[Tags.remaining] = spaceToText(result.remainingSpaces[i]);
}
caseFeatures[Tags.space] = spacesToText(result.caseSpaces[i]);
for (ExhaustivenessError error in result.errors) {
if (error is UnreachableCaseError && error.index == i) {
caseFeatures[Tags.error] = errorToText(error);