Skip to content

Commit

Permalink
[analyzer][cfe] Share constraint generation for nullable types
Browse files Browse the repository at this point in the history
Part of #54902

Change-Id: I9ce094d28de679002c81b0ff95d94d433369afcf
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/393741
Commit-Queue: Chloe Stefantsova <[email protected]>
Reviewed-by: Paul Berry <[email protected]>
  • Loading branch information
chloestefantsova authored and Commit Queue committed Nov 13, 2024
1 parent 7784976 commit be6ca13
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 122 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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].
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () {
Expand Down Expand Up @@ -768,6 +836,16 @@ class _TypeConstraintGatherer extends TypeConstraintGenerator<Type,
return true;
}

if (performSubtypeConstraintGenerationForRightNullableType(p, q,
leftSchema: leftSchema, astNodeForTesting: astNodeForTesting)) {
return true;
}

if (performSubtypeConstraintGenerationForLeftNullableType(p, q,
leftSchema: leftSchema, astNodeForTesting: astNodeForTesting)) {
return true;
}

bool? result = performSubtypeConstraintGenerationForTypeDeclarationTypes(
p, q,
leftSchema: leftSchema, astNodeForTesting: astNodeForTesting);
Expand Down
68 changes: 6 additions & 62 deletions pkg/analyzer/lib/src/dart/element/type_constraint_gatherer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -218,53 +218,9 @@ class TypeConstraintGatherer extends shared.TypeConstraintGenerator<
return true;
}

// If `Q` is `Q0?` the match holds under constraint set `C`:
if (Q_nullability == NullabilitySuffix.question) {
var Q0 = _typeSystemOperations
.withNullabilitySuffix(SharedTypeView(Q), NullabilitySuffix.none)
.unwrapTypeView();
var rewind = _constraints.length;

// If `P` is `P0?` and `P0` is a subtype match for `Q0` under
// constraint set `C`.
if (P_nullability == NullabilitySuffix.question) {
var P0 = _typeSystemOperations
.withNullabilitySuffix(SharedTypeView(P), NullabilitySuffix.none)
.unwrapTypeView();
if (trySubtypeMatch(P0, Q0, leftSchema,
nodeForTesting: nodeForTesting)) {
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 (trySubtypeMatch(_typeSystem.objectNone, Q0, leftSchema,
nodeForTesting: nodeForTesting)) {
return true;
}
}

// Or if `P` is a subtype match for `Q0` under non-empty
// constraint set `C`.
var P_matches_Q0 =
trySubtypeMatch(P, Q0, leftSchema, nodeForTesting: nodeForTesting);
if (P_matches_Q0 && _constraints.length != rewind) {
return true;
}

// Or if `P` is a subtype match for `Null` under constraint set `C`.
if (trySubtypeMatch(P, _typeSystem.nullNone, leftSchema,
nodeForTesting: nodeForTesting)) {
return true;
}

// Or if `P` is a subtype match for `Q0` under empty
// constraint set `C`.
if (P_matches_Q0) {
return true;
}
if (performSubtypeConstraintGenerationForRightNullableType(P, Q,
leftSchema: leftSchema, astNodeForTesting: nodeForTesting)) {
return true;
}

// If `P` is `FutureOr<P0>` the match holds under constraint set `C1 + C2`:
Expand All @@ -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
Expand Down
69 changes: 9 additions & 60 deletions pkg/front_end/lib/src/type_inference/type_constraint_gatherer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<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 (typeOperations.matchFutureOrInternal(p) case DartType p0?) {
final int baseConstraintCount = _protoConstraints.length;
if (_isNullabilityAwareSubtypeMatch(
Expand All @@ -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
Expand Down

0 comments on commit be6ca13

Please sign in to comment.