diff --git a/pkg/compiler/lib/src/elements/types.dart b/pkg/compiler/lib/src/elements/types.dart index b88f90407bb..0c507ad92bf 100644 --- a/pkg/compiler/lib/src/elements/types.dart +++ b/pkg/compiler/lib/src/elements/types.dart @@ -2304,6 +2304,24 @@ abstract class DartTypes { return false; } + // Records + // + // TODO(50081): Reference rules to updated specification + // https://github.com/dart-lang/language/blob/master/resources/type-system/subtyping.md#rules + // + // TODO(50081): record is subtype of interface `Record`. + if (s is RecordType) { + if (t is! RecordType) return false; + if (s.shape != t.shape) return false; + List sFields = s.fields; + List tFields = t.fields; + assert(sFields.length == tFields.length); // Guaranteed by shape. + for (int i = 0; i < sFields.length; i++) { + if (!_isSubtype(sFields[i], tFields[i], env)) return false; + } + return true; + } + return false; } diff --git a/pkg/compiler/lib/src/ir/scope_visitor.dart b/pkg/compiler/lib/src/ir/scope_visitor.dart index 911f72f28cd..a15fa13b38c 100644 --- a/pkg/compiler/lib/src/ir/scope_visitor.dart +++ b/pkg/compiler/lib/src/ir/scope_visitor.dart @@ -741,6 +741,12 @@ class ScopeModelBuilder extends ir.Visitor return visitNodes(node.typeArguments); } + @override + EvaluationComplexity visitRecordType(ir.RecordType node) { + EvaluationComplexity complexity = visitNodes(node.positional); + return complexity.combine(visitNodes(node.named)); + } + @override EvaluationComplexity visitFutureOrType(ir.FutureOrType node) { return visitNode(node.typeArgument); diff --git a/pkg/compiler/lib/src/js_backend/runtime_types_new.dart b/pkg/compiler/lib/src/js_backend/runtime_types_new.dart index b0c071f7c0d..ec2818d039c 100644 --- a/pkg/compiler/lib/src/js_backend/runtime_types_new.dart +++ b/pkg/compiler/lib/src/js_backend/runtime_types_new.dart @@ -294,8 +294,19 @@ class _RecipeGenerator implements DartTypeVisitor { @override void visitRecordType(RecordType type, _) { - // TODO(49718): Implement Rti recipes for records. - throw UnimplementedError(); + _emitCode(Recipe.startRecord); + // Partial shape tag. The full shape is this plus the number of fields. + _emitStringUnescaped(type.shape.fieldNames.join(Recipe.separatorString)); + _emitCode(Recipe.startFunctionArguments); + bool first = true; + for (DartType field in type.fields) { + if (!first) { + _emitCode(Recipe.separator); + } + visit(field, _); + first = false; + } + _emitCode(Recipe.endFunctionArguments); } @override diff --git a/pkg/compiler/lib/src/serialization/helpers.dart b/pkg/compiler/lib/src/serialization/helpers.dart index 1659004d0f7..5a3cf51792c 100644 --- a/pkg/compiler/lib/src/serialization/helpers.dart +++ b/pkg/compiler/lib/src/serialization/helpers.dart @@ -100,6 +100,15 @@ class DartTypeNodeWriter visitTypes(node.typeArguments, functionTypeVariables); } + @override + void visitRecordType( + ir.RecordType node, List functionTypeVariables) { + _sink.writeEnum(DartTypeNodeKind.recordType); + _sink.writeEnum(node.declaredNullability); + visitTypes(node.positional, functionTypeVariables); + _visitNamedTypes(node.named, functionTypeVariables); + } + @override void visitFutureOrType( ir.FutureOrType node, List functionTypeVariables) { @@ -125,13 +134,18 @@ class DartTypeNodeWriter _sink.writeEnum(node.nullability); _sink.writeInt(node.requiredParameterCount); visitTypes(node.positionalParameters, functionTypeVariables); - _sink.writeInt(node.namedParameters.length); - for (ir.NamedType parameter in node.namedParameters) { + _visitNamedTypes(node.namedParameters, functionTypeVariables); + _sink.end(functionTypeNodeTag); + } + + void _visitNamedTypes( + List named, List functionTypeVariables) { + _sink.writeInt(named.length); + for (ir.NamedType parameter in named) { _sink.writeString(parameter.name); _sink.writeBool(parameter.isRequired); _sink._writeDartTypeNode(parameter.type, functionTypeVariables); } - _sink.end(functionTypeNodeTag); } @override diff --git a/pkg/compiler/lib/src/serialization/source.dart b/pkg/compiler/lib/src/serialization/source.dart index 5194f8be1ba..1d3ce2473de 100644 --- a/pkg/compiler/lib/src/serialization/source.dart +++ b/pkg/compiler/lib/src/serialization/source.dart @@ -882,14 +882,7 @@ class DataSourceReader { int requiredParameterCount = readInt(); List positionalParameters = _readDartTypeNodes(functionTypeVariables); - int namedParameterCount = readInt(); - final namedParameters = - List.generate(namedParameterCount, (index) { - String name = readString(); - bool isRequired = readBool(); - ir.DartType type = _readDartTypeNode(functionTypeVariables)!; - return ir.NamedType(name, type, isRequired: isRequired); - }, growable: false); + final namedParameters = _readNamedTypeNodes(functionTypeVariables); end(functionTypeNodeTag); return ir.FunctionType(positionalParameters, returnType, nullability, namedParameters: namedParameters, @@ -914,6 +907,12 @@ class DataSourceReader { List typeArguments = _readDartTypeNodes(functionTypeVariables); return ExactInterfaceType(cls, nullability, typeArguments); + case DartTypeNodeKind.recordType: + ir.Nullability nullability = readEnum(ir.Nullability.values); + List positional = + _readDartTypeNodes(functionTypeVariables); + List named = _readNamedTypeNodes(functionTypeVariables); + return ir.RecordType(positional, named, nullability); case DartTypeNodeKind.typedef: ir.Typedef typedef = readTypedefNode(); ir.Nullability nullability = readEnum(ir.Nullability.values); @@ -931,6 +930,18 @@ class DataSourceReader { } } + List _readNamedTypeNodes( + List functionTypeVariables) { + int count = readInt(); + if (count == 0) return const []; + return List.generate(count, (index) { + String name = readString(); + bool isRequired = readBool(); + ir.DartType type = _readDartTypeNode(functionTypeVariables)!; + return ir.NamedType(name, type, isRequired: isRequired); + }, growable: false); + } + /// Reads a list of kernel type nodes from this data source. /// /// This is a convenience method to be used together with diff --git a/pkg/compiler/lib/src/serialization/tags.dart b/pkg/compiler/lib/src/serialization/tags.dart index 34db060e328..b40781f5a2b 100644 --- a/pkg/compiler/lib/src/serialization/tags.dart +++ b/pkg/compiler/lib/src/serialization/tags.dart @@ -85,6 +85,7 @@ enum DartTypeNodeKind { functionType, functionTypeVariable, interfaceType, + recordType, typedef, dynamicType, invalidType, diff --git a/pkg/js_shared/lib/synced/recipe_syntax.dart b/pkg/js_shared/lib/synced/recipe_syntax.dart index 51b8358ab87..03d49581613 100644 --- a/pkg/js_shared/lib/synced/recipe_syntax.dart +++ b/pkg/js_shared/lib/synced/recipe_syntax.dart @@ -58,6 +58,9 @@ abstract class Recipe { static const String genericFunctionTypeParameterIndexString = _circumflexString; + static const int startRecord = _plus; + static const String startRecordString = _plusString; + static const int extensionOp = _ampersand; static const String extensionOpString = _ampersandString; static const int pushNeverExtension = 0; @@ -198,6 +201,7 @@ abstract class Recipe { requiredNameSeparatorString); test("genericFunctionTypeParameterIndex", genericFunctionTypeParameterIndex, genericFunctionTypeParameterIndexString); + test("startRecord", startRecord, startRecordString); test("extensionOp", extensionOp, extensionOpString); testExtension( "pushNeverExtension", pushNeverExtension, pushNeverExtensionString); diff --git a/sdk/lib/_internal/js_shared/lib/rti.dart b/sdk/lib/_internal/js_shared/lib/rti.dart index c14ebbf4529..1241c457a70 100644 --- a/sdk/lib/_internal/js_shared/lib/rti.dart +++ b/sdk/lib/_internal/js_shared/lib/rti.dart @@ -184,9 +184,10 @@ class Rti { static const int kindInterface = 9; // A vector of type parameters from enclosing functions and closures. static const int kindBinding = 10; - static const int kindFunction = 11; - static const int kindGenericFunction = 12; - static const int kindGenericFunctionParameter = 13; + static const int kindRecord = 11; + static const int kindFunction = 12; + static const int kindGenericFunction = 13; + static const int kindGenericFunctionParameter = 14; static bool _isUnionOfFunctionType(Rti rti) { int kind = Rti._getKind(rti); @@ -202,6 +203,8 @@ class Rti { /// - Underlying type for unary terms. /// - Class part of a type environment inside a generic class, or `null` for /// type tuple. + /// - A tag that, together with the number of fields, distinguishes the shape + /// of a record type. /// - Return type of a function type. /// - Underlying function type for a generic function. /// - de Bruijn index for a generic function parameter. @@ -217,6 +220,7 @@ class Rti { /// - The type arguments of an interface type. /// - The type arguments from enclosing functions and closures for a /// kindBinding. + /// - The field types of a record type. /// - The [_FunctionParameters] of a function type. /// - The type parameter bounds of a generic function. Object? _rest; @@ -248,6 +252,16 @@ class Rti { return JS('JSUnmodifiableArray', '#', _getRest(rti)); } + static String _getRecordPartialShapeTag(Rti rti) { + assert(_getKind(rti) == kindRecord); + return _Utils.asString(_getPrimary(rti)); + } + + static JSArray _getRecordFields(Rti rti) { + assert(_getKind(rti) == kindRecord); + return JS('JSUnmodifiableArray', '#', _getRest(rti)); + } + static Rti _getStarArgument(Rti rti) { assert(_getKind(rti) == kindStar); return _Utils.asRti(_getPrimary(rti)); @@ -1270,6 +1284,37 @@ String _rtiArrayToString(Object? array, List? genericContext) { return s; } +String _recordRtiToString(Rti recordType, List? genericContext) { + // For correctness of subtyping, the partial shape tag could be any encoding + // that maps different sets of names to different tags. + // + // Here we assume that the tag is a comma-separated list of names for the last + // N named fields. + String partialShape = Rti._getRecordPartialShapeTag(recordType); + Object? fields = Rti._getRecordFields(recordType); + if ('' == partialShape) { + // No named fields. + return '(' + _rtiArrayToString(fields, genericContext) + ')'; + } + + int fieldCount = _Utils.arrayLength(fields); + Object names = _Utils.stringSplit(partialShape, ','); + int namesIndex = _Utils.arrayLength(names) - fieldCount; // Can be negative. + + String s = '(', comma = ''; + for (int i = 0; i < fieldCount; i++) { + s += comma; + comma = ', '; + if (namesIndex == 0) s += '{'; + s += _rtiToString(_Utils.asRti(_Utils.arrayAt(fields, i)), genericContext); + if (namesIndex >= 0) { + s += ' ' + _Utils.asString(_Utils.arrayAt(names, namesIndex)); + } + namesIndex++; + } + return s + '})'; +} + String _functionRtiToString(Rti functionType, List? genericContext, {Object? bounds = null}) { String typeParametersText = ''; @@ -1416,6 +1461,10 @@ String _rtiToString(Rti rti, List? genericContext) { return name; } + if (kind == Rti.kindRecord) { + return _recordRtiToString(rti, genericContext); + } + if (kind == Rti.kindFunction) { return _functionRtiToString(rti, genericContext); } @@ -2049,6 +2098,36 @@ class _Universe { return _installTypeTests(universe, rti); } + static String _canonicalRecipeOfRecord( + String partialShapeTag, Object? fields) { + return _recipeJoin5( + Recipe.startRecordString, + partialShapeTag, + Recipe.startFunctionArgumentsString, + _canonicalRecipeJoin(fields), + Recipe.endFunctionArgumentsString); + } + + static Rti _lookupRecordRti( + Object? universe, String partialShapeTag, Object? fields) { + String key = _canonicalRecipeOfRecord(partialShapeTag, fields); + var cache = evalCache(universe); + var probe = _Utils.mapGet(cache, key); + if (probe != null) return _Utils.asRti(probe); + return _installRti(universe, key, + _createRecordRti(universe, partialShapeTag, fields, key)); + } + + static Rti _createRecordRti( + Object? universe, String partialShapeTag, Object? fields, String key) { + Rti rti = Rti.allocate(); + Rti._setKind(rti, Rti.kindRecord); + Rti._setPrimary(rti, partialShapeTag); + Rti._setRest(rti, fields); + Rti._setCanonicalRecipe(rti, key); + return _installTypeTests(universe, rti); + } + static String _canonicalRecipeOfFunction( Rti returnType, _FunctionParameters parameters) => _recipeJoin(Rti._getCanonicalRecipe(returnType), @@ -2413,11 +2492,12 @@ class _Parser { break; case Recipe.startFunctionArguments: + push(stack, gotoFunction); pushStackFrame(parser, stack); break; case Recipe.endFunctionArguments: - handleFunctionArguments(parser, stack); + handleArguments(parser, stack); break; case Recipe.startOptionalGroup: @@ -2436,6 +2516,10 @@ class _Parser { handleNamedGroup(parser, stack); break; + case Recipe.startRecord: + i = handleStartRecord(parser, i, source, stack); + break; + default: JS('', 'throw "Bad character " + #', ch); } @@ -2511,24 +2595,40 @@ class _Parser { } } - static const int optionalPositionalSentinel = -1; - static const int namedSentinel = -2; + static const int optionalPositionalMarker = -1; + static const int namedMarker = -2; + static const int gotoFunction = -3; + static const int gotoRecord = -4; - static void handleFunctionArguments(Object? parser, Object? stack) { + static void handleArguments(Object? parser, Object? stack) { var universe = _Parser.universe(parser); - _FunctionParameters parameters = _FunctionParameters.allocate(); - Object? optionalPositional = _Universe.sharedEmptyArray(universe); - Object? named = _Universe.sharedEmptyArray(universe); + Object? optionalPositional; + Object? named; + + // Parse the stack into a function type or a record type. A 'goto' marker is + // on the stack to distinguish between records and functions (similar to the + // GOTO table of an LR parser), and a marker tag is used for optional and + // named argument groups. + // + // Function types: + // + // R -3 T1 ... Tn -> R(T1,...,Tn) + // R -3 T1 ... Tn optional -1 -> R(T1,...,Tn, [optional...]) + // R -3 T1 ... Tn named -2 -> R(T1,...,Tn, {named...}]) + // + // Record types: + // + // shapeToken -4 T1 ... Tn -> (T1,...,Tn) with shapeToken var head = pop(stack); if (_Utils.isNum(head)) { int sentinel = _Utils.asInt(head); switch (sentinel) { - case optionalPositionalSentinel: + case optionalPositionalMarker: optionalPositional = pop(stack); break; - case namedSentinel: + case namedMarker: named = pop(stack); break; @@ -2540,24 +2640,62 @@ class _Parser { push(stack, head); } - _FunctionParameters._setRequiredPositional( - parameters, collectArray(parser, stack)); - _FunctionParameters._setOptionalPositional(parameters, optionalPositional); - _FunctionParameters._setNamed(parameters, named); - Rti returnType = toType(universe, environment(parser), pop(stack)); - push(stack, _Universe._lookupFunctionRti(universe, returnType, parameters)); + Object? requiredPositional = collectArray(parser, stack); + + head = pop(stack); + switch (head) { + case gotoFunction: + head = pop(stack); + optionalPositional ??= _Universe.sharedEmptyArray(universe); + named ??= _Universe.sharedEmptyArray(universe); + Rti returnType = toType(universe, environment(parser), head); + _FunctionParameters parameters = _FunctionParameters.allocate(); + _FunctionParameters._setRequiredPositional( + parameters, requiredPositional); + _FunctionParameters._setOptionalPositional( + parameters, optionalPositional); + _FunctionParameters._setNamed(parameters, named); + push(stack, + _Universe._lookupFunctionRti(universe, returnType, parameters)); + return; + + case gotoRecord: + assert(optionalPositional == null); + assert(named == null); + head = pop(stack); + assert(_Utils.isString(head)); + push( + stack, + _Universe._lookupRecordRti( + universe, _Utils.asString(head), requiredPositional)); + return; + + default: + throw AssertionError('Unexpected state under `()`: $head'); + } } static void handleOptionalGroup(Object? parser, Object? stack) { var parameters = collectArray(parser, stack); push(stack, parameters); - push(stack, optionalPositionalSentinel); + push(stack, optionalPositionalMarker); } static void handleNamedGroup(Object? parser, Object? stack) { var parameters = collectNamed(parser, stack); push(stack, parameters); - push(stack, namedSentinel); + push(stack, namedMarker); + } + + static int handleStartRecord( + Object? parser, int start, String source, Object? stack) { + int end = _Utils.stringIndexOf( + source, Recipe.startFunctionArgumentsString, start); + assert(end >= 0); + push(stack, _Utils.substring(source, start, end)); + push(stack, gotoRecord); + pushStackFrame(parser, stack); + return end + 1; } static void handleExtendedOperations(Object? parser, Object? stack) { @@ -2861,6 +2999,17 @@ bool _isSubtype(Object? universe, Rti s, Object? sEnv, Rti t, Object? tEnv) { return _isInterfaceSubtype(universe, s, sEnv, t, tEnv); } + // Records + // + // TODO(50081): Reference rules to updated specification + // https://github.com/dart-lang/language/blob/master/resources/type-system/subtyping.md#rules + // + // TODO(50081): record is subtype of interface `Record`. + if (sKind == Rti.kindRecord) { + if (tKind != Rti.kindRecord) return false; + return _isRecordSubtype(universe, s, sEnv, t, tEnv); + } + return false; } @@ -3058,6 +3207,29 @@ bool _areArgumentsSubtypes(Object? universe, Object? sArgs, Object? sVariances, return true; } +bool _isRecordSubtype( + Object? universe, Rti s, Object? sEnv, Rti t, Object? tEnv) { + // `s` is a subtype of `t` if `s` and `t` have the same shape and the fields + // of `s` are pairwise subtypes of the fields of `t`. + final sFields = Rti._getRecordFields(s); + final tFields = Rti._getRecordFields(t); + int sCount = _Utils.arrayLength(sFields); + int tCount = _Utils.arrayLength(tFields); + if (sCount != tCount) return false; + String sTag = Rti._getRecordPartialShapeTag(s); + String tTag = Rti._getRecordPartialShapeTag(t); + if (sTag != tTag) return false; + + for (int i = 0; i < sCount; i++) { + Rti sField = _Utils.asRti(_Utils.arrayAt(sFields, i)); + Rti tField = _Utils.asRti(_Utils.arrayAt(tFields, i)); + if (!_isSubtype(universe, sField, sEnv, tField, tEnv)) { + return false; + } + } + return true; +} + bool isNullable(Rti t) { int kind = Rti._getKind(t); return isNullType(t) || @@ -3153,9 +3325,15 @@ class _Utils { static JSArray arrayConcat(Object? a1, Object? a2) => JS('JSArray', '#.concat(#)', a1, a2); + static JSArray stringSplit(String s, String pattern) => + JS('JSArray', '#.split(#)', s, pattern); + static String substring(String s, int start, int end) => JS('String', '#.substring(#, #)', s, start, end); + static int stringIndexOf(String s, String pattern, int start) => + JS('int', '#.indexOf(#, #)', s, pattern, start); + static bool stringLessThan(String s1, String s2) => JS('bool', '# < #', s1, s2); diff --git a/sdk/lib/_internal/js_shared/lib/synced/recipe_syntax.dart b/sdk/lib/_internal/js_shared/lib/synced/recipe_syntax.dart index 51b8358ab87..03d49581613 100644 --- a/sdk/lib/_internal/js_shared/lib/synced/recipe_syntax.dart +++ b/sdk/lib/_internal/js_shared/lib/synced/recipe_syntax.dart @@ -58,6 +58,9 @@ abstract class Recipe { static const String genericFunctionTypeParameterIndexString = _circumflexString; + static const int startRecord = _plus; + static const String startRecordString = _plusString; + static const int extensionOp = _ampersand; static const String extensionOpString = _ampersandString; static const int pushNeverExtension = 0; @@ -198,6 +201,7 @@ abstract class Recipe { requiredNameSeparatorString); test("genericFunctionTypeParameterIndex", genericFunctionTypeParameterIndex, genericFunctionTypeParameterIndexString); + test("startRecord", startRecord, startRecordString); test("extensionOp", extensionOp, extensionOpString); testExtension( "pushNeverExtension", pushNeverExtension, pushNeverExtensionString);