blob: 07d84b5d14919b895eaa938953b889459236842e [file]
// 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);
}
}