blob: 3280423924c976062f96b293c4de9a14ec905d22 [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 'key.dart';
import 'space.dart';
import 'witness.dart';
/// A static type in the type system.
abstract class StaticType {
/// Built-in top type that all types are a subtype of.
static const StaticType nullableObject =
const NullableStaticType(nonNullableObject);
/// Built-in top type that all types are a subtype of.
static const StaticType nonNullableObject = const _NonNullableObject();
/// Built-in `Null` type.
static const StaticType nullType = const _NullType(neverType);
/// Built-in `Never` type.
static const StaticType neverType = const _NeverType();
/// The static types of the fields this type exposes for record destructuring.
///
/// Includes inherited fields.
Map<Key, StaticType> get fields;
/// Returns the static type for the [name] in this static type, or `null` if
/// no such key exists.
///
/// This is used to support implicit on the constant [StaticType]s
/// [nullableObject], [nonNullableObject], [nullType] and [neverType].
StaticType? getField(ObjectFieldLookup fieldLookup, Key key);
/// Returns the static type for the [key] in this static type, or `null` if
/// no such key exists.
///
/// This is used to model keys in map patterns, and indices and ranges in list
/// patterns.
StaticType? getAdditionalField(Key key);
/// Returns `true` if this static type is a subtype of [other], taking the
/// nullability and subtyping relation into account.
bool isSubtypeOf(StaticType other);
/// Whether this type is sealed. A sealed type is implicitly abstract and has
/// a closed set of known subtypes. This means that every instance of the
/// type must be an instance of one of those subtypes. Conversely, if an
/// instance is *not* an instance of one of those subtypes, that it must not
/// be an instance of this type.
///
/// Note that subtypes of a sealed type do not themselves have to be sealed.
/// Consider:
///
/// (A)
/// / \
/// B C
///
/// Here, A is sealed and B and C are not. There may be many unknown
/// subclasses of B and C, or classes implementing their interfaces. That
/// doesn't interfere with exhaustiveness checking because it's still the
/// case that any instance of A must be either a B or C *or some subtype of
/// one of those two types*.
bool get isSealed;
/// Returns `true` if this is a record type.
///
/// This is only used for print the type as part of a [Witness].
bool get isRecord;
/// Returns the name of this static type.
///
/// This is used for printing [Space]s.
String get name;
/// Returns the nullable static type corresponding to this type.
StaticType get nullable;
/// Returns the non-nullable static type corresponding to this type.
StaticType get nonNullable;
/// The immediate subtypes of this type.
///
/// The [keysOfInterest] of interest are the keys used in one of the case
/// rows. This is used to select how a `List` type should be divided into
/// subtypes that should be used for testing the exhaustiveness of a list.
Iterable<StaticType> getSubtypes(Set<Key> keysOfInterest);
/// Returns a textual representation of a single space consisting of this
/// type and the provided [fields] and [additionalFields].
String spaceToText(
Map<Key, Space> spaceFields, Map<Key, Space> additionalSpaceFields);
void witnessToText(StringBuffer buffer, FieldWitness witness,
Map<Key, FieldWitness> witnessFields);
}
mixin _ObjectFieldMixin on _BaseStaticType {
@override
StaticType? getField(ObjectFieldLookup fieldLookup, Key key) {
return fields[key] ?? fieldLookup.getObjectFieldType(key);
}
}
abstract class _BaseStaticType implements StaticType {
const _BaseStaticType();
@override
bool get isRecord => false;
@override
Map<Key, StaticType> get fields => const {};
@override
StaticType? getField(ObjectFieldLookup fieldLookup, Key name) {
return fields[name];
}
@override
StaticType? getAdditionalField(Key key) => null;
@override
Iterable<StaticType> getSubtypes(Set<Key> keysOfInterest) => const [];
@override
String spaceToText(
Map<Key, Space> spaceFields, Map<Key, Space> additionalSpaceFields) {
assert(additionalSpaceFields.isEmpty,
"Additional fields not supported in ${runtimeType}.");
if (this == StaticType.nullableObject && spaceFields.isEmpty) return '()';
if (this == StaticType.neverType && spaceFields.isEmpty) return '∅';
// If there are no fields, just show the type.
if (spaceFields.isEmpty) return name;
StringBuffer buffer = new StringBuffer();
buffer.write(name);
buffer.write('(');
bool first = true;
spaceFields.forEach((Key key, Space space) {
if (!first) buffer.write(', ');
buffer.write('${key.name}: $space');
first = false;
});
buffer.write(')');
return buffer.toString();
}
@override
void witnessToText(StringBuffer buffer, FieldWitness witness,
Map<Key, FieldWitness> witnessFields) {
if (this == StaticType.nullableObject && witnessFields.isEmpty) {
buffer.write('_');
} else if (this == StaticType.nullType && witnessFields.isEmpty) {
buffer.write('null');
} else {
buffer.write(name);
buffer.write('(');
if (witnessFields.isNotEmpty) {
String comma = '';
for (MapEntry<Key, FieldWitness> entry in witnessFields.entries) {
buffer.write(comma);
comma = ', ';
buffer.write(entry.key.name);
buffer.write(': ');
entry.value.witnessToText(buffer);
}
}
buffer.write(')');
}
}
@override
String toString() => name;
}
class _NonNullableObject extends _BaseStaticType with _ObjectFieldMixin {
const _NonNullableObject();
@override
bool get isSealed => false;
@override
bool isSubtypeOf(StaticType other) {
// Object? is a subtype of itself and Object?.
return this == other || other == StaticType.nullableObject;
}
@override
String get name => 'Object';
@override
StaticType get nullable => StaticType.nullableObject;
@override
StaticType get nonNullable => this;
}
class _NeverType extends _BaseStaticType with _ObjectFieldMixin {
const _NeverType();
@override
bool get isSealed => false;
@override
bool isSubtypeOf(StaticType other) {
// Never is a subtype of all types.
return true;
}
@override
String get name => 'Never';
@override
StaticType get nullable => StaticType.nullType;
@override
StaticType get nonNullable => this;
}
class _NullType extends NullableStaticType with _ObjectFieldMixin {
const _NullType(super.underlying);
@override
bool get isSealed {
// Avoid splitting into [nullType] and [neverType].
return false;
}
@override
Iterable<StaticType> getSubtypes(Set<Key> keysOfInterest) {
// Avoid splitting into [nullType] and [neverType].
return const [];
}
@override
String get name => 'Null';
}
class NullableStaticType extends _BaseStaticType with _ObjectFieldMixin {
final StaticType underlying;
const NullableStaticType(this.underlying);
@override
bool get isSealed => true;
@override
Iterable<StaticType> getSubtypes(Set<Key> keysOfInterest) =>
[underlying, StaticType.nullType];
@override
bool isSubtypeOf(StaticType other) {
// A nullable type is a subtype if the underlying type and Null both are.
return this == other ||
other is NullableStaticType && underlying.isSubtypeOf(other.underlying);
}
@override
String get name => '${underlying.name}?';
@override
StaticType get nullable => this;
@override
StaticType get nonNullable => underlying;
@override
int get hashCode => underlying.hashCode * 11;
@override
bool operator ==(other) {
if (identical(this, other)) return true;
return other is NullableStaticType && underlying == other.underlying;
}
}
abstract class NonNullableStaticType extends _BaseStaticType {
@override
late final StaticType nullable = new NullableStaticType(this);
@override
StaticType get nonNullable => this;
@override
bool isSubtypeOf(StaticType other) {
if (this == other) return true;
// All types are subtypes of Object?.
if (other == StaticType.nullableObject) return true;
// All non-nullable types are subtypes of Object.
if (other == StaticType.nonNullableObject) return true;
// A non-nullable type is a subtype of the underlying type of a nullable
// type.
if (other is NullableStaticType) {
return isSubtypeOf(other.underlying);
}
if (isSubtypeOfInternal(other)) {
return true;
}
if (other is WrappedStaticType) {
return isSubtypeOf(other.wrappedType) && isSubtypeOf(other.impliedType);
}
return false;
}
bool isSubtypeOfInternal(StaticType other);
@override
String toString() => name;
}
/// Static type the behaves like [wrappedType] but is also a subtype of
/// [impliedType].
class WrappedStaticType extends NonNullableStaticType {
final StaticType wrappedType;
final StaticType impliedType;
WrappedStaticType(this.wrappedType, this.impliedType);
@override
Map<Key, StaticType> get fields => wrappedType.fields;
@override
bool get isRecord => wrappedType.isRecord;
@override
bool get isSealed => wrappedType.isSealed;
@override
String get name => wrappedType.name;
@override
Iterable<StaticType> getSubtypes(Set<Key> keysOfInterest) => wrappedType
.getSubtypes(keysOfInterest)
.map((e) => new WrappedStaticType(e, impliedType));
@override
bool isSubtypeOfInternal(StaticType other) {
return wrappedType.isSubtypeOf(other) || impliedType.isSubtypeOf(other);
}
@override
int get hashCode => Object.hash(wrappedType, impliedType);
@override
bool operator ==(other) {
if (identical(this, other)) return true;
return other is WrappedStaticType &&
wrappedType == other.wrappedType &&
impliedType == other.impliedType;
}
@override
void witnessToText(StringBuffer buffer, FieldWitness witness,
Map<Key, FieldWitness> witnessFields) {
return wrappedType.witnessToText(buffer, witness, witnessFields);
}
}
/// Interface for accessing the members defined on `Object`.
abstract class ObjectFieldLookup {
/// Returns the [StaticType] for the member with the given [key] defined on
/// `Object`, or `null` none exists.
StaticType? getObjectFieldType(Key key);
}