blob: 2da21576cf94a84e757251a243086424c9521569 [file] [log] [blame]
// 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 'intersect_empty.dart';
import 'profile.dart' as profile;
import 'space.dart';
import 'static_type.dart';
/// Recursively replaces [left] with a union of its sealed subtypes as long as
/// doing so enables it to more precisely match against [right].
List<StaticType> expandType(StaticType left, StaticType right) {
// If [left] is nullable and right is null or non-nullable, then expand the
// nullable type.
if (left.isNullable && (right == StaticType.nullType || !right.isNullable)) {
return [...expandType(left.underlying, right), StaticType.nullType];
}
// If [right] is nullable, then expand using its underlying type.
if (right.isNullable) {
return expandType(left, right.underlying);
}
// If [left] is a sealed supertype and [right] is in its subtype hierarchy,
// then expand out the subtypes (recursively) to more precisely match [right].
if (left.isSealed && left != right && right.isSubtypeOf(left)) {
return {
for (StaticType subtype in left.subtypes) ...expandType(subtype, right),
}.toList();
}
return [left];
}
/// Returns a new [Space] that contains all of the values of [left] that are
/// not also in [right].
Space subtract(Space left, Space right) {
profile.count('subtract');
// Subtracting from empty is still empty.
if (left == Space.empty) return Space.empty;
// Subtracting nothing leaves it unchanged.
if (right == Space.empty) return left;
// Distribute a union on the left.
// A|B - x => A-x | B-x
if (left is UnionSpace) {
return new Space.union(
left.arms.map((arm) => subtract(arm, right)).toList());
}
// Distribute a union on the right.
// x - A|B => x - A - B
if (right is UnionSpace) {
Space result = left;
for (Space arm in right.arms) {
result = subtract(result, arm);
}
return result;
}
// Otherwise, it must be two extract spaces.
return _subtractExtract(left as ExtractSpace, right as ExtractSpace);
}
/// Returns `true` if every field in [leftFields] is covered by the
/// corresponding field in [rightFields].
bool _isLeftSubspace(StaticType leftType, List<String> fieldNames,
Map<String, Space> leftFields, Map<String, Space> rightFields) {
for (String name in fieldNames) {
if (subtract(leftFields[name]!, rightFields[name]!) != Space.empty) {
return false;
}
}
// If we get here, every field covered.
return true;
}
/// Subtract [right] from [left].
Space _subtractExtract(ExtractSpace left, ExtractSpace right) {
List<String> fieldNames =
{...left.fields.keys, ...right.fields.keys}.toList();
List<Space> spaces = <Space>[];
// If the left type is in a sealed hierarchy, expanding it to its subtypes
// might let us calculate the subtraction more precisely.
List<StaticType> subtypes = expandType(left.type, right.type);
for (StaticType subtype in subtypes) {
spaces.addAll(_subtractExtractAtType(subtype, left, right, fieldNames));
}
return new Space.union(spaces);
}
/// Subtract [right] from [left], but using [type] for left's type, which may
/// be a more specific subtype of [left]'s own type is a sealed supertype.
List<Space> _subtractExtractAtType(StaticType type, ExtractSpace left,
ExtractSpace right, List<String> fieldNames) {
// If the right type doesn't cover the left (even after expanding sealed
// types), then we can't do anything with the fields since they may not
// even come into play for all values. Subtract nothing from this subtype
// and keep all of the current fields.
if (!type.isSubtypeOf(right.type)) return [new Space(type, left.fields)];
// Infer any fields that appear in one space and not the other.
Map<String, Space> leftFields = <String, Space>{};
Map<String, Space> rightFields = <String, Space>{};
for (String name in fieldNames) {
// If the right space matches on a field that the left doesn't have, infer
// it from the static type of the field. That contains the same set of
// values as having no field at all.
leftFields[name] = left.fields[name] ?? new Space(type.fields[name]!);
// If the left matches on a field that the right doesn't have, infer top
// for the right field since the right will accept any of left's values for
// that field.
rightFields[name] = right.fields[name] ?? Space.top;
}
// If any pair of fields have no overlapping values, then no overall value
// that matches the left space will also match the right space. So the right
// space doesn't subtract anything and we keep the left space as-is.
for (String name in fieldNames) {
if (intersectEmpty(leftFields[name]!, rightFields[name]!)) {
return [new Space(type, left.fields)];
}
}
// If all the right's fields strictly cover all of the left's, then the
// right completely subtracts this type and nothing remains.
if (_isLeftSubspace(type, fieldNames, leftFields, rightFields)) {
return const [];
}
// The right side is a supertype but its fields don't totally cover, so
// handle each of them individually.
// Walk the fields and see which ones are modified by the right-hand fields.
Map<String, Space> fixed = <String, Space>{};
Map<String, Space> changedDifference = <String, Space>{};
for (String name in fieldNames) {
Space difference = subtract(leftFields[name]!, rightFields[name]!);
if (difference == Space.empty) {
// The right field accepts all the values that the left field accepts, so
// keep the left field as it is.
fixed[name] = leftFields[name]!;
} else if (difference.isTop) {
// If the resulting field matches everything, simply discard it since
// it's equivalent to omitting the field.
} else {
changedDifference[name] = difference;
}
}
// If no fields are affected by the subtraction, just return a single arm
// with all of the fields.
if (changedDifference.isEmpty) return [new Space(type, fixed)];
// For each field whose `left - right` is different, include an arm that
// includes that one difference.
List<String> changedFields = changedDifference.keys.toList();
List<Space> spaces = <Space>[];
for (int i = 0; i < changedFields.length; i++) {
Map<String, Space> fields = {...fixed};
for (int j = 0; j < changedFields.length; j++) {
String name = changedFields[j];
if (i == j) {
fields[name] = changedDifference[name]!;
} else {
fields[name] = leftFields[name]!;
}
}
spaces.add(new Space(type, fields));
}
return spaces;
}