Front end: initial implementation of type inference for object patterns.

This code handles simple cases.  Still TBD:
- Object patterns that refer to typedefs
- Object patterns that refer to classes with f-bounded polymorphism
- Inference of `dynamic` when there is no bound

Bug: https://github.com/dart-lang/sdk/issues/51795
Change-Id: I00acae6ba111f7b170650cfeffbfd2aaf7f71e42
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/290347
Reviewed-by: Chloe Stefantsova <cstefantsova@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
This commit is contained in:
Paul Berry 2023-03-22 17:19:23 +00:00 committed by Commit Queue
parent 4caae04fc8
commit 6b4878b55f
15 changed files with 197 additions and 22 deletions

View file

@ -9170,10 +9170,13 @@ class BodyBuilder extends StackListenerImpl
push(typeArguments ?? NullValues.TypeArguments);
handleType(firstIdentifier, null);
TypeBuilder typeBuilder = pop() as TypeBuilder;
TypeDeclarationBuilder? typeDeclaration = typeBuilder.declaration;
DartType type = buildDartType(typeBuilder, TypeUse.objectPatternType,
allowPotentiallyConstantType: true);
push(forest.createObjectPattern(
firstIdentifier.charOffset, type, fields ?? <NamedPattern>[]));
push(new ObjectPatternInternal(type, fields ?? <NamedPattern>[],
typeDeclaration is TypeAliasBuilder ? typeDeclaration.typedef : null,
hasExplicitTypeArguments: typeArguments != null)
..fileOffset = firstIdentifier.charOffset);
}
@override

View file

@ -1038,11 +1038,6 @@ class Forest {
return new NullCheckPattern(pattern)..fileOffset = fileOffset;
}
ObjectPattern createObjectPattern(
int fileOffset, DartType type, List<NamedPattern> fields) {
return new ObjectPattern(type, fields)..fileOffset = fileOffset;
}
OrPattern createOrPattern(int fileOffset, Pattern left, Pattern right,
{required List<VariableDeclaration> orPatternJointVariables}) {
return new OrPattern(left, right,

View file

@ -3682,3 +3682,19 @@ class PatternForMapEntry extends TreeNode
return "PatternForMapEntry(${toStringInternal()})";
}
}
/// Data structure used by the body builder in place of [ObjectPattern], to
/// allow additional information to be captured that is needed during type
/// inference.
class ObjectPatternInternal extends ObjectPattern {
/// If the type name in the object pattern refers to a typedef, the typedef in
/// question; otherwise `null`.
final Typedef? typedef;
/// Indicates whether the object pattern included explicit type arguments; if
/// `true` this means that no further type inference needs to be performed.
final bool hasExplicitTypeArguments;
ObjectPatternInternal(super.requiredType, super.fields, this.typedef,
{required this.hasExplicitTypeArguments});
}

View file

