[dart2wasm] Consolidate runtime type classes for bottom and top types

This merges the representations for the `Null` and `Never` types into
a single `_BottomType` class and for the `Object`, `Object?`,
`dynamic` and `void` types into a single `_TopType` class.

Since the merged classes behave identically in subtype checking apart
from nullability, this simplifies subtype checking.

Also makes the runtime type representations for these types plus
`Function`, `Function?`, `Record` and `Record?` singletons in order to
simplify their equality and `hashCode`.

Change-Id: Iad51d1042d0f2f01c1190ba3f63edaeb32dac1dd
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/320540
Reviewed-by: Ömer Ağacan <omersa@google.com>
Commit-Queue: Aske Simon Christensen <askesc@google.com>
This commit is contained in:
Aske Simon Christensen 2023-08-30 08:54:38 +00:00 committed by Commit Queue
parent adb2be018f
commit e2971da563
6 changed files with 153 additions and 169 deletions

View file

@ -265,6 +265,15 @@ class ConstantCreator extends ConstantVisitor<ConstantInfo?> {
w.ModuleBuilder get m => constants.m;
ConstantInfo? ensureConstant(Constant constant) {
// To properly canonicalize type literal constants, we normalize the
// type before canonicalization.
if (constant is TypeLiteralConstant) {
DartType type = types.normalize(constant.type);
if (!identical(type, constant.type)) {
constant = TypeLiteralConstant(type);
}
}
ConstantInfo? info = constants.constantInfo[constant];
if (info == null) {
info = constant.accept(this);
@ -797,9 +806,8 @@ class ConstantCreator extends ConstantVisitor<ConstantInfo?> {
@override
ConstantInfo? visitTypeLiteralConstant(TypeLiteralConstant constant) {
DartType type = types.normalize(constant.type);
ClassInfo info = translator.classInfo[types.classForType(type)]!;
final DartType type = constant.type;
final ClassInfo info = translator.classInfo[types.classForType(type)]!;
translator.functions.allocateClass(info.classId);
if (type is InterfaceType && !types.isSpecializedClass(type.classNode)) {
return _makeInterfaceType(constant, type, info);
@ -856,11 +864,20 @@ class ConstantCreator extends ConstantVisitor<ConstantInfo?> {
function, b, fieldTypes, typeListExpectedType);
b.struct_new(info.struct);
});
} else if (type is VoidType ||
type is DynamicType ||
type is InterfaceType &&
type.classNode == translator.coreTypes.objectClass) {
return createConstant(constant, info.nonNullableType, (function, b) {
b.i32_const(info.classId);
b.i32_const(initialIdentityHash);
b.i32_const(types.encodedNullability(type));
b.i64_const(types.topTypeKind(type));
b.struct_new(info.struct);
});
} else {
assert(type is VoidType ||
type is NeverType ||
assert(type is NeverType ||
type is NullType ||
type is DynamicType ||
type is InterfaceType && types.isSpecializedClass(type.classNode));
return createConstant(constant, info.nonNullableType, (function, b) {
b.i32_const(info.classId);

View file

@ -1119,13 +1119,12 @@ class Intrinsifier {
b.unreachable();
b.end(); // objectType
ClassInfo objectTypeInfo =
translator.classInfo[translator.objectTypeClass]!;
b.i32_const(objectTypeInfo.classId);
b.i32_const(initialIdentityHash);
// Runtime types are never nullable.
b.i32_const(0);
b.struct_new(objectTypeInfo.struct);
translator.constants.instantiateConstant(
function,
b,
TypeLiteralConstant(InterfaceType(translator.coreTypes.objectClass,
Nullability.nonNullable, const [])),
function.type.outputs.single);
b.return_();
b.end(); // functionType

View file

@ -59,9 +59,6 @@ mixin KernelNodes {
// dart:core runtime type classes
late final Class typeClass = index.getClass("dart:core", "_Type");
late final Class dynamicTypeClass =
index.getClass("dart:core", "_DynamicType");
late final Class objectTypeClass = index.getClass("dart:core", "_ObjectType");
late final Class abstractFunctionTypeClass =
index.getClass("dart:core", "_AbstractFunctionType");
late final Class functionTypeClass =
@ -76,9 +73,8 @@ mixin KernelNodes {
index.getClass("dart:core", "_InterfaceTypeParameterType");
late final Class namedParameterClass =
index.getClass("dart:core", "_NamedParameter");
late final Class neverTypeClass = index.getClass("dart:core", "_NeverType");
late final Class nullTypeClass = index.getClass("dart:core", "_NullType");
late final Class voidTypeClass = index.getClass("dart:core", "_VoidType");
late final Class bottomTypeClass = index.getClass("dart:core", "_BottomType");
late final Class topTypeClass = index.getClass("dart:core", "_TopType");
late final Class stackTraceClass = index.getClass("dart:core", "StackTrace");
late final Class typeUniverseClass =
index.getClass("dart:core", "_TypeUniverse");

View file

@ -26,6 +26,14 @@ class TypeCategory {
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 {
static const int objectKind = 0;
static const int dynamicKind = 1;
static const int voidKind = 2;
}
class InterfaceTypeEnvironment {
final Map<TypeParameter, int> _typeOffsets = {};
@ -341,24 +349,18 @@ class Types {
Class classForType(DartType type) {
if (type is DynamicType) {
return translator.dynamicTypeClass;
return translator.topTypeClass;
} else if (type is VoidType) {
return translator.voidTypeClass;
return translator.topTypeClass;
} else if (type is NeverType) {
// For runtime types with sound null safety, `Never?` is the same as
// `Null`.
if (type.nullability == Nullability.nullable) {
return translator.nullTypeClass;
} else {
return translator.neverTypeClass;
}
return translator.bottomTypeClass;
} else if (type is NullType) {
return translator.nullTypeClass;
return translator.bottomTypeClass;
} else if (type is FutureOrType) {
return translator.futureOrTypeClass;
} else if (type is InterfaceType) {
if (type.classNode == coreTypes.objectClass) {
return translator.objectTypeClass;
return translator.topTypeClass;
}
if (type.classNode == coreTypes.functionClass) {
return translator.abstractFunctionTypeClass;
@ -389,6 +391,14 @@ class Types {
cls == coreTypes.recordClass;
}
int topTypeKind(DartType type) {
return type is VoidType
? TopTypeKind.voidKind
: type is DynamicType
? TopTypeKind.dynamicKind
: TopTypeKind.objectKind;
}
/// Allocates a `List<_Type>` from [types] and pushes it to the stack.
void _makeTypeList(CodeGenerator codeGen, Iterable<DartType> types) {
if (types.every(_isTypeConstant)) {
@ -425,8 +435,12 @@ class Types {
}
/// Normalizes a Dart type. Many rules are already applied for us, but we
/// still have to manually normalize [FutureOr].
/// still have to manually turn `Never?` into `Null` and normalize `FutureOr`.
DartType normalize(DartType type) {
if (type is NeverType && type.declaredNullability == Nullability.nullable) {
return const NullType();
}
if (type is! FutureOrType) return type;
final s = normalize(type.typeArgument);

View file

@ -46,20 +46,14 @@ class ClassID {
external static int get cidRecord;
// Class IDs for RTI Types.
@pragma("wasm:class-id", "dart.core#_NeverType")
external static int get cidNeverType;
@pragma("wasm:class-id", "dart.core#_DynamicType")
external static int get cidDynamicType;
@pragma("wasm:class-id", "dart.core#_VoidType")
external static int get cidVoidType;
@pragma("wasm:class-id", "dart.core#_NullType")
external static int get cidNullType;
@pragma("wasm:class-id", "dart.core#_BottomType")
external static int get cidBottomType;
@pragma("wasm:class-id", "dart.core#_TopType")
external static int get cidTopType;
@pragma("wasm:class-id", "dart.core#_FutureOrType")
external static int get cidFutureOrType;
@pragma("wasm:class-id", "dart.core#_InterfaceType")
external static int get cidInterfaceType;
@pragma("wasm:class-id", "dart.core#_ObjectType")
external static int get cidObjectType;
@pragma("wasm:class-id", "dart.core#_AbstractFunctionType")
external static int get cidAbstractFunctionType;
@pragma("wasm:class-id", "dart.core#_FunctionType")
@ -72,6 +66,8 @@ class ClassID {
external static int get cidAbstractRecordType;
@pragma("wasm:class-id", "dart.core#_RecordType")
external static int get cidRecordType;
@pragma("wasm:class-id", "dart.core#_NamedParameter")
external static int get cidNamedParameter;
// Dummy, only used by VM-specific hash table code.
static final int numPredefinedCids = 1;

View file

@ -7,6 +7,13 @@ part of 'core_patch.dart';
// Representation of runtime types. Code in this file should avoid using `is` or
// `as` entirely to avoid a dependency on any inline type checks.
// Helper for type literals used for all singleton types. By using actual type
// literals and letting those through the compiler, rather than calling the
// constructors of the corresponding representation classes, we ensure that they
// are properly canonicalized by the constant instantiator.
@pragma("wasm:prefer-inline")
_Type _literal<T>() => unsafeCast(T);
// TODO(joshualitt): Once we have RTI fully working, we'd like to explore
// implementing [isSubtype] using inheritance.
// TODO(joshualitt): We can cache the result of [_FutureOrType.asFuture].
@ -15,83 +22,75 @@ abstract class _Type implements Type {
const _Type(this.isDeclaredNullable);
@pragma("wasm:prefer-inline")
bool _testID(int value) => ClassID.getID(this) == value;
bool get isNever => _testID(ClassID.cidNeverType);
bool get isDynamic => _testID(ClassID.cidDynamicType);
bool get isVoid => _testID(ClassID.cidVoidType);
bool get isNull => _testID(ClassID.cidNullType);
bool get isBottom => _testID(ClassID.cidBottomType);
bool get isTop => _testID(ClassID.cidTopType);
bool get isFutureOr => _testID(ClassID.cidFutureOrType);
bool get isInterface => _testID(ClassID.cidInterfaceType);
bool get isInterfaceTypeParameterType =>
_testID(ClassID.cidInterfaceTypeParameterType);
bool get isFunctionTypeParameterType =>
_testID(ClassID.cidFunctionTypeParameterType);
bool get isObject => _testID(ClassID.cidObjectType);
bool get isAbstractFunction => _testID(ClassID.cidAbstractFunctionType);
bool get isFunction => _testID(ClassID.cidFunctionType);
bool get isAbstractRecord => _testID(ClassID.cidAbstractRecordType);
bool get isRecord => _testID(ClassID.cidRecordType);
@pragma("wasm:prefer-inline")
T as<T>() => unsafeCast<T>(this);
@pragma("wasm:prefer-inline")
_Type get asNullable => isDeclaredNullable ? this : _asNullable;
_Type get _asNullable;
@override
bool operator ==(Object other) => ClassID.getID(this) == ClassID.getID(other);
@override
int get hashCode => mix64(ClassID.getID(this));
}
@pragma("wasm:entry-point")
class _NeverType extends _Type {
@pragma("wasm:entry-point")
const _NeverType() : super(false);
/// Never? normalizes to Null.
@override
_Type get _asNullable => const _NullType();
class _BottomType extends _Type {
// To ensure that the `Null` and `Never` types are singleton runtime type
// objects, we only allocate these objects via the constant instantiator.
external const _BottomType();
@override
String toString() => 'Never';
_Type get _asNullable => _literal<Null>();
@override
String toString() => isDeclaredNullable ? 'Null' : 'Never';
}
@pragma("wasm:entry-point")
class _DynamicType extends _Type {
@pragma("wasm:entry-point")
const _DynamicType() : super(true);
class _TopType extends _Type {
final int _kind;
// Values for the `_kind` field. Must match the definitions in `TopTypeKind`.
static const int _objectKind = 0;
static const int _dynamicKind = 1;
static const int _voidKind = 2;
// To ensure that the `Object`, `Object?`, `dynamic` and `void` types are
// singleton runtime type objects, we only allocate these objects via the
// constant instantiator.
external const _TopType();
// Only called if Object
@override
_Type get _asNullable => _literal<Object?>();
@override
_Type get _asNullable => this;
@override
String toString() => 'dynamic';
}
@pragma("wasm:entry-point")
class _VoidType extends _Type {
@pragma("wasm:entry-point")
const _VoidType() : super(true);
@override
_Type get _asNullable => this;
@override
String toString() => 'void';
}
@pragma("wasm:entry-point")
class _NullType extends _Type {
@pragma("wasm:entry-point")
const _NullType() : super(true);
@override
_Type get _asNullable => this;
@override
String toString() => 'Null';
String toString() {
switch (_kind) {
case _objectKind:
return isDeclaredNullable ? 'Object?' : 'Object';
case _dynamicKind:
return 'dynamic';
case _voidKind:
return 'void';
default:
throw 'Invalid top type kind';
}
}
}
/// Reference to a type parameter of an interface type.
@ -129,7 +128,7 @@ class _FunctionTypeParameterType extends _Type {
@override
bool operator ==(Object o) {
if (!(super == o)) return false;
if (ClassID.getID(o) != ClassID.cidFunctionTypeParameterType) return false;
_FunctionTypeParameterType other =
unsafeCast<_FunctionTypeParameterType>(o);
// References to different type parameters can have the same index and thus
@ -141,7 +140,7 @@ class _FunctionTypeParameterType extends _Type {
@override
int get hashCode {
int hash = super.hashCode;
int hash = mix64(ClassID.cidFunctionTypeParameterType);
hash = mix64(hash ^ (isDeclaredNullable ? 1 : 0));
return mix64(hash ^ index.hashCode);
}
@ -166,7 +165,7 @@ class _FutureOrType extends _Type {
@override
bool operator ==(Object o) {
if (!(super == o)) return false;
if (ClassID.getID(o) != ClassID.cidFutureOrType) return false;
_FutureOrType other = unsafeCast<_FutureOrType>(o);
if (isDeclaredNullable != other.isDeclaredNullable) return false;
return typeArgument == other.typeArgument;
@ -174,7 +173,7 @@ class _FutureOrType extends _Type {
@override
int get hashCode {
int hash = super.hashCode;
int hash = mix64(ClassID.cidFutureOrType);
hash = mix64(hash ^ (isDeclaredNullable ? 1 : 0));
return mix64(hash ^ typeArgument.hashCode);
}
@ -191,6 +190,7 @@ class _FutureOrType extends _Type {
}
}
@pragma("wasm:entry-point")
class _InterfaceType extends _Type {
final int classId;
final List<_Type> typeArguments;
@ -204,7 +204,7 @@ class _InterfaceType extends _Type {
@override
bool operator ==(Object o) {
if (!(super == o)) return false;
if (ClassID.getID(o) != ClassID.cidInterfaceType) return false;
_InterfaceType other = unsafeCast<_InterfaceType>(o);
if (classId != other.classId) return false;
if (isDeclaredNullable != other.isDeclaredNullable) return false;
@ -217,7 +217,7 @@ class _InterfaceType extends _Type {
@override
int get hashCode {
int hash = super.hashCode;
int hash = mix64(ClassID.cidInterfaceType);
hash = mix64(hash ^ classId);
hash = mix64(hash ^ (isDeclaredNullable ? 1 : 0));
for (int i = 0; i < typeArguments.length; i++) {
@ -253,7 +253,7 @@ class _NamedParameter {
@override
bool operator ==(Object o) {
if (ClassID.getID(this) != ClassID.getID(o)) return false;
if (ClassID.getID(o) != ClassID.cidNamedParameter) return false;
_NamedParameter other = unsafeCast<_NamedParameter>(o);
return this.name == other.name &&
this.type == other.type &&
@ -262,7 +262,7 @@ class _NamedParameter {
@override
int get hashCode {
int hash = mix64(ClassID.getID(this));
int hash = mix64(ClassID.cidNamedParameter);
hash = mix64(hash ^ name.hashCode);
hash = mix64(hash ^ type.hashCode);
return mix64(hash ^ (isRequired ? 1 : 0));
@ -279,51 +279,22 @@ class _NamedParameter {
}
}
class _ObjectType extends _Type {
@pragma("wasm:entry-point")
const _ObjectType(super.isDeclaredNullable);
@override
_Type get _asNullable => const _ObjectType(true);
@override
bool operator ==(Object o) {
if (!(super == o)) return false;
_ObjectType other = unsafeCast<_ObjectType>(o);
return isDeclaredNullable == other.isDeclaredNullable;
}
@override
int get hashCode {
int hash = super.hashCode;
return mix64(hash ^ (isDeclaredNullable ? 1 : 0));
}
@override
String toString() {
StringBuffer s = StringBuffer();
s.write("Object");
if (isDeclaredNullable) s.write("?");
return s.toString();
}
}
@pragma("wasm:entry-point")
class _AbstractFunctionType extends _Type {
@pragma("wasm:entry-point")
const _AbstractFunctionType(super.isDeclaredNullable);
// To ensure that the `Function` and `Function?` types are singleton runtime
// type objects, we only allocate these objects via the constant instantiator.
external const _AbstractFunctionType();
@override
_Type get _asNullable => const _AbstractFunctionType(true);
_Type get _asNullable => _literal<Function?>();
@override
String toString() {
StringBuffer s = StringBuffer();
s.write("Function");
if (isDeclaredNullable) s.write("?");
return s.toString();
return isDeclaredNullable ? 'Function?' : 'Function';
}
}
@pragma("wasm:entry-point")
class _FunctionType extends _Type {
// TODO(askesc): The [typeParameterOffset] is 0 except in the rare case where
// the function type contains a nested generic function type that contains a
@ -361,7 +332,7 @@ class _FunctionType extends _Type {
true);
bool operator ==(Object o) {
if (!(super == o)) return false;
if (ClassID.getID(o) != ClassID.cidFunctionType) return false;
_FunctionType other = unsafeCast<_FunctionType>(o);
if (isDeclaredNullable != other.isDeclaredNullable) return false;
if (typeParameterBounds.length != other.typeParameterBounds.length) {
@ -389,7 +360,7 @@ class _FunctionType extends _Type {
@override
int get hashCode {
int hash = super.hashCode;
int hash = mix64(ClassID.cidFunctionType);
for (int i = 0; i < typeParameterBounds.length; i++) {
hash = mix64(hash ^ typeParameterBounds[i].hashCode);
}
@ -414,9 +385,7 @@ class _FunctionType extends _Type {
if (i > 0) s.write(", ");
s.write("X${typeParameterOffset + i}");
final bound = typeParameterBounds[i];
if (!(bound.isObject && bound.isDeclaredNullable ||
bound.isDynamic ||
bound.isVoid)) {
if (!(bound.isTop && bound.isDeclaredNullable)) {
s.write(" extends ");
s.write(bound);
}
@ -446,22 +415,22 @@ class _FunctionType extends _Type {
}
}
@pragma("wasm:entry-point")
class _AbstractRecordType extends _Type {
@pragma("wasm:entry-point")
const _AbstractRecordType(super.isDeclaredNullable);
// To ensure that the `Record` and `Record?` types are singleton runtime type
// objects, we only allocate these objects via the constant instantiator.
external const factory _AbstractRecordType();
@override
_Type get _asNullable => const _AbstractRecordType(true);
_Type get _asNullable => _literal<Record?>();
@override
String toString() {
StringBuffer s = StringBuffer();
s.write("Record");
if (isDeclaredNullable) s.write("?");
return s.toString();
return isDeclaredNullable ? 'Record?' : 'Record';
}
}
@pragma("wasm:entry-point")
class _RecordType extends _Type {
final List<String> names;
final List<_Type> fieldTypes;
@ -509,10 +478,9 @@ class _RecordType extends _Type {
}
@override
bool operator ==(Object other) {
if (other is! _RecordType) {
return false;
}
bool operator ==(Object o) {
if (ClassID.getID(o) != ClassID.cidRecordType) return false;
_RecordType other = unsafeCast<_RecordType>(o);
if (!_sameShape(other)) {
return false;
@ -640,11 +608,7 @@ class _TypeUniverse {
/// substituted.
static _Type substituteTypeArgument(
_Type type, List<_Type> substitutions, _FunctionType? rootFunction) {
if (type.isNever ||
type.isDynamic ||
type.isVoid ||
type.isNull ||
type.isObject) {
if (type.isBottom || type.isTop) {
return type;
} else if (type.isFutureOr) {
return createNormalizedFutureOrType(
@ -726,15 +690,13 @@ class _TypeUniverse {
static _Type createNormalizedFutureOrType(
bool isDeclaredNullable, _Type typeArgument) {
if (typeArgument.isObject ||
typeArgument.isDynamic ||
typeArgument.isVoid) {
if (typeArgument.isTop) {
return isDeclaredNullable ? typeArgument.asNullable : typeArgument;
} else if (typeArgument.isNever) {
} else if (typeArgument.isBottom) {
return _InterfaceType(
ClassID.cidFuture, isDeclaredNullable, const [_NeverType()]);
} else if (typeArgument.isNull) {
return _InterfaceType(ClassID.cidFuture, true, const [_NullType()]);
ClassID.cidFuture,
isDeclaredNullable || typeArgument.isDeclaredNullable,
[typeArgument]);
}
bool declaredNullability =
@ -783,7 +745,7 @@ class _TypeUniverse {
// arguments.
if (substitutions.isNotEmpty && sTypeArguments.isEmpty) {
sTypeArguments = List<_Type>.generate(
substitutions.length, (int index) => const _DynamicType(),
substitutions.length, (int index) => _literal<dynamic>(),
growable: false);
}
@ -923,13 +885,13 @@ class _TypeUniverse {
}
// Left bottom:
if (s.isNull || s.isNever) return true;
if (s.isBottom) return true;
// Right top:
if (t.isObject || t.isDynamic || t.isVoid) return true;
if (t.isTop) return true;
// Right bottom:
if (t.isNull || t.isNever) return false;
if (t.isBottom) return false;
// Left Type Variable Bound 1:
if (s.isFunctionTypeParameterType) {
@ -1175,11 +1137,11 @@ external _Type _getActualRuntimeType(Object object);
@pragma("wasm:prefer-inline")
_Type _getActualRuntimeTypeNullable(Object? object) =>
object == null ? const _NullType() : _getActualRuntimeType(object);
object == null ? _literal<Null>() : _getActualRuntimeType(object);
@pragma("wasm:entry-point")
external _Type _getMasqueradedRuntimeType(Object object);
@pragma("wasm:prefer-inline")
_Type _getMasqueradedRuntimeTypeNullable(Object? object) =>
object == null ? const _NullType() : _getMasqueradedRuntimeType(object);
object == null ? _literal<Null>() : _getMasqueradedRuntimeType(object);