[dart2js,js_runtime] Record type recipes and runtime subtype test

Change-Id: I54adb8b6184a19667a296a670fecb37d7ce9dce7
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/262126
Reviewed-by: Mayank Patke <fishythefish@google.com>
Commit-Queue: Stephen Adams <sra@google.com>
This commit is contained in:
Stephen Adams 2022-10-05 16:48:01 +00:00 committed by Commit Queue
parent 16d06dac06
commit edf0da2df4
9 changed files with 280 additions and 33 deletions

View file

@ -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<DartType> sFields = s.fields;
List<DartType> 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;
}

View file

@ -741,6 +741,12 @@ class ScopeModelBuilder extends ir.Visitor<EvaluationComplexity>
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);

View file

@ -294,8 +294,19 @@ class _RecipeGenerator implements DartTypeVisitor<void, void> {
@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

View file

@ -100,6 +100,15 @@ class DartTypeNodeWriter
visitTypes(node.typeArguments, functionTypeVariables);
}
@override
void visitRecordType(
ir.RecordType node, List<ir.TypeParameter> functionTypeVariables) {
_sink.writeEnum(DartTypeNodeKind.recordType);
_sink.writeEnum(node.declaredNullability);
visitTypes(node.positional, functionTypeVariables);
_visitNamedTypes(node.named, functionTypeVariables);
}
@override
void visitFutureOrType(
ir.FutureOrType node, List<ir.TypeParameter> 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<ir.NamedType> named, List<ir.TypeParameter> 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

View file

@ -882,14 +882,7 @@ class DataSourceReader {
int requiredParameterCount = readInt();
List<ir.DartType> positionalParameters =
_readDartTypeNodes(functionTypeVariables);
int namedParameterCount = readInt();
final namedParameters =
List<ir.NamedType>.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<ir.DartType> typeArguments =
_readDartTypeNodes(functionTypeVariables);
return ExactInterfaceType(cls, nullability, typeArguments);
case DartTypeNodeKind.recordType:
ir.Nullability nullability = readEnum(ir.Nullability.values);
List<ir.DartType> positional =
_readDartTypeNodes(functionTypeVariables);
List<ir.NamedType> 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<ir.NamedType> _readNamedTypeNodes(
List<ir.TypeParameter> functionTypeVariables) {
int count = readInt();
if (count == 0) return const [];
return List<ir.NamedType>.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

View file

@ -85,6 +85,7 @@ enum DartTypeNodeKind {
functionType,
functionTypeVariable,
interfaceType,
recordType,
typedef,
dynamicType,
invalidType,

View file

@ -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);

View file

@ -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<String>? genericContext) {
return s;
}
String _recordRtiToString(Rti recordType, List<String>? 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<String>? genericContext,
{Object? bounds = null}) {
String typeParametersText = '';
@ -1416,6 +1461,10 @@ String _rtiToString(Rti rti, List<String>? 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 <pos> T1 ... Tn -> R(T1,...,Tn)
// R -3 <pos> T1 ... Tn optional -1 -> R(T1,...,Tn, [optional...])
// R -3 <pos> T1 ... Tn named -2 -> R(T1,...,Tn, {named...}])
//
// Record types:
//
// shapeToken -4 <pos> 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);

View file

@ -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);