e3e82c95cb
These mixin constructors are now marked callable by the dynamic interface annotator: https://dart-review.googlesource.com/c/sdk/+/430002 However, they are not directly invokeable and trying to compile the constructor entrypoints causes dart2wasm to crash. So we explicitly skip them when considering dynamic callable constructors. Note: This is blocking internal rolls as code patterns like this show up in our internal codebase. Change-Id: I76383aec07bc53a0dbd1581c0577cba370d956f9 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/430801 Reviewed-by: Martin Kustermann <kustermann@google.com> Commit-Queue: Nate Biggs <natebiggs@google.com> Auto-Submit: Nate Biggs <natebiggs@google.com>
1412 lines
50 KiB
Dart
1412 lines
50 KiB
Dart
// Copyright (c) 2025, 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:kernel/ast.dart';
|
|
import 'package:kernel/class_hierarchy.dart';
|
|
import 'package:kernel/core_types.dart';
|
|
import 'package:kernel/library_index.dart';
|
|
import 'package:vm/metadata/direct_call.dart' show DirectCallMetadata;
|
|
import 'package:vm/metadata/procedure_attributes.dart';
|
|
import 'package:vm/metadata/table_selector.dart';
|
|
import 'package:vm/transformations/devirtualization.dart';
|
|
import 'package:vm/transformations/dynamic_interface_annotator.dart'
|
|
as dynamic_interface_annotator;
|
|
import 'package:vm/transformations/pragma.dart';
|
|
import 'package:vm/transformations/type_flow/table_selector_assigner.dart';
|
|
import 'package:wasm_builder/wasm_builder.dart' as w;
|
|
|
|
import 'class_info.dart';
|
|
import 'code_generator.dart';
|
|
import 'compiler_options.dart';
|
|
import 'constants.dart' show maxArrayNewFixedLength;
|
|
import 'dispatch_table.dart';
|
|
import 'dynamic_module_kernel_metadata.dart';
|
|
import 'intrinsics.dart' show MemberIntrinsic;
|
|
import 'kernel_nodes.dart';
|
|
import 'modules.dart';
|
|
import 'param_info.dart';
|
|
import 'record_class_generator.dart' show dynamicModulesRecordsLibraryUri;
|
|
import 'reference_extensions.dart';
|
|
import 'target.dart';
|
|
import 'translator.dart';
|
|
import 'types.dart' show InstanceConstantInterfaceType;
|
|
import 'util.dart';
|
|
|
|
// Pragmas used to annotate the kernel during main module compilation.
|
|
const String _mainModLibPragma = 'wasm:mainMod';
|
|
const String _submoduleEntryPointName = '\$invokeEntryPoint';
|
|
|
|
enum DynamicModuleType {
|
|
main,
|
|
submodule;
|
|
|
|
static DynamicModuleType parse(String s) => switch (s) {
|
|
"main" => main,
|
|
"submodule" => submodule,
|
|
_ => throw ArgumentError("Unrecognized dynamic module type $s."),
|
|
};
|
|
}
|
|
|
|
extension DynamicSubmoduleComponent on Component {
|
|
static final Expando<Procedure> _submoduleEntryPoint = Expando<Procedure>();
|
|
|
|
Procedure? get dynamicSubmoduleEntryPoint => _submoduleEntryPoint[this];
|
|
List<Library> getDynamicSubmoduleLibraries(CoreTypes coreTypes) =>
|
|
[...libraries.where((l) => !l.isFromMainModule(coreTypes))];
|
|
}
|
|
|
|
extension DynamicModuleLibrary on Library {
|
|
bool isFromMainModule(CoreTypes coreTypes) =>
|
|
hasPragma(coreTypes, this, _mainModLibPragma);
|
|
}
|
|
|
|
extension DynamicModuleClass on Class {
|
|
bool isDynamicSubmoduleExtendable(CoreTypes coreTypes) =>
|
|
hasPragma(coreTypes, this, kDynModuleExtendablePragmaName) ||
|
|
hasPragma(coreTypes, this, kDynModuleImplicitlyExtendablePragmaName);
|
|
}
|
|
|
|
extension DynamicModuleMember on Member {
|
|
bool isDynamicSubmoduleCallable(CoreTypes coreTypes) =>
|
|
hasPragma(coreTypes, this, kDynModuleCallablePragmaName) ||
|
|
hasPragma(coreTypes, this, kDynModuleImplicitlyCallablePragmaName);
|
|
|
|
bool isDynamicSubmoduleCallableNoTearOff(CoreTypes coreTypes) =>
|
|
getPragma(coreTypes, this, kDynModuleCallablePragmaName,
|
|
defaultValue: '') ==
|
|
'call';
|
|
|
|
bool isDynamicSubmoduleOverridable(CoreTypes coreTypes) =>
|
|
hasPragma(coreTypes, this, kDynModuleCanBeOverriddenPragmaName) ||
|
|
hasPragma(coreTypes, this, kDynModuleCanBeOverriddenImplicitlyPragmaName);
|
|
}
|
|
|
|
class DynamicSubmoduleOutputData extends ModuleOutputData {
|
|
final CoreTypes coreTypes;
|
|
final ModuleOutput _submodule;
|
|
DynamicSubmoduleOutputData(this.coreTypes, super.modules, super.importMap)
|
|
: _submodule = modules[1];
|
|
|
|
@override
|
|
ModuleOutput moduleForReference(Reference reference) {
|
|
// Rather than create tear-offs for all dynamic callable methods in the main
|
|
// module, we create them as needed in the submodules.
|
|
if (reference.isTearOffReference) return _submodule;
|
|
|
|
return super.moduleForReference(reference);
|
|
}
|
|
}
|
|
|
|
class DynamicMainModuleStrategy extends ModuleStrategy with KernelNodes {
|
|
@override
|
|
final Component component;
|
|
@override
|
|
final CoreTypes coreTypes;
|
|
@override
|
|
final LibraryIndex index;
|
|
final Uri dynamicInterfaceSpecificationBaseUri;
|
|
final String dynamicInterfaceSpecification;
|
|
|
|
DynamicMainModuleStrategy(
|
|
this.component,
|
|
this.coreTypes,
|
|
this.dynamicInterfaceSpecification,
|
|
this.dynamicInterfaceSpecificationBaseUri)
|
|
: index = coreTypes.index;
|
|
|
|
@override
|
|
void prepareComponent() {
|
|
// Annotate the kernel with info from dynamic interface.
|
|
dynamic_interface_annotator.annotateComponent(dynamicInterfaceSpecification,
|
|
dynamicInterfaceSpecificationBaseUri, component, coreTypes);
|
|
_addImplicitPragmas();
|
|
|
|
for (final lib in component.libraries) {
|
|
lib.annotations = [...lib.annotations];
|
|
addPragma(lib, _mainModLibPragma, coreTypes);
|
|
}
|
|
|
|
component.addMetadataRepository(DynamicModuleConstantRepository());
|
|
component.addMetadataRepository(DynamicModuleGlobalIdRepository());
|
|
}
|
|
|
|
@override
|
|
ModuleOutputData buildModuleOutputData() {
|
|
final builder = ModuleOutputBuilder();
|
|
final mainModule = builder.buildModule();
|
|
mainModule.libraries.addAll(component.libraries);
|
|
final placeholderModule = builder.buildModule(skipEmit: true);
|
|
return ModuleOutputData([mainModule, placeholderModule], const {});
|
|
}
|
|
|
|
void _addImplicitPragmas() {
|
|
final pragmasAdded = <(Member, String)>{};
|
|
|
|
void add(Member member, String pragma) {
|
|
if (pragmasAdded.add((member, pragma))) {
|
|
addPragma(member, pragma, coreTypes);
|
|
}
|
|
}
|
|
|
|
// These members don't have normal bodies and should therefore not be
|
|
// considered directly callable from submodules.
|
|
final Set<Member> excludedIntrinsics = {
|
|
coreTypes.index.getProcedure("dart:_wasm", "WasmFunction", "get:call"),
|
|
coreTypes.index.getConstructor("dart:_boxed_int", "BoxedInt", "_"),
|
|
coreTypes.index.getConstructor("dart:_boxed_double", "BoxedDouble", "_"),
|
|
};
|
|
|
|
void checkMemberEntryPoint(Member member) {
|
|
if (excludedIntrinsics.contains(member)) return;
|
|
// Entrypoints are all dynamically callable and vice versa.
|
|
final isEntryPoint = getPragma(
|
|
coreTypes, member, kWasmEntryPointPragmaName,
|
|
defaultValue: '') !=
|
|
null;
|
|
final isSubmoduleCallable = member.isDynamicSubmoduleCallable(coreTypes);
|
|
|
|
if (isEntryPoint && !isSubmoduleCallable) {
|
|
add(member, kDynModuleCallablePragmaName);
|
|
}
|
|
}
|
|
|
|
for (final library in component.libraries) {
|
|
for (final member in library.members) {
|
|
checkMemberEntryPoint(member);
|
|
}
|
|
for (final cls in library.classes) {
|
|
for (final member in cls.members) {
|
|
checkMemberEntryPoint(member);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add implicit pragmas
|
|
|
|
// Object has some inherent properties even though it is not explicitly
|
|
// annotated.
|
|
addPragma(coreTypes.objectClass, kDynModuleExtendablePragmaName, coreTypes);
|
|
for (final procedure in coreTypes.objectClass.procedures) {
|
|
add(procedure, kDynModuleCanBeOverriddenPragmaName);
|
|
add(procedure, kDynModuleCallablePragmaName);
|
|
}
|
|
|
|
// Mark all record classes as dynamic module extendable.
|
|
addPragma(coreTypes.recordClass, kDynModuleExtendablePragmaName, coreTypes);
|
|
|
|
// SystemHash.combine used by closures.
|
|
add(systemHashCombine, kDynModuleCallablePragmaName);
|
|
}
|
|
}
|
|
|
|
class DynamicSubmoduleStrategy extends ModuleStrategy {
|
|
final Component component;
|
|
final WasmCompilerOptions options;
|
|
final WasmTarget kernelTarget;
|
|
final Uri mainModuleComponentUri;
|
|
final CoreTypes coreTypes;
|
|
|
|
DynamicSubmoduleStrategy(this.component, this.options, this.kernelTarget,
|
|
this.coreTypes, this.mainModuleComponentUri);
|
|
|
|
@override
|
|
void prepareComponent() {
|
|
final submoduleEntryPoint = _findSubmoduleEntryPoint(component, coreTypes);
|
|
addWasmEntryPointPragma(submoduleEntryPoint, coreTypes);
|
|
DynamicSubmoduleComponent._submoduleEntryPoint[component] =
|
|
submoduleEntryPoint;
|
|
|
|
_registerLibraries();
|
|
_prepareWasmEntryPoint(submoduleEntryPoint);
|
|
_addTfaMetadata();
|
|
}
|
|
|
|
void _prepareWasmEntryPoint(Procedure submoduleEntryPoint) {
|
|
submoduleEntryPoint.function.returnType = const DynamicType();
|
|
|
|
// Export the entry point so that the JS runtime can get the function and
|
|
// pass it to the main module.
|
|
addPragma(submoduleEntryPoint, 'wasm:export', coreTypes,
|
|
value: StringConstant(_submoduleEntryPointName));
|
|
}
|
|
|
|
void _registerLibraries() {
|
|
// Register each library with the SDK. This will ensure no duplicate
|
|
// libraries are included across dynamic modules.
|
|
final registerLibraryUris = coreTypes.index
|
|
.getTopLevelProcedure('dart:_internal', 'registerLibraryUris');
|
|
final entryPoint = component.dynamicSubmoduleEntryPoint!;
|
|
final libraryUris = ListLiteral([
|
|
...component
|
|
.getDynamicSubmoduleLibraries(coreTypes)
|
|
.where((l) => '${l.importUri}' != dynamicModulesRecordsLibraryUri)
|
|
.map((l) => StringLiteral(l.importUri.toString()))
|
|
], typeArgument: coreTypes.stringNonNullableRawType);
|
|
entryPoint.function.body = Block([
|
|
ExpressionStatement(
|
|
StaticInvocation(registerLibraryUris, Arguments([libraryUris]))),
|
|
entryPoint.function.body!,
|
|
])
|
|
..parent = entryPoint.function;
|
|
}
|
|
|
|
static Procedure _findSubmoduleEntryPoint(
|
|
Component component, CoreTypes coreTypes) {
|
|
for (final library in component.libraries) {
|
|
for (final procedure in library.procedures) {
|
|
final entryPointPragma = getPragma(
|
|
coreTypes, procedure, kDynModuleEntryPointPragmaName,
|
|
defaultValue: true) ??
|
|
false;
|
|
if (entryPointPragma) {
|
|
return procedure;
|
|
}
|
|
}
|
|
}
|
|
throw StateError('Entry point not found for dynamic submodule.');
|
|
}
|
|
|
|
void _addTfaMetadata() {
|
|
component.metadata[dynamicMainModuleProcedureAttributeMetadataTag] =
|
|
component
|
|
.metadata[ProcedureAttributesMetadataRepository.repositoryTag]!;
|
|
component.metadata[dynamicMainModuleSelectorMetadataTag] =
|
|
component.metadata[TableSelectorMetadataRepository.repositoryTag]!;
|
|
|
|
final selectorAssigner = TableSelectorAssigner(component);
|
|
for (final selector in selectorAssigner.metadata.selectors) {
|
|
selector.callCount++;
|
|
selector.tornOff = true;
|
|
selector.calledOnNull = true;
|
|
}
|
|
|
|
final selectorMetadataRepository = TableSelectorMetadataRepository();
|
|
component.metadata[TableSelectorMetadataRepository.repositoryTag] =
|
|
selectorMetadataRepository;
|
|
selectorMetadataRepository.mapping[component] = selectorAssigner.metadata;
|
|
|
|
final dynamicModuleProcedureAttributes =
|
|
ProcedureAttributesMetadataRepository();
|
|
for (final library in component.libraries) {
|
|
for (final cls in library.classes) {
|
|
for (final member in cls.members) {
|
|
if (!member.isInstanceMember) continue;
|
|
dynamicModuleProcedureAttributes.mapping[member] =
|
|
ProcedureAttributesMetadata(
|
|
getterSelectorId: selectorAssigner.getterSelectorId(member),
|
|
methodOrSetterSelectorId:
|
|
selectorAssigner.methodOrSetterSelectorId(member));
|
|
}
|
|
}
|
|
}
|
|
component.metadata[ProcedureAttributesMetadataRepository.repositoryTag] =
|
|
dynamicModuleProcedureAttributes;
|
|
|
|
final classHierarchy =
|
|
ClassHierarchy(component, coreTypes) as ClosedWorldClassHierarchy;
|
|
|
|
component.accept(_Devirtualization(coreTypes, component, classHierarchy,
|
|
classHierarchy.computeSubtypesInformation()));
|
|
}
|
|
|
|
@override
|
|
ModuleOutputData buildModuleOutputData() {
|
|
final moduleBuilder = ModuleOutputBuilder();
|
|
final mainModule = moduleBuilder.buildModule(skipEmit: true);
|
|
final submodule = moduleBuilder.buildModule(emitAsMain: true);
|
|
for (final library in component.libraries) {
|
|
final module = hasPragma(coreTypes, library, _mainModLibPragma)
|
|
? mainModule
|
|
: submodule;
|
|
module.libraries.add(library);
|
|
}
|
|
|
|
return DynamicSubmoduleOutputData(
|
|
coreTypes, [mainModule, submodule], const {});
|
|
}
|
|
}
|
|
|
|
void _recordIdMain(w.FunctionBuilder f, Translator translator) {
|
|
final ranges = translator.classIdNumbering
|
|
.getConcreteClassIdRangeForMainModule(translator.coreTypes.recordClass);
|
|
|
|
final ib = f.body;
|
|
ib.local_get(ib.locals[0]);
|
|
ib.emitClassIdRangeCheck(ranges);
|
|
ib.end();
|
|
}
|
|
|
|
void _recordIdSubmodule(w.FunctionBuilder f, Translator translator) {
|
|
final ranges = translator.classIdNumbering
|
|
.getConcreteClassIdRangeForDynamicSubmodule(
|
|
translator.coreTypes.recordClass);
|
|
|
|
final ib = f.body;
|
|
if (ranges.isEmpty) {
|
|
ib.i32_const(0);
|
|
} else {
|
|
ib.local_get(ib.locals[0]);
|
|
translator.callReference(translator.localizeClassId.reference, ib);
|
|
ib.emitClassIdRangeCheck(ranges);
|
|
}
|
|
ib.end();
|
|
}
|
|
|
|
w.FunctionType _recordIdBuildType(Translator translator) {
|
|
return translator.typesBuilder
|
|
.defineFunction(const [w.NumType.i32], const [w.NumType.i32]);
|
|
}
|
|
|
|
enum BuiltinUpdatableFunctions {
|
|
recordId(_recordIdMain, _recordIdSubmodule, _recordIdBuildType);
|
|
|
|
final void Function(w.FunctionBuilder, Translator) _buildMain;
|
|
final void Function(w.FunctionBuilder, Translator) _buildSubmodule;
|
|
final w.FunctionType Function(Translator) _buildType;
|
|
|
|
const BuiltinUpdatableFunctions(
|
|
this._buildMain, this._buildSubmodule, this._buildType);
|
|
}
|
|
|
|
class DynamicModuleInfo {
|
|
final Translator translator;
|
|
Procedure? get submoduleEntryPoint =>
|
|
translator.component.dynamicSubmoduleEntryPoint;
|
|
bool get isSubmodule => submoduleEntryPoint != null;
|
|
late final w.FunctionBuilder initFunction;
|
|
late final MainModuleMetadata metadata;
|
|
|
|
late final w.Global moduleIdGlobal;
|
|
|
|
// null is used to indicate that skipDynamic was passed for this key.
|
|
final Map<int, w.BaseFunction?> overridableFunctions = {};
|
|
|
|
final Map<ClassInfo, Map<w.ModuleBuilder, w.BaseFunction>>
|
|
_constantCacheCheckers = {};
|
|
final Map<w.StorageType, Map<w.ModuleBuilder, w.BaseFunction>>
|
|
_mutableArrayConstantCacheCheckers = {};
|
|
final Map<w.StorageType, Map<w.ModuleBuilder, w.BaseFunction>>
|
|
_immutableArrayConstantCacheCheckers = {};
|
|
|
|
late final w.ModuleBuilder submodule =
|
|
translator.modules.firstWhere((m) => m != translator.mainModule);
|
|
|
|
DynamicModuleInfo(this.translator, this.metadata);
|
|
|
|
void initSubmodule() {
|
|
submodule.functions.start = initFunction = submodule.functions.define(
|
|
translator.typesBuilder.defineFunction(const [], const []), "#init");
|
|
|
|
// Make sure the exception tag is exported from the main module.
|
|
translator.getExceptionTag(submodule);
|
|
|
|
if (isSubmodule) {
|
|
_initSubmoduleId();
|
|
_initModuleRtt();
|
|
} else {
|
|
_initializeSubmoduleAllocatableClasses();
|
|
_initializeCallableReferences();
|
|
}
|
|
|
|
_initializeOverridableReferences();
|
|
}
|
|
|
|
void _initModuleRtt() {
|
|
final b = initFunction.body;
|
|
translator.pushModuleId(b);
|
|
final moduleRtt = translator.types.rtt.getModuleRtt(isMainModule: false);
|
|
translator.constants.instantiateConstant(
|
|
b, moduleRtt, translator.translateType(moduleRtt.interfaceType));
|
|
translator.callReference(translator.registerModuleRtt.reference, b);
|
|
b.drop();
|
|
}
|
|
|
|
void _initSubmoduleId() {
|
|
final global = moduleIdGlobal = submodule.globals
|
|
.define(w.GlobalType(w.NumType.i64, mutable: true), '#_moduleId');
|
|
global.initializer
|
|
..i64_const(0)
|
|
..end();
|
|
|
|
final b = initFunction.body;
|
|
|
|
final rangeSize = translator.classIdNumbering.maxDynamicSubmoduleClassId! -
|
|
translator.classIdNumbering.firstDynamicSubmoduleClassId +
|
|
1;
|
|
|
|
b.i32_const(rangeSize);
|
|
translator.callReference(translator.registerModuleClassRange.reference, b);
|
|
b.global_set(moduleIdGlobal);
|
|
}
|
|
|
|
bool _isClassSubmoduleInstantiable(Class cls) {
|
|
return cls.isDynamicSubmoduleExtendable(translator.coreTypes) ||
|
|
cls.constructors
|
|
.any((e) => e.isDynamicSubmoduleCallable(translator.coreTypes)) ||
|
|
cls.procedures.any((e) =>
|
|
e.isFactory && e.isDynamicSubmoduleCallable(translator.coreTypes));
|
|
}
|
|
|
|
void _initializeCallableReferences() {
|
|
for (final lib in translator.component.libraries) {
|
|
for (final member in lib.members) {
|
|
if (!member.isDynamicSubmoduleCallable(translator.coreTypes)) continue;
|
|
_forEachMemberReference(member, _registerStaticCallableTarget);
|
|
}
|
|
}
|
|
|
|
for (final classInfo in translator.classesSupersFirst) {
|
|
final cls = classInfo.cls;
|
|
if (cls == null) continue;
|
|
|
|
// Register any callable functions defined within this class.
|
|
for (final member in cls.members) {
|
|
if (!member.isDynamicSubmoduleCallable(translator.coreTypes)) continue;
|
|
|
|
if (!member.isInstanceMember) {
|
|
// Generate static members immediately since they are unconditionally
|
|
// callable.
|
|
_forEachMemberReference(member, _registerStaticCallableTarget);
|
|
continue;
|
|
}
|
|
|
|
// Consider callable references invoked and therefore if they're
|
|
// overridable include them in the runtime dispatch table.
|
|
if (member.isDynamicSubmoduleOverridable(translator.coreTypes)) {
|
|
_forEachMemberReference(
|
|
member, metadata.invokedOverridableReferences.add);
|
|
}
|
|
}
|
|
|
|
// Anonymous mixins' targets don't need to be registered since they aren't
|
|
// directly allocatable.
|
|
if (cls.isAnonymousMixin) continue;
|
|
|
|
if (cls.isAbstract && !_isClassSubmoduleInstantiable(cls)) {
|
|
continue;
|
|
}
|
|
|
|
// For each dispatch target, register the member as callable from this
|
|
// class.
|
|
final targets = translator.hierarchy.getDispatchTargets(cls).followedBy(
|
|
translator.hierarchy.getDispatchTargets(cls, setters: true));
|
|
for (final member in targets) {
|
|
if (!member.isDynamicSubmoduleCallable(translator.coreTypes)) continue;
|
|
|
|
_forEachMemberReference(member,
|
|
(reference) => _registerCallableDispatchTarget(reference, cls));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// If class [cls] is marked allocated then ensure we compile [target].
|
|
///
|
|
/// The [cls] may be marked allocated in
|
|
/// [_initializeSubmoduleAllocatableClasses] which (together with this) will
|
|
/// enqueue the [target] for compilation. Otherwise the [cls] must be
|
|
/// allocated via a constructor call in the program itself.
|
|
void _registerCallableDispatchTarget(Reference target, Class cls) {
|
|
final member = target.asMember;
|
|
|
|
if (member.isExternal) {
|
|
final isGeneratedIntrinsic = member is Procedure &&
|
|
MemberIntrinsic.fromProcedure(translator.coreTypes, member) != null;
|
|
if (!isGeneratedIntrinsic) return;
|
|
}
|
|
|
|
final classId =
|
|
(translator.classInfo[cls]!.classId as AbsoluteClassId).value;
|
|
|
|
metadata.callableReferenceIds[target] ??=
|
|
metadata.callableReferenceIds.length;
|
|
// The class must be allocated in order for the target to be live.
|
|
translator.functions.recordClassTargetUse(classId, target);
|
|
}
|
|
|
|
void _registerStaticCallableTarget(Reference target) {
|
|
final member = target.asMember;
|
|
|
|
if (member.isExternal) {
|
|
final isGeneratedIntrinsic = member is Procedure &&
|
|
MemberIntrinsic.fromProcedure(translator.coreTypes, member) != null;
|
|
if (!isGeneratedIntrinsic) return;
|
|
}
|
|
|
|
// Generate static members immediately since they are unconditionally
|
|
// callable.
|
|
metadata.callableReferenceIds[target] ??=
|
|
metadata.callableReferenceIds.length;
|
|
translator.functions.getFunction(target);
|
|
}
|
|
|
|
void _initializeSubmoduleAllocatableClasses() {
|
|
for (final classInfo in translator.classesSupersFirst) {
|
|
final cls = classInfo.cls;
|
|
if (cls == null) continue;
|
|
if (cls.isAnonymousMixin) continue;
|
|
|
|
if (_isClassSubmoduleInstantiable(cls)) {
|
|
translator.functions.recordClassAllocation(classInfo.classId);
|
|
}
|
|
}
|
|
}
|
|
|
|
void _initializeOverridableReferences() {
|
|
for (final builtin in BuiltinUpdatableFunctions.values) {
|
|
_createUpdateableFunction(builtin.index, builtin._buildType(translator),
|
|
buildMain: (f) => builtin._buildMain(f, translator),
|
|
buildSubmodule: (f) => builtin._buildSubmodule(f, translator),
|
|
name: '#r_${builtin.name}');
|
|
}
|
|
|
|
for (final reference in metadata.invokedOverridableReferences) {
|
|
final selector = translator.dispatchTable.selectorForTarget(reference);
|
|
translator.functions.recordSelectorUse(selector, false);
|
|
|
|
final mainSelector = (translator.dynamicMainModuleDispatchTable ??
|
|
translator.dispatchTable)
|
|
.selectorForTarget(reference);
|
|
final signature = _getGeneralizedSignature(mainSelector);
|
|
final buildMain = buildSelectorBranch(reference, mainSelector);
|
|
final buildSubmodule = buildSelectorBranch(reference, mainSelector);
|
|
|
|
_createUpdateableFunction(
|
|
mainSelector.id + BuiltinUpdatableFunctions.values.length, signature,
|
|
buildMain: buildMain,
|
|
buildSubmodule: buildSubmodule,
|
|
name: '#s${mainSelector.id}_${mainSelector.name}');
|
|
}
|
|
}
|
|
|
|
void _forEachMemberReference(Member member, void Function(Reference) f) {
|
|
void passReference(Reference reference) {
|
|
final checkedReference =
|
|
translator.getFunctionEntry(reference, uncheckedEntry: false);
|
|
f(checkedReference);
|
|
|
|
final uncheckedReference =
|
|
translator.getFunctionEntry(reference, uncheckedEntry: true);
|
|
if (uncheckedReference != checkedReference) {
|
|
f(uncheckedReference);
|
|
}
|
|
}
|
|
|
|
if (member is Procedure) {
|
|
passReference(member.reference);
|
|
// We ignore the tear-off and let each submodule generate it for itself.
|
|
} else if (member is Field) {
|
|
passReference(member.getterReference);
|
|
if (member.hasSetter) {
|
|
passReference(member.setterReference!);
|
|
}
|
|
} else if (member is Constructor &&
|
|
// Skip types that don't extend Object in the wasm type hierarchy.
|
|
// These types do not have directly invokable constructors.
|
|
translator.classInfo[member.enclosingClass]!.struct
|
|
.isSubtypeOf(translator.objectInfo.struct)) {
|
|
if (!member.enclosingClass.isAnonymousMixin) {
|
|
passReference(member.reference);
|
|
}
|
|
passReference(member.initializerReference);
|
|
passReference(member.constructorBodyReference);
|
|
}
|
|
}
|
|
|
|
void finishDynamicModule() {
|
|
_registerModuleRefs(
|
|
isSubmodule ? initFunction.body : translator.initFunction.body);
|
|
|
|
initFunction.body.end();
|
|
}
|
|
|
|
void _registerModuleRefs(w.InstructionsBuilder b) {
|
|
final numKeys = overridableFunctions.length;
|
|
assert(numKeys < maxArrayNewFixedLength);
|
|
final orderedFunctions = ([...overridableFunctions.entries]
|
|
..sort((a, b) => a.key.compareTo(b.key)))
|
|
.map((e) => e.value);
|
|
|
|
for (final function in orderedFunctions) {
|
|
if (function != null) {
|
|
b.ref_func(function);
|
|
} else {
|
|
b.ref_null(w.HeapType.func);
|
|
}
|
|
}
|
|
b.array_new_fixed(
|
|
translator.wasmArrayType(w.RefType.func(nullable: true), ''), numKeys);
|
|
translator.callReference(
|
|
translator.registerUpdateableFuncRefs.reference, b);
|
|
b.drop();
|
|
}
|
|
|
|
int _createUpdateableFunction(int key, w.FunctionType type,
|
|
{required void Function(w.FunctionBuilder function) buildMain,
|
|
required void Function(w.FunctionBuilder function) buildSubmodule,
|
|
bool skipSubmodule = false,
|
|
required String name}) {
|
|
final mapKey = key;
|
|
final index = metadata.keyInvocationToIndex[mapKey] ??=
|
|
metadata.keyInvocationToIndex.length;
|
|
overridableFunctions.putIfAbsent(index, () {
|
|
if (!isSubmodule) {
|
|
final mainFunction = translator.mainModule.functions.define(type, name);
|
|
translator.mainModule.functions.declare(mainFunction);
|
|
buildMain(mainFunction);
|
|
return mainFunction;
|
|
}
|
|
|
|
if (skipSubmodule) return null;
|
|
|
|
final submoduleFunction = submodule.functions.define(type, name);
|
|
submodule.functions.declare(submoduleFunction);
|
|
buildSubmodule(submoduleFunction);
|
|
return submoduleFunction;
|
|
});
|
|
|
|
return index;
|
|
}
|
|
|
|
void _callClassIdBranch(
|
|
int key, w.InstructionsBuilder b, w.FunctionType signature,
|
|
{required void Function(w.FunctionBuilder b) buildMainMatch,
|
|
required void Function(w.FunctionBuilder b) buildSubmoduleMatch,
|
|
bool skipSubmodule = false,
|
|
required String name}) {
|
|
// No new types declared in the submodule so the branch would always miss.
|
|
final canSkipSubmoduleBranch = skipSubmodule ||
|
|
translator.classIdNumbering.maxDynamicSubmoduleClassId ==
|
|
translator.classIdNumbering.maxClassId;
|
|
final callIndex = _createUpdateableFunction(key, signature,
|
|
buildMain: buildMainMatch,
|
|
buildSubmodule: buildSubmoduleMatch,
|
|
skipSubmodule: canSkipSubmoduleBranch,
|
|
name: name);
|
|
|
|
translator.callReference(translator.classIdToModuleId.reference, b);
|
|
b.i64_const(callIndex);
|
|
|
|
// getUpdateableFuncRef allows for null entries since a submodule may not
|
|
// implement every key. However, only keys that cannot be queried should be
|
|
// unimplemented so it's safe to cast to a non-nullable function here.
|
|
translator.callReference(translator.getUpdateableFuncRef.reference, b);
|
|
translator.convertType(b, w.RefType.func(nullable: true),
|
|
w.RefType(signature, nullable: false));
|
|
b.call_ref(signature);
|
|
}
|
|
|
|
void callClassIdBranchBuiltIn(
|
|
BuiltinUpdatableFunctions key, w.InstructionsBuilder b,
|
|
{bool skipSubmodule = false}) {
|
|
_callClassIdBranch(key.index, b, key._buildType(translator),
|
|
buildMainMatch: (f) => key._buildMain(f, translator),
|
|
buildSubmoduleMatch: (f) => key._buildSubmodule(f, translator),
|
|
name: '#r_${key.name}',
|
|
skipSubmodule: skipSubmodule);
|
|
}
|
|
|
|
w.FunctionType _getGeneralizedSignature(SelectorInfo mainSelector) {
|
|
final signature = mainSelector.signature;
|
|
|
|
// The shared entry point to this selector has to use 'any' because the
|
|
// selector's signature may change between compilations.
|
|
final generalizedSignature = translator.typesBuilder.defineFunction([
|
|
...signature.inputs.map((e) => const w.RefType.any(nullable: true)),
|
|
w.NumType.i32,
|
|
w.NumType.i32
|
|
], [
|
|
...signature.outputs.map((e) => const w.RefType.any(nullable: true))
|
|
]);
|
|
return generalizedSignature;
|
|
}
|
|
|
|
void Function(w.FunctionBuilder) buildSelectorBranch(
|
|
Reference interfaceTarget, SelectorInfo mainSelector) {
|
|
return (w.FunctionBuilder function) {
|
|
final localSelector =
|
|
translator.dispatchTable.selectorForTarget(interfaceTarget);
|
|
final ib = function.body;
|
|
|
|
final uncheckedTargets = localSelector.targets(unchecked: true);
|
|
final checkedTargets = localSelector.targets(unchecked: false);
|
|
|
|
// Whether we use checked+unchecked (or normal) we'll have the same
|
|
// class-id ranges - only the actual target `Reference` may be a unchecked
|
|
// or checked one.
|
|
assert(uncheckedTargets.targetRanges.length ==
|
|
checkedTargets.targetRanges.length);
|
|
|
|
// NOTE: Keep this in sync with
|
|
// `code_generator.dart:AstCodeGenerator._virtualCall`.
|
|
final bool noTarget = checkedTargets.targetRanges.isEmpty;
|
|
final bool directCall = checkedTargets.targetRanges.length == 1;
|
|
final callPolymorphicDispatcher =
|
|
!directCall && checkedTargets.staticDispatchRanges.isNotEmpty;
|
|
// disabled for dyn overridable selectors atm
|
|
assert(!callPolymorphicDispatcher);
|
|
|
|
if (noTarget) {
|
|
ib.comment('No targets in local module for ${localSelector.name}');
|
|
ib.unreachable();
|
|
ib.end();
|
|
return;
|
|
}
|
|
|
|
final w.FunctionType localSignature;
|
|
final ParameterInfo localParamInfo;
|
|
if (directCall) {
|
|
final target = checkedTargets.targetRanges.single.target;
|
|
localSignature = translator.signatureForDirectCall(target);
|
|
localParamInfo = translator.paramInfoForDirectCall(target);
|
|
} else {
|
|
localSignature = localSelector.signature;
|
|
localParamInfo = localSelector.paramInfo;
|
|
}
|
|
|
|
final generalizedMainSignature = _getGeneralizedSignature(mainSelector);
|
|
final mainParamInfo = mainSelector.paramInfo;
|
|
|
|
assert(mainParamInfo.takesContextOrReceiver ==
|
|
localParamInfo.takesContextOrReceiver);
|
|
|
|
int localsIndex = 0;
|
|
final takesContextOrReceiver = localParamInfo.takesContextOrReceiver;
|
|
if (takesContextOrReceiver) {
|
|
ib.local_get(ib.locals[localsIndex]);
|
|
translator.convertType(ib, generalizedMainSignature.inputs[localsIndex],
|
|
localSignature.inputs[localsIndex]);
|
|
localsIndex++;
|
|
}
|
|
|
|
final mainTypeParamCount = mainParamInfo.typeParamCount;
|
|
assert(mainTypeParamCount == localParamInfo.typeParamCount);
|
|
for (int i = 0; i < mainTypeParamCount; i++, localsIndex++) {
|
|
ib.local_get(ib.locals[localsIndex]);
|
|
translator.convertType(ib, generalizedMainSignature.inputs[localsIndex],
|
|
localSignature.inputs[localsIndex]);
|
|
}
|
|
|
|
final localPositionalCount = localParamInfo.positional.length;
|
|
final mainPositionalCount = mainParamInfo.positional.length;
|
|
assert(localPositionalCount >= mainPositionalCount);
|
|
|
|
for (int i = 0; i < localPositionalCount; i++, localsIndex++) {
|
|
if (i < mainPositionalCount) {
|
|
ib.local_get(ib.locals[localsIndex]);
|
|
translator.convertType(
|
|
ib,
|
|
generalizedMainSignature.inputs[localsIndex],
|
|
localSignature.inputs[localsIndex]);
|
|
continue;
|
|
}
|
|
final constant = localParamInfo.positional[i]!;
|
|
translator.constants.instantiateConstant(
|
|
ib, constant, localSignature.inputs[localsIndex]);
|
|
}
|
|
|
|
final localNamedCount = localParamInfo.named.length;
|
|
final mainNamedCount = mainParamInfo.named.length;
|
|
assert(localNamedCount >= mainNamedCount);
|
|
|
|
for (int i = 0; i < localNamedCount; i++, localsIndex++) {
|
|
final name = localParamInfo.names[i];
|
|
final mainIndex = mainParamInfo.nameIndex[name];
|
|
if (mainIndex != null) {
|
|
final mainLocalIndex =
|
|
(takesContextOrReceiver ? 1 : 0) + mainTypeParamCount + mainIndex;
|
|
ib.local_get(ib.locals[mainLocalIndex]);
|
|
translator.convertType(
|
|
ib,
|
|
generalizedMainSignature.inputs[mainLocalIndex],
|
|
localSignature.inputs[localsIndex]);
|
|
continue;
|
|
}
|
|
final constant = localParamInfo.named[name]!;
|
|
translator.constants.instantiateConstant(
|
|
ib, constant, localSignature.inputs[localsIndex]);
|
|
}
|
|
|
|
if (directCall) {
|
|
if (!localSelector.useMultipleEntryPoints) {
|
|
final target = checkedTargets.targetRanges.single.target;
|
|
ib.invoke(translator.directCallTarget(target));
|
|
} else {
|
|
final uncheckedTarget = uncheckedTargets.targetRanges.single.target;
|
|
final checkedTarget = checkedTargets.targetRanges.single.target;
|
|
// Check if the invocation is checked or unchecked and use the
|
|
// appropriate offset.
|
|
ib.local_get(ib.locals[function.type.inputs.length - 1]);
|
|
ib.if_(localSignature.inputs, localSignature.outputs);
|
|
ib.invoke(translator.directCallTarget(uncheckedTarget));
|
|
ib.else_();
|
|
ib.invoke(translator.directCallTarget(checkedTarget));
|
|
ib.end();
|
|
}
|
|
} else {
|
|
ib.local_get(ib.locals[function.type.inputs.length - 2]);
|
|
if (isSubmodule) {
|
|
translator.callReference(translator.scopeClassId.reference, ib);
|
|
}
|
|
|
|
ib.comment('Local dispatch table call to "${localSelector.name}"');
|
|
final uncheckedOffset = uncheckedTargets.offset;
|
|
final checkedOffset = checkedTargets.offset;
|
|
if (!localSelector.useMultipleEntryPoints) {
|
|
if (checkedOffset != 0) {
|
|
ib.i32_const(checkedOffset!);
|
|
ib.i32_add();
|
|
}
|
|
} else if (checkedOffset != 0 || uncheckedOffset != 0) {
|
|
// Check if the invocation is checked or unchecked and use the
|
|
// appropriate offset.
|
|
ib.local_get(ib.locals[function.type.inputs.length - 1]);
|
|
ib.if_(const [], const [w.NumType.i32]);
|
|
if (uncheckedOffset != null) {
|
|
ib.i32_const(uncheckedOffset);
|
|
} else {
|
|
ib.unreachable();
|
|
}
|
|
ib.else_();
|
|
if (checkedOffset != null) {
|
|
ib.i32_const(checkedOffset);
|
|
} else {
|
|
ib.unreachable();
|
|
}
|
|
ib.end();
|
|
ib.i32_add();
|
|
}
|
|
final table = translator.dispatchTable.getWasmTable(ib.module);
|
|
ib.call_indirect(localSignature, table);
|
|
}
|
|
translator.convertType(ib, localSignature.outputs.single,
|
|
generalizedMainSignature.outputs.single);
|
|
ib.end();
|
|
};
|
|
}
|
|
|
|
void callOverridableDispatch(
|
|
w.InstructionsBuilder b, SelectorInfo selector, Reference interfaceTarget,
|
|
{required bool useUncheckedEntry}) {
|
|
metadata.invokedOverridableReferences.add(interfaceTarget);
|
|
|
|
final localSignature = selector.signature;
|
|
// If any input is not a RefType (i.e. it's an unboxed value) then wrap it
|
|
// so the updated signature works.
|
|
if (localSignature.inputs.any((i) => i is! w.RefType)) {
|
|
final receiverLocal = b.addLocal(translator.topInfo.nullableType);
|
|
b.local_set(receiverLocal);
|
|
final locals = <w.Local>[];
|
|
for (final input in localSignature.inputs.reversed) {
|
|
final local = b.addLocal(input);
|
|
locals.add(local);
|
|
b.local_set(local);
|
|
}
|
|
for (final local in locals.reversed) {
|
|
b.local_get(local);
|
|
translator.convertType(b, local.type, w.RefType.any(nullable: true));
|
|
}
|
|
b.local_get(receiverLocal);
|
|
}
|
|
|
|
final idLocal = b.addLocal(w.NumType.i32);
|
|
b.struct_get(translator.topInfo.struct, FieldIndex.classId);
|
|
b.local_tee(idLocal);
|
|
b.i32_const(useUncheckedEntry ? 1 : 0);
|
|
b.local_get(idLocal);
|
|
|
|
final mainDispatchTable =
|
|
translator.dynamicMainModuleDispatchTable ?? translator.dispatchTable;
|
|
final mainModuleSelector =
|
|
mainDispatchTable.selectorForTarget(interfaceTarget);
|
|
final generalizedSignature = _getGeneralizedSignature(mainModuleSelector);
|
|
|
|
// For consistency, always use the main module selector ID when generating
|
|
// the key.
|
|
final key = mainModuleSelector.id + BuiltinUpdatableFunctions.values.length;
|
|
_callClassIdBranch(key, b, generalizedSignature,
|
|
name: '#s${mainModuleSelector.id}_${mainModuleSelector.name}',
|
|
buildMainMatch:
|
|
buildSelectorBranch(interfaceTarget, mainModuleSelector),
|
|
buildSubmoduleMatch:
|
|
buildSelectorBranch(interfaceTarget, mainModuleSelector),
|
|
skipSubmodule: selector.targets(unchecked: false).targetRanges.isEmpty);
|
|
translator.convertType(
|
|
b, generalizedSignature.outputs.single, localSignature.outputs.single);
|
|
}
|
|
}
|
|
|
|
/// Emits code to canonicalize the provided constant value at runtime.
|
|
///
|
|
/// This canonicalizer works by generating custom equality functions for any
|
|
/// type of constant it encounters. The SDK maintains an array of canonicalized
|
|
/// objects separated by type and the equality function generated here is used
|
|
/// to identify the canonical version of a constant.
|
|
///
|
|
/// For example, for a normal Dart Object of type T, we will first construct a
|
|
/// new instance of T. Then we will fetch an array containing all instances of T
|
|
/// already canonicalized. Using an equality function which does a pairwise
|
|
/// comparison of T's fields, we will walk the array looking for an instance
|
|
/// that matches the new T. If there is one we return the canonical version,
|
|
/// otherwise we add it to the array and return the new T.
|
|
///
|
|
/// Iterables, wasm arrays and wasm builtin types all require special
|
|
/// canonicalization logic.
|
|
///
|
|
/// Only classes defined in the main module require canonicalization because
|
|
/// these are the only classes that can have identical constants instantiated in
|
|
/// different submodules. A class defined in a submodule cannot be accessed from
|
|
/// a different submodule.
|
|
class ConstantCanonicalizer extends ConstantVisitor<void> {
|
|
final Translator translator;
|
|
final w.InstructionsBuilder b;
|
|
|
|
/// A local containing the value to be canonicalized.
|
|
final w.Local valueLocal;
|
|
|
|
ConstantCanonicalizer(this.translator, this.b, this.valueLocal);
|
|
|
|
late final _checkerType = translator.typesBuilder.defineFunction([
|
|
translator.topInfo.nonNullableType,
|
|
translator.topInfo.nonNullableType,
|
|
], const [
|
|
w.NumType.i32
|
|
]);
|
|
|
|
late final _arrayCheckerType = translator.typesBuilder.defineFunction(const [
|
|
w.RefType.array(nullable: false),
|
|
w.RefType.array(nullable: false),
|
|
], const [
|
|
w.NumType.i32
|
|
]);
|
|
|
|
/// Wasm builtin value types that don't need canonicalization.
|
|
late final Set<Class> _wasmValueClasses = {
|
|
translator.wasmI32Class,
|
|
translator.wasmI64Class,
|
|
translator.wasmF32Class,
|
|
translator.wasmF64Class,
|
|
translator.wasmI16Class,
|
|
translator.wasmI8Class,
|
|
translator.wasmAnyRefClass,
|
|
translator.wasmExternRefClass,
|
|
translator.wasmI31RefClass,
|
|
translator.wasmFuncRefClass,
|
|
translator.wasmEqRefClass,
|
|
translator.wasmStructRefClass,
|
|
translator.wasmArrayRefClass,
|
|
};
|
|
|
|
/// Boxed values are comparable by the value they wrap.
|
|
late final Set<Class> _boxedClasses = {
|
|
translator.boxedIntClass,
|
|
translator.boxedDoubleClass,
|
|
translator.boxedBoolClass,
|
|
};
|
|
|
|
/// Values of these types are canonicalized by their == function.
|
|
late final Set<Class> _equalsCheckerClasses = {
|
|
translator.jsStringClass,
|
|
translator.symbolClass,
|
|
translator.closureClass,
|
|
};
|
|
|
|
/// These iterable classes contain lazily initialized data that should not be
|
|
/// considered in comparisons.
|
|
late final Set<Class> _hashingIterableConstClasses = {
|
|
translator.immutableSetClass,
|
|
translator.immutableMapClass,
|
|
};
|
|
|
|
/// Emit code that canonicalizes the instance of [cls] stored in [valueLocal].
|
|
void _canonicalizeInstance(Class cls) {
|
|
final classId = translator.classInfo[cls]!.classId;
|
|
if (classId is RelativeClassId) {
|
|
// This class is not defined in the main module so it doesn't need runtime
|
|
// canonicalization.
|
|
return;
|
|
}
|
|
if (_wasmValueClasses.contains(cls)) {
|
|
// Wasm value types do not need canonicalization.
|
|
return;
|
|
}
|
|
|
|
// Lookup the WasmCache for the value's type.
|
|
b.local_set(valueLocal);
|
|
b.i64_const((classId as AbsoluteClassId).value);
|
|
translator.callReference(translator.constCacheGetter.reference, b);
|
|
|
|
// Get the equality checker for the class. Import it into the submodule and
|
|
// use the import if this is in a submodule.
|
|
w.BaseFunction checker = _getCanonicalChecker(cls, b.module);
|
|
|
|
// Declare the function so it can be used as a ref_func in a constant
|
|
// context.
|
|
b.module.functions.declare(checker);
|
|
|
|
// Invoke the 'canonicalize' function with the value and checker.
|
|
b.local_get(valueLocal);
|
|
b.ref_func(checker);
|
|
final valueType = translator.callReference(
|
|
translator.constCacheCanonicalize.reference, b);
|
|
|
|
// The canonicalizer returns an Object which may be a boxed value. Unbox it
|
|
// if necessary.
|
|
translator.convertType(b, valueType.single, valueLocal.type);
|
|
}
|
|
|
|
void _canonicalizeArray(bool mutable, DartType elementType) {
|
|
b.local_set(valueLocal);
|
|
|
|
final cacheField = (mutable
|
|
? translator.wasmArrayConstCache
|
|
: translator.immutableWasmArrayConstCache)[elementType];
|
|
|
|
if (cacheField == null) {
|
|
throw StateError(
|
|
'Unrecognized const array type (mutable: $mutable): $elementType');
|
|
}
|
|
|
|
translator.callReference(cacheField.getterReference, b);
|
|
|
|
// Get the equality checker for the class. Import it into the submodule and
|
|
// use the import if this is in a submodule.
|
|
w.BaseFunction checker = _getCanonicalArrayChecker(
|
|
translator.translateStorageType(elementType), mutable, b.module);
|
|
|
|
// Declare the function so it can be used as a ref_func in a constant
|
|
// context.
|
|
b.module.functions.declare(checker);
|
|
|
|
// Invoke the canonicalizer function with the value and checker.
|
|
b.local_get(valueLocal);
|
|
b.ref_func(checker);
|
|
final valueType = translator.callReference(
|
|
translator.constCacheArrayCanonicalize.reference, b);
|
|
|
|
// The canonicalizer returns an array ref, cast it to the correct array
|
|
// type.
|
|
translator.convertType(b, valueType.single, valueLocal.type);
|
|
}
|
|
|
|
/// Get a function that will compare two instances of [cls] and return true if
|
|
/// they canonicalize to the same value.
|
|
w.BaseFunction _getCanonicalChecker(Class cls, w.ModuleBuilder module) {
|
|
ClassInfo info = translator.classInfo[cls]!;
|
|
|
|
// We create a checker for each class to ensure we check each struct field.
|
|
return translator.dynamicModuleInfo!._constantCacheCheckers
|
|
.putIfAbsent(info, () => {})
|
|
.putIfAbsent(module, () {
|
|
final checker =
|
|
module.functions.define(_checkerType, '${info.cls} constCheck');
|
|
|
|
final b = checker.body;
|
|
_checkerForClass(b, info);
|
|
b.end();
|
|
return checker;
|
|
});
|
|
}
|
|
|
|
/// Get a function that will compare two arrays with elements of type
|
|
/// [elementType] and return true if they canonicalize to the same value.
|
|
w.BaseFunction _getCanonicalArrayChecker(
|
|
w.StorageType elementType, bool mutable, w.ModuleBuilder module) {
|
|
final cache = mutable
|
|
? translator.dynamicModuleInfo!._mutableArrayConstantCacheCheckers
|
|
: translator.dynamicModuleInfo!._immutableArrayConstantCacheCheckers;
|
|
|
|
// We create a checker for each array element type.
|
|
return cache.putIfAbsent(elementType, () => {}).putIfAbsent(module, () {
|
|
final name = '$elementType';
|
|
final checker = module.functions.define(_arrayCheckerType,
|
|
'$name const${mutable ? '' : 'Immutable'}ArrayCheck');
|
|
|
|
final arrayType =
|
|
translator.wasmArrayType(elementType, name, mutable: mutable);
|
|
final b = checker.body;
|
|
_checkerForArray(b, arrayType, elementType);
|
|
b.end();
|
|
return checker;
|
|
});
|
|
}
|
|
|
|
void _checkerForClass(w.InstructionsBuilder b, ClassInfo classInfo) {
|
|
final cls = classInfo.cls!;
|
|
|
|
if (_boxedClasses.contains(cls)) {
|
|
return _checkerForBoxedClasses(b, classInfo);
|
|
}
|
|
|
|
final structRef = classInfo.nonNullableType;
|
|
b.local_get(b.locals[0]);
|
|
b.ref_cast(structRef);
|
|
b.local_get(b.locals[1]);
|
|
b.ref_cast(structRef);
|
|
|
|
if (_equalsCheckerClasses.contains(cls)) {
|
|
_checkerWithEquals(b);
|
|
} else if (_hashingIterableConstClasses.contains(cls)) {
|
|
_defaultChecker(b, classInfo, fieldsToInclude: {
|
|
FieldIndex.hashBaseData,
|
|
...cls.typeParameters.map((t) => translator.typeParameterIndex[t]!)
|
|
});
|
|
} else {
|
|
_defaultChecker(b, classInfo);
|
|
}
|
|
}
|
|
|
|
/// Compare boxed entites via the values they wrap.
|
|
void _checkerForBoxedClasses(w.InstructionsBuilder b, ClassInfo classInfo) {
|
|
final structRef = classInfo.nonNullableType;
|
|
b.local_get(b.locals[0]);
|
|
b.ref_cast(structRef);
|
|
b.struct_get(classInfo.struct, FieldIndex.boxValue);
|
|
|
|
b.local_get(b.locals[1]);
|
|
b.ref_cast(structRef);
|
|
b.struct_get(classInfo.struct, FieldIndex.boxValue);
|
|
|
|
return _equalsForValueType(
|
|
b, translator.builtinTypes[classInfo.cls] as w.ValueType);
|
|
}
|
|
|
|
/// Compare values using a dispatch call to Object.==
|
|
void _checkerWithEquals(w.InstructionsBuilder b) {
|
|
b.local_get(b.locals[0]);
|
|
final selector = translator.dispatchTable
|
|
.selectorForTarget(translator.coreTypes.objectEquals.reference);
|
|
translator.callDispatchTable(b, selector,
|
|
interfaceTarget: translator.coreTypes.objectEquals.reference,
|
|
useUncheckedEntry: true);
|
|
translator.convertType(
|
|
b, selector.signature.outputs.first, _checkerType.outputs.first);
|
|
}
|
|
|
|
/// Compare two normal class instances whose const identity are determined by
|
|
/// their fields. Do a shallow comparison of the fields assuming the field
|
|
/// values are already canonicalized.
|
|
void _defaultChecker(w.InstructionsBuilder b, ClassInfo classInfo,
|
|
{Set<int>? fieldsToInclude}) {
|
|
classInfo = classInfo.repr;
|
|
final structType = classInfo.struct;
|
|
final structRefType = classInfo.nonNullableType;
|
|
final castedLocal1 = b.addLocal(structRefType);
|
|
final castedLocal2 = b.addLocal(structRefType);
|
|
b.local_set(castedLocal2);
|
|
b.local_set(castedLocal1);
|
|
final falseBlock = b.block();
|
|
classInfo.forEachClassFieldIndex((index, fieldType) {
|
|
if (fieldsToInclude != null && !fieldsToInclude.contains(index)) {
|
|
return;
|
|
}
|
|
b.local_get(castedLocal1);
|
|
b.struct_get(structType, index);
|
|
|
|
b.local_get(castedLocal2);
|
|
b.struct_get(structType, index);
|
|
|
|
final fieldTypeUnpacked = fieldType.type;
|
|
_equalsForValueType(b, fieldTypeUnpacked);
|
|
b.i32_eqz();
|
|
b.br_if(falseBlock);
|
|
});
|
|
b.i32_const(1);
|
|
b.return_();
|
|
b.end();
|
|
b.i32_const(0);
|
|
}
|
|
|
|
/// Compare two arrays for equality by iterating through the elements and
|
|
/// doing a shallow pairwise comparison. Array elements will already be
|
|
/// canonicalized. Assumes the types and lengths of the arrays are already
|
|
/// equivalent.
|
|
void _checkerForArray(w.InstructionsBuilder b, w.ArrayType arrayType,
|
|
w.StorageType elementType) {
|
|
final arrayRefType = w.RefType(arrayType, nullable: false);
|
|
final array1 = b.addLocal(arrayRefType);
|
|
final array2 = b.addLocal(arrayRefType);
|
|
final falseBlock = b.block();
|
|
b.local_get(b.locals[0]);
|
|
b.ref_cast(arrayRefType);
|
|
b.local_set(array1);
|
|
b.local_get(b.locals[1]);
|
|
b.ref_cast(arrayRefType);
|
|
b.local_set(array2);
|
|
|
|
b.incrementingLoop(
|
|
pushStart: () => b.i32_const(0),
|
|
pushLimit: () {
|
|
b.local_get(array1);
|
|
b.array_len();
|
|
},
|
|
genBody: (loopLocal) {
|
|
b.local_get(array1);
|
|
b.local_get(loopLocal);
|
|
if (elementType is w.PackedType) {
|
|
b.array_get_u(arrayType);
|
|
} else {
|
|
b.array_get(arrayType);
|
|
}
|
|
b.local_get(array2);
|
|
b.local_get(loopLocal);
|
|
if (elementType is w.PackedType) {
|
|
b.array_get_u(arrayType);
|
|
} else {
|
|
b.array_get(arrayType);
|
|
}
|
|
_equalsForValueType(b, elementType);
|
|
b.i32_eqz();
|
|
b.br_if(falseBlock);
|
|
});
|
|
b.i32_const(1);
|
|
b.return_();
|
|
b.end();
|
|
b.i32_const(0);
|
|
}
|
|
|
|
/// Invokes the builtin equality function for [storageType].
|
|
static void _equalsForValueType(
|
|
w.InstructionsBuilder b, w.StorageType storageType) {
|
|
if (storageType is w.RefType) {
|
|
b.ref_eq();
|
|
} else if (storageType == w.PackedType.i8 ||
|
|
storageType == w.PackedType.i16) {
|
|
b.i32_eq();
|
|
} else if (storageType == w.NumType.f32) {
|
|
b.f32_eq();
|
|
} else if (storageType == w.NumType.f64) {
|
|
b.f64_eq();
|
|
} else if (storageType == w.NumType.i32) {
|
|
b.i32_eq();
|
|
} else if (storageType == w.NumType.i64) {
|
|
b.i64_eq();
|
|
} else {
|
|
throw UnsupportedError('Could not find eq for $storageType');
|
|
}
|
|
}
|
|
|
|
@override
|
|
Never visitAuxiliaryConstant(AuxiliaryConstant node) {
|
|
throw UnsupportedError('Cannot canonicalize auxiliary constants.');
|
|
}
|
|
|
|
@override
|
|
void visitBoolConstant(BoolConstant node) {
|
|
_canonicalizeInstance(translator.boxedBoolClass);
|
|
}
|
|
|
|
@override
|
|
void visitConstructorTearOffConstant(ConstructorTearOffConstant node) {
|
|
_canonicalizeInstance(translator.closureClass);
|
|
}
|
|
|
|
@override
|
|
void visitDoubleConstant(DoubleConstant node) {
|
|
_canonicalizeInstance(translator.boxedDoubleClass);
|
|
}
|
|
|
|
@override
|
|
void visitInstanceConstant(InstanceConstant node) {
|
|
if (node.classNode == translator.wasmArrayClass) {
|
|
final dartElementType = node.typeArguments.single;
|
|
_canonicalizeArray(true, dartElementType);
|
|
} else if (node.classNode == translator.immutableWasmArrayClass) {
|
|
final dartElementType = node.typeArguments.single;
|
|
_canonicalizeArray(false, dartElementType);
|
|
} else {
|
|
_canonicalizeInstance(node.classNode);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void visitInstantiationConstant(InstantiationConstant node) {
|
|
_canonicalizeInstance(translator.closureClass);
|
|
}
|
|
|
|
@override
|
|
void visitIntConstant(IntConstant node) {
|
|
_canonicalizeInstance(translator.boxedIntClass);
|
|
}
|
|
|
|
@override
|
|
void visitListConstant(ListConstant node) {
|
|
_canonicalizeInstance(translator.immutableListClass);
|
|
}
|
|
|
|
@override
|
|
void visitMapConstant(MapConstant node) {
|
|
_canonicalizeInstance(translator.immutableMapClass);
|
|
}
|
|
|
|
@override
|
|
void visitNullConstant(NullConstant node) {}
|
|
|
|
@override
|
|
void visitRecordConstant(RecordConstant node) {
|
|
_canonicalizeInstance(translator.coreTypes.recordClass);
|
|
}
|
|
|
|
@override
|
|
void visitRedirectingFactoryTearOffConstant(
|
|
RedirectingFactoryTearOffConstant node) {
|
|
_canonicalizeInstance(translator.closureClass);
|
|
}
|
|
|
|
@override
|
|
void visitSetConstant(SetConstant node) {
|
|
_canonicalizeInstance(translator.immutableSetClass);
|
|
}
|
|
|
|
@override
|
|
void visitStaticTearOffConstant(StaticTearOffConstant node) {
|
|
_canonicalizeInstance(translator.closureClass);
|
|
}
|
|
|
|
@override
|
|
void visitStringConstant(StringConstant node) {
|
|
_canonicalizeInstance(translator.jsStringClass);
|
|
}
|
|
|
|
@override
|
|
void visitSymbolConstant(SymbolConstant node) {
|
|
return _canonicalizeInstance(translator.symbolClass);
|
|
}
|
|
|
|
@override
|
|
void visitTypeLiteralConstant(TypeLiteralConstant node) {
|
|
_canonicalizeInstance(translator.typeClass);
|
|
}
|
|
|
|
@override
|
|
Never visitTypedefTearOffConstant(TypedefTearOffConstant node) {
|
|
throw UnsupportedError('Cannot canonicalize typedef tearoff constants.');
|
|
}
|
|
|
|
@override
|
|
Never visitUnevaluatedConstant(UnevaluatedConstant node) {
|
|
throw UnsupportedError('Cannot canonicalize unevaluated constants.');
|
|
}
|
|
}
|
|
|
|
/// Populates [DirectCallMetadata] for a visited component.
|
|
class _Devirtualization extends CHADevirtualization {
|
|
final CoreTypes coreTypes;
|
|
|
|
_Devirtualization(
|
|
this.coreTypes,
|
|
Component component,
|
|
ClosedWorldClassHierarchy hierarchy,
|
|
ClassHierarchySubtypes hierarchySubtype)
|
|
: super(coreTypes, component, hierarchy, hierarchySubtype);
|
|
|
|
@override
|
|
void makeDirectCall(
|
|
TreeNode node, Member? target, DirectCallMetadata directCall) {
|
|
if (target != null && target.isDynamicSubmoduleOverridable(coreTypes)) {
|
|
return;
|
|
}
|
|
super.makeDirectCall(node, target, directCall);
|
|
}
|
|
}
|