@ -10251,6 +10251,8 @@ class InferenceVisitorImpl extends InferenceVisitorBase
ValueKinds.Pattern, node.fields.length)
]));
node.requiredType = analysisResult.requiredType;
Pattern? replacement;
InvalidExpression? error =
@ -10873,15 +10875,65 @@ class InferenceVisitorImpl extends InferenceVisitorBase
return new RecordType(positional, namedFields, Nullability.nonNullable);
}
/// Infers type arguments corresponding to [typeParameters] so that, when
/// substituted into [declaredType], the resulting type matches [contextType].
List<DartType> _inferTypeArguments(
{required List<TypeParameter> typeParameters,
required DartType declaredType,
required DartType contextType}) {
TypeConstraintGatherer gatherer = typeSchemaEnvironment
.setupGenericTypeInference(declaredType, typeParameters, contextType,
isNonNullableByDefault: isNonNullableByDefault);
List<DartType> inferredTypes = typeSchemaEnvironment.partialInfer(
gatherer, typeParameters, null,
isNonNullableByDefault: isNonNullableByDefault);
// Type inference may not be able to infer a type for every type parameter;
// if it can't fall back on the type parameter's bound.
// TODO(paulberry): this doesn't work if the type is F-bounded because in
// that case, the bound may refer to other type variables.
for (int i = 0; i < inferredTypes.length; i++) {
if (inferredTypes[i] is UnknownType) {
DartType bound = typeParameters[i].bound;
inferredTypes[i] = bound;
}
}
return inferredTypes;
}
@override
DartType downwardInferObjectPatternRequiredType({
required DartType matchedType,
required Pattern pattern,
required covariant ObjectPatternInternal pattern,
}) {
if (pattern is! ObjectPattern) return const InvalidType();
// TODO(johnniwinther): Update this when language issue #2770 has been
// resolved.
return pattern.requiredType;
DartType requiredType = pattern.requiredType;
if (!pattern.hasExplicitTypeArguments) {
if (pattern.typedef != null) {
// TODO(paulberry): handle typedefs properly.
}
if (requiredType is InterfaceType &&
requiredType.classNode.typeParameters.isNotEmpty) {
List<TypeParameter> typeParameters =
requiredType.classNode.typeParameters;
// It's possible that one of the callee type parameters might match a
// type that already exists as part of inference. This might happen,
// for instance, in the case where a method in a generic class contains
// an object pattern naming the enclosing class. To avoid creating
// invalid inference results, we need to create fresh type parameters.
FreshTypeParameters fresh = getFreshTypeParameters(typeParameters);
InterfaceType declaredType = new InterfaceType(requiredType.classNode,
requiredType.declaredNullability, fresh.freshTypeArguments);
typeParameters = fresh.freshTypeParameters;
List<DartType> inferredTypeArguments = _inferTypeArguments(
typeParameters: typeParameters,
declaredType: declaredType,
contextType: matchedType);
requiredType = new InterfaceType(requiredType.classNode,
requiredType.declaredNullability, inferredTypeArguments);
}
}
return requiredType;
}
@override

View file

