Skip to content

Commit be6ca13

Browse files
chloestefantsovaCommit Queue
authored andcommitted
[analyzer][cfe] Share constraint generation for nullable types
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]>
1 parent 7784976 commit be6ca13

File tree

4 files changed

+204
-122
lines changed

4 files changed

+204
-122
lines changed

pkg/_fe_analyzer_shared/lib/src/type_inference/type_analyzer_operations.dart

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1233,6 +1233,117 @@ abstract class TypeConstraintGenerator<
12331233
}
12341234
}
12351235

1236+
/// Matches [p] against [q] as a subtype against supertype.
1237+
///
1238+
/// - If [p] is `p0?` for some `p0` and [p] is a subtype of [q] under some
1239+
/// constraints, the constraints making the relation possible are recorded,
1240+
/// and `true` is returned.
1241+
/// - Otherwise, the constraint state is unchanged (or rolled back using
1242+
/// [restoreState]), and `false` is returned.
1243+
///
1244+
/// An invariant of the type inference is that only [p] or [q] may be a
1245+
/// schema (in other words, may contain the unknown type `_`); the other must
1246+
/// be simply a type. If [leftSchema] is `true`, [p] may contain `_`; if it is
1247+
/// `false`, [q] may contain `_`.
1248+
bool performSubtypeConstraintGenerationForLeftNullableType(
1249+
TypeStructure p, TypeStructure q,
1250+
{required bool leftSchema, required AstNode? astNodeForTesting}) {
1251+
// If `P` is `P0?` the match holds under constraint set `C1 + C2`:
1252+
NullabilitySuffix pNullability = p.nullabilitySuffix;
1253+
if (pNullability == NullabilitySuffix.question) {
1254+
TypeStructure p0 = typeAnalyzerOperations
1255+
.withNullabilitySuffix(new SharedTypeView(p), NullabilitySuffix.none)
1256+
.unwrapTypeView();
1257+
final TypeConstraintGeneratorState state = currentState;
1258+
1259+
// If `P0` is a subtype match for `Q` under constraint set `C1`.
1260+
// And if `Null` is a subtype match for `Q` under constraint set `C2`.
1261+
if (performSubtypeConstraintGenerationInternal(p0, q,
1262+
leftSchema: leftSchema, astNodeForTesting: astNodeForTesting) &&
1263+
performSubtypeConstraintGenerationInternal(
1264+
typeAnalyzerOperations.nullType.unwrapTypeView(), q,
1265+
leftSchema: leftSchema, astNodeForTesting: astNodeForTesting)) {
1266+
return true;
1267+
}
1268+
1269+
restoreState(state);
1270+
}
1271+
1272+
return false;
1273+
}
1274+
1275+
/// Matches [p] against [q] as a subtype against supertype.
1276+
///
1277+
/// - If [q] is `q0?` for some `q0` and [p] is a subtype of [q] under some
1278+
/// constraints, the constraints making the relation possible are recorded,
1279+
/// and `true` is returned.
1280+
/// - Otherwise, the constraint state is unchanged (or rolled back using
1281+
/// [restoreState]), and `false` is returned.
1282+
///
1283+
/// An invariant of the type inference is that only [p] or [q] may be a
1284+
/// schema (in other words, may contain the unknown type `_`); the other must
1285+
/// be simply a type. If [leftSchema] is `true`, [p] may contain `_`; if it is
1286+
/// `false`, [q] may contain `_`.
1287+
bool performSubtypeConstraintGenerationForRightNullableType(
1288+
TypeStructure p, TypeStructure q,
1289+
{required bool leftSchema, required AstNode? astNodeForTesting}) {
1290+
// If `Q` is `Q0?` the match holds under constraint set `C`:
1291+
NullabilitySuffix qNullability = q.nullabilitySuffix;
1292+
if (qNullability == NullabilitySuffix.question) {
1293+
TypeStructure q0 = typeAnalyzerOperations
1294+
.withNullabilitySuffix(new SharedTypeView(q), NullabilitySuffix.none)
1295+
.unwrapTypeView();
1296+
final TypeConstraintGeneratorState state = currentState;
1297+
1298+
// If `P` is `P0?` and `P0` is a subtype match for `Q0` under
1299+
// constraint set `C`.
1300+
NullabilitySuffix pNullability = p.nullabilitySuffix;
1301+
if (pNullability == NullabilitySuffix.question) {
1302+
TypeStructure p0 = typeAnalyzerOperations
1303+
.withNullabilitySuffix(
1304+
new SharedTypeView(p), NullabilitySuffix.none)
1305+
.unwrapTypeView();
1306+
if (performSubtypeConstraintGenerationInternal(p0, q0,
1307+
leftSchema: leftSchema, astNodeForTesting: astNodeForTesting)) {
1308+
return true;
1309+
}
1310+
}
1311+
1312+
// Or if `P` is `dynamic` or `void` and `Object` is a subtype match
1313+
// for `Q0` under constraint set `C`.
1314+
if (p is SharedDynamicTypeStructure || p is SharedVoidTypeStructure) {
1315+
if (performSubtypeConstraintGenerationInternal(
1316+
typeAnalyzerOperations.objectType.unwrapTypeView(), q0,
1317+
leftSchema: leftSchema, astNodeForTesting: astNodeForTesting)) {
1318+
return true;
1319+
}
1320+
}
1321+
1322+
// Or if `P` is a subtype match for `Q0` under non-empty
1323+
// constraint set `C`.
1324+
bool pMatchesQ0 = performSubtypeConstraintGenerationInternal(p, q0,
1325+
leftSchema: leftSchema, astNodeForTesting: astNodeForTesting);
1326+
if (pMatchesQ0 && state != currentState) {
1327+
return true;
1328+
}
1329+
1330+
// Or if `P` is a subtype match for `Null` under constraint set `C`.
1331+
if (performSubtypeConstraintGenerationInternal(
1332+
p, typeAnalyzerOperations.nullType.unwrapTypeView(),
1333+
leftSchema: leftSchema, astNodeForTesting: astNodeForTesting)) {
1334+
return true;
1335+
}
1336+
1337+
// Or if `P` is a subtype match for `Q0` under empty
1338+
// constraint set `C`.
1339+
if (pMatchesQ0) {
1340+
return true;
1341+
}
1342+
}
1343+
1344+
return false;
1345+
}
1346+
12361347
/// Implementation backing [performSubtypeConstraintGenerationLeftSchema] and
12371348
/// [performSubtypeConstraintGenerationRightSchema].
12381349
///

