blob: 6779a5e478efa20ce87b250d78a370166a4e9b4f [file] [log] [blame]
// 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.
/// Records are implemented as classes introduced in the K- to J- lowering.
///
/// The record classes are arranged in a hierarchy, with single base class. The
/// base class has subclasses for each 'basic' shape, the number of fields. This
/// is called the 'arity' class. The arity class has subclasses for each actual
/// record shape. There can be further subclasses of the shape class to allow
/// specialization on the basis of the value stored in the field.
///
/// Example
///
/// _Record - base class
///
/// _Record2 - class for Record arity (number of fields)
///
/// _Record_2_end_start - class for shape `(start:, end:)`
///
/// _Record_2_end_start__int_int - class for specialization within
/// shape when the field are known to in `int`s. This allows more
/// efficient `==` and `.hashCode` operations.
///
/// RecordDataBuilder creates the new classes. The arity classes exist as Dart
/// code in `js_runtime/lib/records.dart`. RecordDataBuilder creates shape
/// classes and specialization classes.
///
/// (Specialization classes have not yet been implemented).
library;
import 'package:js_shared/variance.dart';
import '../common.dart';
import '../constants/values.dart' show ConstantValue;
import '../elements/entities.dart';
import '../elements/names.dart';
import '../elements/types.dart';
import '../ir/element_map.dart' show IrToElementMap;
import '../js_backend/annotations.dart';
import '../ordered_typeset.dart';
import '../serialization/serialization.dart';
import '../universe/record_shape.dart';
import 'class_type_variable_access.dart';
import 'element_map.dart';
import 'element_map_impl.dart' show JsKernelToElementMap;
import 'elements.dart';
import 'env.dart';
import 'js_world_builder.dart' show JClosedWorldBuilder;
class RecordData {
/// Tag used for identifying serialized [RecordData] objects in a
/// debugging data stream.
static const String tag = 'record-data';
final JsToElementMap _elementMap;
final List<RecordRepresentation> _representations;
final Map<RecordShape, List<MemberEntity>> _gettersByShape;
final Map<ClassEntity, RecordRepresentation> _classToRepresentation = {};
final Map<RecordShape, RecordRepresentation> _shapeToRepresentation = {};
RecordData._(this._elementMap, this._representations, this._gettersByShape) {
// Unpack representations into lookup maps.
for (final info in _representations) {
_classToRepresentation[info.cls] = info;
if (info.definesShape) _shapeToRepresentation[info.shape] = info;
}
}
Iterable<MemberEntity> get allGetters =>
_gettersByShape.values.expand((e) => e);
List<MemberEntity> gettersForShape(RecordShape shape) =>
_gettersByShape[shape]!;
Iterable<ClassEntity> get allClasses => _classToRepresentation.keys;
factory RecordData.readFromDataSource(
JsToElementMap elementMap,
DataSourceReader source,
) {
source.begin(tag);
List<RecordRepresentation> representations = source.readList(
() => RecordRepresentation.readFromDataSource(source),
);
final shapes = source.readList(
() => RecordShape.readFromDataSource(source),
);
final getters = source.readList(source.readMembers);
source.end(tag);
return RecordData._(
elementMap,
representations,
Map.fromIterables(shapes, getters),
);
}
/// Serializes this [RecordData] to [sink].
void writeToDataSink(DataSinkWriter sink) {
sink.begin(tag);
sink.writeList<RecordRepresentation>(
_representations,
(info) => info.writeToDataSink(sink),
);
sink.writeList(
_gettersByShape.keys,
(RecordShape v) => v.writeToDataSink(sink),
);
sink.writeList(_gettersByShape.values, sink.writeMembers);
sink.end(tag);
}
/// Returns a fresh List of representations that define shapes.
List<RecordRepresentation> representationsForShapes() => [
..._shapeToRepresentation.values,
];
/// Returns the representation for a shape. Returns `null` if the record
/// shape is not instantiated.
RecordRepresentation? representationForShape(RecordShape shape) {
return _shapeToRepresentation[shape];
}
/// This should only be called at record creation sites, ensuring that there
/// is a representation.
RecordRepresentation representationForStaticType(RecordType type) {
// TODO(49718): Implement specialization when fields have types that allow
// better code for `==` and `.hashCode`.
// TODO(50081): Ensure the specialization is correctly identified the
// 'static' type of a constant record where the type is generated from the
// field values.
return representationForShape(type.shape) ??
(throw StateError(
'representationForStaticType $type '
'for uninstantiated shape ${type.shape}',
));
}
/// Returns `null` if [cls] is not a record representation.
RecordRepresentation? representationForClass(ClassEntity cls) {
return _classToRepresentation[cls];
}
/// Returns field and possibly index for accessing into a shape. The shape
/// needs to have a representation, so calls may need to be guarded by
/// checking that there is a representation for the shape
/// ([representationForShape]).
RecordAccessPath pathForAccess(RecordShape shape, int indexInShape) {
// TODO(sra): Cache lookup.
final representation = representationForShape(shape)!;
final cls = representation.cls;
if (representation.usesList) {
final field = _elementMap.elementEnvironment.lookupClassMember(
cls,
Name('_values', cls.library.canonicalUri),
);
return RecordAccessPath(field as FieldEntity, indexInShape);
} else {
final field = _elementMap.elementEnvironment.lookupClassMember(
cls,
Name('_$indexInShape', cls.library.canonicalUri),
);
return RecordAccessPath(field as FieldEntity, null);
}
}
}
/// How to access a field of a record. Currently there are two forms, a single
/// field access (e.g. `r._2`), used for small records, or a field access
/// followed by an indexing, used for records that hold the values in a JSArray
/// (e.g. `r._values[2]`).
class RecordAccessPath {
final FieldEntity field;
final int? index; // `null` for single field access.
RecordAccessPath(this.field, this.index);
}
class RecordRepresentation {
static const String tag = 'record-class-info';
/// There is one [RecordRepresentation] per class.
final ClassEntity cls;
/// The record shape of [cls]. There can be many classes defining records of
/// the same shape, for example, when there are specializations of a record
/// shape.
final RecordShape shape;
/// [definesShape] is `true` if this record class is a shape class. There may
/// be subclasses of this class which share the same shape. In this case
/// [definesShape] is `false`, as the subclasses can inherit shape metadata.
///
/// A shape class defines some metadata properties on the prototype:
///
/// (1) The shapeTag, a small integer (see below).
/// (2) The top-type recipe for the shape. The recipe is a function of the
/// [shape]. e.g. `"+end,start(@,@)"`.
final bool definesShape;
/// `true` if this class is based on the general record class that uses a
/// `List` to store the fields.
final bool usesList;
/// [shapeTag] is a small integer that is a function of the shape. The
/// shapeTag can be used as an index into runtime computed derived data.
final int shapeTag;
/// This is non-null for a specialization subclass of a shape class.
// TODO(50081): This is a placeholder for the specialization key. We might do
// something like interceptors, where 'i' means 'int', 's' means 'string', so
// they key 's_i_is' would be a specialization for a record where the fields
// are {int}, {string} and {int,string}. Operator `==` could be specialized to
// use `===` for each field, but `.hashCode` would need a dispatch for the
// last field. Or we could do something completely different like have a
// `List` of inferred types.
final String? _specializationKey;
RecordRepresentation._(
this.cls,
this.shape,
this.definesShape,
this.usesList,
this.shapeTag,
this._specializationKey,
);
factory RecordRepresentation.readFromDataSource(DataSourceReader source) {
source.begin(tag);
final cls = source.readClass();
final shape = RecordShape.readFromDataSource(source);
final definesShape = source.readBool();
final usesList = source.readBool();
final shapeTag = source.readInt();
final specializationKey = source.readStringOrNull();
source.end(tag);
return RecordRepresentation._(
cls,
shape,
definesShape,
usesList,
shapeTag,
specializationKey,
);
}
/// Serializes this [RecordData] to [sink].
void writeToDataSink(DataSinkWriter sink) {
sink.begin(tag);
sink.writeClass(cls);
shape.writeToDataSink(sink);
sink.writeBool(definesShape);
sink.writeBool(usesList);
sink.writeInt(shapeTag);
sink.writeStringOrNull(_specializationKey);
sink.end(tag);
}
@override
String toString() {
final sb = StringBuffer('RecordRepresentation(');
sb.writeAll([
'cls=$cls',
'shape=$shape',
'shapeTag=$shapeTag',
if (_specializationKey != null) 'specializationKey=$_specializationKey',
], ',');
sb.write(')');
return sb.toString();
}
}
/// Conversion of records to classes.
class RecordDataBuilder {
final DiagnosticReporter _reporter;
final JsToElementMap _elementMap;
final AnnotationsData _annotationsData;
final Map<RecordShape, List<MemberEntity>> _gettersByShape = {};
RecordDataBuilder(this._reporter, this._elementMap, this._annotationsData);
RecordData createRecordData(
JClosedWorldBuilder closedWorldBuilder,
Iterable<RecordType> recordTypes,
) {
_reporter;
_annotationsData;
// Sorted shapes lead to a more consistent class ordering in the generated
// code.
final shapes =
recordTypes.map((type) => type.shape).toSet().toList()
..sort(RecordShape.compare);
List<RecordRepresentation> representations = [];
for (int i = 0; i < shapes.length; i++) {
final shape = shapes[i];
final getters = <MemberEntity>[];
final cls =
shape.fieldCount == 0
? _elementMap.commonElements.emptyRecordClass
: closedWorldBuilder.buildRecordShapeClass(shape, getters);
_gettersByShape[shape] = getters;
int shapeTag = i;
bool usesList = _computeUsesGeneralClass(cls);
final info = RecordRepresentation._(
cls,
shape,
true,
usesList,
shapeTag,
null,
);
representations.add(info);
}
return RecordData._(_elementMap, representations, _gettersByShape);
}
bool _computeUsesGeneralClass(ClassEntity? cls) {
while (cls != null) {
if (cls == _elementMap.commonElements.recordGeneralBaseClass) return true;
cls = _elementMap.elementEnvironment.getSuperClass(cls);
}
return false;
}
}
// TODO(sra): Use a regular JClass with a different Definition?
class JRecordClass extends JClass {
/// Tag used for identifying serialized [JRecordClass] objects in a
/// debugging data stream.
static const String tag = 'record-class';
JRecordClass(super.library, super.name, {required super.isAbstract});
factory JRecordClass.readFromDataSource(DataSourceReader source) {
source.begin(tag);
JLibrary library = source.readLibrary() as JLibrary;
String name = source.readString();
bool isAbstract = source.readBool();
source.end(tag);
return JRecordClass(library, name, isAbstract: isAbstract);
}
@override
void writeToDataSink(DataSinkWriter sink) {
sink.writeEnum(JClassKind.record);
sink.begin(tag);
sink.writeLibrary(library);
sink.writeString(name);
sink.writeBool(isAbstract);
sink.end(tag);
}
@override
String toString() => '${jsElementPrefix}record_class($name)';
}
class RecordClassData implements JClassData {
/// Tag used for identifying serialized [RecordClassData] objects in a
/// debugging data stream.
static const String tag = 'record-class-data';
@override
final ClassDefinition definition;
@override
final InterfaceType? thisType;
@override
final OrderedTypeSet orderedTypeSet;
@override
final InterfaceType? supertype;
RecordClassData(
this.definition,
this.thisType,
this.supertype,
this.orderedTypeSet,
);
factory RecordClassData.readFromDataSource(DataSourceReader source) {
source.begin(tag);
ClassDefinition definition = ClassDefinition.readFromDataSource(source);
InterfaceType thisType = source.readDartType() as InterfaceType;
InterfaceType supertype = source.readDartType() as InterfaceType;
OrderedTypeSet orderedTypeSet = OrderedTypeSet.readFromDataSource(source);
source.end(tag);
return RecordClassData(definition, thisType, supertype, orderedTypeSet);
}
@override
void writeToDataSink(DataSinkWriter sink) {
sink.writeEnum(JClassDataKind.record);
sink.begin(tag);
definition.writeToDataSink(sink);
sink.writeDartType(thisType!);
sink.writeDartType(supertype!);
orderedTypeSet.writeToDataSink(sink);
sink.end(tag);
}
@override
bool get isMixinApplication => false;
@override
bool get isEnumClass => false;
@override
FunctionType? get callType => null;
@override
List<InterfaceType> get interfaces => const <InterfaceType>[];
@override
InterfaceType? get mixedInType => null;
@override
InterfaceType? get jsInteropType => thisType;
@override
InterfaceType? get rawType => thisType;
@override
InterfaceType? get instantiationToBounds => thisType;
@override
List<Variance> getVariances() => [];
}
class JRecordGetter extends JFunction {
/// Tag used for identifying serialized [JRecordGetter] objects in a
/// debugging data stream.
static const String tag = 'record-getter';
JRecordGetter(JClass enclosingClass, Name name)
: super(
enclosingClass.library,
enclosingClass,
name,
ParameterStructure.getter,
AsyncMarker.sync,
isStatic: false,
isExternal: false,
);
factory JRecordGetter.readFromDataSource(DataSourceReader source) {
source.begin(tag);
JClass enclosingClass = source.readClass() as JClass;
Name memberName = source.readMemberName();
source.end(tag);
return JRecordGetter(enclosingClass, memberName);
}
@override
void writeToDataSink(DataSinkWriter sink) {
sink.writeEnum(JMemberKind.recordGetter);
sink.begin(tag);
sink.writeClass(enclosingClass!);
sink.writeMemberName(memberName);
sink.end(tag);
}
@override
bool get isAbstract => false;
@override
bool get isGetter => true;
@override
String toString() =>
'${jsElementPrefix}record_getter'
'(${enclosingClass!.name}.$name)';
}
abstract class RecordMemberData implements JMemberData {
@override
final MemberDefinition definition;
final InterfaceType? memberThisType;
RecordMemberData(this.definition, this.memberThisType);
@override
InterfaceType? getMemberThisType(covariant JsToElementMap elementMap) {
return memberThisType;
}
}
class RecordGetterData extends RecordMemberData implements FunctionData {
/// Tag used for identifying serialized [RecordGetterData] objects in a
/// debugging data stream.
static const String tag = 'record-getter-data';
final FunctionType functionType;
RecordGetterData(super.definition, super.memberThisType, this.functionType);
RecordGetterData._deserialized(
super.definition,
super.memberThisType,
this.functionType,
);
factory RecordGetterData.readFromDataSource(DataSourceReader source) {
source.begin(tag);
MemberDefinition definition = MemberDefinition.readFromDataSource(source);
InterfaceType? memberThisType =
source.readDartTypeOrNull() as InterfaceType?;
FunctionType functionType = source.readDartType() as FunctionType;
source.end(tag);
return RecordGetterData._deserialized(
definition,
memberThisType,
functionType,
);
}
@override
void writeToDataSink(DataSinkWriter sink) {
sink.writeEnum(JMemberDataKind.recordGetter);
sink.begin(tag);
definition.writeToDataSink(sink);
sink.writeDartTypeOrNull(memberThisType);
sink.writeDartType(functionType);
sink.end(tag);
}
@override
ClassTypeVariableAccess get classTypeVariableAccess =>
ClassTypeVariableAccess.none;
@override
List<TypeVariableType> getFunctionTypeVariables(
covariant JsKernelToElementMap unusedElementMap,
) {
return const <TypeVariableType>[];
}
@override
void forEachParameter(
JsToElementMap elementMap,
ParameterStructure parameterStructure,
void Function(DartType type, String? name, ConstantValue? defaultValue) f, {
bool isNative = false,
}) {}
@override
// It is a bit of a code-smell here that an synthetic element introduced
// during creation of the K-world depends on Kernel. Perhaps it would be
// better to compute this type and serialize it for all functions, although
// that is redundant with the Kernel IR for most functions.
FunctionType getFunctionType(IrToElementMap unusedElementMap) {
return functionType;
}
}