blob: ad44998934dc9ec9023421de93407e45c3be5631 [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 dart2js.js_model.records;
import '../common.dart';
import '../elements/entities.dart';
import '../elements/names.dart';
import '../elements/types.dart';
import '../js_backend/annotations.dart';
import '../js_model/element_map.dart';
import '../ordered_typeset.dart';
import '../serialization/serialization.dart';
import '../universe/record_shape.dart';
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<ClassEntity, RecordRepresentation> _classToRepresentation = {};
final Map<RecordShape, RecordRepresentation> _shapeToRepresentation = {};
RecordData._(this._elementMap, this._representations) {
// Unpack representations into lookup maps.
for (final info in _representations) {
_classToRepresentation[info.cls] = info;
if (info.definesShape) _shapeToRepresentation[info.shape] = info;
}
}
factory RecordData.readFromDataSource(
JsToElementMap elementMap, DataSourceReader source) {
source.begin(tag);
List<RecordRepresentation> representations =
source.readList(() => RecordRepresentation.readFromDataSource(source));
source.end(tag);
return RecordData._(elementMap, representations);
}
/// Serializes this [RecordData] to [sink].
void writeToDataSink(DataSinkWriter sink) {
sink.begin(tag);
sink.writeList<RecordRepresentation>(
_representations, (info) => info.writeToDataSink(sink));
sink.end(tag);
}
/// Returns a fresh List of representations that define shapes.
List<RecordRepresentation> representationsForShapes() =>
[..._shapeToRepresentation.values];
RecordRepresentation representationForShape(RecordShape shape) {
return _shapeToRepresentation[shape] ??
(throw StateError('representationForShape $shape'));
}
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);
}
/// 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.
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;
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 cls = shape.fieldCount == 0
? _elementMap.commonElements.emptyRecordClass
: closedWorldBuilder.buildRecordShapeClass(shape);
int shapeTag = i;
bool usesList = _computeUsesGeneralClass(cls);
final info =
RecordRepresentation._(cls, shape, true, usesList, shapeTag, null);
representations.add(info);
}
return RecordData._(
_elementMap,
representations,
);
}
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() => [];
}