-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge 123e680 into dev
- Loading branch information
Showing
27 changed files
with
793 additions
and
476 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
307 changes: 307 additions & 0 deletions
307
pkg/_fe_analyzer_shared/lib/src/type_inference/type_constraint.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,307 @@ | ||
// Copyright (c) 2024, 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 'type_analyzer_operations.dart'; | ||
|
||
/// Tracks a single constraint on a single type parameter. | ||
/// | ||
/// We require that `typeParameter <: constraint` if `isUpper` is true, and | ||
/// `constraint <: typeParameter` otherwise. | ||
class GeneratedTypeConstraint<Type extends Object, TypeSchema extends Object, | ||
TypeParameter extends Object, Variable extends Object> { | ||
/// The type parameter that is constrained by [constraint]. | ||
final TypeParameter typeParameter; | ||
|
||
/// The type schema constraining the type parameter. | ||
final TypeSchema constraint; | ||
|
||
/// True if `typeParameter <: constraint`, and false otherwise. | ||
/// | ||
/// Note that we require that either `typeParameter <: constraint` or | ||
/// `constraint <: typeParameter`. | ||
final bool isUpper; | ||
|
||
GeneratedTypeConstraint.lower(this.typeParameter, this.constraint) | ||
: isUpper = false; | ||
|
||
GeneratedTypeConstraint.upper(this.typeParameter, this.constraint) | ||
: isUpper = true; | ||
|
||
@override | ||
String toString() { | ||
return isUpper ? "<type> <: ${constraint}" : "${constraint} <: <type>"; | ||
} | ||
} | ||
|
||
/// A constraint on a type parameter that we're inferring. | ||
class MergedTypeConstraint<Type extends Object, TypeSchema extends Object, | ||
TypeParameter extends Object, Variable extends Object> { | ||
/// The lower bound of the type being constrained. This bound must be a | ||
/// subtype of the type being constrained. In other words, lowerBound <: T. | ||
/// | ||
/// This kind of constraint cannot be expressed in Dart, but it applies when | ||
/// we're doing inference. For example, consider a signature like: | ||
/// | ||
/// T pickAtRandom<T>(T x, T y); | ||
/// | ||
/// and a call to it like: | ||
/// | ||
/// pickAtRandom(1, 2.0) | ||
/// | ||
/// when we see the first parameter is an `int`, we know that `int <: T`. | ||
/// When we see `double` this implies `double <: T`. | ||
/// Combining these constraints results in a lower bound of `num`. | ||
/// | ||
/// In the example above `num` is chosen as the greatest upper bound between | ||
/// `int` and `double`, so the resulting constraint is equal or stronger than | ||
/// either of the two. | ||
TypeSchema lower; | ||
|
||
/// The upper bound of the type being constrained. The type being constrained | ||
/// must be a subtype of this bound. In other words, T <: upperBound. | ||
/// | ||
/// In Dart this can be written as `<T extends UpperBoundType>`. | ||
/// | ||
/// In inference, this can happen as a result of parameters of function type. | ||
/// For example, consider a signature like: | ||
/// | ||
/// T reduce<T>(List<T> values, T f(T x, T y)); | ||
/// | ||
/// and a call to it like: | ||
/// | ||
/// reduce(values, (num x, num y) => ...); | ||
/// | ||
/// From the function expression's parameters, we conclude `T <: num`. We may | ||
/// still be able to conclude a different [lower] based on `values` or | ||
/// the type of the elided `=> ...` body. For example: | ||
/// | ||
/// reduce(['x'], (num x, num y) => 'hi'); | ||
/// | ||
/// Here the [lower] will be `String` and the upper bound will be `num`, | ||
/// which cannot be satisfied, so this is ill typed. | ||
TypeSchema upper; | ||
|
||
/// Where this constraint comes from, used for error messages. | ||
TypeConstraintOrigin<Type, TypeSchema, Variable> origin; | ||
|
||
MergedTypeConstraint( | ||
{required this.lower, required this.upper, required this.origin}); | ||
|
||
MergedTypeConstraint.fromExtends( | ||
{required String typeParameterName, | ||
required Type boundType, | ||
required Type extendsType, | ||
required TypeAnalyzerOperations<Variable, Type, TypeSchema> | ||
typeAnalyzerOperations}) | ||
: this( | ||
origin: new TypeConstraintFromExtendsClause( | ||
typeParameterName: typeParameterName, | ||
boundType: boundType, | ||
extendsType: extendsType, | ||
), | ||
upper: typeAnalyzerOperations.typeToSchema(extendsType), | ||
lower: typeAnalyzerOperations.unknownType); | ||
|
||
MergedTypeConstraint<Type, TypeSchema, TypeParameter, Variable> clone() { | ||
return new MergedTypeConstraint(lower: lower, upper: upper, origin: origin); | ||
} | ||
|
||
bool isEmpty( | ||
TypeAnalyzerOperations<Variable, Type, TypeSchema> | ||
typeAnalyzerOperations) { | ||
return typeAnalyzerOperations.isUnknownType(lower) && | ||
typeAnalyzerOperations.isUnknownType(upper); | ||
} | ||
|
||
bool isSatisfiedBy( | ||
Type type, | ||
TypeAnalyzerOperations<Variable, Type, TypeSchema> | ||
typeAnalyzerOperations) { | ||
return typeAnalyzerOperations.typeIsSubtypeOfTypeSchema(type, upper) && | ||
typeAnalyzerOperations.typeSchemaIsSubtypeOfType(lower, type); | ||
} | ||
|
||
void mergeIn( | ||
GeneratedTypeConstraint<Type, TypeSchema, TypeParameter, Variable> | ||
generatedTypeConstraint, | ||
TypeAnalyzerOperations<Variable, Type, TypeSchema> | ||
typeAnalyzerOperations) { | ||
if (generatedTypeConstraint.isUpper) { | ||
mergeInTypeSchemaUpper( | ||
generatedTypeConstraint.constraint, typeAnalyzerOperations); | ||
} else { | ||
mergeInTypeSchemaLower( | ||
generatedTypeConstraint.constraint, typeAnalyzerOperations); | ||
} | ||
} | ||
|
||
void mergeInTypeSchemaUpper( | ||
TypeSchema constraint, | ||
TypeAnalyzerOperations<Variable, Type, TypeSchema> | ||
typeAnalyzerOperations) { | ||
upper = typeAnalyzerOperations.typeSchemaGlb(upper, constraint); | ||
} | ||
|
||
void mergeInTypeSchemaLower( | ||
TypeSchema constraint, | ||
TypeAnalyzerOperations<Variable, Type, TypeSchema> | ||
typeAnalyzerOperations) { | ||
lower = typeAnalyzerOperations.typeSchemaLub(lower, constraint); | ||
} | ||
|
||
@override | ||
String toString() { | ||
return '${lower} <: <type> <: ${upper}'; | ||
} | ||
} | ||
|
||
/// The origin of a type constraint, for the purposes of producing a human | ||
/// readable error message during type inference as well as determining whether | ||
/// the constraint was used to fix the type parameter or not. | ||
abstract class TypeConstraintOrigin<Type extends Object, | ||
TypeSchema extends Object, Variable extends Object> { | ||
const TypeConstraintOrigin(); | ||
|
||
List<String> formatError( | ||
TypeAnalyzerOperations<Variable, Type, TypeSchema> | ||
typeAnalyzerOperations); | ||
} | ||
|
||
class UnknownTypeConstraintOrigin<Type extends Object, | ||
TypeSchema extends Object, Variable extends Object> | ||
extends TypeConstraintOrigin<Type, TypeSchema, Variable> { | ||
const UnknownTypeConstraintOrigin(); | ||
|
||
@override | ||
List<String> formatError( | ||
TypeAnalyzerOperations<Variable, Type, TypeSchema> | ||
typeAnalyzerOperations) { | ||
return <String>[]; | ||
} | ||
} | ||
|
||
class TypeConstraintFromArgument<Type extends Object, TypeSchema extends Object, | ||
Variable extends Object> | ||
extends TypeConstraintOrigin<Type, TypeSchema, Variable> { | ||
final Type argumentType; | ||
final Type parameterType; | ||
final String parameterName; | ||
final String? genericClassName; | ||
final bool isGenericClassInDartCore; | ||
|
||
TypeConstraintFromArgument( | ||
{required this.argumentType, | ||
required this.parameterType, | ||
required this.parameterName, | ||
required this.genericClassName, | ||
this.isGenericClassInDartCore = false}); | ||
|
||
@override | ||
List<String> formatError( | ||
TypeAnalyzerOperations<Variable, Type, TypeSchema> | ||
typeAnalyzerOperations) { | ||
// TODO(cstefantsova): we should highlight the span. That would be more | ||
// useful. However in summary code it doesn't look like the AST node with | ||
// span is available. | ||
String prefix; | ||
if ((genericClassName == "List" || genericClassName == "Map") && | ||
isGenericClassInDartCore) { | ||
// This will become: | ||
// "List element" | ||
// "Map key" | ||
// "Map value" | ||
prefix = "${genericClassName} $parameterName"; | ||
} else { | ||
prefix = "Parameter '$parameterName'"; | ||
} | ||
|
||
return [ | ||
prefix, | ||
"declared as " | ||
"'${typeAnalyzerOperations.getDisplayString(parameterType)}'", | ||
"but argument is " | ||
"'${typeAnalyzerOperations.getDisplayString(argumentType)}'." | ||
]; | ||
} | ||
} | ||
|
||
class TypeConstraintFromExtendsClause<Type extends Object, | ||
TypeSchema extends Object, Variable extends Object> | ||
extends TypeConstraintOrigin<Type, TypeSchema, Variable> { | ||
/// Name of the type parameter with the extends clause. | ||
final String typeParameterName; | ||
|
||
/// The declared bound of [typeParam], not `null`, because we create | ||
/// this clause only when it is not `null`. | ||
/// | ||
/// For example `Iterable<T>` for `<T, E extends Iterable<T>>`. | ||
final Type boundType; | ||
|
||
/// [boundType] in which type parameters are substituted with inferred | ||
/// type arguments. | ||
/// | ||
/// For example `Iterable<int>` if `T` inferred to `int`. | ||
final Type extendsType; | ||
|
||
TypeConstraintFromExtendsClause( | ||
{required this.typeParameterName, | ||
required this.boundType, | ||
required this.extendsType}); | ||
|
||
@override | ||
List<String> formatError( | ||
TypeAnalyzerOperations<Variable, Type, TypeSchema> | ||
typeAnalyzerOperations) { | ||
String boundStr = typeAnalyzerOperations.getDisplayString(boundType); | ||
String extendsStr = typeAnalyzerOperations.getDisplayString(extendsType); | ||
return [ | ||
"Type parameter '${typeParameterName}'", | ||
"is declared to extend '${boundStr}' producing '${extendsStr}'." | ||
]; | ||
} | ||
} | ||
|
||
class TypeConstraintFromFunctionContext<Type extends Object, | ||
TypeSchema extends Object, Variable extends Object> | ||
extends TypeConstraintOrigin<Type, TypeSchema, Variable> { | ||
final Type contextType; | ||
final Type functionType; | ||
|
||
TypeConstraintFromFunctionContext( | ||
{required this.functionType, required this.contextType}); | ||
|
||
@override | ||
List<String> formatError( | ||
TypeAnalyzerOperations<Variable, Type, TypeSchema> | ||
typeAnalyzerOperations) { | ||
return [ | ||
"Function type", | ||
"declared as '${typeAnalyzerOperations.getDisplayString(functionType)}'", | ||
"used where '${typeAnalyzerOperations.getDisplayString(contextType)}' " | ||
"is required." | ||
]; | ||
} | ||
} | ||
|
||
class TypeConstraintFromReturnType<Type extends Object, | ||
TypeSchema extends Object, Variable extends Object> | ||
extends TypeConstraintOrigin<Type, TypeSchema, Variable> { | ||
final Type contextType; | ||
final Type declaredType; | ||
|
||
TypeConstraintFromReturnType( | ||
{required this.declaredType, required this.contextType}); | ||
|
||
@override | ||
List<String> formatError( | ||
TypeAnalyzerOperations<Variable, Type, TypeSchema> | ||
typeAnalyzerOperations) { | ||
return [ | ||
"Return type", | ||
"declared as '${typeAnalyzerOperations.getDisplayString(declaredType)}'", | ||
"used where '${typeAnalyzerOperations.getDisplayString(contextType)}' " | ||
"is required." | ||
]; | ||
} | ||
} |
Oops, something went wrong.