pkg/_fe_analyzer_shared/test/type_inference/type_constraint_gatherer_test.dart

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,74 @@ main() {
517517
});
518518
});
519519

520+
group('performSubtypeConstraintGenerationForLeftNullableType:', () {
521+
test('Nullable matches nullable with constraints based on base types', () {
522+
// `T? <# int?` reduces to `T <# int?`
523+
var tcg = _TypeConstraintGatherer({'T'});
524+
check(tcg.performSubtypeConstraintGenerationForLeftNullableType(
525+
Type('T?'), Type('Null'),
526+
leftSchema: false, astNodeForTesting: Node.placeholder()))
527+
.isTrue();
528+
check(tcg._constraints).deepEquals(['T <: Null']);
529+
});
530+
531+
test('Nullable does not match Nullable because base types fail to match',
532+
() {
533+
// `int? <# String?` reduces to `int <# String`
534+
var tcg = _TypeConstraintGatherer({});
535+
check(tcg.performSubtypeConstraintGenerationForLeftNullableType(
536+
Type('int?'), Type('String?'),
537+
leftSchema: false, astNodeForTesting: Node.placeholder()))
538+
.isFalse();
539+
check(tcg._constraints).isEmpty();
540+
});
541+
});
542+
543+
group('performSubtypeConstraintGenerationForRightNullableType:', () {
544+
test('Null matches Nullable favoring non-Null branch', () {
545+
// `Null <# T?` could match in two possible ways:
546+
// - `Null <# Null` (taking the "Null" branch of the FutureOr), producing
547+
// the empty constraint set.
548+
// - `Null <# T` (taking the "non-Null" branch of the FutureOr),
549+
// producing `Null <: T`
550+
// In cases where both branches produce a constraint, the "non-Null"
551+
// branch is favored.
552+
var tcg = _TypeConstraintGatherer({'T'});
553+
check(tcg.performSubtypeConstraintGenerationForRightNullableType(
554+
Type('Null'), Type('T?'),
555+
leftSchema: false, astNodeForTesting: Node.placeholder()))
556+
.isTrue();
557+
check(tcg._constraints).deepEquals(['Null <: T']);
558+
});
559+
560+
test('Type matches Nullable favoring the non-Null branch', () {
561+
// `T <# int?` could match in two possible ways:
562+
// - `T <# Null` (taking the "Null" branch of the Nullable),
563+
// producing `T <: Null`
564+
// - `T <# int` (taking the "non-Null" branch of the Nullable),
565+
// producing `T <: int`
566+
// In cases where both branches produce a constraint, the "non-Null"
567+
// branch is favored.
568+
var tcg = _TypeConstraintGatherer({'T'});
569+
check(tcg.performSubtypeConstraintGenerationForRightNullableType(
570+
Type('T'), Type('int?'),
571+
leftSchema: false, astNodeForTesting: Node.placeholder()))
572+
.isTrue();
573+
check(tcg._constraints).deepEquals(['T <: int']);
574+
});
575+
576+
test('Null matches Nullable with no constraints', () {
577+
// `Null <# int?` matches (taking the "Null" branch of
578+
// the Nullable) without generating any constraints.
579+
var tcg = _TypeConstraintGatherer({});
580+
check(tcg.performSubtypeConstraintGenerationForRightNullableType(
581+
Type('Null'), Type('int?'),
582+
leftSchema: false, astNodeForTesting: Node.placeholder()))
583+
.isTrue();
584+
check(tcg._constraints).isEmpty();
585+
});
586+
});
587+
520588
group('performSubtypeConstraintGenerationForTypeDeclarationTypes', () {
521589
group('Same base type on both sides:', () {
522590
test('Covariant, matching', () {
@@ -768,6 +836,16 @@ class _TypeConstraintGatherer extends TypeConstraintGenerator<Type,
768836
return true;
769837
}
770838

839+
if (performSubtypeConstraintGenerationForRightNullableType(p, q,
840+
leftSchema: leftSchema, astNodeForTesting: astNodeForTesting)) {
841+
return true;
842+
}
843+
844+
if (performSubtypeConstraintGenerationForLeftNullableType(p, q,
845+
leftSchema: leftSchema, astNodeForTesting: astNodeForTesting)) {
846+
return true;
847+
}
848+
771849
bool? result = performSubtypeConstraintGenerationForTypeDeclarationTypes(
772850
p, q,
773851
leftSchema: leftSchema, astNodeForTesting: astNodeForTesting);

pkg/analyzer/lib/src/dart/element/type_constraint_gatherer.dart

Lines changed: 6 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -218,53 +218,9 @@ class TypeConstraintGatherer extends shared.TypeConstraintGenerator<
218218
return true;
219219
}
220220

221-
// If `Q` is `Q0?` the match holds under constraint set `C`:
222-
if (Q_nullability == NullabilitySuffix.question) {
223-
var Q0 = _typeSystemOperations
224-
.withNullabilitySuffix(SharedTypeView(Q), NullabilitySuffix.none)
225-
.unwrapTypeView();
226-
var rewind = _constraints.length;
227-
228-
// If `P` is `P0?` and `P0` is a subtype match for `Q0` under
229-
// constraint set `C`.
230-
if (P_nullability == NullabilitySuffix.question) {
231-
var P0 = _typeSystemOperations
232-
.withNullabilitySuffix(SharedTypeView(P), NullabilitySuffix.none)
233-
.unwrapTypeView();
234-
if (trySubtypeMatch(P0, Q0, leftSchema,
235-
nodeForTesting: nodeForTesting)) {
236-
return true;
237-
}
238-
}
239-
240-
// Or if `P` is `dynamic` or `void` and `Object` is a subtype match
241-
// for `Q0` under constraint set `C`.
242-
if (P is SharedDynamicTypeStructure || P is SharedVoidTypeStructure) {
243-
if (trySubtypeMatch(_typeSystem.objectNone, Q0, leftSchema,
244-
nodeForTesting: nodeForTesting)) {
245-
return true;
246-
}
247-
}
248-
249-
// Or if `P` is a subtype match for `Q0` under non-empty
250-
// constraint set `C`.
251-
var P_matches_Q0 =
252-
trySubtypeMatch(P, Q0, leftSchema, nodeForTesting: nodeForTesting);
253-
if (P_matches_Q0 && _constraints.length != rewind) {
254-
return true;
255-
}
256-
257-
// Or if `P` is a subtype match for `Null` under constraint set `C`.
258-
if (trySubtypeMatch(P, _typeSystem.nullNone, leftSchema,
259-
nodeForTesting: nodeForTesting)) {
260-
return true;
261-
}
262-
263-
// Or if `P` is a subtype match for `Q0` under empty
264-
// constraint set `C`.
265-
if (P_matches_Q0) {
266-
return true;
267-
}
221+
if (performSubtypeConstraintGenerationForRightNullableType(P, Q,
222+
leftSchema: leftSchema, astNodeForTesting: nodeForTesting)) {
223+
return true;
268224
}
269225

270226
// If `P` is `FutureOr<P0>` the match holds under constraint set `C1 + C2`:
@@ -285,21 +241,9 @@ class TypeConstraintGatherer extends shared.TypeConstraintGenerator<
285241
}
286242

287243
// If `P` is `P0?` the match holds under constraint set `C1 + C2`:
288-
if (P_nullability == NullabilitySuffix.question) {
289-
var P0 = _typeSystemOperations
290-
.withNullabilitySuffix(SharedTypeView(P), NullabilitySuffix.none)
291-
.unwrapTypeView();
292-
var rewind = _constraints.length;
293-
294-
// If `P0` is a subtype match for `Q` under constraint set `C1`.
295-
// And if `Null` is a subtype match for `Q` under constraint set `C2`.
296-
if (trySubtypeMatch(P0, Q, leftSchema, nodeForTesting: nodeForTesting) &&
297-
trySubtypeMatch(_typeSystem.nullNone, Q, leftSchema,
298-
nodeForTesting: nodeForTesting)) {
299-
return true;
300-
}
301-
302-
_constraints.length = rewind;
244+
if (performSubtypeConstraintGenerationForLeftNullableType(P, Q,
245+
leftSchema: leftSchema, astNodeForTesting: nodeForTesting)) {
246+
return true;
303247
}
304248

305249
// If `Q` is `dynamic`, `Object?`, or `void` then the match holds under

pkg/front_end/lib/src/type_inference/type_constraint_gatherer.dart

Lines changed: 9 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -304,55 +304,17 @@ class TypeConstraintGatherer extends shared.TypeConstraintGenerator<
304304
// Or if P is a subtype match for Q0 under non-empty constraint set C.
305305
// Or if P is a subtype match for Null under constraint set C.
306306
// Or if P is a subtype match for Q0 under empty constraint set C.
307-
if (qNullability == NullabilitySuffix.question) {
308-
final int baseConstraintCount = _protoConstraints.length;
309-
final DartType rawP = typeOperations
310-
.withNullabilitySuffix(new SharedTypeView(p), NullabilitySuffix.none)
311-
.unwrapTypeView();
312-
final DartType rawQ = typeOperations
313-
.withNullabilitySuffix(new SharedTypeView(q), NullabilitySuffix.none)
314-
.unwrapTypeView();
315-
316-
if (pNullability == NullabilitySuffix.question &&
317-
_isNullabilityAwareSubtypeMatch(rawP, rawQ,
318-
constrainSupertype: constrainSupertype,
319-
treeNodeForTesting: treeNodeForTesting)) {
320-
return true;
321-
}
322-
323-
if ((p is SharedDynamicTypeStructure || p is SharedVoidTypeStructure) &&
324-
_isNullabilityAwareSubtypeMatch(
325-
typeOperations.objectType.unwrapTypeView(), rawQ,
326-
constrainSupertype: constrainSupertype,
327-
treeNodeForTesting: treeNodeForTesting)) {
328-
return true;
329-
}
330-
331-
bool isMatchWithRawQ = _isNullabilityAwareSubtypeMatch(p, rawQ,
332-
constrainSupertype: constrainSupertype,
333-
treeNodeForTesting: treeNodeForTesting);
334-
bool matchWithRawQAddsConstraints =
335-
_protoConstraints.length != baseConstraintCount;
336-
if (isMatchWithRawQ && matchWithRawQAddsConstraints) {
337-
return true;
338-
}
339-
340-
if (_isNullabilityAwareSubtypeMatch(
341-
p, typeOperations.nullType.unwrapTypeView(),
342-
constrainSupertype: constrainSupertype,
343-
treeNodeForTesting: treeNodeForTesting)) {
344-
return true;
345-
}
346-
347-
if (isMatchWithRawQ && !matchWithRawQAddsConstraints) {
348-
return true;
349-
}
307+
if (performSubtypeConstraintGenerationForRightNullableType(p, q,
308+
leftSchema: constrainSupertype,
309+
astNodeForTesting: treeNodeForTesting)) {
310+
return true;
350311
}
351312

352313
// If P is FutureOr<P0> the match holds under constraint set C1 + C2:
353314
//
354315
// If Future<P0> is a subtype match for Q under constraint set C1.
355316
// And if P0 is a subtype match for Q under constraint set C2.
317+
356318
if (typeOperations.matchFutureOrInternal(p) case DartType p0?) {
357319
final int baseConstraintCount = _protoConstraints.length;
358320
if (_isNullabilityAwareSubtypeMatch(
@@ -372,23 +334,10 @@ class TypeConstraintGatherer extends shared.TypeConstraintGenerator<
372334
//
373335
// If P0 is a subtype match for Q under constraint set C1.
374336
// And if Null is a subtype match for Q under constraint set C2.
375-
if (pNullability == NullabilitySuffix.question) {
376-
final int baseConstraintCount = _protoConstraints.length;
377-
if (_isNullabilityAwareSubtypeMatch(
378-
typeOperations
379-
.withNullabilitySuffix(
380-
new SharedTypeView(p), NullabilitySuffix.none)
381-
.unwrapTypeView(),
382-
q,
383-
constrainSupertype: constrainSupertype,
384-
treeNodeForTesting: treeNodeForTesting) &&
385-
_isNullabilityAwareSubtypeMatch(
386-
typeOperations.nullType.unwrapTypeView(), q,
387-
constrainSupertype: constrainSupertype,
388-
treeNodeForTesting: treeNodeForTesting)) {
389-
return true;
390-
}
391-
_protoConstraints.length = baseConstraintCount;
337+
if (performSubtypeConstraintGenerationForLeftNullableType(p, q,
338+
leftSchema: constrainSupertype,
339+
astNodeForTesting: treeNodeForTesting)) {
340+
return true;
392341
}
393342

394343
// If Q is dynamic, Object?, or void then the match holds under no

0 commit comments

Comments
 (0)