| // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'dart:typed_data'; |
| |
| import 'package:kernel/ast.dart'; |
| import 'package:kernel/class_hierarchy.dart' |
| show ClassHierarchy, ClassHierarchySubtypes, ClosedWorldClassHierarchy; |
| import 'package:kernel/core_types.dart'; |
| import 'package:kernel/library_index.dart'; |
| import 'package:kernel/names.dart'; |
| import 'package:kernel/src/printer.dart'; |
| import 'package:kernel/type_environment.dart'; |
| import 'package:vm/metadata/direct_call.dart'; |
| import 'package:vm/metadata/inferred_type.dart'; |
| import 'package:vm/metadata/procedure_attributes.dart'; |
| import 'package:vm/metadata/unboxing_info.dart'; |
| import 'package:vm/metadata/unreachable.dart'; |
| import 'package:wasm_builder/wasm_builder.dart' as w; |
| |
| import 'class_info.dart'; |
| import 'closures.dart'; |
| import 'code_generator.dart'; |
| import 'compiler_options.dart'; |
| import 'constants.dart'; |
| import 'constructor_info.dart'; |
| import 'dispatch_table.dart'; |
| import 'dynamic_dispatch_table.dart'; |
| import 'dynamic_dispatchers.dart'; |
| import 'functions.dart'; |
| import 'globals.dart'; |
| import 'kernel_nodes.dart'; |
| import 'modules.dart'; |
| import 'namer.dart'; |
| import 'param_info.dart'; |
| import 'records.dart'; |
| import 'reference_extensions.dart'; |
| import 'static_dispatch_table.dart'; |
| import 'symbols.dart'; |
| import 'table_based_globals.dart'; |
| import 'tags.dart'; |
| import 'types.dart'; |
| import 'util.dart' as util; |
| import 'wasm_annotations.dart'; |
| |
| /// Options controlling the translation. |
| class TranslatorOptions { |
| bool? enableUniqueTypes; |
| bool enableAsserts = false; |
| bool importSharedMemory = false; |
| bool uniqueConstantNames = true; |
| bool minifyInteropNames = true; |
| int optimizationLevel = 1; |
| bool? inliningOverride; |
| bool jsCompatibility = false; |
| bool standalone = false; |
| bool? omitImplicitTypeChecksOverride; |
| bool omitExplicitTypeChecks = false; |
| bool? omitBoundsChecksOverride; |
| bool? omitErrorDetailsOverride; |
| bool polymorphicSpecialization = false; |
| bool printKernel = false; |
| bool printWasm = false; |
| bool? minifyOverride; |
| bool verifyTypeChecks = false; |
| bool verbose = false; |
| bool enableExperimentalFfi = false; |
| bool enableExperimentalWasmInterop = false; |
| bool generateSourceMaps = true; |
| bool enableDeferredLoading = false; |
| bool enableMultiModuleStressTestMode = false; |
| bool enableProtobufTreeShaker = false; |
| bool enableProtobufMixinTreeShaker = false; |
| int inliningLimit = 0; |
| int? sharedMemoryMaxPages; |
| bool requireJsStringBuiltin = false; |
| List<int> watchPoints = []; |
| |
| bool get inlining => inliningOverride ?? optimizationLevel >= 1; |
| bool get minify => minifyOverride ?? optimizationLevel >= 2; |
| bool get omitErrorDetails => |
| omitErrorDetailsOverride ?? optimizationLevel >= 2; |
| bool get omitImplicitTypeChecks => |
| omitImplicitTypeChecksOverride ?? optimizationLevel >= 3; |
| bool get omitBoundsChecks => |
| omitBoundsChecksOverride ?? optimizationLevel >= 4; |
| bool get uniqueTypes => enableUniqueTypes ?? optimizationLevel >= 2; |
| } |
| |
| /// The main entry point for the translation from kernel to Wasm and the hub for |
| /// all global state in the compiler. |
| /// |
| /// This class also contains utility methods for types and code generation used |
| /// throughout the compiler. |
| class Translator with KernelNodes { |
| // Options for the translation. |
| final TranslatorOptions options; |
| |
| final Symbols symbols; |
| |
| // Kernel input and context. |
| final Component component; |
| final List<Library> libraries; |
| @override |
| final CoreTypes coreTypes; |
| late final TypeEnvironment typeEnvironment; |
| final ClosedWorldClassHierarchy hierarchy; |
| late final ClassHierarchySubtypes subtypes; |
| |
| @override |
| bool get isStandalone => options.standalone; |
| |
| // TFA-inferred metadata. |
| late final Map<TreeNode, DirectCallMetadata> directCallMetadata = |
| (component.metadata[DirectCallMetadataRepository.repositoryTag] |
| as DirectCallMetadataRepository) |
| .mapping; |
| late final Map<TreeNode, InferredType> inferredTypeMetadata = |
| (component.metadata[InferredTypeMetadataRepository.repositoryTag] |
| as InferredTypeMetadataRepository) |
| .mapping; |
| late final Map<TreeNode, InferredType> inferredArgTypeMetadata = |
| (component.metadata[InferredArgTypeMetadataRepository.repositoryTag] |
| as InferredArgTypeMetadataRepository) |
| .mapping; |
| late final Map<TreeNode, InferredType> inferredReturnTypeMetadata = |
| (component.metadata[InferredReturnTypeMetadataRepository.repositoryTag] |
| as InferredReturnTypeMetadataRepository) |
| .mapping; |
| late final Map<TreeNode, UnboxingInfoMetadata> unboxingInfoMetadata = |
| (component.metadata[UnboxingInfoMetadataRepository.repositoryTag] |
| as UnboxingInfoMetadataRepository) |
| .mapping; |
| late final Map<TreeNode, ProcedureAttributesMetadata> |
| procedureAttributeMetadata = |
| (component.metadata[ProcedureAttributesMetadataRepository.repositoryTag] |
| as ProcedureAttributesMetadataRepository) |
| .mapping; |
| late final UnreachableNodeMetadataRepository unreachableMetadata = |
| component.metadata[UnreachableNodeMetadataRepository.repositoryTag] |
| as UnreachableNodeMetadataRepository; |
| |
| // Other parts of the global compiler state. |
| @override |
| final LibraryIndex index; |
| late final ClosureLayouter closureLayouter; |
| late final ClassInfoCollector classInfoCollector; |
| late final CrossModuleFunctionTable crossModuleFunctionTable; |
| late final TableBasedGlobals tableBasedGlobals; |
| late final DispatchTable dispatchTable; |
| late final DynamicDispatchTable dynamicDispatchTable; |
| late final Globals globals; |
| late final DartGlobals dartGlobals; |
| late final Constants constants; |
| late final Types types; |
| late final ExceptionTags _exceptionTags; |
| late final CompilationQueue compilationQueue; |
| late final FunctionCollector functions; |
| |
| late final DeferredModuleLoadingMap loadingMap; |
| |
| final Namer exportNamer; |
| late final InteropMemberNamer interopMemberNamer; |
| |
| // Information about the program used and updated by the various phases. |
| |
| /// [ClassInfo]s of classes in the compilation unit and the [ClassInfo] for |
| /// the `#Top` struct. Indexed by class ID. Entries added by |
| /// [ClassInfoCollector]. |
| /// |
| /// Because anonymous mixin application classes don't have class IDs, they're |
| /// not in this list. |
| late final List<ClassInfo> classes; |
| |
| /// Same as [classes] but ordered such that info for class at index I will |
| /// have class info for superlass/superinterface at <I). |
| /// |
| /// This also includes anonymous mixin application classes. |
| late final List<ClassInfo> classesSupersFirst; |
| |
| late final ClassIdNumbering classIdNumbering; |
| |
| /// [ClassInfo]s of classes in the compilation unit. Entries added by |
| /// [ClassInfoCollector]. |
| final Map<Class, ClassInfo> classInfo = {}; |
| |
| /// Internalized strings to move to the JS runtime |
| final List<String> internalizedStringsForJSRuntime = []; |
| final Map<(w.ModuleBuilder, String), w.Global> _internalizedStringGlobals = |
| {}; |
| |
| final Map<w.HeapType, bool> _isCyclicHeapType = {}; |
| final Map<Field, int> fieldIndex = {}; |
| final Map<TypeParameter, int> typeParameterIndex = {}; |
| final Map<Reference, ParameterInfo> staticParamInfo = {}; |
| final Map<Field, w.Table> _declaredFieldTables = {}; |
| late final WasmTableImporter _importedFieldTables = WasmTableImporter( |
| this, |
| 'fieldTable', |
| ); |
| final Set<Member> membersContainingInnerFunctions = {}; |
| final Set<Member> membersBeingGenerated = {}; |
| final Map<Reference, Closures> constructorClosures = {}; |
| final Map<Reference, ConstructorInfo> constructorInfo = {}; |
| late final w.ValueType voidMarker = w.RefType.def( |
| w.StructType("void"), |
| nullable: true, |
| ); |
| final Map<Procedure, w.Memory> _memories = {}; |
| |
| // Lazily import FFI memory if used. |
| final _ffiMemoryImports = <w.ModuleBuilder, w.Memory>{}; |
| w.Memory ffiMemory(w.ModuleBuilder usingModule) { |
| return _ffiMemoryImports.putIfAbsent(usingModule, () { |
| return usingModule.memories.import( |
| "ffi", |
| "memory", |
| options.importSharedMemory, |
| 0, |
| options.sharedMemoryMaxPages, |
| ); |
| }); |
| } |
| |
| /// Maps record shapes to the record class for the shape. Classes generated |
| /// by `record_class_generator` library. |
| final Map<RecordShape, Class> recordClasses; |
| |
| // Caches for when identical source constructs need a common representation. |
| final Map<w.StorageType, w.ArrayType> immutableArrayTypeCache = {}; |
| final Map<w.StorageType, w.ArrayType> mutableArrayTypeCache = {}; |
| final Map<w.BaseFunction, w.Global> functionRefCache = {}; |
| final Map<Member, Map<w.ModuleBuilder, ClosureImplementation>> |
| tearOffFunctionCache = {}; |
| |
| final Map<FunctionNode, Map<w.ModuleBuilder, ClosureImplementation>> |
| closureImplementations = {}; |
| |
| // Some convenience accessors for commonly used values. |
| late final ClassInfo objectInfo = classInfo[coreTypes.objectClass]!; |
| late final ClassInfo closureInfo = classInfo[closureClass]!; |
| late final ClassInfo stackTraceInfo = classInfo[stackTraceClass]!; |
| late final ClassInfo recordInfo = classInfo[coreTypes.recordClass]!; |
| late final w.ArrayType typeArrayType = arrayTypeForDartType( |
| InterfaceType(typeClass, Nullability.nonNullable), |
| mutable: true, |
| ); |
| late final w.ArrayType listArrayType = |
| (classInfo[listBaseClass]!.struct.fields[FieldIndex.listArray].type |
| as w.RefType) |
| .heapType |
| as w.ArrayType; |
| late final w.ArrayType nullableObjectArrayType = arrayTypeForDartType( |
| coreTypes.objectRawType(Nullability.nullable), |
| mutable: true, |
| ); |
| late final w.RefType typeArrayTypeRef = w.RefType.def( |
| typeArrayType, |
| nullable: false, |
| ); |
| late final w.RefType nullableObjectArrayTypeRef = w.RefType.def( |
| nullableObjectArrayType, |
| nullable: false, |
| ); |
| |
| late final boxedIntType = boxedIntClass.getThisType( |
| coreTypes, |
| Nullability.nonNullable, |
| ); |
| late final boxedDoubleType = boxedDoubleClass.getThisType( |
| coreTypes, |
| Nullability.nonNullable, |
| ); |
| |
| // The wasm type used to hold values of Dart top types |
| // (e.g. `Object?`, `dynamic`) |
| late final w.RefType topType = classes[0].nullableType; |
| |
| // The wasm type used to hold values of Dart top types excluding null |
| // (e.g. `Object`) |
| late final w.RefType topTypeNonNullable = topType.withNullability(false); |
| |
| // The wasm type used to hold values of `StackTrace` |
| late final w.RefType stackTraceType = |
| translateType(coreTypes.stackTraceNonNullableRawType) as w.RefType; |
| |
| // The wasm type used to hold values of `StackTrace?` |
| late final w.RefType stackTraceTypeNullable = stackTraceType.withNullability( |
| true, |
| ); |
| |
| // The wasm type used to hold values of `String` |
| late final w.RefType stringType = |
| translateType(coreTypes.stringNonNullableRawType) as w.RefType; |
| |
| // The wasm type used to hold values of `String?` |
| late final w.RefType stringTypeNullable = stringType.withNullability(true); |
| |
| // The wasm type used to hold values of `Invocation` |
| late final w.RefType invocationType = |
| translateType( |
| InterfaceType(coreTypes.invocationClass, Nullability.nonNullable), |
| ) |
| as w.RefType; |
| |
| final Map<w.ModuleBuilder, PartialInstantiator> _partialInstantiators = {}; |
| PartialInstantiator getPartialInstantiatorForModule(w.ModuleBuilder module) { |
| return _partialInstantiators[module] ??= PartialInstantiator(this, module); |
| } |
| |
| final Map<w.ModuleBuilder, PolymorphicDispatchers> _polymorphicDispatchers = |
| {}; |
| PolymorphicDispatchers getPolymorphicDispatchersForModule( |
| w.ModuleBuilder module, |
| ) { |
| return _polymorphicDispatchers[module] ??= PolymorphicDispatchers( |
| this, |
| module, |
| ); |
| } |
| |
| final Map<w.ModuleBuilder, DynamicDispatchers> _dynamicDispatchers = {}; |
| DynamicDispatchers getDynamicDispatchersForModule(w.ModuleBuilder module) { |
| return _dynamicDispatchers[module] ??= DynamicDispatchers(this, module); |
| } |
| |
| final Map<w.ModuleBuilder, DummyValuesCollector> _dummyValueCollectors = {}; |
| DummyValuesCollector getDummyValuesCollectorForModule( |
| w.ModuleBuilder module, |
| ) { |
| return _dummyValueCollectors[module] ??= DummyValuesCollector(this, module); |
| } |
| |
| /// Dart types that have specialized Wasm representations. |
| late final Map<Class, w.StorageType> builtinTypes = { |
| coreTypes.boolClass: w.NumType.i32, |
| coreTypes.intClass: w.NumType.i64, |
| coreTypes.doubleClass: w.NumType.f64, |
| boxedBoolClass: w.NumType.i32, |
| boxedIntClass: w.NumType.i64, |
| boxedDoubleClass: w.NumType.f64, |
| wasmI8Class: w.PackedType.i8, |
| wasmI16Class: w.PackedType.i16, |
| wasmI32Class: w.NumType.i32, |
| wasmI64Class: w.NumType.i64, |
| wasmF32Class: w.NumType.f32, |
| wasmF64Class: w.NumType.f64, |
| wasmV128Class: w.NumType.v128, |
| wasmI8x16ImplClass: w.NumType.v128, |
| wasmI16x8ImplClass: w.NumType.v128, |
| wasmI32x4ImplClass: w.NumType.v128, |
| wasmI64x2ImplClass: w.NumType.v128, |
| wasmF32x4ImplClass: w.NumType.v128, |
| wasmF64x2ImplClass: w.NumType.v128, |
| wasmAnyRefClass: const w.RefType.any(nullable: false), |
| wasmExternRefClass: const w.RefType.extern(nullable: false), |
| wasmI31RefClass: const w.RefType.i31(nullable: false), |
| wasmFuncRefClass: const w.RefType.func(nullable: false), |
| wasmEqRefClass: const w.RefType.eq(nullable: false), |
| wasmStructRefClass: const w.RefType.struct(nullable: false), |
| wasmArrayRefClass: const w.RefType.array(nullable: false), |
| }; |
| |
| /// The box classes corresponding to each of the value types. |
| late final Map<w.ValueType, Class> boxedClasses = { |
| w.NumType.i32: boxedBoolClass, |
| w.NumType.i64: boxedIntClass, |
| w.NumType.f64: boxedDoubleClass, |
| }; |
| |
| late final Set<Class> boxClasses = { |
| boxedBoolClass, |
| boxedIntClass, |
| boxedDoubleClass, |
| }; |
| |
| /// Classes whose identity hash code is their hash code rather than the |
| /// identity hash code field in the struct. Each implementation class maps to |
| /// the class containing the implementation of its `hashCode` getter. |
| late final Map<Class, Class> valueClasses = { |
| boxedIntClass: boxedIntClass, |
| boxedDoubleClass: boxedDoubleClass, |
| boxedBoolClass: coreTypes.boolClass, |
| stringImplClass: stringImplClass, |
| }; |
| |
| /// Type for vtable entries for dynamic calls. These entries are used in |
| /// dynamic invocations and `Function.apply`. |
| late final w.FunctionType dynamicCallVtableEntryFunctionType = typesBuilder |
| .defineFunction( |
| [ |
| // Closure |
| w.RefType.def(closureLayouter.closureBaseStruct, nullable: false), |
| |
| // Type arguments |
| typeArrayTypeRef, |
| |
| // Positional arguments |
| nullableObjectArrayTypeRef, |
| |
| // Named arguments, represented as array of symbol and object pairs |
| nullableObjectArrayTypeRef, |
| ], |
| [topType], |
| ); |
| |
| // Module predicates and helpers |
| final ModuleOutputData _moduleOutputData; |
| Iterable<w.ModuleBuilder> get modules => _builderToOutput.keys; |
| w.ModuleBuilder get mainModule => |
| _outputToBuilder[_moduleOutputData.mainModule]!; |
| w.TypesBuilder get typesBuilder => mainModule.types; |
| final Map<ModuleMetadata, w.ModuleBuilder> _outputToBuilder = {}; |
| final Map<w.ModuleBuilder, ModuleMetadata> _builderToOutput = {}; |
| final Map<w.Module, w.ModuleBuilder> moduleToBuilder = {}; |
| bool get hasMultipleModules => _moduleOutputData.hasMultipleModules; |
| |
| w.ModuleBuilder moduleForReference(Reference reference) { |
| final module = _moduleOutputData.moduleForReference(reference); |
| return _outputToBuilder[module]!; |
| } |
| |
| /// The module where [constant] should be placed |
| /// |
| /// NOTE: This may return `null` for constants that are e.g. synthesized by |
| /// the backend. In that case the backend decides where to place the constant. |
| w.ModuleBuilder? moduleForConstant(Constant constant) { |
| final module = _moduleOutputData.moduleForConstant(constant); |
| if (module == null) return null; |
| return _outputToBuilder[module]; |
| } |
| |
| List<w.ModuleBuilder> modulesForLoadId(Library enclosingLibrary, int loadId) { |
| return [ |
| for (final moduleMetadata in loadingMap.moduleMap[loadId]) |
| _outputToBuilder[moduleMetadata]!, |
| ]; |
| } |
| |
| String nameForModule(w.ModuleBuilder module) => |
| _builderToOutput[module]!.moduleImportName; |
| |
| bool isMainModule(w.ModuleBuilder module) => _builderToOutput[module]!.isMain; |
| |
| /// Maps compiled members to their [Closures], with capture information. |
| final Map<Member, Closures> _memberClosures = {}; |
| |
| final List<void Function()> linkingActions = []; |
| |
| Closures getClosures(Member member, {bool findCaptures = true}) => |
| findCaptures |
| ? _memberClosures.putIfAbsent( |
| member, |
| () => Closures(this, member, findCaptures: true), |
| ) |
| : Closures(this, member, findCaptures: false); |
| |
| ConstructorInfo getConstructorInfo(Constructor node) => |
| constructorInfo[node.reference] ??= ConstructorInfo(node, this); |
| |
| Translator( |
| this.component, |
| this.coreTypes, |
| this.index, |
| this.recordClasses, |
| this.loadingMap, |
| this._moduleOutputData, |
| this.options, |
| ) : symbols = Symbols(options.minify), |
| libraries = component.libraries, |
| hierarchy = |
| ClassHierarchy(component, coreTypes) as ClosedWorldClassHierarchy, |
| exportNamer = Namer(minify: options.minify) { |
| typeEnvironment = TypeEnvironment(coreTypes, hierarchy); |
| subtypes = hierarchy.computeSubtypesInformation(); |
| closureLayouter = ClosureLayouter(this); |
| classInfoCollector = ClassInfoCollector(this); |
| crossModuleFunctionTable = CrossModuleFunctionTable(this); |
| tableBasedGlobals = TableBasedGlobals(this); |
| dispatchTable = DispatchTable(this); |
| dynamicDispatchTable = DynamicDispatchTable(this); |
| compilationQueue = CompilationQueue(this); |
| functions = FunctionCollector(this); |
| types = Types(this); |
| _exceptionTags = ExceptionTags(this); |
| |
| interopMemberNamer = InteropMemberNamer(coreTypes, exportNamer, options); |
| } |
| |
| void _initModules(Uri Function(String moduleName)? sourceMapUrlGenerator) { |
| for (final outputModule in _moduleOutputData.modules) { |
| // `moduleName` is the suffix appended to the filename which is the empty |
| // string for the main module. `moduleImportName` provides a non-empty |
| // name for every module. We provide the former to generate source map |
| // uris and the latter to fill the NameSection of the module. |
| final builder = w.ModuleBuilder( |
| outputModule.moduleImportName, |
| sourceMapUrlGenerator?.call(outputModule.moduleName), |
| parent: outputModule.isMain ? null : mainModule, |
| watchPoints: options.watchPoints, |
| ); |
| _outputToBuilder[outputModule] = builder; |
| _builderToOutput[builder] = outputModule; |
| moduleToBuilder[builder.module] = builder; |
| } |
| } |
| |
| void drainCompletionQueue() { |
| while (!compilationQueue.isEmpty) { |
| final task = compilationQueue.pop(); |
| task.run(this, options.printKernel, options.printWasm); |
| } |
| } |
| |
| Map<ModuleMetadata, w.Module> translate( |
| Uri Function(String moduleName)? sourceMapUrlGenerator, |
| ) { |
| _initModules(sourceMapUrlGenerator); |
| final dynamicCallShapes = DynamicCallSiteCollector.collect(component); |
| |
| closureLayouter.collect(); |
| classInfoCollector.collect(); |
| |
| globals = Globals(this); |
| dartGlobals = DartGlobals(this); |
| constants = Constants(this); |
| |
| dispatchTable.build(); |
| dynamicDispatchTable.build(dynamicCallShapes); |
| functions.initialize(); |
| |
| drainCompletionQueue(); |
| |
| assert(compilationQueue.isEmpty); |
| for (final action in linkingActions) { |
| action(); |
| } |
| assert(compilationQueue.isEmpty); |
| |
| constructorClosures.clear(); |
| dispatchTable.output(); |
| dynamicDispatchTable.output(); |
| crossModuleFunctionTable.output(); |
| tableBasedGlobals.outputTables(); |
| |
| for (ConstantInfo info in constants.constantInfo.values) { |
| info.printInitializer( |
| (function) { |
| _printFunction(function, info.constant); |
| }, |
| (global) { |
| if (options.printWasm) { |
| print("Global #${global.name}: ${info.constant}"); |
| if (global is w.GlobalBuilder) { |
| print(global.initializer.trace); |
| } |
| } |
| }, |
| ); |
| } |
| _printFunction(mainModule.startFunction, "init"); |
| |
| final neededForLoadList = <ModuleMetadata>{ |
| for ( |
| int loadId = 0; |
| loadId < loadingMap.dominatingLoadIds.length; |
| ++loadId |
| ) |
| if (loadingMap.dominatingLoadIds[loadId]?.isNotEmpty ?? false) |
| loadingMap.dedicatedModule[loadId]!, |
| }; |
| |
| // Remove empty modules. |
| _outputToBuilder.removeWhere((outputModule, moduleBuilder) { |
| if (moduleBuilder == mainModule) { |
| assert(!moduleBuilder.hasNoEffect); |
| return false; |
| } |
| return moduleBuilder.hasNoEffect && |
| !neededForLoadList.contains(outputModule); |
| }); |
| |
| // Now that we know which modules we're going to emit, let's prune the |
| // loading map to only contain those modules. |
| for (final loadList in loadingMap.moduleMap) { |
| loadList.removeWhere( |
| (moduleMetadata) => !_outputToBuilder.containsKey(moduleMetadata), |
| ); |
| } |
| |
| // This getter will be null if we pass e.g. `--use-load-ids` as the |
| // runtime code will then be pruned to call out to embedder instead of |
| // consulting the load mapping bundled in the app. |
| final loadingMapGetter = dartInternalLoadingMapGetter; |
| if (loadingMapGetter != null && !options.standalone) { |
| // This function will be null if we didn't pass `--use-load-ids` but we |
| // ended up not having any actual deferred code (e.g. `await |
| // foo.loadLibrary()` is never called anywhere). |
| final function = |
| (functions.getExistingFunction(loadingMapGetter.reference) |
| as w.FunctionBuilder?); |
| if (function != null) { |
| _patchLoadingMapGetter(function); |
| } |
| } |
| |
| // If original program uses deferred loading this will be non-null. |
| final loadingMapNamesGetter = dartInternalLoadingMapNamesGetter; |
| if (loadingMapNamesGetter != null && !options.standalone) { |
| // If the actual emitted code accesses the names (i.e. --no-minify and |
| // code emits a deferred library load) |
| assert(!options.minify); |
| final function = |
| (functions.getExistingFunction(loadingMapNamesGetter.reference) |
| as w.FunctionBuilder?); |
| if (function != null) { |
| _patchLoadingMapNamesGetter(function); |
| } |
| } |
| |
| final result = <ModuleMetadata, w.Module>{}; |
| _outputToBuilder.forEach((outputModule, builder) { |
| final module = builder.build(); |
| if (builder != mainModule) { |
| if (module.exports.exported.isNotEmpty) { |
| throw StateError('Deferred modules are not allowed to have exports.'); |
| } |
| } |
| result[outputModule] = module; |
| }); |
| return result; |
| } |
| |
| // NOTE: We do this after code generation is complete. So the code generation |
| // phase has the opportunity to generate more wasm modules and add them to the |
| // loading map. |
| // |
| // Keep in sync with sdk/lib/_internal/wasm/js_common/deferred_patch.dart's |
| // `_decodeEncodedModuleIds` and `_loadLibraryViaEmbedderModuleNames` |
| void _patchLoadingMapGetter(w.FunctionBuilder function) { |
| final moduleMap = loadingMap.moduleMap; |
| final byteArrayType = wasmArrayType(w.PackedType.i8, 'WasmI8'); |
| final arrayOfNullableByteArray = wasmArrayType( |
| w.RefType(byteArrayType, nullable: true), |
| 'WasmArray<WasmI8>', |
| ); |
| |
| // Make a global containing the load id -> module id list table. |
| final loadingMapGlobal = mainModule.globals.define( |
| w.GlobalType(w.RefType(arrayOfNullableByteArray, nullable: false)), |
| 'deferredLoadLists', |
| ); |
| loadingMapGlobal.initializer |
| ..i32_const(moduleMap.length) |
| ..array_new_default(arrayOfNullableByteArray) |
| ..end(); |
| |
| // Make the getter return that array. |
| _replaceBody(function) |
| ..global_get(loadingMapGlobal) |
| ..end(); |
| |
| // Emit code to initialize the load id -> module id list table. |
| final encodedSegments = <w.ModuleBuilder, w.DataSegmentBuilder>{}; |
| for (int loadId = 0; loadId < moduleMap.length; ++loadId) { |
| final moduleList = moduleMap[loadId]; |
| final domLoadId = loadingMap.dominatingLoadId[loadId]; |
| if (domLoadId == null) { |
| assert(moduleList.isEmpty); |
| // This must be the root module. It has only one module and it's loaded |
| // by the application loader, so nothing to do here. |
| continue; |
| } |
| |
| if (moduleList.isEmpty) continue; |
| |
| final moduleIdsEncoded = encodeLoadList(moduleList); |
| |
| final dominatorModuleMetadata = loadingMap.dedicatedModule[domLoadId]!; |
| final dominatorModuleBuilder = _outputToBuilder[dominatorModuleMetadata]!; |
| final startFunction = dominatorModuleBuilder.startFunction.body; |
| final dataSegment = encodedSegments.putIfAbsent( |
| dominatorModuleBuilder, |
| () => dominatorModuleBuilder.dataSegments.define(), |
| ); |
| |
| // Append the encoded module id list to the data segment & make start |
| // function patch the runtime with the list. |
| globals.readGlobal(startFunction, loadingMapGlobal); |
| startFunction.i32_const(loadId); |
| { |
| startFunction.i32_const(dataSegment.length); |
| startFunction.i32_const(moduleIdsEncoded.length); |
| startFunction.array_new_data(byteArrayType, dataSegment); |
| dataSegment.append(moduleIdsEncoded); |
| } |
| startFunction.array_set(arrayOfNullableByteArray); |
| } |
| |
| final mainModuleOutput = _builderToOutput[mainModule]!; |
| final prefix = WasmCompilerOptions.deferredModuleFilenamePrefix( |
| mainModuleOutput.moduleName, |
| ); |
| final prefixGetter = |
| functions.getExistingFunction( |
| dartInternalModuleNamePrefixGetter!.reference, |
| ) |
| as w.FunctionBuilder; |
| _replaceBody(prefixGetter) |
| ..global_get(getInternalizedStringGlobal(mainModule, prefix)) |
| ..end(); |
| } |
| |
| Uint8List encodeLoadList(List<ModuleMetadata> moduleList) { |
| // We sort the module ids increasingly, thereby allowing us to encode them |
| // via delta to previous module id. |
| final moduleIds = <int>[]; |
| for (int k = 0; k < moduleList.length; ++k) { |
| final moduleId = WasmCompilerOptions.idFromDeferredModuleFilename( |
| moduleList[k].moduleName, |
| ); |
| moduleIds.add(moduleId); |
| } |
| moduleIds.sort(); |
| |
| // Make the encoded list of module ids. |
| final moduleIdsEncoded = BytesBuilder(); |
| moduleIdsEncoded.writeULEB128(moduleList.length); |
| int lastId = 0; |
| for (int idIndex = 0; idIndex < moduleIds.length; ++idIndex) { |
| final moduleId = moduleIds[idIndex]; |
| final diff = moduleId - lastId; |
| moduleIdsEncoded.writeULEB128(diff); |
| lastId = moduleId; |
| } |
| return moduleIdsEncoded.takeBytes(); |
| } |
| |
| void _patchLoadingMapNamesGetter(w.FunctionBuilder function) { |
| final externRef = w.RefType.extern(nullable: false); |
| final arrayExternRef = wasmArrayType( |
| externRef, |
| externRef.toString(), |
| mutable: false, |
| ); |
| |
| _lazyInitializeGlobal( |
| function, |
| w.RefType(arrayExternRef, nullable: false), |
| 'loadIdModuleImportInfo', |
| (b) { |
| int index = 0; |
| loadingMap.loadIds.forEach((tuple, loadId) { |
| assert(index == loadId); |
| index++; |
| final libraryName = tuple.$1.importUri.toString(); |
| final prefixName = tuple.$2; |
| b.global_get( |
| getInternalizedStringGlobal(function.moduleBuilder, libraryName), |
| ); |
| b.global_get( |
| getInternalizedStringGlobal(function.moduleBuilder, prefixName), |
| ); |
| }); |
| b.array_new_fixed(arrayExternRef, 2 * loadingMap.loadIds.length); |
| }, |
| ); |
| } |
| |
| void _lazyInitializeGlobal( |
| w.FunctionBuilder f, |
| w.ValueType type, |
| String name, |
| void Function(w.InstructionsBuilder) gen, |
| ) { |
| final globalType = w.GlobalType(type.withNullability(true)); |
| final global = f.moduleBuilder.globals.define(globalType, name); |
| global.initializer |
| ..ref_null(w.HeapType.none) |
| ..end(); |
| |
| final b = _replaceBody(f); |
| |
| final label = b.block(const [], [type]); |
| b.global_get(global); |
| b.br_on_non_null(label); |
| gen(b); |
| final local = b.addLocal(type); |
| b.local_tee(local); |
| b.global_set(global); |
| b.local_get(local); |
| b.end(); |
| b.end(); |
| } |
| |
| w.InstructionsBuilder _replaceBody(w.FunctionBuilder function) { |
| final newBody = w.InstructionsBuilder( |
| function.moduleBuilder, |
| function.type.inputs, |
| function.type.outputs, |
| ); |
| function.replaceBody(newBody); |
| return newBody; |
| } |
| |
| void _printFunction(w.BaseFunction function, Object name) { |
| if (options.printWasm) { |
| print("#${function.name}: $name"); |
| final f = function; |
| if (f is w.FunctionBuilder) { |
| print(f.body.trace); |
| } |
| } |
| } |
| |
| /// Calls the function referred to in [reference] either directly or via a |
| /// cross-module call. |
| /// |
| /// When performing a direct call it may inline the target if allowed and |
| /// beneficial. |
| List<w.ValueType> callReference( |
| Reference reference, |
| w.InstructionsBuilder b, |
| ) => callTarget(directCallTarget(reference), b, reference); |
| |
| List<w.ValueType> callTarget( |
| CallTarget callTarget, |
| w.InstructionsBuilder b, [ |
| Reference? reference, |
| ]) { |
| late final List<w.ValueType> outputs; |
| if (callTarget.supportsInlining) { |
| final decision = callTarget.shouldInline; |
| if (decision.shouldInline) { |
| b.comment('Inlining ${callTarget.name}, reason: ${decision.reason}'); |
| outputs = b.inlineCallTo(callTarget); |
| } else { |
| b.comment('Not inlining, reason: ${decision.reason}'); |
| outputs = callFunction(callTarget.function, b, reference); |
| } |
| } else { |
| outputs = callFunction(callTarget.function, b, reference); |
| } |
| if (callTarget.synthesizeNullReturnValue) { |
| assert(outputs.isEmpty); |
| b.ref_null(w.HeapType.none); |
| return [w.RefType(w.HeapType.none, nullable: true)]; |
| } |
| if (callTarget.synthesizeNoReturn) { |
| assert(outputs.isEmpty); |
| b.unreachable(); |
| return const []; |
| } |
| return outputs; |
| } |
| |
| late final WasmMemoryImporter _importedMemories = WasmMemoryImporter( |
| this, |
| 'memory', |
| ); |
| |
| /// Generates a set of instructions to call [function] adding indirection |
| /// if the call crosses a module boundary. Calls the function directly if it |
| /// is local. Imports the function and calls it directly if is in the main |
| /// module. Otherwise does an indirect call through the static dispatch table. |
| List<w.ValueType> callFunction( |
| w.BaseFunction function, |
| w.InstructionsBuilder b, [ |
| Reference? target, |
| ]) { |
| // If the target function is defined in the same module as the caller, just |
| // invoke it. |
| final targetModuleBuilder = moduleToBuilder[function.enclosingModule]!; |
| if (targetModuleBuilder == b.moduleBuilder) { |
| b.call(function); |
| return b.emitUnreachableIfNoResult(function.type.outputs); |
| } |
| |
| // If the target function is already available via the dispatch table, use |
| // it from there. |
| if (target != null) { |
| final dispatchTableIndex = dispatchTable.indexForTarget(target); |
| if (dispatchTableIndex != null) { |
| b.i32_const(dispatchTableIndex); |
| b.call_indirect( |
| function.type, |
| dispatchTable.getWasmTable(b.moduleBuilder), |
| ); |
| return b.emitUnreachableIfNoResult(function.type.outputs); |
| } |
| } |
| |
| // Otherwise add & call via the cross module function table. |
| b.i32_const(crossModuleFunctionTable.indexForFunction(function)); |
| b.call_indirect( |
| function.type, |
| crossModuleFunctionTable.getWasmTable(b.moduleBuilder), |
| ); |
| return b.emitUnreachableIfNoResult(function.type.outputs); |
| } |
| |
| void callDispatchTable( |
| w.InstructionsBuilder b, |
| SelectorInfo selector, { |
| Reference? interfaceTarget, |
| required bool useUncheckedEntry, |
| }) { |
| functions.recordSelectorUse(selector, useUncheckedEntry); |
| |
| final offset = selector.targets(unchecked: useUncheckedEntry).offset; |
| if (offset == null) { |
| b.unreachable(); |
| return; |
| } |
| |
| final receiverType = selector.signature.inputs.first; |
| b.loadClassId(this, receiverType); |
| if (offset != 0) { |
| b.i32_const(offset); |
| b.i32_add(); |
| } |
| final signature = selector.signature; |
| b.call_indirect(signature, dispatchTable.getWasmTable(b.moduleBuilder)); |
| b.emitUnreachableIfNoResult(signature.outputs); |
| } |
| |
| Class classForType(DartType type) { |
| return toMostSpecificInterfaceType(type).classNode; |
| } |
| |
| InterfaceType toMostSpecificInterfaceType(DartType originalType) { |
| var type = originalType; |
| while (type is TypeParameterType) { |
| type = type.bound; |
| } |
| while (type is StructuralParameterType) { |
| type = type.bound; |
| } |
| final objectType = coreTypes.objectNonNullableRawType; |
| final nullability = originalType.isPotentiallyNullable |
| ? Nullability.nullable |
| : Nullability.nonNullable; |
| return (switch (type) { |
| InterfaceType() => type, |
| FunctionType() => coreTypes.functionNonNullableRawType, |
| RecordType() => coreTypes.recordNonNullableRawType, |
| IntersectionType() => toMostSpecificInterfaceType(type.right), |
| ExtensionType() => toMostSpecificInterfaceType(type.extensionTypeErasure), |
| DynamicType() || VoidType() => objectType, |
| NullType() => objectType, |
| NeverType() => objectType, |
| FutureOrType() => objectType, |
| StructuralParameterType() || |
| TypeParameterType() => throw 'unreachable, handled above', |
| TypedefType() => throw 'unreachable, should be desugared by CFE', |
| InvalidType() => throw 'unreachable, should be compile-time error', |
| AuxiliaryType() => throw 'unreachable, unused by dart2wasm', |
| // ignore: unreachable_switch_case |
| ExperimentalType() => throw 'unreachable, experimental', |
| }).withDeclaredNullability(nullability); |
| } |
| |
| /// Compute the runtime type of a tear-off. This is the signature of the |
| /// method with the types of all covariant parameters replaced by `Object?`. |
| FunctionType getTearOffType(Member method) { |
| if (method is Constructor) { |
| return method.function.computeFunctionType(Nullability.nonNullable); |
| } |
| |
| method as Procedure; |
| assert( |
| method.kind == ProcedureKind.Method || |
| method.kind == ProcedureKind.Factory, |
| ); |
| final FunctionType staticType = method.function.computeFunctionType( |
| Nullability.nonNullable, |
| ); |
| |
| final positionalParameters = List.of(staticType.positionalParameters); |
| assert( |
| positionalParameters.length == |
| method.function.positionalParameters.length, |
| ); |
| |
| final namedParameters = List.of(staticType.namedParameters); |
| assert(namedParameters.length == method.function.namedParameters.length); |
| |
| for (int i = 0; i < positionalParameters.length; i++) { |
| final param = method.function.positionalParameters[i]; |
| if (param.isCovariantByDeclaration || param.isCovariantByClass) { |
| positionalParameters[i] = coreTypes.objectNullableRawType; |
| } |
| } |
| |
| for (int i = 0; i < namedParameters.length; i++) { |
| final param = method.function.namedParameters[i]; |
| if (param.isCovariantByDeclaration || param.isCovariantByClass) { |
| namedParameters[i] = NamedType( |
| namedParameters[i].name, |
| coreTypes.objectNullableRawType, |
| isRequired: namedParameters[i].isRequired, |
| ); |
| } |
| } |
| |
| return FunctionType( |
| positionalParameters, |
| staticType.returnType, |
| Nullability.nonNullable, |
| namedParameters: namedParameters, |
| typeParameters: staticType.typeParameters, |
| requiredParameterCount: staticType.requiredParameterCount, |
| ); |
| } |
| |
| /// Get the Dart exception tag for [module]. |
| /// |
| /// This tag catches Dart exceptions. |
| w.Tag getDartExceptionTag(w.ModuleBuilder module) => |
| _exceptionTags.getDartExceptionTag(module); |
| |
| /// Get the JS exception tag for [module]. |
| /// |
| /// This tag catches JS exceptions. |
| w.Tag getJsExceptionTag(w.ModuleBuilder module) => |
| _exceptionTags.getJsExceptionTag(module); |
| |
| w.ValueType translateReturnType(DartType type) { |
| if (type is NeverType && !type.isPotentiallyNullable) { |
| return const w.RefType.none(nullable: false); |
| } |
| return translateType(type); |
| } |
| |
| w.ValueType translateType(DartType type) { |
| w.StorageType wasmType = translateStorageType(type); |
| if (wasmType is w.ValueType) return wasmType; |
| |
| // We represent the packed i8/i16 types as zero-extended i32 type. |
| // Dart code can currently only obtain them via loading from packed arrays |
| // and only use them for storing into packed arrays (there are no |
| // conversion or other operations on WasmI8/WasmI16). |
| if (wasmType is w.PackedType) return w.NumType.i32; |
| throw "Cannot translate $type to wasm type."; |
| } |
| |
| bool _hasSuperclass(Class cls, Class superclass) { |
| while (cls.superclass != null) { |
| cls = cls.superclass!; |
| if (cls == superclass) return true; |
| } |
| return false; |
| } |
| |
| bool isWasmType(Class cls) => |
| cls == wasmTypesBaseClass || _hasSuperclass(cls, wasmTypesBaseClass); |
| |
| w.StorageType translateStorageType(DartType type, {bool unbox = true}) { |
| bool nullable = type.isPotentiallyNullable; |
| if (type is InterfaceType) { |
| Class cls = type.classNode; |
| |
| if (cls == coreTypes.deprecatedNullClass) { |
| return const w.RefType.none(nullable: true); |
| } |
| |
| // Abstract `Function`? |
| if (cls == coreTypes.functionClass) { |
| return w.RefType.def( |
| closureLayouter.closureBaseStruct, |
| nullable: nullable, |
| ); |
| } |
| |
| // Wasm array? |
| if (cls == wasmArrayClass) { |
| DartType elementType = type.typeArguments.single; |
| return w.RefType.def( |
| arrayTypeForDartType(elementType, mutable: true), |
| nullable: nullable, |
| ); |
| } |
| |
| // Immutable Wasm array? |
| if (cls == immutableWasmArrayClass) { |
| DartType elementType = type.typeArguments.single; |
| return w.RefType.def( |
| arrayTypeForDartType(elementType, mutable: false), |
| nullable: nullable, |
| ); |
| } |
| |
| // Wasm function? |
| if (cls == wasmFunctionClass) { |
| DartType functionType = type.typeArguments.single; |
| if (functionType is! FunctionType) { |
| throw "The type argument of a WasmFunction must be a function type"; |
| } |
| if (functionType.typeParameters.isNotEmpty || |
| functionType.namedParameters.isNotEmpty || |
| functionType.requiredParameterCount != |
| functionType.positionalParameters.length) { |
| throw "A WasmFunction can't have optional/type parameters"; |
| } |
| DartType returnType = functionType.returnType; |
| bool voidReturn = |
| returnType is InterfaceType && |
| returnType.classNode == wasmVoidClass; |
| List<w.ValueType> inputs = [ |
| for (DartType type in functionType.positionalParameters) |
| translateType(type), |
| ]; |
| List<w.ValueType> outputs = [ |
| if (!voidReturn) translateType(functionType.returnType), |
| ]; |
| w.FunctionType wasmType = typesBuilder.defineFunction(inputs, outputs); |
| return w.RefType.def(wasmType, nullable: nullable); |
| } |
| |
| // Other built-in type? |
| w.StorageType? builtin = (unbox || !boxClasses.contains(cls)) |
| ? builtinTypes[cls] |
| : null; |
| if (builtin != null) { |
| if (!nullable) { |
| return builtin; |
| } |
| if (isWasmType(cls)) { |
| if (builtin.isPrimitive) throw "Wasm numeric types can't be nullable"; |
| return (builtin as w.RefType).withNullability(nullable); |
| } |
| final boxedBuiltin = classInfo[boxedClasses[builtin]!]!; |
| return boxedBuiltin.typeWithNullability(nullable); |
| } |
| |
| // Regular class. |
| return classInfo[cls]!.repr.withNullability(nullable); |
| } |
| if (type is DynamicType || type is VoidType) { |
| return topType; |
| } |
| if (type is NullType) { |
| return const w.RefType.none(nullable: true); |
| } |
| if (type is NeverType) { |
| // We should translate `Never` to a bottom type in wasm. Though right now |
| // for examples like this |
| // ``` |
| // Never a; |
| // try { |
| // a = throw 'a; |
| // } catch (e, s) {} |
| // ``` |
| // our code generator makes a local for `a` and tries to initialize it |
| // with a default value (of which there are none if we make it real |
| // bottom). |
| // => We make it nullable here. |
| return const w.RefType.none(nullable: true); |
| } |
| if (type is TypeParameterType) { |
| return translateStorageType( |
| nullable |
| ? type.bound.withDeclaredNullability(Nullability.nullable) |
| : type.bound, |
| ); |
| } |
| if (type is IntersectionType) { |
| return translateStorageType(type.left); |
| } |
| if (type is FutureOrType) { |
| return topType.withNullability(nullable); |
| } |
| if (type is FunctionType) { |
| ClosureRepresentation? representation = closureLayouter |
| .getClosureRepresentation( |
| type.typeParameters.length, |
| type.positionalParameters.length, |
| type.namedParameters.map((p) => p.name).toList(), |
| ); |
| return w.RefType.def( |
| representation != null |
| ? representation.closureStruct |
| : classInfo[typeClass]!.struct, |
| nullable: nullable, |
| ); |
| } |
| if (type is ExtensionType) { |
| return translateStorageType(type.extensionTypeErasure); |
| } |
| if (type is RecordType) { |
| return getRecordClassInfo(type).typeWithNullability(nullable); |
| } |
| throw "Unsupported type ${type.runtimeType}"; |
| } |
| |
| w.ArrayType arrayTypeForDartType(DartType type, {required bool mutable}) { |
| while (type is TypeParameterType) { |
| type = type.bound; |
| } |
| // If we write `WasmArray<BoxedInt>` we actually want an array of boxed |
| // integers and not a `WasmArray<WasmI64>`. |
| return wasmArrayType( |
| translateStorageType(type, unbox: false), |
| type.toText(defaultAstTextStrategy), |
| mutable: mutable, |
| ); |
| } |
| |
| w.ArrayType wasmArrayType( |
| w.StorageType type, |
| String name, { |
| bool mutable = true, |
| }) { |
| final cache = mutable ? mutableArrayTypeCache : immutableArrayTypeCache; |
| return cache.putIfAbsent( |
| type, |
| () => typesBuilder.defineArray( |
| "${mutable ? '' : 'Immutable'}Array<$name>", |
| elementType: w.FieldType(type, mutable: mutable), |
| ), |
| ); |
| } |
| |
| /// Translate a Dart type as it should appear on parameters and returns of |
| /// imported and exported functions. All wasm types are allowed on the interop |
| /// boundary, but in order to be compatible with the `--closed-world` mode of |
| /// Binaryen, we coerce all reference types to abstract reference types |
| /// (`anyref`, `funcref` or `externref`). |
| /// This function can be called before the class info is built. |
| w.ValueType translateExternalType(DartType type) { |
| final bool isPotentiallyNullable = type.isPotentiallyNullable; |
| if (type is InterfaceType) { |
| Class cls = type.classNode; |
| if (cls == wasmFuncRefClass || cls == wasmFunctionClass) { |
| return w.RefType.func(nullable: isPotentiallyNullable); |
| } |
| if (cls == wasmExternRefClass) { |
| return w.RefType.extern(nullable: isPotentiallyNullable); |
| } |
| if (cls == wasmArrayRefClass) { |
| return w.RefType.array(nullable: isPotentiallyNullable); |
| } |
| if (cls == wasmArrayClass) { |
| final elementType = translateExternalStorageType( |
| type.typeArguments.single, |
| ); |
| return w.RefType.def( |
| wasmArrayType(elementType, '$elementType', mutable: true), |
| nullable: isPotentiallyNullable, |
| ); |
| } |
| if (!isPotentiallyNullable) { |
| w.StorageType? builtin = builtinTypes[cls]; |
| if (builtin != null && builtin.isPrimitive) { |
| return builtin as w.ValueType; |
| } |
| } |
| } |
| // TODO(joshualitt): We'd like to use the potential nullability here too, |
| // but unfortunately this seems to break things. |
| return w.RefType.any(nullable: true); |
| } |
| |
| w.StorageType translateExternalStorageType(DartType type) { |
| if (type is InterfaceType) { |
| final cls = type.classNode; |
| if (isWasmType(cls)) { |
| final isNullable = type.isPotentiallyNullable; |
| final w.StorageType? builtin = builtinTypes[cls]; |
| if (builtin != null) { |
| if (!isNullable) return builtin; |
| if (builtin.isPrimitive) throw "Wasm numeric types can't be nullable"; |
| return (builtin as w.RefType).withNullability(isNullable); |
| } |
| } |
| } |
| return translateExternalType(type) as w.RefType; |
| } |
| |
| /// Creates a global reference to [f] in its [w.BaseFunction.enclosingModule]. |
| w.Global makeFunctionRef(w.BaseFunction f) { |
| final fModuleBuilder = moduleToBuilder[f.enclosingModule]!; |
| return functionRefCache.putIfAbsent(f, () { |
| final global = fModuleBuilder.globals.define( |
| w.GlobalType(w.RefType.def(f.type, nullable: false), mutable: false), |
| ); |
| global.initializer.ref_func(f); |
| global.initializer.end(); |
| return global; |
| }); |
| } |
| |
| ClosureImplementation getTearOffClosure( |
| Member member, |
| w.ModuleBuilder closureModule, |
| ) { |
| assert( |
| member is Constructor || |
| member is Procedure && |
| (member.kind == ProcedureKind.Method || |
| member.kind == ProcedureKind.Factory), |
| ); |
| final innerCache = tearOffFunctionCache.putIfAbsent(member, () => {}); |
| return innerCache.putIfAbsent(closureModule, () { |
| final reference = getFunctionEntry( |
| member.reference, |
| uncheckedEntry: false, |
| ); |
| return getClosure( |
| member.function!, |
| directCallTarget(reference), |
| closureModule, |
| paramInfoForDirectCall(reference), |
| "$member tear-off", |
| ); |
| }); |
| } |
| |
| final _closureArgumentsDispatchers = |
| <w.ModuleBuilder, Map<ClosureRepresentation, w.BaseFunction>>{}; |
| w.BaseFunction getClosureArgumentsDispatcher( |
| w.ModuleBuilder module, |
| ClosureRepresentation r, |
| ) { |
| // We can only unpack (type, positional, named) argument arrays and forward |
| // to specific vtable entries if we have closed-world knowledge of all used |
| // name combinations. |
| assert(!closureLayouter.usesFunctionApplyWithNamedArguments); |
| |
| final moduleCache = _closureArgumentsDispatchers[module] ??= {}; |
| return moduleCache.putIfAbsent(r, () { |
| final representationString = |
| '${r.typeCount}-' |
| '${r.maxPositionalCount}' |
| '${r.hasNamed ? '-' : ''}' |
| '${r.nameCombinations.join('-')}'; |
| final function = module.functions.define( |
| dynamicCallVtableEntryFunctionType, |
| "closure arguments dispatcher representation=$representationString", |
| ); |
| compilationQueue.add( |
| CompilationTask( |
| function, |
| _ClosureArgumentsToVtableEntryDispatcherGenerator(this, r, function), |
| ), |
| ); |
| return function; |
| }); |
| } |
| |
| ClosureImplementation getClosure( |
| FunctionNode functionNode, |
| CallTarget target, |
| w.ModuleBuilder closureModule, |
| ParameterInfo paramInfo, |
| String name, |
| ) { |
| // We compile a block multiple times in try-catch, to catch Dart exceptions |
| // and then again to catch JS exceptions. We may also ask for |
| // `ClosureImplementation` for a local function multiple times as we see |
| // direct calls to the closure (in TFA direct-call metadata). Avoid |
| // recompiling the closures in these cases by caching implementations. |
| // |
| // Note that every `FunctionNode` passed to this method will have one |
| // `ParameterInfo` for them. For local functions, the `ParameterInfo` will |
| // be the one generated by `ParameterInfo.fromLocalFunction`, for others it |
| // will be the value returned by `paramInfoForDirectCall`. So the key for |
| // this cache can be just `FunctionNode`, instead of `(FunctionNode, |
| // ParameterInfo)`. |
| final existingImplementation = |
| closureImplementations[functionNode]?[closureModule]; |
| if (existingImplementation != null) { |
| return existingImplementation; |
| } |
| |
| final functionType = functionNode.computeFunctionType( |
| Nullability.nonNullable, |
| ); |
| |
| // Look up the closure representation for the signature. |
| final int typeCount = functionType.typeParameters.length; |
| final int positionalCount = functionType.positionalParameters.length; |
| final namedParamsSorted = functionType.namedParameters.toList() |
| ..sort((p1, p2) => p1.name.compareTo(p2.name)); |
| List<String> names = namedParamsSorted.map((p) => p.name).toList(); |
| assert(typeCount == paramInfo.typeParamCount); |
| assert(positionalCount <= paramInfo.positional.length); |
| assert(names.length <= paramInfo.named.length); |
| assert( |
| target.signature.inputs.length == |
| (paramInfo.takesContextOrReceiver ? 1 : 0) + |
| paramInfo.typeParamCount + |
| paramInfo.positional.length + |
| paramInfo.named.length, |
| ); |
| ClosureRepresentation representation = closureLayouter |
| .getClosureRepresentation(typeCount, positionalCount, names)!; |
| assert( |
| representation.vtableStruct.fields.length == |
| representation.vtableBaseIndex + |
| (1 + positionalCount) + |
| representation.nameCombinations.length, |
| ); |
| |
| List<w.BaseFunction> functions = []; |
| |
| bool canBeCalledWith(int posArgCount, List<String> argNames) { |
| if (posArgCount < functionNode.requiredParameterCount) { |
| return false; |
| } |
| |
| int namedArgIdx = 0, namedParamIdx = 0; |
| while (namedArgIdx < argNames.length && |
| namedParamIdx < namedParamsSorted.length) { |
| int comp = argNames[namedArgIdx].compareTo( |
| namedParamsSorted[namedParamIdx].name, |
| ); |
| if (comp < 0) { |
| // Unexpected named argument passed |
| return false; |
| } else if (comp > 0) { |
| if (namedParamsSorted[namedParamIdx].isRequired) { |
| // Required named parameter not passed |
| return false; |
| } else { |
| // Optional named parameter not passed |
| namedParamIdx++; |
| continue; |
| } |
| } else { |
| // Expected required or optional named parameter passed |
| namedArgIdx++; |
| namedParamIdx++; |
| } |
| } |
| |
| if (namedArgIdx < argNames.length) { |
| // Unexpected named argument(s) passed |
| return false; |
| } |
| |
| while (namedParamIdx < namedParamsSorted.length) { |
| if (namedParamsSorted[namedParamIdx++].isRequired) { |
| // Required named parameter not passed |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| w.BaseFunction makeTrampoline( |
| w.FunctionType signature, |
| int posArgCount, |
| List<String> argNames, |
| ) { |
| final trampoline = closureModule.functions.define( |
| signature, |
| "$name trampoline", |
| ); |
| compilationQueue.add( |
| CompilationTask( |
| trampoline, |
| _ClosureTrampolineGenerator( |
| this, |
| trampoline, |
| target, |
| typeCount, |
| posArgCount, |
| argNames, |
| paramInfo, |
| ), |
| ), |
| ); |
| return trampoline; |
| } |
| |
| w.BaseFunction makeDynamicCallEntry() { |
| final function = closureModule.functions.define( |
| dynamicCallVtableEntryFunctionType, |
| "$name dynamic call entry", |
| ); |
| compilationQueue.add( |
| CompilationTask( |
| function, |
| _ClosureDynamicEntryGenerator( |
| this, |
| functionNode, |
| target, |
| paramInfo, |
| name, |
| function, |
| ), |
| ), |
| ); |
| return function; |
| } |
| |
| void fillVtableEntry( |
| w.InstructionsBuilder ib, |
| int posArgCount, |
| List<String> argNames, |
| ) { |
| int fieldIndex = representation.vtableBaseIndex + functions.length; |
| assert( |
| fieldIndex == |
| representation.fieldIndexForSignature(posArgCount, argNames), |
| ); |
| w.FunctionType signature = representation.vtableStruct.getVtableEntryAt( |
| fieldIndex, |
| ); |
| w.BaseFunction function = canBeCalledWith(posArgCount, argNames) |
| ? makeTrampoline(signature, posArgCount, argNames) |
| : getDummyValuesCollectorForModule( |
| ib.moduleBuilder, |
| ).getDummyFunction(signature); |
| functions.add(function); |
| ib.ref_func(function); |
| } |
| |
| final vtable = closureModule.globals.define( |
| w.GlobalType( |
| w.RefType.def(representation.vtableStruct, nullable: false), |
| mutable: false, |
| ), |
| ); |
| final ib = vtable.initializer; |
| |
| // NOTE: If anywhere in the program `Function.apply` is used with named |
| // arguments, then we don't know which name-combinations may be used and we |
| // want to avoid creating vtable entries for all possible name combinations. |
| // So also in this situation we cannot dispatch to representation-specific |
| // vtable entries. |
| // |
| // If none of the two cases above apply, we can make the dynamic call entry |
| // be a shared stub that dispatches (based on arguments) to the right |
| // representation specific vtable entry. This saves code size as we don't |
| // have 1 dynamic call entry function per closure but rather 1 per closure |
| // shape / representation. |
| w.BaseFunction? dynamicCallEntry; |
| if (closureLayouter.usesFunctionApplyWithNamedArguments) { |
| ib.ref_func(dynamicCallEntry = makeDynamicCallEntry()); |
| } |
| if (representation.isGeneric) { |
| ib.ref_func( |
| representation.instantiationTypeComparisonFunctionForModule( |
| ib.moduleBuilder, |
| ), |
| ); |
| ib.ref_func( |
| representation.instantiationTypeHashFunctionForModule(ib.moduleBuilder), |
| ); |
| ib.ref_func( |
| representation.instantiationFunctionForModule(ib.moduleBuilder), |
| ); |
| } |
| for (int posArgCount = 0; posArgCount <= positionalCount; posArgCount++) { |
| fillVtableEntry(ib, posArgCount, const []); |
| } |
| for (NameCombination nameCombination in representation.nameCombinations) { |
| fillVtableEntry(ib, positionalCount, nameCombination.names); |
| } |
| |
| ib.struct_new(representation.vtableStruct); |
| ib.end(); |
| |
| final implementation = ClosureImplementation( |
| representation, |
| functions, |
| dynamicCallEntry, |
| vtable, |
| closureModule, |
| paramInfo, |
| ); |
| (closureImplementations[functionNode] ??= {})[closureModule] = |
| implementation; |
| return implementation; |
| } |
| |
| w.ValueType outputOrVoid(List<w.ValueType> outputs) { |
| return outputs.isEmpty ? voidMarker : outputs.single; |
| } |
| |
| bool needsConversion(w.ValueType from, w.ValueType to) { |
| return (from == voidMarker) ^ (to == voidMarker) || !from.isSubtypeOf(to); |
| } |
| |
| void convertType(w.InstructionsBuilder b, w.ValueType from, w.ValueType to) { |
| if (identical(from, to)) return; |
| if (from == voidMarker || to == voidMarker) { |
| if (from != voidMarker) { |
| b.drop(); |
| return; |
| } |
| if (to != voidMarker) { |
| // This can happen e.g. when a `return;` is guaranteed to be never taken |
| // but TFA didn't remove the dead code. |
| b.unreachable(); |
| return; |
| } |
| } |
| |
| if (!from.isSubtypeOf(to)) { |
| if (from is w.RefType && to is w.RefType) { |
| if (from.withNullability(false).isSubtypeOf(to)) { |
| // Null check |
| b.ref_as_non_null(); |
| } else { |
| // Downcast |
| b.ref_cast(to); |
| } |
| } else if (to is w.RefType) { |
| // Boxing |
| Class cls = boxedClasses[from]!; |
| ClassInfo info = classInfo[cls]!; |
| assert( |
| info.struct.isSubtypeOf(to.heapType), |
| '${info.struct} is not a subtype of ${to.heapType}', |
| ); |
| |
| if (cls == boxedBoolClass) { |
| final constantType = w.RefType(info.struct, nullable: false); |
| b.if_([], [constantType]); |
| constants.instantiateConstant(b, BoolConstant(true), constantType); |
| b.else_(); |
| constants.instantiateConstant(b, BoolConstant(false), constantType); |
| b.end(); |
| return; |
| } |
| |
| w.Local temp = b.addLocal(from); |
| b.local_set(temp); |
| b.i32_const(info.classId); |
| b.local_get(temp); |
| b.struct_new(info.struct); |
| } else if (from is w.RefType) { |
| // Unboxing |
| ClassInfo info = classInfo[boxedClasses[to]!]!; |
| if (!from.heapType.isSubtypeOf(info.struct)) { |
| // Cast to box type |
| b.ref_cast(info.nonNullableType); |
| } |
| b.struct_get(info.struct, FieldIndex.boxValue); |
| } else { |
| if (options.omitExplicitTypeChecks || options.omitImplicitTypeChecks) { |
| b.unreachable(); |
| } else { |
| throw "Conversion between non-reference types (from $from to $to)"; |
| } |
| } |
| } |
| } |
| |
| Reference getFunctionEntry(Reference target, {required bool uncheckedEntry}) { |
| final Member member = target.asMember; |
| if (member.isAbstract || !member.isInstanceMember) return target; |
| |
| // Getters and tear-offs never have to check any parameters, so we don't |
| // have checked/unchecked entries for them. |
| if (target.isGetter || target.isTearOffReference) return target; |
| |
| // We only generate checked & unchecked entry points if there's any |
| // parameters that may need to be checked. |
| if (needToCheckTypesFor(member)) { |
| return uncheckedEntry |
| ? member.uncheckedEntryReference |
| : member.checkedEntryReference; |
| } |
| |
| return target; |
| } |
| |
| final Map<Member, bool> _needToCheck = {}; |
| bool needToCheckTypesFor(Member member) { |
| if (options.omitImplicitTypeChecks) return false; |
| |
| if (!member.isInstanceMember) return false; |
| if (member is Procedure && member.isGetter) return false; |
| |
| return _needToCheck[member] ??= _needToCheckTypesFor(member); |
| } |
| |
| bool _needToCheckTypesFor(Member member) { |
| // We may have global guarantee that all call sites can use the unchecked |
| // entrypoint. |
| final metadata = procedureAttributeMetadata[member]!; |
| |
| // If there's only uses of the member via `this`, then we know that |
| // covariant parameters will type check correctly, except parameters that |
| // were marked explicitly with the `covariant` keyword. |
| final useUncheckedEntry = |
| !metadata.hasTearOffUses && !metadata.hasNonThisUses; |
| |
| if (member is Field) { |
| return needToCheckImplicitSetterValue( |
| member, |
| uncheckedEntry: useUncheckedEntry, |
| ); |
| } |
| |
| final ( |
| :typeParameters, |
| :typeParametersToTypeCheck, |
| :positional, |
| :positionalToTypeCheck, |
| :named, |
| :namedToTypeCheck, |
| ) = getParametersToCheck( |
| member, |
| ); |
| |
| for (final typeParameter in typeParameters) { |
| if (needToCheckTypeParameter(typeParameter)) return true; |
| } |
| for (final parameter in positional) { |
| if (needToCheckParameter(parameter, uncheckedEntry: useUncheckedEntry)) { |
| return true; |
| } |
| } |
| for (final parameter in named) { |
| if (needToCheckParameter(parameter, uncheckedEntry: useUncheckedEntry)) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| bool needToCheckImplicitSetterValue( |
| Field field, { |
| required bool uncheckedEntry, |
| }) { |
| if (options.omitImplicitTypeChecks) return false; |
| if (field.isCovariantByDeclaration) return true; |
| if (!uncheckedEntry && field.isCovariantByClass) return true; |
| return false; |
| } |
| |
| bool needToCheckTypeParameter(TypeParameter typeParameter) { |
| if (options.omitImplicitTypeChecks) return false; |
| return typeParameter.isCovariantByClass && |
| typeParameter.bound != coreTypes.objectNullableRawType; |
| } |
| |
| bool needToCheckParameter( |
| Variable parameter, { |
| required bool uncheckedEntry, |
| }) { |
| if (options.omitImplicitTypeChecks) return false; |
| if (canSkipImplicitCheck(parameter)) return false; |
| if (parameter.isCovariantByDeclaration) return true; |
| if (!uncheckedEntry && parameter.isCovariantByClass) return true; |
| return false; |
| } |
| |
| ({ |
| List<TypeParameter> typeParameters, |
| List<DartType> typeParametersToTypeCheck, |
| List<PositionalParameter> positional, |
| List<DartType> positionalToTypeCheck, |
| List<NamedParameter> named, |
| List<DartType> namedToTypeCheck, |
| }) |
| getParametersToCheck(Member member) { |
| final memberFunction = member.function!; |
| final List<TypeParameter> typeParameters = member is Constructor |
| ? member.enclosingClass.typeParameters |
| : member.function!.typeParameters; |
| final List<PositionalParameter> positional = |
| memberFunction.positionalParameters; |
| final List<NamedParameter> named = memberFunction.namedParameters; |
| |
| // If this is a CFE-inserted `forwarding-stub` then the types we have to |
| // check against are those from the forwarding target. |
| // |
| // This mirrors what the VM does in |
| // - FlowGraphBuilder::BuildTypeArgumentTypeChecks |
| // - FlowGraphBuilder::BuildArgumentTypeChecks |
| Member? procedureForwardingTarget; |
| if (member is Procedure && member.isForwardingStub) { |
| final forwardingTarget = member.concreteForwardingStubTarget; |
| if (forwardingTarget is Field) { |
| assert( |
| typeParameters.isEmpty && named.isEmpty && positional.length == 1, |
| ); |
| return ( |
| typeParameters: [], |
| typeParametersToTypeCheck: [], |
| positional: positional, |
| positionalToTypeCheck: [forwardingTarget.type], |
| named: named, |
| namedToTypeCheck: [], |
| ); |
| } |
| procedureForwardingTarget = forwardingTarget as Procedure; |
| } |
| return ( |
| typeParameters: typeParameters, |
| typeParametersToTypeCheck: _typesFromTypeParameterBounds( |
| procedureForwardingTarget?.function?.typeParameters ?? typeParameters, |
| ), |
| positional: positional, |
| positionalToTypeCheck: _typesFromPositionalParameters( |
| procedureForwardingTarget?.function?.positionalParameters ?? positional, |
| ), |
| named: named, |
| namedToTypeCheck: _typeFromNamedParameters( |
| named, |
| procedureForwardingTarget?.function?.namedParameters ?? named, |
| ), |
| ); |
| } |
| |
| List<DartType> _typesFromTypeParameterBounds( |
| List<TypeParameter> typeParameters, |
| ) { |
| if (typeParameters.isEmpty) return const []; |
| return [for (final param in typeParameters) param.bound]; |
| } |
| |
| List<DartType> _typesFromPositionalParameters(List<Variable> typeParameters) { |
| if (typeParameters.isEmpty) return const []; |
| return [for (final param in typeParameters) param.type]; |
| } |
| |
| List<DartType> _typeFromNamedParameters( |
| List<NamedParameter> namedOrder, |
| List<NamedParameter> namedType, |
| ) { |
| if (namedOrder.isEmpty) return const []; |
| final namedTypes = <DartType>[]; |
| for (int i = 0; i < namedOrder.length; ++i) { |
| final named = namedOrder[i]; |
| DartType? type; |
| |
| for (int j = 0; j < namedType.length; ++j) { |
| final other = namedType[j]; |
| if (named.parameterName == other.parameterName) { |
| type = other.type; |
| break; |
| } |
| } |
| namedTypes.add(type!); |
| } |
| return namedTypes; |
| } |
| |
| AstCallTarget directCallTarget(Reference target) { |
| final signature = signatureForDirectCall(target); |
| return AstCallTarget(signature, this, target); |
| } |
| |
| w.FunctionType signatureForDirectCall(Reference target) { |
| if (target.asMember.isInstanceMember && !target.isBodyReference) { |
| final selector = dispatchTable.selectorForTarget(target); |
| if (selector.containsTarget(target)) { |
| return selector.signature; |
| } |
| } |
| return functions.getFunctionType(target); |
| } |
| |
| bool synthesizeNullReturnValue(Reference target) { |
| final member = target.asMember; |
| if (member.isInstanceMember) { |
| final table = dispatchTable; |
| final selector = table.selectorForTarget(target); |
| if (selector.containsTarget(target)) { |
| return selector.synthesizeNullReturnValue; |
| } |
| } |
| return functions.synthesizeNullReturnValue(target); |
| } |
| |
| bool synthesizeNoReturn(Reference target) { |
| final member = target.asMember; |
| if (member.isInstanceMember) { |
| final table = dispatchTable; |
| final selector = table.selectorForTarget(target); |
| if (selector.containsTarget(target)) { |
| return selector.synthesizeNoReturn; |
| } |
| } |
| return functions.synthesizeNoReturn(target); |
| } |
| |
| ParameterInfo paramInfoForDirectCall(Reference target) { |
| if (target.asMember.isInstanceMember) { |
| final selector = dispatchTable.selectorForTarget(target); |
| if (selector.containsTarget(target)) { |
| return selector.paramInfo; |
| } |
| } |
| return staticParamInfo.putIfAbsent( |
| target, |
| () => ParameterInfo.fromMember(target, target.asMember.isAbstract), |
| ); |
| } |
| |
| w.ValueType preciseThisFor(Member member, {bool nullable = false}) { |
| assert(member.isInstanceMember || member is Constructor); |
| |
| Class cls = member.enclosingClass!; |
| final w.StorageType? builtin = builtinTypes[cls]; |
| final boxClass = boxedClasses[builtin]; |
| if (boxClass != null) { |
| // We represent `this` as an unboxed type. |
| if (!nullable) return builtin as w.ValueType; |
| // Otherwise we use [boxClass] to represent `this`. |
| cls = boxClass; |
| } |
| return classInfo[cls]!.repr.withNullability(nullable); |
| } |
| |
| /// Get the Wasm table declared by [field], or `null` if [field] is not a |
| /// declaration of a Wasm table. |
| /// |
| /// This function participates in tree shaking in the sense that if it's |
| /// never called for a particular table declaration, that table is not added |
| /// to the output module. |
| w.Table? getTable(w.ModuleBuilder module, Field field) { |
| DartType fieldType = field.type; |
| if (fieldType is! InterfaceType || fieldType.classNode != wasmTableClass) { |
| return null; |
| } |
| final mainTable = _declaredFieldTables.putIfAbsent(field, () { |
| w.RefType elementType = |
| translateType(fieldType.typeArguments.single) as w.RefType; |
| Expression sizeExp = (field.initializer as ConstructorInvocation) |
| .arguments |
| .positional |
| .single; |
| if (sizeExp is StaticGet && sizeExp.target is Field) { |
| sizeExp = (sizeExp.target as Field).initializer!; |
| } |
| int size = sizeExp is ConstantExpression |
| ? (sizeExp.constant as IntConstant).value |
| : (sizeExp as IntLiteral).value; |
| return mainModule.tables.define(elementType, size); |
| }); |
| |
| return _importedFieldTables.get(mainTable, module); |
| } |
| |
| Member? singleTarget(TreeNode node) { |
| return directCallMetadata[node]?.targetMember; |
| } |
| |
| /// Direct call information of a [FunctionInvocation] based on TFA's direct |
| /// call metadata. |
| SingleClosureTarget? singleClosureTarget( |
| FunctionInvocation node, |
| ClosureRepresentation representation, |
| StaticTypeContext typeContext, |
| ) { |
| final (Member, int)? directClosureCall = |
| directCallMetadata[node]?.targetClosure; |
| |
| if (directClosureCall == null) { |
| return null; |
| } |
| |
| // To avoid using the `Null` class, avoid devirtualizing to `Null` members. |
| // `noSuchMethod` is also not allowed as `Null` inherits it. |
| if (directClosureCall.$1.enclosingClass == coreTypes.deprecatedNullClass || |
| directClosureCall.$1 == objectNoSuchMethod) { |
| return null; |
| } |
| |
| final member = directClosureCall.$1; |
| final closureId = directClosureCall.$2; |
| |
| if (closureId == 0) { |
| // The member is called as a closure (tear-off). We'll generate a direct |
| // call to the member. |
| final lambdaDartType = member.function!.computeFunctionType( |
| Nullability.nonNullable, |
| ); |
| |
| // Check that type of the receiver is a subtype of |
| if (!typeEnvironment.isSubtypeOf( |
| lambdaDartType, |
| node.receiver.getStaticType(typeContext), |
| )) { |
| return null; |
| } |
| |
| final entryReference = getFunctionEntry( |
| member.reference, |
| uncheckedEntry: false, |
| ); |
| |
| return SingleClosureTarget._( |
| directCallTarget(entryReference), |
| paramInfoForDirectCall(entryReference), |
| ); |
| } |
| |
| // A closure in the member is called. |
| final Closures enclosingMemberClosures = getClosures( |
| member, |
| findCaptures: true, |
| ); |
| final Lambda lambda = enclosingMemberClosures.lambdas.values.firstWhere( |
| (lambda) => lambda.index == closureId - 1, |
| ); |
| final FunctionType lambdaDartType = lambda.functionNode.computeFunctionType( |
| Nullability.nonNullable, |
| ); |
| |
| if (!typeEnvironment.isSubtypeOf( |
| lambdaDartType, |
| node.receiver.getStaticType(typeContext), |
| )) { |
| return null; |
| } |
| |
| return SingleClosureTarget._( |
| lambda.callTarget, |
| ParameterInfo.fromLocalFunction(lambda.functionNode), |
| ); |
| } |
| |
| bool canSkipImplicitCheck(Variable node) { |
| return inferredArgTypeMetadata[node]?.skipCheck ?? false; |
| } |
| |
| bool canUseUncheckedEntry(Expression receiver, Expression node) { |
| if (receiver is ThisExpression) return true; |
| if (node is InstanceInvocation && node.isInvariant) return true; |
| return inferredTypeMetadata[node]?.skipCheck ?? false; |
| } |
| |
| DartType typeOfParameterVariable( |
| Variable node, |
| bool isRequired, { |
| bool isNoSuchMethodForwarder = false, |
| }) { |
| // We have a guarantee that inferred types are correct. |
| final inferredType = _inferredTypeOfParameterVariable(node); |
| if (inferredType != null) { |
| return isRequired |
| ? inferredType |
| : inferredType.withDeclaredNullability(Nullability.nullable); |
| } |
| |
| final isCovariant = |
| node.isCovariantByDeclaration || node.isCovariantByClass; |
| if (isCovariant) { |
| // If [node] is a parameter of a `operator==` method, then the argument to |
| // it cannot be nullable. |
| final member = node.parent!.parent; |
| if (member is Procedure && member.name == equalsName) { |
| return coreTypes.objectNonNullableRawType; |
| } |
| // The type argument of a static type is not required to conform |
| // to the bounds of the type variable. Thus, any object can be |
| // passed to a parameter that is covariant by class. |
| return coreTypes.objectNullableRawType; |
| } |
| |
| // Special case for NSM forwarders: A method with a non-nullable optional |
| // parameter requires an implementation to (normally) have a default value, |
| // in case a call site didn't pass the optional parameter. |
| // |
| // Though an implementation may be done via defining a `noSuchMethod` |
| // method, which will make CFE auto generate all unimplemented methods via |
| // no-such-method forwarder functions. Those may not have a sensible |
| // default. To make this still work we use `null` as default which will then |
| // be in the `Invocation` object passed to the `noSuchMethod` call. |
| // |
| // See |
| // * https://github.com/dart-lang/language/issues/3331 |
| // * https://github.com/dart-lang/sdk/issues/63958 |
| return (!isRequired && isNoSuchMethodForwarder) |
| ? node.type.withDeclaredNullability(Nullability.nullable) |
| : node.type; |
| } |
| |
| // The type to use assuming the argument was already checked (in case a |
| // covariant check is needed). |
| DartType typeOfCheckedParameterVariable(Variable node) { |
| // We have a guarantee that inferred types are correct. |
| final inferredType = _inferredTypeOfParameterVariable(node); |
| if (inferredType != null) { |
| return inferredType; |
| } |
| return node.type; |
| } |
| |
| DartType typeOfReturnValue(Member member) { |
| if (member is Field) return typeOfField(member); |
| |
| return _inferredTypeOfReturnValue(member) ?? member.function!.returnType; |
| } |
| |
| DartType typeOfField(Field node) { |
| assert(!node.isLate); |
| return _inferredTypeOfField(node) ?? node.type; |
| } |
| |
| w.ValueType translateTypeOfParameter( |
| Variable node, |
| bool isRequired, |
| Member member, |
| ) { |
| return translateType( |
| typeOfParameterVariable( |
| node, |
| isRequired, |
| isNoSuchMethodForwarder: |
| member is Procedure && member.isNoSuchMethodForwarder, |
| ), |
| ); |
| } |
| |
| w.ValueType translateTypeOfField(Field node) { |
| return translateType(typeOfField(node)); |
| } |
| |
| w.ValueType translateTypeOfLocalVariable(Variable node) { |
| final dartType = _inferredTypeOfLocalVariable(node) ?? node.type; |
| final wasmType = translateType(dartType); |
| if (wasmType case w.RefType(nullable: false, heapType: final heapType)) { |
| if (isCyclicHeapType(heapType)) { |
| // Cyclic types can't be instantiated, so locals with cyclic types won't |
| // be assigned and we can give them a more general type. Returning a |
| // nullable type here makes dummy initialization of the variable |
| // shorter, with just a `ref.null`. |
| return topType; |
| } |
| } |
| return wasmType; |
| } |
| |
| DartType? _inferredTypeOfParameterVariable(Variable node) { |
| return _filterInferredType(node.type, inferredArgTypeMetadata[node]); |
| } |
| |
| DartType? _inferredTypeOfReturnValue(Member node) { |
| return _filterInferredType( |
| node.function!.returnType, |
| inferredReturnTypeMetadata[node], |
| ); |
| } |
| |
| DartType? _inferredTypeOfField(Field node) { |
| return _filterInferredType(node.type, inferredTypeMetadata[node]); |
| } |
| |
| DartType? _inferredTypeOfLocalVariable(Variable variable) { |
| InferredType? inferredType = inferredTypeMetadata[variable]; |
| if (variable.isFinal) { |
| if (variable.parent is Let && variable.type is VoidType) { |
| // This is most likely a CFE desugaring construct of index setters where |
| // the result of the `[]=` call is stored in a let variable of void type |
| // and never used. Since `[]=` calls don't have an actual return value we |
| // use top type for the (unused) variable for which we can synthesize a |
| // `null`. |
| // |
| // See also http://dartbug.com/63360 |
| return null; |
| } |
| inferredType ??= inferredTypeMetadata[variable.initializer]; |
| } |
| return _filterInferredType(variable.type, inferredType); |
| } |
| |
| DartType? _filterInferredType( |
| DartType defaultType, |
| InferredType? inferredType, |
| ) { |
| if (inferredType == null) return null; |
| |
| if (defaultType is VoidType) { |
| defaultType = coreTypes.objectNullableRawType; |
| } |
| |
| // To check whether [inferredType] is more precise than [defaultType] we |
| // require it (for now) to be an interface type. |
| if (defaultType is! InterfaceType) return null; |
| |
| final concreteClass = inferredType.concreteClass; |
| if (concreteClass == null) return null; |
| // TFA doesn't know how dart2wasm represents closures |
| if (concreteClass == closureClass) return null; |
| // The WasmFunction<>/WasmArray<>/WasmTable<> types need concrete type |
| // arguments. |
| if (concreteClass == wasmFunctionClass) return null; |
| if (concreteClass == wasmArrayClass) return null; |
| if (concreteClass == wasmTableClass) return null; |
| |
| // If the TFA inferred class is the same as the [defaultType] we prefer the |
| // latter as it has the correct type arguments. |
| if (concreteClass == defaultType.classNode) return null; |
| |
| // Sometimes we get inferred types that violate soundness (and would result |
| // in a runtime error, e.g. in a dynamic invocation forwarder passing an |
| // object of incorrect type to a target). |
| if (!hierarchy.isSubInterfaceOf(concreteClass, defaultType.classNode)) { |
| return null; |
| } |
| |
| if (concreteClass == coreTypes.deprecatedNullClass) return const NullType(); |
| |
| final typeParameters = concreteClass.typeParameters; |
| final typeArguments = typeParameters.isEmpty |
| ? const <DartType>[] |
| : List<DartType>.filled(typeParameters.length, const DynamicType()); |
| final nullability = inferredType.nullable |
| ? Nullability.nullable |
| : Nullability.nonNullable; |
| return InterfaceType(concreteClass, nullability, typeArguments); |
| } |
| |
| InliningDecision shouldInline(Reference target, w.FunctionType signature) { |
| if (!options.inlining) return InliningDecision(false, 'inlining disabled'); |
| |
| // Unchecked entry point functions perform very little, mainly optional |
| // parameter handling and then call the real body function. |
| // |
| // By inlining them we can often avoid downcasts and sometimes boxing. The |
| // force inlining here seem to even lead to overall size decreases. |
| if (target.isUncheckedEntryReference) { |
| return InliningDecision(true, 'unchecked entry'); |
| } |
| |
| final member = target.asMember; |
| if (member.isExternal) return InliningDecision(false, 'external'); |
| if (util.getWasmNeverInlinePragma(coreTypes, member) ?? false) { |
| return InliningDecision(false, '@pragma("wasm:never-inline")'); |
| } |
| if (util.getWasmPreferInlinePragma(coreTypes, member) ?? false) { |
| return InliningDecision(true, '@pragma("wasm:prefer-inline")'); |
| } |
| if (member is Field) { |
| return _shouldInlineFieldAccessor(target, member); |
| } |
| if (member is Constructor) { |
| return _shouldInlineConstructorCall(target, signature, member); |
| } |
| return _shouldInlineProcedureCall(target, signature, member as Procedure); |
| } |
| |
| InliningDecision _shouldInlineFieldAccessor(Reference target, Field field) { |
| if (field.isInstanceMember) { |
| // Implicit instance getters are just loads. |
| if (target.isImplicitGetter) { |
| return InliningDecision(true, 'Implicit getter.'); |
| } |
| |
| // Implicit instance setters are just stores, except if the value needs |
| // to be type checked. |
| assert(target.isImplicitSetter); |
| if (target == field.checkedEntryReference) { |
| return InliningDecision(false, 'Implicit setter with type check.'); |
| } |
| return InliningDecision(true, 'Implicit setter without type check.'); |
| } |
| |
| // Implicit setter for static fields are just stores. |
| if (target == field.setterReference) { |
| return InliningDecision(true, 'Implicit static setter'); |
| } |
| |
| if (target == field.getterReference) { |
| // Implicit getter for static fields may invoke lazy static initializer. |
| if (dartGlobals.getConstantInitializer(field) != null) { |
| // This global will get it's initializer eagerly set, so no lazy init |
| // function to be called. |
| return InliningDecision( |
| true, |
| 'Implicit static getter without initializer', |
| ); |
| } |
| return InliningDecision(false, 'static getter with initializer'); |
| } |
| throw UnimplementedError(); |
| } |
| |
| /// Whether the initializer function should never be inlined (neither by |
| /// dart2wasm, nor by binaryen or wasm runtime). |
| /// |
| /// Static field initializers are exectued at most once, so if they are big, |
| /// we want to prevent them from ever being inlined. For small ones, we leave |
| /// it up to normal inlining heuristics (which may decide it's beneficial to |
| /// inline e.g. due to size). |
| /// |
| /// If we didn't do this and a static field is only accessed at one place, |
| /// then binaryen would inline it always (due to only one caller), which would |
| /// make the callee possibly very large, which in return may prevent that one |
| /// from getting inlined into other functions (by binaryen & wasm runtime) |
| bool neverInlineStaticFieldInitializer(Field field) { |
| if (dartGlobals.getConstantInitializer(field) != null) { |
| // The initializer is a constant. |
| return false; |
| } |
| final nodeCounter = NodeCounter(this); |
| field.initializer!.accept(nodeCounter); |
| // The cost of an initializer function is the wasm function type, wasm |
| // function body and calls to it. |
| return nodeCounter.count > 10; |
| } |
| |
| InliningDecision _shouldInlineConstructorCall( |
| Reference target, |
| w.FunctionType signature, |
| Constructor constructor, |
| ) { |
| final callOverhead = signature.inputs.length + /* call instruction = */ 1; |
| if (target.isInitializerReference) { |
| return InliningDecision(true, 'Initializer'); |
| } |
| if (target.isConstructorBodyReference) { |
| final nodeCounter = NodeCounter(this); |
| for (final init in constructor.initializers) { |
| // The body will have to call the super body with evaluated arguments |
| // supplied as to the body function. |
| if (init is SuperInitializer) { |
| nodeCounter.count += getConstructorInfo( |
| init.target, |
| ).bodyParameters.length; |
| break; |
| } |
| if (init is RedirectingInitializer) { |
| nodeCounter.count += getConstructorInfo( |
| init.target, |
| ).bodyParameters.length; |
| break; |
| } |
| } |
| // If we think the overhead of pushing arguments is around the same as the |
| // body itself, we always inline. |
| constructor.function.body?.accept(nodeCounter); |
| return InliningDecision( |
| nodeCounter.count < callOverhead, |
| 'SizeEstimate=${nodeCounter.count} < CallOverhead=$callOverhead', |
| ); |
| } |
| |
| // The size of the constructor allocator is always guaranteed to be |
| // larger than the caller as it comes with this base cost: |
| // |
| // i32.const <classid> |
| // i32.const 0 |
| // <N fields> |
| // struct.new |
| assert(constructor.reference == target); |
| return InliningDecision(false, 'Constructor allocator'); |
| } |
| |
| InliningDecision _shouldInlineProcedureCall( |
| Reference target, |
| w.FunctionType signature, |
| Procedure member, |
| ) { |
| final callOverhead = signature.inputs.length + /* call instruction = */ 1; |
| final function = member.function; |
| if (function.returnType is NeverType) { |
| // Procedure always throws. |
| return InliningDecision(false, 'Throwing function'); |
| } |
| |
| if (target.isUncheckedEntryReference) { |
| // Unchecked entry point functions perform very little, mainly optional |
| // parameter handling and then call the real body function. |
| // |
| // By inlining them we can often avoid downcasts and sometimes boxing. The |
| // force inlining here seem to even lead to overall size decreases. |
| return InliningDecision(true, 'Unchecked entry'); |
| } |
| |
| if (target.isCheckedEntryReference) { |
| // Checked entry point functions have to perform extra type checks on |
| // parameters. |
| return InliningDecision(false, 'Checked entry'); |
| } |
| if (target.isTearOffReference) { |
| // This has to perform closure allocation. |
| return InliningDecision(false, 'TearOff'); |
| } |
| |
| assert(target.isBodyReference || target == member.reference); |
| |
| final nodeCounter = NodeCounter(this); |
| function.body?.accept(nodeCounter); |
| int nodeCount = nodeCounter.count; |
| |
| // Special cases for iterator inlining: |
| // class ... implements Iterable<T> { |
| // Iterator<T> get iterator => FooIterator(...) |
| // } |
| final klass = member.enclosingClass; |
| if (klass != null) { |
| final name = member.name.text; |
| if (name == 'iterator') { |
| if (typeEnvironment.isSubtypeOf( |
| klass.getThisType(coreTypes, Nullability.nonNullable), |
| coreTypes.iterableRawType(Nullability.nonNullable), |
| )) { |
| nodeCount--; // Give slightly more budget. |
| } |
| } |
| } |
| |
| // If we think the overhead of pushing arguments is around the same as the |
| // body itself, we always inline. |
| if (nodeCount <= callOverhead) { |
| return InliningDecision( |
| true, |
| 'SizeEstimate=$nodeCount <= CallOverhead=$callOverhead', |
| ); |
| } |
| |
| return InliningDecision( |
| nodeCount <= options.inliningLimit, |
| '$nodeCount <= inliningLimit=${options.inliningLimit}', |
| ); |
| } |
| |
| bool supportsInlining(Reference target) { |
| final Member member = target.asMember; |
| if (membersContainingInnerFunctions.contains(member)) return false; |
| if (membersBeingGenerated.contains(member)) { |
| // Guard against recursive inlining. |
| // |
| // Though we allow inlining calls to constructor initializer & body |
| // functions while generating the constructor. |
| // |
| // We also allow inlining calls to the member body functions as any |
| // recursive inlining would call to checked or unchecked entry which would |
| // disallow it. |
| if (!target.isInitializerReference && |
| !target.isConstructorBodyReference && |
| !target.isBodyReference) { |
| return false; |
| } |
| } |
| if (member is Field) return true; |
| if (member.function!.asyncMarker != AsyncMarker.Sync) return false; |
| return true; |
| } |
| |
| T? getPragma<T>(Annotatable node, String name, [T? defaultValue]) { |
| return util.getPragma(coreTypes, node, name, defaultValue: defaultValue); |
| } |
| |
| w.ValueType makeArray( |
| w.InstructionsBuilder b, |
| w.ArrayType arrayType, |
| int length, |
| void Function(w.ValueType, int) generateItem, |
| ) { |
| final w.ValueType elementType = arrayType.elementType.type.unpacked; |
| final arrayTypeRef = w.RefType.def(arrayType, nullable: false); |
| |
| if (length > maxArrayNewFixedLength) { |
| assert(arrayType.elementType.mutable); |
| // Too long for `array.new_fixed`. Set elements individually. |
| b.i32_const(length); |
| b.array_new_default(arrayType); |
| if (length > 0) { |
| final w.Local arrayLocal = b.addLocal(arrayTypeRef); |
| b.local_set(arrayLocal); |
| for (int i = 0; i < length; i++) { |
| b.local_get(arrayLocal); |
| b.i32_const(i); |
| generateItem(elementType, i); |
| b.array_set(arrayType); |
| } |
| b.local_get(arrayLocal); |
| } |
| } else { |
| for (int i = 0; i < length; i++) { |
| generateItem(elementType, i); |
| } |
| b.array_new_fixed(arrayType, length); |
| } |
| return arrayTypeRef; |
| } |
| |
| /// Indexes a Dart `WasmListBase` on the stack. |
| void indexList( |
| w.InstructionsBuilder b, |
| void Function(w.InstructionsBuilder b) pushIndex, |
| ) { |
| getListBaseArray(b); |
| pushIndex(b); |
| b.array_get(nullableObjectArrayType); |
| } |
| |
| /// Pushes a Dart `List`'s length onto the stack as `i32`. |
| void getListLength(w.InstructionsBuilder b) { |
| ClassInfo info = classInfo[listBaseClass]!; |
| b.struct_get(info.struct, FieldIndex.listLength); |
| b.i32_wrap_i64(); |
| } |
| |
| /// Get the `WasmListBase._data` field of type `WasmArray<Object?>`. |
| void getListBaseArray(w.InstructionsBuilder b) { |
| ClassInfo info = classInfo[listBaseClass]!; |
| b.struct_get(info.struct, FieldIndex.listArray); |
| } |
| |
| ClassInfo getRecordClassInfo(RecordType recordType) => |
| classInfo[recordClasses[RecordShape.fromType(recordType)]!]!; |
| |
| w.Global getInternalizedStringGlobal(w.ModuleBuilder module, String s) { |
| assert(!options.standalone, "Standalone mode doesn't have string globals"); |
| |
| w.Global? internalizedString = _internalizedStringGlobals[(module, s)]; |
| if (internalizedString != null) { |
| return internalizedString; |
| } |
| |
| bool hasUnpairedSurrogate(String str) { |
| for (int i = 0; i < str.length; i++) { |
| int codeUnit = str.codeUnitAt(i); |
| if (codeUnit >= 0xD800 && codeUnit <= 0xDBFF) { |
| if (i + 1 >= str.length || |
| str.codeUnitAt(i + 1) < 0xDC00 || |
| str.codeUnitAt(i + 1) > 0xDFFF) { |
| return true; |
| } else { |
| i++; |
| } |
| } else if (codeUnit >= 0xDC00 && codeUnit <= 0xDFFF) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| if (hasUnpairedSurrogate(s)) { |
| // Unpaired surrogates can't be encoded as UTF-8, import them from JS |
| // runtime. |
| final i = internalizedStringsForJSRuntime.length; |
| internalizedString = module.globals.import( |
| 's', |
| '$i', |
| w.GlobalType(w.RefType.extern(nullable: false), mutable: false), |
| ); |
| internalizedStringsForJSRuntime.add(s); |
| } else { |
| internalizedString = module.globals.import( |
| '', |
| s, |
| w.GlobalType(w.RefType.extern(nullable: false), mutable: false), |
| ); |
| } |
| _internalizedStringGlobals[(module, s)] = internalizedString; |
| return internalizedString; |
| } |
| |
| void pushStandaloneStringConstant( |
| w.InstructionsBuilder instructions, |
| w.DataSegmentBuilder data, |
| String s, |
| ) { |
| assert(options.standalone); |
| |
| Uint8List byteContents; |
| bool isAscii; |
| if (s.codeUnits.every((c) => c <= 127)) { |
| byteContents = Uint8List.fromList(s.codeUnits); |
| isAscii = true; |
| } else { |
| final list = ByteData(s.length * 2); |
| for (var i = 0; i < s.length; i++) { |
| list.setUint16(2 * i, s.codeUnitAt(i), .little); |
| } |
| byteContents = list.buffer.asUint8List(); |
| isAscii = false; |
| } |
| |
| final kernelFunction = isAscii |
| ? embedderStringFromAsciiBytes |
| : embedderStringFromCharCodeArray; |
| final importedFunction = |
| functions.getFunction(kernelFunction.reference) as w.ImportedFunction; |
| // The signature is (WasmArray<i8 | i16> data, i32 offset, i32 length) |
| final arrayRefType = |
| (importedFunction.type.inputs[0] as w.RefType).heapType as w.ArrayType; |
| |
| instructions |
| ..i32_const(data.length) |
| ..i32_const(s.length) |
| ..array_new_data(arrayRefType, data) |
| ..i32_const(0) |
| ..i32_const(s.length) |
| ..call(importedFunction); |
| |
| data.content.add(byteContents); |
| } |
| |
| w.Memory findMemory( |
| Procedure topLevelExternalMemoryGetter, |
| w.ModuleBuilder moduleBuilder, |
| ) { |
| final inMain = _findMemoryForMainModule(topLevelExternalMemoryGetter); |
| if (moduleBuilder == mainModule) { |
| return inMain; |
| } |
| |
| return _importedMemories.get(inMain, moduleBuilder); |
| } |
| |
| w.Memory _findMemoryForMainModule(Procedure topLevelExternalMemoryGetter) { |
| return _memories.putIfAbsent(topLevelExternalMemoryGetter, () { |
| final limits = MemoryLimits.readAnnotation( |
| this, |
| topLevelExternalMemoryGetter, |
| )!; |
| final exportName = interopMemberNamer.getExportName( |
| topLevelExternalMemoryGetter, |
| ); |
| final import = util.getWasmImportPragma( |
| coreTypes, |
| topLevelExternalMemoryGetter, |
| ); |
| |
| w.Memory memory; |
| if (import != null) { |
| memory = mainModule.memories.import( |
| import.moduleName, |
| import.itemName, |
| false, |
| limits.minSize, |
| limits.maxSize, |
| ); |
| } else { |
| memory = mainModule.memories.define( |
| false, |
| limits.minSize, |
| limits.maxSize, |
| ); |
| } |
| |
| if (exportName != null) { |
| mainModule.exports.export(exportName, memory); |
| } |
| return memory; |
| }); |
| } |
| |
| void instantiateDummyValueHeapType( |
| w.InstructionsBuilder b, |
| w.HeapType type, |
| String name, |
| void Function(w.InstructionsBuilder b, w.HeapType heapType) |
| instantiateHeapType, |
| ) { |
| if (type == w.HeapType.struct) { |
| final structType = typesBuilder.defineStruct( |
| name, |
| brand: options.uniqueTypes, |
| ); |
| b.struct_new(structType); |
| return; |
| } else if (type is w.DefType) { |
| if (type is w.StructType) { |
| for (w.FieldType field in type.fields) { |
| instantiateDummyValue(b, field.type.unpacked, instantiateHeapType); |
| } |
| b.struct_new(type); |
| return; |
| } else if (type is w.ArrayType) { |
| b.array_new_fixed(type, 0); |
| return; |
| } else if (type is w.FunctionType) { |
| b.ref_func( |
| getDummyValuesCollectorForModule( |
| b.moduleBuilder, |
| ).getDummyFunction(type), |
| ); |
| return; |
| } |
| } |
| } |
| |
| /// Whether we can have an instance of [klass] or any of its transitive |
| /// subclasses. |
| bool isAllocatable(Class klass) => |
| !isCyclicHeapType(classInfo[klass]!.repr.heapType); |
| |
| bool isCyclicHeapType(w.HeapType type) { |
| final alreadyCalculated = _isCyclicHeapType[type]; |
| if (alreadyCalculated != null) return alreadyCalculated; |
| |
| _isCyclicHeapType[type] = true; |
| if (type is w.StructType) { |
| for (final field in type.fields) { |
| if (field.type case w.RefType( |
| nullable: false, |
| heapType: final fieldHeapType, |
| )) { |
| if (isCyclicHeapType(fieldHeapType)) { |
| return _isCyclicHeapType[type] = true; |
| } |
| } |
| } |
| } |
| return _isCyclicHeapType[type] = false; |
| } |
| } |
| |
| class CompilationQueue { |
| final Translator translator; |
| final List<CompilationTask> _pending = []; |
| |
| CompilationQueue(this.translator); |
| |
| bool get isEmpty => _pending.isEmpty; |
| void add(CompilationTask entry) { |
| _pending.add(entry); |
| } |
| |
| CompilationTask pop() => _pending.removeLast(); |
| } |
| |
| class CompilationTask { |
| final w.FunctionBuilder function; |
| final CodeGenerator _codeGenerator; |
| |
| CompilationTask(this.function, this._codeGenerator); |
| |
| void run(Translator translator, bool printKernel, bool printWasm) { |
| if (printWasm) { |
| print("#${function.name} (synthetic)"); |
| print(function.type); |
| } |
| _codeGenerator.generate(function.body, function.locals.toList(), null); |
| if (printWasm) { |
| print(function.body.trace); |
| } |
| } |
| } |
| |
| // Compilation task for AST. |
| class AstCompilationTask extends CompilationTask { |
| final Reference reference; |
| |
| AstCompilationTask(super.function, super._createCodeGenerator, this.reference) |
| : super(); |
| |
| @override |
| void run(Translator translator, bool printKernel, bool printWasm) { |
| final member = reference.asMember; |
| |
| if (printKernel || printWasm) { |
| final (:name, :exportName) = _getNames(translator); |
| |
| String header = "#${function.name}: $name"; |
| if (exportName != null) { |
| header = "$header (exported as $exportName)"; |
| } |
| print(header); |
| print(function.type); |
| print( |
| member.function |
| ?.computeFunctionType(Nullability.nonNullable) |
| .toStringInternal(), |
| ); |
| } |
| if (printKernel) { |
| if (member is Constructor) { |
| Class cls = member.enclosingClass; |
| for (Field field in cls.fields) { |
| if (field.isInstanceMember && field.initializer != null) { |
| print("${field.name}: ${field.initializer}"); |
| } |
| } |
| for (Initializer initializer in member.initializers) { |
| print(initializer); |
| } |
| } |
| Statement? body = member.function?.body; |
| if (body != null) { |
| print(body); |
| } |
| if (!printWasm) print(""); |
| } |
| |
| _codeGenerator.generate(function.body, function.locals.toList(), null); |
| if (printWasm) { |
| print(function.body.trace); |
| } |
| } |
| |
| ({String name, String? exportName}) _getNames(Translator translator) { |
| final member = reference.asMember; |
| String canonicalName = "$member"; |
| if (reference.isSetter) { |
| canonicalName = "$canonicalName="; |
| } else if (reference.isGetter || reference.isTearOffReference) { |
| int dot = canonicalName.indexOf('.'); |
| canonicalName = |
| '${canonicalName.substring(0, dot + 1)}=${canonicalName.substring(dot + 1)}'; |
| } |
| canonicalName = |
| member.enclosingLibrary == |
| translator.component.mainMethod!.enclosingLibrary |
| ? canonicalName |
| : "${member.enclosingLibrary.importUri} $canonicalName"; |
| |
| return ( |
| name: canonicalName, |
| exportName: translator.functions.getExportName(reference), |
| ); |
| } |
| } |
| |
| class _ClosureTrampolineGenerator implements CodeGenerator { |
| final Translator translator; |
| final w.FunctionBuilder trampoline; |
| final CallTarget target; |
| final int typeCount; |
| final int posArgCount; |
| final List<String> argNames; |
| final ParameterInfo paramInfo; |
| |
| _ClosureTrampolineGenerator( |
| this.translator, |
| this.trampoline, |
| this.target, |
| this.typeCount, |
| this.posArgCount, |
| this.argNames, |
| this.paramInfo, |
| ); |
| |
| @override |
| void generate( |
| w.InstructionsBuilder b, |
| List<w.Local> paramLocals, |
| w.Label? returnLabel, |
| ) { |
| assert(returnLabel == null); |
| |
| int targetIndex = 0; |
| if (paramInfo.takesContextOrReceiver) { |
| w.Local receiver = trampoline.locals[0]; |
| b.local_get(receiver); |
| translator.convertType( |
| b, |
| receiver.type, |
| target.signature.inputs[targetIndex++], |
| ); |
| } |
| int argIndex = 1; |
| for (int i = 0; i < typeCount; i++) { |
| b.local_get(trampoline.locals[argIndex++]); |
| targetIndex++; |
| } |
| for (int i = 0; i < paramInfo.positional.length; i++) { |
| if (i < posArgCount) { |
| w.Local arg = trampoline.locals[argIndex++]; |
| b.local_get(arg); |
| translator.convertType( |
| b, |
| arg.type, |
| target.signature.inputs[targetIndex++], |
| ); |
| } else { |
| translator.constants.instantiateConstant( |
| b, |
| paramInfo.positional[i]!, |
| target.signature.inputs[targetIndex++], |
| ); |
| } |
| } |
| int argNameIndex = 0; |
| for (int i = 0; i < paramInfo.names.length; i++) { |
| String argName = paramInfo.names[i]; |
| if (argNameIndex < argNames.length && argNames[argNameIndex] == argName) { |
| w.Local arg = trampoline.locals[argIndex++]; |
| b.local_get(arg); |
| translator.convertType( |
| b, |
| arg.type, |
| target.signature.inputs[targetIndex++], |
| ); |
| argNameIndex++; |
| } else { |
| translator.constants.instantiateConstant( |
| b, |
| paramInfo.named[argName]!, |
| target.signature.inputs[targetIndex++], |
| ); |
| } |
| } |
| assert(argIndex == trampoline.type.inputs.length); |
| assert(targetIndex == target.signature.inputs.length); |
| assert(argNameIndex == argNames.length); |
| |
| final outputs = translator.callTarget(target, b); |
| translator.convertType( |
| b, |
| translator.outputOrVoid(outputs), |
| translator.outputOrVoid(trampoline.type.outputs), |
| ); |
| b.end(); |
| } |
| } |
| |
| /// Similar to [_ClosureTrampolineGenerator], but generates dynamic call |
| /// entries. |
| class _ClosureDynamicEntryGenerator implements CodeGenerator { |
| final Translator translator; |
| final FunctionNode functionNode; |
| final CallTarget target; |
| final ParameterInfo paramInfo; |
| final String name; |
| final w.FunctionBuilder function; |
| |
| _ClosureDynamicEntryGenerator( |
| this.translator, |
| this.functionNode, |
| this.target, |
| this.paramInfo, |
| this.name, |
| this.function, |
| ); |
| |
| @override |
| void generate( |
| w.InstructionsBuilder b, |
| List<w.Local> paramLocals, |
| w.Label? returnLabel, |
| ) { |
| assert(returnLabel == null); |
| |
| final b = function.body; |
| |
| final member = functionNode.parent; |
| final int typeCount = member is Constructor |
| ? member.enclosingClass.typeParameters.length |
| : functionNode.typeParameters.length; |
| |
| final closureLocal = function.locals[0]; |
| final typeArgsListLocal = function.locals[1]; |
| final posArgsListLocal = function.locals[2]; |
| final namedArgsListLocal = function.locals[3]; |
| |
| final positionalRequired = paramInfo.positional |
| .where((arg) => arg == null) |
| .length; |
| final positionalTotal = paramInfo.positional.length; |
| |
| // At this point the shape and type checks passed. We have right number |
| // of type arguments in the list, but optional positional and named |
| // parameters may be missing. |
| |
| final targetInputs = target.signature.inputs; |
| int inputIdx = 0; |
| |
| // Push context or receiver |
| if (paramInfo.takesContextOrReceiver) { |
| final closureBaseType = w.RefType.def( |
| translator.closureLayouter.closureBaseStruct, |
| nullable: false, |
| ); |
| |
| // Get context, downcast it to expected type |
| b.local_get(closureLocal); |
| translator.convertType(b, closureLocal.type, closureBaseType); |
| b.struct_get( |
| translator.closureLayouter.closureBaseStruct, |
| FieldIndex.closureContext, |
| ); |
| translator.convertType( |
| b, |
| closureContextFieldType, |
| targetInputs[inputIdx], |
| ); |
| inputIdx += 1; |
| } |
| |
| // Push type arguments |
| for (int typeIdx = 0; typeIdx < typeCount; typeIdx += 1) { |
| b.local_get(typeArgsListLocal); |
| b.i32_const(typeIdx); |
| b.array_get(translator.typeArrayType); |
| translator.convertType(b, translator.topType, targetInputs[inputIdx]); |
| inputIdx += 1; |
| } |
| |
| // Push positional arguments |
| for (int posIdx = 0; posIdx < positionalTotal; posIdx += 1) { |
| if (posIdx < positionalRequired) { |
| // Shape check passed, argument must be passed |
| b.local_get(posArgsListLocal); |
| b.i32_const(posIdx); |
| b.array_get(translator.nullableObjectArrayType); |
| } else { |
| // Argument may be missing |
| b.i32_const(posIdx); |
| b.local_get(posArgsListLocal); |
| b.array_len(); |
| b.i32_lt_u(); |
| b.if_([], [translator.topType]); |
| b.local_get(posArgsListLocal); |
| b.i32_const(posIdx); |
| b.array_get(translator.nullableObjectArrayType); |
| b.else_(); |
| translator.constants.instantiateConstant( |
| b, |
| paramInfo.positional[posIdx]!, |
| translator.topType, |
| ); |
| b.end(); |
| } |
| translator.convertType(b, translator.topType, targetInputs[inputIdx]); |
| inputIdx += 1; |
| } |
| |
| // Push named arguments |
| |
| Expression? initializerForNamedParamInMember(String paramName) { |
| for (int i = 0; i < functionNode.namedParameters.length; i += 1) { |
| if (functionNode.namedParameters[i].parameterName == paramName) { |
| return functionNode.namedParameters[i].defaultValue; |
| } |
| } |
| return null; |
| } |
| |
| final namedArgValueIndexLocal = b.addLocal( |
| translator.classInfo[translator.boxedIntClass]!.nullableType, |
| ); |
| |
| for (String paramName in paramInfo.names) { |
| final Constant? paramInfoDefaultValue = paramInfo.named[paramName]; |
| final Expression? functionNodeDefaultValue = |
| initializerForNamedParamInMember(paramName); |
| |
| // Get passed value |
| b.local_get(namedArgsListLocal); |
| translator.constants.instantiateConstant( |
| b, |
| translator.symbols.symbolForNamedParameter(paramName), |
| translator.classInfo[translator.symbolClass]!.nonNullableType, |
| ); |
| translator.callReference(translator.getNamedParameterIndex.reference, b); |
| b.local_set(namedArgValueIndexLocal); |
| |
| if (functionNodeDefaultValue == null && paramInfoDefaultValue == null) { |
| // Shape check passed, parameter must be passed |
| b.local_get(namedArgsListLocal); |
| b.local_get(namedArgValueIndexLocal); |
| translator.convertType(b, namedArgValueIndexLocal.type, w.NumType.i64); |
| b.i32_wrap_i64(); |
| b.array_get(translator.nullableObjectArrayType); |
| translator.convertType( |
| b, |
| translator.nullableObjectArrayType.elementType.type.unpacked, |
| target.signature.inputs[inputIdx], |
| ); |
| } else { |
| // Parameter may not be passed. |
| b.local_get(namedArgValueIndexLocal); |
| b.ref_is_null(); |
| b.if_([], [translator.topType]); |
| if (functionNodeDefaultValue != null) { |
| // Used by the member, has a default value |
| translator.constants.instantiateConstant( |
| b, |
| (functionNodeDefaultValue as ConstantExpression).constant, |
| translator.topType, |
| ); |
| } else { |
| // Not used by the member |
| translator.constants.instantiateConstant( |
| b, |
| paramInfoDefaultValue!, |
| translator.topType, |
| ); |
| } |
| b.else_(); // value index not null |
| b.local_get(namedArgsListLocal); |
| b.local_get(namedArgValueIndexLocal); |
| translator.convertType(b, namedArgValueIndexLocal.type, w.NumType.i64); |
| b.i32_wrap_i64(); |
| b.array_get(translator.nullableObjectArrayType); |
| b.end(); |
| translator.convertType(b, translator.topType, targetInputs[inputIdx]); |
| } |
| inputIdx += 1; |
| } |
| |
| final outputs = translator.callTarget(target, b); |
| translator.convertType( |
| b, |
| translator.outputOrVoid(outputs), |
| translator.outputOrVoid(function.type.outputs), |
| ); |
| |
| b.end(); // end function |
| } |
| } |
| |
| class _ClosureArgumentsToVtableEntryDispatcherGenerator |
| implements CodeGenerator { |
| final Translator translator; |
| final ClosureRepresentation representation; |
| final w.FunctionBuilder function; |
| |
| _ClosureArgumentsToVtableEntryDispatcherGenerator( |
| this.translator, |
| this.representation, |
| this.function, |
| ); |
| |
| @override |
| void generate( |
| w.InstructionsBuilder b, |
| List<w.Local> paramLocals, |
| w.Label? returnLabel, |
| ) { |
| assert(returnLabel == null); |
| |
| final b = function.body; |
| |
| final closureLocal = function.locals[0]; |
| final typeArgsLocal = function.locals[1]; |
| final posArgsLocal = function.locals[2]; |
| final namedArgsLocal = function.locals[3]; |
| |
| assert(typeArgsLocal.type == translator.typeArrayTypeRef); |
| assert(posArgsLocal.type == translator.nullableObjectArrayTypeRef); |
| assert(namedArgsLocal.type == translator.nullableObjectArrayTypeRef); |
| |
| _verifyAssumptions( |
| b, |
| closureLocal, |
| typeArgsLocal, |
| posArgsLocal, |
| namedArgsLocal, |
| ); |
| |
| final vtableStruct = representation.vtableStruct; |
| |
| // Downcast closure to this representation's closure type & get |
| // representation-specific vtable. |
| b.comment('Obtaining representation-specific vtable'); |
| b.local_get(closureLocal); |
| b.ref_cast(w.RefType(representation.closureStruct, nullable: false)); |
| b.struct_get(representation.closureStruct, FieldIndex.closureVtable); |
| final vtableVar = b.addLocal(w.RefType(vtableStruct, nullable: false)); |
| b.local_set(vtableVar); |
| |
| final typeStack = <w.ValueType>[]; |
| |
| // Load closure context. |
| b.comment('Loading closure.context'); |
| b.local_get(closureLocal); |
| b.struct_get(translator.closureInfo.struct, FieldIndex.closureContext); |
| typeStack.add(w.RefType.struct(nullable: false)); |
| |
| // Load required type arguments. |
| for (int i = 0; i < representation.typeCount; ++i) { |
| b.comment('Loading type argument $i'); |
| b.local_get(typeArgsLocal); |
| b.i32_const(i); |
| b.array_get(translator.typeArrayType); |
| typeStack.add(translator.translateType(translator.typeType)); |
| } |
| |
| // Load optional parameters. |
| if (representation.hasNamed) { |
| b.comment('Handle optional named parameters'); |
| _handleOptionalNamedCase( |
| b, |
| closureLocal, |
| typeArgsLocal, |
| posArgsLocal, |
| namedArgsLocal, |
| vtableVar, |
| vtableStruct, |
| typeStack, |
| ); |
| } else { |
| b.comment('Handle optional positional parameters'); |
| _handleOptionalPositionalCase( |
| b, |
| closureLocal, |
| typeArgsLocal, |
| posArgsLocal, |
| namedArgsLocal, |
| vtableVar, |
| vtableStruct, |
| typeStack, |
| ); |
| } |
| |
| b.end(); // end function |
| } |
| |
| void _handleOptionalPositionalCase( |
| w.InstructionsBuilder b, |
| w.Local closureLocal, |
| w.Local typeArgsLocal, |
| w.Local posArgsLocal, |
| w.Local namedArgsLocal, |
| w.Local vtableVar, |
| w.StructType vtableStruct, |
| List<w.ValueType> typeStack, |
| ) { |
| // Possibly variable number of positionals. |
| for (int i = 0; i <= representation.maxPositionalCount; ++i) { |
| b.comment('Check whether all positionals are loaded'); |
| b.local_get(posArgsLocal); |
| b.array_len(); |
| b.i32_const(i); |
| b.i32_eq(); |
| b.if_(typeStack, typeStack); |
| b.comment('All positionals loaded, calling corresponding vtable entry'); |
| b.local_get(vtableVar); |
| final index = representation.vtableBaseIndex + i; |
| b.struct_get(vtableStruct, index); |
| b.call_ref( |
| (vtableStruct.fields[index].type.unpacked as w.RefType).heapType |
| as w.FunctionType, |
| ); |
| b.return_(); |
| b.end(); |
| |
| if (i <= representation.maxPositionalCount) { |
| // Otherwise load more arguments. |
| b.comment('Loading positional $i (optional)'); |
| b.local_get(posArgsLocal); |
| b.i32_const(i); |
| b.array_get(translator.nullableObjectArrayType); |
| typeStack.add(translator.topType); |
| } |
| } |
| |
| b.unreachable(); |
| } |
| |
| void _handleOptionalNamedCase( |
| w.InstructionsBuilder b, |
| w.Local closureLocal, |
| w.Local typeArgsLocal, |
| w.Local posArgsLocal, |
| w.Local namedArgsLocal, |
| w.Local vtableVar, |
| w.StructType vtableStruct, |
| List<w.ValueType> typeStack, |
| ) { |
| // All positionals are required, so load them. |
| for (int i = 0; i < representation.maxPositionalCount; ++i) { |
| b.comment('Loading positional $i (required)'); |
| b.local_get(posArgsLocal); |
| b.i32_const(i); |
| b.array_get(translator.nullableObjectArrayType); |
| typeStack.add(translator.topType); |
| } |
| |
| // Check for each name whether it's there or not. |
| final allCombinations = representation.nameCombinations.toList(); |
| final sortedNames = |
| allCombinations.expand((nc) => nc.names).toSet().toList()..sort(); |
| final nameIndexVar = b.addLocal(w.NumType.i32); |
| |
| int matchingCombinations(List<String> currentNames, int nextNameIndex) { |
| int prefixMatches = 0; |
| bool exactMatch = false; |
| if (nextNameIndex == 0) { |
| assert(currentNames.isEmpty); |
| exactMatch = true; |
| prefixMatches = 1 + allCombinations.length; |
| } else { |
| for (final nc in allCombinations) { |
| if (currentNames.length <= nc.names.length) { |
| bool found = true; |
| for (int i = 0; i < currentNames.length; ++i) { |
| if (currentNames[i] != nc.names[i]) { |
| found = false; |
| break; |
| } |
| } |
| if (found) { |
| if (currentNames.length == nc.names.length) { |
| prefixMatches++; |
| exactMatch = true; |
| } else { |
| if (sortedNames[nextNameIndex - 1].compareTo( |
| nc.names[currentNames.length], |
| ) < |
| 0) { |
| prefixMatches++; |
| } |
| } |
| } |
| } |
| } |
| } |
| return exactMatch ? prefixMatches : -prefixMatches; |
| } |
| |
| final currentNames = <String>[]; |
| |
| void generateNameHandling(int nextNameIndex) { |
| final match = matchingCombinations(currentNames, nextNameIndex); |
| final hasExactMatch = match > 0; |
| final hasNonExactMatches = match < 0 || match > 1; |
| final hasMoreMatches = match != 0; |
| if (hasExactMatch) { |
| b.comment('Check whether all named are loaded'); |
| b.local_get(namedArgsLocal); |
| b.array_len(); |
| b.local_get(nameIndexVar); |
| b.i32_eq(); |
| b.if_(typeStack, typeStack); |
| b.comment('All named loaded, calling corresponding vtable entry'); |
| b.comment('(passed named arguments: ${currentNames.join('-')})'); |
| final index = representation.fieldIndexForSignature( |
| representation.maxPositionalCount, |
| currentNames, |
| ); |
| b.local_get(vtableVar); |
| b.struct_get(vtableStruct, index); |
| b.call_ref( |
| (vtableStruct.fields[index].type.unpacked as w.RefType).heapType |
| as w.FunctionType, |
| ); |
| b.return_(); |
| b.end(); |
| if (!hasNonExactMatches) { |
| b.comment('More names passed than expected.'); |
| b.unreachable(); |
| return; |
| } |
| } else if (hasMoreMatches) { |
| if (util.compilerAssertsEnabled) { |
| b.comment('Check there are more names passed by the caller,'); |
| b.comment('because the currently processed name set'); |
| b.comment('(which are: ${currentNames.join('-')}) does not'); |
| b.comment(' correspond to a valid name combination.'); |
| b.local_get(namedArgsLocal); |
| b.array_len(); |
| b.local_get(nameIndexVar); |
| b.i32_eq(); |
| b.if_(); |
| b.comment('Unsupported name combination.'); |
| b.comment('May be bug in closure representation building'); |
| b.unreachable(); |
| b.end(); |
| } |
| } else { |
| b.comment( |
| 'The names "${currentNames.join('-')}" are not part ' |
| 'of a used name combination.', |
| ); |
| b.unreachable(); |
| return; |
| } |
| |
| final newName = sortedNames[nextNameIndex]; |
| final symbol = translator.symbols.symbolForNamedParameter(newName); |
| |
| b.comment('Load next name and see if it corresponds to "$newName"'); |
| b.local_get(namedArgsLocal); |
| b.local_get(nameIndexVar); |
| b.array_get(translator.nullableObjectArrayType); |
| translator.constants.instantiateConstant(b, symbol, translator.topType); |
| b.ref_eq(); |
| |
| b.if_(typeStack, typeStack); |
| { |
| b.comment('Name "$newName" was provided by caller. Loading its value.'); |
| b.local_get(namedArgsLocal); |
| b.local_get(nameIndexVar); |
| b.i32_const(1); |
| b.i32_add(); |
| b.array_get(translator.nullableObjectArrayType); |
| |
| b.comment('Increment index in named argument array.'); |
| b.local_get(nameIndexVar); |
| b.i32_const(2); |
| b.i32_add(); |
| b.local_set(nameIndexVar); |
| |
| currentNames.add(newName); |
| typeStack.add(translator.topType); |
| generateNameHandling(nextNameIndex + 1); |
| typeStack.removeLast(); |
| currentNames.removeLast(); |
| } |
| b.end(); |
| |
| b.comment('Name "$newName" was *not* provided by caller.'); |
| generateNameHandling(nextNameIndex + 1); |
| } |
| |
| generateNameHandling(0); |
| } |
| |
| // This function is purely used for checking assumptions made by the code this |
| // generator is producing. |
| // |
| // Namely, we assume that the caller has |
| // * populated default type arguments (if needed) |
| // * checked the shape of arguments & closure matches |
| // * performed necessary type checks on arguments. |
| void _verifyAssumptions( |
| w.InstructionsBuilder b, |
| w.Local closureLocal, |
| w.Local typeArgsLocal, |
| w.Local posArgsLocal, |
| w.Local namedArgsLocal, |
| ) { |
| if (!util.compilerAssertsEnabled) { |
| return; |
| } |
| b.comment('Verify assumptions of arguments and closure'); |
| final functionTypeLocal = b.addLocal( |
| translator.closureLayouter.functionTypeType, |
| ); |
| b.local_get(closureLocal); |
| b.struct_get( |
| translator.closureLayouter.closureBaseStruct, |
| FieldIndex.closureRuntimeType, |
| ); |
| b.local_tee(functionTypeLocal); |
| |
| // Ensure type arguments were passed. |
| b.local_get(typeArgsLocal); |
| b.array_len(); |
| b.i32_const(representation.typeCount); |
| b.i32_ne(); |
| b.if_(); |
| b.unreachable(); |
| b.end(); |
| |
| // Ensure closure shape is correct. |
| b.local_get(typeArgsLocal); |
| b.local_get(posArgsLocal); |
| b.local_get(namedArgsLocal); |
| translator.callReference(translator.checkClosureShape.reference, b); |
| b.i32_eqz(); |
| b.if_(); |
| b.unreachable(); |
| b.end(); |
| |
| // Ensure types are correct. |
| if (!translator.options.omitImplicitTypeChecks) { |
| b.local_get(functionTypeLocal); |
| b.local_get(typeArgsLocal); |
| b.local_get(posArgsLocal); |
| b.local_get(namedArgsLocal); |
| translator.callReference(translator.checkClosureType.reference, b); |
| b.drop(); |
| } |
| } |
| } |
| |
| class NodeCounter extends VisitorDefault<void> with VisitorVoidMixin { |
| final Translator translator; |
| |
| NodeCounter(this.translator); |
| |
| bool hadReturn = false; |
| int count = 0; |
| |
| // We only count tree nodes and do not recurse into things that aren't part of |
| // the tree (e.g. constants, variable types, ...) |
| |
| @override |
| void defaultTreeNode(TreeNode node) { |
| count++; |
| node.visitChildren(this); |
| } |
| |
| // Constructor initializers |
| @override |
| void visitFieldInitializer(FieldInitializer node) { |
| handleFieldInitializerValue(node.value); |
| } |
| |
| @override |
| void visitLocalInitializer(LocalInitializer node) { |
| node.variable.initializer!.accept(this); |
| } |
| |
| @override |
| void visitSuperInitializer(SuperInitializer node) { |
| node.arguments.accept(this); |
| } |
| |
| @override |
| void visitRedirectingInitializer(RedirectingInitializer node) { |
| node.arguments.accept(this); |
| } |
| |
| void handleFieldInitializerValue(Expression? value) { |
| // These compress very well, let's not count those field initializer |
| // expressions for the size of the initializer function. |
| if (value == null || value is NullLiteral || value is NullConstant) return; |
| if (value is BoolLiteral || value is BoolConstant) return; |
| value.accept(this); |
| } |
| |
| // The following AST nodes do not actually emit any code, so we don't count |
| // those nodes but we recurse into children that do emit code and therefore |
| // should count. |
| |
| @override |
| void visitBlock(Block node) { |
| node.visitChildren(this); |
| } |
| |
| @override |
| void visitEmptyStatement(EmptyStatement node) { |
| node.visitChildren(this); |
| } |
| |
| @override |
| void visitReturnStatement(ReturnStatement node) { |
| node.expression?.accept(this); |
| if (!hadReturn) { |
| // The first return is free. |
| hadReturn = true; |
| return; |
| } |
| count++; |
| } |
| |
| @override |
| void visitLabeledStatement(LabeledStatement node) { |
| node.visitChildren(this); |
| } |
| |
| @override |
| void visitBlockExpression(BlockExpression node) { |
| node.visitChildren(this); |
| } |
| |
| @override |
| void visitExpressionStatement(ExpressionStatement node) { |
| node.visitChildren(this); |
| } |
| |
| @override |
| void visitLet(Let node) { |
| node.visitChildren(this); |
| } |
| |
| @override |
| void visitArguments(Arguments node) { |
| count += node.types.length; |
| node.visitChildren(this); |
| } |
| |
| @override |
| void visitNamedExpression(NamedExpression node) { |
| node.visitChildren(this); |
| } |
| |
| @override |
| void visitIsExpression(IsExpression node) { |
| node.operand.accept(this); |
| count += 2; |
| } |
| |
| @override |
| void visitAsExpression(AsExpression node) { |
| node.operand.accept(this); |
| count += 3; |
| } |
| |
| @override |
| void defaultDartType(DartType node) { |
| // The only [DartType]s we care about are those passed in calls and they are |
| // handled already in [visitArguments]. |
| return; |
| } |
| |
| // Some nodes are more costly. |
| @override |
| void visitInstanceGet(InstanceGet node) { |
| node.visitChildren(this); |
| _countInstanceCallCost(node); |
| } |
| |
| @override |
| void visitInstanceSet(InstanceSet node) { |
| node.visitChildren(this); |
| _countInstanceCallCost(node); |
| } |
| |
| @override |
| void visitInstanceInvocation(InstanceInvocation node) { |
| node.visitChildren(this); |
| _countInstanceCallCost(node); |
| } |
| |
| @override |
| void visitEqualsCall(EqualsCall node) { |
| node.visitChildren(this); |
| _countInstanceCallCost(node); |
| } |
| |
| void _countInstanceCallCost(TreeNode node) { |
| count++; // Call cost. |
| |
| // Indirect calls are more costly. |
| if (translator.singleTarget(node) == null) { |
| count += 2; // Additional cost for indirect calls. |
| } |
| } |
| } |
| |
| class InliningDecision { |
| final bool shouldInline; |
| final String? reason; |
| InliningDecision(this.shouldInline, this.reason); |
| } |
| |
| /// Creates forwarders for generic functions where the caller passes a constant |
| /// type argument. |
| /// |
| /// Let's say we have |
| /// |
| /// foo<T>(args) => ...; |
| /// |
| /// and 3 call sites |
| /// |
| /// foo<int>(args) |
| /// foo<int>(args) |
| /// foo<double>(args) |
| /// |
| /// the callsites can instead call a forwarder |
| /// |
| /// fooInt(args) |
| /// fooInt(args) |
| /// fooDouble(args) |
| /// |
| /// fooInt(args) => foo<int>(args) |
| /// fooDouble(args) => foo<double>(args) |
| /// |
| /// This saves code size on the call site. |
| class PartialInstantiator { |
| final Translator translator; |
| final w.ModuleBuilder callingModule; |
| |
| final Map<(Reference, DartType), w.BaseFunction> _oneTypeArgument = {}; |
| final Map<(Reference, DartType, DartType), w.BaseFunction> _twoTypeArguments = |
| {}; |
| |
| PartialInstantiator(this.translator, this.callingModule); |
| |
| w.BaseFunction getOneTypeArgumentForwarder( |
| Reference target, |
| DartType type, |
| String name, |
| ) { |
| assert(translator.types.isTypeConstant(type)); |
| |
| return _oneTypeArgument.putIfAbsent((target, type), () { |
| final wasmTarget = translator.functions.getFunction(target); |
| |
| final function = callingModule.functions.define( |
| translator.typesBuilder.defineFunction([ |
| ...wasmTarget.type.inputs.skip(1), |
| ], wasmTarget.type.outputs), |
| name, |
| ); |
| final b = function.body; |
| translator.constants.instantiateConstant( |
| b, |
| TypeLiteralConstant(type), |
| translator.types.nonNullableTypeType, |
| ); |
| for (int i = 1; i < wasmTarget.type.inputs.length; ++i) { |
| b.local_get(b.locals[i - 1]); |
| } |
| translator.callFunction(wasmTarget, b); |
| b.return_(); |
| b.end(); |
| |
| return function; |
| }); |
| } |
| |
| w.BaseFunction getTwoTypeArgumentForwarder( |
| Reference target, |
| DartType type1, |
| DartType type2, |
| String name, |
| ) { |
| assert(translator.types.isTypeConstant(type1)); |
| assert(translator.types.isTypeConstant(type2)); |
| |
| return _twoTypeArguments.putIfAbsent((target, type1, type2), () { |
| final wasmTarget = translator.functions.getFunction(target); |
| |
| final function = callingModule.functions.define( |
| translator.typesBuilder.defineFunction([ |
| ...wasmTarget.type.inputs.skip(2), |
| ], wasmTarget.type.outputs), |
| name, |
| ); |
| final b = function.body; |
| translator.constants.instantiateConstant( |
| b, |
| TypeLiteralConstant(type1), |
| translator.types.nonNullableTypeType, |
| ); |
| translator.constants.instantiateConstant( |
| b, |
| TypeLiteralConstant(type2), |
| translator.types.nonNullableTypeType, |
| ); |
| for (int i = 2; i < wasmTarget.type.inputs.length; ++i) { |
| b.local_get(b.locals[i - 2]); |
| } |
| translator.callFunction(wasmTarget, b); |
| b.return_(); |
| b.end(); |
| |
| return function; |
| }); |
| } |
| } |
| |
| class PolymorphicDispatchers { |
| final Translator translator; |
| final w.ModuleBuilder callingModule; |
| final cache = <SelectorInfo, PolymorphicDispatcherCallTarget>{}; |
| final uncheckedCache = <SelectorInfo, PolymorphicDispatcherCallTarget>{}; |
| |
| PolymorphicDispatchers(this.translator, this.callingModule); |
| |
| CallTarget getPolymorphicDispatcher( |
| SelectorInfo selector, { |
| required bool useUncheckedEntry, |
| }) { |
| assert( |
| selector.targets(unchecked: useUncheckedEntry).allTargetRanges.length > 1, |
| ); |
| return (useUncheckedEntry && selector.useMultipleEntryPoints |
| ? uncheckedCache |
| : cache) |
| .putIfAbsent(selector, () { |
| return PolymorphicDispatcherCallTarget( |
| translator, |
| selector, |
| callingModule, |
| useUncheckedEntry, |
| ); |
| }); |
| } |
| } |
| |
| class PolymorphicDispatcherCallTarget extends CallTarget { |
| final Translator translator; |
| final SelectorInfo selector; |
| final w.ModuleBuilder callingModule; |
| final bool useUncheckedEntry; |
| |
| PolymorphicDispatcherCallTarget( |
| this.translator, |
| this.selector, |
| this.callingModule, |
| this.useUncheckedEntry, |
| ) : super( |
| translator.typesBuilder.defineFunction([ |
| w.NumType.i32, |
| ...selector.signature.inputs, |
| ], selector.signature.outputs), |
| ); |
| |
| @override |
| String get name => '${selector.name} (polymorphic dispatcher)'; |
| |
| @override |
| bool get supportsInlining => true; |
| |
| @override |
| InliningDecision get shouldInline => InliningDecision( |
| selector |
| .targets(unchecked: useUncheckedEntry) |
| .staticDispatchRanges |
| .length <= |
| 1, |
| 'staticDispatchRanges <= 1', |
| ); |
| |
| @override |
| CodeGenerator get inliningCodeGen => PolymorphicDispatcherCodeGenerator( |
| translator, |
| selector, |
| useUncheckedEntry, |
| ); |
| |
| @override |
| late final w.BaseFunction function = (() { |
| final function = callingModule.functions.define(signature, name); |
| translator.compilationQueue.add(CompilationTask(function, inliningCodeGen)); |
| return function; |
| })(); |
| } |
| |
| class PolymorphicDispatcherCodeGenerator implements CodeGenerator { |
| final Translator translator; |
| final SelectorInfo selector; |
| final bool useUncheckedEntry; |
| |
| PolymorphicDispatcherCodeGenerator( |
| this.translator, |
| this.selector, |
| this.useUncheckedEntry, |
| ); |
| |
| @override |
| void generate( |
| w.InstructionsBuilder b, |
| List<w.Local> paramLocals, |
| w.Label? returnLabel, |
| ) { |
| final signature = selector.signature; |
| |
| final targets = selector.targets(unchecked: useUncheckedEntry); |
| |
| final targetRanges = targets.staticDispatchRanges |
| .map((entry) => (range: entry.range, value: entry.target)) |
| .toList(); |
| |
| final bool needFallback = |
| targets.allTargetRanges.length > targets.staticDispatchRanges.length; |
| |
| // First parameter to the dispatcher is the class id. |
| const int classIdParameterOffset = 1; |
| |
| void emitDirectCall(Reference target) { |
| for (int i = 0; i < signature.inputs.length; ++i) { |
| b.local_get(paramLocals[classIdParameterOffset + i]); |
| } |
| translator.callReference(target, b); |
| } |
| |
| void emitDispatchTableCall() { |
| for (int i = 0; i < signature.inputs.length; ++i) { |
| b.local_get(paramLocals[classIdParameterOffset + i]); |
| } |
| b.local_get(paramLocals[1]); |
| translator.callDispatchTable( |
| b, |
| selector, |
| useUncheckedEntry: useUncheckedEntry, |
| ); |
| } |
| |
| b.local_get(paramLocals[0]); |
| b.classIdSearch( |
| targetRanges, |
| signature.outputs, |
| emitDirectCall, |
| needFallback ? emitDispatchTableCall : null, |
| ); |
| |
| if (returnLabel != null) { |
| b.br(returnLabel); |
| } else { |
| b.return_(); |
| } |
| b.end(); |
| } |
| } |
| |
| class DummyValuesCollector { |
| final w.ModuleBuilder module; |
| final Translator translator; |
| |
| final Map<w.FunctionType, w.BaseFunction> _dummyFunctions = {}; |
| final Map<w.HeapType, w.Global> _dummyValues = {}; |
| |
| /// A global with type `ref struct`, initialized as an empty struct. |
| /// |
| /// This can be used as the dummy value for contexts. |
| late final w.Global dummyStructGlobal; |
| |
| DummyValuesCollector(this.translator, this.module); |
| |
| void instantiateLocalDummyValue(w.InstructionsBuilder b, w.ValueType type) { |
| void initializeHeapType(ib, heapType) { |
| final moduleBuilder = b.moduleBuilder; |
| final global = _dummyValues.putIfAbsent(heapType, () { |
| final global = moduleBuilder.globals.define( |
| w.GlobalType(w.RefType(heapType, nullable: false), mutable: false), |
| ); |
| final init = global.initializer; |
| translator.instantiateDummyValueHeapType( |
| init, |
| heapType, |
| "dummy $heapType", |
| initializeHeapType, |
| ); |
| init.end(); |
| return global; |
| }); |
| ib.global_get(global); |
| } |
| |
| instantiateDummyValue(b, type, initializeHeapType); |
| } |
| |
| /// Provide a dummy function with the given signature. Used for empty entries |
| /// in vtables and for dummy values of function reference type. |
| w.BaseFunction getDummyFunction(w.FunctionType type) { |
| return _dummyFunctions.putIfAbsent(type, () { |
| final function = module.functions.define(type, "#dummy function $type"); |
| final b = function.body; |
| b.unreachable(); |
| b.end(); |
| return function; |
| }); |
| } |
| |
| /// Returns whether the given function was provided by [getDummyFunction]. |
| bool isDummyFunction(w.BaseFunction function) { |
| return _dummyFunctions[function.type] == function; |
| } |
| } |
| |
| void instantiateDummyValue( |
| w.InstructionsBuilder b, |
| w.ValueType type, |
| void Function(w.InstructionsBuilder b, w.HeapType type) instantiateHeapType, |
| ) { |
| switch (type) { |
| case w.NumType.i32: |
| b.i32_const(0); |
| break; |
| case w.NumType.i64: |
| b.i64_const(0); |
| break; |
| case w.NumType.f32: |
| b.f32_const(0); |
| break; |
| case w.NumType.f64: |
| b.f64_const(0); |
| break; |
| default: |
| if (type is w.RefType) { |
| w.HeapType heapType = type.heapType; |
| if (type.nullable) { |
| b.ref_null(heapType.bottomType); |
| } else { |
| instantiateHeapType(b, heapType); |
| } |
| } else { |
| throw "Unsupported global type $type ($type)"; |
| } |
| } |
| } |
| |
| /// Manages wasm entities that are shared between internal Dart wasm modules. |
| /// |
| /// Deferred loading depends on sharing wasm entities between modules. This |
| /// class manages the naming and import/export of those entities. |
| /// |
| /// As these entities are internal to the Dart wasm modules, we can minify the |
| /// names used to refer to them. |
| abstract class _WasmImporter<T extends w.Exportable> { |
| final Translator _translator; |
| final String _exportPrefix; |
| final Map<T, Map<w.ModuleBuilder, T>> _map = {}; |
| |
| _WasmImporter(this._translator, this._exportPrefix); |
| |
| T import( |
| w.ModuleBuilder importingModule, |
| T definition, |
| String moduleName, |
| String importName, |
| ); |
| |
| Iterable<T> get imports => _map.values.expand((v) => v.values); |
| |
| T get(T key, w.ModuleBuilder module) { |
| final keyModuleBuilder = _translator.moduleToBuilder[key.enclosingModule]!; |
| if (keyModuleBuilder == module) return key; |
| |
| final innerMap = _map.putIfAbsent(key, () { |
| final name = _translator.exportNamer.getName( |
| '$_exportPrefix${_map.length}', |
| ); |
| keyModuleBuilder.exports.export(name, key); |
| return {}; |
| }); |
| return innerMap.putIfAbsent(module, () { |
| return import( |
| module, |
| key, |
| _translator.nameForModule(keyModuleBuilder), |
| key.exportedName, |
| ); |
| }); |
| } |
| |
| bool has(T key) { |
| return _map.containsKey(key); |
| } |
| } |
| |
| class WasmFunctionImporter extends _WasmImporter<w.BaseFunction> { |
| WasmFunctionImporter(super._translator, super._exportPrefix); |
| |
| @override |
| w.BaseFunction import( |
| w.ModuleBuilder importingModule, |
| w.BaseFunction definition, |
| String moduleName, |
| String importName, |
| ) { |
| final function = importingModule.functions.import( |
| moduleName, |
| importName, |
| definition.type, |
| definition.name, |
| ); |
| function.functionName = definition.functionName; |
| return function; |
| } |
| } |
| |
| class WasmGlobalImporter extends _WasmImporter<w.Global> { |
| WasmGlobalImporter(super._translator, super._exportPrefix); |
| |
| @override |
| w.Global import( |
| w.ModuleBuilder importingModule, |
| w.Global definition, |
| String moduleName, |
| String importName, |
| ) { |
| final global = importingModule.globals.import( |
| moduleName, |
| importName, |
| definition.type, |
| ); |
| global.globalName = definition.globalName; |
| return global; |
| } |
| } |
| |
| class WasmMemoryImporter extends _WasmImporter<w.Memory> { |
| WasmMemoryImporter(super._translator, super._exportPrefix); |
| |
| @override |
| w.Memory import( |
| w.ModuleBuilder importingModule, |
| w.Memory definition, |
| String moduleName, |
| String importName, |
| ) { |
| return importingModule.memories.import( |
| moduleName, |
| importName, |
| definition.shared, |
| definition.minSize, |
| definition.maxSize, |
| ); |
| } |
| } |
| |
| class WasmTableImporter extends _WasmImporter<w.Table> { |
| WasmTableImporter(super._translator, super._exportPrefix); |
| |
| @override |
| w.Table import( |
| w.ModuleBuilder importingModule, |
| w.Table definition, |
| String moduleName, |
| String importName, |
| ) { |
| return importingModule.tables.import( |
| moduleName, |
| importName, |
| definition.type, |
| definition.minSize, |
| definition.maxSize, |
| ); |
| } |
| } |
| |
| class WasmTagImporter extends _WasmImporter<w.Tag> { |
| WasmTagImporter(super._translator, super._exportPrefix); |
| |
| @override |
| w.Tag import( |
| w.ModuleBuilder importingModule, |
| w.Tag definition, |
| String moduleName, |
| String importName, |
| ) { |
| return importingModule.tags.import(moduleName, importName, definition.type); |
| } |
| } |
| |
| class SingleClosureTarget { |
| final CallTarget callTarget; |
| |
| /// [ParameterInfo] specifying how to compile arguments to the closure or |
| /// member. |
| final ParameterInfo paramInfo; |
| |
| SingleClosureTarget._(this.callTarget, this.paramInfo); |
| } |
| |
| extension on BytesBuilder { |
| void writeULEB128(int value) { |
| assert(value >= 0); |
| do { |
| int byte = value & 0x7F; |
| value >>>= 7; |
| if (value != 0) byte |= 0x80; |
| addByte(byte); |
| } while (value != 0); |
| } |
| } |