From be6ca13322ca456cfa1bd80ec464ed0908d9e3a7 Mon Sep 17 00:00:00 2001 From: Chloe Stefantsova Date: Wed, 13 Nov 2024 02:51:21 +0000 Subject: [PATCH] [analyzer][cfe] Share constraint generation for nullable types Part of https://github.com/dart-lang/sdk/issues/54902 Change-Id: I9ce094d28de679002c81b0ff95d94d433369afcf Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/393741 Commit-Queue: Chloe Stefantsova Reviewed-by: Paul Berry --- .../type_analyzer_operations.dart | 111 ++++++++++++++++++ .../type_constraint_gatherer_test.dart | 78 ++++++++++++ .../element/type_constraint_gatherer.dart | 68 +---------- .../type_constraint_gatherer.dart | 69 ++--------- 4 files changed, 204 insertions(+), 122 deletions(-) diff --git a/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer_operations.dart b/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer_operations.dart index 204e83c4ec34..d47f83167811 100644 --- a/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer_operations.dart +++ b/pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer_operations.dart @@ -1233,6 +1233,117 @@ abstract class TypeConstraintGenerator< } } + /// Matches [p] against [q] as a subtype against supertype. + /// + /// - If [p] is `p0?` for some `p0` and [p] is a subtype of [q] under some + /// constraints, the constraints making the relation possible are recorded, + /// and `true` is returned. + /// - Otherwise, the constraint state is unchanged (or rolled back using + /// [restoreState]), and `false` is returned. + /// + /// An invariant of the type inference is that only [p] or [q] may be a + /// schema (in other words, may contain the unknown type `_`); the other must + /// be simply a type. If [leftSchema] is `true`, [p] may contain `_`; if it is + /// `false`, [q] may contain `_`. + bool performSubtypeConstraintGenerationForLeftNullableType( + TypeStructure p, TypeStructure q, + {required bool leftSchema, required AstNode? astNodeForTesting}) { + // If `P` is `P0?` the match holds under constraint set `C1 + C2`: + NullabilitySuffix pNullability = p.nullabilitySuffix; + if (pNullability == NullabilitySuffix.question) { + TypeStructure p0 = typeAnalyzerOperations + .withNullabilitySuffix(new SharedTypeView(p), NullabilitySuffix.none) + .unwrapTypeView(); + final TypeConstraintGeneratorState state = currentState; + + // 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 (performSubtypeConstraintGenerationInternal(p0, q, + leftSchema: leftSchema, astNodeForTesting: astNodeForTesting) && + performSubtypeConstraintGenerationInternal( + typeAnalyzerOperations.nullType.unwrapTypeView(), q, + leftSchema: leftSchema, astNodeForTesting: astNodeForTesting)) { + return true; + } + + restoreState(state); + } + + return false; + } + + /// Matches [p] against [q] as a subtype against supertype. + /// + /// - If [q] is `q0?` for some `q0` and [p] is a subtype of [q] under some + /// constraints, the constraints making the relation possible are recorded, + /// and `true` is returned. + /// - Otherwise, the constraint state is unchanged (or rolled back using + /// [restoreState]), and `false` is returned. + /// + /// An invariant of the type inference is that only [p] or [q] may be a + /// schema (in other words, may contain the unknown type `_`); the other must + /// be simply a type. If [leftSchema] is `true`, [p] may contain `_`; if it is + /// `false`, [q] may contain `_`. + bool performSubtypeConstraintGenerationForRightNullableType( + TypeStructure p, TypeStructure q, + {required bool leftSchema, required AstNode? astNodeForTesting}) { + // If `Q` is `Q0?` the match holds under constraint set `C`: + NullabilitySuffix qNullability = q.nullabilitySuffix; + if (qNullability == NullabilitySuffix.question) { + TypeStructure q0 = typeAnalyzerOperations + .withNullabilitySuffix(new SharedTypeView(q), NullabilitySuffix.none) + .unwrapTypeView(); + final TypeConstraintGeneratorState state = currentState; + + // If `P` is `P0?` and `P0` is a subtype match for `Q0` under + // constraint set `C`. + NullabilitySuffix pNullability = p.nullabilitySuffix; + if (pNullability == NullabilitySuffix.question) { + TypeStructure p0 = typeAnalyzerOperations + .withNullabilitySuffix( + new SharedTypeView(p), NullabilitySuffix.none) + .unwrapTypeView(); + if (performSubtypeConstraintGenerationInternal(p0, q0, + leftSchema: leftSchema, astNodeForTesting: astNodeForTesting)) { + return true; + } + } + + // Or if `P` is `dynamic` or `void` and `Object` is a subtype match + // for `Q0` under constraint set `C`. + if (p is SharedDynamicTypeStructure || p is SharedVoidTypeStructure) { + if (performSubtypeConstraintGenerationInternal( + typeAnalyzerOperations.objectType.unwrapTypeView(), q0, + leftSchema: leftSchema, astNodeForTesting: astNodeForTesting)) { + return true; + } + } + + // Or if `P` is a subtype match for `Q0` under non-empty + // constraint set `C`. + bool pMatchesQ0 = performSubtypeConstraintGenerationInternal(p, q0, + leftSchema: leftSchema, astNodeForTesting: astNodeForTesting); + if (pMatchesQ0 && state != currentState) { + return true; + } + + // Or if `P` is a subtype match for `Null` under constraint set `C`. + if (performSubtypeConstraintGenerationInternal( + p, typeAnalyzerOperations.nullType.unwrapTypeView(), + leftSchema: leftSchema, astNodeForTesting: astNodeForTesting)) { + return true; + } + + // Or if `P` is a subtype match for `Q0` under empty + // constraint set `C`. + if (pMatchesQ0) { + return true; + } + } + + return false; + } + /// Implementation backing [performSubtypeConstraintGenerationLeftSchema] and /// [performSubtypeConstraintGenerationRightSchema]. /// diff --git a/pkg/_fe_analyzer_shared/test/type_inference/type_constraint_gatherer_test.dart b/pkg/_fe_analyzer_shared/test/type_inference/type_constraint_gatherer_test.dart index eb489fb2fa49..856f02b5ac40 100644 --- a/pkg/_fe_analyzer_shared/test/type_inference/type_constraint_gatherer_test.dart +++ b/pkg/_fe_analyzer_shared/test/type_inference/type_constraint_gatherer_test.dart @@ -517,6 +517,74 @@ main() { }); }); + group('performSubtypeConstraintGenerationForLeftNullableType:', () { + test('Nullable matches nullable with constraints based on base types', () { + // `T? <# int?` reduces to `T <# int?` + var tcg = _TypeConstraintGatherer({'T'}); + check(tcg.performSubtypeConstraintGenerationForLeftNullableType( + Type('T?'), Type('Null'), + leftSchema: false, astNodeForTesting: Node.placeholder())) + .isTrue(); + check(tcg._constraints).deepEquals(['T <: Null']); + }); + + test('Nullable does not match Nullable because base types fail to match', + () { + // `int? <# String?` reduces to `int <# String` + var tcg = _TypeConstraintGatherer({}); + check(tcg.performSubtypeConstraintGenerationForLeftNullableType( + Type('int?'), Type('String?'), + leftSchema: false, astNodeForTesting: Node.placeholder())) + .isFalse(); + check(tcg._constraints).isEmpty(); + }); + }); + + group('performSubtypeConstraintGenerationForRightNullableType:', () { + test('Null matches Nullable favoring non-Null branch', () { + // `Null <# T?` could match in two possible ways: + // - `Null <# Null` (taking the "Null" branch of the FutureOr), producing + // the empty constraint set. + // - `Null <# T` (taking the "non-Null" branch of the FutureOr), + // producing `Null <: T` + // In cases where both branches produce a constraint, the "non-Null" + // branch is favored. + var tcg = _TypeConstraintGatherer({'T'}); + check(tcg.performSubtypeConstraintGenerationForRightNullableType( + Type('Null'), Type('T?'), + leftSchema: false, astNodeForTesting: Node.placeholder())) + .isTrue(); + check(tcg._constraints).deepEquals(['Null <: T']); + }); + + test('Type matches Nullable favoring the non-Null branch', () { + // `T <# int?` could match in two possible ways: + // - `T <# Null` (taking the "Null" branch of the Nullable), + // producing `T <: Null` + // - `T <# int` (taking the "non-Null" branch of the Nullable), + // producing `T <: int` + // In cases where both branches produce a constraint, the "non-Null" + // branch is favored. + var tcg = _TypeConstraintGatherer({'T'}); + check(tcg.performSubtypeConstraintGenerationForRightNullableType( + Type('T'), Type('int?'), + leftSchema: false, astNodeForTesting: Node.placeholder())) + .isTrue(); + check(tcg._constraints).deepEquals(['T <: int']); + }); + + test('Null matches Nullable with no constraints', () { + // `Null <# int?` matches (taking the "Null" branch of + // the Nullable) without generating any constraints. + var tcg = _TypeConstraintGatherer({}); + check(tcg.performSubtypeConstraintGenerationForRightNullableType( + Type('Null'), Type('int?'), + leftSchema: false, astNodeForTesting: Node.placeholder())) + .isTrue(); + check(tcg._constraints).isEmpty(); + }); + }); + group('performSubtypeConstraintGenerationForTypeDeclarationTypes', () { group('Same base type on both sides:', () { test('Covariant, matching', () { @@ -768,6 +836,16 @@ class _TypeConstraintGatherer extends TypeConstraintGenerator` the match holds under constraint set `C1 + C2`: @@ -285,21 +241,9 @@ class TypeConstraintGatherer extends shared.TypeConstraintGenerator< } // If `P` is `P0?` the match holds under constraint set `C1 + C2`: - if (P_nullability == NullabilitySuffix.question) { - var P0 = _typeSystemOperations - .withNullabilitySuffix(SharedTypeView(P), NullabilitySuffix.none) - .unwrapTypeView(); - var rewind = _constraints.length; - - // 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 (trySubtypeMatch(P0, Q, leftSchema, nodeForTesting: nodeForTesting) && - trySubtypeMatch(_typeSystem.nullNone, Q, leftSchema, - nodeForTesting: nodeForTesting)) { - return true; - } - - _constraints.length = rewind; + if (performSubtypeConstraintGenerationForLeftNullableType(P, Q, + leftSchema: leftSchema, astNodeForTesting: nodeForTesting)) { + return true; } // If `Q` is `dynamic`, `Object?`, or `void` then the match holds under diff --git a/pkg/front_end/lib/src/type_inference/type_constraint_gatherer.dart b/pkg/front_end/lib/src/type_inference/type_constraint_gatherer.dart index d0239e5a5803..cc7f70166309 100644 --- a/pkg/front_end/lib/src/type_inference/type_constraint_gatherer.dart +++ b/pkg/front_end/lib/src/type_inference/type_constraint_gatherer.dart @@ -304,55 +304,17 @@ class TypeConstraintGatherer extends shared.TypeConstraintGenerator< // 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 (qNullability == NullabilitySuffix.question) { - final int baseConstraintCount = _protoConstraints.length; - final DartType rawP = typeOperations - .withNullabilitySuffix(new SharedTypeView(p), NullabilitySuffix.none) - .unwrapTypeView(); - final DartType rawQ = typeOperations - .withNullabilitySuffix(new SharedTypeView(q), NullabilitySuffix.none) - .unwrapTypeView(); - - if (pNullability == NullabilitySuffix.question && - _isNullabilityAwareSubtypeMatch(rawP, rawQ, - constrainSupertype: constrainSupertype, - treeNodeForTesting: treeNodeForTesting)) { - return true; - } - - if ((p is SharedDynamicTypeStructure || p is SharedVoidTypeStructure) && - _isNullabilityAwareSubtypeMatch( - typeOperations.objectType.unwrapTypeView(), rawQ, - constrainSupertype: constrainSupertype, - treeNodeForTesting: treeNodeForTesting)) { - return true; - } - - bool isMatchWithRawQ = _isNullabilityAwareSubtypeMatch(p, rawQ, - constrainSupertype: constrainSupertype, - treeNodeForTesting: treeNodeForTesting); - bool matchWithRawQAddsConstraints = - _protoConstraints.length != baseConstraintCount; - if (isMatchWithRawQ && matchWithRawQAddsConstraints) { - return true; - } - - if (_isNullabilityAwareSubtypeMatch( - p, typeOperations.nullType.unwrapTypeView(), - constrainSupertype: constrainSupertype, - treeNodeForTesting: treeNodeForTesting)) { - return true; - } - - if (isMatchWithRawQ && !matchWithRawQAddsConstraints) { - return true; - } + if (performSubtypeConstraintGenerationForRightNullableType(p, q, + leftSchema: constrainSupertype, + astNodeForTesting: treeNodeForTesting)) { + return true; } // If P is FutureOr the match holds under constraint set C1 + C2: // // If Future is a subtype match for Q under constraint set C1. // And if P0 is a subtype match for Q under constraint set C2. + if (typeOperations.matchFutureOrInternal(p) case DartType p0?) { final int baseConstraintCount = _protoConstraints.length; if (_isNullabilityAwareSubtypeMatch( @@ -372,23 +334,10 @@ class TypeConstraintGatherer extends shared.TypeConstraintGenerator< // // 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 (pNullability == NullabilitySuffix.question) { - final int baseConstraintCount = _protoConstraints.length; - if (_isNullabilityAwareSubtypeMatch( - typeOperations - .withNullabilitySuffix( - new SharedTypeView(p), NullabilitySuffix.none) - .unwrapTypeView(), - q, - constrainSupertype: constrainSupertype, - treeNodeForTesting: treeNodeForTesting) && - _isNullabilityAwareSubtypeMatch( - typeOperations.nullType.unwrapTypeView(), q, - constrainSupertype: constrainSupertype, - treeNodeForTesting: treeNodeForTesting)) { - return true; - } - _protoConstraints.length = baseConstraintCount; + if (performSubtypeConstraintGenerationForLeftNullableType(p, q, + leftSchema: constrainSupertype, + astNodeForTesting: treeNodeForTesting)) { + return true; } // If Q is dynamic, Object?, or void then the match holds under no