7d199f2171
Fixes several asserts that were throwing when running with dynamic modules enabled. Bug: https://github.com/dart-lang/sdk/issues/60743 Change-Id: I6bcd3723ecdee784b3d1603e1c87a47e3703a7f3 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/423261 Reviewed-by: Martin Kustermann <kustermann@google.com> Commit-Queue: Nate Biggs <natebiggs@google.com>
1022 lines
39 KiB
Dart
1022 lines
39 KiB
Dart
// 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 'dart:math' show min;
|
|
|
|
import 'package:kernel/ast.dart';
|
|
import 'package:vm/metadata/procedure_attributes.dart';
|
|
import 'package:vm/metadata/table_selector.dart';
|
|
import 'package:wasm_builder/wasm_builder.dart' as w;
|
|
|
|
import 'class_info.dart';
|
|
import 'dynamic_module_kernel_metadata.dart';
|
|
import 'dynamic_modules.dart';
|
|
import 'param_info.dart';
|
|
import 'reference_extensions.dart';
|
|
import 'serialization.dart';
|
|
import 'translator.dart';
|
|
|
|
/// Information for a dispatch table selector.
|
|
///
|
|
/// A selector encapsulates information to generate code that selects the right
|
|
/// member (method, getter, setter) implementation in an instance invocation,
|
|
/// from the dispatch table. Dispatch table is generated by [DispatchTable].
|
|
///
|
|
/// Target of a selector is a method, getter, or setter [Reference]. A target
|
|
/// does not have to correspond to a user-written Dart member, it can be for a
|
|
/// generated one. For example, for torn-off methods, we generate a [Reference]
|
|
/// for the tear-off getter a selector for it.
|
|
class SelectorInfo {
|
|
final DispatchTable dispatchTable;
|
|
|
|
Translator get translator => dispatchTable.translator;
|
|
|
|
/// Unique ID of the selector.
|
|
final int id;
|
|
|
|
/// Number of use sites of the selector.
|
|
final int callCount;
|
|
|
|
/// Least upper bound of [ParameterInfo]s of all targets.
|
|
late final ParameterInfo paramInfo;
|
|
|
|
/// Is this an implicit or explicit setter?
|
|
final bool isSetter;
|
|
|
|
/// Whether we create multiple entry points for the selector.
|
|
///
|
|
/// We create multiple entry points when any implementation of this selector
|
|
/// performs type checks on the passed arguments.
|
|
late bool useMultipleEntryPoints;
|
|
|
|
late bool isDynamicSubmoduleOverridable;
|
|
late bool isDynamicSubmoduleCallable;
|
|
|
|
/// Whether the computation of [paramInfo] should enforce usage of sentinels
|
|
/// for optional parameters.
|
|
late bool _useSentinelForOptionalParameters;
|
|
|
|
/// Wasm function type for the selector.
|
|
///
|
|
/// This should be read after all targets have been added to the selector.
|
|
late final w.FunctionType signature = _computeSignature();
|
|
|
|
/// The selector's member's name.
|
|
final String name;
|
|
|
|
SelectorTargets? _checked;
|
|
SelectorTargets? _unchecked;
|
|
SelectorTargets? _normal;
|
|
|
|
/// The set of references we use to calculate the [paramInfo].
|
|
final List<Reference> _references = [];
|
|
|
|
SelectorTargets targets({required bool unchecked}) {
|
|
if (useMultipleEntryPoints) {
|
|
assert(_checked!.targetRanges.length == _unchecked!.targetRanges.length);
|
|
return unchecked ? _unchecked! : _checked!;
|
|
}
|
|
assert(_checked == null && _unchecked == null);
|
|
return _normal!;
|
|
}
|
|
|
|
SelectorInfo._(
|
|
this.dispatchTable,
|
|
this.id,
|
|
this.name,
|
|
this.callCount, {
|
|
required this.isSetter,
|
|
});
|
|
|
|
void serialize(DataSerializer sink) {
|
|
sink.writeInt(id);
|
|
sink.writeString(name);
|
|
sink.writeInt(callCount);
|
|
sink.writeBoolList([
|
|
isSetter,
|
|
useMultipleEntryPoints,
|
|
isDynamicSubmoduleOverridable,
|
|
isDynamicSubmoduleCallable,
|
|
_useSentinelForOptionalParameters,
|
|
]);
|
|
sink.writeNullable(_checked, (targets) => targets.serialize(sink));
|
|
sink.writeNullable(_unchecked, (targets) => targets.serialize(sink));
|
|
sink.writeNullable(_normal, (targets) => targets.serialize(sink));
|
|
sink.writeList(_references, sink.writeReference);
|
|
}
|
|
|
|
factory SelectorInfo.deserialize(
|
|
DataDeserializer source, DispatchTable dispatchTable) {
|
|
final id = source.readInt();
|
|
final name = source.readString();
|
|
final callCount = source.readInt();
|
|
final [
|
|
isSetter,
|
|
useMultipleEntryPoints,
|
|
isDynamicSubmoduleOverridable,
|
|
isDynamicSubmoduleCallable,
|
|
useSentinelForOptionalParameters,
|
|
] = source.readBoolList();
|
|
final checked =
|
|
source.readNullable(() => SelectorTargets.deserialize(source));
|
|
final unchecked =
|
|
source.readNullable(() => SelectorTargets.deserialize(source));
|
|
final normal =
|
|
source.readNullable(() => SelectorTargets.deserialize(source));
|
|
final references = source.readList(source.readReference);
|
|
|
|
final paramInfo = _parameterInfoFromReferences(
|
|
references, useSentinelForOptionalParameters);
|
|
return SelectorInfo._(dispatchTable, id, name, callCount,
|
|
isSetter: isSetter)
|
|
..useMultipleEntryPoints = useMultipleEntryPoints
|
|
..isDynamicSubmoduleCallable = isDynamicSubmoduleCallable
|
|
..isDynamicSubmoduleOverridable = isDynamicSubmoduleOverridable
|
|
.._useSentinelForOptionalParameters = useSentinelForOptionalParameters
|
|
.._checked = checked
|
|
.._unchecked = unchecked
|
|
.._normal = normal
|
|
.._references.addAll(references)
|
|
..paramInfo = paramInfo;
|
|
}
|
|
|
|
String entryPointName(bool unchecked) {
|
|
if (!useMultipleEntryPoints) return name;
|
|
return '$name (${unchecked ? 'unchecked' : 'checked'})';
|
|
}
|
|
|
|
/// Compute the signature for the functions implementing members targeted by
|
|
/// this selector.
|
|
///
|
|
/// When the selector has multiple targets, the type of each parameter/return
|
|
/// is the upper bound across all targets, such that all targets have the
|
|
/// same signature, and the actual representation types of the parameters and
|
|
/// returns are subtypes (resp. supertypes) of the types in the signature.
|
|
w.FunctionType _computeSignature() {
|
|
var nameIndex = paramInfo.nameIndex;
|
|
final int returnCount = isSetter ? 0 : 1;
|
|
List<Set<w.ValueType>> inputSets =
|
|
List.generate(1 + paramInfo.paramCount, (_) => {});
|
|
List<Set<w.ValueType>> outputSets = List.generate(returnCount, (_) => {});
|
|
List<bool> ensureBoxed = List.filled(1 + paramInfo.paramCount, false);
|
|
Iterable<({Reference target, Range range})> targetRanges =
|
|
targets(unchecked: false).targetRanges;
|
|
for (final (range: _, :target) in targetRanges) {
|
|
Member member = target.asMember;
|
|
DartType receiver =
|
|
InterfaceType(member.enclosingClass!, Nullability.nonNullable);
|
|
List<DartType> positional;
|
|
Map<String, DartType> named;
|
|
List<DartType> returns;
|
|
if (member is Field) {
|
|
if (target.isImplicitGetter) {
|
|
positional = const [];
|
|
named = const {};
|
|
returns = [translator.typeOfReturnValue(member)];
|
|
} else {
|
|
positional = [member.setterType];
|
|
named = const {};
|
|
returns = const [];
|
|
}
|
|
} else {
|
|
FunctionNode function = member.function!;
|
|
if (target.isTearOffReference) {
|
|
positional = const [];
|
|
named = const {};
|
|
returns = [function.computeFunctionType(Nullability.nonNullable)];
|
|
} else {
|
|
final typeForParam = translator.typeOfParameterVariable;
|
|
positional = [
|
|
for (int i = 0; i < function.positionalParameters.length; i++)
|
|
typeForParam(function.positionalParameters[i],
|
|
i < function.requiredParameterCount)
|
|
];
|
|
named = {
|
|
for (VariableDeclaration param in function.namedParameters)
|
|
param.name!: typeForParam(param, param.isRequired)
|
|
};
|
|
returns = target.isSetter
|
|
? const []
|
|
: [translator.typeOfReturnValue(member)];
|
|
}
|
|
}
|
|
assert(returns.length <= outputSets.length);
|
|
inputSets[0].add(isDynamicSubmoduleOverridable
|
|
? translator.topInfo.nonNullableType
|
|
: translator.translateType(receiver));
|
|
for (int i = 0; i < positional.length; i++) {
|
|
DartType type = positional[i];
|
|
inputSets[1 + i].add(translator.translateType(type));
|
|
ensureBoxed[1 + i] |=
|
|
paramInfo.positional[i] == ParameterInfo.defaultValueSentinel;
|
|
}
|
|
for (String name in named.keys) {
|
|
int i = nameIndex[name]!;
|
|
DartType type = named[name]!;
|
|
inputSets[1 + i].add(translator.translateType(type));
|
|
ensureBoxed[1 + i] |=
|
|
paramInfo.named[name] == ParameterInfo.defaultValueSentinel;
|
|
}
|
|
for (int i = 0; i < returnCount; i++) {
|
|
if (i < returns.length) {
|
|
DartType type = returns[i];
|
|
outputSets[i].add(translator.translateReturnType(type));
|
|
} else {
|
|
outputSets[i].add(translator.topInfo.nullableType);
|
|
}
|
|
}
|
|
}
|
|
|
|
List<w.ValueType> typeParameters = List.filled(paramInfo.typeParamCount,
|
|
translator.classInfo[translator.typeClass]!.nonNullableType);
|
|
List<w.ValueType> inputs = List.generate(
|
|
inputSets.length,
|
|
(i) => _upperBound(inputSets[i],
|
|
ensureBoxed: ensureBoxed[i], isReceiver: i == 0));
|
|
if (name == '==') {
|
|
// == can't be called with null
|
|
inputs[1] = inputs[1].withNullability(false);
|
|
}
|
|
List<w.ValueType> outputs = List.generate(outputSets.length,
|
|
(i) => _upperBound(outputSets[i], ensureBoxed: false));
|
|
return translator.typesBuilder.defineFunction(
|
|
[inputs[0], ...typeParameters, ...inputs.sublist(1)], outputs);
|
|
}
|
|
|
|
w.ValueType _upperBound(Set<w.ValueType> types,
|
|
{required bool ensureBoxed, bool isReceiver = false}) {
|
|
if (types.isEmpty) {
|
|
// This happens if the selector doesn't have any targets. Any call site of
|
|
// such a selector is unreachable. Though such call sites still have to
|
|
// evaluate receiver and arguments. Doing so requires the signature. So we
|
|
// create a dummy signature with top types. Receivers specifically should
|
|
// be non-nullable since we must be invoking a selector on some object.
|
|
assert(!isReceiver || isDynamicSubmoduleOverridable);
|
|
return isReceiver
|
|
? translator.topInfo.nonNullableType
|
|
: translator.topInfo.nullableType;
|
|
}
|
|
if (!ensureBoxed && types.length == 1 && types.single.isPrimitive) {
|
|
// Unboxed primitive.
|
|
return types.single;
|
|
}
|
|
final bool nullable = types.any((type) => type.nullable);
|
|
int minDepth = 999999999;
|
|
Set<w.DefType> heapTypes = types
|
|
.where((type) => type is! w.RefType || type.heapType is w.DefType)
|
|
.map((type) {
|
|
w.DefType def = type is w.RefType
|
|
? type.heapType as w.DefType
|
|
: translator.classInfo[translator.boxedClasses[type]!]!.struct;
|
|
minDepth = min(minDepth, def.depth);
|
|
return def;
|
|
}).toSet();
|
|
if (heapTypes.isEmpty) {
|
|
// Only abstract heap types.
|
|
Set<w.HeapType> heapTypes =
|
|
types.map((type) => (type as w.RefType).heapType).toSet();
|
|
return w.RefType(heapTypes.single, nullable: nullable);
|
|
}
|
|
int targetDepth = minDepth;
|
|
while (heapTypes.length > 1) {
|
|
heapTypes = heapTypes.map((s) {
|
|
while (s.depth > targetDepth) {
|
|
s = s.superType!;
|
|
}
|
|
return s;
|
|
}).toSet();
|
|
targetDepth -= 1;
|
|
}
|
|
return w.RefType.def(heapTypes.single, nullable: nullable);
|
|
}
|
|
|
|
late final Set<Reference> _targetSet = useMultipleEntryPoints
|
|
? {
|
|
..._checked!._targetSet,
|
|
..._unchecked!._targetSet,
|
|
}
|
|
: _normal!._targetSet;
|
|
|
|
bool containsTarget(Reference target) => _targetSet.contains(target);
|
|
}
|
|
|
|
class SelectorTargets {
|
|
/// Targets for all concrete classes implementing this selector.
|
|
///
|
|
/// As a subclass hierarchy often inherits the same target, we associate the
|
|
/// target with a range of class ids. The ranges are non-empty,
|
|
/// non-overlapping and sorted in ascending order.
|
|
final List<({Range range, Reference target})> targetRanges;
|
|
|
|
/// Targets that a interface call will check & directly call before falling
|
|
/// back to dispatch table calls.
|
|
///
|
|
/// The targets in here are mainly the ones annotated with
|
|
/// `@pragma('wasm:static-dispatch')`. The compiler will generate then code
|
|
/// that first checks the receiver for those targets directly and issue direct
|
|
/// calls before falling back to dispatch table calls.
|
|
final List<({Range range, Reference target})> staticDispatchRanges;
|
|
|
|
/// Offset of the selector in the dispatch table.
|
|
///
|
|
/// For a class in [targetRanges], `class ID + offset` gives the offset of the
|
|
/// class member for this selector.
|
|
int? offset;
|
|
|
|
SelectorTargets(this.targetRanges, this.staticDispatchRanges);
|
|
|
|
late final Set<Reference> _targetSet =
|
|
targetRanges.map((e) => e.target).toSet();
|
|
|
|
void serialize(DataSerializer sink) {
|
|
sink.writeInt(offset == null ? 0 : offset! + 1);
|
|
sink.writeInt(targetRanges.length);
|
|
for (final (:range, :target) in targetRanges) {
|
|
range.serialize(sink);
|
|
sink.writeReference(target);
|
|
}
|
|
sink.writeInt(staticDispatchRanges.length);
|
|
for (final (:range, :target) in staticDispatchRanges) {
|
|
range.serialize(sink);
|
|
sink.writeReference(target);
|
|
}
|
|
}
|
|
|
|
factory SelectorTargets.deserialize(DataDeserializer source) {
|
|
final offset = source.readInt();
|
|
final targetRangesLength = source.readInt();
|
|
final targetRanges = <({Range range, Reference target})>[];
|
|
for (int i = 0; i < targetRangesLength; i++) {
|
|
final range = Range.deserialize(source);
|
|
final target = source.readReference();
|
|
targetRanges.add((range: range, target: target));
|
|
}
|
|
final staticDispatchRangesLength = source.readInt();
|
|
final staticDispatchRanges = <({Range range, Reference target})>[];
|
|
for (int i = 0; i < staticDispatchRangesLength; i++) {
|
|
final range = Range.deserialize(source);
|
|
final target = source.readReference();
|
|
staticDispatchRanges.add((range: range, target: target));
|
|
}
|
|
return SelectorTargets(targetRanges, staticDispatchRanges)
|
|
..offset = offset == 0 ? null : offset - 1;
|
|
}
|
|
}
|
|
|
|
/// Builds the dispatch table for member calls.
|
|
class DispatchTable {
|
|
static const _functionType = w.RefType.func(nullable: true);
|
|
final bool isDynamicSubmoduleTable;
|
|
|
|
late final Map<TreeNode, ProcedureAttributesMetadata>
|
|
procedureAttributeMetadata =
|
|
translator.isDynamicSubmodule && !isDynamicSubmoduleTable
|
|
? (translator.component
|
|
.metadata[dynamicMainModuleProcedureAttributeMetadataTag]
|
|
as ProcedureAttributesMetadataRepository)
|
|
.mapping
|
|
: translator.procedureAttributeMetadata;
|
|
|
|
late final Translator translator;
|
|
|
|
late final List<TableSelectorInfo> _selectorMetadata =
|
|
translator.isDynamicSubmodule && !isDynamicSubmoduleTable
|
|
? (translator.component.metadata[dynamicMainModuleSelectorMetadataTag]
|
|
as TableSelectorMetadataRepository)
|
|
.mapping[translator.component]!
|
|
.selectors
|
|
: (translator.component
|
|
.metadata[TableSelectorMetadataRepository.repositoryTag]
|
|
as TableSelectorMetadataRepository)
|
|
.mapping[translator.component]!
|
|
.selectors;
|
|
late final int minClassId = isDynamicSubmoduleTable
|
|
? translator.classIdNumbering.firstDynamicSubmoduleClassId
|
|
: 0;
|
|
late final int maxClassId = isDynamicSubmoduleTable
|
|
? translator.classIdNumbering.maxDynamicSubmoduleConcreteClassId!
|
|
: translator.classIdNumbering.maxConcreteClassId;
|
|
|
|
/// Maps selector IDs to selectors.
|
|
final Map<int, SelectorInfo> _selectorInfo = {};
|
|
|
|
/// Maps member names to getter selectors with the same member name.
|
|
final Map<String, Set<SelectorInfo>> _dynamicGetters = {};
|
|
|
|
/// Maps member names to setter selectors with the same member name.
|
|
final Map<String, Set<SelectorInfo>> _dynamicSetters = {};
|
|
|
|
/// Maps member names to method selectors with the same member name.
|
|
final Map<String, Set<SelectorInfo>> _dynamicMethods = {};
|
|
|
|
/// Contents of [_definedWasmTable]. For a selector with ID S and a target
|
|
/// class of the selector with ID C, `table[S + C]` gives the reference to the
|
|
/// class member for the selector.
|
|
late final List<Reference?> _table;
|
|
|
|
late final w.TableBuilder _definedWasmTable;
|
|
late final WasmTableImporter _importedWasmTables =
|
|
WasmTableImporter(translator, 'dispatch');
|
|
|
|
/// The Wasm table for the dispatch table.
|
|
w.Table getWasmTable(w.ModuleBuilder module) =>
|
|
_importedWasmTables.get(_definedWasmTable, module);
|
|
|
|
DispatchTable({this.isDynamicSubmoduleTable = false});
|
|
|
|
void serialize(DataSerializer sink) {
|
|
sink.writeList(_selectorInfo.values, (s) => s.serialize(sink));
|
|
sink.writeList(_table, (r) => sink.writeNullable(r, sink.writeReference));
|
|
// Preserve call selectors for closure calls which are handled dynamically.
|
|
final callSelectors = _dynamicGetters['call']!;
|
|
sink.writeList(callSelectors, (s) => sink.writeInt(s.id));
|
|
}
|
|
|
|
factory DispatchTable.deserialize(DataDeserializer source) {
|
|
final dispatchTable = DispatchTable();
|
|
|
|
final selectors =
|
|
source.readList(() => SelectorInfo.deserialize(source, dispatchTable));
|
|
final table = source
|
|
.readList(() => source.readNullable(() => source.readReference()));
|
|
final callSelectorIds = source.readList(source.readInt);
|
|
|
|
for (final selector in selectors) {
|
|
dispatchTable._selectorInfo[selector.id] = selector;
|
|
}
|
|
dispatchTable._table = table;
|
|
|
|
// Preserve call selectors for closure calls which are handled dynamically.
|
|
final callSelectors = <SelectorInfo>{};
|
|
for (final selectorId in callSelectorIds) {
|
|
callSelectors.add(dispatchTable._selectorInfo[selectorId]!);
|
|
}
|
|
dispatchTable._dynamicGetters['call'] = callSelectors;
|
|
|
|
return dispatchTable;
|
|
}
|
|
|
|
SelectorInfo selectorForTarget(Reference target) {
|
|
Member member = target.asMember;
|
|
bool isGetter = target.isGetter || target.isTearOffReference;
|
|
ProcedureAttributesMetadata metadata = procedureAttributeMetadata[member]!;
|
|
int selectorId = isGetter
|
|
? metadata.getterSelectorId
|
|
: metadata.methodOrSetterSelectorId;
|
|
return _selectorInfo[selectorId]!;
|
|
}
|
|
|
|
SelectorInfo _createSelectorForTarget(Reference target) {
|
|
Member member = target.asMember;
|
|
bool isGetter = target.isGetter || target.isTearOffReference;
|
|
bool isSetter = target.isSetter;
|
|
ProcedureAttributesMetadata metadata = procedureAttributeMetadata[member]!;
|
|
int selectorId = isGetter
|
|
? metadata.getterSelectorId
|
|
: metadata.methodOrSetterSelectorId;
|
|
|
|
// _WasmBase and its subclass methods cannot be called dynamically
|
|
final cls = member.enclosingClass;
|
|
final isWasmType = cls != null && translator.isWasmType(cls);
|
|
|
|
final calledDynamically = !isWasmType &&
|
|
(metadata.getterCalledDynamically ||
|
|
metadata.methodOrSetterCalledDynamically ||
|
|
member.name.text == "call");
|
|
|
|
// The compiler will generate calls to `noSuchMethod` in the dynamic
|
|
// invocation forwarders. So we ensure that the call count is positive.
|
|
final isNoSuchMethod = member == translator.objectNoSuchMethod;
|
|
final callCount =
|
|
_selectorMetadata[selectorId].callCount + (isNoSuchMethod ? 1 : 0);
|
|
final selector = _selectorInfo.putIfAbsent(
|
|
selectorId,
|
|
() => SelectorInfo._(this, selectorId, member.name.text, callCount,
|
|
isSetter: isSetter));
|
|
assert(selector.isSetter == isSetter);
|
|
selector._references.add(target);
|
|
|
|
if (calledDynamically) {
|
|
if (isGetter) {
|
|
(_dynamicGetters[member.name.text] ??= {}).add(selector);
|
|
} else if (isSetter) {
|
|
(_dynamicSetters[member.name.text] ??= {}).add(selector);
|
|
} else {
|
|
(_dynamicMethods[member.name.text] ??= {}).add(selector);
|
|
}
|
|
}
|
|
|
|
return selector;
|
|
}
|
|
|
|
/// Get selectors for getters and tear-offs with the given name.
|
|
Iterable<SelectorInfo> dynamicGetterSelectors(String memberName) =>
|
|
_dynamicGetters[memberName] ?? Iterable.empty();
|
|
|
|
/// Get selectors for setters with the given name.
|
|
Iterable<SelectorInfo> dynamicSetterSelectors(String memberName) =>
|
|
_dynamicSetters[memberName] ?? Iterable.empty();
|
|
|
|
/// Get selectors for methods with the given name.
|
|
Iterable<SelectorInfo> dynamicMethodSelectors(String memberName) =>
|
|
_dynamicMethods[memberName] ?? Iterable.empty();
|
|
|
|
void _initializeWasmTable() {
|
|
final module = isDynamicSubmoduleTable
|
|
? translator.dynamicSubmodule
|
|
: translator.mainModule;
|
|
_definedWasmTable = module.tables.define(_functionType, _table.length);
|
|
if (!isDynamicSubmoduleTable) {
|
|
for (final module in translator.modules) {
|
|
// Ensure the dispatch table is imported into every module as the first
|
|
// table.
|
|
getWasmTable(module);
|
|
}
|
|
}
|
|
}
|
|
|
|
void build() {
|
|
if (!isDynamicSubmoduleTable && translator.isDynamicSubmodule) {
|
|
_initializeWasmTable();
|
|
return;
|
|
}
|
|
// Collect class/selector combinations
|
|
|
|
// Maps class to selector IDs of the class
|
|
final selectorsInClass = <Class, Map<SelectorInfo, Reference>>{};
|
|
final staticDispatchPragmas = <Reference>{};
|
|
|
|
// Add classes to selector targets for their members
|
|
for (ClassInfo info in translator.classesSupersFirst) {
|
|
final Class cls = info.cls ?? translator.coreTypes.objectClass;
|
|
final Map<SelectorInfo, Reference> selectors;
|
|
|
|
// Add the class to its inherited members' selectors. Skip `_WasmBase`:
|
|
// it's defined as a Dart class (in `dart._wasm` library) but it's special
|
|
// and does not inherit from `Object`.
|
|
final ClassInfo? superInfo = info.superInfo;
|
|
if (superInfo == null || cls == translator.wasmTypesBaseClass) {
|
|
selectors = {};
|
|
} else {
|
|
final Class superCls =
|
|
superInfo.cls ?? translator.coreTypes.objectClass;
|
|
selectors = Map.of(selectorsInClass[superCls]!);
|
|
}
|
|
|
|
final classIsDynamicSubmoduleExtendable =
|
|
cls.isDynamicSubmoduleExtendable(translator.coreTypes);
|
|
|
|
/// Add a method (or getter, setter) of the current class ([info]) to
|
|
/// [reference]'s selector's targets.
|
|
///
|
|
/// Because we visit a superclass before its subclasses, if the class
|
|
/// inherits [reference], then the selector will already have a target
|
|
/// for the class. Override that target if [reference] is a not abstract.
|
|
/// If it's abstract, then the superclass's method will be called, so do
|
|
/// not update the target.
|
|
void addMember(Reference reference, bool staticDispatch) {
|
|
SelectorInfo selector = _createSelectorForTarget(reference);
|
|
if (reference.asMember.isAbstract) {
|
|
// Reference is abstract, do not override inherited concrete member
|
|
selectors[selector] ??= reference;
|
|
} else {
|
|
// Reference is concrete, override inherited member
|
|
selectors[selector] = reference;
|
|
|
|
if (staticDispatch) staticDispatchPragmas.add(reference);
|
|
}
|
|
}
|
|
|
|
// Add the class to its non-static members' selectors. If `info.cls` is
|
|
// `null`, that means [info] represents the `#Top` type, which is not a
|
|
// Dart class but has the members of `Object`.
|
|
for (Member member in cls.members) {
|
|
// Skip static members
|
|
if (!member.isInstanceMember) {
|
|
continue;
|
|
}
|
|
final bool staticDispatch =
|
|
translator.getPragma<bool>(member, 'wasm:static-dispatch', true) ??
|
|
false;
|
|
if (member is Field) {
|
|
addMember(member.getterReference, staticDispatch);
|
|
if (member.hasSetter) {
|
|
final target = member.setterReference!;
|
|
addMember(target, staticDispatch);
|
|
}
|
|
} else if (member is Procedure) {
|
|
final target = member.reference;
|
|
addMember(target, staticDispatch);
|
|
final procedureMetadata = procedureAttributeMetadata[member]!;
|
|
// `hasTearOffUses` can be true for operators as well, even though
|
|
// it's not possible to tear-off an operator. (no syntax for it)
|
|
if (member.kind == ProcedureKind.Method &&
|
|
(procedureMetadata.hasTearOffUses ||
|
|
// If the member can be invoked from a dynamic submodule then
|
|
// we need to include the tearoff too.
|
|
member.isDynamicSubmoduleCallable(translator.coreTypes) ||
|
|
classIsDynamicSubmoduleExtendable)) {
|
|
addMember(member.tearOffReference, staticDispatch);
|
|
}
|
|
}
|
|
}
|
|
selectorsInClass[cls] = selectors;
|
|
}
|
|
|
|
final selectorTargets = <SelectorInfo, Map<int, Reference>>{};
|
|
for (int classId = minClassId; classId <= maxClassId; ++classId) {
|
|
final cls = translator.classes[classId].cls;
|
|
if (cls != null) {
|
|
selectorsInClass[cls]!.forEach((selectorInfo, target) {
|
|
if (!target.asMember.isAbstract) {
|
|
selectorTargets.putIfAbsent(selectorInfo, () => {})[classId] =
|
|
target;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
_selectorInfo.forEach((_, selector) {
|
|
bool isDynamicSubmoduleCallable = false;
|
|
bool isDynamicSubmoduleOverridable = false;
|
|
for (final target in selector._references) {
|
|
final member = target.asMember;
|
|
isDynamicSubmoduleOverridable |=
|
|
member.isDynamicSubmoduleOverridable(translator.coreTypes);
|
|
isDynamicSubmoduleCallable |=
|
|
member.isDynamicSubmoduleCallable(translator.coreTypes);
|
|
}
|
|
selector.isDynamicSubmoduleOverridable = isDynamicSubmoduleOverridable;
|
|
selector.isDynamicSubmoduleCallable = isDynamicSubmoduleCallable;
|
|
|
|
if (!selectorTargets.containsKey(selector)) {
|
|
// There are no concrete implementations for the given [selector].
|
|
selector._normal = SelectorTargets([], []);
|
|
selector.useMultipleEntryPoints = false;
|
|
selector._useSentinelForOptionalParameters = true;
|
|
selector.paramInfo = _parameterInfoFromReferences(
|
|
selector._references, selector._useSentinelForOptionalParameters);
|
|
} else {
|
|
// Will be initialized in the `selectorTargets.forEach()` below.
|
|
}
|
|
});
|
|
selectorTargets
|
|
.forEach((SelectorInfo selector, Map<int, Reference> targets) {
|
|
final List<({Range range, Reference target})> ranges = targets.entries
|
|
.map((entry) =>
|
|
(range: Range(entry.key, entry.key), target: entry.value))
|
|
.toList()
|
|
..sort((a, b) => a.range.start.compareTo(b.range.start));
|
|
assert(ranges.isNotEmpty);
|
|
int writeIndex = 0;
|
|
for (int readIndex = 1; readIndex < ranges.length; ++readIndex) {
|
|
final current = ranges[writeIndex];
|
|
final next = ranges[readIndex];
|
|
assert(next.range.length == 1);
|
|
if ((current.range.end + 1) == next.range.start &&
|
|
identical(current.target, next.target)) {
|
|
ranges[writeIndex] = (
|
|
range: Range(current.range.start, next.range.end),
|
|
target: current.target
|
|
);
|
|
} else {
|
|
ranges[++writeIndex] = next;
|
|
}
|
|
}
|
|
ranges.length = writeIndex + 1;
|
|
|
|
bool useMultipleEntryPoints = false;
|
|
final implementationReferences = <Reference>[];
|
|
for (final targetRange in ranges) {
|
|
final target = targetRange.target;
|
|
final member = target.asMember;
|
|
assert(!member.isAbstract);
|
|
|
|
// Compute [useMultipleEntryPoints]
|
|
if (!member.isExternal &&
|
|
!target.isGetter &&
|
|
!target.isTearOffReference &&
|
|
translator.needToCheckTypesFor(member)) {
|
|
useMultipleEntryPoints = true;
|
|
}
|
|
implementationReferences.add(target);
|
|
}
|
|
selector.useMultipleEntryPoints = useMultipleEntryPoints;
|
|
|
|
if (!selector.isDynamicSubmoduleOverridable &&
|
|
implementationReferences.isNotEmpty) {
|
|
// We have global knowledge of all targets of the selector. We can use
|
|
// this global knowledge to compute the [ParameterInfo].
|
|
selector._references.clear();
|
|
selector._references.addAll(implementationReferences);
|
|
selector._useSentinelForOptionalParameters = false;
|
|
} else {
|
|
// Case 1) We may have no targets for the selector, but there may
|
|
// still be calls to it (see e.g. https://dartbug.com/60733).
|
|
//
|
|
// Case 2) We may have 3rd party implementations of the selector
|
|
// in a dynamic module with unknown default values for optionals.
|
|
//
|
|
// => We make caller pass sentinel if the optional parameter is not
|
|
// provided.
|
|
selector._useSentinelForOptionalParameters = true;
|
|
}
|
|
selector.paramInfo = _parameterInfoFromReferences(
|
|
selector._references, selector._useSentinelForOptionalParameters);
|
|
|
|
// Split up [ranges] into those that are statically dispatched to and
|
|
// those are used via dispatch table.
|
|
final staticDispatchRanges = selector.isDynamicSubmoduleOverridable
|
|
? const <({Range range, Reference target})>[]
|
|
: (translator.options.polymorphicSpecialization || ranges.length == 1)
|
|
? ranges
|
|
: ranges
|
|
.where(
|
|
(range) => staticDispatchPragmas.contains(range.target))
|
|
.toList();
|
|
if (selector.useMultipleEntryPoints) {
|
|
({Range range, Reference target}) getChecked(
|
|
({Range range, Reference target}) targetRange,
|
|
bool unchecked,
|
|
) =>
|
|
(
|
|
range: targetRange.range,
|
|
target: translator.getFunctionEntry(targetRange.target,
|
|
uncheckedEntry: unchecked)
|
|
);
|
|
final checkedTargets = SelectorTargets(
|
|
ranges.map((r) => getChecked(r, false)).toList(),
|
|
staticDispatchRanges.map((r) => getChecked(r, false)).toList(),
|
|
);
|
|
final uncheckedTargets = SelectorTargets(
|
|
ranges.map((r) => getChecked(r, true)).toList(),
|
|
staticDispatchRanges.map((r) => getChecked(r, true)).toList(),
|
|
);
|
|
selector._checked = checkedTargets;
|
|
selector._unchecked = uncheckedTargets;
|
|
} else {
|
|
final normalTargets = SelectorTargets(ranges, staticDispatchRanges);
|
|
selector._normal = normalTargets;
|
|
}
|
|
});
|
|
|
|
// Assign selector offsets
|
|
|
|
final List<SelectorInfo> selectors =
|
|
selectorTargets.keys.where(_isUsedViaDispatchTableCall).toList();
|
|
|
|
// Sort the selectors based on number of targets and number of use sites.
|
|
// This is a heuristic to keep the table small.
|
|
//
|
|
// Place selectors with more targets first as they are less likely to fit
|
|
// into the gaps left by selectors placed earlier.
|
|
//
|
|
// Among the selectors with approximately same number of targets, place
|
|
// more used ones first, as the smaller selector offset will have a smaller
|
|
// instruction encoding.
|
|
int selectorSortWeight(SelectorInfo selector) =>
|
|
selectorTargets[selector]!.length * 10 + selector.callCount;
|
|
|
|
selectors.sort((a, b) => selectorSortWeight(b) - selectorSortWeight(a));
|
|
|
|
final rows = <Row<Reference>>[];
|
|
for (final selector in selectors) {
|
|
Row<Reference> buildRow(
|
|
List<({Range range, Reference target})> targetRanges) {
|
|
final rowValues = <({int index, Reference value})>[];
|
|
for (final (:range, :target) in targetRanges) {
|
|
for (int classId = range.start; classId <= range.end; ++classId) {
|
|
final adjustedClassId = classId - minClassId;
|
|
rowValues.add((index: adjustedClassId, value: target));
|
|
}
|
|
}
|
|
rowValues.sort((a, b) => a.index.compareTo(b.index));
|
|
return Row(rowValues);
|
|
}
|
|
|
|
if (selector.useMultipleEntryPoints) {
|
|
rows.add(buildRow((selector._checked!.targetRanges)));
|
|
rows.add(buildRow((selector._unchecked!.targetRanges)));
|
|
} else {
|
|
rows.add(buildRow((selector._normal!.targetRanges)));
|
|
}
|
|
}
|
|
|
|
_table = buildRowDisplacementTable<Reference>(rows);
|
|
|
|
int rowIndex = 0;
|
|
for (final selector in selectors) {
|
|
if (selector.useMultipleEntryPoints) {
|
|
selector._checked!.offset = rows[rowIndex++].offset;
|
|
selector._unchecked!.offset = rows[rowIndex++].offset;
|
|
} else {
|
|
selector._normal!.offset = rows[rowIndex++].offset;
|
|
}
|
|
}
|
|
|
|
_initializeWasmTable();
|
|
}
|
|
|
|
void output() {
|
|
final Map<w.BaseFunction, w.BaseFunction> wrappedDynamicSubmoduleImports =
|
|
{};
|
|
for (int i = 0; i < _table.length; i++) {
|
|
Reference? target = _table[i];
|
|
if (target != null) {
|
|
w.BaseFunction? fun = translator.functions.getExistingFunction(target);
|
|
// Any call to the dispatch table is guaranteed to hit a target.
|
|
//
|
|
// If a target is in a deferred module and that deferred module hasn't
|
|
// been loaded yet, then the entry is `null`.
|
|
//
|
|
// Though we can only hit a target if that target's class has been
|
|
// allocated. In order for the class to be allocated, the deferred
|
|
// module must've been loaded to call the constructor.
|
|
if (fun != null) {
|
|
final targetModule = fun.enclosingModule;
|
|
if (targetModule == _definedWasmTable.enclosingModule) {
|
|
if (isDynamicSubmoduleTable &&
|
|
targetModule == translator.dynamicSubmodule &&
|
|
fun is w.ImportedFunction) {
|
|
// Functions imported into submodules may need to be wrapped to
|
|
// match the updated dispatch table signature.
|
|
fun = wrappedDynamicSubmoduleImports[fun] ??=
|
|
_wrapDynamicSubmoduleFunction(target, fun);
|
|
}
|
|
_definedWasmTable.setElement(i, fun);
|
|
} else {
|
|
// This will generate the imported table if it doesn't already
|
|
// exist.
|
|
(getWasmTable(targetModule) as w.ImportedTable).setElements[i] =
|
|
fun;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
w.BaseFunction _wrapDynamicSubmoduleFunction(
|
|
Reference target, w.BaseFunction importedFunction) {
|
|
final mainSelector =
|
|
translator.dynamicMainModuleDispatchTable!.selectorForTarget(target);
|
|
final mainSignature = translator.signatureForMainModule(target);
|
|
final localSelector = translator.dispatchTable.selectorForTarget(target);
|
|
final localSignature = localSelector.signature;
|
|
|
|
// If the type is the same in both the main module and the submodule, use
|
|
// the imported function itself.
|
|
if (mainSignature.isStructurallyEqualTo(localSignature)) {
|
|
return importedFunction;
|
|
}
|
|
|
|
// Otherwise we need to create a wrapper to handle the differing types.
|
|
// The local signature should include all the parameters necessary to call
|
|
// the target in main since the local signature must include the target
|
|
// member itself and any other members in the main module's selector range.
|
|
final wrapper = translator.dynamicSubmodule.functions
|
|
.define(localSignature, '${target.asMember} wrapper');
|
|
|
|
final ib = wrapper.body;
|
|
|
|
assert(mainSignature.inputs.length <= localSignature.inputs.length);
|
|
|
|
final mainModulePreParamCount =
|
|
(mainSelector.paramInfo.takesContextOrReceiver ? 1 : 0) +
|
|
mainSelector.paramInfo.typeParamCount;
|
|
final mainModuleBeforeNamedCount =
|
|
mainModulePreParamCount + mainSelector.paramInfo.positional.length;
|
|
int mainIndex = 0;
|
|
for (; mainIndex < mainModuleBeforeNamedCount; mainIndex++) {
|
|
final local = ib.locals[mainIndex];
|
|
ib.local_get(local);
|
|
translator.convertType(ib, local.type, mainSignature.inputs[mainIndex]);
|
|
}
|
|
|
|
final localPreParamCount =
|
|
(localSelector.paramInfo.takesContextOrReceiver ? 1 : 0) +
|
|
localSelector.paramInfo.typeParamCount;
|
|
|
|
for (final name in mainSelector.paramInfo.names) {
|
|
final namedIndex = localSelector.paramInfo.nameIndex[name]!;
|
|
final local = ib.locals[localPreParamCount + namedIndex];
|
|
ib.local_get(local);
|
|
translator.convertType(ib, local.type, mainSignature.inputs[mainIndex++]);
|
|
}
|
|
ib.call(importedFunction);
|
|
translator.convertType(
|
|
ib, mainSignature.outputs.single, localSignature.outputs.single);
|
|
ib.end();
|
|
|
|
return wrapper;
|
|
}
|
|
}
|
|
|
|
bool _isUsedViaDispatchTableCall(SelectorInfo selector) {
|
|
// If there's no callers in this module and no callers in dynamic modules then
|
|
// there's no need for us to create a dispatch table entry.
|
|
if (selector.callCount == 0 && !selector.isDynamicSubmoduleCallable) {
|
|
return false;
|
|
}
|
|
|
|
final targets = selector.targets(unchecked: false);
|
|
|
|
// If there's 0 or 1 target than the call sites will either emit an
|
|
// unreachable or a direct call, so no call site goes via dispatch table, so
|
|
// we don't need a row in the table.
|
|
if (targets.targetRanges.length <= 1) return false;
|
|
|
|
// If all targets are dispatched to via static polymorphic dispatchers,
|
|
// then no callsite will emit a dispatch table call, so we don't need a row in
|
|
// the table.
|
|
if (targets.staticDispatchRanges.length == targets.targetRanges.length) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
ParameterInfo _parameterInfoFromReferences(
|
|
List<Reference> references, bool useDefaultValueSentinel) {
|
|
// We know all target implementations (closed world) if all of them use
|
|
// the same default value for optionals, we can make the caller pass it.
|
|
final first = references.first;
|
|
final paramInfo = ParameterInfo.fromMember(
|
|
first, useDefaultValueSentinel || first.asMember.isAbstract);
|
|
for (final target in references.skip(1)) {
|
|
paramInfo.merge(ParameterInfo.fromMember(
|
|
target, useDefaultValueSentinel || target.asMember.isAbstract));
|
|
}
|
|
return paramInfo;
|
|
}
|
|
|
|
/// Build a row-displacement table based on fitting the [rows].
|
|
///
|
|
/// The returned list is the resulting row displacement table with `null`
|
|
/// entries representing unused space.
|
|
///
|
|
/// The offset of all [Row]s will be initialized.
|
|
List<V?> buildRowDisplacementTable<V extends Object>(List<Row<V>> rows,
|
|
{int firstAvailable = 0}) {
|
|
final table = <V?>[];
|
|
for (final row in rows) {
|
|
final values = row.values;
|
|
int offset = firstAvailable - values.first.index;
|
|
bool fits;
|
|
do {
|
|
fits = true;
|
|
for (final value in values) {
|
|
final int entry = offset + value.index;
|
|
if (entry >= table.length) {
|
|
// Fits
|
|
break;
|
|
}
|
|
if (table[entry] != null) {
|
|
fits = false;
|
|
break;
|
|
}
|
|
}
|
|
if (!fits) offset++;
|
|
} while (!fits);
|
|
row.offset = offset;
|
|
for (final (:index, :value) in values) {
|
|
final int tableIndex = offset + index;
|
|
while (table.length <= tableIndex) {
|
|
table.add(null);
|
|
}
|
|
assert(table[tableIndex] == null);
|
|
table[tableIndex] = value;
|
|
}
|
|
while (firstAvailable < table.length && table[firstAvailable] != null) {
|
|
firstAvailable++;
|
|
}
|
|
}
|
|
return table;
|
|
}
|
|
|
|
class Row<V extends Object> {
|
|
/// The values of the table row, represented sparsely as (index, value) tuples.
|
|
final List<({int index, V value})> values;
|
|
|
|
/// The given [values] must not be empty and should be sorted by index.
|
|
Row(this.values) {
|
|
assert(values.isNotEmpty);
|
|
assert(() {
|
|
int previous = values.first.index;
|
|
for (final value in values.skip(1)) {
|
|
if (value.index <= previous) return false;
|
|
previous = value.index;
|
|
}
|
|
return true;
|
|
}());
|
|
}
|
|
|
|
/// The selected offset of this row.
|
|
late final int offset;
|
|
|
|
int get width => values.last.index - values.first.index + 1;
|
|
int get holes => width - values.length;
|
|
int get density => (100 * values.length) ~/ width;
|
|
int get sparsity => 100 - density;
|
|
}
|