| // 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.topTypeNonNullable); |
| 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.loadClassId(translator, translator.topTypeNonNullable); |
| 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.topTypeNonNullable, |
| translator.topTypeNonNullable, |
| ], 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}) { |
| 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); |
| } |
| } |