[dart2wasm] Use a flat index space for function type parameters

The current two-level indexing scheme for function type parameters
(depth and index) breaks down in the case of type substitution when
the substituted type is a generic function type, since its internal
type parameter types have been encoded assuming that the function type
had nesting depth zero, but after substitution its nesting depth can
be higher.

Relative indexing schemes such as De Bruijn indices will also not
work, since function type parameter types are constant types, and the
constant infrastructure assumes that the same constant always has the
same representation.

This change introduces a flat indexing scheme where function type
parameters are indexed using a single index which is independent of
the context in which the type parameter type appears. To avoid
collisions in the case of nested generic function types, every function
type has a type parameter offset, which conceptually shifts the
indexing range of its type parameters so it doesn't necessarily start
at zero.

Looking up a function type parameter in its environment thus involves
searching outwards until a function type is found whose type parameter
index range contains the index encoded in the function type parameter
type.

Change-Id: I544056d52711ff829b170f78a7274a93871825a4
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/272361
Reviewed-by: Joshua Litt <joshualitt@google.com>
This commit is contained in:
Aske Simon Christensen 2022-12-08 13:54:57 +00:00
parent f72f2e92d5
commit ad06d73ace
5 changed files with 273 additions and 132 deletions

View file

@ -33,7 +33,7 @@ class FieldIndex {
static const instantiationContextTypeArgumentsBase = 1;
static const typeIsDeclaredNullable = 2;
static const interfaceTypeTypeArguments = 4;
static const functionTypeNamedParameters = 7;
static const functionTypeNamedParameters = 8;
static const typedListBaseLength = 2;
static const typedListArray = 3;
static const typedListViewTypedData = 3;

View file

@ -137,25 +137,6 @@ class Constants {
if (expectedType == translator.voidMarker) return;
ConstantInstantiator(this, function, b, expectedType).instantiate(constant);
}
/// Emit code to push a constant onto the stack that forms part of a type.
///
/// If the constant is part of a generic function type, [env] contains the
/// environment that maps the function's type parameters to their runtime
/// representation.
///
/// It is assumed that constants that form part of a type are never lazy.
/// Hitting the forced laziness criterion (a list longer than the maximum
/// number of elements allowed by the `array.new_fixed` Wasm instruction)
/// would need to involve a function type with this many parameters, but such
/// a function would hit the (lower) limit on the maximum number of parameters
/// to a Wasm function anyway.
void instantiateTypeConstant(w.DefinedFunction? function, w.Instructions b,
Constant constant, FunctionTypeEnvironment? env) {
ConstantInfo info = ConstantCreator(this, env).ensureConstant(constant)!;
assert(!info.isLazy);
b.global_get(info.global);
}
}
class ConstantInstantiator extends ConstantVisitor<w.ValueType> {
@ -261,12 +242,7 @@ class ConstantInstantiator extends ConstantVisitor<w.ValueType> {
class ConstantCreator extends ConstantVisitor<ConstantInfo?> {
final Constants constants;
/// Environment that maps function type parameters to their runtime
/// representation when inside a generic function type.
FunctionTypeEnvironment _env;
ConstantCreator(this.constants, [FunctionTypeEnvironment? env])
: _env = env ?? FunctionTypeEnvironment();
ConstantCreator(this.constants);
Translator get translator => constants.translator;
Types get types => translator.types;
@ -692,7 +668,7 @@ class ConstantCreator extends ConstantVisitor<ConstantInfo?> {
ConstantInfo? _makeFunctionType(
TypeLiteralConstant constant, FunctionType type, ClassInfo info) {
_env.enterFunctionType(type);
int typeParameterOffset = types.computeFunctionTypeParameterOffset(type);
ListConstant typeParameterBoundsConstant = constants
.makeTypeList(type.typeParameters.map((p) => p.bound).toList());
TypeLiteralConstant returnTypeConstant =
@ -708,11 +684,11 @@ class ConstantCreator extends ConstantVisitor<ConstantInfo?> {
ensureConstant(positionalParametersConstant);
ensureConstant(requiredParameterCountConstant);
ensureConstant(namedParametersConstant);
_env.leaveFunctionType();
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(typeParameterOffset);
constants.instantiateConstant(
function, b, typeParameterBoundsConstant, types.typeListExpectedType);
constants.instantiateConstant(
@ -741,13 +717,17 @@ class ConstantCreator extends ConstantVisitor<ConstantInfo?> {
return _makeFunctionType(constant, type, info);
} else if (type is TypeParameterType) {
if (types.isFunctionTypeParameter(type)) {
// The indexing scheme used by function type parameters ensures that
// function type parameter types that are identical as constants (have
// the same nullability and refer to the same type parameter) have the
// same representation and thus can be canonicalized like other
// constants.
return createConstant(constant, info.nonNullableType, (function, b) {
int index = types.getFunctionTypeParameterIndex(type.parameter);
b.i32_const(info.classId);
b.i32_const(initialIdentityHash);
b.i32_const(types.encodedNullability(type));
FunctionTypeParameterType param = _env.lookup(type.parameter);
b.i64_const(param.depth);
b.i64_const(param.index);
b.i64_const(index);
b.struct_new(info.struct);
});
}

View file

@ -2,6 +2,8 @@
// 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:math' show max;
import 'package:dart2wasm/class_info.dart';
import 'package:dart2wasm/code_generator.dart';
import 'package:dart2wasm/translator.dart';
@ -28,57 +30,6 @@ class InterfaceTypeEnvironment {
int lookup(TypeParameter typeParameter) => _typeOffsets[typeParameter]!;
}
/// Environment that maps function type parameters to their runtime
/// representation when inside a generic function type.
class FunctionTypeEnvironment {
/// Mapping from function type parameters to their runtime representation.
late final Map<TypeParameter, FunctionTypeParameterType> _typeOffsets = {};
/// Current nesting depth of function types (number of function types
/// enclosing the current function type), or -1 if currently not inside a
/// function type.
int _depth = -1;
FunctionTypeEnvironment();
/// Enter the scope of a function type and add its type parameters to the
/// environment.
void enterFunctionType(FunctionType type) {
_depth++;
for (int i = 0; i < type.typeParameters.length; i++) {
_typeOffsets[type.typeParameters[i]] =
FunctionTypeParameterType(_depth, i);
}
}
/// Leave the scope of a function type.
void leaveFunctionType() {
if (--_depth == -1) {
// This clear is not strictly necessary, since type parameters for
// different function types are distinct, but it avoids bloating the
// map throughout the compilation.
_typeOffsets.clear();
}
}
/// Look up a function type parameter in the environment.
FunctionTypeParameterType lookup(TypeParameter typeParameter) =>
_typeOffsets[typeParameter]!;
}
/// Description of the runtime representation of a function type parameter.
class FunctionTypeParameterType {
/// The nesting depth of the function type declaring this type parameter,
/// i.e. the number of function types it is embedded inside.
final int depth;
/// The index of this type parameter in the function type's list of type
/// parameters.
final int index;
FunctionTypeParameterType(this.depth, this.index);
}
/// Helper class for building runtime types.
class Types {
final Translator translator;
@ -124,9 +75,13 @@ class Types {
/// A list which maps class ID to the classes [String] name.
late final List<String> typeNames = _buildTypeNames();
/// Environment that maps function type parameters to their runtime
/// representation when inside a generic function type.
FunctionTypeEnvironment _env = FunctionTypeEnvironment();
/// Type parameter offset for function types, specifying the lower end of
/// their index range for type parameter types.
Map<FunctionType, int> functionTypeParameterOffset = Map.identity();
/// Index value for function type parameter types, indexing into the type
/// parameter index range of their corresponding function type.
Map<TypeParameter, int> functionTypeParameterIndex = Map.identity();
Types(this.translator);
@ -398,21 +353,28 @@ class Types {
}
void _makeFunctionType(CodeGenerator codeGen, FunctionType type) {
int typeParameterOffset = computeFunctionTypeParameterOffset(type);
w.Instructions b = codeGen.b;
b.i32_const(encodedNullability(type));
_env.enterFunctionType(type);
b.i64_const(typeParameterOffset);
_makeTypeList(codeGen, type.typeParameters.map((p) => p.bound).toList());
makeType(codeGen, type.returnType);
if (type.positionalParameters.every(_isTypeConstant)) {
translator.constants.instantiateTypeConstant(codeGen.function, b,
translator.constants.makeTypeList(type.positionalParameters), _env);
translator.constants.instantiateConstant(
codeGen.function,
b,
translator.constants.makeTypeList(type.positionalParameters),
typeListExpectedType);
} else {
_makeTypeList(codeGen, type.positionalParameters);
}
b.i64_const(type.requiredParameterCount);
if (type.namedParameters.every((n) => _isTypeConstant(n.type))) {
translator.constants.instantiateTypeConstant(codeGen.function, b,
translator.constants.makeNamedParametersList(type), _env);
translator.constants.instantiateConstant(
codeGen.function,
b,
translator.constants.makeNamedParametersList(type),
namedParametersExpectedType);
} else {
Class namedParameterClass = translator.namedParameterClass;
Constructor namedParameterConstructor =
@ -436,7 +398,6 @@ class Types {
translator.convertType(codeGen.function, namedParametersListType,
namedParametersExpectedType);
}
_env.leaveFunctionType();
}
/// Makes a `_Type` object on the stack.
@ -447,8 +408,8 @@ class Types {
type = normalize(type);
w.Instructions b = codeGen.b;
if (_isTypeConstant(type)) {
translator.constants.instantiateTypeConstant(
codeGen.function, b, TypeLiteralConstant(type), _env);
translator.constants.instantiateConstant(
codeGen.function, b, TypeLiteralConstant(type), nonNullableTypeType);
return nonNullableTypeType;
}
// All of the singleton types represented by canonical objects should be
@ -486,6 +447,31 @@ class Types {
return info.nonNullableType;
}
/// Compute the lower end of the type parameter index range for this function
/// type. This is computed such that it avoids overlap between the index range
/// of this function type and the index ranges of all generic function types
/// nested within it that contain references to the type parameters of this
/// function type.
///
/// This will also compute the index values for all of the function's type
/// parameters, which can subsequently be queried using
/// [getFunctionTypeParameterIndex].
int computeFunctionTypeParameterOffset(FunctionType type) {
if (type.typeParameters.isEmpty) return 0;
int? offset = functionTypeParameterOffset[type];
if (offset != null) return offset;
_FunctionTypeParameterOffsetCollector(this).visitFunctionType(type);
return functionTypeParameterOffset[type]!;
}
/// Get the index value for a function type parameter, indexing into the
/// type parameter index range of its corresponding function type.
int getFunctionTypeParameterIndex(TypeParameter type) {
assert(functionTypeParameterIndex.containsKey(type),
"Type parameter offset has not been computed for function type");
return functionTypeParameterIndex[type]!;
}
/// Test value against a Dart type. Expects the value on the stack as a
/// (ref null #Top) and leaves the result on the stack as an i32.
/// TODO(joshualitt): Remove dependency on [CodeGenerator]
@ -571,3 +557,69 @@ class Types {
int encodedNullability(DartType type) =>
type.declaredNullability == Nullability.nullable ? 1 : 0;
}
/// For a function type F = `... Function<X0, ..., Xn-1>(...)` compute offset(F)
/// such that for any function type G = `... Function<Y0, ..., Ym-1>(...)`
/// nested inside F, if G contains a reference to any type parameters of F, then
/// offset(F) >= offset(G) + m.
///
/// Conceptually, the type parameters of F are indexed from offset(F) inclusive
/// to offset(F) + n exclusive.
///
/// Also assign to each type parameter Xi the index offset(F) + i such that it
/// indexes the correct type parameter in the conceptual type parameter index
/// range of F.
///
/// This ensures that for every reference to a type parameter, its corresponding
/// function type is the innermost function type enclosing it for which the
/// index falls within the type parameter index range of the function type.
class _FunctionTypeParameterOffsetCollector extends RecursiveVisitor {
final Types types;
final List<FunctionType> _functionStack = [];
final List<Set<FunctionType>> _functionsContainingParameters = [];
final Map<TypeParameter, int> _functionForParameter = {};
_FunctionTypeParameterOffsetCollector(this.types);
@override
void visitFunctionType(FunctionType node) {
int slot = _functionStack.length;
_functionStack.add(node);
_functionsContainingParameters.add({});
for (int i = 0; i < node.typeParameters.length; i++) {
TypeParameter parameter = node.typeParameters[i];
_functionForParameter[parameter] = slot;
}
super.visitFunctionType(node);
int offset = 0;
for (FunctionType inner in _functionsContainingParameters.last) {
offset = max(
offset,
types.functionTypeParameterOffset[inner]! +
inner.typeParameters.length);
}
types.functionTypeParameterOffset[node] = offset;
for (int i = 0; i < node.typeParameters.length; i++) {
TypeParameter parameter = node.typeParameters[i];
types.functionTypeParameterIndex[parameter] = offset + i;
}
_functionsContainingParameters.removeLast();
_functionStack.removeLast();
}
@override
void visitTypeParameterType(TypeParameterType node) {
if (types.isFunctionTypeParameter(node)) {
int slot = _functionForParameter[node.parameter]!;
for (int inner = slot + 1; inner < _functionStack.length; inner++) {
_functionsContainingParameters[slot].add(_functionStack[inner]);
}
}
}
}

View file

@ -137,41 +137,36 @@ class _InterfaceTypeParameterType extends _Type {
/// This type only occurs inside generic function types.
@pragma("wasm:entry-point")
class _FunctionTypeParameterType extends _Type {
/// The nesting depth of the function type declaring this type parameter,
/// i.e. the number of function types it is embedded inside.
final int depth;
/// The index of this type parameter in the function type's list of type
/// parameters.
final int index;
@pragma("wasm:entry-point")
const _FunctionTypeParameterType(
super.isDeclaredNullable, this.depth, this.index);
const _FunctionTypeParameterType(super.isDeclaredNullable, this.index);
@override
_Type get _asNonNullable => _FunctionTypeParameterType(false, depth, index);
_Type get _asNonNullable => _FunctionTypeParameterType(false, index);
@override
_Type get _asNullable => _FunctionTypeParameterType(true, depth, index);
_Type get _asNullable => _FunctionTypeParameterType(true, index);
@override
bool operator ==(Object o) {
if (!(super == o)) return false;
_FunctionTypeParameterType other =
unsafeCast<_FunctionTypeParameterType>(o);
return depth == other.depth && index == other.index;
// References to different type parameters can have the same index and thus
// sometimes compare equal even if they are not. However, this can only
// happen if the containing types are different in other places, in which
// case the comparison as a whole correctly compares unequal.
return index == other.index;
}
@override
int get hashCode {
int hash = super.hashCode;
hash = mix64(hash ^ (isDeclaredNullable ? 1 : 0));
hash = mix64(hash ^ depth.hashCode);
return mix64(hash ^ index.hashCode);
}
// TODO(askesc): Distinguish the depth of function type parameters.
@override
String toString() => 'X$index';
}
@ -313,6 +308,12 @@ class _NamedParameter {
}
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
// reference to one of this type's type parameters. It seems wasteful to have
// an `i64` in every function type object for this. Consider alternative
// representations that don't have this overhead in the common case.
final int typeParameterOffset;
final List<_Type> typeParameterBounds;
final _Type returnType;
final List<_Type> positionalParameters;
@ -321,6 +322,7 @@ class _FunctionType extends _Type {
@pragma("wasm:entry-point")
const _FunctionType(
this.typeParameterOffset,
this.typeParameterBounds,
this.returnType,
this.positionalParameters,
@ -329,12 +331,24 @@ class _FunctionType extends _Type {
super.isDeclaredNullable);
@override
_Type get _asNonNullable => _FunctionType(typeParameterBounds, returnType,
positionalParameters, requiredParameterCount, namedParameters, false);
_Type get _asNonNullable => _FunctionType(
typeParameterOffset,
typeParameterBounds,
returnType,
positionalParameters,
requiredParameterCount,
namedParameters,
false);
@override
_Type get _asNullable => _FunctionType(typeParameterBounds, returnType,
positionalParameters, requiredParameterCount, namedParameters, true);
_Type get _asNullable => _FunctionType(
typeParameterOffset,
typeParameterBounds,
returnType,
positionalParameters,
requiredParameterCount,
namedParameters,
true);
bool operator ==(Object o) {
if (!(super == o)) return false;
@ -390,9 +404,11 @@ class _FunctionType extends _Type {
s.write("<");
for (int i = 0; i < typeParameterBounds.length; i++) {
if (i > 0) s.write(", ");
// TODO(askesc): Distinguish the depth of function type parameters.
s.write("X$i extends ");
s.write(typeParameterBounds[i]);
s.write("X${typeParameterOffset + i}");
if (!_TypeUniverse.isTopType(typeParameterBounds[i])) {
s.write(" extends ");
s.write(typeParameterBounds[i]);
}
}
s.write(">");
}
@ -432,22 +448,38 @@ class _Environment {
/// outermost function type.
final _Environment? parent;
/// The type parameter bounds of the current function type.
final List<_Type> bounds;
/// The current function type.
final _FunctionType type;
/// The nesting depth of the current function type.
final int depth;
_Environment(this.parent, this.bounds)
_Environment(this.parent, this.type)
: depth = parent == null ? 0 : parent.depth + 1;
/// Look up the bound of a function type parameter in the environment.
_Type lookup(_FunctionTypeParameterType param) {
return adjust(param).lookupAdjusted(param);
}
/// Adjust the environment to the one where the type parameter is declared.
_Environment adjust(_FunctionTypeParameterType param) {
// The `typeParameterOffset` of the function types and the `index` of the
// function type parameters are assigned such that the function type to
// which a type parameter belongs is the innermost function type enclosing
// the type parameter type for which the index falls within the type
// parameter index range of the function type.
_Environment env = this;
while (env.depth != param.depth) {
while (param.index - env.type.typeParameterOffset >=
env.type.typeParameterBounds.length) {
env = env.parent!;
}
return env.bounds[param.index];
return env;
}
/// Look up the bound of a type parameter in its adjusted environment.
_Type lookupAdjusted(_FunctionTypeParameterType param) {
return type.typeParameterBounds[param.index - type.typeParameterOffset];
}
}
@ -523,6 +555,7 @@ class _TypeUniverse {
} else if (type.isFunction) {
_FunctionType functionType = type.as<_FunctionType>();
return _FunctionType(
functionType.typeParameterOffset,
functionType.typeParameterBounds
.map((type) => substituteTypeArgument(type, substitutions))
.toList(),
@ -622,8 +655,8 @@ class _TypeUniverse {
bool isFunctionSubtype(_FunctionType s, _Environment? sEnv, _FunctionType t,
_Environment? tEnv) {
// Set up environments
sEnv = _Environment(sEnv, s.typeParameterBounds);
tEnv = _Environment(tEnv, t.typeParameterBounds);
sEnv = _Environment(sEnv, s);
tEnv = _Environment(tEnv, t);
// Check that [s] and [t] have the same number of type parameters and that
// their bounds are equivalent.
@ -720,9 +753,23 @@ class _TypeUniverse {
if (isBottomType(s)) return true;
// Left Type Variable Bound 1:
// TODO(joshualitt): Implement for generic function type parameters.
if (s.isInterfaceTypeParameterType) {
throw 'Unbound type parameter $s';
if (s.isFunctionTypeParameterType) {
final sTypeParam = s.as<_FunctionTypeParameterType>();
_Environment sEnvAdjusted = sEnv!.adjust(sTypeParam);
// A function type parameter type is a subtype of another function type
// parameter type if they refer to the same type parameter.
if (t.isFunctionTypeParameterType) {
final tTypeParam = t.as<_FunctionTypeParameterType>();
_Environment tEnvAdjusted = tEnv!.adjust(tTypeParam);
if (sEnvAdjusted.depth == tEnvAdjusted.depth &&
sTypeParam.index - sEnvAdjusted.type.typeParameterOffset ==
tTypeParam.index - tEnvAdjusted.type.typeParameterOffset) {
return true;
}
}
// Otherwise, compare the bound to the other type.
_Type bound = sEnvAdjusted.lookupAdjusted(sTypeParam);
return isSubtype(bound, sEnvAdjusted, t, tEnv);
}
// Left Null:
@ -776,15 +823,6 @@ class _TypeUniverse {
// Left Promoted Variable does not apply at runtime.
if (s.isFunctionTypeParameterType) {
// A function type parameter type is a subtype of another function type
// parameter type if they refer to the same type parameter.
if (s == t) return true;
// Otherwise, compare the bound to the other type.
final sTypeParam = s.as<_FunctionTypeParameterType>();
return isSubtype(sEnv!.lookup(sTypeParam), sEnv, t, tEnv);
}
// Function Type / Function:
if (s.isFunction && isFunctionType(t)) {
return true;

View file

@ -0,0 +1,71 @@
// Copyright (c) 2022, 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:expect/expect.dart";
class A {}
class B {}
class C<T> {
void a<X extends A>(void Function<Y extends B>(A) p) {}
void b<X extends A>(void Function<Y extends B>(B) p) {}
void x<X extends A>(void Function<Y extends B>(X) p) {}
void y<X extends A>(void Function<Y extends B>(Y) p) {}
void ar<X extends A>(A Function<Y extends B>() p) {}
void br<X extends A>(B Function<Y extends B>() p) {}
void xr<X extends A>(X Function<Y extends B>() p) {}
void yr<X extends A>(Y Function<Y extends B>() p) {}
bool testA() => a is void Function<X extends A>(T);
bool testB() => b is void Function<X extends A>(T);
bool testX() => x is void Function<X extends A>(T);
bool testY() => y is void Function<X extends A>(T);
bool testAR() => ar is void Function<X extends A>(T);
bool testBR() => br is void Function<X extends A>(T);
bool testXR() => xr is void Function<X extends A>(T);
bool testYR() => yr is void Function<X extends A>(T);
}
typedef AF = void Function<Y extends B>(A);
typedef BF = void Function<Y extends B>(B);
typedef YF = void Function<Y extends B>(Y);
typedef ARF = A Function<Y extends B>();
typedef BRF = B Function<Y extends B>();
typedef YRF = Y Function<Y extends B>();
main() {
Expect.isTrue(C<AF>().testA());
Expect.isFalse(C<AF>().testB());
Expect.isTrue(C<AF>().testX());
Expect.isFalse(C<AF>().testY());
Expect.isFalse(C<BF>().testA());
Expect.isTrue(C<BF>().testB());
Expect.isFalse(C<BF>().testX());
Expect.isTrue(C<BF>().testY());
Expect.isFalse(C<YF>().testA());
Expect.isFalse(C<YF>().testB());
Expect.isFalse(C<YF>().testX());
Expect.isTrue(C<YF>().testY());
Expect.isTrue(C<ARF>().testAR());
Expect.isFalse(C<ARF>().testBR());
Expect.isFalse(C<ARF>().testXR());
Expect.isFalse(C<ARF>().testYR());
Expect.isFalse(C<BRF>().testAR());
Expect.isTrue(C<BRF>().testBR());
Expect.isFalse(C<BRF>().testXR());
Expect.isFalse(C<BRF>().testYR());
Expect.isFalse(C<YRF>().testAR());
Expect.isTrue(C<YRF>().testBR());
Expect.isFalse(C<YRF>().testXR());
Expect.isTrue(C<YRF>().testYR());
}