Files
sdk/pkg/dart2wasm/lib/functions.dart
T
Nate Biggs 7b28c5a94f [dart2wasm] Update serialization strategy for dynamic modules.
Introduces some more machinery to simplify the serialization pipeline. Now the logic to index kernel nodes is hidden away in the serializer. Now throughout the compiler if entities (i.e. classes, members and references) need to be serialized they can be passed to the serializer directly.

Also adds support for tracking brand type assignments from the main module to the dynamic module. This is necessary to decouple the execution order of both compilations which is a WIP.

Change-Id: I38dd0ed11811ea12a2230959588b261719976592
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/412461
Reviewed-by: Martin Kustermann <kustermann@google.com>
Commit-Queue: Nate Biggs <natebiggs@google.com>
2025-03-07 16:45:35 -08:00

629 lines
22 KiB
Dart

// 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 'package:kernel/ast.dart';
import 'package:wasm_builder/wasm_builder.dart' as w;
import 'class_info.dart';
import 'closures.dart';
import 'code_generator.dart';
import 'dispatch_table.dart';
import 'dynamic_modules.dart';
import 'reference_extensions.dart';
import 'translator.dart';
/// This class is responsible for collecting import and export annotations.
/// It also creates Wasm functions for Dart members and manages the compilation
/// queue used to achieve tree shaking.
class FunctionCollector {
final Translator translator;
// Wasm function for each Dart function
final Map<Reference, w.BaseFunction> _functions = {};
// Wasm function for each function expression and local function.
final Map<Lambda, w.BaseFunction> _lambdas = {};
// Names of exported functions
final Map<Reference, String> _exports = {};
// Selector IDs that are invoked via GDT.
final Set<int> _calledSelectors = {};
final Set<int> _calledUncheckedSelectors = {};
// Class IDs for classes that are allocated somewhere in the program
final Set<int> _allocatedClasses = {};
// For each class ID, which functions should be added to the compilation queue
// if an allocation of that class is encountered
final Map<int, List<Reference>> _pendingAllocation = {};
late final WasmFunctionImporter _importedDynamicModuleFunctions =
WasmFunctionImporter(translator, '#dmf');
FunctionCollector(this.translator);
void _collectImportsAndExports() {
for (Library library in translator.libraries) {
library.procedures.forEach(_importOrExport);
library.fields.forEach(_importOrExport);
for (Class cls in library.classes) {
cls.procedures.forEach(_importOrExport);
}
}
}
void _importOrExport(Member member) {
String? importName =
translator.getPragma(member, "wasm:import", member.name.text);
if (importName != null) {
int dot = importName.indexOf('.');
if (dot != -1) {
assert(!member.isInstanceMember);
String module = importName.substring(0, dot);
String name = importName.substring(dot + 1);
if (member is Procedure) {
w.FunctionType ftype = _makeFunctionType(
translator, member.reference, null,
isImportOrExport: true);
_functions[member.reference] = translator
.moduleForReference(member.reference)
.functions
.import(module, name, ftype, "$importName (import)");
}
}
}
String? exportName =
translator.getPragma(member, "wasm:export", member.name.text);
if (exportName != null) {
if (member is Procedure) {
_makeFunctionType(translator, member.reference, null,
isImportOrExport: true);
}
_exports[member.reference] = exportName;
}
}
/// If the member with the reference [target] is exported, get the export
/// name.
String? getExportName(Reference target) => _exports[target];
w.BaseFunction importFunctionToDynamicModule(w.BaseFunction fun) {
assert(translator.isDynamicModule);
assert(fun.enclosingModule == translator.mainModule);
if (!_importedDynamicModuleFunctions.has(fun)) {
throw StateError('Function not callable from dynamic module: $fun');
}
return _importedDynamicModuleFunctions.get(fun, translator.dynamicModule);
}
void initialize() {
_collectImportsAndExports();
if (translator.dynamicModuleSupportEnabled) {
// We have to mark any class which can be constructed in a dynamic
// module as allocated.
for (final library in translator.libraries) {
for (final cls in library.classes) {
if (!cls.isAbstract &&
cls.constructors.any(
(c) => c.isDynamicModuleCallable(translator.coreTypes))) {
recordClassAllocation(translator.classInfo[cls]!.classId);
}
}
}
}
// Add exports to the module and add exported functions to the
// compilationQueue.
for (var export in _exports.entries) {
Reference target = export.key;
Member node = target.asMember;
if (node is Procedure) {
assert(!node.isInstanceMember);
assert(!node.isGetter);
w.FunctionType ftype =
_makeFunctionType(translator, target, null, isImportOrExport: true);
final module = translator.moduleForReference(target);
w.FunctionBuilder function = module.functions.define(ftype, "$node");
_functions[target] = function;
module.exports.export(export.value, function);
translator.compilationQueue.add(AstCompilationTask(function,
getMemberCodeGenerator(translator, function, target), target));
} else if (node is Field) {
final module = translator.moduleForReference(target);
w.Table? table = translator.getTable(module, node);
if (table != null) {
module.exports.export(export.value, table);
}
}
}
// Value classes are always implicitly allocated.
recordClassAllocation(
translator.classInfo[translator.boxedBoolClass]!.classId);
recordClassAllocation(
translator.classInfo[translator.boxedIntClass]!.classId);
recordClassAllocation(
translator.classInfo[translator.boxedDoubleClass]!.classId);
}
w.BaseFunction? getExistingFunction(Reference target) {
return _functions[target];
}
w.BaseFunction getFunction(Reference target) {
return _functions.putIfAbsent(target, () {
final module = translator.moduleForReference(target);
final function = module.functions.define(
translator.signatureForDirectCall(target), getFunctionName(target));
translator.compilationQueue.add(AstCompilationTask(function,
getMemberCodeGenerator(translator, function, target), target));
// Export the function from the main module if it is callable from dynamic
// modules.
if (translator.dynamicModuleSupportEnabled &&
translator.dynamicModuleInfo!.metadata.callableReferences
.contains(target)) {
_importedDynamicModuleFunctions.get(function, translator.dynamicModule,
exportOnly: true);
}
return function;
});
}
w.BaseFunction getLambdaFunction(
Lambda lambda, Member enclosingMember, Closures enclosingMemberClosures) {
return _lambdas.putIfAbsent(lambda, () {
translator.compilationQueue.add(CompilationTask(
lambda.function,
getLambdaCodeGenerator(
translator, lambda, enclosingMember, enclosingMemberClosures)));
return lambda.function;
});
}
w.FunctionType getFunctionType(Reference target) {
// We first try to get the function type by seeing if we already
// compiled the [target] function.
//
// We do that because [target] may refer to a imported/exported function
// which get their function type translated differently (it would be
// incorrect to use [_getFunctionType]).
final existingFunction = getExistingFunction(target);
if (existingFunction != null) return existingFunction.type;
return _getFunctionType(target);
}
w.FunctionType _getFunctionType(Reference target) {
final Member member = target.asMember;
if (target.isBodyReference) {
// This is the function body that is always called directly (never via
// dispatch table) and with checked arguments. That means we can make a
// precise function type signature based on that member's argument types.
return makeFunctionTypeForBody(translator, member);
}
if (target.isTypeCheckerReference) {
if (member is Field || (member is Procedure && member.isSetter)) {
return translator.dynamicSetForwarderFunctionType;
} else {
return translator.dynamicInvocationForwarderFunctionType;
}
}
if (target.isTearOffReference) {
assert(!translator.dispatchTable
.selectorForTarget(target)
.containsTarget(target));
return translator.signatureForDirectCall(target);
}
return member.accept1(_FunctionTypeGenerator(translator), target);
}
String getFunctionName(Reference target) {
if (target.isTearOffReference) {
return "${target.asMember} tear-off";
}
if (target.isCheckedEntryReference) {
return "${target.asMember} (checked entry)";
}
if (target.isUncheckedEntryReference) {
return "${target.asMember} (unchecked entry)";
}
if (target.isBodyReference) {
return "${target.asMember} (body)";
}
final Member member = target.asMember;
String memberName = member.toString();
if (memberName.endsWith('.')) {
memberName = memberName.substring(0, memberName.length - 1);
}
if (target.isTypeCheckerReference) {
if (member is Field || (member is Procedure && member.isSetter)) {
return '$memberName setter type checker';
} else {
return '$memberName invocation type checker';
}
}
if (member is Field) {
if (target.isImplicitSetter) {
return '$memberName= implicit setter';
}
return '$memberName implicit getter';
}
if (target.isInitializerReference) {
return 'new $memberName (initializer)';
} else if (target.isConstructorBodyReference) {
return 'new $memberName (constructor body)';
} else if (member is Procedure && member.isFactory) {
return 'new $memberName';
} else {
return memberName;
}
}
void recordSelectorUse(SelectorInfo selector, bool useUncheckedEntry) {
final set =
useUncheckedEntry ? _calledUncheckedSelectors : _calledSelectors;
if (set.add(selector.id)) {
for (final (:range, :target) in selector
.targets(unchecked: useUncheckedEntry, dynamicModule: false)
.targetRanges) {
for (int classId = range.start; classId <= range.end; ++classId) {
recordClassTargetUse(classId, target);
}
}
if (translator.isDynamicModule) {
for (final (:range, :target) in selector
.targets(unchecked: useUncheckedEntry, dynamicModule: true)
.targetRanges) {
for (int classId = range.start; classId <= range.end; ++classId) {
recordClassTargetUse(classId, target);
}
}
}
}
}
void recordClassTargetUse(int classId, Reference target) {
if (_allocatedClasses.contains(classId)) {
// Class declaring or inheriting member is allocated somewhere.
getFunction(target);
} else {
// Remember the member in case an allocation is encountered later.
_pendingAllocation.putIfAbsent(classId, () => []).add(target);
}
}
void recordClassAllocation(ClassId classId) {
final id = switch (classId) {
RelativeClassId() => classId.relativeValue,
AbsoluteClassId() => classId.value,
};
if (_allocatedClasses.add(id)) {
// Schedule all members that were pending allocation of this class.
for (Reference target in _pendingAllocation[id] ?? const []) {
getFunction(target);
}
}
}
/// Returns an iterable of translated procedures.
Iterable<Procedure> get translatedProcedures =>
_functions.keys.map((k) => k.node).whereType<Procedure>();
}
class _FunctionTypeGenerator extends MemberVisitor1<w.FunctionType, Reference> {
final Translator translator;
_FunctionTypeGenerator(this.translator);
@override
w.FunctionType visitField(Field node, Reference target) {
if (!node.isInstanceMember) {
// Static field initializer function or implicit getter/setter.
return _makeFunctionType(translator, target, null);
}
assert(!translator.dispatchTable
.selectorForTarget(target)
.containsTarget(target));
final receiverType = target.asMember.enclosingClass!
.getThisType(translator.coreTypes, Nullability.nonNullable);
return _makeFunctionType(
translator, target, translator.translateType(receiverType));
}
@override
w.FunctionType visitProcedure(Procedure node, Reference target) {
// Compilations for dynamic modules can contain interface calls to methods
// that are not implemented yet.
assert(!node.isAbstract || translator.dynamicModuleSupportEnabled);
if (!node.isInstanceMember) {
return _makeFunctionType(translator, target, null);
}
assert(!translator.dispatchTable
.selectorForTarget(target)
.containsTarget(target));
final receiverType = target.asMember.enclosingClass!
.getThisType(translator.coreTypes, Nullability.nonNullable);
return _makeFunctionType(
translator, target, translator.translateType(receiverType));
}
@override
w.FunctionType visitConstructor(Constructor node, Reference target) {
// Get this constructor's argument types
List<w.ValueType> arguments = _getInputTypes(
translator, target, null, false, translator.translateType);
// We need the contexts of the constructor before generating the initializer
// and constructor body functions, as these functions will return/take a
// context argument if context must be shared between them. Generate the
// contexts the first time we visit a constructor.
translator.constructorClosures[node.reference] ??=
translator.getClosures(node);
if (target.isInitializerReference) {
return _getInitializerType(node, target, arguments);
}
if (target.isConstructorBodyReference) {
return _getConstructorBodyType(node, arguments);
}
return _getConstructorAllocatorType(node, arguments);
}
w.FunctionType _getConstructorAllocatorType(
Constructor node, List<w.ValueType> arguments) {
return translator.typesBuilder.defineFunction(arguments,
[translator.classInfo[node.enclosingClass]!.nonNullableType.unpacked]);
}
w.FunctionType _getInitializerType(
Constructor node, Reference target, List<w.ValueType> arguments) {
final ClassInfo info = translator.classInfo[node.enclosingClass]!;
assert(translator.constructorClosures.containsKey(node.reference));
Closures closures = translator.constructorClosures[node.reference]!;
List<w.ValueType> superOrRedirectedInitializerArgs = [];
for (Initializer initializer in node.initializers) {
if (initializer is SuperInitializer) {
Supertype? supersupertype = initializer.target.enclosingClass.supertype;
if (supersupertype != null) {
ClassInfo superInfo = info.superInfo!;
w.FunctionType superInitializer = translator
.signatureForDirectCall(initializer.target.initializerReference);
final int numSuperclassFields = superInfo.getClassFieldTypes().length;
final int numSuperContextAndConstructorArgs =
superInitializer.outputs.length - numSuperclassFields;
// get types of super initializer outputs, ignoring the superclass
// fields
superOrRedirectedInitializerArgs = superInitializer.outputs
.sublist(0, numSuperContextAndConstructorArgs);
}
} else if (initializer is RedirectingInitializer) {
Supertype? supersupertype = initializer.target.enclosingClass.supertype;
if (supersupertype != null) {
w.FunctionType redirectedInitializer = translator
.signatureForDirectCall(initializer.target.initializerReference);
final int numClassFields = info.getClassFieldTypes().length;
final int numRedirectedContextAndConstructorArgs =
redirectedInitializer.outputs.length - numClassFields;
// get types of redirecting initializer outputs, ignoring the class
// fields
superOrRedirectedInitializerArgs = redirectedInitializer.outputs
.sublist(0, numRedirectedContextAndConstructorArgs);
}
}
}
// Get this classes's field types
final List<w.ValueType> fieldTypes = info.getClassFieldTypes();
// Add nullable context reference for when the constructor has a non-empty
// context
Context? context = closures.contexts[node];
w.ValueType? contextRef;
if (context != null) {
assert(!context.isEmpty);
contextRef = w.RefType.struct(nullable: true);
}
final List<w.ValueType> outputs = superOrRedirectedInitializerArgs +
arguments.reversed.toList() +
(contextRef != null ? [contextRef] : []) +
fieldTypes;
return translator.typesBuilder.defineFunction(arguments, outputs);
}
w.FunctionType _getConstructorBodyType(
Constructor node, List<w.ValueType> arguments) {
assert(translator.constructorClosures.containsKey(node.reference));
Closures closures = translator.constructorClosures[node.reference]!;
Context? context = closures.contexts[node];
List<w.ValueType> inputs = [
translator.classInfo[node.enclosingClass]!.nonNullableType.unpacked
];
if (context != null) {
assert(!context.isEmpty);
// Nullable context reference for when the constructor has a non-empty
// context
w.ValueType contextRef = w.RefType.struct(nullable: true);
inputs.add(contextRef);
}
inputs += arguments;
for (Initializer initializer in node.initializers) {
if (initializer is SuperInitializer ||
initializer is RedirectingInitializer) {
Constructor target = initializer is SuperInitializer
? initializer.target
: (initializer as RedirectingInitializer).target;
Supertype? supersupertype = target.enclosingClass.supertype;
if (supersupertype != null) {
w.FunctionType superOrRedirectedConstructorBodyType = translator
.signatureForDirectCall(target.constructorBodyReference);
// drop receiver param
inputs += superOrRedirectedConstructorBodyType.inputs.sublist(1);
}
}
}
return translator.typesBuilder.defineFunction(inputs, []);
}
}
List<w.ValueType> _getInputTypes(
Translator translator,
Reference target,
w.ValueType? receiverType,
bool isImportOrExport,
w.ValueType Function(DartType) translateType) {
Member member = target.asMember;
int typeParamCount = 0;
Iterable<DartType> params;
if (member is Field) {
params = [if (target.isImplicitSetter) member.setterType];
} else {
FunctionNode function = member.function!;
typeParamCount = (member is Constructor
? member.enclosingClass.typeParameters
: function.typeParameters)
.length;
List<String> names = [for (var p in function.namedParameters) p.name!]
..sort();
final typeForParam = translator.typeOfParameterVariable;
Map<String, DartType> nameTypes = {
for (var p in function.namedParameters)
p.name!: typeForParam(p, p.isRequired)
};
final positionals = function.positionalParameters;
params = [
for (int i = 0; i < positionals.length; ++i)
typeForParam(positionals[i], i < function.requiredParameterCount),
for (String name in names) nameTypes[name]!
];
}
final List<w.ValueType> typeParameters = List.filled(
typeParamCount,
translateType(
InterfaceType(translator.typeClass, Nullability.nonNullable)));
final List<w.ValueType> inputs = [];
if (receiverType != null) {
assert(!isImportOrExport);
inputs.add(receiverType);
}
inputs.addAll(typeParameters);
inputs.addAll(params.map(translateType));
return inputs;
}
// Functions that get checked & unchecked variants will run the actual body by
// calling a body function. This builds the signature of such body functions.
//
// Implicit setters also support checked/unchecked entries, but those will not
// call a shared body but have such body (which is trivial) in the checked &
// unchecked functions directly.
w.FunctionType makeFunctionTypeForBody(Translator translator, Member member) {
assert(member.isInstanceMember);
assert(member is Procedure);
final function = member.function!;
final receiverType = member.enclosingClass!
.getThisType(translator.coreTypes, Nullability.nonNullable);
final inputs = <w.ValueType>[
translator.translateType(receiverType),
for (final _ in function.typeParameters)
translator.translateType(translator.types.typeType),
for (final p in function.positionalParameters)
translator.translateType(translator.typeOfCheckedParameterVariable(p)),
for (final p in function.namedParameters)
translator.translateType(translator.typeOfCheckedParameterVariable(p)),
];
final isSetter = member is Procedure && member.isSetter;
final outputs = [
if (!isSetter)
translator.translateReturnType(translator.typeOfReturnValue(member)),
];
return translator.typesBuilder.defineFunction(inputs, outputs);
}
w.FunctionType _makeFunctionType(
Translator translator, Reference target, w.ValueType? receiverType,
{bool isImportOrExport = false}) {
Member member = target.asMember;
if (member is Field && !member.isInstanceMember) {
final isGetter = target.isImplicitGetter;
final isSetter = target.isImplicitSetter;
if (isGetter || isSetter) {
final fieldType = translator.translateTypeOfField(member);
if (isGetter) {
return translator.typesBuilder.defineFunction(const [], [fieldType]);
}
return translator.typesBuilder.defineFunction([fieldType], const []);
}
}
// Translate types differently for imports and exports.
w.ValueType translateType(DartType type) => isImportOrExport
? translator.translateExternalType(type)
: translator.translateType(type);
w.ValueType translateReturnType(DartType type) => isImportOrExport
? translator.translateExternalType(type)
: translator.translateReturnType(type);
final List<w.ValueType> inputs = _getInputTypes(
translator, target, receiverType, isImportOrExport, translateType);
bool isVoidType(DartType t) =>
(isImportOrExport && t is VoidType) ||
(t is InterfaceType && t.classNode == translator.wasmVoidClass);
final List<w.ValueType> outputs;
if (target.isSetter) {
// Setters are the only functions without any returned values. All other
// functions can either return values (even `void` returning functions)
outputs = const [];
} else {
final DartType returnType = translator.typeOfReturnValue(member);
outputs =
!isVoidType(returnType) ? [translateReturnType(returnType)] : const [];
}
return translator.typesBuilder.defineFunction(inputs, outputs);
}