Skip to content

Commit

Permalink
Version 3.4.0-162.0.dev
Browse files Browse the repository at this point in the history
Merge 123e680 into dev
  • Loading branch information
Dart CI committed Feb 21, 2024
2 parents e1d1e8b + 123e680 commit 3947494
Show file tree
Hide file tree
Showing 27 changed files with 793 additions and 476 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ abstract interface class TypeAnalyzerOperations<Variable extends Object,
/// If [type] is a record type, returns it.
RecordType<Type>? asRecordType(Type type);

/// Return the presentation of this type as it should appear when presented
/// to users in contexts such as error messages.
///
/// Clients should not depend on the content of the returned value as it will
/// be changed if doing so would improve the UX.
String getDisplayString(Type type);

/// Computes the greatest lower bound of [type1] and [type2].
Type glb(Type type1, Type type2);

Expand All @@ -64,8 +71,8 @@ abstract interface class TypeAnalyzerOperations<Variable extends Object,
bool isTypeSchemaSatisfied(
{required TypeSchema typeSchema, required Type type});

/// Returns `true` if [type] is the unknown type context (`_`).
bool isUnknownType(Type type);
/// Returns `true` if [typeSchema] is the unknown type context (`_`).
bool isUnknownType(TypeSchema typeSchema);

/// Returns whether [node] is final.
bool isVariableFinal(Variable node);
Expand Down Expand Up @@ -138,13 +145,47 @@ abstract interface class TypeAnalyzerOperations<Variable extends Object,
/// Returns the type schema `Stream`, with type argument [elementTypeSchema].
TypeSchema streamTypeSchema(TypeSchema elementTypeSchema);

/// Returns `true` if [leftType] is a subtype of the greatest closure of
/// [rightSchema].
///
/// This method can be implemented directly, by computing the greatest
/// closure of [rightSchema] and then comparing the resulting type and
/// [leftType] via [isSubtypeOf]. However, that would mean at least two
/// recursive descends over types. This method is supposed to have optimized
/// implementations that only use one recursive descend.
bool typeIsSubtypeOfTypeSchema(Type leftType, TypeSchema rightSchema);

/// Computes the greatest lower bound of [typeSchema1] and [typeSchema2].
TypeSchema typeSchemaGlb(TypeSchema typeSchema1, TypeSchema typeSchema2);

/// Determines whether the given type schema corresponds to the `dynamic`
/// type.
bool typeSchemaIsDynamic(TypeSchema typeSchema);

/// Returns `true` if least closure of [leftSchema] is a subtype of
/// the greatest closure of [rightSchema].
///
/// This method can be implemented directly, by computing the least closure of
/// [leftSchema], the greatest closure of [rightSchema], and then comparing
/// the resulting types via [isSubtypeOf]. However, that would mean at least
/// three recursive descends over types. This method is supposed to have
/// optimized implementations that only use one recursive descend.
bool typeSchemaIsSubtypeOfTypeSchema(
TypeSchema leftSchema, TypeSchema rightSchema);

/// Returns `true` if the least closure of [leftSchema] is a subtype of
/// [rightType].
///
/// This method can be implemented directly, by computing the least closure
/// of [leftSchema] and then comparing the resulting type and [rightType] via
/// [isSubtypeOf]. However, that would mean at least two recursive descends
/// over types. This method is supposed to have optimized implementations
/// that only use one recursive descend.
bool typeSchemaIsSubtypeOfType(TypeSchema leftSchema, Type rightType);

/// Computes the least upper bound of [typeSchema1] and [typeSchema2].
TypeSchema typeSchemaLub(TypeSchema typeSchema1, TypeSchema typeSchema2);

/// Converts a type into a corresponding type schema.
TypeSchema typeToSchema(Type type);
}
307 changes: 307 additions & 0 deletions pkg/_fe_analyzer_shared/lib/src/type_inference/type_constraint.dart
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."
];
}
}
Loading

0 comments on commit 3947494

Please sign in to comment.