blob: 07c6f0109137f51137cf9683ec357c299c041699 [file] [log] [blame]
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:kernel/ast.dart';
import 'package:kernel/core_types.dart';
import 'package:kernel/library_index.dart';
import 'package:kernel/target/targets.dart' show Target;
import 'package:vm/metadata/procedure_attributes.dart'
show ProcedureAttributesMetadata;
import 'package:vm/transformations/dynamic_interface_annotator.dart'
as dynamic_interface_annotator;
import 'package:vm/transformations/pragma.dart';
import 'package:wasm_builder/wasm_builder.dart' as w;
import 'class_info.dart';
import 'code_generator.dart';
import 'compiler_options.dart';
import 'constants.dart' show maxArrayNewFixedLength;
import 'dispatch_table.dart';
import 'dynamic_module_kernel_metadata.dart';
import 'intrinsics.dart' show MemberIntrinsic;
import 'kernel_nodes.dart';
import 'modules.dart';
import 'reference_extensions.dart';
import 'target.dart';
import 'translator.dart';
import 'types.dart' show InstanceConstantInterfaceType;
import 'util.dart';
// Pragmas used to annotate the kernel during main module compilation.
const String _mainModLibPragma = 'wasm:mainMod';
const String _mainLibPragma = 'wasm:mainLib';
const String _mainMethodPragma = 'wasm:mainMethod';
const String _globalIdPragma = 'wasm:globalId';
const String _dynamicModuleEntryPointName = '\$invokeEntryPoint';
extension DynamicModuleComponent on Component {
static final Expando<Procedure> _dynamicModuleEntryPoint =
Expando<Procedure>();
Procedure? get dynamicModuleEntryPoint => _dynamicModuleEntryPoint[this];
List<Library> getMainModuleLibraries(CoreTypes coreTypes) =>
[...libraries.where((l) => l.isFromMainModule(coreTypes))];
}
extension DynamicModuleLibrary on Library {
bool isFromMainModule(CoreTypes coreTypes) =>
hasPragma(coreTypes, this, _mainModLibPragma);
}
extension DynamicModuleClass on Class {
bool isDynamicModuleExtendable(CoreTypes coreTypes) =>
hasPragma(coreTypes, this, kDynModuleExtendablePragmaName) ||
hasPragma(coreTypes, this, kDynModuleImplicitlyExtendablePragmaName);
bool isMainModuleLive(CoreTypes coreTypes) =>
getPragma<int>(coreTypes, this, _globalIdPragma, defaultValue: 0) != null;
}
extension DynamicModuleMember on Member {
bool isDynamicModuleCallable(CoreTypes coreTypes) =>
hasPragma(coreTypes, this, kDynModuleCallablePragmaName) ||
hasPragma(coreTypes, this, kDynModuleImplicitlyCallablePragmaName);
bool isDynamicModuleCallableNoTearOff(CoreTypes coreTypes) =>
getPragma(coreTypes, this, kDynModuleCallablePragmaName,
defaultValue: '') ==
'call';
bool isDynamicModuleOverrideable(CoreTypes coreTypes) =>
hasPragma(coreTypes, this, kDynModuleCanBeOverriddenPragmaName) ||
hasPragma(coreTypes, this, kDynModuleCanBeOverriddenImplicitlyPragmaName);
bool isMainModuleLive(CoreTypes coreTypes) =>
getPragma<int>(coreTypes, this, _globalIdPragma, defaultValue: 0) != null;
}
class DynamicModuleOutputData extends ModuleOutputData {
final CoreTypes coreTypes;
DynamicModuleOutputData(this.coreTypes, super.modules, super.importMap);
ModuleOutput get _dynamicModule => modules[1];
@override
ModuleOutput moduleForReference(Reference reference) {
// Rather than create tear-offs for all dynamic callable methods in the main
// module, we create them as needed in the dynamic modules.
if (reference.isTearOffReference) return _dynamicModule;
final member = reference.asMember;
// Members in new record classes should get generated in the dynamic module.
if (member.enclosingClass?.superclass == coreTypes.recordClass &&
!member.isMainModuleLive(coreTypes)) {
return _dynamicModule;
}
return super.moduleForReference(reference);
}
}
class DynamicMainModuleStrategy extends DefaultModuleStrategy with KernelNodes {
@override
final CoreTypes coreTypes;
final Target target;
@override
final LibraryIndex index;
final Uri dynamicInterfaceSpecificationBaseUri;
final String dynamicInterfaceSpecification;
DynamicMainModuleStrategy(
super.component,
this.coreTypes,
this.target,
this.dynamicInterfaceSpecification,
this.dynamicInterfaceSpecificationBaseUri)
: index = coreTypes.index;
@override
void prepareComponent() {
// Annotate the kernel with info from dynamic interface.
dynamic_interface_annotator.annotateComponent(dynamicInterfaceSpecification,
dynamicInterfaceSpecificationBaseUri, component, coreTypes, target);
_addImplicitPragmas();
_addMetadataPragmas();
}
@override
ModuleOutputData buildModuleOutputData() {
final builder = ModuleOutputBuilder();
final mainModule = builder.buildModule();
mainModule.libraries.addAll(component.libraries);
final placeholderModule = builder.buildModule(skipEmit: true);
return ModuleOutputData([mainModule, placeholderModule], const {});
}
void _addImplicitPragmas() {
final pragmasAdded = <(Member, String)>{};
void add(Member member, String pragma) {
if (pragmasAdded.add((member, pragma))) {
addPragma(member, pragma, coreTypes);
}
}
// These members don't have normal bodies and should therefore not be
// considered directly callable from dynamic modules.
final Set<Member> excludedIntrinsics = {
coreTypes.index.getProcedure("dart:_wasm", "WasmFunction", "get:call"),
coreTypes.index.getConstructor("dart:_boxed_int", "BoxedInt", "_"),
coreTypes.index.getConstructor("dart:_boxed_double", "BoxedDouble", "_"),
};
void checkMemberEntryPoint(Member member) {
if (excludedIntrinsics.contains(member)) return;
// Entrypoints are all dynamically callable and vice versa.
final isEntryPoint = getPragma(
coreTypes, member, kWasmEntryPointPragmaName,
defaultValue: '') !=
null;
final isDynamicModuleCallable = member.isDynamicModuleCallable(coreTypes);
if (isEntryPoint && !isDynamicModuleCallable) {
add(member, kDynModuleCallablePragmaName);
}
}
for (final library in component.libraries) {
for (final member in library.members) {
checkMemberEntryPoint(member);
}
for (final cls in library.classes) {
for (final member in cls.members) {
checkMemberEntryPoint(member);
}
}
}
// Add implicit pragmas
// Object has some inherent properties even though it is not explicitly
// annotated.
addPragma(coreTypes.objectClass, kDynModuleExtendablePragmaName, coreTypes);
for (final procedure in coreTypes.objectClass.procedures) {
add(procedure, kDynModuleCanBeOverriddenPragmaName);
add(procedure, kDynModuleCallablePragmaName);
}
// Mark all record classes as dynamic module extendable.
addPragma(coreTypes.recordClass, kDynModuleExtendablePragmaName, coreTypes);
// SystemHash.combine used by closures.
add(systemHashCombine, kDynModuleCallablePragmaName);
}
void _addMetadataPragmas() {
// Annotate with kernel with metadata that will help subsequent dynamic
// module compilations to identify members and classes.
addPragma(
component.mainMethod!.enclosingLibrary, _mainLibPragma, coreTypes);
addPragma(component.mainMethod!, _mainMethodPragma, coreTypes);
int nextId = 0;
final idRepo = DynamicModuleGlobalIdRepository();
component.addMetadataRepository(idRepo);
void annotateMember(Member member) {
final memberId = nextId++;
addPragma(member, _globalIdPragma, coreTypes,
value: IntConstant(memberId));
idRepo.mapping[member] = memberId;
}
for (final lib in component.libraries) {
lib.annotations = [...lib.annotations];
addPragma(lib, _mainModLibPragma, coreTypes);
for (final member in lib.members) {
annotateMember(member);
}
for (final cls in lib.classes) {
final classId = nextId++;
idRepo.mapping[cls] = classId;
addPragma(cls, _globalIdPragma, coreTypes, value: IntConstant(classId));
for (final member in cls.members) {
annotateMember(member);
}
}
}
}
}
class DynamicModuleStrategy extends DefaultModuleStrategy with KernelNodes {
final WasmCompilerOptions options;
final WasmTarget kernelTarget;
final Uri mainModuleComponentUri;
@override
final CoreTypes coreTypes;
@override
final LibraryIndex index;
final Set<Library> _mainModuleLibraries = {};
final Set<Library> _dynamicModuleLibraries = {};
DynamicModuleStrategy(super.component, this.options, this.kernelTarget,
this.coreTypes, this.mainModuleComponentUri)
: index = coreTypes.index;
@override
void prepareComponent() {
final dynamicEntryPoint = _findDynamicEntryPoint(component, coreTypes);
addWasmEntryPointPragma(dynamicEntryPoint, coreTypes);
DynamicModuleComponent._dynamicModuleEntryPoint[component] =
dynamicEntryPoint;
_processMetadataPragmas();
_registerLibraries();
_prepareWasmEntryPoint(dynamicEntryPoint);
}
void _prepareWasmEntryPoint(Procedure dynamicEntryPoint) {
dynamicEntryPoint.function.returnType = const DynamicType();
// Export the entry point so that the JS runtime can get the function and
// pass it to the main module.
addPragma(dynamicEntryPoint, 'wasm:export', coreTypes,
value: StringConstant(_dynamicModuleEntryPointName));
}
void _processMetadataPragmas() {
// Unpack metadata from the kernel AST nodes that were annotated during the
// main module compilation.
final idRepo = DynamicModuleGlobalIdRepository();
component.addMetadataRepository(idRepo);
void processMember(Member member) {
idRepo.mapping[member] = getPragma(coreTypes, member, _globalIdPragma)!;
}
for (final library in component.libraries) {
if (hasPragma(coreTypes, library, _mainModLibPragma)) {
for (final member in library.members) {
processMember(member);
}
_mainModuleLibraries.add(library);
for (final cls in library.classes) {
final classId = getPragma(coreTypes, cls, _globalIdPragma);
if (classId == null) continue;
idRepo.mapping[cls] = classId;
for (final member in cls.members) {
processMember(member);
}
}
} else {
_dynamicModuleLibraries.add(library);
}
if (hasPragma(coreTypes, library, _mainLibPragma)) {
final mainMethod = library.procedures
.firstWhere((m) => hasPragma(coreTypes, m, _mainMethodPragma));
component.setMainMethodAndMode(mainMethod.reference, true);
}
}
}
void _registerLibraries() {
// Register each library with the SDK. This will ensure no duplicate
// libraries are included across dynamic modules.
final registerLibraryUris = coreTypes.index
.getTopLevelProcedure('dart:_internal', 'registerLibraryUris');
final entryPoint = component.dynamicModuleEntryPoint!;
final libraryUris = ListLiteral([
..._dynamicModuleLibraries
.map((l) => StringLiteral(l.importUri.toString()))
], typeArgument: coreTypes.stringNonNullableRawType);
entryPoint.function.body = Block([
ExpressionStatement(
StaticInvocation(registerLibraryUris, Arguments([libraryUris]))),
entryPoint.function.body!,
]);
}
static Procedure _findDynamicEntryPoint(
Component component, CoreTypes coreTypes) {
for (final library in component.libraries) {
for (final procedure in library.procedures) {
final entryPointPragma = getPragma(
coreTypes, procedure, kDynModuleEntryPointPragmaName,
defaultValue: true) ??
false;
if (entryPointPragma) {
return procedure;
}
}
}
throw StateError('Entry point not found for dynamic module.');
}
@override
ModuleOutputData buildModuleOutputData() {
final moduleBuilder = ModuleOutputBuilder();
final mainModule = moduleBuilder.buildModule(skipEmit: true);
mainModule.libraries.addAll(_mainModuleLibraries);
final dynamicModule = moduleBuilder.buildModule(emitAsMain: true);
dynamicModule.libraries.addAll(_dynamicModuleLibraries);
return DynamicModuleOutputData(
coreTypes, [mainModule, dynamicModule], const {});
}
}
void _recordIdMain(w.FunctionBuilder f, Translator translator) {
final ranges = translator.classIdNumbering
.getConcreteClassIdRangeForMainModule(translator.coreTypes.recordClass);
final ib = f.body;
ib.local_get(ib.locals[0]);
ib.emitClassIdRangeCheck(ranges);
ib.end();
}
void _recordIdDynamic(w.FunctionBuilder f, Translator translator) {
final ranges = translator.classIdNumbering
.getConcreteClassIdRangeForDynamicModule(
translator.coreTypes.recordClass);
final ib = f.body;
if (ranges.isEmpty) {
ib.i32_const(0);
} else {
ib.local_get(ib.locals[0]);
translator.callReference(translator.localizeClassId.reference, ib);
ib.emitClassIdRangeCheck(ranges);
}
ib.end();
}
w.FunctionType _recordIdBuildType(Translator translator) {
return translator.typesBuilder
.defineFunction(const [w.NumType.i32], const [w.NumType.i32]);
}
enum BuiltinUpdatableFunctions {
recordId(_recordIdMain, _recordIdDynamic, _recordIdBuildType);
final void Function(w.FunctionBuilder, Translator) _buildMain;
final void Function(w.FunctionBuilder, Translator) _buildDynamic;
final w.FunctionType Function(Translator) _buildType;
const BuiltinUpdatableFunctions(
this._buildMain, this._buildDynamic, this._buildType);
}
class DynamicModuleInfo {
final Translator translator;
Procedure? get dynamicEntryPoint =>
translator.component.dynamicModuleEntryPoint;
bool get isDynamicModule => dynamicEntryPoint != null;
late final w.FunctionBuilder initFunction;
late final MainModuleMetadata metadata;
late final w.Global moduleIdGlobal;
// null is used to indicate that skipDynamic was passed for this key.
final Map<int, w.BaseFunction?> overrideableFunctions = {};
final Map<ClassInfo, Map<w.ModuleBuilder, w.BaseFunction>>
_constantCacheCheckers = {};
final Map<w.StorageType, Map<w.ModuleBuilder, w.BaseFunction>>
_mutableArrayConstantCacheCheckers = {};
final Map<w.StorageType, Map<w.ModuleBuilder, w.BaseFunction>>
_immutableArrayConstantCacheCheckers = {};
late final w.ModuleBuilder dynamicModule =
translator.modules.firstWhere((m) => m != translator.mainModule);
final Map<Member, ProcedureAttributesMetadata> mainModuleProcedureAttributes =
{};
DynamicModuleInfo(this.translator, this.metadata) {
metadata.memberMetadata.forEach((member, metadata) {
mainModuleProcedureAttributes[member] = metadata.procedureAttributes;
});
}
void initDynamicModule() {
dynamicModule.functions.start = initFunction = dynamicModule.functions
.define(translator.typesBuilder.defineFunction(const [], const []),
"#init");
// Make sure the exception tag is exported from the main module.
translator.getExceptionTag(dynamicModule);
if (isDynamicModule) {
_initDynamicModuleId();
_initModuleRtt();
} else {
_initializeDynamicAllocatableClasses();
_initializeCallableReferences();
}
_initializeOverrideableReferences();
}
void _initModuleRtt() {
final b = initFunction.body;
translator.pushModuleId(b);
final moduleRtt = translator.types.rtt.getModuleRtt(isMainModule: false);
translator.constants.instantiateConstant(
b, moduleRtt, translator.translateType(moduleRtt.interfaceType));
translator.callReference(translator.registerModuleRtt.reference, b);
b.drop();
}
void _initDynamicModuleId() {
final global = moduleIdGlobal = dynamicModule.globals
.define(w.GlobalType(w.NumType.i64, mutable: true), '#_moduleId');
global.initializer
..i64_const(0)
..end();
final b = initFunction.body;
final rangeSize = translator.classIdNumbering.maxDynamicModuleClassId! -
translator.classIdNumbering.firstDynamicModuleClassId +
1;
b.i32_const(rangeSize);
translator.callReference(translator.registerModuleClassRange.reference, b);
b.global_set(moduleIdGlobal);
}
void _initializeCallableReferences() {
void collectCallableReference(Reference reference) {
final member = reference.asMember;
if (member.isExternal) {
final isGeneratedIntrinsic = member is Procedure &&
MemberIntrinsic.fromProcedure(translator.coreTypes, member) != null;
if (!isGeneratedIntrinsic) return;
}
metadata.callableReferenceIds[reference] =
metadata.callableReferenceIds.length;
if (!member.isInstanceMember) {
// Generate static members immediately since they are unconditionally
// callable.
translator.functions.getFunction(reference);
return;
}
final selector = translator.dispatchTable.selectorForTarget(reference);
final targetRanges = selector
.targets(unchecked: false)
.targetRanges
.followedBy(selector.targets(unchecked: true).targetRanges);
// Instance members are only callable if their enclosing class is
// allocated.
for (final (:range, :target) in targetRanges) {
if (target != reference) continue;
for (int classId = range.start; classId <= range.end; ++classId) {
translator.functions.recordClassTargetUse(classId, target);
}
}
}
void collectCallableReferences(Member member) {
if (member is Procedure) {
collectCallableReference(member.reference);
// We ignore the tear-off and let each dynamic module generate it for
// itself.
} else if (member is Field) {
collectCallableReference(member.getterReference);
if (member.hasSetter) {
collectCallableReference(member.setterReference!);
}
} else if (member is Constructor &&
// Skip types that don't extend Object in the wasm type hierarchy.
// These types do not have directly invokable constructors.
(translator.classInfo[member.enclosingClass]!.superInfo !=
translator.topInfo ||
member.enclosingClass == translator.coreTypes.objectClass)) {
collectCallableReference(member.reference);
collectCallableReference(member.initializerReference);
collectCallableReference(member.constructorBodyReference);
}
}
for (final lib in translator.component.libraries) {
for (final member in lib.members) {
if (!member.isDynamicModuleCallable(translator.coreTypes)) continue;
collectCallableReferences(member);
}
for (final cls in lib.classes) {
for (final member in cls.members) {
if (!member.isDynamicModuleCallable(translator.coreTypes)) continue;
collectCallableReferences(member);
}
}
}
}
void _initializeDynamicAllocatableClasses() {
for (final lib in translator.component.libraries) {
for (final cls in lib.classes) {
if (cls.isDynamicModuleExtendable(translator.coreTypes) ||
cls.constructors
.any((e) => e.isDynamicModuleCallable(translator.coreTypes))) {
translator.functions
.recordClassAllocation(translator.classInfo[cls]!.classId);
}
}
}
}
void _initializeOverrideableReferences() {
for (final builtin in BuiltinUpdatableFunctions.values) {
_createUpdateableFunction(
builtin.index, false, builtin._buildType(translator),
buildMain: (f) => builtin._buildMain(f, translator),
buildDynamic: (f) => builtin._buildDynamic(f, translator),
name: '#r_${builtin.name}');
}
for (final (reference, useUncheckedEntry) in metadata.invokedReferences) {
final selector = translator.dispatchTable.selectorForTarget(reference);
translator.functions.recordSelectorUse(selector, useUncheckedEntry);
w.FunctionType signature;
void Function(w.FunctionBuilder) buildMain;
void Function(w.FunctionBuilder) buildDynamic;
final mainSelector = translator.dynamicMainModuleDispatchTable!
.selectorForTarget(reference);
signature = _getGeneralizedSignature(mainSelector);
buildMain =
buildSelectorBranch(reference, useUncheckedEntry, mainSelector);
buildDynamic =
buildSelectorBranch(reference, useUncheckedEntry, mainSelector);
_createUpdateableFunction(
mainSelector.id + BuiltinUpdatableFunctions.values.length,
useUncheckedEntry,
signature,
buildMain: buildMain,
buildDynamic: buildDynamic,
name: '#s${mainSelector.id}_${mainSelector.name}');
}
}
void finishDynamicModule() {
_registerModuleRefs(
isDynamicModule ? initFunction.body : translator.initFunction.body);
initFunction.body.end();
}
void _registerModuleRefs(w.InstructionsBuilder b) {
final numKeys = overrideableFunctions.length;
assert(numKeys < maxArrayNewFixedLength);
final orderedFunctions = ([...overrideableFunctions.entries]
..sort((a, b) => a.key.compareTo(b.key)))
.map((e) => e.value);
for (final function in orderedFunctions) {
if (function != null) {
b.ref_func(function);
} else {
b.ref_null(w.HeapType.func);
}
}
b.array_new_fixed(
translator.wasmArrayType(w.RefType.func(nullable: true), ''), numKeys);
translator.callReference(
translator.registerUpdateableFuncRefs.reference, b);
b.drop();
}
int _createUpdateableFunction(
int key, bool useUncheckedEntry, w.FunctionType type,
{required void Function(w.FunctionBuilder function) buildMain,
required void Function(w.FunctionBuilder function) buildDynamic,
bool skipDynamic = false,
required String name}) {
final mapKey = (key, useUncheckedEntry);
final index = metadata.keyInvocationToIndex[mapKey] ??=
metadata.keyInvocationToIndex.length;
overrideableFunctions.putIfAbsent(index, () {
if (!isDynamicModule) {
final mainFunction = translator.mainModule.functions.define(type, name);
translator.mainModule.functions.declare(mainFunction);
buildMain(mainFunction);
return mainFunction;
}
if (skipDynamic) {
return null;
}
final dynamicModuleFunction = dynamicModule.functions.define(type, name);
dynamicModule.functions.declare(dynamicModuleFunction);
buildDynamic(dynamicModuleFunction);
return dynamicModuleFunction;
});
return index;
}
void _callClassIdBranch(int key, bool useUncheckedEntry,
w.InstructionsBuilder b, w.FunctionType signature,
{required void Function(w.FunctionBuilder b) buildMainMatch,
required void Function(w.FunctionBuilder b) buildDynamicMatch,
bool skipDynamic = false,
required String name}) {
// No new types declared in the dynamic module so the branch would always
// miss.
final canSkipDynamicBranch = skipDynamic ||
translator.classIdNumbering.maxDynamicModuleClassId ==
translator.classIdNumbering.maxClassId;
final callIndex = _createUpdateableFunction(
key, useUncheckedEntry, signature,
buildMain: buildMainMatch,
buildDynamic: buildDynamicMatch,
skipDynamic: canSkipDynamicBranch,
name: name);
translator.callReference(translator.classIdToModuleId.reference, b);
b.i64_const(callIndex);
// getUpdateableFuncRef allows for null entries since a dynamic module may
// not implement every key. However, only keys that cannot be queried should
// be unimplemented so it's safe to cast to a non-nullable function here.
translator.callReference(translator.getUpdateableFuncRef.reference, b);
translator.convertType(b, w.RefType.func(nullable: true),
w.RefType(signature, nullable: false));
b.call_ref(signature);
}
void callClassIdBranchBuiltIn(
BuiltinUpdatableFunctions key, w.InstructionsBuilder b,
{bool skipDynamic = false}) {
_callClassIdBranch(key.index, false, b, key._buildType(translator),
buildMainMatch: (f) => key._buildMain(f, translator),
buildDynamicMatch: (f) => key._buildDynamic(f, translator),
name: '#r_${key.name}',
skipDynamic: skipDynamic);
}
w.FunctionType _getGeneralizedSignature(SelectorInfo mainSelector) {
final signature = mainSelector.signature;
// The shared entry point to this selector has to use 'any' because the
// selector's signature may change between compilations.
final generalizedSignature = translator.typesBuilder.defineFunction([
...signature.inputs.map((e) => const w.RefType.any(nullable: true)),
w.NumType.i32
], [
...signature.outputs.map((e) => const w.RefType.any(nullable: true))
]);
return generalizedSignature;
}
void Function(w.FunctionBuilder) buildSelectorBranch(
Reference target, bool useUncheckedEntry, SelectorInfo mainSelector) {
return (w.FunctionBuilder function) {
final localSelector = translator.dispatchTable.selectorForTarget(target);
final localSignature = localSelector.signature;
final ib = function.body;
final offset = localSelector.targets(unchecked: useUncheckedEntry).offset;
if (offset == null) {
ib.unreachable();
ib.end();
return;
}
final generalizedMainSignature = _getGeneralizedSignature(mainSelector);
final mainParamInfo = mainSelector.paramInfo;
final localParamInfo = localSelector.paramInfo;
assert(mainParamInfo.takesContextOrReceiver ==
localParamInfo.takesContextOrReceiver);
int localsIndex = 0;
final takesContextOrReceiver = localParamInfo.takesContextOrReceiver;
if (takesContextOrReceiver) {
ib.local_get(ib.locals[localsIndex]);
translator.convertType(ib, generalizedMainSignature.inputs[localsIndex],
localSignature.inputs[localsIndex]);
localsIndex++;
}
final mainTypeParamCount = mainParamInfo.typeParamCount;
assert(mainTypeParamCount == localParamInfo.typeParamCount);
for (int i = 0; i < mainTypeParamCount; i++, localsIndex++) {
ib.local_get(ib.locals[localsIndex]);
translator.convertType(ib, generalizedMainSignature.inputs[localsIndex],
localSignature.inputs[localsIndex]);
}
final localPositionalCount = localParamInfo.positional.length;
final mainPositionalCount = mainParamInfo.positional.length;
assert(localPositionalCount >= mainPositionalCount);
for (int i = 0; i < localPositionalCount; i++, localsIndex++) {
if (i < mainPositionalCount) {
ib.local_get(ib.locals[localsIndex]);
translator.convertType(
ib,
generalizedMainSignature.inputs[localsIndex],
localSignature.inputs[localsIndex]);
continue;
}
final constant = localParamInfo.positional[i]!;
translator.constants.instantiateConstant(
ib, constant, localSignature.inputs[localsIndex]);
}
final localNamedCount = localParamInfo.named.length;
final mainNamedCount = mainParamInfo.named.length;
assert(localNamedCount >= mainNamedCount);
for (int i = 0; i < localNamedCount; i++, localsIndex++) {
final name = localParamInfo.names[i];
final mainIndex = mainParamInfo.nameIndex[name];
if (mainIndex != null) {
final mainLocalIndex =
(takesContextOrReceiver ? 1 : 0) + mainTypeParamCount + mainIndex;
ib.local_get(ib.locals[mainLocalIndex]);
translator.convertType(
ib,
generalizedMainSignature.inputs[mainLocalIndex],
localSignature.inputs[localsIndex]);
continue;
}
final constant = localParamInfo.named[name]!;
translator.constants.instantiateConstant(
ib, constant, localSignature.inputs[localsIndex]);
}
ib.local_get(ib.locals.last);
if (isDynamicModule) {
translator.callReference(translator.scopeClassId.reference, ib);
}
if (offset != 0) {
ib.i32_const(offset);
ib.i32_add();
}
final table = translator.dispatchTable.getWasmTable(ib.module);
ib.call_indirect(localSignature, table);
translator.convertType(ib, localSignature.outputs.single,
generalizedMainSignature.outputs.single);
ib.end();
};
}
void callOverrideableDispatch(
w.InstructionsBuilder b, SelectorInfo selector, Reference interfaceTarget,
{required bool useUncheckedEntry}) {
metadata.invokedReferences.add((interfaceTarget, useUncheckedEntry));
final localSignature = selector.signature;
// If any input is not a RefType (i.e. it's an unboxed value) then wrap it
// so the updated signature works.
if (localSignature.inputs.any((i) => i is! w.RefType)) {
final receiverLocal = b.addLocal(translator.topInfo.nullableType);
b.local_set(receiverLocal);
final locals = <w.Local>[];
for (final input in localSignature.inputs.reversed) {
final local = b.addLocal(input);
locals.add(local);
b.local_set(local);
}
for (final local in locals.reversed) {
b.local_get(local);
translator.convertType(b, local.type, translator.topInfo.nullableType);
}
b.local_get(receiverLocal);
}
final idLocal = b.addLocal(w.NumType.i32);
b.struct_get(translator.topInfo.struct, FieldIndex.classId);
b.local_tee(idLocal);
b.local_get(idLocal);
final mainDispatchTable =
translator.dynamicMainModuleDispatchTable ?? translator.dispatchTable;
final mainModuleSelector =
mainDispatchTable.selectorForTarget(interfaceTarget);
final generalizedSignature = _getGeneralizedSignature(mainModuleSelector);
// For consistency, always use the main module selector ID when generating
// the key.
final key = mainModuleSelector.id + BuiltinUpdatableFunctions.values.length;
_callClassIdBranch(key, useUncheckedEntry, b, generalizedSignature,
name: '#s${mainModuleSelector.id}_${mainModuleSelector.name}',
buildMainMatch: buildSelectorBranch(
interfaceTarget, useUncheckedEntry, mainModuleSelector),
buildDynamicMatch: buildSelectorBranch(
interfaceTarget, useUncheckedEntry, mainModuleSelector),
skipDynamic: translator.isDynamicModule &&
selector
.targets(unchecked: useUncheckedEntry)
.targetRanges
.isEmpty);
translator.convertType(
b, generalizedSignature.outputs.single, localSignature.outputs.single);
}
}
/// Emits code to canonicalize the provided constant value at runtime.
///
/// This canonicalizer works by generating custom equality functions for any
/// type of constant it encounters. The SDK maintains an array of canonicalized
/// objects separated by type and the equality function generated here is used
/// to identify the canonical version of a constant.
///
/// For example, for a normal Dart Object of type T, we will first construct a
/// new instance of T. Then we will fetch an array containing all instances of T
/// already canonicalized. Using an equality function which does a pairwise
/// comparison of T's fields, we will walk the array looking for an instance
/// that matches the new T. If there is one we return the canonical version,
/// otherwise we add it to the array and return the new T.
///
/// Iterables, wasm arrays and wasm builtin types all require special
/// canonicalization logic.
///
/// Only classes defined in the main module require canonicalization because
/// these are the only classes that can have identical constants instantiated in
/// different dynamic modules. A class defined a dynamic module cannot be
/// accessed from a different dynamic module.
class ConstantCanonicalizer extends ConstantVisitor<void> {
final Translator translator;
final w.InstructionsBuilder b;
/// A local containing the value to be canonicalized.
final w.Local valueLocal;
ConstantCanonicalizer(this.translator, this.b, this.valueLocal);
late final _checkerType = translator.typesBuilder.defineFunction([
translator.topInfo.nonNullableType,
translator.topInfo.nonNullableType,
], const [
w.NumType.i32
]);
late final _arrayCheckerType = translator.typesBuilder.defineFunction(const [
w.RefType.array(nullable: false),
w.RefType.array(nullable: false),
], const [
w.NumType.i32
]);
/// Wasm builtin value types that don't need canonicalization.
late final Set<Class> _wasmValueClasses = {
translator.wasmI32Class,
translator.wasmI64Class,
translator.wasmF32Class,
translator.wasmF64Class,
translator.wasmI16Class,
translator.wasmI8Class,
translator.wasmAnyRefClass,
translator.wasmExternRefClass,
translator.wasmFuncRefClass,
translator.wasmEqRefClass,
translator.wasmStructRefClass,
translator.wasmArrayRefClass,
};
/// Boxed values are comparable by the value they wrap.
late final Set<Class> _boxedClasses = {
translator.boxedIntClass,
translator.boxedDoubleClass,
translator.boxedBoolClass,
};
/// Values of these types are canonicalized by their == function.
late final Set<Class> _equalsCheckerClasses = {
translator.jsStringClass,
translator.symbolClass,
translator.closureClass,
};
/// These iterable classes contain lazily initialized data that should not be
/// considered in comparisons.
late final Set<Class> _hashingIterableConstClasses = {
translator.immutableSetClass,
translator.immutableMapClass,
};
/// Emit code that canonicalizes the instance of [cls] stored in [valueLocal].
void _canonicalizeInstance(Class cls) {
final classId = translator.classInfo[cls]!.classId;
if (classId is RelativeClassId) {
// This class is not defined in the main module so it doesn't need runtime
// canonicalization.
return;
}
if (_wasmValueClasses.contains(cls)) {
// Wasm value types do not need canonicalization.
return;
}
// Lookup the WasmCache for the value's type.
b.local_set(valueLocal);
b.i64_const((classId as AbsoluteClassId).value);
translator.callReference(translator.constCacheGetter.reference, b);
// Get the equality checker for the class. Import it into the dynamic module
// and use the import if this is in a dynamic module.
w.BaseFunction checker = _getCanonicalChecker(cls, b.module);
// Declare the function so it can be used as a ref_func in a constant
// context.
b.module.functions.declare(checker);
// Invoke the 'canonicalize' function with the value and checker.
b.local_get(valueLocal);
b.ref_func(checker);
final valueType = translator.callReference(
translator.constCacheCanonicalize.reference, b);
// The canonicalizer returns an Object which may be a boxed value. Unbox it
// if necessary.
translator.convertType(b, valueType.single, valueLocal.type);
}
void _canonicalizeArray(bool mutable, DartType elementType) {
b.local_set(valueLocal);
final cacheField = (mutable
? translator.wasmArrayConstCache
: translator.immutableWasmArrayConstCache)[elementType];
if (cacheField == null) {
throw StateError(
'Unrecognized const array type (mutable: $mutable): $elementType');
}
translator.callReference(cacheField.getterReference, b);
// Get the equality checker for the class. Import it into the dynamic module
// and use the import if this is in a dynamic module.
w.BaseFunction checker = _getCanonicalArrayChecker(
translator.translateStorageType(elementType), mutable, b.module);
// Declare the function so it can be used as a ref_func in a constant
// context.
b.module.functions.declare(checker);
// Invoke the canonicalizer function with the value and checker.
b.local_get(valueLocal);
b.ref_func(checker);
final valueType = translator.callReference(
translator.constCacheArrayCanonicalize.reference, b);
// The canonicalizer returns an array ref, cast it to the correct array
// type.
translator.convertType(b, valueType.single, valueLocal.type);
}
/// Get a function that will compare two instances of [cls] and return true if
/// they canonicalize to the same value.
w.BaseFunction _getCanonicalChecker(Class cls, w.ModuleBuilder module) {
ClassInfo info = translator.classInfo[cls]!;
// We create a checker for each class to ensure we check each struct field.
return translator.dynamicModuleInfo!._constantCacheCheckers
.putIfAbsent(info, () => {})
.putIfAbsent(module, () {
final checker =
module.functions.define(_checkerType, '${info.cls} constCheck');
final b = checker.body;
_checkerForClass(b, info);
b.end();
return checker;
});
}
/// Get a function that will compare two arrays with elements of type
/// [elementType] and return true if they canonicalize to the same value.
w.BaseFunction _getCanonicalArrayChecker(
w.StorageType elementType, bool mutable, w.ModuleBuilder module) {
final cache = mutable
? translator.dynamicModuleInfo!._mutableArrayConstantCacheCheckers
: translator.dynamicModuleInfo!._immutableArrayConstantCacheCheckers;
// We create a checker for each array element type.
return cache.putIfAbsent(elementType, () => {}).putIfAbsent(module, () {
final name = '$elementType';
final checker = module.functions.define(_arrayCheckerType,
'$name const${mutable ? '' : 'Immutable'}ArrayCheck');
final arrayType =
translator.wasmArrayType(elementType, name, mutable: mutable);
final b = checker.body;
_checkerForArray(b, arrayType, elementType);
b.end();
return checker;
});
}
void _checkerForClass(w.InstructionsBuilder b, ClassInfo classInfo) {
final cls = classInfo.cls!;
if (_boxedClasses.contains(cls)) {
return _checkerForBoxedClasses(b, classInfo);
}
final structRef = classInfo.nonNullableType;
b.local_get(b.locals[0]);
b.ref_cast(structRef);
b.local_get(b.locals[1]);
b.ref_cast(structRef);
if (_equalsCheckerClasses.contains(cls)) {
_checkerWithEquals(b);
} else if (_hashingIterableConstClasses.contains(cls)) {
_defaultChecker(b, classInfo, fieldsToInclude: {
FieldIndex.hashBaseData,
...cls.typeParameters.map((t) => translator.typeParameterIndex[t]!)
});
} else {
_defaultChecker(b, classInfo);
}
}
/// Compare boxed entites via the values they wrap.
void _checkerForBoxedClasses(w.InstructionsBuilder b, ClassInfo classInfo) {
final structRef = classInfo.nonNullableType;
b.local_get(b.locals[0]);
b.ref_cast(structRef);
b.struct_get(classInfo.struct, FieldIndex.boxValue);
b.local_get(b.locals[1]);
b.ref_cast(structRef);
b.struct_get(classInfo.struct, FieldIndex.boxValue);
return _equalsForValueType(
b, translator.builtinTypes[classInfo.cls] as w.ValueType);
}
/// Compare values using a dispatch call to Object.==
void _checkerWithEquals(w.InstructionsBuilder b) {
b.local_get(b.locals[0]);
final selector = translator.dispatchTable
.selectorForTarget(translator.coreTypes.objectEquals.reference);
translator.callDispatchTable(b, selector,
interfaceTarget: translator.coreTypes.objectEquals.reference,
useUncheckedEntry: true);
translator.convertType(
b, selector.signature.outputs.first, _checkerType.outputs.first);
}
/// Compare two normal class instances whose const identity are determined by
/// their fields. Do a shallow comparison of the fields assuming the field
/// values are already canonicalized.
void _defaultChecker(w.InstructionsBuilder b, ClassInfo classInfo,
{Set<int>? fieldsToInclude}) {
classInfo = classInfo.repr;
final structType = classInfo.struct;
final structRefType = classInfo.nonNullableType;
final castedLocal1 = b.addLocal(structRefType);
final castedLocal2 = b.addLocal(structRefType);
b.local_set(castedLocal2);
b.local_set(castedLocal1);
final falseBlock = b.block();
classInfo.forEachClassFieldIndex((index, fieldType) {
if (fieldsToInclude != null && !fieldsToInclude.contains(index)) {
return;
}
b.local_get(castedLocal1);
b.struct_get(structType, index);
b.local_get(castedLocal2);
b.struct_get(structType, index);
final fieldTypeUnpacked = fieldType.type;
_equalsForValueType(b, fieldTypeUnpacked);
b.i32_eqz();
b.br_if(falseBlock);
});
b.i32_const(1);
b.return_();
b.end();
b.i32_const(0);
}
/// Compare two arrays for equality by iterating through the elements and
/// doing a shallow pairwise comparison. Array elements will already be
/// canonicalized. Assumes the types and lengths of the arrays are already
/// equivalent.
void _checkerForArray(w.InstructionsBuilder b, w.ArrayType arrayType,
w.StorageType elementType) {
final arrayRefType = w.RefType(arrayType, nullable: false);
final array1 = b.addLocal(arrayRefType);
final array2 = b.addLocal(arrayRefType);
final falseBlock = b.block();
b.local_get(b.locals[0]);
b.ref_cast(arrayRefType);
b.local_set(array1);
b.local_get(b.locals[1]);
b.ref_cast(arrayRefType);
b.local_set(array2);
b.incrementingLoop(
pushStart: () => b.i32_const(0),
pushLimit: () {
b.local_get(array1);
b.array_len();
},
genBody: (loopLocal) {
b.local_get(array1);
b.local_get(loopLocal);
if (elementType is w.PackedType) {
b.array_get_u(arrayType);
} else {
b.array_get(arrayType);
}
b.local_get(array2);
b.local_get(loopLocal);
if (elementType is w.PackedType) {
b.array_get_u(arrayType);
} else {
b.array_get(arrayType);
}
_equalsForValueType(b, elementType);
b.i32_eqz();
b.br_if(falseBlock);
});
b.i32_const(1);
b.return_();
b.end();
b.i32_const(0);
}
/// Invokes the builtin equality function for [storageType].
static void _equalsForValueType(
w.InstructionsBuilder b, w.StorageType storageType) {
if (storageType is w.RefType) {
b.ref_eq();
} else if (storageType == w.PackedType.i8 ||
storageType == w.PackedType.i16) {
b.i32_eq();
} else if (storageType == w.NumType.f32) {
b.f32_eq();
} else if (storageType == w.NumType.f64) {
b.f64_eq();
} else if (storageType == w.NumType.i32) {
b.i32_eq();
} else if (storageType == w.NumType.i64) {
b.i64_eq();
} else {
throw UnsupportedError('Could not find eq for $storageType');
}
}
@override
Never visitAuxiliaryConstant(AuxiliaryConstant node) {
throw UnsupportedError('Cannot canonicalize auxiliary constants.');
}
@override
void visitBoolConstant(BoolConstant node) {
_canonicalizeInstance(translator.boxedBoolClass);
}
@override
void visitConstructorTearOffConstant(ConstructorTearOffConstant node) {
_canonicalizeInstance(translator.closureClass);
}
@override
void visitDoubleConstant(DoubleConstant node) {
_canonicalizeInstance(translator.boxedDoubleClass);
}
@override
void visitInstanceConstant(InstanceConstant node) {
if (node.classNode == translator.wasmArrayClass) {
final dartElementType = node.typeArguments.single;
_canonicalizeArray(true, dartElementType);
} else if (node.classNode == translator.immutableWasmArrayClass) {
final dartElementType = node.typeArguments.single;
_canonicalizeArray(false, dartElementType);
} else {
_canonicalizeInstance(node.classNode);
}
}
@override
void visitInstantiationConstant(InstantiationConstant node) {
_canonicalizeInstance(translator.closureClass);
}
@override
void visitIntConstant(IntConstant node) {
_canonicalizeInstance(translator.boxedIntClass);
}
@override
void visitListConstant(ListConstant node) {
_canonicalizeInstance(translator.immutableListClass);
}
@override
void visitMapConstant(MapConstant node) {
_canonicalizeInstance(translator.immutableMapClass);
}
@override
void visitNullConstant(NullConstant node) {}
@override
void visitRecordConstant(RecordConstant node) {
_canonicalizeInstance(translator.coreTypes.recordClass);
}
@override
void visitRedirectingFactoryTearOffConstant(
RedirectingFactoryTearOffConstant node) {
_canonicalizeInstance(translator.closureClass);
}
@override
void visitSetConstant(SetConstant node) {
_canonicalizeInstance(translator.immutableSetClass);
}
@override
void visitStaticTearOffConstant(StaticTearOffConstant node) {
_canonicalizeInstance(translator.closureClass);
}
@override
void visitStringConstant(StringConstant node) {
_canonicalizeInstance(translator.jsStringClass);
}
@override
void visitSymbolConstant(SymbolConstant node) {
return _canonicalizeInstance(translator.symbolClass);
}
@override
void visitTypeLiteralConstant(TypeLiteralConstant node) {
_canonicalizeInstance(translator.typeClass);
}
@override
Never visitTypedefTearOffConstant(TypedefTearOffConstant node) {
throw UnsupportedError('Cannot canonicalize typedef tearoff constants.');
}
@override
Never visitUnevaluatedConstant(UnevaluatedConstant node) {
throw UnsupportedError('Cannot canonicalize unevaluated constants.');
}
}