Skip to content

Commit 41db3ab

Browse files
chloestefantsovaCommit Queue
authored andcommitted
[analyzer][cfe] Share constraint generation for record types
Part of #54902 Change-Id: I90ac37472e14f9d00ef5e7d20a41f2878fb2b8f6 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/393100 Commit-Queue: Chloe Stefantsova <[email protected]> Reviewed-by: Paul Berry <[email protected]>
1 parent 351a3b0 commit 41db3ab

File tree

8 files changed

+227
-106
lines changed

8 files changed

+227
-106
lines changed

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,6 +1065,58 @@ abstract class TypeConstraintGenerator<
10651065
return false;
10661066
}
10671067

1068+
/// Matches [p] against [q], where [p] and [q] are both record types.
1069+
///
1070+
/// If [p] is a subtype of [q] under some constraints, the constraints making
1071+
/// the relation possible are recorded, and `true` is returned. Otherwise,
1072+
/// the constraint state is unchanged (or rolled back), and `false` is
1073+
/// returned.
1074+
bool performSubtypeConstraintGenerationForRecordTypes(
1075+
TypeStructure p, TypeStructure q,
1076+
{required bool leftSchema, required AstNode? astNodeForTesting}) {
1077+
if (p is! SharedRecordTypeStructure<TypeStructure> ||
1078+
q is! SharedRecordTypeStructure<TypeStructure>) {
1079+
return false;
1080+
}
1081+
1082+
// A record type `(M0,..., Mk, {M{k+1} d{k+1}, ..., Mm dm])` is a subtype
1083+
// match for a record type `(N0,..., Nk, {N{k+1} d{k+1}, ..., Nm dm])`
1084+
// with respect to `L` under constraints `C0 + ... + Cm`
1085+
// If for `i` in `0...m`, `Mi` is a subtype match for `Ni` with respect
1086+
// to `L` under constraints `Ci`.
1087+
if (p.positionalTypes.length != q.positionalTypes.length ||
1088+
p.sortedNamedTypes.length != q.sortedNamedTypes.length) {
1089+
return false;
1090+
}
1091+
1092+
final TypeConstraintGeneratorState state = currentState;
1093+
1094+
for (int i = 0; i < p.positionalTypes.length; ++i) {
1095+
if (!performSubtypeConstraintGenerationInternal(
1096+
p.positionalTypes[i], q.positionalTypes[i],
1097+
leftSchema: leftSchema, astNodeForTesting: astNodeForTesting)) {
1098+
restoreState(state);
1099+
return false;
1100+
}
1101+
}
1102+
1103+
// Since record types don't allow optional positional or named
1104+
// parameters, and the named parameters are sorted, it's sufficient to
1105+
// check that the named parameters at the same index have the same name
1106+
// and matching types.
1107+
for (int i = 0; i < p.sortedNamedTypes.length; ++i) {
1108+
if (p.sortedNamedTypes[i].name != q.sortedNamedTypes[i].name ||
1109+
!performSubtypeConstraintGenerationInternal(
1110+
p.sortedNamedTypes[i].type, q.sortedNamedTypes[i].type,
1111+
leftSchema: leftSchema, astNodeForTesting: astNodeForTesting)) {
1112+
restoreState(state);
1113+
return false;
1114+
}
1115+
}
1116+
1117+
return true;
1118+
}
1119+
10681120
/// Matches [p] against [q].
10691121
///
10701122
/// If [q] is of the form `FutureOr<q0>` for some `q0`, and [p] is a subtype

