[dart2wasm] Make cid numbering of concrete classes perfect DFS pre-order

The CL removes a hack that assigned fixed class ids due to the need of
special treatment for masquerading types.

=> Instead of hard coding the type category values, we discover feasible
slots automatically, so we can remove the hack
=> Makes our cid numbering of concrete classes pure DFS pre-order
=> Ensures all concrete subclasses of a class have continious class ids

We also replace an assembly stub that deals with masquerading with one
written in Dart, adding the necessary primitives (e.g.
getting category type table & kinds).

We also align how to get the function type of a closure, namely
add `_Closure._getClosureRuntimeType()` analogous to the method on
`_Record._getRecordRuntimeType()`

Change-Id: I5169fc4953e8e99c4f84a1bbe8e535c08fb47cc5
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/348840
Commit-Queue: Martin Kustermann <kustermann@google.com>
Reviewed-by: Slava Egorov <vegorov@google.com>
This commit is contained in:
Martin Kustermann 2024-01-29 12:53:57 +00:00 committed by Commit Queue
parent 41128c56e2
commit a2ee738dfc
7 changed files with 160 additions and 140 deletions

View file

@ -221,6 +221,9 @@ class ClassInfoCollector {
/// subtypes of the key masquerade as the value.
late final Map<Class, Class> _masquerades = _computeMasquerades();
/// Masqueraded types are mapped to these classes.
late final Set<Class> masqueradeValues = _masquerades.values.toSet();
Map<Class, Class> _computeMasquerades() {
final map = {
translator.coreTypes.boolClass: translator.coreTypes.boolClass,
@ -581,6 +584,7 @@ class ClassInfoCollector {
final importUri = klass.enclosingLibrary.importUri.toString();
if (importUri.startsWith('dart:')) {
if (masqueradeValues.contains(klass)) return -1;
// Bundle the typed data and collection together, they may not have
// common base class except for `Object` but most of them have similar
// selectors.
@ -617,24 +621,35 @@ class ClassInfoCollector {
final classIds = <Class, int>{};
// TODO: Remove this special case that violates strict DFS pre-order
// numbering by re-doing the masquerades.
// TODO: We may consider removing the type category table. But until we do
// we have to make sure that all masqueraded types that concrete classes may
// be mapped to have class ids that are <=255.
//
// We need to use 4 classes here that don't require name mangling.
classIds[translator.coreTypes.objectClass] = nextClassId++;
classIds[translator.functionTypeClass] = nextClassId++;
classIds[translator.interfaceTypeClass] = nextClassId++;
classIds[translator.recordTypeClass] = nextClassId++;
for (Class cls in _masquerades.values) {
classIds[cls] = classIds[cls] ?? nextClassId++;
// We still want to maintain that all classes's concrete subclasses form a
// single continious class-id range.
//
// => So we move the abstract masqeraded classes before all the concrete
// ones.
for (Class cls in masqueradeValues) {
if (cls.isAbstract) {
assert(classIds[cls] == null);
classIds[cls] = nextClassId++;
}
}
for (final cls in dfsOrder) {
if (!cls.isAbstract) classIds[cls] = classIds[cls] ?? nextClassId++;
if (!cls.isAbstract) {
assert(classIds[cls] == null);
classIds[cls] = nextClassId++;
}
}
for (final cls in dfsOrder) {
if (cls.isAbstract) classIds[cls] = classIds[cls] ?? nextClassId++;
}
// Abstract masquerade
for (Class cls in masqueradeValues) {
assert(classIds[cls]! <= 255);
}
return (dfsOrder, classIds);
}

View file

@ -6,7 +6,6 @@ import 'package:dart2wasm/class_info.dart';
import 'package:dart2wasm/code_generator.dart';
import 'package:dart2wasm/dynamic_forwarders.dart';
import 'package:dart2wasm/translator.dart';
import 'package:dart2wasm/types.dart';
import 'package:kernel/ast.dart';
@ -433,13 +432,43 @@ class Intrinsifier {
return w.NumType.i32;
}
if (target.enclosingLibrary.name == "dart.core" &&
target.name.text == "_isIntrinsified") {
// This is part of the VM's [BigInt] implementation. We just return false.
// TODO(joshualitt): Can we find another way to reuse this patch file
// without hardcoding this case?
b.i32_const(0);
return w.NumType.i32;
if (target.enclosingLibrary.name == "dart.core") {
if (target.name.text == "_isIntrinsified") {
// This is part of the VM's [BigInt] implementation. We just return false.
// TODO(joshualitt): Can we find another way to reuse this patch file
// without hardcoding this case?
b.i32_const(0);
return w.NumType.i32;
}
if (node.target.enclosingLibrary == translator.coreTypes.coreLibrary) {
switch (node.target.name.text) {
case "_typeCategoryAbstractClass":
translator.types.typeCategoryTable;
b.i32_const(translator.types.typeCategoryAbstractClass);
return w.NumType.i32;
case "_typeCategoryObject":
translator.types.typeCategoryTable;
b.i32_const(translator.types.typeCategoryObject);
return w.NumType.i32;
case "_typeCategoryFunction":
translator.types.typeCategoryTable;
b.i32_const(translator.types.typeCategoryFunction);
return w.NumType.i32;
case "_typeCategoryRecord":
translator.types.typeCategoryTable;
b.i32_const(translator.types.typeCategoryRecord);
return w.NumType.i32;
case "_typeCategoryNotMasqueraded":
translator.types.typeCategoryTable;
b.i32_const(translator.types.typeCategoryNotMasqueraded);
return w.NumType.i32;
case "_typeCategoryTable":
translator.types.typeCategoryTable;
b.global_get(translator.types.typeCategoryTable);
return translator.types.typeCategoryTable.type.type;
}
}
}
return null;
@ -630,16 +659,6 @@ class Intrinsifier {
return translator.types.makeTypeRulesSubstitutions(b);
case "_getTypeNames":
return translator.types.makeTypeNames(b);
case "_getFunctionRuntimeType":
Expression f = node.arguments.positional.single;
final w.StructType closureBaseStruct =
translator.closureLayouter.closureBaseStruct;
final w.RefType closureBaseStructRef =
w.RefType.def(closureBaseStruct, nullable: false);
codeGen.wrap(f, closureBaseStructRef);
b.struct_get(closureBaseStruct, FieldIndex.closureRuntimeType);
return closureBaseStruct
.fields[FieldIndex.closureRuntimeType].type.unpacked;
case "_isRecordInstance":
Expression o = node.arguments.positional.single;
b.global_get(translator.types.typeCategoryTable);
@ -648,7 +667,7 @@ class Intrinsifier {
b.array_get_u(
(translator.types.typeCategoryTable.type.type as w.RefType)
.heapType as w.ArrayType);
b.i32_const(TypeCategory.record);
b.i32_const(translator.types.typeCategoryRecord);
b.i32_eq();
return w.NumType.i32;
}
@ -1143,89 +1162,6 @@ class Intrinsifier {
return true;
}
// _getActualRuntimeType and _getMasqueradedRuntimeType
if (member.enclosingLibrary == translator.coreTypes.coreLibrary &&
(name == "_getActualRuntimeType" ||
name == "_getMasqueradedRuntimeType")) {
final bool masqueraded = name == "_getMasqueradedRuntimeType";
final w.Local object = paramLocals[0];
final w.Local classId = function.addLocal(w.NumType.i32);
final w.Local resultClassId = function.addLocal(w.NumType.i32);
w.Label interfaceType = b.block();
w.Label notMasqueraded = b.block();
w.Label recordType = b.block();
w.Label functionType = b.block();
w.Label objectType = b.block();
w.Label abstractClass = b.block();
// Look up the type category by class ID and switch on it.
b.global_get(translator.types.typeCategoryTable);
b.local_get(object);
b.struct_get(translator.topInfo.struct, FieldIndex.classId);
b.local_tee(classId);
b.array_get_u((translator.types.typeCategoryTable.type.type as w.RefType)
.heapType as w.ArrayType);
b.local_tee(resultClassId);
b.br_table([
abstractClass,
objectType,
functionType,
recordType,
if (masqueraded) notMasqueraded
], masqueraded ? interfaceType : notMasqueraded);
b.end(); // abstractClass
// We should never see class IDs for abstract types.
b.unreachable();
b.end(); // objectType
translator.constants.instantiateConstant(
function,
b,
TypeLiteralConstant(InterfaceType(translator.coreTypes.objectClass,
Nullability.nonNullable, const [])),
function.type.outputs.single);
b.return_();
b.end(); // functionType
w.StructType closureBase = translator.closureLayouter.closureBaseStruct;
b.local_get(object);
b.ref_cast(w.RefType.def(closureBase, nullable: false));
b.struct_get(closureBase, FieldIndex.closureRuntimeType);
b.return_();
b.end(); // recordType
b.local_get(object);
translator.convertType(
function,
object.type,
translator.classInfo[translator.coreTypes.recordClass]!.repr
.nonNullableType);
codeGen.call(translator.recordGetRecordRuntimeType.reference);
b.return_();
b.end(); // notMasqueraded
b.local_get(classId);
b.local_set(resultClassId);
b.end(); // interfaceType
ClassInfo info = translator.classInfo[translator.interfaceTypeClass]!;
b.i32_const(info.classId);
b.i32_const(initialIdentityHash);
// Runtime types are never nullable.
b.i32_const(0);
// Set class ID of interface type.
b.local_get(resultClassId);
b.i64_extend_i32_u();
// Call _typeArguments to get the array of type arguments.
b.local_get(object);
codeGen.call(translator.objectGetTypeArguments.reference);
b.struct_new(info.struct);
b.return_();
}
// identical
if (member == translator.coreTypes.identicalProcedure) {
w.Local first = paramLocals[0];
@ -1311,6 +1247,16 @@ class Intrinsifier {
return true;
}
// _Closure._getClosureRuntimeType
if (member == translator.getClosureRuntimeType) {
final w.Local object = paramLocals[0];
w.StructType closureBase = translator.closureLayouter.closureBaseStruct;
b.local_get(object);
b.ref_cast(w.RefType.def(closureBase, nullable: false));
b.struct_get(closureBase, FieldIndex.closureRuntimeType);
return true;
}
if (member.enclosingLibrary == translator.coreTypes.coreLibrary &&
name == "identityHashCode") {
final w.Local arg = paramLocals[0];

View file

@ -255,6 +255,8 @@ mixin KernelNodes {
index.getProcedure("dart:core", "Error", "_throw");
// dart:core type procedures
late final Procedure getClosureRuntimeType =
index.getProcedure("dart:core", '_Closure', "_getClosureRuntimeType");
late final Procedure getActualRuntimeType =
index.getTopLevelProcedure("dart:core", "_getActualRuntimeType");
late final Procedure getMasqueradedRuntimeType =

View file

@ -14,18 +14,6 @@ import 'package:kernel/core_types.dart';
import 'package:wasm_builder/wasm_builder.dart' as w;
/// Values for the type category table. Entries for masqueraded classes contain
/// the class ID of the masquerade.
class TypeCategory {
static const abstractClass = 0;
static const object = 1;
static const function = 2;
static const record = 3;
static const notMasqueraded = 4;
static const minMasqueradeClassId = 5;
static const maxMasqueradeClassId = 63; // Leaves 2 unused bits for future use
}
/// Values for the `_kind` field in `_TopType`. Must match the definitions in
/// `_TopType`.
class TopTypeKind {
@ -303,8 +291,31 @@ class Types {
return typeNamesType;
}
// None of these integers belong to masqueraded types like Uint8List.
late final int typeCategoryAbstractClass;
late final int typeCategoryObject;
late final int typeCategoryFunction;
late final int typeCategoryRecord;
late final int typeCategoryNotMasqueraded;
/// Build a global array of byte values used to categorize runtime types.
w.Global _buildTypeCategoryTable() {
// Find 5 class ids whose classes are not masqueraded.
final typeCategories = <int>[];
for (int i = 0; i < translator.classes.length; i++) {
final info = translator.classes[i];
if (!translator.classInfoCollector.masqueradeValues.contains(info.cls)) {
typeCategories.add(i);
if (typeCategories.length == 5) break;
}
}
assert(typeCategories.length == 5 && typeCategories.last <= 255);
typeCategoryAbstractClass = typeCategories[0];
typeCategoryObject = typeCategories[1];
typeCategoryFunction = typeCategories[2];
typeCategoryRecord = typeCategories[3];
typeCategoryNotMasqueraded = typeCategories[4];
Set<Class> recordClasses = Set.from(translator.recordClasses.values);
Uint8List table = Uint8List(translator.classes.length);
for (int i = 0; i < translator.classes.length; i++) {
@ -313,28 +324,28 @@ class Types {
Class? cls = info.cls;
int category;
if (cls == null || cls.isAbstract) {
category = TypeCategory.abstractClass;
category = typeCategoryAbstractClass;
} else if (cls == coreTypes.objectClass) {
category = TypeCategory.object;
category = typeCategoryObject;
} else if (cls == translator.closureClass) {
category = TypeCategory.function;
category = typeCategoryFunction;
} else if (recordClasses.contains(cls)) {
category = TypeCategory.record;
category = typeCategoryRecord;
} else if (masquerade == null || masquerade.classId == i) {
category = TypeCategory.notMasqueraded;
category = typeCategoryNotMasqueraded;
} else {
// Masqueraded class
assert(cls.enclosingLibrary.importUri.scheme == "dart");
assert(masquerade.classId >= TypeCategory.minMasqueradeClassId);
assert(masquerade.classId <= TypeCategory.maxMasqueradeClassId);
assert(!typeCategories.contains(masquerade.classId));
assert(masquerade.classId <= 255);
category = masquerade.classId;
}
table[i] = category;
}
final segment = translator.m.dataSegments.define(table);
w.ArrayType arrayType =
translator.wasmArrayType(w.PackedType.i8, "const i8", mutable: false);
w.ArrayType arrayType = translator.arrayTypeForDartType(
InterfaceType(translator.wasmI8Class, Nullability.nonNullable));
final global = translator.m.globals
.define(w.GlobalType(w.RefType.def(arrayType, nullable: false)));
// Initialize the global to a dummy array, since `array.new_data` is not

View file

@ -22,6 +22,10 @@ final class _Closure implements Function {
external static bool _equals(Function a, Function b);
@pragma("wasm:entry-point")
@pragma("wasm:prefer-inline")
external static _FunctionType _getClosureRuntimeType(_Closure closure);
// Simple hash code for now, we can optimize later
@override
int get hashCode => runtimeType.hashCode;

View file

@ -9,12 +9,12 @@ part of "core_patch.dart";
@pragma('wasm:entry-point')
@patch
abstract class Record {
_Type get _recordRuntimeType;
_RecordType get _recordRuntimeType;
// An instance member needs a call from Dart code to be properly included in
// the dispatch table. Hence we use an inlined static wrapper as entry point.
@pragma("wasm:entry-point")
@pragma("wasm:prefer-inline")
static _Type _getRecordRuntimeType(Record record) =>
static _RecordType _getRecordRuntimeType(Record record) =>
record._recordRuntimeType;
}

View file

@ -413,7 +413,7 @@ class _FunctionType extends _Type {
bool _checkInstance(Object o) {
if (ClassID.getID(o) != ClassID.cid_Closure) return false;
return _typeUniverse.isFunctionSubtype(
_getFunctionRuntimeType(unsafeCast(o)), null, this, null);
_Closure._getClosureRuntimeType(unsafeCast(o)), null, this, null);
}
bool operator ==(Object o) {
@ -604,6 +604,13 @@ external WasmArray<WasmArray<WasmI32>> _getTypeRulesSupers();
external WasmArray<WasmArray<WasmArray<_Type>>> _getTypeRulesSubstitutions();
external WasmArray<String>? _getTypeNames();
external WasmArray<WasmI8> get _typeCategoryTable;
external WasmI32 get _typeCategoryAbstractClass;
external WasmI32 get _typeCategoryObject;
external WasmI32 get _typeCategoryFunction;
external WasmI32 get _typeCategoryRecord;
external WasmI32 get _typeCategoryNotMasqueraded;
/// Type parameter environment used while comparing function types.
///
/// In the case of nested function types, the environment refers to the
@ -1280,19 +1287,54 @@ void _checkClosureType(
}
@pragma("wasm:entry-point")
external _Type _getActualRuntimeType(Object object);
_Type _getActualRuntimeType(Object object) {
final classId = ClassID.getID(object);
final category = _typeCategoryTable.readUnsigned(classId);
if (category == _typeCategoryFunction.toIntUnsigned()) {
return _Closure._getClosureRuntimeType(unsafeCast<_Closure>(object));
}
if (category == _typeCategoryRecord.toIntUnsigned()) {
return Record._getRecordRuntimeType(unsafeCast<Record>(object));
}
if (category == _typeCategoryObject.toIntUnsigned()) {
return _literal<Object>();
}
if (category == _typeCategoryAbstractClass.toIntUnsigned()) {
throw 'unreachable';
}
return _InterfaceType(classId, false, Object._getTypeArguments(object));
}
@pragma("wasm:prefer-inline")
_Type _getActualRuntimeTypeNullable(Object? object) =>
object == null ? _literal<Null>() : _getActualRuntimeType(object);
@pragma("wasm:entry-point")
external _Type _getMasqueradedRuntimeType(Object object);
_Type _getMasqueradedRuntimeType(Object object) {
final classId = ClassID.getID(object);
final category = _typeCategoryTable.readUnsigned(classId);
if (category == _typeCategoryNotMasqueraded.toIntUnsigned()) {
return _InterfaceType(classId, false, Object._getTypeArguments(object));
}
if (category == _typeCategoryFunction.toIntUnsigned()) {
return _Closure._getClosureRuntimeType(unsafeCast<_Closure>(object));
}
if (category == _typeCategoryRecord.toIntUnsigned()) {
return Record._getRecordRuntimeType(unsafeCast<Record>(object));
}
if (category == _typeCategoryObject.toIntUnsigned()) {
return _literal<Object>();
}
if (category == _typeCategoryAbstractClass.toIntUnsigned()) {
throw 'unreachable';
}
return _InterfaceType(category, false, Object._getTypeArguments(object));
}
@pragma("wasm:prefer-inline")
_Type _getMasqueradedRuntimeTypeNullable(Object? object) =>
object == null ? _literal<Null>() : _getMasqueradedRuntimeType(object);
external _FunctionType _getFunctionRuntimeType(Function f);
external bool _isRecordInstance(Object o);