| // 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.topTypeNonNullable |
| : 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.topType); |
| } |
| } |
| } |
| |
| 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.topTypeNonNullable : translator.topType; |
| } |
| 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<Name, Set<SelectorInfo>> _dynamicGetters = {}; |
| |
| /// Maps member names to setter selectors with the same member name. |
| final Map<Name, Set<SelectorInfo>> _dynamicSetters = {}; |
| |
| /// Maps member names to method selectors with the same member name. |
| final Map<Name, 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[Name('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[Name('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] ??= {}).add(selector); |
| } else if (isSetter) { |
| (_dynamicSetters[member.name] ??= {}).add(selector); |
| } else { |
| (_dynamicMethods[member.name] ??= {}).add(selector); |
| } |
| } |
| |
| return selector; |
| } |
| |
| /// Get selectors for getters and tear-offs with the given name. |
| Iterable<SelectorInfo> dynamicGetterSelectors(Name memberName) => |
| _dynamicGetters[memberName] ?? Iterable.empty(); |
| |
| /// Get selectors for setters with the given name. |
| Iterable<SelectorInfo> dynamicSetterSelectors(Name memberName) => |
| _dynamicSetters[memberName] ?? Iterable.empty(); |
| |
| /// Get selectors for methods with the given name. |
| Iterable<SelectorInfo> dynamicMethodSelectors(Name 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; |
| } |