pkg/_fe_analyzer_shared/lib/src/types/shared_type.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ abstract interface class SharedRecordTypeStructure<
7272
TypeStructure extends SharedTypeStructure<TypeStructure>>
7373
implements SharedTypeStructure<TypeStructure> {
7474
/// All the named fields, sorted by name.
75-
List<SharedNamedTypeStructure<TypeStructure>> get namedTypes;
75+
List<SharedNamedTypeStructure<TypeStructure>> get sortedNamedTypes;
7676

7777
List<TypeStructure> get positionalTypes;
7878
}
@@ -158,7 +158,7 @@ extension type SharedRecordTypeSchemaView<
158158
SharedRecordTypeStructure<TypeStructure> _typeStructure)
159159
implements SharedTypeSchemaView<TypeStructure> {
160160
List<SharedNamedTypeSchemaView<TypeStructure>> get namedTypes {
161-
return _typeStructure.namedTypes
161+
return _typeStructure.sortedNamedTypes
162162
as List<SharedNamedTypeSchemaView<TypeStructure>>;
163163
}
164164

@@ -173,7 +173,7 @@ extension type SharedRecordTypeView<
173173
SharedRecordTypeStructure<TypeStructure> _typeStructure)
174174
implements SharedTypeView<TypeStructure> {
175175
List<SharedNamedTypeView<TypeStructure>> get namedTypes {
176-
return _typeStructure.namedTypes
176+
return _typeStructure.sortedNamedTypes
177177
as List<SharedNamedTypeView<TypeStructure>>;
178178
}
179179

pkg/_fe_analyzer_shared/test/mini_types.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,9 +299,11 @@ class RecordType extends Type implements SharedRecordTypeStructure<Type> {
299299
@override
300300
final List<Type> positionalTypes;
301301

302-
@override
303302
final List<NamedType> namedTypes;
304303

304+
@override
305+
List<NamedType> get sortedNamedTypes => namedTypes;
306+
305307
RecordType({
306308
required this.positionalTypes,
307309
required this.namedTypes,

pkg/_fe_analyzer_shared/test/type_inference/type_constraint_gatherer_test.dart

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,155 @@ main() {
232232
});
233233
});
234234

235+
group('performSubtypeConstraintGenerationForRecordTypes:', () {
236+
test('Matching empty records', () {
237+
var tcg = _TypeConstraintGatherer({});
238+
check(tcg.performSubtypeConstraintGenerationForRecordTypes(
239+
Type('()'), Type('()'),
240+
leftSchema: false, astNodeForTesting: Node.placeholder()))
241+
.equals(true);
242+
check(tcg._constraints).isEmpty();
243+
});
244+
245+
group('Matching records:', () {
246+
test('Without named parameters', () {
247+
var tcg = _TypeConstraintGatherer({'T', 'U'});
248+
check(tcg.performSubtypeConstraintGenerationForRecordTypes(
249+
Type('(int, String)'), Type('(T, U)'),
250+
leftSchema: true, astNodeForTesting: Node.placeholder()))
251+
.equals(true);
252+
check(tcg._constraints).unorderedEquals(['int <: T', 'String <: U']);
253+
});
254+
255+
test('With named parameters', () {
256+
var tcg = _TypeConstraintGatherer({'T', 'U'});
257+
check(tcg.performSubtypeConstraintGenerationForRecordTypes(
258+
Type('(int, {String foo})'), Type('(T, {U foo})'),
259+
leftSchema: true, astNodeForTesting: Node.placeholder()))
260+
.equals(true);
261+
check(tcg._constraints).unorderedEquals(['int <: T', 'String <: U']);
262+
});
263+
});
264+
265+
group('Non-matching records without named parameters:', () {
266+
test('Non-matching due to positional types', () {
267+
var tcg = _TypeConstraintGatherer({});
268+
check(tcg.performSubtypeConstraintGenerationForRecordTypes(
269+
Type('(int,)'), Type('(String,)'),
270+
leftSchema: false, astNodeForTesting: Node.placeholder()))
271+
.isFalse();
272+
check(tcg._constraints).isEmpty();
273+
});
274+
275+
test('Non-matching due to parameter numbers', () {
276+
var tcg = _TypeConstraintGatherer({});
277+
check(tcg.performSubtypeConstraintGenerationForFunctionTypes(
278+
Type('()'), Type('(int,)'),
279+
leftSchema: false, astNodeForTesting: Node.placeholder()))
280+
.isFalse();
281+
check(tcg._constraints).isEmpty();
282+
});
283+
284+
test('Non-matching due to more parameters on LHS', () {
285+
var tcg = _TypeConstraintGatherer({});
286+
check(tcg.performSubtypeConstraintGenerationForFunctionTypes(
287+
Type('(int,)'), Type('()'),
288+
leftSchema: false, astNodeForTesting: Node.placeholder()))
289+
.isFalse();
290+
check(tcg._constraints).isEmpty();
291+
});
292+
});
293+
294+
group('Matching records with named parameters:', () {
295+
test('No type parameter occurrences', () {
296+
var tcg = _TypeConstraintGatherer({'T', 'U'});
297+
check(tcg.performSubtypeConstraintGenerationForRecordTypes(
298+
Type('({int x, String y})'), Type('({int x, String y})'),
299+
leftSchema: false, astNodeForTesting: Node.placeholder()))
300+
.isTrue();
301+
check(tcg._constraints).unorderedEquals([]);
302+
});
303+
304+
test('Type parameters in RHS', () {
305+
var tcg = _TypeConstraintGatherer({'T', 'U'});
306+
check(tcg.performSubtypeConstraintGenerationForRecordTypes(
307+
Type('({int x, String y})'), Type('({T x, U y})'),
308+
leftSchema: false, astNodeForTesting: Node.placeholder()))
309+
.isTrue();
310+
check(tcg._constraints).unorderedEquals(['int <: T', 'String <: U']);
311+
});
312+
313+
test('Type parameters in LHS', () {
314+
var tcg = _TypeConstraintGatherer({'T', 'U'});
315+
check(tcg.performSubtypeConstraintGenerationForRecordTypes(
316+
Type('({T x, U y})'), Type('({int x, String y})'),
317+
leftSchema: false, astNodeForTesting: Node.placeholder()))
318+
.isTrue();
319+
check(tcg._constraints).unorderedEquals(['T <: int', 'U <: String']);
320+
});
321+
});
322+
323+
group('Matching records with named parameters:', () {
324+
test('No type parameter occurrences', () {
325+
var tcg = _TypeConstraintGatherer({'T', 'U'});
326+
check(tcg.performSubtypeConstraintGenerationForRecordTypes(
327+
Type('({int x, String y})'), Type('({int x, String y})'),
328+
leftSchema: false, astNodeForTesting: Node.placeholder()))
329+
.isTrue();
330+
check(tcg._constraints).unorderedEquals([]);
331+
});
332+
333+
test('Type parameters in RHS', () {
334+
var tcg = _TypeConstraintGatherer({'T', 'U'});
335+
check(tcg.performSubtypeConstraintGenerationForRecordTypes(
336+
Type('({int x, String y})'), Type('({T x, U y})'),
337+
leftSchema: false, astNodeForTesting: Node.placeholder()))
338+
.isTrue();
339+
check(tcg._constraints).unorderedEquals(['int <: T', 'String <: U']);
340+
});
341+
342+
test('Type parameters in LHS', () {
343+
var tcg = _TypeConstraintGatherer({'T', 'U'});
344+
check(tcg.performSubtypeConstraintGenerationForRecordTypes(
345+
Type('({T x, U y})'), Type('({int x, String y})'),
346+
leftSchema: false, astNodeForTesting: Node.placeholder()))
347+
.isTrue();
348+
check(tcg._constraints).unorderedEquals(['T <: int', 'U <: String']);
349+
});
350+
});
351+
352+
group('Non-matching records with named parameters:', () {
353+
test('Non-matching due to positional parameter numbers', () {
354+
var tcg = _TypeConstraintGatherer({'T', 'U'});
355+
check(tcg.performSubtypeConstraintGenerationForRecordTypes(
356+
Type('(num, num, {T x, U y})'),
357+
Type('(num, {int x, String y})'),
358+
leftSchema: false,
359+
astNodeForTesting: Node.placeholder()))
360+
.isFalse();
361+
check(tcg._constraints).isEmpty();
362+
});
363+
364+
test('Non-matching due to named parameter numbers', () {
365+
var tcg = _TypeConstraintGatherer({'T', 'U'});
366+
check(tcg.performSubtypeConstraintGenerationForRecordTypes(
367+
Type('({T x, U y, num z})'), Type('({int x, String y})'),
368+
leftSchema: false, astNodeForTesting: Node.placeholder()))
369+
.isFalse();
370+
check(tcg._constraints).isEmpty();
371+
});
372+
373+
test('Non-matching due to named parameter names', () {
374+
var tcg = _TypeConstraintGatherer({'T', 'U'});
375+
check(tcg.performSubtypeConstraintGenerationForRecordTypes(
376+
Type('(num, {T x, U y})'), Type('(num, {int x, String x2})'),
377+
leftSchema: false, astNodeForTesting: Node.placeholder()))
378+
.isFalse();
379+
check(tcg._constraints).isEmpty();
380+
});
381+
});
382+
});
383+
235384
group('performSubtypeConstraintGenerationForFutureOr:', () {
236385
test('FutureOr matches FutureOr with constraints based on arguments', () {
237386
// `FutureOr<T> <# FutureOr<int>` reduces to `T <# int`
@@ -626,6 +775,11 @@ class _TypeConstraintGatherer extends TypeConstraintGenerator<Type,
626775
return result;
627776
}
628777

778+
if (performSubtypeConstraintGenerationForRecordTypes(p, q,
779+
leftSchema: leftSchema, astNodeForTesting: astNodeForTesting)) {
780+
return true;
781+
}
782+
629783
// Assume for the moment that nothing else matches.
630784
// TODO(paulberry): expand this as needed.
631785
return false;

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1199,9 +1199,11 @@ class RecordTypeImpl extends TypeImpl implements RecordType {
11991199
@override
12001200
String? get name => null;
12011201

1202-
@override
12031202
List<SharedNamedTypeStructure<DartType>> get namedTypes => namedFields;
12041203

1204+
@override
1205+
List<SharedNamedTypeStructure<DartType>> get sortedNamedTypes => namedTypes;
1206+
12051207
@override
12061208
bool operator ==(Object other) {
12071209
if (identical(other, this)) {

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

Lines changed: 3 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -368,10 +368,9 @@ class TypeConstraintGatherer extends shared.TypeConstraintGenerator<
368368
}
369369
}
370370

371-
if (P is SharedRecordTypeStructure<DartType> &&
372-
Q is SharedRecordTypeStructure<DartType>) {
373-
return _recordType(P as RecordTypeImpl, Q as RecordTypeImpl, leftSchema,
374-
nodeForTesting: nodeForTesting);
371+
if (performSubtypeConstraintGenerationForRecordTypes(P, Q,
372+
leftSchema: leftSchema, astNodeForTesting: nodeForTesting)) {
373+
return true;
375374
}
376375

377376
return false;
@@ -466,67 +465,6 @@ class TypeConstraintGatherer extends shared.TypeConstraintGenerator<
466465

467466
return true;
468467
}
469-
470-
/// Matches [P] against [Q], where [P] and [Q] are both record types.
471-
///
472-
/// If [P] is a subtype of [Q] under some constraints, the constraints making
473-
/// the relation possible are recorded to [_constraints], and `true` is
474-
/// returned. Otherwise, [_constraints] is left unchanged (or rolled back),
475-
/// and `false` is returned.
476-
bool _recordType(RecordTypeImpl P, RecordTypeImpl Q, bool leftSchema,
477-
{required AstNode? nodeForTesting}) {
478-
// If `P` is `(M0, ..., Mk)` and `Q` is `(N0, ..., Nk)`, then the match
479-
// holds under constraints `C0 + ... + Ck`:
480-
// If `Mi` is a subtype match for `Ni` with respect to L under
481-
// constraints `Ci`.
482-
if (P.nullabilitySuffix != NullabilitySuffix.none) {
483-
return false;
484-
}
485-
486-
if (Q.nullabilitySuffix != NullabilitySuffix.none) {
487-
return false;
488-
}
489-
490-
var positionalP = P.positionalFields;
491-
var positionalQ = Q.positionalFields;
492-
if (positionalP.length != positionalQ.length) {
493-
return false;
494-
}
495-
496-
var namedP = P.namedFields;
497-
var namedQ = Q.namedFields;
498-
if (namedP.length != namedQ.length) {
499-
return false;
500-
}
501-
502-
var rewind = _constraints.length;
503-
504-
for (var i = 0; i < positionalP.length; i++) {
505-
var fieldP = positionalP[i];
506-
var fieldQ = positionalQ[i];
507-
if (!trySubtypeMatch(fieldP.type, fieldQ.type, leftSchema,
508-
nodeForTesting: nodeForTesting)) {
509-
_constraints.length = rewind;
510-
return false;
511-
}
512-
}
513-
514-
for (var i = 0; i < namedP.length; i++) {
515-
var fieldP = namedP[i];
516-
var fieldQ = namedQ[i];
517-
if (fieldP.name != fieldQ.name) {
518-
_constraints.length = rewind;
519-
return false;
520-
}
521-
if (!trySubtypeMatch(fieldP.type, fieldQ.type, leftSchema,
522-
nodeForTesting: nodeForTesting)) {
523-
_constraints.length = rewind;
524-
return false;
525-
}
526-
}
527-
528-
return true;
529-
}
530468
}
531469

532470
/// Data structure maintaining intermediate type inference results, such as

0 commit comments

Comments
 (0)