blob: c3db30d2d26bcd4a413283211ae057823e7e5226 [file] [log] [blame]
// Copyright (c) 2016, 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.
library kernel.type_environment;
import 'ast.dart';
import 'class_hierarchy.dart';
import 'core_types.dart';
import 'type_algebra.dart';
import 'src/bounds_checks.dart';
import 'src/hierarchy_based_type_environment.dart'
show HierarchyBasedTypeEnvironment;
import 'src/types.dart';
typedef void ErrorHandler(TreeNode node, String message);
abstract class TypeEnvironment extends Types {
@override
final CoreTypes coreTypes;
TypeEnvironment.fromSubclass(this.coreTypes, ClassHierarchyBase base)
: super(base);
factory TypeEnvironment(CoreTypes coreTypes, ClassHierarchy hierarchy) {
return new HierarchyBasedTypeEnvironment(coreTypes, hierarchy);
}
@override
ClassHierarchy get hierarchy;
Class get functionClass => coreTypes.functionClass;
Class get objectClass => coreTypes.objectClass;
InterfaceType get objectLegacyRawType => coreTypes.objectLegacyRawType;
InterfaceType get objectNonNullableRawType =>
coreTypes.objectNonNullableRawType;
InterfaceType get objectNullableRawType => coreTypes.objectNullableRawType;
/// Returns the type `List<E>` with the given [nullability] and [elementType]
/// as `E`.
InterfaceType listType(DartType elementType, Nullability nullability) {
return new InterfaceType(
coreTypes.listClass, nullability, <DartType>[elementType]);
}
/// Returns the type `Set<E>` with the given [nullability] and [elementType]
/// as `E`.
InterfaceType setType(DartType elementType, Nullability nullability) {
return new InterfaceType(
coreTypes.setClass, nullability, <DartType>[elementType]);
}
/// Returns the type `Map<K,V>` with the given [nullability], [key] as `K`
/// and [value] is `V`.
InterfaceType mapType(DartType key, DartType value, Nullability nullability) {
return new InterfaceType(
coreTypes.mapClass, nullability, <DartType>[key, value]);
}
/// Returns the type `Iterable<E>` with the given [nullability] and [type]
/// as `E`.
InterfaceType iterableType(DartType type, Nullability nullability) {
return new InterfaceType(
coreTypes.iterableClass, nullability, <DartType>[type]);
}
/// Returns the type `Future<E>` with the given [nullability] and [type]
/// as `E`.
InterfaceType futureType(DartType type, Nullability nullability) {
return new InterfaceType(
coreTypes.futureClass, nullability, <DartType>[type]);
}
DartType? _futureTypeOf(DartType t) {
// We say that S is the "future type of a type" T in the following cases,
// using the first applicable case:
//
// * T implements S and there is a U such that S is Future<U>.
// * T is S bounded and there is a U such that S is FutureOr<U>,
// Future<U>?, or FutureOr<U>?.
//
// When none of these cases are applicable, we say that T does not have a
// future type.
DartType resolved = t.nonTypeVariableBound;
if (resolved is TypeDeclarationType) {
DartType? futureType =
getTypeAsInstanceOf(resolved, coreTypes.futureClass, coreTypes);
if (futureType != null) {
return futureType.withDeclaredNullability(resolved.declaredNullability);
}
} else if (resolved is FutureOrType) {
return resolved;
}
return null;
}
DartType flatten(DartType t) {
// We define the auxiliary function flatten(T) as follows, using the first
// applicable case:
//
// * If T is S? for some S then flatten(T) = flatten(S)?.
// * If T is X & S for some type variable X and type S then
// - if S has future type U then flatten(T) = flatten(U).
// - otherwise, flatten(T) = flatten(X).
// * If T has future type Future<S> or FutureOr<S> then flatten(T) = S.
// * If T has future type Future<S>? or FutureOr<S>? then flatten(T) = S?.
// * Otherwise, flatten(T) = T.
if (t is IntersectionType) {
DartType bound = t.right;
DartType? futureType = _futureTypeOf(bound);
if (futureType != null) {
return flatten(futureType);
} else {
return flatten(t.left);
}
} else {
DartType? futureType = _futureTypeOf(t);
if (futureType is InterfaceType) {
assert(futureType.classNode == coreTypes.futureClass);
DartType typeArgument = futureType.typeArguments.single;
return typeArgument.withDeclaredNullability(
combineNullabilitiesForSubstitution(
combineNullabilitiesForSubstitution(
typeArgument.declaredNullability,
futureType.declaredNullability),
t.declaredNullability));
} else if (futureType is FutureOrType) {
DartType typeArgument = futureType.typeArgument;
return typeArgument.withDeclaredNullability(
combineNullabilitiesForSubstitution(
combineNullabilitiesForSubstitution(
typeArgument.declaredNullability,
futureType.declaredNullability),
t.declaredNullability));
} else {
return t;
}
}
}
/// Computes the underlying type of a union type
///
/// Dart doesn't have generalized union types, but two specific ones: the
/// FutureOr<T> type, which can be seen as a union of T and Future<T>, and the
/// nullable type T?, which can be seen as the union of T and Null. In both
/// cases the union type can be seen as application of the corresponding type
/// constructor, FutureOr or ?, to the underlying type T. [getUnionFreeType]
/// computes the underlying type of the given union type, accounting for
/// potential nesting of the union types.
///
/// The following are examples of the union-free types computed on for the
/// given types.
///
/// getUnionFreeType(int) = int
/// getUnionFreeType(int?) = int
/// getUnionFreeType(FutureOr<int>) = int
/// getUnionFreeType(FutureOr<int?>?) = int
DartType getUnionFreeType(DartType type) {
if (isNullableTypeConstructorApplication(type)) {
return getUnionFreeType(computeTypeWithoutNullabilityMarker(type));
} else if (type is FutureOrType) {
return getUnionFreeType(type.typeArgument);
} else {
return type;
}
}
/// True if [member] is a binary operator whose return type is defined by
/// the both operand types.
bool isSpecialCasedBinaryOperator(Procedure member) {
Class? class_ = member.enclosingClass;
// TODO(johnniwinther): Do we need to recognize backend implementation
// methods?
if (class_ == coreTypes.intClass ||
class_ == coreTypes.numClass ||
class_ == coreTypes.doubleClass) {
String name = member.name.text;
return name == '+' ||
name == '-' ||
name == '*' ||
name == 'remainder' ||
name == '%';
}
return false;
}
/// True if [member] is a ternary operator whose return type is defined by
/// the least upper bound of the operand types.
bool isSpecialCasedTernaryOperator(Procedure member) {
Class? class_ = member.enclosingClass;
if (class_ == coreTypes.intClass || class_ == coreTypes.numClass) {
String name = member.name.text;
return name == 'clamp';
}
return false;
}
/// Returns the static return type of a special cased binary operator
/// (see [isSpecialCasedBinaryOperator]) given the static type of the
/// operands.
DartType getTypeOfSpecialCasedBinaryOperator(DartType type1, DartType type2) {
// Let e be an expression of one of the forms e1 + e2, e1 - e2, e1 * e2,
// e1 % e2 or e1.remainder(e2), where the static type of e1 is a non-Never
// type T and T <: num, and where the static type of e2 is S and S is
// assignable to num. Then:
if (type1 is! NeverType &&
isSubtypeOf(type1, coreTypes.numNonNullableRawType,
SubtypeCheckMode.withNullabilities) &&
type2 is DynamicType ||
isSubtypeOf(type2, coreTypes.numNonNullableRawType,
SubtypeCheckMode.withNullabilities)) {
if (isSubtypeOf(type1, coreTypes.doubleNonNullableRawType,
SubtypeCheckMode.withNullabilities)) {
// If T <: double then the static type of e is double. This includes S
// being dynamic or Never.
return coreTypes.doubleNonNullableRawType;
} else if (type2 is! NeverType &&
isSubtypeOf(type2, coreTypes.doubleNonNullableRawType,
SubtypeCheckMode.withNullabilities)) {
// If S <: double and not S <:Never, then the static type of e is
// double.
return coreTypes.doubleNonNullableRawType;
} else if (isSubtypeOf(type1, coreTypes.intNonNullableRawType,
SubtypeCheckMode.withNullabilities) &&
type2 is! NeverType &&
isSubtypeOf(type2, coreTypes.intNonNullableRawType,
SubtypeCheckMode.withNullabilities)) {
// If T <: int , S <: int and not S <: Never, then the static type of
// e is int.
return coreTypes.intNonNullableRawType;
} else if (type2 is! NeverType &&
isSubtypeOf(type2, type1, SubtypeCheckMode.withNullabilities)) {
// Otherwise the static type of e is num.
return coreTypes.numNonNullableRawType;
}
}
// Otherwise the static type of e is num.
return coreTypes.numNonNullableRawType;
}
DartType getTypeOfSpecialCasedTernaryOperator(
DartType type1, DartType type2, DartType type3) {
// Let e be a normal invocation of the form e1.clamp(e2, e3), where the
// static types of e1, e2 and e3 are T1, T2 and T3 respectively, and where
// T1, T2, and T3 are all non-Never subtypes of num. Then:
if (type1 is! NeverType && type2 is! NeverType && type3 is! NeverType
/* We skip the check that all types are subtypes of num because, if
not, we'll compute the static type to be num, anyway.*/
) {
if (isSubtypeOf(type1, coreTypes.intNonNullableRawType,
SubtypeCheckMode.withNullabilities) &&
isSubtypeOf(type2, coreTypes.intNonNullableRawType,
SubtypeCheckMode.withNullabilities) &&
isSubtypeOf(type3, coreTypes.intNonNullableRawType,
SubtypeCheckMode.withNullabilities)) {
// If T1, T2 and T3 are all subtypes of int, the static type of e is
// int.
return coreTypes.intNonNullableRawType;
} else if (isSubtypeOf(type1, coreTypes.doubleNonNullableRawType,
SubtypeCheckMode.withNullabilities) &&
isSubtypeOf(type2, coreTypes.doubleNonNullableRawType,
SubtypeCheckMode.withNullabilities) &&
isSubtypeOf(type3, coreTypes.doubleNonNullableRawType,
SubtypeCheckMode.withNullabilities)) {
// If T1, T2 and T3 are all subtypes of double, the static type of e
// is double.
return coreTypes.doubleNonNullableRawType;
}
}
// Otherwise the static type of e is num.
return coreTypes.numNonNullableRawType;
}
bool _isRawTypeArgumentEquivalent(
TypeDeclarationType type, int typeArgumentIndex,
{required SubtypeCheckMode subtypeCheckMode}) {
assert(0 <= typeArgumentIndex &&
typeArgumentIndex < type.typeArguments.length);
DartType typeArgument = type.typeArguments[typeArgumentIndex];
DartType defaultType =
type.typeDeclaration.typeParameters[typeArgumentIndex].defaultType;
return areMutualSubtypes(typeArgument, defaultType, subtypeCheckMode);
}
/// Computes sufficiency of a shape check for the given types.
///
/// In expressions of the form `e is T` and `e as T` the static type of the
/// expression `e` is [expressionStaticType], and the type `T` is
/// [checkTargetType].
TypeShapeCheckSufficiency computeTypeShapeCheckSufficiency(
{required DartType expressionStaticType,
required DartType checkTargetType,
required SubtypeCheckMode subtypeCheckMode}) {
if (!IsSubtypeOf.basedSolelyOnNullabilities(
expressionStaticType, checkTargetType)
.inMode(subtypeCheckMode)) {
return TypeShapeCheckSufficiency.insufficient;
} else if (checkTargetType is InterfaceType &&
expressionStaticType is InterfaceType) {
// Analyze if an interface shape check is sufficient.
// If `T` in `e is/as T` doesn't have type arguments, there's nothing more to
// check besides the shape.
// TODO(cstefantsova): Investigate if [expressionStaticType] can be of any
// kind for the following sufficiency check to work.
if (checkTargetType.typeArguments.isEmpty) {
return TypeShapeCheckSufficiency.interfaceShape;
}
// If all of the type arguments of `A<T1, ..., Tn>` in `e is/as A<T1,
// ..., Tn>` are mutual subtypes with the default values for the
// corresponding type parameters, they don't need to be checked because
// in a well-bounded type the type arguments must be subtypes of the
// default types.
bool targetTypeArgumentsAreDefaultTypes = true;
List<TypeParameter> checkTargetTypeOwnTypeParameters =
checkTargetType.classNode.typeParameters;
assert(checkTargetType.typeArguments.length ==
checkTargetTypeOwnTypeParameters.length);
for (int typeParameterIndex = 0;
typeParameterIndex < checkTargetTypeOwnTypeParameters.length;
typeParameterIndex++) {
// TODO(cstefantsova): Investigate if super-bounded types can appear as
// [checkTargetType]s. In that case a subtype check should be done
// instead of the mutual subtype check.
if (!_isRawTypeArgumentEquivalent(checkTargetType, typeParameterIndex,
subtypeCheckMode: subtypeCheckMode)) {
targetTypeArgumentsAreDefaultTypes = false;
break;
}
}
// TODO(cstefantsova): Investigate if [expressionStaticType] can be of any
// kind for the following sufficiency check to work.
if (targetTypeArgumentsAreDefaultTypes) {
return TypeShapeCheckSufficiency.interfaceShape;
}
// If `A<T1, ..., Tn>` in `e is/as A<T1, ... Tn>` has non-trivial type
// arguments, but `e` is of static type `B` without type arguments, where
// `B` is a name of an interface, and `A` is a subclass of `B`, we have
// the following relation holding for any valid `S1`, ..., `Sn`: `A<S1,
// ..., Sn> <: B`, and we can't skip the type argument checks.
if (expressionStaticType.typeArguments.isEmpty) {
return TypeShapeCheckSufficiency.insufficient;
}
// The computation of the sufficiency of the shape check is based on the
// following observations. Let `T` be `A<T1, ..., Tn>` and `B<S1, ...,
// Sk>` be the static type of `e` in `e as/is T`. Let `A` be a subclass
// of `B` and let `B<Q1, ..., Qk>` be `A<T1, ..., Tn>` taken as an
// instance of `B`.
//
// Then the shape check is sufficient if (1) `B<S1, ..., Sk>` is a
// subtype of `B<Q1, ..., Qk>`, and (2) `A<T1, ..., Tn>` is the only
// instance of `A` that yields `B<Q1, ..., Qk>` being taken as an
// instance of `B`.
//
// For example, consider the following:
//
// class C1<X> {}
// class C2<Y> extends C1<Y> {}
//
// If we need to determine the sufficiency of a shape check in the
// expression `e is C2<num>` where `e` is of static type `C1<int>`, we
// compute (condition (2)) `C2<num>` as an instance of `C1`, which yields
// `C1<num>`. Obviously, `C2<TYPE>` taken as an instance of `C1` won't
// yield `C1<num>` for any `TYPE` that isn't `num`. Additionally
// (condition (1)), `C1<int>` is a subtype of `C1<num>`. Therefore, we
// conclude the shape check is sufficient in `e is C2<num>`.
//
// As an example of a case where the shape check isn't sufficient,
// consider the following:
//
// class D1<X> {}
// class D2 extends D1<int> {}
// class D3<Y> extends D2 {}
//
// In the expression `e is D3<num>` let `e` be of static type `D1<int>`.
// Taken as an instance of `D1`, `D3<num>` yields `D1<int>`, and
// `D1<int>` is a subtype of `D1<int>` (condition (1)). However,
// `D3<num>` isn't the only instance of `D3` that yields `D1<int>` when
// taken as an instance of `D1` (failed condition (2)). Examples of other
// such instances are `D3<String>` or `D3<bool>`. Therefore, we conclude
// that the shape check is insufficient in the expression `e is D3<num>`,
// which proves true when we consider the possibility of the runtime type
// of the value of `e` being `D3<String>` or `D3<bool>`.
//
// Finally, we can relax condition (2) by allowing multiple instances of
// `A<T1, ..., Tn>` yielding the same `B<Q1, ..., Qk>` being taken as an
// instance of `B` in case `A<T1, ..., Tn>` is a supertype of all such
// types. Let's call `AA` the set of all instances of `A` that yield
// `B<Q1, ..., Qk>` when taken as an instance of `B`. We replace
// condition (2) with the relaxed condition (2*) requiring that `A<T1,
// ..., Tn>` is such that for every `Ti` either `Ti` is the type argument
// in the i-th position for all instances in `AA` or `Ti` is the default
// type for the i-th type parameter of `A`.
//
// Consider the following example.
//
// class E1<X> {}
// class E2<Y1, Y2> extends E1<Y1> {}
//
// In the expression `e is E2<num, dynamic>` let `e` be of static type
// `E1<int>`. Taken as an instance of `E1`, `E2<num, dynamic>` yields
// `E1<num>`. Condition (1) is satisfied because `E1<int>` is a subtype
// `E1<num>`. Then, to check the condition (2*), we notice that `E2<num,
// TYPE>` yields `E1<num>` for any type `TYPE` taken as an instance of
// `E1`. The first type argument of `E2<num, dynamic>` is the same in all
// such types, and the second type argument is the default type for the
// second parameter of `E2`, so condition (2*) is satisfied. We conclude
// that the shape check is sufficient in `e is E2<enum, dynamic>`.
// First, we compute `B<Q1, ..., Qk>`, which is `A<T1, ..., Tn>` taken as
// an instance of `B` in `e is/as A<T1, ..., Tn>`, where `B<S1, ..., Sk>`
// is the static type of `e`.
InterfaceType? testedAgainstTypeAsOperandClass = hierarchy
.getInterfaceTypeAsInstanceOfClass(
checkTargetType, expressionStaticType.classNode)
?.withDeclaredNullability(checkTargetType.declaredNullability);
// If `A<T1, ..., Tn>` isn't an instance of `B`, the full type check
// should be done.
if (testedAgainstTypeAsOperandClass == null) {
return TypeShapeCheckSufficiency.insufficient;
} else {
// If `A<T1, ..., Tn>` is an instance of `B`, we proceed to checking
// condition (2*). For that we compute `A<X1, ..., Xn>` as an instance
// of `B`, where `X1`, ..., `Xn` are the type variables declared by
// `A`. The resulting type is `B<R1, ..., Rk>`, where `R1`, ..., `Rk`
// may contain occurrences of `X1`, ..., `Xn`.
InterfaceType unsubstitutedTestedAgainstTypeAsOperandClass =
hierarchy.getInterfaceTypeAsInstanceOfClass(
new InterfaceType(checkTargetType.classNode,
checkTargetType.declaredNullability, [
for (TypeParameter typeParameter
in checkTargetTypeOwnTypeParameters)
new TypeParameterType(
typeParameter,
TypeParameterType.computeNullabilityFromBound(
typeParameter))
]),
expressionStaticType.classNode)!;
// Now we search for the occurrences of `X1`, ..., `Xn` in `B<R1,
// ..., Rk>`. Those that are found indicate the positions in `A<T1,
// ..., Tn>` that are fixed and supposed to be the same for every
// instance of `A` that yields `B<Q1, ..., Qk>` when taken as an
// instance of `B`. The other positions, that is, the indices of
// `Xi` that don't occur in `B<R1, ..., Rk>`, are supposed to hold
// the default types for the corresponding parameter of `A` in order
// to satisfy condition (2*).
OccurrenceCollectorVisitor occurrenceCollectorVisitor =
new OccurrenceCollectorVisitor(
checkTargetTypeOwnTypeParameters.toSet());
occurrenceCollectorVisitor
.visit(unsubstitutedTestedAgainstTypeAsOperandClass);
// Check that those of `Xi` that don't occur in `B<R1, ..., Rk>`
// indicate the positions of type arguments in `A<T1, ..., Tn>`
// that are equivalent to default types.
bool allNonOccurringAreDefaultTypes = true;
for (int typeParameterIndex = 0;
typeParameterIndex < checkTargetTypeOwnTypeParameters.length;
typeParameterIndex++) {
if (!occurrenceCollectorVisitor.occurred.contains(
checkTargetTypeOwnTypeParameters[typeParameterIndex]) &&
!_isRawTypeArgumentEquivalent(checkTargetType, typeParameterIndex,
subtypeCheckMode: subtypeCheckMode)) {
allNonOccurringAreDefaultTypes = false;
break;
}
}
if (allNonOccurringAreDefaultTypes) {
// Condition (2*) is satisfied. We need to check condition (1).
return isSubtypeOf(
expressionStaticType,
testedAgainstTypeAsOperandClass,
SubtypeCheckMode.withNullabilities)
? TypeShapeCheckSufficiency.interfaceShape
: TypeShapeCheckSufficiency.insufficient;
} else {
// Condition (2*) is not satisfied.
return TypeShapeCheckSufficiency.insufficient;
}
}
} else if (checkTargetType is RecordType &&
expressionStaticType is RecordType) {
bool isTopRecordTypeForTheShape = true;
for (DartType positional in checkTargetType.positional) {
if (!isTop(positional)) {
isTopRecordTypeForTheShape = false;
break;
}
}
for (NamedType named in checkTargetType.named) {
if (!isTop(named.type)) {
isTopRecordTypeForTheShape = false;
break;
}
}
if (isTopRecordTypeForTheShape) {
// TODO(cstefantsova): Investigate if [expressionStaticType] can be of
// any kind for the following sufficiency check to work.
return TypeShapeCheckSufficiency.recordShape;
}
if (isSubtypeOf(
expressionStaticType, checkTargetType, subtypeCheckMode)) {
return TypeShapeCheckSufficiency.recordShape;
} else {
return TypeShapeCheckSufficiency.insufficient;
}
} else if (checkTargetType is FunctionType &&
expressionStaticType is FunctionType) {
if (checkTargetType.typeParameters.isEmpty &&
expressionStaticType.typeParameters.isEmpty) {
bool isTopFunctionTypeForTheShape = true;
for (DartType positional in checkTargetType.positionalParameters) {
if (!isBottom(positional)) {
isTopFunctionTypeForTheShape = false;
}
}
for (NamedType named in checkTargetType.namedParameters) {
if (!isBottom(named.type)) {
isTopFunctionTypeForTheShape = false;
}
}
if (!isTop(checkTargetType.returnType)) {
isTopFunctionTypeForTheShape = false;
}
if (isTopFunctionTypeForTheShape) {
// TODO(cstefantsova): Investigate if [expressionStaticType] can be of
// any kind for the following sufficiency check to work.
return TypeShapeCheckSufficiency.functionShape;
}
}
if (isSubtypeOf(
expressionStaticType, checkTargetType, subtypeCheckMode)) {
return TypeShapeCheckSufficiency.functionShape;
} else {
return TypeShapeCheckSufficiency.insufficient;
}
} else if (checkTargetType is FutureOrType &&
expressionStaticType is FutureOrType) {
if (isTop(checkTargetType.typeArgument)) {
// TODO(cstefantsova): Investigate if [expressionStaticType] can be of
// any kind for the following sufficiency check to work.
return TypeShapeCheckSufficiency.futureOrShape;
} else if (isSubtypeOf(expressionStaticType.typeArgument,
checkTargetType.typeArgument, subtypeCheckMode)) {
return TypeShapeCheckSufficiency.futureOrShape;
} else {
return TypeShapeCheckSufficiency.insufficient;
}
} else {
return TypeShapeCheckSufficiency.insufficient;
}
}
}
/// Tri-state logical result of a nullability-aware subtype check.
class IsSubtypeOf {
/// Internal value constructed via [IsSubtypeOf.never].
///
/// The integer values of [_valueNever], [_valueOnlyIfIgnoringNullabilities],
/// and [_valueAlways] are important for the implementations of [_andValues],
/// [_all], and [and]. They should be kept in sync.
static const int _valueNever = 0;
/// Internal value constructed via [IsSubtypeOf.onlyIfIgnoringNullabilities].
static const int _valueOnlyIfIgnoringNullabilities = 1;
/// Internal value constructed via [IsSubtypeOf.always].
static const int _valueAlways = 3;
static const List<IsSubtypeOf> _all = const <IsSubtypeOf>[
const IsSubtypeOf.never(),
const IsSubtypeOf.onlyIfIgnoringNullabilities(),
// There's no value for this index so we use `IsSubtypeOf.never()` as a
// dummy value.
const IsSubtypeOf.never(),
const IsSubtypeOf.always()
];
/// Combines results of subtype checks on parts into the overall result.
///
/// It's an implementation detail for [and]. See the comment on [and] for
/// more details and examples. Both [value1] and [value2] should be chosen
/// from [_valueNever], [_valueOnlyIfIgnoringNullabilities], and
/// [_valueAlways]. The method produces the result which is one of
/// [_valueNever], [_valueOnlyIfIgnoringNullabilities], and [_valueAlways].
static int _andValues(int value1, int value2) => value1 & value2;
/// Combines results of the checks on alternatives into the overall result.
///
/// It's an implementation detail for [or]. See the comment on [or] for more
/// details and examples. Both [value1] and [value2] should be chosen from
/// [_valueNever], [_valueOnlyIfIgnoringNullabilities], and [_valueAlways].
/// The method produces the result which is one of [_valueNever],
/// [_valueOnlyIfIgnoringNullabilities], and [_valueAlways].
static int _orValues(int value1, int value2) => value1 | value2;
/// The only state of an [IsSubtypeOf] object.
final int _value;
final DartType? subtype;
final DartType? supertype;
const IsSubtypeOf._internal(int value, this.subtype, this.supertype)
: _value = value;
/// Subtype check succeeds in both modes.
const IsSubtypeOf.always() : this._internal(_valueAlways, null, null);
/// Subtype check succeeds only if the nullability markers are ignored.
///
/// It is assumed that if a subtype check succeeds for two types in full-NNBD
/// mode, it also succeeds for those two types if the nullability markers on
/// the types and all of their sub-terms are ignored (that is, in the pre-NNBD
/// mode). By contraposition, if a subtype check fails for two types when the
/// nullability markers are ignored, it should also fail for those types in
/// full-NNBD mode.
const IsSubtypeOf.onlyIfIgnoringNullabilities(
{DartType? subtype, DartType? supertype})
: this._internal(_valueOnlyIfIgnoringNullabilities, subtype, supertype);
/// Subtype check fails in both modes.
const IsSubtypeOf.never() : this._internal(_valueNever, null, null);
/// Checks if two types are in relation based solely on their nullabilities.
///
/// This is useful on its own if the types are known to be the same modulo the
/// nullability attribute, but mostly it's useful to combine the result from
/// [IsSubtypeOf.basedSolelyOnNullabilities] via [and] with the partial
/// results obtained from other type parts. For example, the overall result
/// for `List<int>? <: List<num>*` can be computed as `Ra.and(Rn)` where `Ra`
/// is the result of a subtype check on the arguments `int` and `num`, and
/// `Rn` is the result of [IsSubtypeOf.basedSolelyOnNullabilities] on the
/// types `List<int>?` and `List<num>*`.
factory IsSubtypeOf.basedSolelyOnNullabilities(
DartType subtype, DartType supertype) {
if (subtype is InvalidType) {
if (supertype is InvalidType) {
return const IsSubtypeOf.always();
}
return new IsSubtypeOf.onlyIfIgnoringNullabilities(
subtype: subtype, supertype: supertype);
}
if (supertype is InvalidType) {
return new IsSubtypeOf.onlyIfIgnoringNullabilities(
subtype: subtype, supertype: supertype);
}
return _basedSolelyOnNullabilitiesNotInvalidType(subtype, supertype);
}
/// Checks if two types are in relation based solely on their nullabilities
/// and where the caller knows that neither type is a `InvalidType`.
factory IsSubtypeOf.basedSolelyOnNullabilitiesNotInvalidType(
DartType subtype, DartType supertype) {
return _basedSolelyOnNullabilitiesNotInvalidType(subtype, supertype);
}
@pragma("vm:prefer-inline")
static IsSubtypeOf _basedSolelyOnNullabilitiesNotInvalidType(
DartType subtype, DartType supertype) {
if (subtype.isPotentiallyNullable && supertype.isPotentiallyNonNullable) {
// It's a special case to test X% <: X%, FutureOr<X%> <: FutureOr<X%>,
// FutureOr<FutureOr<X%>> <: FutureOr<FutureOr<X%>>, etc, where X is a
// type parameter. In that case, the nullabilities of the subtype and the
// supertype are related, that is, they are both nullable or non-nullable
// at run time.
if (subtype.nullability == Nullability.undetermined &&
supertype.nullability == Nullability.undetermined) {
DartType unwrappedSubtype = subtype;
DartType unwrappedSupertype = supertype;
while (unwrappedSubtype is FutureOrType) {
unwrappedSubtype = unwrappedSubtype.typeArgument;
}
while (unwrappedSupertype is FutureOrType) {
unwrappedSupertype = unwrappedSupertype.typeArgument;
}
if (unwrappedSubtype.nullability == unwrappedSupertype.nullability) {
// The relationship between the types must be established elsewhere.
return const IsSubtypeOf.always();
}
}
return new IsSubtypeOf.onlyIfIgnoringNullabilities(
subtype: subtype, supertype: supertype);
}
return const IsSubtypeOf.always();
}
/// Combines results for the type parts into the overall result for the type.
///
/// For example, the result of `A<B1, C1> <: A<B2, C2>` can be computed from
/// the results of the checks `B1 <: B2` and `C1 <: C2`. Using the binary
/// outcome of the checks, the combination of the check results on parts is
/// simply done via `&&`, and [and] is the analog to `&&` for the ternary
/// outcome. So, in the example above the overall result is computed as
/// `Rb.and(Rc)` where `Rb` is the result of `B1 <: B2`, `Rc` is the result
/// of `C1 <: C2`.
IsSubtypeOf and(IsSubtypeOf other) {
int resultValue = _andValues(_value, other._value);
if (resultValue == IsSubtypeOf._valueOnlyIfIgnoringNullabilities) {
// If the type mismatch is due to nullabilities, the mismatching parts are
// remembered in either 'this' or [other]. In that case we need to return
// exactly one of those objects, so that the information about mismatching
// parts is propagated upwards.
if (_value == IsSubtypeOf._valueOnlyIfIgnoringNullabilities) {
return this;
} else {
assert(other._value == IsSubtypeOf._valueOnlyIfIgnoringNullabilities);
return other;
}
} else {
return _all[resultValue];
}
}
/// Shorts the computation of [and] if `this` is [IsSubtypeOf.never].
///
/// Use this instead of [and] for optimization in case the argument to [and]
/// is, for example, a potentially expensive subtype check. Unlike [and],
/// [andSubtypeCheckFor] will immediately return if `this` was constructed as
/// [IsSubtypeOf.never] because the right-hand side will not change the
/// overall result anyway.
IsSubtypeOf andSubtypeCheckFor(
DartType subtype, DartType supertype, Types tester) {
if (_value == _valueNever) return this;
return this
.and(tester.performNullabilityAwareSubtypeCheck(subtype, supertype));
}
/// Combines results of the checks on alternatives into the overall result.
///
/// For example, the result of `T <: FutureOr<S>` can be computed from the
/// results of the checks `T <: S` and `T <: Future<S>`. Using the binary
/// outcome of the checks, the combination of the check results on parts is
/// simply done via logical "or", and [or] is the analog to "or" for the
/// ternary outcome. So, in the example above the overall result is computed
/// as `Rs.or(Rf)` where `Rs` is the result of `T <: S`, `Rf` is the result of
/// `T <: Future<S>`.
IsSubtypeOf or(IsSubtypeOf other) {
int resultValue = _orValues(_value, other._value);
if (resultValue == IsSubtypeOf._valueOnlyIfIgnoringNullabilities) {
// If the type mismatch is due to nullabilities, the mismatching parts are
// remembered in either 'this' or [other]. In that case we need to return
// exactly one of those objects, so that the information about mismatching
// parts is propagated upwards.
if (_value == IsSubtypeOf._valueOnlyIfIgnoringNullabilities) {
return this;
} else {
assert(other._value == IsSubtypeOf._valueOnlyIfIgnoringNullabilities);
return other;
}
} else {
return _all[resultValue];
}
}
/// Shorts the computation of [or] if `this` is [IsSubtypeOf.always].
///
/// Use this instead of [or] for optimization in case the argument to [or] is,
/// for example, a potentially expensive subtype check. Unlike [or],
/// [orSubtypeCheckFor] will immediately return if `this` was constructed
/// as [IsSubtypeOf.always] because the right-hand side will not change the
/// overall result anyway.
IsSubtypeOf orSubtypeCheckFor(
DartType subtype, DartType supertype, Types tester) {
if (_value == _valueAlways) return this;
return this
.or(tester.performNullabilityAwareSubtypeCheck(subtype, supertype));
}
bool isSubtypeWhenIgnoringNullabilities() {
return _value != _valueNever;
}
bool isSubtypeWhenUsingNullabilities() {
return _value == _valueAlways;
}
@override
String toString() {
switch (_value) {
case _valueAlways:
return "IsSubtypeOf.always";
case _valueNever:
return "IsSubtypeOf.never";
case _valueOnlyIfIgnoringNullabilities:
return "IsSubtypeOf.onlyIfIgnoringNullabilities";
}
return "IsSubtypeOf.<unknown value '${_value}'>";
}
bool inMode(SubtypeCheckMode subtypeCheckMode) {
switch (subtypeCheckMode) {
case SubtypeCheckMode.withNullabilities:
return isSubtypeWhenUsingNullabilities();
case SubtypeCheckMode.ignoringNullabilities:
return isSubtypeWhenIgnoringNullabilities();
}
}
}
enum SubtypeCheckMode {
withNullabilities,
ignoringNullabilities,
}
abstract class StaticTypeCache {
DartType getExpressionType(Expression node, StaticTypeContext context);
DartType getForInIteratorType(ForInStatement node, StaticTypeContext context);
DartType getForInElementType(ForInStatement node, StaticTypeContext context);
}
class StaticTypeCacheImpl implements StaticTypeCache {
late Map<Expression, DartType> _expressionTypes = {};
late Map<ForInStatement, DartType> _forInIteratorTypes = {};
late Map<ForInStatement, DartType> _forInElementTypes = {};
@override
DartType getExpressionType(Expression node, StaticTypeContext context) {
return _expressionTypes[node] ??= node.getStaticTypeInternal(context);
}
@override
DartType getForInIteratorType(
ForInStatement node, StaticTypeContext context) {
return _forInIteratorTypes[node] ??= node.getIteratorTypeInternal(context);
}
@override
DartType getForInElementType(ForInStatement node, StaticTypeContext context) {
return _forInElementTypes[node] ??= node.getElementTypeInternal(context);
}
}
/// Context object needed for computing `Expression.getStaticType`.
///
/// The [StaticTypeContext] provides access to the [TypeEnvironment] and the
/// current 'this type' as well as determining the nullability state of the
/// enclosing library.
abstract class StaticTypeContext {
/// The [TypeEnvironment] used for the static type computation.
///
/// This provides access to the core types and the class hierarchy.
TypeEnvironment get typeEnvironment;
/// The static type of a `this` expression.
InterfaceType? get thisType;
/// The enclosing library of this context.
Library get enclosingLibrary;
/// Creates a static type context for computing static types in the body
/// of [member].
factory StaticTypeContext(Member member, TypeEnvironment typeEnvironment,
{StaticTypeCache cache}) = StaticTypeContextImpl;
/// Creates a static type context for computing static types of annotations
/// in [library].
factory StaticTypeContext.forAnnotations(
Library library, TypeEnvironment typeEnvironment,
{StaticTypeCache cache}) = StaticTypeContextImpl.forAnnotations;
/// The [Nullability] used for non-nullable types.
///
/// For opt out libraries this is [Nullability.legacy].
Nullability get nonNullable;
/// The [Nullability] used for nullable types.
///
/// For opt out libraries this is [Nullability.legacy].
Nullability get nullable;
/// Returns the mode under which the current library was compiled.
NonNullableByDefaultCompiledMode get nonNullableByDefaultCompiledMode;
/// Returns the static type of [node].
DartType getExpressionType(Expression node);
/// Returns the static type of the iterator in for-in statement [node].
DartType getForInIteratorType(ForInStatement node);
/// Returns the static type of the element in for-in statement [node].
DartType getForInElementType(ForInStatement node);
}
class StaticTypeContextImpl implements StaticTypeContext {
/// The [TypeEnvironment] used for the static type computation.
///
/// This provides access to the core types and the class hierarchy.
@override
final TypeEnvironment typeEnvironment;
/// The library in which the static type is computed.
final Library _library;
/// The static type of a `this` expression.
@override
final InterfaceType? thisType;
final StaticTypeCache? _cache;
/// Creates a static type context for computing static types in the body
/// of [member].
StaticTypeContextImpl(Member member, TypeEnvironment typeEnvironment,
{StaticTypeCache? cache})
: this.direct(member.enclosingLibrary, typeEnvironment,
thisType: member.enclosingClass?.getThisType(
typeEnvironment.coreTypes, member.enclosingLibrary.nonNullable),
cache: cache);
/// Creates a static type context for computing static types in the body of
/// a member, provided the enclosing [_library] and [thisType].
StaticTypeContextImpl.direct(this._library, this.typeEnvironment,
{this.thisType, StaticTypeCache? cache})
: _cache = cache;
/// Creates a static type context for computing static types of annotations
/// in [library].
StaticTypeContextImpl.forAnnotations(
Library library, TypeEnvironment typeEnvironment,
{StaticTypeCache? cache})
: this.direct(library, typeEnvironment, cache: cache);
@override
Library get enclosingLibrary => _library;
/// The [Nullability] used for non-nullable types.
///
/// For opt out libraries this is [Nullability.legacy].
@override
Nullability get nonNullable => _library.nonNullable;
/// The [Nullability] used for nullable types.
///
/// For opt out libraries this is [Nullability.legacy].
@override
Nullability get nullable => _library.nullable;
/// Returns the mode under which the current library was compiled.
@override
NonNullableByDefaultCompiledMode get nonNullableByDefaultCompiledMode =>
_library.nonNullableByDefaultCompiledMode;
@override
DartType getExpressionType(Expression node) {
if (_cache != null) {
return _cache!.getExpressionType(node, this);
} else {
return node.getStaticTypeInternal(this);
}
}
@override
DartType getForInIteratorType(ForInStatement node) {
if (_cache != null) {
return _cache!.getForInIteratorType(node, this);
} else {
return node.getIteratorTypeInternal(this);
}
}
@override
DartType getForInElementType(ForInStatement node) {
if (_cache != null) {
return _cache!.getForInElementType(node, this);
} else {
return node.getElementTypeInternal(this);
}
}
}
/// Implementation of [StaticTypeContext] that update its state when entering
/// and leaving libraries and members.
abstract class StatefulStaticTypeContext implements StaticTypeContext {
@override
final TypeEnvironment typeEnvironment;
/// Creates a [StatefulStaticTypeContext] that supports entering multiple
/// libraries and/or members successively.
factory StatefulStaticTypeContext.stacked(TypeEnvironment typeEnvironment) =
_StackedStatefulStaticTypeContext;
/// Creates a [StatefulStaticTypeContext] that only supports entering one
/// library and/or member at a time.
factory StatefulStaticTypeContext.flat(TypeEnvironment typeEnvironment) =
_FlatStatefulStaticTypeContext;
StatefulStaticTypeContext._internal(this.typeEnvironment);
/// Updates the [nonNullable] and [thisType] to match static type context for
/// the member [node].
///
/// This should be called before computing static types on the body of member
/// [node].
void enterMember(Member node);
/// Reverts the [nonNullable] and [thisType] values to the previous state.
///
/// This should be called after computing static types on the body of member
/// [node].
void leaveMember(Member node);
/// Updates the [nonNullable] and [thisType] to match static type context for
/// the library [node].
///
/// This should be called before computing static types on annotations in the
/// library [node].
void enterLibrary(Library node);
/// Reverts the [nonNullable] and [thisType] values to the previous state.
///
/// This should be called after computing static types on annotations in the
/// library [node].
void leaveLibrary(Library node);
}
/// Implementation of [StatefulStaticTypeContext] that only supports entering
/// one library and/or at a time.
class _FlatStatefulStaticTypeContext extends StatefulStaticTypeContext {
Library? _currentLibrary;
Member? _currentMember;
_FlatStatefulStaticTypeContext(TypeEnvironment typeEnvironment)
: super._internal(typeEnvironment);
@override
Library get enclosingLibrary => _library;
Library get _library {
Library? library = _currentLibrary ?? _currentMember?.enclosingLibrary;
assert(library != null,
"No library currently associated with StaticTypeContext.");
return library!;
}
@override
InterfaceType? get thisType {
assert(_currentMember != null,
"No member currently associated with StaticTypeContext.");
return _currentMember?.enclosingClass?.getThisType(
typeEnvironment.coreTypes,
_currentMember!.enclosingLibrary.nonNullable);
}
@override
Nullability get nonNullable => _library.nonNullable;
@override
Nullability get nullable => _library.nullable;
@override
NonNullableByDefaultCompiledMode get nonNullableByDefaultCompiledMode =>
_library.nonNullableByDefaultCompiledMode;
/// Updates the [nonNullable] and [thisType] to match static type context for
/// the member [node].
///
/// This should be called before computing static types on the body of member
/// [node].
///
/// Only one member can be entered at a time.
@override
void enterMember(Member node) {
assert(_currentMember == null, "Already in context of $_currentMember");
_currentMember = node;
}
/// Reverts the [nonNullable] and [thisType] values to the previous state.
///
/// This should be called after computing static types on the body of member
/// [node].
@override
void leaveMember(Member node) {
assert(
_currentMember == node,
"Inconsistent static type context stack: "
"Trying to leave $node but current is ${_currentMember}.");
_currentMember = null;
}
/// Updates the [nonNullable] and [thisType] to match static type context for
/// the library [node].
///
/// This should be called before computing static types on annotations in the
/// library [node].
///
/// Only one library can be entered at a time, and not while a member is
/// entered through [enterMember].
@override
void enterLibrary(Library node) {
assert(_currentLibrary == null, "Already in context of $_currentLibrary");
assert(_currentMember == null, "Already in context of $_currentMember");
_currentLibrary = node;
}
/// Reverts the [nonNullable] and [thisType] values to the previous state.
///
/// This should be called after computing static types on annotations in the
/// library [node].
@override
void leaveLibrary(Library node) {
assert(
_currentLibrary == node,
"Inconsistent static type context stack: "
"Trying to leave $node but current is ${_currentLibrary}.");
_currentLibrary = null;
}
@override
DartType getExpressionType(Expression node) =>
node.getStaticTypeInternal(this);
@override
DartType getForInIteratorType(ForInStatement node) =>
node.getIteratorTypeInternal(this);
@override
DartType getForInElementType(ForInStatement node) =>
node.getElementTypeInternal(this);
}
/// Implementation of [StatefulStaticTypeContext] that use a stack to change
/// state when entering and leaving libraries and members.
class _StackedStatefulStaticTypeContext extends StatefulStaticTypeContext {
final List<_StaticTypeContextState> _contextStack =
<_StaticTypeContextState>[];
_StackedStatefulStaticTypeContext(TypeEnvironment typeEnvironment)
: super._internal(typeEnvironment);
@override
Library get enclosingLibrary => _library;
Library get _library {
assert(_contextStack.isNotEmpty,
"No library currently associated with StaticTypeContext.");
return _contextStack.last._library;
}
@override
InterfaceType? get thisType {
assert(_contextStack.isNotEmpty,
"No this type currently associated with StaticTypeContext.");
return _contextStack.last._thisType;
}
@override
Nullability get nonNullable => _library.nonNullable;
@override
Nullability get nullable => _library.nullable;
@override
NonNullableByDefaultCompiledMode get nonNullableByDefaultCompiledMode =>
_library.nonNullableByDefaultCompiledMode;
/// Updates the [library] and [thisType] to match static type context for
/// the member [node].
///
/// This should be called before computing static types on the body of member
/// [node].
@override
void enterMember(Member node) {
_contextStack.add(new _StaticTypeContextState(
node,
node.enclosingLibrary,
node.enclosingClass?.getThisType(
typeEnvironment.coreTypes, node.enclosingLibrary.nonNullable)));
}
/// Reverts the [library] and [thisType] values to the previous state.
///
/// This should be called after computing static types on the body of member
/// [node].
@override
void leaveMember(Member node) {
_StaticTypeContextState state = _contextStack.removeLast();
assert(
state._node == node,
"Inconsistent static type context stack: "
"Trying to leave $node but current is ${state._node}.");
}
/// Updates the [library] and [thisType] to match static type context for
/// the library [node].
///
/// This should be called before computing static types on annotations in the
/// library [node].
@override
void enterLibrary(Library node) {
_contextStack.add(new _StaticTypeContextState(node, node, null));
}
/// Reverts the [library] and [thisType] values to the previous state.
///
/// This should be called after computing static types on annotations in the
/// library [node].
@override
void leaveLibrary(Library node) {
_StaticTypeContextState state = _contextStack.removeLast();
assert(
state._node == node,
"Inconsistent static type context stack: "
"Trying to leave $node but current is ${state._node}.");
}
@override
DartType getExpressionType(Expression node) =>
node.getStaticTypeInternal(this);
@override
DartType getForInIteratorType(ForInStatement node) =>
node.getIteratorTypeInternal(this);
@override
DartType getForInElementType(ForInStatement node) =>
node.getElementTypeInternal(this);
}
class _StaticTypeContextState {
final TreeNode _node;
final Library _library;
final InterfaceType? _thisType;
_StaticTypeContextState(this._node, this._library, this._thisType);
}
/// Describes whether only performing a shape check is sufficient for a
/// successful type check.
///
/// In the following code, in expression `a is B<num>` it is sufficient just to
/// check that the value stored in `a` has the shape `B<_>`, and the checks of
/// the type arguments can be omitted. In contrast, in expression `a is B<int>`
/// the check of the type arguments can't be skipped.
///
/// class A<X> {}
/// class B<Y> extends A<Y> {}
///
/// test(A<num> a) {
/// a is B<num>;
/// a is B<int>;
/// }
enum TypeShapeCheckSufficiency {
/// Indicates that only the shape of the interface type needs to be checked.
interfaceShape,
/// Indicates that only the shape of the record type needs to be checked.
recordShape,
/// Indicates that only the shape of the function type needs to be checked.
functionShape,
/// Indicates that only the shape of the FutureOr type needs to be checked.
futureOrShape,
/// Indicates that a shape check is insufficient, and the full check is
/// required.
insufficient;
}