@ -64,7 +64,7 @@ static method test(dynamic value, core::bool expected) → void {
}
{
final synthesized dynamic #4#0 = value;
if(#4#0 is{ForNonNullableByDefault} self::Const<dynamic>) {
if(#4#0 is{ForNonNullableByDefault} self::Const<core::Object?>) {
matched = true;
}
}

View file

@ -64,7 +64,7 @@ static method test(dynamic value, core::bool expected) → void {
}
{
final synthesized dynamic #4#0 = value;
if(#4#0 is{ForNonNullableByDefault} self::Const<dynamic>) {
if(#4#0 is{ForNonNullableByDefault} self::Const<core::Object?>) {
matched = true;
}
}

View file

@ -64,7 +64,7 @@ static method test(dynamic value, core::bool expected) → void {
}
{
final synthesized dynamic #4#0 = value;
if(#4#0 is{ForNonNullableByDefault} self::Const<dynamic>) {
if(#4#0 is{ForNonNullableByDefault} self::Const<core::Object?>) {
matched = true;
}
}

View file

@ -64,7 +64,7 @@ static method test(dynamic value, core::bool expected) → void {
}
{
final synthesized dynamic #4#0 = value;
if(#4#0 is{ForNonNullableByDefault} self::Const<dynamic>) {
if(#4#0 is{ForNonNullableByDefault} self::Const<core::Object?>) {
matched = true;
}
}

View file

@ -64,7 +64,7 @@ static method test(dynamic value, core::bool expected) → void {
}
{
final synthesized dynamic #4#0 = value;
if(#4#0 is{ForNonNullableByDefault} self::Const<dynamic>) {
if(#4#0 is{ForNonNullableByDefault} self::Const<core::Object?>) {
matched = true;
}
}

View file

@ -82,7 +82,7 @@ static method test(dynamic value, core::bool expected) → void {
}
}
{
if(#0#0 is{ForNonNullableByDefault} self::Const<dynamic>) {
if(#0#0 is{ForNonNullableByDefault} self::Const<core::Object?>) {
{
matched = true;
break #L1;

View file

@ -82,7 +82,7 @@ static method test(dynamic value, core::bool expected) → void {
}
}
{
if(#0#0 is{ForNonNullableByDefault} self::Const<dynamic>) {
if(#0#0 is{ForNonNullableByDefault} self::Const<core::Object?>) {
{
matched = true;
break #L1;

View file

@ -82,7 +82,7 @@ static method test(dynamic value, core::bool expected) → void {
}
}
{
if(#0#0 is{ForNonNullableByDefault} self::Const<dynamic>) {
if(#0#0 is{ForNonNullableByDefault} self::Const<core::Object?>) {
{
matched = true;
break #L1;

View file

@ -82,7 +82,7 @@ static method test(dynamic value, core::bool expected) → void {
}
}
{
if(#0#0 is{ForNonNullableByDefault} self::Const<dynamic>) {
if(#0#0 is{ForNonNullableByDefault} self::Const<core::Object?>) {
{
matched = true;
break #L1;

View file

@ -82,7 +82,7 @@ static method test(dynamic value, core::bool expected) → void {
}
}
{
if(#0#0 is{ForNonNullableByDefault} self::Const<dynamic>) {
if(#0#0 is{ForNonNullableByDefault} self::Const<core::Object?>) {
{
matched = true;
break #L1;

View file

@ -0,0 +1,109 @@
// Copyright (c) 2023, 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.
// SharedOptions=--enable-experiment=patterns,records
// Test that the type arguments of object patterns are properly inferred.
import "package:expect/expect.dart";
import "../static_type_helper.dart";
sealed class B<T> {}
class C<T> extends B<T> {
final T t;
// Note: we use this getter to obtain values for `expectStaticType` (rather
// than a getter returning simply `T`) to ensure that an error is reported if
// `T` gets inferred to be `dynamic`.
List<T> get listOfT => [t];
C(this.t);
}
bool explicitTypeArguments(B<int> b) {
switch (b) {
case C<num>(listOfT: var x):
x.expectStaticType<Exactly<List<num>>>();
// Since C<num> isn't a subtype of B<int>, `b` is not promoted.
b.expectStaticType<Exactly<B<int>>>();
return true;
}
// No need for a `return` since the switch is exhaustive.
}
bool simpleInference(B<num> b) {
switch (b) {
case C(listOfT: var x):
x.expectStaticType<Exactly<List<num>>>();
b.expectStaticType<Exactly<C<num>>>();
return true;
}
// No need for a `return` since the switch is exhaustive.
}
void inferDynamic(Object o) {
switch (o) {
case C(listOfT: var x, t: var t):
x.expectStaticType<Exactly<List<dynamic>>>();
// TODO(paulberry): make the following like work properly
// t.foo(); // Should be ok since T is `dynamic`.
o.expectStaticType<Exactly<C<dynamic>>>();
default:
Expect.fail('Match failure');
}
}
class D<T extends num> {
final T t;
// Note: we use this getter to obtain values for `expectStaticType` (rather
// than a getter returning simply `T`) to ensure that an error is reported if
// `T` gets inferred to be `dynamic`.
List<T> get listOfT => [t];
D(this.t);
}
void inferBound(Object o) {
switch (o) {
case D(listOfT: var x):
x.expectStaticType<Exactly<List<num>>>();
o.expectStaticType<Exactly<D<num>>>();
default:
Expect.fail('Match failure');
}
}
class E<T, U> {
final T t;
final U u;
List<T> get listOfT => [t];
List<U> get listOfU => [u];
E(this.t, this.u);
bool inferEnclosingTypeParameters(E<Set<U>, Set<T>> e) {
// This test verifies that the inference logic properly distinguishes
// between the type parameters of the enclosing class and those that are
// part of the type of `e`.
if (e case E(listOfT: var x, listOfU: var y)) {
x.expectStaticType<Exactly<List<Set<U>>>>();
y.expectStaticType<Exactly<List<Set<T>>>>();
return true;
}
// No need for a `return` since the case fully covers the type of `e`.
}
}
main() {
explicitTypeArguments(C(0));
simpleInference(C(0));
inferDynamic(C(0));
inferBound(D(0));
E<int, String>(0, '')
.inferEnclosingTypeParameters(E<Set<String>, Set<int>>({''}, {0}));
}