Version 3.0.0-233.0.dev
Merge 95e6dba347b3db04f7cf4958032db9610df5084a into dev
diff --git a/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart b/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart
index 7b4046f..4c9fded 100644
--- a/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart
+++ b/pkg/_fe_analyzer_shared/test/flow_analysis/flow_analysis_test.dart
@@ -8080,6 +8080,99 @@
});
});
+ group('Wildcard pattern:', () {
+ group('covers matched type:', () {
+ test('without promotion candidate', () {
+ // In `if(<some int> case num _) ...`, the `else` branch should be
+ // unreachable because the type `num` fully covers the type `int`.
+ h.run([
+ ifCase(expr('int'), wildcard(type: 'num'), [
+ checkReachable(true),
+ ], [
+ checkReachable(false),
+ ]),
+ ]);
+ });
+
+ test('with promotion candidate', () {
+ // In `if(x case num _) ...`, the `else` branch should be unreachable
+ // because the type `num` fully covers the type `int`.
+ var x = Var('x');
+ h.run([
+ declare(x, type: 'int'),
+ ifCase(x.expr, wildcard(type: 'num'), [
+ checkReachable(true),
+ checkNotPromoted(x),
+ ], [
+ checkReachable(false),
+ checkNotPromoted(x),
+ ]),
+ ]);
+ });
+ });
+
+ group("doesn't cover matched type:", () {
+ test('without promotion candidate', () {
+ // In `if(<some num> case int _) ...`, the `else` branch should be
+ // reachable because the type `int` doesn't fully cover the type
+ // `num`.
+ h.run([
+ ifCase(expr('num'), wildcard(type: 'int'), [
+ checkReachable(true),
+ ], [
+ checkReachable(true),
+ ]),
+ ]);
+ });
+
+ group('with promotion candidate:', () {
+ test('without factor', () {
+ var x = Var('x');
+ h.run([
+ declare(x, type: 'num'),
+ ifCase(x.expr, wildcard(type: 'int'), [
+ checkReachable(true),
+ checkPromoted(x, 'int'),
+ ], [
+ checkReachable(true),
+ checkNotPromoted(x),
+ ]),
+ ]);
+ });
+
+ test('with factor', () {
+ var x = Var('x');
+ h.run([
+ declare(x, type: 'int?'),
+ ifCase(x.expr, wildcard(type: 'Null'), [
+ checkReachable(true),
+ checkPromoted(x, 'Null'),
+ ], [
+ checkReachable(true),
+ checkPromoted(x, 'int'),
+ ]),
+ ]);
+ });
+ });
+ });
+
+ test("Subpattern doesn't promote scrutinee", () {
+ var x = Var('x');
+ h.run([
+ declare(x, initializer: expr('Object')),
+ ifCase(
+ x.expr,
+ objectPattern(
+ requiredType: 'num',
+ fields: [wildcard(type: 'int').recordField('sign')]),
+ [
+ checkPromoted(x, 'num'),
+ // TODO(paulberry): should promote `x.sign` to `int`.
+ ]),
+ ]);
+ });
+ });
+
test('Pattern inside guard', () {
// Roughly equivalent Dart code:
// FutureOr<int> x = ...;
diff --git a/pkg/dart2wasm/lib/class_info.dart b/pkg/dart2wasm/lib/class_info.dart
index 911f715..07fe27e 100644
--- a/pkg/dart2wasm/lib/class_info.dart
+++ b/pkg/dart2wasm/lib/class_info.dart
@@ -35,6 +35,8 @@
static const typeIsDeclaredNullable = 2;
static const interfaceTypeTypeArguments = 4;
static const functionTypeNamedParameters = 8;
+ static const recordTypeNames = 3;
+ static const recordTypeFieldTypes = 4;
static const typedListBaseLength = 2;
static const typedListArray = 3;
static const typedListViewTypedData = 3;
@@ -47,6 +49,7 @@
static const suspendStateTargetIndex = 6;
static const syncStarIteratorCurrent = 3;
static const syncStarIteratorYieldStarIterable = 4;
+ static const recordFieldBase = 2;
static void validate(Translator translator) {
void check(Class cls, String name, int expectedIndex) {
@@ -73,6 +76,9 @@
FieldIndex.interfaceTypeTypeArguments);
check(translator.functionTypeClass, "namedParameters",
FieldIndex.functionTypeNamedParameters);
+ check(translator.recordTypeClass, "names", FieldIndex.recordTypeNames);
+ check(translator.recordTypeClass, "fieldTypes",
+ FieldIndex.recordTypeFieldTypes);
check(translator.suspendStateClass, "_iterator",
FieldIndex.suspendStateIterator);
check(translator.suspendStateClass, "_context",
@@ -174,6 +180,10 @@
int _nextClassId = 0;
late final ClassInfo topInfo;
+ /// Maps number of record fields to the struct type to be used for a record
+ /// shape class with that many fields.
+ final Map<int, w.StructType> _recordStructs = {};
+
/// Wasm field type for fields with type [_Type]. Fields of this type are
/// added to classes for type parameters.
///
@@ -282,6 +292,26 @@
translator.classForHeapType.putIfAbsent(info.struct, () => info!);
}
+ void _initializeRecordClass(Class cls) {
+ final numFields = cls.fields.length;
+
+ final struct = _recordStructs.putIfAbsent(
+ numFields,
+ () => m.addStructType(
+ 'Record$numFields',
+ superType: translator.recordInfo.struct,
+ ));
+
+ final ClassInfo superInfo = translator.recordInfo;
+
+ final info =
+ ClassInfo(cls, _nextClassId++, superInfo.depth + 1, struct, superInfo);
+
+ translator.classes.add(info);
+ translator.classInfo[cls] = info;
+ translator.classForHeapType.putIfAbsent(info.struct, () => info);
+ }
+
void _computeRepresentation(ClassInfo info) {
info.repr = upperBound(info.implementedBy);
}
@@ -333,6 +363,30 @@
}
}
+ void _generateRecordFields(ClassInfo info) {
+ final struct = info.struct;
+ final ClassInfo superInfo = info.superInfo!;
+ assert(identical(superInfo, translator.recordInfo));
+
+ // Different record classes can share the same struct, check if the struct
+ // is already initialized
+ if (struct.fields.isEmpty) {
+ // Copy fields from superclass
+ for (w.FieldType fieldType in superInfo.struct.fields) {
+ info._addField(fieldType);
+ }
+
+ for (Field _ in info.cls!.fields) {
+ info._addField(w.FieldType(topInfo.nullableType));
+ }
+ }
+
+ int fieldIdx = superInfo.struct.fields.length;
+ for (Field field in info.cls!.fields) {
+ translator.fieldIndex[field] = fieldIdx++;
+ }
+ }
+
/// Create class info and Wasm struct for all classes.
void collect() {
_initializeTop();
@@ -347,9 +401,18 @@
// parameters.
_initialize(translator.typeClass);
+ // Initialize the record base class if we have record classes.
+ if (translator.recordClasses.isNotEmpty) {
+ _initialize(translator.coreTypes.recordClass);
+ }
+
for (Library library in translator.component.libraries) {
for (Class cls in library.classes) {
- _initialize(cls);
+ if (cls.superclass == translator.coreTypes.recordClass) {
+ _initializeRecordClass(cls);
+ } else {
+ _initialize(cls);
+ }
}
}
@@ -364,7 +427,11 @@
// Now that the representation types for all classes have been computed,
// fill in the types of the fields in the generated Wasm structs.
for (ClassInfo info in translator.classes) {
- _generateFields(info);
+ if (info.superInfo == translator.recordInfo) {
+ _generateRecordFields(info);
+ } else {
+ _generateFields(info);
+ }
}
// Add hidden fields of typed_data classes.
diff --git a/pkg/dart2wasm/lib/code_generator.dart b/pkg/dart2wasm/lib/code_generator.dart
index 8f2172f..271194b 100644
--- a/pkg/dart2wasm/lib/code_generator.dart
+++ b/pkg/dart2wasm/lib/code_generator.dart
@@ -8,6 +8,7 @@
import 'package:dart2wasm/dynamic_forwarders.dart';
import 'package:dart2wasm/intrinsics.dart';
import 'package:dart2wasm/param_info.dart';
+import 'package:dart2wasm/records.dart';
import 'package:dart2wasm/reference_extensions.dart';
import 'package:dart2wasm/sync_star.dart';
import 'package:dart2wasm/translator.dart';
@@ -1319,7 +1320,11 @@
late final w.ValueType nullableType;
late final w.ValueType nonNullableType;
late final void Function() compare;
- if (node.cases.every((c) => c.expressions.isEmpty && c.isDefault)) {
+ if (node.cases.every((c) =>
+ c.expressions.isEmpty && c.isDefault ||
+ c.expressions.every((e) =>
+ e is NullLiteral ||
+ e is ConstantExpression && e.constant is NullConstant))) {
// default-only switch
nonNullableType = w.RefType.eq(nullable: false);
nullableType = w.RefType.eq(nullable: true);
@@ -2864,6 +2869,55 @@
return nonNullableTypeType;
}
+ @override
+ w.ValueType visitRecordLiteral(RecordLiteral node, w.ValueType expectedType) {
+ final ClassInfo recordClassInfo =
+ translator.getRecordClassInfo(node.recordType);
+ translator.functions.allocateClass(recordClassInfo.classId);
+
+ b.i32_const(recordClassInfo.classId);
+ b.i32_const(initialIdentityHash);
+ for (Expression positional in node.positional) {
+ wrap(positional, translator.topInfo.nullableType);
+ }
+ for (NamedExpression named in node.named) {
+ wrap(named.value, translator.topInfo.nullableType);
+ }
+ b.struct_new(recordClassInfo.struct);
+
+ return recordClassInfo.nonNullableType;
+ }
+
+ @override
+ w.ValueType visitRecordIndexGet(
+ RecordIndexGet node, w.ValueType expectedType) {
+ final RecordShape recordShape = RecordShape.fromType(node.receiverType);
+ final ClassInfo recordClassInfo =
+ translator.getRecordClassInfo(node.receiverType);
+ translator.functions.allocateClass(recordClassInfo.classId);
+
+ wrap(node.receiver, translator.topInfo.nonNullableType);
+ b.ref_cast(w.RefType(recordClassInfo.struct, nullable: false));
+ b.struct_get(
+ recordClassInfo.struct, recordShape.getPositionalIndex(node.index));
+
+ return translator.topInfo.nullableType;
+ }
+
+ @override
+ w.ValueType visitRecordNameGet(RecordNameGet node, w.ValueType expectedType) {
+ final RecordShape recordShape = RecordShape.fromType(node.receiverType);
+ final ClassInfo recordClassInfo =
+ translator.getRecordClassInfo(node.receiverType);
+ translator.functions.allocateClass(recordClassInfo.classId);
+
+ wrap(node.receiver, translator.topInfo.nonNullableType);
+ b.ref_cast(w.RefType(recordClassInfo.struct, nullable: false));
+ b.struct_get(recordClassInfo.struct, recordShape.getNameIndex(node.name));
+
+ return translator.topInfo.nullableType;
+ }
+
/// Generate type checker method for a setter.
///
/// This function will be called by a setter forwarder in a dynamic set to
diff --git a/pkg/dart2wasm/lib/compile.dart b/pkg/dart2wasm/lib/compile.dart
index 4570716..4029b8c 100644
--- a/pkg/dart2wasm/lib/compile.dart
+++ b/pkg/dart2wasm/lib/compile.dart
@@ -31,6 +31,8 @@
import 'package:dart2wasm/compiler_options.dart' as compiler;
import 'package:dart2wasm/js_runtime_generator.dart';
+import 'package:dart2wasm/record_class_generator.dart';
+import 'package:dart2wasm/records.dart';
import 'package:dart2wasm/target.dart';
import 'package:dart2wasm/translator.dart';
@@ -91,6 +93,9 @@
JSRuntimeFinalizer jsRuntimeFinalizer =
createJSRuntimeFinalizer(component, coreTypes, classHierarchy);
+ final Map<RecordShape, Class> recordClasses =
+ generateRecordClasses(component, coreTypes);
+
globalTypeFlow.transformComponent(target, coreTypes, component,
treeShakeSignatures: true,
treeShakeWriteOnlyFields: true,
@@ -102,7 +107,8 @@
return true;
}());
- var translator = Translator(component, coreTypes, options.translatorOptions);
+ var translator = Translator(
+ component, coreTypes, recordClasses, options.translatorOptions);
String? depFile = options.depFile;
if (depFile != null) {
diff --git a/pkg/dart2wasm/lib/constants.dart b/pkg/dart2wasm/lib/constants.dart
index a26fa3e..3891551 100644
--- a/pkg/dart2wasm/lib/constants.dart
+++ b/pkg/dart2wasm/lib/constants.dart
@@ -789,6 +789,28 @@
b.i64_const(environmentIndex);
b.struct_new(info.struct);
});
+ } else if (type is RecordType) {
+ final names = ListConstant(
+ InterfaceType(
+ translator.coreTypes.stringClass, Nullability.nonNullable),
+ type.named.map((t) => StringConstant(t.name)).toList());
+ ensureConstant(names);
+ final fieldTypes = constants.makeTypeList(
+ type.positional.followedBy(type.named.map((n) => n.type)).toList());
+ ensureConstant(fieldTypes);
+ return createConstant(constant, info.nonNullableType, (function, b) {
+ b.i32_const(info.classId);
+ b.i32_const(initialIdentityHash);
+ b.i32_const(types.encodedNullability(type));
+ final namesExpectedType =
+ info.struct.fields[FieldIndex.recordTypeNames].type.unpacked;
+ constants.instantiateConstant(function, b, names, namesExpectedType);
+ final typeListExpectedType =
+ info.struct.fields[FieldIndex.recordTypeFieldTypes].type.unpacked;
+ constants.instantiateConstant(
+ function, b, fieldTypes, typeListExpectedType);
+ b.struct_new(info.struct);
+ });
} else {
assert(type is VoidType ||
type is NeverType ||
@@ -819,4 +841,29 @@
b.struct_new(info.struct);
});
}
+
+ @override
+ ConstantInfo? visitRecordConstant(RecordConstant constant) {
+ final ClassInfo recordClassInfo =
+ translator.getRecordClassInfo(constant.recordType);
+ translator.functions.allocateClass(recordClassInfo.classId);
+
+ final List<Constant> arguments = constant.positional.toList();
+ arguments.addAll(constant.named.values);
+
+ for (Constant argument in arguments) {
+ ensureConstant(argument);
+ }
+
+ return createConstant(constant, recordClassInfo.nonNullableType,
+ lazy: false, (function, b) {
+ b.i32_const(recordClassInfo.classId);
+ b.i32_const(initialIdentityHash);
+ for (Constant argument in arguments) {
+ constants.instantiateConstant(
+ function, b, argument, translator.topInfo.nullableType);
+ }
+ b.struct_new(recordClassInfo.struct);
+ });
+ }
}
diff --git a/pkg/dart2wasm/lib/dispatch_table.dart b/pkg/dart2wasm/lib/dispatch_table.dart
index 1ac87a6..6bed33a 100644
--- a/pkg/dart2wasm/lib/dispatch_table.dart
+++ b/pkg/dart2wasm/lib/dispatch_table.dart
@@ -250,10 +250,16 @@
final cls = member.enclosingClass;
final isWasmType = cls != null && translator.isWasmType(cls);
+ // TODO(51363): Dynamic call metadata is not accurate for record fields, so
+ // we consider them to be dynamically called.
+ final isRecordMember =
+ member.enclosingClass?.superclass == translator.recordInfo.cls;
+
final calledDynamically = !isWasmType &&
- (metadata.getterCalledDynamically ||
- metadata.methodOrSetterCalledDynamically ||
- member.name.text == "call");
+ (metadata.getterCalledDynamically ||
+ metadata.methodOrSetterCalledDynamically ||
+ member.name.text == "call") ||
+ isRecordMember;
final selector = _selectorInfo.putIfAbsent(
selectorId,
diff --git a/pkg/dart2wasm/lib/kernel_nodes.dart b/pkg/dart2wasm/lib/kernel_nodes.dart
index b6f1932..7b3e869 100644
--- a/pkg/dart2wasm/lib/kernel_nodes.dart
+++ b/pkg/dart2wasm/lib/kernel_nodes.dart
@@ -77,6 +77,7 @@
late final Class stackTraceClass = index.getClass("dart:core", "StackTrace");
late final Class typeUniverseClass =
index.getClass("dart:core", "_TypeUniverse");
+ late final Class recordTypeClass = index.getClass("dart:core", "_RecordType");
// dart:core sync* support classes
late final Class suspendStateClass =
diff --git a/pkg/dart2wasm/lib/record_class_generator.dart b/pkg/dart2wasm/lib/record_class_generator.dart
new file mode 100644
index 0000000..0d7ecbb
--- /dev/null
+++ b/pkg/dart2wasm/lib/record_class_generator.dart
@@ -0,0 +1,442 @@
+// 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:dart2wasm/records.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')
+/// _Type get _runtimeType =>
+/// // Uses of `runtimeType` below will be fixed with #51134.
+/// _RecordType(
+/// const ["a"],
+/// [$1.runtimeType, $2.runtimeType, a.runtimeType]);
+///
+/// @pragma('wasm:entry-point')
+/// Type get runtimeType => _runtimeType;
+///
+/// @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);
+ final visitor = _RecordVisitor(recordClassGenerator);
+ component.libraries.forEach(visitor.visitLibrary);
+ return recordClasses;
+}
+
+class _RecordClassGenerator {
+ final CoreTypes coreTypes;
+ final Map<RecordShape, Class> classes;
+
+ late final Class recordRuntimeTypeClass =
+ coreTypes.index.getClass('dart:core', '_RecordType');
+
+ late final Class internalRuntimeTypeClass =
+ coreTypes.index.getClass('dart:core', '_Type');
+
+ 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 objectRuntimeTypeProcedure =
+ coreTypes.index.getProcedure('dart:core', 'Object', 'get:runtimeType');
+
+ late final Procedure objectToStringProcedure =
+ coreTypes.index.getProcedure('dart:core', 'Object', 'toString');
+
+ late final Procedure objectEqualsProcedure = coreTypes.objectEquals;
+
+ late final Procedure stringPlusProcedure =
+ coreTypes.index.getProcedure('dart:core', 'String', '+');
+
+ DartType get nullableObjectType => coreTypes.objectNullableRawType;
+
+ DartType get internalRuntimeTypeType =>
+ InterfaceType(internalRuntimeTypeClass, Nullability.nonNullable);
+
+ DartType get nonNullableStringType => coreTypes.stringNonNullableRawType;
+
+ DartType get boolType => coreTypes.boolNonNullableRawType;
+
+ DartType get intType => coreTypes.intNonNullableRawType;
+
+ DartType get runtimeTypeType => coreTypes.typeNonNullableRawType;
+
+ Library get library => coreTypes.coreLibrary;
+
+ _RecordClassGenerator(this.classes, this.coreTypes);
+
+ void generateClassForRecordType(RecordType recordType) {
+ final shape = RecordShape.fromType(recordType);
+ final id = classes.length;
+ classes.putIfAbsent(shape, () => _generateClass(shape, id));
+ }
+
+ /// Add a `@pragma('wasm:entry-point')` annotation to an annotatable.
+ T _addWasmEntryPointPragma<T extends Annotatable>(T node) => node
+ ..addAnnotation(ConstantExpression(
+ InstanceConstant(coreTypes.pragmaClass.reference, [], {
+ coreTypes.pragmaName.fieldReference: StringConstant("wasm:entry-point"),
+ coreTypes.pragmaOptions.fieldReference: NullConstant(),
+ })));
+
+ 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,
+ ));
+ library.addClass(cls);
+ cls.addProcedure(_generateEquals(shape, fields, cls));
+ final internalRuntimeType = _generateInternalRuntimeType(shape, fields);
+ // With `_runtimeType` we also need to override `runtimeType`, as
+ // `Object.runtimeType` is implemented as a direct call to
+ // `Object._runtimeType` instead of a virtual call.
+ final runtimeType = _generateRuntimeType(internalRuntimeType);
+ cls.addProcedure(internalRuntimeType);
+ cls.addProcedure(runtimeType);
+ 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,
+ )));
+ }
+
+ for (String name in shape.names) {
+ fields.add(_addWasmEntryPointPragma(Field.immutable(
+ Name(name, library),
+ isFinal: true,
+ fileUri: library.fileUri,
+ )));
+ }
+
+ return fields;
+ }
+
+ /// Generate a constructor with name `_`. Named fields are passed in sorted
+ /// order.
+ Constructor _generateConstructor(RecordShape shape, List<Field> fields) {
+ final List<VariableDeclaration> positionalParameters =
+ List.generate(fields.length, (i) => VariableDeclaration('field$i'));
+
+ 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));
+ }
+
+ /// 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,
+ ));
+ }
+
+ /// 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)),
+ fileUri: library.fileUri,
+ ));
+ }
+
+ /// Generate `bool operator ==` member.
+ Procedure _generateEquals(RecordShape shape, List<Field> fields, Class cls) {
+ final equalsFunctionType = FunctionType(
+ [nullableObjectType],
+ boolType,
+ Nullability.nonNullable,
+ );
+
+ final VariableDeclaration parameter =
+ VariableDeclaration('other', type: nullableObjectType);
+
+ 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,
+ ));
+ }
+
+ /// Generate `_Type get _runtimeType` member.
+ Procedure _generateInternalRuntimeType(
+ RecordShape shape, List<Field> fields) {
+ final List<Statement> statements = [];
+
+ // const ["name1", "name2", ...]
+ final fieldNamesList = ConstantExpression(ListConstant(
+ nonNullableStringType,
+ shape.names.map((name) => StringConstant(name)).toList()));
+
+ // Generate `this.field.runtimeType` for a given field.
+ // TODO(51134): We shouldn't use user-provided runtimeType below.
+ Expression fieldRuntimeTypeExpr(Field field) => InstanceGet(
+ InstanceAccessKind.Object,
+ InstanceGet(InstanceAccessKind.Instance, ThisExpression(), field.name,
+ interfaceTarget: field, resultType: nullableObjectType),
+ objectRuntimeTypeProcedure.name,
+ interfaceTarget: objectRuntimeTypeProcedure,
+ resultType: runtimeTypeType,
+ );
+
+ // [this.$1.runtimeType, this.x.runtimeType, ...]
+ final fieldTypesList = ListLiteral(
+ fields.map(fieldRuntimeTypeExpr).toList(),
+ typeArgument: 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('_runtimeType', library),
+ ProcedureKind.Getter,
+ function,
+ fileUri: library.fileUri,
+ ));
+ }
+
+ /// Generate `Type get runtimeType member = _runtimeType;`.
+ Procedure _generateRuntimeType(Procedure internalRuntimeType) {
+ final FunctionNode function = FunctionNode(
+ ReturnStatement(InstanceGet(InstanceAccessKind.Instance, ThisExpression(),
+ internalRuntimeType.name,
+ interfaceTarget: internalRuntimeType,
+ resultType: internalRuntimeTypeType)),
+ positionalParameters: [],
+ returnType: runtimeTypeType,
+ );
+
+ return _addWasmEntryPointPragma(Procedure(
+ Name('runtimeType', library),
+ ProcedureKind.Getter,
+ function,
+ fileUri: library.fileUri,
+ ));
+ }
+}
+
+class _RecordVisitor extends RecursiveVisitor<void> {
+ 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);
+ }
+ }
+}
diff --git a/pkg/dart2wasm/lib/records.dart b/pkg/dart2wasm/lib/records.dart
new file mode 100644
index 0000000..f658f16
--- /dev/null
+++ b/pkg/dart2wasm/lib/records.dart
@@ -0,0 +1,76 @@
+// 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 'dart:collection' show SplayTreeMap;
+
+import 'package:dart2wasm/class_info.dart';
+
+import 'package:kernel/ast.dart';
+
+/// Describes shape of a record as the number of positionals + set of field
+/// names.
+class RecordShape {
+ /// Number of positional fields.
+ final int positionals;
+
+ /// Maps names of the named fields in the record to their indices in the
+ /// record payload.
+ final SplayTreeMap<String, int> _names;
+
+ /// Names of named fields, sorted.
+ Iterable<String> get names => _names.keys;
+
+ /// Total number of fields.
+ int get numFields => positionals + _names.length;
+
+ RecordShape.fromType(RecordType recordType)
+ : positionals = recordType.positional.length,
+ // RecordType.named is already sorted
+ _names = SplayTreeMap.fromIterables(
+ recordType.named.map((ty) => ty.name),
+ Iterable.generate(recordType.named.length,
+ (i) => i + recordType.positional.length));
+
+ @override
+ String toString() => 'Record(positionals: $positionals, names: $_names)';
+
+ @override
+ bool operator ==(Object other) {
+ if (other is! RecordShape) {
+ return false;
+ }
+
+ if (positionals != other.positionals) {
+ return false;
+ }
+
+ if (_names.length != other._names.length) {
+ return false;
+ }
+
+ final names1Iter = _names.keys.iterator;
+ final names2Iter = other._names.keys.iterator;
+ while (names1Iter.moveNext()) {
+ names2Iter.moveNext();
+ if (names1Iter.current != names2Iter.current) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ @override
+ int get hashCode => Object.hash(positionals, Object.hashAll(_names.keys));
+
+ /// Struct index of a positional field.
+ int getPositionalIndex(int position) => FieldIndex.recordFieldBase + position;
+
+ /// Struct index of a named field.
+ int getNameIndex(String name) =>
+ FieldIndex.recordFieldBase +
+ (_names[name] ??
+ (throw 'RecordImplementation.getNameIndex: '
+ 'name $name not in record: $this'));
+}
diff --git a/pkg/dart2wasm/lib/translator.dart b/pkg/dart2wasm/lib/translator.dart
index f8983c5..31e30a6 100644
--- a/pkg/dart2wasm/lib/translator.dart
+++ b/pkg/dart2wasm/lib/translator.dart
@@ -14,6 +14,7 @@
import 'package:dart2wasm/globals.dart';
import 'package:dart2wasm/kernel_nodes.dart';
import 'package:dart2wasm/param_info.dart';
+import 'package:dart2wasm/records.dart';
import 'package:dart2wasm/reference_extensions.dart';
import 'package:dart2wasm/types.dart';
@@ -99,6 +100,10 @@
late final w.Memory ffiMemory = m.importMemory("ffi", "memory",
options.importSharedMemory, 0, options.sharedMemoryMaxPages);
+ /// Maps record shapes to the record class for the shape. Classes generated
+ /// by `record_class_generator` library.
+ final Map<RecordShape, Class> recordClasses;
+
// Caches for when identical source constructs need a common representation.
final Map<w.StorageType, w.ArrayType> arrayTypeCache = {};
final Map<w.BaseFunction, w.DefinedGlobal> functionRefCache = {};
@@ -109,6 +114,7 @@
late final ClassInfo objectInfo = classInfo[coreTypes.objectClass]!;
late final ClassInfo closureInfo = classInfo[closureClass]!;
late final ClassInfo stackTraceInfo = classInfo[stackTraceClass]!;
+ late final ClassInfo recordInfo = classInfo[coreTypes.recordClass]!;
late final w.ArrayType listArrayType = (classInfo[listBaseClass]!
.struct
.fields[FieldIndex.listArray]
@@ -203,7 +209,7 @@
topInfo.nullableType
]);
- Translator(this.component, this.coreTypes, this.options)
+ Translator(this.component, this.coreTypes, this.recordClasses, this.options)
: libraries = component.libraries,
hierarchy =
ClassHierarchy(component, coreTypes) as ClosedWorldClassHierarchy {
@@ -551,6 +557,9 @@
if (type is InlineType) {
return translateStorageType(type.instantiatedRepresentationType);
}
+ if (type is RecordType) {
+ return typeForInfo(getRecordClassInfo(type), type.isPotentiallyNullable);
+ }
throw "Unsupported type ${type.runtimeType}";
}
@@ -964,6 +973,9 @@
b.struct_get(info.struct, FieldIndex.listLength);
b.i32_wrap_i64();
}
+
+ ClassInfo getRecordClassInfo(RecordType recordType) =>
+ classInfo[recordClasses[RecordShape.fromType(recordType)]!]!;
}
abstract class _FunctionGenerator {
diff --git a/pkg/dart2wasm/lib/types.dart b/pkg/dart2wasm/lib/types.dart
index 4fa44c8..3aebee1 100644
--- a/pkg/dart2wasm/lib/types.dart
+++ b/pkg/dart2wasm/lib/types.dart
@@ -46,6 +46,10 @@
late final w.ValueType namedParametersExpectedType = classAndFieldToType(
translator.functionTypeClass, FieldIndex.functionTypeNamedParameters);
+ /// Wasm value type of `_RecordType.names` field.
+ late final w.ValueType recordTypeNamesFieldExpectedType = classAndFieldToType(
+ translator.recordTypeClass, FieldIndex.recordTypeNames);
+
/// A mapping from concrete subclass `classID` to [Map]s of superclass
/// `classID` and the necessary substitutions which must be performed to test
/// for a valid subtyping relationship.
@@ -269,6 +273,9 @@
type.positionalParameters.every(_isTypeConstant) &&
type.namedParameters.every((n) => _isTypeConstant(n.type))) ||
type is InterfaceType && type.typeArguments.every(_isTypeConstant) ||
+ (type is RecordType &&
+ type.positional.every(_isTypeConstant) &&
+ type.named.every((n) => _isTypeConstant(n.type))) ||
type is TypeParameterType && isFunctionTypeParameter(type) ||
type is InlineType &&
_isTypeConstant(type.instantiatedRepresentationType);
@@ -303,6 +310,8 @@
}
} else if (type is InlineType) {
return classForType(type.instantiatedRepresentationType);
+ } else if (type is RecordType) {
+ return translator.recordTypeClass;
}
throw "Unexpected DartType: $type";
}
@@ -322,6 +331,21 @@
_makeTypeList(codeGen, type.typeArguments);
}
+ void _makeRecordType(CodeGenerator codeGen, RecordType type) {
+ codeGen.b.i32_const(encodedNullability(type));
+ translator.constants.instantiateConstant(
+ codeGen.function,
+ codeGen.b,
+ ListConstant(
+ InterfaceType(
+ translator.coreTypes.stringClass, Nullability.nonNullable),
+ type.named.map((t) => StringConstant(t.name)).toList(),
+ ),
+ recordTypeNamesFieldExpectedType);
+ _makeTypeList(codeGen,
+ type.positional.followedBy(type.named.map((t) => t.type)).toList());
+ }
+
/// Normalizes a Dart type. Many rules are already applied for us, but we
/// still have to manually normalize [FutureOr].
DartType normalize(DartType type) {
@@ -422,7 +446,8 @@
type is InlineType ||
type is InterfaceType ||
type is FutureOrType ||
- type is FunctionType);
+ type is FunctionType ||
+ type is RecordType);
if (type is TypeParameterType) {
assert(!isFunctionTypeParameter(type));
codeGen.instantiateTypeParameter(type.parameter);
@@ -449,6 +474,8 @@
_makeInterfaceType(codeGen, type);
} else if (type is FunctionType) {
_makeFunctionType(codeGen, type);
+ } else if (type is RecordType) {
+ _makeRecordType(codeGen, type);
} else {
throw '`$type` should have already been handled.';
}
diff --git a/pkg/dds/CHANGELOG.md b/pkg/dds/CHANGELOG.md
index 91a7c0c..108e830 100644
--- a/pkg/dds/CHANGELOG.md
+++ b/pkg/dds/CHANGELOG.md
@@ -1,3 +1,6 @@
+# 2.7.5
+- Updated `vm_service` version to >=9.0.0 <12.0.0.
+
# 2.7.4
- [DAP] Added support for `,d` (decimal), `,h` (hex) and `,nq` (no quotes) format specifiers to be used as suffixes to evaluation requests.
- [DAP] Added support for `format.hex` in `variablesRequest` and `evaluateRequest`.
diff --git a/pkg/dds/pubspec.yaml b/pkg/dds/pubspec.yaml
index f297d5f..0c386bf 100644
--- a/pkg/dds/pubspec.yaml
+++ b/pkg/dds/pubspec.yaml
@@ -1,5 +1,5 @@
name: dds
-version: 2.7.4
+version: 2.7.5
description: >-
A library used to spawn the Dart Developer Service, used to communicate with
a Dart VM Service instance.
@@ -26,7 +26,7 @@
sse: ^4.0.0
stack_trace: ^1.10.0
stream_channel: ^2.0.0
- vm_service: '>=9.0.0 <11.0.0'
+ vm_service: '>=9.0.0 <12.0.0'
web_socket_channel: ^2.0.0
# We use 'any' version constraints here as we get our package versions from
diff --git a/sdk/lib/_internal/wasm/lib/class_id.dart b/sdk/lib/_internal/wasm/lib/class_id.dart
index 9be0749..6b247f5 100644
--- a/sdk/lib/_internal/wasm/lib/class_id.dart
+++ b/sdk/lib/_internal/wasm/lib/class_id.dart
@@ -42,6 +42,8 @@
external static int get cidGrowableList;
@pragma("wasm:class-id", "dart.core#_ImmutableList")
external static int get cidImmutableList;
+ @pragma("wasm:class-id", "dart.core#Record")
+ external static int get cidRecord;
// Class IDs for RTI Types.
@pragma("wasm:class-id", "dart.core#_NeverType")
@@ -62,6 +64,8 @@
external static int get cidFunctionTypeParameterType;
@pragma("wasm:class-id", "dart.core#_InterfaceTypeParameterType")
external static int get cidInterfaceTypeParameterType;
+ @pragma("wasm:class-id", "dart.core#_RecordType")
+ external static int get cidRecordType;
// Dummy, only used by VM-specific hash table code.
static final int numPredefinedCids = 1;
diff --git a/sdk/lib/_internal/wasm/lib/core_patch.dart b/sdk/lib/_internal/wasm/lib/core_patch.dart
index 487d58a..5bad2fc 100644
--- a/sdk/lib/_internal/wasm/lib/core_patch.dart
+++ b/sdk/lib/_internal/wasm/lib/core_patch.dart
@@ -63,6 +63,7 @@
part "list.dart";
part "named_parameters.dart";
part "object_patch.dart";
+part "record_patch.dart";
part "regexp_patch.dart";
part "stack_trace_patch.dart";
part "stopwatch_patch.dart";
diff --git a/sdk/lib/_internal/wasm/lib/record_patch.dart b/sdk/lib/_internal/wasm/lib/record_patch.dart
new file mode 100644
index 0000000..7b78474
--- /dev/null
+++ b/sdk/lib/_internal/wasm/lib/record_patch.dart
@@ -0,0 +1,10 @@
+// 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.
+
+part of "core_patch.dart";
+
+// `entry-point` needed to make sure the class will be in the class hierarchy
+// in programs without records.
+@pragma('wasm:entry-point')
+abstract class Record {}
diff --git a/sdk/lib/_internal/wasm/lib/type.dart b/sdk/lib/_internal/wasm/lib/type.dart
index 422c933..693b2ec 100644
--- a/sdk/lib/_internal/wasm/lib/type.dart
+++ b/sdk/lib/_internal/wasm/lib/type.dart
@@ -31,6 +31,7 @@
bool get isFunctionTypeParameterType =>
_testID(ClassID.cidFunctionTypeParameterType);
bool get isFunction => _testID(ClassID.cidFunctionType);
+ bool get isRecord => _testID(ClassID.cidRecordType);
T as<T>() => unsafeCast<T>(this);
@@ -433,6 +434,80 @@
}
}
+class _RecordType extends _Type {
+ final List<String> names;
+ final List<_Type> fieldTypes;
+
+ @pragma("wasm:entry-point")
+ _RecordType(this.names, this.fieldTypes, super.isDeclaredNullable);
+
+ @override
+ _Type get _asNonNullable => _RecordType(names, fieldTypes, false);
+
+ @override
+ _Type get _asNullable => _RecordType(names, fieldTypes, true);
+
+ @override
+ String toString() {
+ StringBuffer buffer = StringBuffer('(');
+
+ final int numPositionals = fieldTypes.length - names.length;
+ final int numNames = names.length;
+
+ for (int i = 0; i < numPositionals; i += 1) {
+ buffer.write(fieldTypes[i]);
+ if (i != fieldTypes.length - 1) {
+ buffer.write(', ');
+ }
+ }
+
+ if (names.isNotEmpty) {
+ buffer.write('{');
+ for (int i = 0; i < numNames; i += 1) {
+ final String fieldName = names[i];
+ final _Type fieldType = fieldTypes[numPositionals + i];
+ buffer.write(fieldType);
+ buffer.write(' ');
+ buffer.write(fieldName);
+ if (i != numNames - 1) {
+ buffer.write(', ');
+ }
+ }
+ buffer.write('}');
+ }
+
+ buffer.write(')');
+ if (isDeclaredNullable) {
+ buffer.write('?');
+ }
+ return buffer.toString();
+ }
+
+ @override
+ bool operator ==(Object other) {
+ if (other is! _RecordType) {
+ return false;
+ }
+
+ if (!_sameShape(other)) {
+ return false;
+ }
+
+ for (int fieldIdx = 0; fieldIdx < fieldTypes.length; fieldIdx += 1) {
+ if (fieldTypes[fieldIdx] != other.fieldTypes[fieldIdx]) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ bool _sameShape(_RecordType other) =>
+ fieldTypes.length == other.fieldTypes.length &&
+ // Name lists are constants and can be compared with `identical`.
+ identical(names, other.names);
+}
+
external List<List<int>> _getTypeRulesSupers();
external List<List<List<_Type>>> _getTypeRulesSubstitutions();
external List<String> _getTypeNames();
@@ -793,6 +868,25 @@
return true;
}
+ bool isRecordSubtype(
+ _RecordType s, _Environment? sEnv, _RecordType t, _Environment? tEnv) {
+ // [s] <: [t] iff s and t have the same shape and fields of `s` are
+ // subtypes of the same field in `t` by index.
+ if (!s._sameShape(t)) {
+ return false;
+ }
+
+ final int numFields = s.fieldTypes.length;
+ for (int fieldIdx = 0; fieldIdx < numFields; fieldIdx += 1) {
+ if (!isSubtype(
+ s.fieldTypes[fieldIdx], sEnv, t.fieldTypes[fieldIdx], tEnv)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
// Subtype check based off of sdk/lib/_internal/js_runtime/lib/rti.dart.
// Returns true if [s] is a subtype of [t], false otherwise.
bool isSubtype(_Type s, _Environment? sEnv, _Type t, _Environment? tEnv) {
@@ -889,6 +983,18 @@
s.as<_FunctionType>(), sEnv, t.as<_FunctionType>(), tEnv);
}
+ // Records:
+ if (s.isRecord && t.isRecord) {
+ return isRecordSubtype(
+ s.as<_RecordType>(), sEnv, t.as<_RecordType>(), tEnv);
+ }
+
+ // Records are subtypes of the `Record` type:
+ if (s.isRecord && t.isInterface) {
+ final tInterfaceType = t.as<_InterfaceType>();
+ return tInterfaceType.classId == ClassID.cidRecord;
+ }
+
// Interface Compositionality + Super-Interface:
if (s.isInterface &&
t.isInterface &&
diff --git a/tools/VERSION b/tools/VERSION
index a02aaff..6f4cbcd 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
MAJOR 3
MINOR 0
PATCH 0
-PRERELEASE 232
+PRERELEASE 233
PRERELEASE_PATCH 0