| // Copyright (c) 2017, 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.md file. |
| |
| import 'package:kernel/ast.dart'; |
| |
| import 'package:kernel/core_types.dart'; |
| |
| import 'package:kernel/type_algebra.dart'; |
| |
| import 'package:kernel/type_environment.dart'; |
| |
| import '../names.dart' show callName; |
| |
| import 'type_schema.dart'; |
| |
| import 'type_schema_environment.dart'; |
| |
| /// Creates a collection of [TypeConstraint]s corresponding to type parameters, |
| /// based on an attempt to make one type schema a subtype of another. |
| abstract class TypeConstraintGatherer { |
| final List<_ProtoConstraint> _protoConstraints = []; |
| |
| final List<TypeParameter> _parametersToConstrain; |
| |
| final Library _currentLibrary; |
| |
| /// Creates a [TypeConstraintGatherer] which is prepared to gather type |
| /// constraints for the given [typeParameters]. |
| TypeConstraintGatherer.subclassing( |
| Iterable<TypeParameter> typeParameters, this._currentLibrary) |
| : _parametersToConstrain = typeParameters.toList(); |
| |
| factory TypeConstraintGatherer(TypeSchemaEnvironment environment, |
| Iterable<TypeParameter> typeParameters, Library currentLibrary) { |
| return new TypeSchemaConstraintGatherer( |
| environment, typeParameters, currentLibrary); |
| } |
| |
| CoreTypes get coreTypes; |
| |
| void addUpperBound( |
| TypeConstraint constraint, DartType upper, Library clientLibrary); |
| |
| void addLowerBound( |
| TypeConstraint constraint, DartType lower, Library clientLibrary); |
| |
| Member? getInterfaceMember(Class class_, Name name, {bool setter: false}); |
| |
| InterfaceType? getTypeAsInstanceOf(InterfaceType type, Class superclass, |
| Library clientLibrary, CoreTypes coreTypes); |
| |
| List<DartType>? getTypeArgumentsAsInstanceOf( |
| InterfaceType type, Class superclass); |
| |
| InterfaceType futureType(DartType type, Nullability nullability); |
| |
| /// Returns the set of type constraints that was gathered. |
| Map<TypeParameter, TypeConstraint> computeConstraints(Library clientLibrary) { |
| Map<TypeParameter, TypeConstraint> result = |
| <TypeParameter, TypeConstraint>{}; |
| for (TypeParameter parameter in _parametersToConstrain) { |
| result[parameter] = new TypeConstraint(); |
| } |
| for (_ProtoConstraint protoConstraint in _protoConstraints) { |
| if (protoConstraint.isUpper) { |
| addUpperBound(result[protoConstraint.parameter]!, protoConstraint.bound, |
| clientLibrary); |
| } else { |
| addLowerBound(result[protoConstraint.parameter]!, protoConstraint.bound, |
| clientLibrary); |
| } |
| } |
| return result; |
| } |
| |
| /// Tries to constrain type parameters in [type], so that [bound] <: [type]. |
| /// |
| /// Doesn't change the already accumulated set of constraints if [bound] isn't |
| /// a subtype of [type] under any set of constraints. |
| bool tryConstrainLower(DartType type, DartType bound) { |
| if (_currentLibrary.isNonNullableByDefault) { |
| return _tryNullabilityAwareSubtypeMatch(bound, type, |
| constrainSupertype: true); |
| } else { |
| return _tryNullabilityObliviousSubtypeMatch(bound, type); |
| } |
| } |
| |
| /// Tries to constrain type parameters in [type], so that [type] <: [bound]. |
| /// |
| /// Doesn't change the already accumulated set of constraints if [type] isn't |
| /// a subtype of [bound] under any set of constraints. |
| bool tryConstrainUpper(DartType type, DartType bound) { |
| if (_currentLibrary.isNonNullableByDefault) { |
| return _tryNullabilityAwareSubtypeMatch(type, bound, |
| constrainSupertype: false); |
| } else { |
| return _tryNullabilityObliviousSubtypeMatch(type, bound); |
| } |
| } |
| |
| /// Tries to match [subtype] against [supertype]. |
| /// |
| /// If the match succeeds, the member returns true, and the resulting type |
| /// constraints are recorded for later use by [computeConstraints]. If the |
| /// match fails, the member returns false, and the set of type |
| /// constraints is unchanged. |
| bool _tryNullabilityObliviousSubtypeMatch( |
| DartType subtype, DartType supertype) { |
| int baseConstraintCount = _protoConstraints.length; |
| bool isMatch = _isNullabilityObliviousSubtypeMatch(subtype, supertype); |
| if (!isMatch) { |
| _protoConstraints.length = baseConstraintCount; |
| } |
| return isMatch; |
| } |
| |
| /// Tries to match [subtype] against [supertype]. |
| /// |
| /// If the match succeeds, the member returns true, and the resulting type |
| /// constraints are recorded for later use by [computeConstraints]. If the |
| /// match fails, the member returns false, and the set of type constraints is |
| /// unchanged. |
| /// |
| /// In contrast with [_tryNullabilityObliviousSubtypeMatch], this method |
| /// distinguishes between cases when the type parameters to constraint occur |
| /// in [subtype] and in [supertype]. If [constrainSupertype] is true, the |
| /// type parameters to constrain occur in [supertype]; otherwise, they occur |
| /// in [subtype]. If one type contains the type parameters to constrain, the |
| /// other one isn't allowed to contain them. The type that contains the type |
| /// parameters isn't allowed to also contain [UnknownType], that is, to be a |
| /// type schema. |
| bool _tryNullabilityAwareSubtypeMatch(DartType subtype, DartType supertype, |
| {required bool constrainSupertype}) { |
| // ignore: unnecessary_null_comparison |
| assert(constrainSupertype != null); |
| int baseConstraintCount = _protoConstraints.length; |
| bool isMatch = _isNullabilityAwareSubtypeMatch(subtype, supertype, |
| constrainSupertype: constrainSupertype); |
| if (!isMatch) { |
| _protoConstraints.length = baseConstraintCount; |
| } |
| return isMatch; |
| } |
| |
| /// Add constraint: [lower] <: [parameter] <: TOP. |
| void _constrainParameterLower(TypeParameter parameter, DartType lower) { |
| _protoConstraints.add(new _ProtoConstraint.lower(parameter, lower)); |
| } |
| |
| /// Add constraint: BOTTOM <: [parameter] <: [upper]. |
| void _constrainParameterUpper(TypeParameter parameter, DartType upper) { |
| _protoConstraints.add(new _ProtoConstraint.upper(parameter, upper)); |
| } |
| |
| bool _isFunctionSubtypeMatch(FunctionType subtype, FunctionType supertype) { |
| // A function type `(M0,..., Mn, [M{n+1}, ..., Mm]) -> R0` is a subtype |
| // match for a function type `(N0,..., Nk, [N{k+1}, ..., Nr]) -> R1` with |
| // respect to `L` under constraints `C0 + ... + Cr + C` |
| // - If `R0` is a subtype match for a type `R1` with respect to `L` under |
| // constraints `C`: |
| // - If `n <= k` and `r <= m`. |
| // - And for `i` in `0...r`, `Ni` is a subtype match for `Mi` with respect |
| // to `L` under constraints `Ci`. |
| // Function types with named parameters are treated analogously to the |
| // positional parameter case above. |
| // A generic function type `<T0 extends B0, ..., Tn extends Bn>F0` is a |
| // subtype match for a generic function type `<S0 extends B0, ..., Sn |
| // extends Bn>F1` with respect to `L` under constraints `Cl`: |
| // - If `F0[Z0/T0, ..., Zn/Tn]` is a subtype match for `F0[Z0/S0, ..., |
| // Zn/Sn]` with respect to `L` under constraints `C`, where each `Zi` is a |
| // fresh type variable with bound `Bi`. |
| // - And `Cl` is `C` with each constraint replaced with its closure with |
| // respect to `[Z0, ..., Zn]`. |
| if (subtype.requiredParameterCount > supertype.requiredParameterCount) { |
| return false; |
| } |
| if (subtype.positionalParameters.length < |
| supertype.positionalParameters.length) { |
| return false; |
| } |
| if (subtype.typeParameters.length != supertype.typeParameters.length) { |
| return false; |
| } |
| if (subtype.typeParameters.isNotEmpty) { |
| Map<TypeParameter, DartType> subtypeSubstitution = |
| <TypeParameter, DartType>{}; |
| Map<TypeParameter, DartType> supertypeSubstitution = |
| <TypeParameter, DartType>{}; |
| List<TypeParameter> freshTypeVariables = <TypeParameter>[]; |
| if (!_matchTypeFormals(subtype.typeParameters, supertype.typeParameters, |
| subtypeSubstitution, supertypeSubstitution, freshTypeVariables)) { |
| return false; |
| } |
| |
| subtype = substituteTypeParams( |
| subtype, subtypeSubstitution, freshTypeVariables); |
| supertype = substituteTypeParams( |
| supertype, supertypeSubstitution, freshTypeVariables); |
| } |
| |
| // Test the return types. |
| if (supertype.returnType is! VoidType && |
| !_isNullabilityObliviousSubtypeMatch( |
| subtype.returnType, supertype.returnType)) { |
| return false; |
| } |
| |
| // Test the parameter types. |
| for (int i = 0; i < supertype.positionalParameters.length; ++i) { |
| DartType supertypeParameter = supertype.positionalParameters[i]; |
| DartType subtypeParameter = subtype.positionalParameters[i]; |
| // Termination: Both types shrink in size. |
| if (!_isNullabilityObliviousSubtypeMatch( |
| supertypeParameter, subtypeParameter)) { |
| return false; |
| } |
| } |
| int subtypeNameIndex = 0; |
| for (NamedType supertypeParameter in supertype.namedParameters) { |
| while (subtypeNameIndex < subtype.namedParameters.length && |
| subtype.namedParameters[subtypeNameIndex].name != |
| supertypeParameter.name) { |
| ++subtypeNameIndex; |
| } |
| if (subtypeNameIndex == subtype.namedParameters.length) return false; |
| NamedType subtypeParameter = subtype.namedParameters[subtypeNameIndex]; |
| // Termination: Both types shrink in size. |
| if (!_isNullabilityObliviousSubtypeMatch( |
| supertypeParameter.type, subtypeParameter.type)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| bool _isInterfaceSubtypeMatch( |
| InterfaceType subtype, InterfaceType supertype) { |
| // A type `P<M0, ..., Mk>` is a subtype match for `P<N0, ..., Nk>` with |
| // respect to `L` under constraints `C0 + ... + Ck`: |
| // - If `Mi` is a subtype match for `Ni` with respect to `L` under |
| // constraints `Ci`. |
| // A type `P<M0, ..., Mk>` is a subtype match for `Q<N0, ..., Nj>` with |
| // respect to `L` under constraints `C`: |
| // - If `R<B0, ..., Bj>` is the superclass of `P<M0, ..., Mk>` and `R<B0, |
| // ..., Bj>` is a subtype match for `Q<N0, ..., Nj>` with respect to `L` |
| // under constraints `C`. |
| // - Or `R<B0, ..., Bj>` is one of the interfaces implemented by `P<M0, ..., |
| // Mk>` (considered in lexical order) and `R<B0, ..., Bj>` is a subtype |
| // match for `Q<N0, ..., Nj>` with respect to `L` under constraints `C`. |
| // - Or `R<B0, ..., Bj>` is a mixin into `P<M0, ..., Mk>` (considered in |
| // lexical order) and `R<B0, ..., Bj>` is a subtype match for `Q<N0, ..., |
| // Nj>` with respect to `L` under constraints `C`. |
| |
| // Note that since kernel requires that no class may only appear in the set |
| // of supertypes of a given type more than once, the order of the checks |
| // above is irrelevant; we just need to find the matched superclass, |
| // substitute, and then iterate through type variables. |
| List<DartType>? matchingSupertypeOfSubtypeArguments = |
| getTypeArgumentsAsInstanceOf(subtype, supertype.classNode); |
| if (matchingSupertypeOfSubtypeArguments == null) return false; |
| for (int i = 0; i < supertype.classNode.typeParameters.length; i++) { |
| // Generate constraints and subtype match with respect to variance. |
| int parameterVariance = supertype.classNode.typeParameters[i].variance; |
| if (parameterVariance == Variance.contravariant) { |
| if (!_isNullabilityObliviousSubtypeMatch(supertype.typeArguments[i], |
| matchingSupertypeOfSubtypeArguments[i])) { |
| return false; |
| } |
| } else if (parameterVariance == Variance.invariant) { |
| if (!_isNullabilityObliviousSubtypeMatch(supertype.typeArguments[i], |
| matchingSupertypeOfSubtypeArguments[i]) || |
| !_isNullabilityObliviousSubtypeMatch( |
| matchingSupertypeOfSubtypeArguments[i], |
| supertype.typeArguments[i])) { |
| return false; |
| } |
| } else { |
| if (!_isNullabilityObliviousSubtypeMatch( |
| matchingSupertypeOfSubtypeArguments[i], |
| supertype.typeArguments[i])) { |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| |
| bool _isNull(DartType type) { |
| // TODO(paulberry): would it be better to call this "_isBottom", and to have |
| // it return `true` for both Null and bottom types? Revisit this once |
| // enough functionality is implemented that we can compare the behavior with |
| // the old analyzer-based implementation. |
| return type is NullType; |
| } |
| |
| /// Matches [p] against [q] as a subtype against supertype. |
| /// |
| /// Returns true if [p] is a subtype of [q] under some constraints, and false |
| /// otherwise. The constraints making the relation possible are recorded to |
| /// [_protoConstraints]. It is the responsibility of the caller to cleanup |
| /// [_protoConstraints] in case [p] can't be a subtype of [q]. |
| /// |
| /// If [constrainSupertype] is true, the type parameters to constrain occur in |
| /// [supertype]; otherwise, they occur in [subtype]. If one type contains the |
| /// type parameters to constrain, the other one isn't allowed to contain them. |
| /// The type that contains the type parameters isn't allowed to also contain |
| /// [UnknownType], that is, to be a type schema. |
| bool _isNullabilityAwareSubtypeMatch(DartType p, DartType q, |
| {required bool constrainSupertype}) { |
| // ignore: unnecessary_null_comparison |
| assert(p != null); |
| // ignore: unnecessary_null_comparison |
| assert(q != null); |
| // ignore: unnecessary_null_comparison |
| assert(constrainSupertype != null); |
| |
| // If the type parameters being constrained occur in the supertype (that is, |
| // [q]), the subtype (that is, [p]) is not allowed to contain them. To |
| // check that, the assert below uses the equivalence of the following: X -> |
| // Y <=> !X || Y. |
| assert( |
| !constrainSupertype || |
| !containsTypeVariable(p, _parametersToConstrain.toSet(), |
| unhandledTypeHandler: (DartType type, ignored) => |
| type is UnknownType |
| ? false |
| : throw new UnsupportedError( |
| "Unsupported type '${type.runtimeType}'.")), |
| "Failed implication check: " |
| "constrainSupertype -> !containsTypeVariable(q)"); |
| |
| // If the type parameters being constrained occur in the supertype (that is, |
| // [q]), the supertype is not allowed to contain [UnknownType] as its part, |
| // that is, the supertype should be fully known. To check that, the assert |
| // below uses the equivalence of the following: X -> Y <=> !X || Y. |
| assert( |
| !constrainSupertype || isKnown(q), |
| "Failed implication check: " |
| "constrainSupertype -> isKnown(q)"); |
| |
| // If the type parameters being constrained occur in the subtype (that is, |
| // [p]), the subtype is not allowed to contain [UnknownType] as its part, |
| // that is, the subtype should be fully known. To check that, the assert |
| // below uses the equivalence of the following: X -> Y <=> !X || Y. |
| assert( |
| constrainSupertype || isKnown(p), |
| "Failed implication check: " |
| "!constrainSupertype -> isKnown(p)"); |
| |
| // If the type parameters being constrained occur in the subtype (that is, |
| // [p]), the supertype (that is, [q]) is not allowed to contain them. To |
| // check that, the assert below uses the equivalence of the following: X -> |
| // Y <=> !X || Y. |
| assert( |
| constrainSupertype || |
| !containsTypeVariable(q, _parametersToConstrain.toSet(), |
| unhandledTypeHandler: (DartType type, ignored) => |
| type is UnknownType |
| ? false |
| : throw new UnsupportedError( |
| "Unsupported type '${type.runtimeType}'.")), |
| "Failed implication check: " |
| "!constrainSupertype -> !containsTypeVariable(q)"); |
| |
| if (p is InvalidType || q is InvalidType) return false; |
| |
| // If P is _ then the match holds with no constraints. |
| if (p is UnknownType) return true; |
| |
| // If Q is _ then the match holds with no constraints. |
| if (q is UnknownType) return true; |
| |
| // If P is a type variable X in L, then the match holds: |
| // |
| // Under constraint _ <: X <: Q. |
| if (p is TypeParameterType && |
| isTypeParameterTypeWithoutNullabilityMarker(p, |
| isNonNullableByDefault: _currentLibrary.isNonNullableByDefault) && |
| _parametersToConstrain.contains(p.parameter)) { |
| _constrainParameterUpper(p.parameter, q); |
| return true; |
| } |
| |
| // If Q is a type variable X in L, then the match holds: |
| // |
| // Under constraint P <: X <: _. |
| if (q is TypeParameterType && |
| isTypeParameterTypeWithoutNullabilityMarker(q, |
| isNonNullableByDefault: _currentLibrary.isNonNullableByDefault) && |
| _parametersToConstrain.contains(q.parameter)) { |
| _constrainParameterLower(q.parameter, p); |
| return true; |
| } |
| |
| // If P and Q are identical types, then the subtype match holds under no |
| // constraints. |
| // |
| // We're only checking primitive types for equality, because the algorithm |
| // will recurse over non-primitive types anyway. |
| if (identical(p, q) || |
| isPrimitiveDartType(p) && isPrimitiveDartType(q) && p == q) { |
| return true; |
| } |
| |
| // If P is a legacy type P0* then the match holds under constraint set C: |
| // |
| // Only if P0 is a subtype match for Q under constraint set C. |
| if (isLegacyTypeConstructorApplication(p, |
| isNonNullableByDefault: _currentLibrary.isNonNullableByDefault)) { |
| return _isNullabilityAwareSubtypeMatch( |
| computeTypeWithoutNullabilityMarker(p, |
| isNonNullableByDefault: _currentLibrary.isNonNullableByDefault), |
| q, |
| constrainSupertype: constrainSupertype); |
| } |
| |
| // If Q is a legacy type Q0* then the match holds under constraint set C: |
| // |
| // If P is dynamic or void and P is a subtype match for Q0 under constraint |
| // set C. |
| // Or if P is not dynamic or void and P is a subtype match for Q0? under |
| // constraint set C. |
| if (isLegacyTypeConstructorApplication(q, |
| isNonNullableByDefault: _currentLibrary.isNonNullableByDefault)) { |
| final int baseConstraintCount = _protoConstraints.length; |
| |
| if ((p is DynamicType || p is VoidType) && |
| _isNullabilityAwareSubtypeMatch( |
| p, |
| computeTypeWithoutNullabilityMarker(q, |
| isNonNullableByDefault: |
| _currentLibrary.isNonNullableByDefault), |
| constrainSupertype: constrainSupertype)) { |
| return true; |
| } |
| _protoConstraints.length = baseConstraintCount; |
| |
| if (p is! DynamicType && |
| p is! VoidType && |
| _isNullabilityAwareSubtypeMatch( |
| p, q.withDeclaredNullability(Nullability.nullable), |
| constrainSupertype: constrainSupertype)) { |
| return true; |
| } |
| _protoConstraints.length = baseConstraintCount; |
| } |
| |
| // If Q is FutureOr<Q0> the match holds under constraint set C: |
| // |
| // If P is FutureOr<P0> and P0 is a subtype match for Q0 under constraint |
| // set C. Or if P is a subtype match for Future<Q0> under non-empty |
| // constraint set C. Or if P is a subtype match for Q0 under constraint set |
| // C. Or if P is a subtype match for Future<Q0> under empty constraint set |
| // C. |
| if (q is FutureOrType) { |
| final int baseConstraintCount = _protoConstraints.length; |
| |
| if (p is FutureOrType && |
| _isNullabilityAwareSubtypeMatch(p.typeArgument, q.typeArgument, |
| constrainSupertype: constrainSupertype)) { |
| return true; |
| } |
| _protoConstraints.length = baseConstraintCount; |
| |
| bool isMatchWithFuture = _isNullabilityAwareSubtypeMatch( |
| p, futureType(q.typeArgument, Nullability.nonNullable), |
| constrainSupertype: constrainSupertype); |
| bool matchWithFutureAddsConstraints = |
| _protoConstraints.length != baseConstraintCount; |
| if (isMatchWithFuture && matchWithFutureAddsConstraints) { |
| return true; |
| } |
| _protoConstraints.length = baseConstraintCount; |
| |
| if (_isNullabilityAwareSubtypeMatch(p, q.typeArgument, |
| constrainSupertype: constrainSupertype)) { |
| return true; |
| } |
| _protoConstraints.length = baseConstraintCount; |
| |
| if (isMatchWithFuture && !matchWithFutureAddsConstraints) { |
| return true; |
| } |
| _protoConstraints.length = baseConstraintCount; |
| } |
| |
| // If Q is Q0? the match holds under constraint set C: |
| // |
| // If P is P0? and P0 is a subtype match for Q0 under constraint set C. |
| // Or if P is dynamic or void and Object is a subtype match for Q0 under |
| // constraint set C. |
| // Or if P is a subtype match for Q0 under non-empty constraint set C. |
| // Or if P is a subtype match for Null under constraint set C. |
| // Or if P is a subtype match for Q0 under empty constraint set C. |
| if (isNullableTypeConstructorApplication(q)) { |
| final int baseConstraintCount = _protoConstraints.length; |
| final DartType rawP = computeTypeWithoutNullabilityMarker(p, |
| isNonNullableByDefault: _currentLibrary.isNonNullableByDefault); |
| final DartType rawQ = computeTypeWithoutNullabilityMarker(q, |
| isNonNullableByDefault: _currentLibrary.isNonNullableByDefault); |
| |
| if (isNullableTypeConstructorApplication(p) && |
| _isNullabilityAwareSubtypeMatch(rawP, rawQ, |
| constrainSupertype: constrainSupertype)) { |
| return true; |
| } |
| _protoConstraints.length = baseConstraintCount; |
| |
| if ((p is DynamicType || p is VoidType) && |
| _isNullabilityAwareSubtypeMatch( |
| coreTypes.objectNonNullableRawType, rawQ, |
| constrainSupertype: constrainSupertype)) { |
| return true; |
| } |
| _protoConstraints.length = baseConstraintCount; |
| |
| bool isMatchWithRawQ = _isNullabilityAwareSubtypeMatch(p, rawQ, |
| constrainSupertype: constrainSupertype); |
| bool matchWithRawQAddsConstraints = |
| _protoConstraints.length != baseConstraintCount; |
| if (isMatchWithRawQ && matchWithRawQAddsConstraints) { |
| return true; |
| } |
| _protoConstraints.length = baseConstraintCount; |
| |
| if (_isNullabilityAwareSubtypeMatch(p, const NullType(), |
| constrainSupertype: constrainSupertype)) { |
| return true; |
| } |
| _protoConstraints.length = baseConstraintCount; |
| |
| if (isMatchWithRawQ && !matchWithRawQAddsConstraints) { |
| return true; |
| } |
| _protoConstraints.length = baseConstraintCount; |
| } |
| |
| // If P is FutureOr<P0> the match holds under constraint set C1 + C2: |
| // |
| // If Future<P0> is a subtype match for Q under constraint set C1. |
| // And if P0 is a subtype match for Q under constraint set C2. |
| if (p is FutureOrType) { |
| final int baseConstraintCount = _protoConstraints.length; |
| if (_isNullabilityAwareSubtypeMatch( |
| futureType(p.typeArgument, Nullability.nonNullable), q, |
| constrainSupertype: constrainSupertype) && |
| _isNullabilityAwareSubtypeMatch(p.typeArgument, q, |
| constrainSupertype: constrainSupertype)) { |
| return true; |
| } |
| _protoConstraints.length = baseConstraintCount; |
| } |
| |
| // If P is P0? the match holds under constraint set C1 + C2: |
| // |
| // If P0 is a subtype match for Q under constraint set C1. |
| // And if Null is a subtype match for Q under constraint set C2. |
| if (isNullableTypeConstructorApplication(p)) { |
| final int baseConstraintCount = _protoConstraints.length; |
| if (_isNullabilityAwareSubtypeMatch( |
| computeTypeWithoutNullabilityMarker(p, |
| isNonNullableByDefault: |
| _currentLibrary.isNonNullableByDefault), |
| q, |
| constrainSupertype: constrainSupertype) && |
| _isNullabilityAwareSubtypeMatch(const NullType(), q, |
| constrainSupertype: constrainSupertype)) { |
| return true; |
| } |
| _protoConstraints.length = baseConstraintCount; |
| } |
| |
| // If Q is dynamic, Object?, or void then the match holds under no |
| // constraints. |
| if (q is DynamicType || |
| q is VoidType || |
| q == coreTypes.objectNullableRawType) { |
| return true; |
| } |
| |
| // If P is Never then the match holds under no constraints. |
| if (p is NeverType && p.declaredNullability == Nullability.nonNullable) { |
| return true; |
| } |
| |
| // If Q is Object, then the match holds under no constraints: |
| // |
| // Only if P is non-nullable. |
| if (q == coreTypes.objectNonNullableRawType) { |
| return p.nullability == Nullability.nonNullable; |
| } |
| |
| // If P is Null, then the match holds under no constraints: |
| // |
| // Only if Q is nullable. |
| if (p is NullType) { |
| return q.nullability == Nullability.nullable; |
| } |
| |
| // If P is a type variable X with bound B (or a promoted type variable X & |
| // B), the match holds with constraint set C: |
| // |
| // If B is a subtype match for Q with constraint set C. Note that we have |
| // already eliminated the case that X is a variable in L. |
| if (p is TypeParameterType) { |
| final int baseConstraintCount = _protoConstraints.length; |
| if (_isNullabilityAwareSubtypeMatch(p.bound, q, |
| constrainSupertype: constrainSupertype)) { |
| return true; |
| } |
| _protoConstraints.length = baseConstraintCount; |
| } |
| |
| // If P is C<M0, ..., Mk> and Q is C<N0, ..., Nk>, then the match holds |
| // under constraints C0 + ... + Ck: |
| // |
| // If Mi is a subtype match for Ni with respect to L under constraints Ci. |
| if (p is InterfaceType && |
| q is InterfaceType && |
| p.classNode == q.classNode) { |
| assert(p.typeArguments.length == q.typeArguments.length); |
| |
| final int baseConstraintCount = _protoConstraints.length; |
| bool isMatch = true; |
| for (int i = 0; isMatch && i < p.typeArguments.length; ++i) { |
| isMatch = isMatch && |
| _isNullabilityAwareSubtypeMatch( |
| p.typeArguments[i], q.typeArguments[i], |
| constrainSupertype: constrainSupertype); |
| } |
| if (isMatch) return true; |
| _protoConstraints.length = baseConstraintCount; |
| } |
| |
| // If P is C0<M0, ..., Mk> and Q is C1<N0, ..., Nj> then the match holds |
| // with respect to L under constraints C: |
| // |
| // If C1<B0, ..., Bj> is a superinterface of C0<M0, ..., Mk> and C1<B0, ..., |
| // Bj> is a subtype match for C1<N0, ..., Nj> with respect to L under |
| // constraints C. |
| if (p is InterfaceType && q is InterfaceType) { |
| final List<DartType>? sArguments = |
| getTypeArgumentsAsInstanceOf(p, q.classNode); |
| if (sArguments != null) { |
| assert(sArguments.length == q.typeArguments.length); |
| |
| final int baseConstraintCount = _protoConstraints.length; |
| bool isMatch = true; |
| for (int i = 0; isMatch && i < sArguments.length; ++i) { |
| isMatch = isMatch && |
| _isNullabilityAwareSubtypeMatch(sArguments[i], q.typeArguments[i], |
| constrainSupertype: constrainSupertype); |
| } |
| if (isMatch) return true; |
| _protoConstraints.length = baseConstraintCount; |
| } |
| } |
| |
| // If Q is Function then the match holds under no constraints: |
| // |
| // If P is a function type. |
| if (q == coreTypes.functionNonNullableRawType && p is FunctionType) { |
| return true; |
| } |
| |
| // A function type (M0,..., Mn, [M{n+1}, ..., Mm]) -> R0 is a subtype match |
| // for a function type (N0,..., Nk, [N{k+1}, ..., Nr]) -> R1 with respect to |
| // L under constraints C0 + ... + Cr + C |
| // |
| // If R0 is a subtype match for a type R1 with respect to L under |
| // constraints C. If n <= k and r <= m. And for i in 0...r, Ni is a |
| // subtype match for Mi with respect to L under constraints Ci. |
| if (p is FunctionType && |
| q is FunctionType && |
| p.typeParameters.isEmpty && |
| q.typeParameters.isEmpty && |
| p.namedParameters.isEmpty && |
| q.namedParameters.isEmpty && |
| p.requiredParameterCount <= q.requiredParameterCount && |
| p.positionalParameters.length >= q.positionalParameters.length) { |
| final int baseConstraintCount = _protoConstraints.length; |
| |
| if (_isNullabilityAwareSubtypeMatch(p.returnType, q.returnType, |
| constrainSupertype: constrainSupertype)) { |
| bool isMatch = true; |
| for (int i = 0; isMatch && i < q.positionalParameters.length; ++i) { |
| isMatch = isMatch && |
| _isNullabilityAwareSubtypeMatch( |
| q.positionalParameters[i], p.positionalParameters[i], |
| constrainSupertype: !constrainSupertype); |
| } |
| if (isMatch) return true; |
| } |
| _protoConstraints.length = baseConstraintCount; |
| } |
| |
| // Function types with named parameters are treated analogously to the |
| // positional parameter case above. |
| if (p is FunctionType && |
| q is FunctionType && |
| p.typeParameters.isEmpty && |
| q.typeParameters.isEmpty && |
| p.positionalParameters.length == p.requiredParameterCount && |
| q.positionalParameters.length == q.requiredParameterCount && |
| p.requiredParameterCount == q.requiredParameterCount && |
| (p.namedParameters.isNotEmpty || q.namedParameters.isNotEmpty)) { |
| final int baseConstraintCount = _protoConstraints.length; |
| |
| if (_isNullabilityAwareSubtypeMatch(p.returnType, q.returnType, |
| constrainSupertype: constrainSupertype)) { |
| bool isMatch = true; |
| for (int i = 0; isMatch && i < p.positionalParameters.length; ++i) { |
| isMatch = isMatch && |
| _isNullabilityAwareSubtypeMatch( |
| q.positionalParameters[i], p.positionalParameters[i], |
| constrainSupertype: !constrainSupertype); |
| } |
| Map<String, DartType> pNamedTypes = {}; |
| for (int i = 0; isMatch && i < p.namedParameters.length; ++i) { |
| pNamedTypes[p.namedParameters[i].name] = p.namedParameters[i].type; |
| } |
| for (int i = 0; isMatch && i < q.namedParameters.length; ++i) { |
| isMatch = |
| isMatch && pNamedTypes.containsKey(q.namedParameters[i].name); |
| isMatch = isMatch && |
| _isNullabilityAwareSubtypeMatch(q.namedParameters[i].type, |
| pNamedTypes[q.namedParameters[i].name]!, |
| constrainSupertype: !constrainSupertype); |
| } |
| if (isMatch) return true; |
| } |
| _protoConstraints.length = baseConstraintCount; |
| } |
| |
| // A generic function type <T0 extends B00, ..., Tn extends B0n>F0 is a |
| // subtype match for a generic function type <S0 extends B10, ..., Sn |
| // extends B1n>F1 with respect to L under constraint set C2 |
| // |
| // If B0i is a subtype match for B1i with constraint set Ci0. And B1i is a |
| // subtype match for B0i with constraint set Ci1. And Ci2 is Ci0 + Ci1. |
| // |
| // And Z0...Zn are fresh variables with bounds B20, ..., B2n, Where B2i is |
| // B0i[Z0/T0, ..., Zn/Tn] if P is a type schema. Or B2i is B1i[Z0/S0, ..., |
| // Zn/Sn] if Q is a type schema. In other words, we choose the bounds for |
| // the fresh variables from whichever of the two generic function types is a |
| // type schema and does not contain any variables from L. |
| // |
| // And F0[Z0/T0, ..., Zn/Tn] is a subtype match for F1[Z0/S0, ..., Zn/Sn] |
| // with respect to L under constraints C0. And C1 is C02 + ... + Cn2 + C0. |
| // And C2 is C1 with each constraint replaced with its closure with respect |
| // to [Z0, ..., Zn]. |
| if (p is FunctionType && |
| q is FunctionType && |
| p.typeParameters.isNotEmpty && |
| q.typeParameters.isNotEmpty && |
| p.typeParameters.length == q.typeParameters.length) { |
| final int baseConstraintCount = _protoConstraints.length; |
| |
| bool isMatch = true; |
| for (int i = 0; isMatch && i < p.typeParameters.length; ++i) { |
| isMatch = isMatch && |
| _isNullabilityAwareSubtypeMatch( |
| p.typeParameters[i].bound, q.typeParameters[i].bound, |
| constrainSupertype: constrainSupertype) && |
| _isNullabilityAwareSubtypeMatch( |
| q.typeParameters[i].bound, p.typeParameters[i].bound, |
| constrainSupertype: !constrainSupertype); |
| } |
| if (isMatch) { |
| FreshTypeParameters freshTypeParameters = getFreshTypeParameters( |
| constrainSupertype ? p.typeParameters : q.typeParameters); |
| Substitution substitutionForBound = freshTypeParameters.substitution; |
| FunctionType constrainedType = constrainSupertype ? q : p; |
| Map<TypeParameter, DartType> substitutionMapForConstrainedType = {}; |
| for (int i = 0; i < constrainedType.typeParameters.length; ++i) { |
| substitutionMapForConstrainedType[constrainedType.typeParameters[i]] = |
| new TypeParameterType.forAlphaRenaming( |
| constrainedType.typeParameters[i], |
| freshTypeParameters.freshTypeParameters[i]); |
| } |
| Substitution substitutionForConstrainedType = |
| Substitution.fromMap(substitutionMapForConstrainedType); |
| Substitution substitutionForP = constrainSupertype |
| ? substitutionForBound |
| : substitutionForConstrainedType; |
| Substitution substitutionForQ = constrainSupertype |
| ? substitutionForConstrainedType |
| : substitutionForBound; |
| if (_isNullabilityAwareSubtypeMatch( |
| substitutionForP.substituteType(p.withoutTypeParameters), |
| substitutionForQ.substituteType(q.withoutTypeParameters), |
| constrainSupertype: constrainSupertype)) { |
| List<_ProtoConstraint> constraints = |
| _protoConstraints.sublist(baseConstraintCount); |
| _protoConstraints.length = baseConstraintCount; |
| NullabilityAwareTypeVariableEliminator eliminator = |
| new NullabilityAwareTypeVariableEliminator( |
| eliminationTargets: |
| freshTypeParameters.freshTypeParameters.toSet(), |
| bottomType: const NeverType.nonNullable(), |
| topType: coreTypes.objectNullableRawType, |
| topFunctionType: coreTypes.functionNonNullableRawType, |
| unhandledTypeHandler: (DartType type, ignored) => |
| type is UnknownType |
| ? false |
| : throw new UnsupportedError( |
| "Unsupported type '${type.runtimeType}'.")); |
| for (_ProtoConstraint constraint in constraints) { |
| if (constraint.isUpper) { |
| _constrainParameterUpper(constraint.parameter, |
| eliminator.eliminateToLeast(constraint.bound)); |
| } else { |
| _constrainParameterLower(constraint.parameter, |
| eliminator.eliminateToGreatest(constraint.bound)); |
| } |
| } |
| return true; |
| } |
| } |
| _protoConstraints.length = baseConstraintCount; |
| } |
| |
| return false; |
| } |
| |
| /// Attempts to match [subtype] as a subtype of [supertype], gathering any |
| /// constraints discovered in the process. |
| /// |
| /// If a set of constraints was found, `true` is returned and the caller |
| /// may proceed to call [computeConstraints]. Otherwise, `false` is returned. |
| /// |
| /// In the case where `false` is returned, some bogus constraints may have |
| /// been added to [_protoConstraints]. It is the caller's responsibility to |
| /// discard them if necessary. |
| bool _isNullabilityObliviousSubtypeMatch( |
| DartType subtype, DartType supertype) { |
| // The unknown type `?` is a subtype match for any type `Q` with no |
| // constraints. |
| if (subtype is UnknownType) return true; |
| // Any type `P` is a subtype match for the unknown type `?` with no |
| // constraints. |
| if (supertype is UnknownType) return true; |
| // A type variable `T` in `L` is a subtype match for any type schema `Q`: |
| // - Under constraint `T <: Q`. |
| if (subtype is TypeParameterType && |
| _parametersToConstrain.contains(subtype.parameter)) { |
| _constrainParameterUpper(subtype.parameter, supertype); |
| return true; |
| } |
| // A type schema `Q` is a subtype match for a type variable `T` in `L`: |
| // - Under constraint `Q <: T`. |
| if (supertype is TypeParameterType && |
| _parametersToConstrain.contains(supertype.parameter)) { |
| _constrainParameterLower(supertype.parameter, subtype); |
| return true; |
| } |
| // Any two equal types `P` and `Q` are subtype matches under no constraints. |
| // Note: to avoid making the algorithm quadratic, we just check for |
| // identical(). If P and Q are equal but not identical, recursing through |
| // the types will give the proper result. |
| if (identical(subtype, supertype)) return true; |
| |
| // Handle FutureOr<T> union type. |
| if (subtype is FutureOrType) { |
| DartType subtypeArg = subtype.typeArgument; |
| if (supertype is FutureOrType) { |
| // `FutureOr<P>` is a subtype match for `FutureOr<Q>` with respect to |
| // `L` under constraints `C`: |
| // - If `P` is a subtype match for `Q` with respect to `L` under |
| // constraints `C`. |
| DartType supertypeArg = supertype.typeArgument; |
| return _isNullabilityObliviousSubtypeMatch(subtypeArg, supertypeArg); |
| } |
| |
| // `FutureOr<P>` is a subtype match for `Q` with respect to `L` under |
| // constraints `C0 + C1`: |
| // - If `Future<P>` is a subtype match for `Q` with respect to `L` under |
| // constraints `C0`. |
| // - And `P` is a subtype match for `Q` with respect to `L` under |
| // constraints `C1`. |
| InterfaceType subtypeFuture = |
| futureType(subtypeArg, _currentLibrary.nonNullable); |
| return _isNullabilityObliviousSubtypeMatch(subtypeFuture, supertype) && |
| _isNullabilityObliviousSubtypeMatch(subtypeArg, supertype) && |
| new IsSubtypeOf.basedSolelyOnNullabilities(subtype, supertype) |
| .isSubtypeWhenUsingNullabilities(); |
| } |
| |
| if (supertype is FutureOrType) { |
| // `P` is a subtype match for `FutureOr<Q>` with respect to `L` under |
| // constraints `C`: |
| // - If `P` is a subtype match for `Future<Q>` with respect to `L` under |
| // constraints `C`. |
| // - Or `P` is not a subtype match for `Future<Q>` with respect to `L` |
| // under constraints `C` |
| // - And `P` is a subtype match for `Q` with respect to `L` under |
| // constraints `C` |
| |
| // Since FutureOr<S> is a union type Future<S> U S where U denotes the |
| // union operation on types, T? is T U Null, T U T = T, S U T = T U S, and |
| // S U (T U V) = (S U T) U V, the following is true: |
| // |
| // - FutureOr<S?> = S? U Future<S?>? |
| // - FutureOr<S>? = S? U Future<S>? |
| // |
| // To compute the nullabilities for the two types in the union, the |
| // nullability of the argument and the declared nullability of FutureOr |
| // should be united. Also, computeNullability is used to fetch the |
| // nullability of the argument because it can be a FutureOr itself. |
| Nullability unitedNullability = uniteNullabilities( |
| supertype.typeArgument.nullability, supertype.nullability); |
| DartType supertypeArg = |
| supertype.typeArgument.withDeclaredNullability(unitedNullability); |
| DartType supertypeFuture = futureType(supertypeArg, unitedNullability); |
| |
| // The match against FutureOr<X> succeeds if the match against either |
| // Future<X> or X succeeds. If they both succeed, the one adding new |
| // constraints should be preferred. If both matches against Future<X> and |
| // X add new constraints, the former should be preferred over the latter. |
| int oldProtoConstraintsLength = _protoConstraints.length; |
| bool matchesFuture = |
| _tryNullabilityObliviousSubtypeMatch(subtype, supertypeFuture); |
| bool matchesArg = oldProtoConstraintsLength != _protoConstraints.length |
| ? false |
| : _isNullabilityObliviousSubtypeMatch(subtype, supertypeArg); |
| return matchesFuture || matchesArg; |
| } |
| |
| // Any type `P` is a subtype match for `dynamic`, `Object`, or `void` under |
| // no constraints. |
| if (_isTop(supertype)) return true; |
| // `Null` is a subtype match for any type `Q` under no constraints. |
| // Note that nullable types will change this. |
| if (_isNull(subtype)) return true; |
| |
| // A type variable `T` not in `L` with bound `P` is a subtype match for the |
| // same type variable `T` with bound `Q` with respect to `L` under |
| // constraints `C`: |
| // - If `P` is a subtype match for `Q` with respect to `L` under constraints |
| // `C`. |
| if (subtype is TypeParameterType) { |
| if (supertype is TypeParameterType && |
| identical(subtype.parameter, supertype.parameter)) { |
| // Kernel doesn't yet allow a type variable to have different bounds |
| // under different circumstances (see dartbug.com/29529) so for now if |
| // we get here, the bounds must be the same. |
| // TODO(paulberry): update this code once dartbug.com/29529 is |
| // addressed. |
| return true; |
| } |
| // A type variable `T` not in `L` with bound `P` is a subtype match for a |
| // type `Q` with respect to `L` under constraints `C`: |
| // - If `P` is a subtype match for `Q` with respect to `L` under |
| // constraints `C`. |
| return _isNullabilityObliviousSubtypeMatch( |
| subtype.parameter.bound, supertype); |
| } |
| if (subtype is InterfaceType && supertype is InterfaceType) { |
| return _isInterfaceSubtypeMatch(subtype, supertype); |
| } |
| if (subtype is FunctionType) { |
| if (supertype is InterfaceType) { |
| return supertype == coreTypes.functionLegacyRawType || |
| supertype == coreTypes.objectLegacyRawType; |
| } else if (supertype is FunctionType) { |
| return _isFunctionSubtypeMatch(subtype, supertype); |
| } |
| } |
| // A type `P` is a subtype match for a type `Q` with respect to `L` under |
| // constraints `C`: |
| // - If `P` is an interface type which implements a call method of type `F`, |
| // and `F` is a subtype match for a type `Q` with respect to `L` under |
| // constraints `C`. |
| if (subtype is InterfaceType) { |
| Member? callMember = getInterfaceMember(subtype.classNode, callName); |
| if (callMember is Procedure && !callMember.isGetter) { |
| DartType callType = callMember.getterType; |
| // ignore: unnecessary_null_comparison |
| if (callType != null) { |
| callType = |
| Substitution.fromInterfaceType(subtype).substituteType(callType); |
| // TODO(kmillikin): The subtype check will fail if the type of a |
| // generic call method is a subtype of a non-generic function type. |
| // For example, if `T call<T>(T arg)` is a subtype of `S->S` for some |
| // S. However, explicitly tearing off that call method will work and |
| // insert an explicit instantiation, so the implicit tear off should |
| // work as well. Figure out how to support this case. |
| return _isNullabilityObliviousSubtypeMatch(callType, supertype); |
| } |
| } |
| } |
| return false; |
| } |
| |
| bool _isTop(DartType type) => |
| type is DynamicType || |
| type is VoidType || |
| type == coreTypes.objectLegacyRawType; |
| |
| /// Given two lists of function type formal parameters, checks that their |
| /// bounds are compatible. |
| /// |
| /// The return value indicates whether a match was found. If it was, entries |
| /// are added to [substitution1] and [substitution2] which substitute a fresh |
| /// set of type variables for the type parameters [params1] and [params2], |
| /// respectively, allowing further comparison. |
| bool _matchTypeFormals( |
| List<TypeParameter> params1, |
| List<TypeParameter> params2, |
| Map<TypeParameter, DartType> substitution1, |
| Map<TypeParameter, DartType> substitution2, |
| List<TypeParameter> freshTypeVariables) { |
| assert(params1.length == params2.length); |
| // TODO(paulberry): in imitation of analyzer, we're checking the bounds as |
| // we build up the substitutions. But I don't think that's correct--I think |
| // we should build up both substitutions completely before checking any |
| // bounds. See dartbug.com/29629. |
| for (int i = 0; i < params1.length; ++i) { |
| TypeParameter pFresh = new TypeParameter(params2[i].name); |
| freshTypeVariables.add(pFresh); |
| DartType variableFresh = |
| new TypeParameterType.forAlphaRenaming(params2[i], pFresh); |
| substitution1[params1[i]] = variableFresh; |
| substitution2[params2[i]] = variableFresh; |
| DartType bound1 = substitute(params1[i].bound, substitution1); |
| DartType bound2 = substitute(params2[i].bound, substitution2); |
| pFresh.bound = bound2; |
| if (!_isNullabilityObliviousSubtypeMatch(bound2, bound1)) return false; |
| } |
| return true; |
| } |
| } |
| |
| class TypeSchemaConstraintGatherer extends TypeConstraintGatherer { |
| final TypeSchemaEnvironment environment; |
| |
| TypeSchemaConstraintGatherer(this.environment, |
| Iterable<TypeParameter> typeParameters, Library currentLibrary) |
| : super.subclassing(typeParameters, currentLibrary); |
| |
| @override |
| CoreTypes get coreTypes => environment.coreTypes; |
| |
| @override |
| void addUpperBound( |
| TypeConstraint constraint, DartType upper, Library clientLibrary) { |
| environment.addUpperBound(constraint, upper, clientLibrary); |
| } |
| |
| @override |
| void addLowerBound( |
| TypeConstraint constraint, DartType lower, Library clientLibrary) { |
| environment.addLowerBound(constraint, lower, clientLibrary); |
| } |
| |
| @override |
| Member? getInterfaceMember(Class class_, Name name, {bool setter: false}) { |
| return environment.hierarchy |
| .getInterfaceMember(class_, name, setter: setter); |
| } |
| |
| @override |
| InterfaceType? getTypeAsInstanceOf(InterfaceType type, Class superclass, |
| Library clientLibrary, CoreTypes coreTypes) { |
| return environment.getTypeAsInstanceOf( |
| type, superclass, clientLibrary, coreTypes); |
| } |
| |
| @override |
| List<DartType>? getTypeArgumentsAsInstanceOf( |
| InterfaceType type, Class superclass) { |
| return environment.getTypeArgumentsAsInstanceOf(type, superclass); |
| } |
| |
| @override |
| InterfaceType futureType(DartType type, Nullability nullability) { |
| return environment.futureType(type, nullability); |
| } |
| } |
| |
| /// Tracks a single constraint on a single type variable. |
| /// |
| /// This is called "_ProtoConstraint" to distinguish from [TypeConstraint], |
| /// which tracks the upper and lower bounds that are together implied by a set |
| /// of [_ProtoConstraint]s. |
| class _ProtoConstraint { |
| final TypeParameter parameter; |
| |
| final DartType bound; |
| |
| final bool isUpper; |
| |
| _ProtoConstraint.lower(this.parameter, this.bound) : isUpper = false; |
| |
| _ProtoConstraint.upper(this.parameter, this.bound) : isUpper = true; |
| |
| String toString() { |
| return isUpper |
| ? "${parameter.name} <: $bound" |
| : "$bound <: ${parameter.name}"; |
| } |
| } |