[dart2wasm] Perform some missing runtime type normalizations.

This CL also changes the runtime algorithm to fully substitute
interface type parameters before performing type checks. This change is unfortunate, but necessary so that normalization of types works as expected. In the longer term we should cache generated types to avoid redundant work.

Change-Id: I9441ccdea8c6a9ac598cae043937cf683616d227
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/262821
Commit-Queue: Joshua Litt <joshualitt@google.com>
Reviewed-by: Aske Simon Christensen <askesc@google.com>
Reviewed-by: Mayank Patke <fishythefish@google.com>
This commit is contained in:
Joshua Litt 2022-10-18 17:36:41 +00:00 committed by Commit Queue
parent 7972d580f3
commit ea541423d0
3 changed files with 110 additions and 62 deletions

View file

@ -135,6 +135,7 @@ class Translator {
late final Procedure invocationGenericMethodFactory;
late final Procedure nullToString;
late final Procedure nullNoSuchMethod;
late final Procedure createNormalizedFutureOrType;
late final Map<Class, w.StorageType> builtinTypes;
late final Map<w.ValueType, Class> boxedClasses;
@ -315,6 +316,8 @@ class Translator {
nullNoSuchMethod = lookupCore("Object")
.procedures
.firstWhere((p) => p.name.text == "_nullNoSuchMethod");
createNormalizedFutureOrType = typeUniverseClass.procedures
.firstWhere((p) => p.name.text == "createNormalizedFutureOrType");
builtinTypes = {
coreTypes.boolClass: w.NumType.i32,
coreTypes.intClass: w.NumType.i64,

View file

@ -368,6 +368,7 @@ class Types {
w.Instructions b = codeGen.b;
b.i32_const(encodedNullability(type));
makeType(codeGen, type.typeArgument);
codeGen.call(translator.createNormalizedFutureOrType.reference);
}
void _makeFunctionType(
@ -441,14 +442,18 @@ class Types {
}
return nonNullableTypeType;
}
ClassInfo info = translator.classInfo[classForType(type)]!;
if (type is FutureOrType) {
_makeFutureOrType(codeGen, type);
return info.nonNullableType;
}
translator.functions.allocateClass(info.classId);
b.i32_const(info.classId);
b.i32_const(initialIdentityHash);
if (type is InterfaceType) {
_makeInterfaceType(codeGen, info, type);
} else if (type is FutureOrType) {
_makeFutureOrType(codeGen, type);
} else if (type is FunctionType) {
if (isGenericFunction(type)) {
// TODO(joshualitt): Implement generic function types and share most of

View file

@ -159,11 +159,13 @@ class _FutureOrType extends _Type {
_InterfaceType get asFuture =>
_InterfaceType(ClassID.cid_Future, isDeclaredNullable, [typeArgument]);
// Removing a `?` from a type should not require additional normalization.
@override
_Type get _asNonNullable => _FutureOrType(false, typeArgument);
@override
_Type get _asNullable => _FutureOrType(true, typeArgument);
_Type get _asNullable =>
_TypeUniverse.createNormalizedFutureOrType(true, typeArgument);
@override
bool operator ==(Object o) {
@ -386,41 +388,9 @@ external List<List<int>> _getTypeRulesSupers();
external List<List<List<_Type>>> _getTypeRulesSubstitutions();
external List<String> _getTypeNames();
class _Environment {
List<List<_Type>> scopes = [];
_Environment();
factory _Environment.from(List<_Type> initialScope) {
final env = _Environment();
env.push(initialScope);
return env;
}
void push(List<_Type> scope) => scopes.add(scope);
void pop() => scopes.removeLast();
_Type _substituteTypeParameter(bool declaredNullable, _Type type) {
// If the type parameter is non-nullable, or the substitution type is
// nullable, then just return the substitution type. Otherwise, we return
// [type] as nullable.
// Note: This will throw if the required nullability is impossible to
// generate.
if (!declaredNullable || type.isDeclaredNullable) {
return type;
}
return type.asNullable;
}
_Type lookup(_InterfaceTypeParameterType typeParameter) {
// Lookup `InterfaceType` parameters in the top environment.
// TODO(joshualitt): When we implement generic functions be sure to keep the
// environments distinct.
return _substituteTypeParameter(typeParameter.isDeclaredNullable,
scopes.last[typeParameter.environmentIndex]);
}
}
// TODO(joshualitt): This environment will be used for type arguments for
// generic functions.
class _Environment {}
class _TypeUniverse {
/// 'Map' of classId to the transitive set of super classes it implements.
@ -436,28 +406,103 @@ class _TypeUniverse {
return _TypeUniverse._(_getTypeRulesSupers(), _getTypeRulesSubstitutions());
}
bool isSpecificInterfaceType(_Type t, int classId) {
static bool isSpecificInterfaceType(_Type t, int classId) {
if (!t.isInterface) return false;
_InterfaceType type = t.as<_InterfaceType>();
return type.classId == classId;
}
bool isObjectQuestionType(_Type t) => isObjectType(t) && t.isDeclaredNullable;
static bool isObjectQuestionType(_Type t) =>
isObjectType(t) && t.isDeclaredNullable;
bool isObjectType(_Type t) => isSpecificInterfaceType(t, ClassID.cidObject);
static bool isObjectType(_Type t) =>
isSpecificInterfaceType(t, ClassID.cidObject);
bool isTopType(_Type type) {
static bool isTopType(_Type type) {
return isObjectQuestionType(type) || type.isDynamic || type.isVoid;
}
bool isBottomType(_Type type) {
static bool isBottomType(_Type type) {
return type.isNever;
}
bool isFunctionType(_Type t) =>
static bool isFunctionType(_Type t) =>
isSpecificInterfaceType(t, ClassID.cidFunction) ||
isSpecificInterfaceType(t, ClassID.cid_Function);
static _Type substituteTypeParameter(
_InterfaceTypeParameterType typeParameter, List<_Type> substitutions) {
// If the type parameter is non-nullable, or the substitution type is
// nullable, then just return the substitution type. Otherwise, we return
// [type] as nullable.
// Note: This will throw if the required nullability is impossible to
// generate.
_Type substitution = substitutions[typeParameter.environmentIndex];
if (typeParameter.isDeclaredNullable) return substitution.asNullable;
return substitution;
}
static _Type substituteTypeArgument(_Type type, List<_Type> substitutions) {
if (type.isNever || type.isDynamic || type.isVoid || type.isNull) {
return type;
} else if (type.isFutureOr) {
return createNormalizedFutureOrType(
type.isDeclaredNullable,
substituteTypeArgument(
type.as<_FutureOrType>().typeArgument, substitutions));
} else if (type.isInterface) {
_InterfaceType interfaceType = type.as<_InterfaceType>();
return _InterfaceType(
interfaceType.classId,
interfaceType.isDeclaredNullable,
interfaceType.typeArguments
.map((type) => substituteTypeArgument(type, substitutions))
.toList());
} else if (type.isInterfaceTypeParameterType) {
return substituteTypeParameter(
type.as<_InterfaceTypeParameterType>(), substitutions);
} else if (type.isFunction) {
_FunctionType functionType = type.as<_FunctionType>();
return _FunctionType(
substituteTypeArgument(functionType.returnType, substitutions),
functionType.positionalParameters
.map((type) => substituteTypeArgument(type, substitutions))
.toList(),
functionType.requiredParameterCount,
functionType.namedParameters
.map((named) => _NamedParameter(
named.name,
substituteTypeArgument(named.type, substitutions),
named.isRequired))
.toList(),
functionType.isDeclaredNullable);
} else {
// TODO(joshualitt): Support generic functions.
throw 'Type argument substitution not supported for $type';
}
}
static List<_Type> substituteTypeArguments(
List<_Type> types, List<_Type> substitutions) =>
List<_Type>.generate(types.length,
(int index) => substituteTypeArgument(types[index], substitutions),
growable: false);
static _Type createNormalizedFutureOrType(
bool isDeclaredNullable, _Type typeArgument) {
if (isTopType(typeArgument) || isObjectType(typeArgument)) {
return typeArgument;
} else if (typeArgument.isNever) {
return _InterfaceType(ClassID.cid_Future, false, [const _NeverType()]);
} else if (typeArgument.isNull) {
return _InterfaceType(ClassID.cid_Future, true, [const _NullType()]);
}
bool declaredNullability =
typeArgument.isDeclaredNullable ? false : isDeclaredNullable;
return _FutureOrType(declaredNullability, typeArgument);
}
bool areTypeArgumentsSubtypes(List<_Type> sArgs, _Environment? sEnv,
List<_Type> tArgs, _Environment? tEnv) {
assert(sArgs.length == tArgs.length);
@ -488,29 +533,28 @@ class _TypeUniverse {
if (sSuperIndexOfT == -1) return false;
assert(sSuperIndexOfT < typeRulesSubstitutions[sId].length);
// Return early if we don't have have to check type arguments.
List<_Type> sTypeArguments = s.typeArguments;
List<_Type> substitutions = typeRulesSubstitutions[sId][sSuperIndexOfT];
if (substitutions.isEmpty && sTypeArguments.isEmpty) {
return true;
}
// If we have empty type arguments then create a list of dynamic type
// arguments.
List<_Type> sTypeArguments = s.typeArguments;
if (substitutions.isNotEmpty && sTypeArguments.isEmpty) {
sTypeArguments = List<_Type>.generate(
substitutions.length, (int index) => const _DynamicType(),
growable: false);
}
// If [sEnv] is null, then create a new environment. Otherwise, we are doing
// a recursive type check, so extend the existing environment with [s]'s
// type arguments.
if (sEnv == null) {
sEnv = _Environment.from(sTypeArguments);
} else {
sEnv.push(sTypeArguments);
}
bool result =
areTypeArgumentsSubtypes(substitutions, sEnv, t.typeArguments, tEnv);
sEnv.pop();
return result;
// Finally substitute arguments. We must do this upfront so we can normalize
// the type.
// TODO(joshualitt): This process is expensive so we should cache the
// result.
List<_Type> substituted =
substituteTypeArguments(substitutions, sTypeArguments);
return areTypeArgumentsSubtypes(substituted, sEnv, t.typeArguments, tEnv);
}
bool isFunctionSubtype(_FunctionType s, _Environment? sEnv, _FunctionType t,
@ -600,8 +644,7 @@ class _TypeUniverse {
// Left Type Variable Bound 1:
// TODO(joshualitt): Implement for generic function type parameters.
if (s.isInterfaceTypeParameterType) {
return isSubtype(
sEnv!.lookup(s.as<_InterfaceTypeParameterType>()), sEnv, t, tEnv);
throw 'Unbound type parameter $s';
}
// Left Null:
@ -650,9 +693,6 @@ class _TypeUniverse {
// Right Nullable:
if (t.isDeclaredNullable) {
// `s` is neither null nor declared nullable so return false if `t` is
// truly `null`.
if (t.isNull) return false;
return isSubtype(s, sEnv, t.asNonNullable, tEnv);
}