blob: 2c69ee42d1d072672a12f2765b3c71e42bcfc02f [file]
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:kernel/ast.dart';
import 'package:kernel/core_types.dart';
import 'package:kernel/names.dart';
import 'records.dart';
import 'util.dart';
/// Generates a class extending `Record` for each record shape in the
/// [Component].
///
/// Shape of a record is described by the [RecordShape] type.
///
/// Example: for the record `(1, a: 'hi', false)`, this generates:
///
/// ```
/// @pragma('wasm:entry-point')
/// class Record_2_a {
/// @pragma('wasm:entry-point')
/// final Object? $1;
///
/// @pragma('wasm:entry-point')
/// final Object? $2;
///
/// @pragma('wasm:entry-point')
/// final Object? a;
///
/// @pragma('wasm:entry-point')
/// Record_2_a(this.$1, this.$2, this.a);
///
/// @pragma('wasm:entry-point')
/// bool _checkRecordType(WasmArray<_Type> types, ImmutableWasmArray<String> names) {
/// if (types.length != 3) return false;
/// if (!identical(names, const ImmutableWasmArray(["a"]))) return false;
///
/// if (!_isSubtype($1, types[0])) return false;
/// if (!_isSubtype($2, types[1])) return false;
/// if (!_isSubtype($a, types[2])) return false;
///
/// return true;
/// }
///
/// @pragma('wasm:entry-point')
/// _Type get _masqueradedRecordRuntimeType =>
/// _RecordType(
/// const ImmutableWasmArray(["a"]),
/// WasmArray.literal([
/// _getMasqueradedRuntimeTypeNullable($1),
/// _getMasqueradedRuntimeTypeNullable($2),
/// _getMasqueradedRuntimeTypeNullable(a)
/// ]));
///
/// @pragma('wasm:entry-point')
/// _Type get _recordRuntimeType =>
/// _RecordType(
/// const ImmutableWasmArray(["a"]),
/// WasmArray.literal([
/// _getActualRuntimeTypeNullable($1),
/// _getActualRuntimeTypeNullable($2),
/// _getActualRuntimeTypeNullable(a)
/// ]));
///
/// @pragma('wasm:entry-point')
/// String toString() =>
/// "(" + $1 + ", " + $2 + ", " + "a: " + a + ")";
///
/// @pragma('wasm:entry-point')
/// bool operator ==(Object other) {
/// if (other is! Record_2_a) return false;
/// if ($1 != other.$1) return false;
/// if ($2 != other.$2) return false;
/// if (a != other.a) return false;
/// return true;
/// }
///
/// @pragma('wasm:entry-point')
/// int hashCode =>
/// Object.hash(shapeID, $1, $2, a);
/// }
/// ```
Map<RecordShape, Class> generateRecordClasses(
Component component,
CoreTypes coreTypes,
) {
final Map<RecordShape, Class> recordClasses = {};
final recordClassGenerator = _RecordClassGenerator(
recordClasses,
coreTypes.coreLibrary,
coreTypes,
);
final visitor = _RecordVisitor(recordClassGenerator);
component.libraries.forEach(visitor.visitLibrary);
return recordClasses;
}
class _RecordClassGenerator {
final CoreTypes coreTypes;
final Map<RecordShape, Class> classes;
final Library library;
late final Class typeRuntimetypeTypeClass = coreTypes.index.getClass(
"dart:core",
"_Type",
);
late final Class recordRuntimeTypeClass = coreTypes.index.getClass(
'dart:core',
'_RecordType',
);
late final Constructor recordRuntimeTypeConstructor =
recordRuntimeTypeClass.constructors.single;
late final Procedure objectHashProcedure = coreTypes.index.getProcedure(
'dart:core',
'Object',
'hash',
);
late final Procedure objectHashAllProcedure = coreTypes.index.getProcedure(
'dart:core',
'Object',
'hashAll',
);
late final Procedure objectToStringProcedure = coreTypes.index.getProcedure(
'dart:core',
'Object',
'toString',
);
late final Procedure identical = coreTypes.index.getTopLevelProcedure(
'dart:core',
'identical',
);
late final Procedure objectEqualsProcedure = coreTypes.objectEquals;
late final FunctionType integerEqualsFunctionType = FunctionType(
[intType, intType],
boolType,
Nullability.nonNullable,
);
late final Procedure stringPlusProcedure = coreTypes.index.getProcedure(
'dart:core',
'String',
'+',
);
late final Procedure isSubtype = coreTypes.index.getTopLevelProcedure(
'dart:core',
'_isSubtype',
);
late final Class wasmArrayClass = coreTypes.index.getClass(
'dart:_wasm',
'WasmArray',
);
late final Class immutableWasmArrayClass = coreTypes.index.getClass(
'dart:_wasm',
'ImmutableWasmArray',
);
late final Procedure wasmArrayRefLength = coreTypes.index.getProcedure(
'dart:_wasm',
'WasmArrayRef',
'get:length',
);
late final Procedure wasmArrayIndex =
coreTypes.index
.getLibrary('dart:_wasm')
.extensions
.singleWhere((e) => e.name == 'WasmArrayExt')
.memberDescriptors
.singleWhere((member) => member.name == indexGetName)
.memberReference!
.node
as Procedure;
late final Constructor wasmArrayLiteralConstructor = coreTypes.index
.getConstructor('dart:_wasm', 'WasmArray', 'literal');
late final Field wasmArrayValueField = coreTypes.index.getField(
"dart:_wasm",
"WasmArray",
"_value",
);
late final Field immutableWasmArrayValueField = coreTypes.index.getField(
"dart:_wasm",
"ImmutableWasmArray",
"_value",
);
late final InterfaceType wasmArrayOfType = InterfaceType(
wasmArrayClass,
Nullability.nonNullable,
[nonNullableTypeType],
);
late final InterfaceType immutableWasmArrayOfString = InterfaceType(
immutableWasmArrayClass,
Nullability.nonNullable,
[nonNullableStringType],
);
late final InterfaceType runtimeTypeType = InterfaceType(
typeRuntimetypeTypeClass,
Nullability.nonNullable,
);
late final InterfaceType nonNullableTypeType = InterfaceType(
typeRuntimetypeTypeClass,
Nullability.nonNullable,
);
late final Procedure getActualRuntimeTypeNullable = coreTypes.index
.getTopLevelProcedure('dart:core', '_getActualRuntimeTypeNullable');
late final Procedure getMasqueradedRuntimeTypeNullableProcedure = coreTypes
.index
.getTopLevelProcedure('dart:core', '_getMasqueradedRuntimeTypeNullable');
DartType get nullableObjectType => coreTypes.objectNullableRawType;
DartType get nonNullableStringType => coreTypes.stringNonNullableRawType;
DartType get boolType => coreTypes.boolNonNullableRawType;
DartType get intType => coreTypes.intNonNullableRawType;
_RecordClassGenerator(this.classes, this.library, this.coreTypes);
void generateClassForRecordType(RecordType recordType) {
final shape = RecordShape.fromType(recordType);
final id = classes.length;
classes.putIfAbsent(shape, () => _generateClass(shape, id));
}
Class _generateClass(RecordShape shape, int id) {
final fields = _generateFields(shape);
String className = 'Record_${shape.positionals}';
if (shape.names.isNotEmpty) {
className = '${className}_${shape.names.join('_')}';
}
final cls = addWasmEntryPointPragma(
Class(
name: className,
isAbstract: false,
isAnonymousMixin: false,
supertype: Supertype(coreTypes.recordClass, []),
constructors: [_generateConstructor(shape, fields)],
procedures: [
_generateHashCode(fields, id),
_generateToString(shape, fields),
],
fields: fields,
fileUri: library.fileUri,
),
coreTypes,
);
library.addClass(cls);
final getRti = _generateRecordRuntimeType(shape, fields);
cls.addProcedure(_generateEquals(shape, fields, cls, getRti));
cls.addProcedure(_generateCheckRecordType(shape, fields));
cls.addProcedure(getRti);
cls.addProcedure(_generateMasqueradedRecordRuntimeType(shape, fields));
return cls;
}
List<Field> _generateFields(RecordShape shape) {
final List<Field> fields = [];
for (int i = 0; i < shape.positionals; i += 1) {
fields.add(
addWasmEntryPointPragma(
Field.immutable(
Name('\$${i + 1}', library),
isFinal: true,
fileUri: library.fileUri,
),
coreTypes,
),
);
}
for (String name in shape.names) {
fields.add(
addWasmEntryPointPragma(
Field.immutable(
Name(name, library),
isFinal: true,
fileUri: library.fileUri,
),
coreTypes,
),
);
}
return fields;
}
/// Generate a constructor with name `_`. Named fields are passed in sorted
/// order.
Constructor _generateConstructor(RecordShape shape, List<Field> fields) {
final List<PositionalParameter> positionalParameters = List.generate(
fields.length,
(i) => PositionalParameter(cosmeticName: 'field$i', isSynthesized: true),
);
final List<Initializer> initializers = List.generate(
fields.length,
(i) => FieldInitializer(fields[i], VariableGet(positionalParameters[i])),
);
final function = FunctionNode(
null,
positionalParameters: positionalParameters,
);
return addWasmEntryPointPragma(
Constructor(
function,
name: Name('_', library),
isConst: true,
initializers: initializers,
fileUri: library.fileUri,
),
coreTypes,
);
}
/// Generate `int get hashCode` member.
Procedure _generateHashCode(List<Field> fields, int shapeId) {
final Expression returnValue;
if (fields.isEmpty) {
returnValue = IntLiteral(shapeId);
} else {
final List<Expression> arguments = [];
arguments.add(IntLiteral(shapeId));
for (Field field in fields) {
arguments.add(
InstanceGet(
InstanceAccessKind.Instance,
ThisExpression(),
field.name,
interfaceTarget: field,
resultType: nullableObjectType,
),
);
}
if (fields.length <= 20) {
// Object.hash(field1, field2, ...)
returnValue = StaticInvocation(
objectHashProcedure,
Arguments(arguments),
);
} else {
// Object.hashAll([field1, field2, ...])
returnValue = StaticInvocation(
objectHashAllProcedure,
Arguments([ListLiteral(arguments)]),
);
}
}
return addWasmEntryPointPragma(
Procedure(
Name('hashCode', library),
ProcedureKind.Getter,
FunctionNode(ReturnStatement(returnValue), returnType: intType),
fileUri: library.fileUri,
),
coreTypes,
);
}
/// Generate `String toString()` member.
Procedure _generateToString(RecordShape shape, List<Field> fields) {
final List<Expression> stringExprs = [];
Expression fieldToStringExpression(Field field) => InstanceInvocation(
InstanceAccessKind.Object,
InstanceGet(
InstanceAccessKind.Instance,
ThisExpression(),
field.name,
interfaceTarget: field,
resultType: nullableObjectType,
),
Name('toString'),
Arguments([]),
interfaceTarget: objectToStringProcedure,
functionType: FunctionType(
[],
nonNullableStringType,
Nullability.nonNullable,
),
);
int fieldIdx = 0;
for (; fieldIdx < shape.positionals; fieldIdx += 1) {
final Field field = fields[fieldIdx];
stringExprs.add(fieldToStringExpression(field));
if (fieldIdx != shape.numFields - 1) {
stringExprs.add(StringLiteral(', '));
}
}
for (String name in shape.names) {
final Field field = fields[fieldIdx];
stringExprs.add(StringLiteral('$name: '));
stringExprs.add(fieldToStringExpression(field));
if (fieldIdx != shape.numFields - 1) {
stringExprs.add(StringLiteral(', '));
}
fieldIdx += 1;
}
stringExprs.add(StringLiteral(')'));
final Expression stringExpression = stringExprs.fold(
StringLiteral('('),
(string, next) => InstanceInvocation(
InstanceAccessKind.Instance,
string,
Name('+'),
Arguments([next]),
interfaceTarget: stringPlusProcedure,
functionType: FunctionType(
[nonNullableStringType],
nonNullableStringType,
Nullability.nonNullable,
),
),
);
return addWasmEntryPointPragma(
Procedure(
Name('toString', library),
ProcedureKind.Method,
FunctionNode(
ReturnStatement(stringExpression),
returnType: coreTypes.stringNonNullableRawType,
),
fileUri: library.fileUri,
),
coreTypes,
);
}
/// Generate `bool operator ==` member.
Procedure _generateEquals(
RecordShape shape,
List<Field> fields,
Class cls,
Procedure getRti,
) {
final equalsFunctionType = FunctionType(
[nullableObjectType],
boolType,
Nullability.nonNullable,
);
final PositionalParameter parameter = PositionalParameter(
cosmeticName: 'other',
type: nullableObjectType,
isSynthesized: true,
);
final List<Statement> statements = [];
statements.add(
IfStatement(
Not(
IsExpression(
VariableGet(parameter),
InterfaceType(cls, Nullability.nonNullable),
),
),
ReturnStatement(BoolLiteral(false)),
null,
),
);
// Compare fields.
for (Field field in fields) {
statements.add(
IfStatement(
Not(
EqualsCall(
InstanceGet(
InstanceAccessKind.Instance,
ThisExpression(),
field.name,
interfaceTarget: field,
resultType: nullableObjectType,
),
InstanceGet(
InstanceAccessKind.Instance,
VariableGet(parameter),
field.name,
interfaceTarget: field,
resultType: nullableObjectType,
),
interfaceTarget: objectEqualsProcedure,
functionType: equalsFunctionType,
),
),
ReturnStatement(BoolLiteral(false)),
null,
),
);
}
statements.add(ReturnStatement(BoolLiteral(true)));
final FunctionNode function = FunctionNode(
Block(statements),
positionalParameters: [parameter],
returnType: boolType,
);
return addWasmEntryPointPragma(
Procedure(
Name('==', library),
ProcedureKind.Operator,
function,
fileUri: library.fileUri,
),
coreTypes,
);
}
/// Generate `_checkRecordType` member.
Procedure _generateCheckRecordType(RecordShape shape, List<Field> fields) {
final typesParameter = PositionalParameter(
cosmeticName: 'types',
type: wasmArrayOfType,
isSynthesized: true,
);
final namesParameter = PositionalParameter(
cosmeticName: 'names',
type: immutableWasmArrayOfString,
isSynthesized: true,
);
final List<Statement> statements = [];
// if (types.length != shape.numFields) return false;
statements.add(
IfStatement(
Not(
EqualsCall(
InstanceGet(
InstanceAccessKind.Instance,
VariableGet(typesParameter),
wasmArrayRefLength.name,
interfaceTarget: wasmArrayRefLength,
resultType: intType,
),
IntLiteral(shape.numFields),
functionType: integerEqualsFunctionType,
interfaceTarget: objectEqualsProcedure,
),
),
ReturnStatement(BoolLiteral(false)),
null,
),
);
// if (!identical(names, _fieldNamesConstant(shape))) return false;
statements.add(
IfStatement(
Not(
StaticInvocation(
identical,
Arguments([
VariableGet(namesParameter),
ConstantExpression(_fieldNamesConstant(shape)),
]),
),
),
ReturnStatement(BoolLiteral(false)),
null,
),
);
// if (!_isSubtype($..., types[...])) return false;
for (int i = 0; i < shape.numFields; ++i) {
final field = fields[i];
statements.add(
IfStatement(
Not(
StaticInvocation(
isSubtype,
Arguments([
InstanceGet(
InstanceAccessKind.Instance,
ThisExpression(),
field.name,
interfaceTarget: field,
resultType: nullableObjectType,
),
StaticInvocation(
wasmArrayIndex,
Arguments(
[VariableGet(typesParameter), IntLiteral(i)],
types: [nonNullableTypeType],
),
),
]),
),
),
ReturnStatement(BoolLiteral(false)),
null,
),
);
}
// return true
statements.add(ReturnStatement(BoolLiteral(true)));
final FunctionNode function = FunctionNode(
Block(statements),
positionalParameters: [typesParameter, namesParameter],
returnType: boolType,
);
return addWasmEntryPointPragma(
Procedure(
Name('_checkRecordType', coreTypes.coreLibrary),
ProcedureKind.Method,
function,
fileUri: library.fileUri,
),
coreTypes,
);
}
/// Generate `_Type get _recordRuntimeType` member.
Procedure _generateRecordRuntimeType(RecordShape shape, List<Field> fields) {
return _generateRecordRuntimeTypeHelper(
'_recordRuntimeType',
getActualRuntimeTypeNullable,
shape,
fields,
);
}
/// Generate `_Type get _masqueradedRecordRuntimeType ` member.
Procedure _generateMasqueradedRecordRuntimeType(
RecordShape shape,
List<Field> fields,
) {
return _generateRecordRuntimeTypeHelper(
'_masqueradedRecordRuntimeType',
getMasqueradedRuntimeTypeNullableProcedure,
shape,
fields,
);
}
Procedure _generateRecordRuntimeTypeHelper(
String name,
Procedure target,
RecordShape shape,
List<Field> fields,
) {
final List<Statement> statements = [];
// const ImmutableWasmArray(["name1", "name2", ...])
final fieldNamesList = ConstantExpression(_fieldNamesConstant(shape));
Expression fieldRuntimeTypeExpr(Field field) => StaticInvocation(
target,
Arguments([
InstanceGet(
InstanceAccessKind.Instance,
ThisExpression(),
field.name,
interfaceTarget: field,
resultType: nullableObjectType,
),
]),
);
// WasmArray.literal([_get*RuntimeTypeNullable(this.$1), ...])
final fieldTypesList = ConstructorInvocation(
wasmArrayLiteralConstructor,
Arguments(
[
ListLiteral(
fields.map(fieldRuntimeTypeExpr).toList(),
typeArgument: runtimeTypeType,
),
],
types: [runtimeTypeType],
),
);
statements.add(
ReturnStatement(
ConstructorInvocation(
recordRuntimeTypeConstructor,
Arguments([
fieldNamesList,
fieldTypesList,
BoolLiteral(false), // declared nullable
]),
),
),
);
final FunctionNode function = FunctionNode(
Block(statements),
positionalParameters: [],
returnType: InterfaceType(
recordRuntimeTypeClass,
Nullability.nonNullable,
),
);
return addWasmEntryPointPragma(
Procedure(
Name(name, library),
ProcedureKind.Getter,
function,
fileUri: library.fileUri,
),
coreTypes,
);
}
Constant _fieldNamesConstant(RecordShape shape) {
return InstanceConstant(
immutableWasmArrayClass.reference,
[nonNullableStringType],
{
immutableWasmArrayValueField.fieldReference: ListConstant(
nonNullableStringType,
shape.names.map((name) => StringConstant(name)).toList(),
),
},
);
}
}
class _RecordVisitor extends RecursiveVisitor {
final _RecordClassGenerator classGenerator;
final Set<Constant> constantCache = Set.identity();
_RecordVisitor(this.classGenerator);
@override
void visitRecordType(RecordType node) {
classGenerator.generateClassForRecordType(node);
super.visitRecordType(node);
}
@override
void defaultConstantReference(Constant node) {
if (constantCache.add(node)) {
node.visitChildren(this);
}
}
}