blob: fed50ea424ca648e792aa0f0fe4c49a6296bc2bf [file] [log] [blame] [edit]
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:kernel/ast.dart';
import 'package:kernel/class_hierarchy.dart';
import 'package:kernel/core_types.dart';
import 'package:kernel/library_index.dart';
import 'package:vm/metadata/direct_call.dart' show DirectCallMetadata;
import 'package:vm/metadata/procedure_attributes.dart';
import 'package:vm/metadata/table_selector.dart';
import 'package:vm/transformations/devirtualization.dart';
import 'package:vm/transformations/dynamic_interface_annotator.dart'
as dynamic_interface_annotator;
import 'package:vm/transformations/pragma.dart';
import 'package:vm/transformations/type_flow/table_selector_assigner.dart';
import 'package:wasm_builder/wasm_builder.dart' as w;
import 'class_info.dart';
import 'code_generator.dart';
import 'compiler_options.dart';
import 'constants.dart' show maxArrayNewFixedLength;
import 'dispatch_table.dart';
import 'dynamic_module_kernel_metadata.dart';
import 'intrinsics.dart' show MemberIntrinsic;
import 'kernel_nodes.dart';
import 'modules.dart';
import 'param_info.dart';
import 'record_class_generator.dart' show dynamicModulesRecordsLibraryUri;
import 'reference_extensions.dart';
import 'target.dart';
import 'translator.dart';
import 'types.dart' show InstanceConstantInterfaceType;
import 'util.dart';
// Pragmas used to annotate the kernel during main module compilation.
const String _mainModLibPragma = 'wasm:mainMod';
const String _submoduleEntryPointName = '\$invokeEntryPoint';
enum DynamicModuleType {
main,
submodule;
static DynamicModuleType parse(String s) => switch (s) {
"main" => main,
"submodule" => submodule,
_ => throw ArgumentError("Unrecognized dynamic module type $s."),
};
}
extension DynamicSubmoduleComponent on Component {
static final Expando<Procedure> _submoduleEntryPoint = Expando<Procedure>();
Procedure? get dynamicSubmoduleEntryPoint => _submoduleEntryPoint[this];
List<Library> getDynamicSubmoduleLibraries(CoreTypes coreTypes) =>
[...libraries.where((l) => !l.isFromMainModule(coreTypes))];
}
extension DynamicModuleLibrary on Library {
bool isFromMainModule(CoreTypes coreTypes) =>
hasPragma(coreTypes, this, _mainModLibPragma);
}
extension DynamicModuleClass on Class {
bool isDynamicSubmoduleExtendable(CoreTypes coreTypes) =>
hasPragma(coreTypes, this, kDynModuleExtendablePragmaName) ||
hasPragma(coreTypes, this, kDynModuleImplicitlyExtendablePragmaName);
}
extension DynamicModuleMember on Member {
bool isDynamicSubmoduleCallable(CoreTypes coreTypes) =>
hasPragma(coreTypes, this, kDynModuleCallablePragmaName) ||
hasPragma(coreTypes, this, kDynModuleImplicitlyCallablePragmaName);
bool isDynamicSubmoduleCallableNoTearOff(CoreTypes coreTypes) =>
getPragma(coreTypes, this, kDynModuleCallablePragmaName,
defaultValue: '') ==
'call';
bool isDynamicSubmoduleOverridable(CoreTypes coreTypes) =>
hasPragma(coreTypes, this, kDynModuleCanBeOverriddenPragmaName) ||
hasPragma(coreTypes, this, kDynModuleCanBeOverriddenImplicitlyPragmaName);
}
class DynamicSubmoduleOutputData extends ModuleOutputData {
final CoreTypes coreTypes;
final ModuleOutput _submodule;
DynamicSubmoduleOutputData(this.coreTypes, super.modules, super.importMap)
: _submodule = modules[1];
@override
ModuleOutput moduleForReference(Reference reference) {
// Rather than create tear-offs for all dynamic callable methods in the main
// module, we create them as needed in the submodules.
if (reference.isTearOffReference) return _submodule;
return super.moduleForReference(reference);
}
}
class DynamicMainModuleStrategy extends ModuleStrategy with KernelNodes {
@override
final Component component;
@override
final CoreTypes coreTypes;
@override
final LibraryIndex index;
final Uri dynamicInterfaceSpecificationBaseUri;
final String dynamicInterfaceSpecification;
DynamicMainModuleStrategy(
this.component,
this.coreTypes,
this.dynamicInterfaceSpecification,
this.dynamicInterfaceSpecificationBaseUri)
: index = coreTypes.index;
@override
void prepareComponent() {
// Annotate the kernel with info from dynamic interface.
dynamic_interface_annotator.annotateComponent(dynamicInterfaceSpecification,
dynamicInterfaceSpecificationBaseUri, component, coreTypes);
_addImplicitPragmas();
for (final lib in component.libraries) {
lib.annotations = [...lib.annotations];
addPragma(lib, _mainModLibPragma, coreTypes);
}
component.addMetadataRepository(DynamicModuleConstantRepository());
component.addMetadataRepository(DynamicModuleGlobalIdRepository());
}
@override
ModuleOutputData buildModuleOutputData() {
final builder = ModuleOutputBuilder();
final mainModule = builder.buildModule();
mainModule.libraries.addAll(component.libraries);
final placeholderModule = builder.buildModule(skipEmit: true);
return ModuleOutputData([mainModule, placeholderModule], const {});
}
void _addImplicitPragmas() {
final pragmasAdded = <(Member, String)>{};
void add(Member member, String pragma) {
if (pragmasAdded.add((member, pragma))) {
addPragma(member, pragma, coreTypes);
}
}
// These members don't have normal bodies and should therefore not be
// considered directly callable from submodules.
final Set<Member> excludedIntrinsics = {
coreTypes.index.getProcedure("dart:_wasm", "WasmFunction", "get:call"),
coreTypes.index.getConstructor("dart:_boxed_int", "BoxedInt", "_"),
coreTypes.index.getConstructor("dart:_boxed_double", "BoxedDouble", "_"),
};
void checkMemberEntryPoint(Member member) {
if (excludedIntrinsics.contains(member)) return;
// Entrypoints are all dynamically callable and vice versa.
final isEntryPoint = getPragma(
coreTypes, member, kWasmEntryPointPragmaName,
defaultValue: '') !=
null;
final isSubmoduleCallable = member.isDynamicSubmoduleCallable(coreTypes);
if (isEntryPoint && !isSubmoduleCallable) {
add(member, kDynModuleCallablePragmaName);
}
}
for (final library in component.libraries) {
for (final member in library.members) {
checkMemberEntryPoint(member);
}
for (final cls in library.classes) {
for (final member in cls.members) {
checkMemberEntryPoint(member);
}
}
}
// Add implicit pragmas
// Object has some inherent properties even though it is not explicitly
// annotated.
addPragma(coreTypes.objectClass, kDynModuleExtendablePragmaName, coreTypes);
for (final procedure in coreTypes.objectClass.procedures) {
add(procedure, kDynModuleCanBeOverriddenPragmaName);
add(procedure, kDynModuleCallablePragmaName);
}
// Mark all record classes as dynamic module extendable.
addPragma(coreTypes.recordClass, kDynModuleExtendablePragmaName, coreTypes);
// SystemHash.combine used by closures.
add(systemHashCombine, kDynModuleCallablePragmaName);
}
}
class DynamicSubmoduleStrategy extends ModuleStrategy {
final Component component;
final WasmCompilerOptions options;
final WasmTarget kernelTarget;
final Uri mainModuleComponentUri;
final CoreTypes coreTypes;
DynamicSubmoduleStrategy(this.component, this.options, this.kernelTarget,
this.coreTypes, this.mainModuleComponentUri);
@override
void prepareComponent() {
final submoduleEntryPoint = _findSubmoduleEntryPoint(component, coreTypes);
addWasmEntryPointPragma(submoduleEntryPoint, coreTypes);
DynamicSubmoduleComponent._submoduleEntryPoint[component] =
submoduleEntryPoint;
_registerLibraries();
_prepareWasmEntryPoint(submoduleEntryPoint);
_addTfaMetadata();
}
void _prepareWasmEntryPoint(Procedure submoduleEntryPoint) {
submoduleEntryPoint.function.returnType = const DynamicType();
// Export the entry point so that the JS runtime can get the function and
// pass it to the main module.
addPragma(submoduleEntryPoint, 'wasm:export', coreTypes,
value: StringConstant(_submoduleEntryPointName));
}
void _registerLibraries() {
// Register each library with the SDK. This will ensure no duplicate
// libraries are included across dynamic modules.
final registerLibraryUris = coreTypes.index
.getTopLevelProcedure('dart:_internal', 'registerLibraryUris');
final entryPoint = component.dynamicSubmoduleEntryPoint!;
final libraryUris = ListLiteral([
...component
.getDynamicSubmoduleLibraries(coreTypes)
.where((l) => '${l.importUri}' != dynamicModulesRecordsLibraryUri)
.map((l) => StringLiteral(l.importUri.toString()))
], typeArgument: coreTypes.stringNonNullableRawType);
entryPoint.function.body = Block([
ExpressionStatement(
StaticInvocation(registerLibraryUris, Arguments([libraryUris]))),
entryPoint.function.body!,
])
..parent = entryPoint.function;
}
static Procedure _findSubmoduleEntryPoint(
Component component, CoreTypes coreTypes) {
for (final library in component.libraries) {
for (final procedure in library.procedures) {
final entryPointPragma = getPragma(
coreTypes, procedure, kDynModuleEntryPointPragmaName,
defaultValue: true) ??
false;
if (entryPointPragma) {
return procedure;
}
}
}
throw StateError('Entry point not found for dynamic submodule.');
}
void _addTfaMetadata() {
component.metadata[dynamicMainModuleProcedureAttributeMetadataTag] =
component
.metadata[ProcedureAttributesMetadataRepository.repositoryTag]!;
component.metadata[dynamicMainModuleSelectorMetadataTag] =
component.metadata[TableSelectorMetadataRepository.repositoryTag]!;
final selectorAssigner = TableSelectorAssigner(component);
for (final selector in selectorAssigner.metadata.selectors) {
selector.callCount++;
selector.tornOff = true;
selector.calledOnNull = true;
}
final selectorMetadataRepository = TableSelectorMetadataRepository();
component.metadata[TableSelectorMetadataRepository.repositoryTag] =
selectorMetadataRepository;
selectorMetadataRepository.mapping[component] = selectorAssigner.metadata;
final dynamicModuleProcedureAttributes =
ProcedureAttributesMetadataRepository();
for (final library in component.libraries) {
for (final cls in library.classes) {
for (final member in cls.members) {
if (!member.isInstanceMember) continue;
dynamicModuleProcedureAttributes.mapping[member] =
ProcedureAttributesMetadata(
getterSelectorId: selectorAssigner.getterSelectorId(member),
methodOrSetterSelectorId:
selectorAssigner.methodOrSetterSelectorId(member));
}
}
}
component.metadata[ProcedureAttributesMetadataRepository.repositoryTag] =
dynamicModuleProcedureAttributes;
final classHierarchy =
ClassHierarchy(component, coreTypes) as ClosedWorldClassHierarchy;
component.accept(_Devirtualization(coreTypes, component, classHierarchy,
classHierarchy.computeSubtypesInformation()));
}
@override
ModuleOutputData buildModuleOutputData() {
final moduleBuilder = ModuleOutputBuilder();
final mainModule = moduleBuilder.buildModule(skipEmit: true);
final submodule = moduleBuilder.buildModule(emitAsMain: true);
for (final library in component.libraries) {
final module = hasPragma(coreTypes, library, _mainModLibPragma)
? mainModule
: submodule;
module.libraries.add(library);
}
return DynamicSubmoduleOutputData(
coreTypes, [mainModule, submodule], const {});
}
}
void _recordIdMain(w.FunctionBuilder f, Translator translator) {
final ranges = translator.classIdNumbering
.getConcreteClassIdRangeForMainModule(translator.coreTypes.recordClass);
final ib = f.body;
ib.local_get(ib.locals[0]);
ib.emitClassIdRangeCheck(ranges);
ib.end();
}
void _recordIdSubmodule(w.FunctionBuilder f, Translator translator) {
final ranges = translator.classIdNumbering
.getConcreteClassIdRangeForDynamicSubmodule(
translator.coreTypes.recordClass);
final ib = f.body;
if (ranges.isEmpty) {
ib.i32_const(0);
} else {
ib.local_get(ib.locals[0]);
translator.callReference(translator.localizeClassId.reference, ib);
ib.emitClassIdRangeCheck(ranges);
}
ib.end();
}
w.FunctionType _recordIdBuildType(Translator translator) {
return translator.typesBuilder
.defineFunction(const [w.NumType.i32], const [w.NumType.i32]);
}
enum BuiltinUpdatableFunctions {
recordId(_recordIdMain, _recordIdSubmodule, _recordIdBuildType);
final void Function(w.FunctionBuilder, Translator) _buildMain;
final void Function(w.FunctionBuilder, Translator) _buildSubmodule;
final w.FunctionType Function(Translator) _buildType;
const BuiltinUpdatableFunctions(
this._buildMain, this._buildSubmodule, this._buildType);
}
class DynamicModuleInfo {
final Translator translator;
Procedure? get submoduleEntryPoint =>
translator.component.dynamicSubmoduleEntryPoint;
bool get isSubmodule => submoduleEntryPoint != null;
late final w.FunctionBuilder initFunction;
late final MainModuleMetadata metadata;
late final w.Global moduleIdGlobal;
// null is used to indicate that skipDynamic was passed for this key.
final Map<int, w.BaseFunction?> overridableFunctions = {};
final Map<ClassInfo, Map<w.ModuleBuilder, w.BaseFunction>>
_constantCacheCheckers = {};
final Map<w.StorageType, Map<w.ModuleBuilder, w.BaseFunction>>
_mutableArrayConstantCacheCheckers = {};
final Map<w.StorageType, Map<w.ModuleBuilder, w.BaseFunction>>
_immutableArrayConstantCacheCheckers = {};
late final w.ModuleBuilder submodule =
translator.modules.firstWhere((m) => m != translator.mainModule);
DynamicModuleInfo(this.translator, this.metadata);
void initSubmodule() {
submodule.functions.start = initFunction = submodule.functions.define(
translator.typesBuilder.defineFunction(const [], const []), "#init");
// Make sure the exception tag is exported from the main module.
translator.getExceptionTag(submodule);
if (isSubmodule) {
_initSubmoduleId();
_initModuleRtt();
} else {
_initializeSubmoduleAllocatableClasses();
_initializeCallableReferences();
}
_initializeOverridableReferences();
}
void _initModuleRtt() {
final b = initFunction.body;
translator.pushModuleId(b);
final moduleRtt = translator.types.rtt.getModuleRtt(isMainModule: false);
translator.constants.instantiateConstant(
b, moduleRtt, translator.translateType(moduleRtt.interfaceType));
translator.callReference(translator.registerModuleRtt.reference, b);
b.drop();
}
void _initSubmoduleId() {
final global = moduleIdGlobal = submodule.globals
.define(w.GlobalType(w.NumType.i64, mutable: true), '#_moduleId');
global.initializer
..i64_const(0)
..end();
final b = initFunction.body;
final rangeSize = translator.classIdNumbering.maxDynamicSubmoduleClassId! -
translator.classIdNumbering.firstDynamicSubmoduleClassId +
1;
b.i32_const(rangeSize);
translator.callReference(translator.registerModuleClassRange.reference, b);
b.global_set(moduleIdGlobal);
}
bool _isClassSubmoduleInstantiable(Class cls) {
return cls.isDynamicSubmoduleExtendable(translator.coreTypes) ||
cls.constructors
.any((e) => e.isDynamicSubmoduleCallable(translator.coreTypes)) ||
cls.procedures.any((e) =>
e.isFactory && e.isDynamicSubmoduleCallable(translator.coreTypes));
}
void _initializeCallableReferences() {
for (final lib in translator.component.libraries) {
for (final member in lib.members) {
if (!member.isDynamicSubmoduleCallable(translator.coreTypes)) continue;
_forEachMemberReference(member, _registerStaticCallableTarget);
}
}
for (final classInfo in translator.classesSupersFirst) {
final cls = classInfo.cls;
if (cls == null) continue;
// Register any callable functions defined within this class.
for (final member in cls.members) {
if (!member.isDynamicSubmoduleCallable(translator.coreTypes)) continue;
if (!member.isInstanceMember) {
// Generate static members immediately since they are unconditionally
// callable.
_forEachMemberReference(member, _registerStaticCallableTarget);
continue;
}
// Consider callable references invoked and therefore if they're
// overridable include them in the runtime dispatch table.
if (member.isDynamicSubmoduleOverridable(translator.coreTypes)) {
_forEachMemberReference(
member, metadata.invokedOverridableReferences.add);
}
}
// Anonymous mixins' targets don't need to be registered since they aren't
// directly allocatable.
if (cls.isAnonymousMixin) continue;
if (cls.isAbstract && !_isClassSubmoduleInstantiable(cls)) {
continue;
}
// For each dispatch target, register the member as callable from this
// class.
final targets = translator.hierarchy.getDispatchTargets(cls).followedBy(
translator.hierarchy.getDispatchTargets(cls, setters: true));
for (final member in targets) {
if (!member.isDynamicSubmoduleCallable(translator.coreTypes)) continue;
_forEachMemberReference(member,
(reference) => _registerCallableDispatchTarget(reference, cls));
}
}
}
/// If class [cls] is marked allocated then ensure we compile [target].
///
/// The [cls] may be marked allocated in
/// [_initializeSubmoduleAllocatableClasses] which (together with this) will
/// enqueue the [target] for compilation. Otherwise the [cls] must be
/// allocated via a constructor call in the program itself.
void _registerCallableDispatchTarget(Reference target, Class cls) {
final member = target.asMember;
if (member.isExternal) {
final isGeneratedIntrinsic = member is Procedure &&
MemberIntrinsic.fromProcedure(translator.coreTypes, member) != null;
if (!isGeneratedIntrinsic) return;
}
final classId =
(translator.classInfo[cls]!.classId as AbsoluteClassId).value;
metadata.callableReferenceIds[target] ??=
metadata.callableReferenceIds.length;
// The class must be allocated in order for the target to be live.
translator.functions.recordClassTargetUse(classId, target);
}
void _registerStaticCallableTarget(Reference target) {
final member = target.asMember;
if (member.isExternal) {
final isGeneratedIntrinsic = member is Procedure &&
MemberIntrinsic.fromProcedure(translator.coreTypes, member) != null;
if (!isGeneratedIntrinsic) return;
}
// Generate static members immediately since they are unconditionally
// callable.
metadata.callableReferenceIds[target] ??=
metadata.callableReferenceIds.length;
translator.functions.getFunction(target);
}
void _initializeSubmoduleAllocatableClasses() {
for (final classInfo in translator.classesSupersFirst) {
final cls = classInfo.cls;
if (cls == null) continue;
if (cls.isAnonymousMixin) continue;
if (_isClassSubmoduleInstantiable(cls)) {
translator.functions.recordClassAllocation(classInfo.classId);
}
}
}
void _initializeOverridableReferences() {
for (final builtin in BuiltinUpdatableFunctions.values) {
_createUpdateableFunction(builtin.index, builtin._buildType(translator),
buildMain: (f) => builtin._buildMain(f, translator),
buildSubmodule: (f) => builtin._buildSubmodule(f, translator),
name: '#r_${builtin.name}');
}
for (final reference in metadata.invokedOverridableReferences) {
final selector = translator.dispatchTable.selectorForTarget(reference);
translator.functions.recordSelectorUse(selector, false);
final mainSelector = (translator.dynamicMainModuleDispatchTable ??
translator.dispatchTable)
.selectorForTarget(reference);
final signature = _getGeneralizedSignature(mainSelector);
final buildMain = buildSelectorBranch(reference, mainSelector);
final buildSubmodule = buildSelectorBranch(reference, mainSelector);
_createUpdateableFunction(
mainSelector.id + BuiltinUpdatableFunctions.values.length, signature,
buildMain: buildMain,
buildSubmodule: buildSubmodule,
name: '#s${mainSelector.id}_${mainSelector.name}');
}
}
void _forEachMemberReference(Member member, void Function(Reference) f) {
void passReference(Reference reference) {
final checkedReference =
translator.getFunctionEntry(reference, uncheckedEntry: false);
f(checkedReference);
final uncheckedReference =
translator.getFunctionEntry(reference, uncheckedEntry: true);
if (uncheckedReference != checkedReference) {
f(uncheckedReference);
}
}
if (member is Procedure) {
passReference(member.reference);
// We ignore the tear-off and let each submodule generate it for itself.
} else if (member is Field) {
passReference(member.getterReference);
if (member.hasSetter) {
passReference(member.setterReference!);
}
} else if (member is Constructor &&
// Skip types that don't extend Object in the wasm type hierarchy.
// These types do not have directly invokable constructors.
translator.classInfo[member.enclosingClass]!.struct
.isSubtypeOf(translator.objectInfo.struct)) {
if (!member.enclosingClass.isAnonymousMixin) {
passReference(member.reference);
}
passReference(member.initializerReference);
passReference(member.constructorBodyReference);
}
}
void finishDynamicModule() {
_registerModuleRefs(
isSubmodule ? initFunction.body : translator.initFunction.body);
initFunction.body.end();
}
void _registerModuleRefs(w.InstructionsBuilder b) {
final numKeys = overridableFunctions.length;
assert(numKeys < maxArrayNewFixedLength);
final orderedFunctions = ([...overridableFunctions.entries]
..sort((a, b) => a.key.compareTo(b.key)))
.map((e) => e.value);
for (final function in orderedFunctions) {
if (function != null) {
b.ref_func(function);
} else {
b.ref_null(w.HeapType.func);
}
}
b.array_new_fixed(
translator.wasmArrayType(w.RefType.func(nullable: true), ''), numKeys);
translator.callReference(
translator.registerUpdateableFuncRefs.reference, b);
b.drop();
}
int _createUpdateableFunction(int key, w.FunctionType type,
{required void Function(w.FunctionBuilder function) buildMain,
required void Function(w.FunctionBuilder function) buildSubmodule,
bool skipSubmodule = false,
required String name}) {
final mapKey = key;
final index = metadata.keyInvocationToIndex[mapKey] ??=
metadata.keyInvocationToIndex.length;
overridableFunctions.putIfAbsent(index, () {
if (!isSubmodule) {
final mainFunction = translator.mainModule.functions.define(type, name);
translator.mainModule.functions.declare(mainFunction);
buildMain(mainFunction);
return mainFunction;
}
if (skipSubmodule) return null;
final submoduleFunction = submodule.functions.define(type, name);
submodule.functions.declare(submoduleFunction);
buildSubmodule(submoduleFunction);
return submoduleFunction;
});
return index;
}
void _callClassIdBranch(
int key, w.InstructionsBuilder b, w.FunctionType signature,
{required void Function(w.FunctionBuilder b) buildMainMatch,
required void Function(w.FunctionBuilder b) buildSubmoduleMatch,
bool skipSubmodule = false,
required String name}) {
// No new types declared in the submodule so the branch would always miss.
final canSkipSubmoduleBranch = skipSubmodule ||
translator.classIdNumbering.maxDynamicSubmoduleClassId ==
translator.classIdNumbering.maxClassId;
final callIndex = _createUpdateableFunction(key, signature,
buildMain: buildMainMatch,
buildSubmodule: buildSubmoduleMatch,
skipSubmodule: canSkipSubmoduleBranch,
name: name);
translator.callReference(translator.classIdToModuleId.reference, b);
b.i64_const(callIndex);
// getUpdateableFuncRef allows for null entries since a submodule may not
// implement every key. However, only keys that cannot be queried should be
// unimplemented so it's safe to cast to a non-nullable function here.
translator.callReference(translator.getUpdateableFuncRef.reference, b);
translator.convertType(b, w.RefType.func(nullable: true),
w.RefType(signature, nullable: false));
b.call_ref(signature);
}
void callClassIdBranchBuiltIn(
BuiltinUpdatableFunctions key, w.InstructionsBuilder b,
{bool skipSubmodule = false}) {
_callClassIdBranch(key.index, b, key._buildType(translator),
buildMainMatch: (f) => key._buildMain(f, translator),
buildSubmoduleMatch: (f) => key._buildSubmodule(f, translator),
name: '#r_${key.name}',
skipSubmodule: skipSubmodule);
}
w.FunctionType _getGeneralizedSignature(SelectorInfo mainSelector) {
final signature = mainSelector.signature;
// The shared entry point to this selector has to use 'any' because the
// selector's signature may change between compilations.
final generalizedSignature = translator.typesBuilder.defineFunction([
...signature.inputs.map((e) => const w.RefType.any(nullable: true)),
w.NumType.i32,
w.NumType.i32
], [
...signature.outputs.map((e) => const w.RefType.any(nullable: true))
]);
return generalizedSignature;
}
void Function(w.FunctionBuilder) buildSelectorBranch(
Reference interfaceTarget, SelectorInfo mainSelector) {
return (w.FunctionBuilder function) {
final localSelector =
translator.dispatchTable.selectorForTarget(interfaceTarget);
final ib = function.body;
final uncheckedTargets = localSelector.targets(unchecked: true);
final checkedTargets = localSelector.targets(unchecked: false);
// Whether we use checked+unchecked (or normal) we'll have the same
// class-id ranges - only the actual target `Reference` may be a unchecked
// or checked one.
assert(uncheckedTargets.targetRanges.length ==
checkedTargets.targetRanges.length);
// NOTE: Keep this in sync with
// `code_generator.dart:AstCodeGenerator._virtualCall`.
final bool noTarget = checkedTargets.targetRanges.isEmpty;
final bool directCall = checkedTargets.targetRanges.length == 1;
final callPolymorphicDispatcher =
!directCall && checkedTargets.staticDispatchRanges.isNotEmpty;
// disabled for dyn overridable selectors atm
assert(!callPolymorphicDispatcher);
if (noTarget) {
ib.comment('No targets in local module for ${localSelector.name}');
ib.unreachable();
ib.end();
return;
}
final w.FunctionType localSignature;
final ParameterInfo localParamInfo;
if (directCall) {
final target = checkedTargets.targetRanges.single.target;
localSignature = translator.signatureForDirectCall(target);
localParamInfo = translator.paramInfoForDirectCall(target);
} else {
localSignature = localSelector.signature;
localParamInfo = localSelector.paramInfo;
}
final generalizedMainSignature = _getGeneralizedSignature(mainSelector);
final mainParamInfo = mainSelector.paramInfo;
assert(mainParamInfo.takesContextOrReceiver ==
localParamInfo.takesContextOrReceiver);
int localsIndex = 0;
final takesContextOrReceiver = localParamInfo.takesContextOrReceiver;
if (takesContextOrReceiver) {
ib.local_get(ib.locals[localsIndex]);
translator.convertType(ib, generalizedMainSignature.inputs[localsIndex],
localSignature.inputs[localsIndex]);
localsIndex++;
}
final mainTypeParamCount = mainParamInfo.typeParamCount;
assert(mainTypeParamCount == localParamInfo.typeParamCount);
for (int i = 0; i < mainTypeParamCount; i++, localsIndex++) {
ib.local_get(ib.locals[localsIndex]);
translator.convertType(ib, generalizedMainSignature.inputs[localsIndex],
localSignature.inputs[localsIndex]);
}
final localPositionalCount = localParamInfo.positional.length;
final mainPositionalCount = mainParamInfo.positional.length;
assert(localPositionalCount >= mainPositionalCount);
for (int i = 0; i < localPositionalCount; i++, localsIndex++) {
if (i < mainPositionalCount) {
ib.local_get(ib.locals[localsIndex]);
translator.convertType(
ib,
generalizedMainSignature.inputs[localsIndex],
localSignature.inputs[localsIndex]);
continue;
}
final constant = localParamInfo.positional[i]!;
translator.constants.instantiateConstant(
ib, constant, localSignature.inputs[localsIndex]);
}
final localNamedCount = localParamInfo.named.length;
final mainNamedCount = mainParamInfo.named.length;
assert(localNamedCount >= mainNamedCount);
for (int i = 0; i < localNamedCount; i++, localsIndex++) {
final name = localParamInfo.names[i];
final mainIndex = mainParamInfo.nameIndex[name];
if (mainIndex != null) {
final mainLocalIndex =
(takesContextOrReceiver ? 1 : 0) + mainTypeParamCount + mainIndex;
ib.local_get(ib.locals[mainLocalIndex]);
translator.convertType(
ib,
generalizedMainSignature.inputs[mainLocalIndex],
localSignature.inputs[localsIndex]);
continue;
}
final constant = localParamInfo.named[name]!;
translator.constants.instantiateConstant(
ib, constant, localSignature.inputs[localsIndex]);
}
if (directCall) {
if (!localSelector.useMultipleEntryPoints) {
final target = checkedTargets.targetRanges.single.target;
ib.invoke(translator.directCallTarget(target));
} else {
final uncheckedTarget = uncheckedTargets.targetRanges.single.target;
final checkedTarget = checkedTargets.targetRanges.single.target;
// Check if the invocation is checked or unchecked and use the
// appropriate offset.
ib.local_get(ib.locals[function.type.inputs.length - 1]);
ib.if_(localSignature.inputs, localSignature.outputs);
ib.invoke(translator.directCallTarget(uncheckedTarget));
ib.else_();
ib.invoke(translator.directCallTarget(checkedTarget));
ib.end();
}
} else {
ib.local_get(ib.locals[function.type.inputs.length - 2]);
if (isSubmodule) {
translator.callReference(translator.scopeClassId.reference, ib);
}
ib.comment('Local dispatch table call to "${localSelector.name}"');
final uncheckedOffset = uncheckedTargets.offset;
final checkedOffset = checkedTargets.offset;
if (!localSelector.useMultipleEntryPoints) {
if (checkedOffset != 0) {
ib.i32_const(checkedOffset!);
ib.i32_add();
}
} else if (checkedOffset != 0 || uncheckedOffset != 0) {
// Check if the invocation is checked or unchecked and use the
// appropriate offset.
ib.local_get(ib.locals[function.type.inputs.length - 1]);
ib.if_(const [], const [w.NumType.i32]);
if (uncheckedOffset != null) {
ib.i32_const(uncheckedOffset);
} else {
ib.unreachable();
}
ib.else_();
if (checkedOffset != null) {
ib.i32_const(checkedOffset);
} else {
ib.unreachable();
}
ib.end();
ib.i32_add();
}
final table = translator.dispatchTable.getWasmTable(ib.module);
ib.call_indirect(localSignature, table);
}
translator.convertType(ib, localSignature.outputs.single,
generalizedMainSignature.outputs.single);
ib.end();
};
}
void callOverridableDispatch(
w.InstructionsBuilder b, SelectorInfo selector, Reference interfaceTarget,
{required bool useUncheckedEntry}) {
metadata.invokedOverridableReferences.add(interfaceTarget);
final localSignature = selector.signature;
// If any input is not a RefType (i.e. it's an unboxed value) then wrap it
// so the updated signature works.
if (localSignature.inputs.any((i) => i is! w.RefType)) {
final receiverLocal = b.addLocal(translator.topTypeNonNullable);
b.local_set(receiverLocal);
final locals = <w.Local>[];
for (final input in localSignature.inputs.reversed) {
final local = b.addLocal(input);
locals.add(local);
b.local_set(local);
}
for (final local in locals.reversed) {
b.local_get(local);
translator.convertType(b, local.type, w.RefType.any(nullable: true));
}
b.local_get(receiverLocal);
}
final idLocal = b.addLocal(w.NumType.i32);
b.loadClassId(translator, translator.topTypeNonNullable);
b.local_tee(idLocal);
b.i32_const(useUncheckedEntry ? 1 : 0);
b.local_get(idLocal);
final mainDispatchTable =
translator.dynamicMainModuleDispatchTable ?? translator.dispatchTable;
final mainModuleSelector =
mainDispatchTable.selectorForTarget(interfaceTarget);
final generalizedSignature = _getGeneralizedSignature(mainModuleSelector);
// For consistency, always use the main module selector ID when generating
// the key.
final key = mainModuleSelector.id + BuiltinUpdatableFunctions.values.length;
_callClassIdBranch(key, b, generalizedSignature,
name: '#s${mainModuleSelector.id}_${mainModuleSelector.name}',
buildMainMatch:
buildSelectorBranch(interfaceTarget, mainModuleSelector),
buildSubmoduleMatch:
buildSelectorBranch(interfaceTarget, mainModuleSelector),
skipSubmodule: selector.targets(unchecked: false).targetRanges.isEmpty);
translator.convertType(
b, generalizedSignature.outputs.single, localSignature.outputs.single);
}
}
/// Emits code to canonicalize the provided constant value at runtime.
///
/// This canonicalizer works by generating custom equality functions for any
/// type of constant it encounters. The SDK maintains an array of canonicalized
/// objects separated by type and the equality function generated here is used
/// to identify the canonical version of a constant.
///
/// For example, for a normal Dart Object of type T, we will first construct a
/// new instance of T. Then we will fetch an array containing all instances of T
/// already canonicalized. Using an equality function which does a pairwise
/// comparison of T's fields, we will walk the array looking for an instance
/// that matches the new T. If there is one we return the canonical version,
/// otherwise we add it to the array and return the new T.
///
/// Iterables, wasm arrays and wasm builtin types all require special
/// canonicalization logic.
///
/// Only classes defined in the main module require canonicalization because
/// these are the only classes that can have identical constants instantiated in
/// different submodules. A class defined in a submodule cannot be accessed from
/// a different submodule.
class ConstantCanonicalizer extends ConstantVisitor<void> {
final Translator translator;
final w.InstructionsBuilder b;
/// A local containing the value to be canonicalized.
final w.Local valueLocal;
ConstantCanonicalizer(this.translator, this.b, this.valueLocal);
late final _checkerType = translator.typesBuilder.defineFunction([
translator.topTypeNonNullable,
translator.topTypeNonNullable,
], const [
w.NumType.i32
]);
late final _arrayCheckerType = translator.typesBuilder.defineFunction(const [
w.RefType.array(nullable: false),
w.RefType.array(nullable: false),
], const [
w.NumType.i32
]);
/// Wasm builtin value types that don't need canonicalization.
late final Set<Class> _wasmValueClasses = {
translator.wasmI32Class,
translator.wasmI64Class,
translator.wasmF32Class,
translator.wasmF64Class,
translator.wasmI16Class,
translator.wasmI8Class,
translator.wasmAnyRefClass,
translator.wasmExternRefClass,
translator.wasmI31RefClass,
translator.wasmFuncRefClass,
translator.wasmEqRefClass,
translator.wasmStructRefClass,
translator.wasmArrayRefClass,
};
/// Boxed values are comparable by the value they wrap.
late final Set<Class> _boxedClasses = {
translator.boxedIntClass,
translator.boxedDoubleClass,
translator.boxedBoolClass,
};
/// Values of these types are canonicalized by their == function.
late final Set<Class> _equalsCheckerClasses = {
translator.jsStringClass,
translator.symbolClass,
translator.closureClass,
};
/// These iterable classes contain lazily initialized data that should not be
/// considered in comparisons.
late final Set<Class> _hashingIterableConstClasses = {
translator.immutableSetClass,
translator.immutableMapClass,
};
/// Emit code that canonicalizes the instance of [cls] stored in [valueLocal].
void _canonicalizeInstance(Class cls) {
final classId = translator.classInfo[cls]!.classId;
if (classId is RelativeClassId) {
// This class is not defined in the main module so it doesn't need runtime
// canonicalization.
return;
}
if (_wasmValueClasses.contains(cls)) {
// Wasm value types do not need canonicalization.
return;
}
// Lookup the WasmCache for the value's type.
b.local_set(valueLocal);
b.i64_const((classId as AbsoluteClassId).value);
translator.callReference(translator.constCacheGetter.reference, b);
// Get the equality checker for the class. Import it into the submodule and
// use the import if this is in a submodule.
w.BaseFunction checker = _getCanonicalChecker(cls, b.module);
// Declare the function so it can be used as a ref_func in a constant
// context.
b.module.functions.declare(checker);
// Invoke the 'canonicalize' function with the value and checker.
b.local_get(valueLocal);
b.ref_func(checker);
final valueType = translator.callReference(
translator.constCacheCanonicalize.reference, b);
// The canonicalizer returns an Object which may be a boxed value. Unbox it
// if necessary.
translator.convertType(b, valueType.single, valueLocal.type);
}
void _canonicalizeArray(bool mutable, DartType elementType) {
b.local_set(valueLocal);
final cacheField = (mutable
? translator.wasmArrayConstCache
: translator.immutableWasmArrayConstCache)[elementType];
if (cacheField == null) {
throw StateError(
'Unrecognized const array type (mutable: $mutable): $elementType');
}
translator.callReference(cacheField.getterReference, b);
// Get the equality checker for the class. Import it into the submodule and
// use the import if this is in a submodule.
w.BaseFunction checker = _getCanonicalArrayChecker(
translator.translateStorageType(elementType), mutable, b.module);
// Declare the function so it can be used as a ref_func in a constant
// context.
b.module.functions.declare(checker);
// Invoke the canonicalizer function with the value and checker.
b.local_get(valueLocal);
b.ref_func(checker);
final valueType = translator.callReference(
translator.constCacheArrayCanonicalize.reference, b);
// The canonicalizer returns an array ref, cast it to the correct array
// type.
translator.convertType(b, valueType.single, valueLocal.type);
}
/// Get a function that will compare two instances of [cls] and return true if
/// they canonicalize to the same value.
w.BaseFunction _getCanonicalChecker(Class cls, w.ModuleBuilder module) {
ClassInfo info = translator.classInfo[cls]!;
// We create a checker for each class to ensure we check each struct field.
return translator.dynamicModuleInfo!._constantCacheCheckers
.putIfAbsent(info, () => {})
.putIfAbsent(module, () {
final checker =
module.functions.define(_checkerType, '${info.cls} constCheck');
final b = checker.body;
_checkerForClass(b, info);
b.end();
return checker;
});
}
/// Get a function that will compare two arrays with elements of type
/// [elementType] and return true if they canonicalize to the same value.
w.BaseFunction _getCanonicalArrayChecker(
w.StorageType elementType, bool mutable, w.ModuleBuilder module) {
final cache = mutable
? translator.dynamicModuleInfo!._mutableArrayConstantCacheCheckers
: translator.dynamicModuleInfo!._immutableArrayConstantCacheCheckers;
// We create a checker for each array element type.
return cache.putIfAbsent(elementType, () => {}).putIfAbsent(module, () {
final name = '$elementType';
final checker = module.functions.define(_arrayCheckerType,
'$name const${mutable ? '' : 'Immutable'}ArrayCheck');
final arrayType =
translator.wasmArrayType(elementType, name, mutable: mutable);
final b = checker.body;
_checkerForArray(b, arrayType, elementType);
b.end();
return checker;
});
}
void _checkerForClass(w.InstructionsBuilder b, ClassInfo classInfo) {
final cls = classInfo.cls!;
if (_boxedClasses.contains(cls)) {
return _checkerForBoxedClasses(b, classInfo);
}
final structRef = classInfo.nonNullableType;
b.local_get(b.locals[0]);
b.ref_cast(structRef);
b.local_get(b.locals[1]);
b.ref_cast(structRef);
if (_equalsCheckerClasses.contains(cls)) {
_checkerWithEquals(b);
} else if (_hashingIterableConstClasses.contains(cls)) {
_defaultChecker(b, classInfo, fieldsToInclude: {
FieldIndex.hashBaseData,
...cls.typeParameters.map((t) => translator.typeParameterIndex[t]!)
});
} else {
_defaultChecker(b, classInfo);
}
}
/// Compare boxed entites via the values they wrap.
void _checkerForBoxedClasses(w.InstructionsBuilder b, ClassInfo classInfo) {
final structRef = classInfo.nonNullableType;
b.local_get(b.locals[0]);
b.ref_cast(structRef);
b.struct_get(classInfo.struct, FieldIndex.boxValue);
b.local_get(b.locals[1]);
b.ref_cast(structRef);
b.struct_get(classInfo.struct, FieldIndex.boxValue);
return _equalsForValueType(
b, translator.builtinTypes[classInfo.cls] as w.ValueType);
}
/// Compare values using a dispatch call to Object.==
void _checkerWithEquals(w.InstructionsBuilder b) {
b.local_get(b.locals[0]);
final selector = translator.dispatchTable
.selectorForTarget(translator.coreTypes.objectEquals.reference);
translator.callDispatchTable(b, selector,
interfaceTarget: translator.coreTypes.objectEquals.reference,
useUncheckedEntry: true);
translator.convertType(
b, selector.signature.outputs.first, _checkerType.outputs.first);
}
/// Compare two normal class instances whose const identity are determined by
/// their fields. Do a shallow comparison of the fields assuming the field
/// values are already canonicalized.
void _defaultChecker(w.InstructionsBuilder b, ClassInfo classInfo,
{Set<int>? fieldsToInclude}) {
final structType = classInfo.struct;
final structRefType = classInfo.nonNullableType;
final castedLocal1 = b.addLocal(structRefType);
final castedLocal2 = b.addLocal(structRefType);
b.local_set(castedLocal2);
b.local_set(castedLocal1);
final falseBlock = b.block();
classInfo.forEachClassFieldIndex((index, fieldType) {
if (fieldsToInclude != null && !fieldsToInclude.contains(index)) {
return;
}
b.local_get(castedLocal1);
b.struct_get(structType, index);
b.local_get(castedLocal2);
b.struct_get(structType, index);
final fieldTypeUnpacked = fieldType.type;
_equalsForValueType(b, fieldTypeUnpacked);
b.i32_eqz();
b.br_if(falseBlock);
});
b.i32_const(1);
b.return_();
b.end();
b.i32_const(0);
}
/// Compare two arrays for equality by iterating through the elements and
/// doing a shallow pairwise comparison. Array elements will already be
/// canonicalized. Assumes the types and lengths of the arrays are already
/// equivalent.
void _checkerForArray(w.InstructionsBuilder b, w.ArrayType arrayType,
w.StorageType elementType) {
final arrayRefType = w.RefType(arrayType, nullable: false);
final array1 = b.addLocal(arrayRefType);
final array2 = b.addLocal(arrayRefType);
final falseBlock = b.block();
b.local_get(b.locals[0]);
b.ref_cast(arrayRefType);
b.local_set(array1);
b.local_get(b.locals[1]);
b.ref_cast(arrayRefType);
b.local_set(array2);
b.incrementingLoop(
pushStart: () => b.i32_const(0),
pushLimit: () {
b.local_get(array1);
b.array_len();
},
genBody: (loopLocal) {
b.local_get(array1);
b.local_get(loopLocal);
if (elementType is w.PackedType) {
b.array_get_u(arrayType);
} else {
b.array_get(arrayType);
}
b.local_get(array2);
b.local_get(loopLocal);
if (elementType is w.PackedType) {
b.array_get_u(arrayType);
} else {
b.array_get(arrayType);
}
_equalsForValueType(b, elementType);
b.i32_eqz();
b.br_if(falseBlock);
});
b.i32_const(1);
b.return_();
b.end();
b.i32_const(0);
}
/// Invokes the builtin equality function for [storageType].
static void _equalsForValueType(
w.InstructionsBuilder b, w.StorageType storageType) {
if (storageType is w.RefType) {
b.ref_eq();
} else if (storageType == w.PackedType.i8 ||
storageType == w.PackedType.i16) {
b.i32_eq();
} else if (storageType == w.NumType.f32) {
b.f32_eq();
} else if (storageType == w.NumType.f64) {
b.f64_eq();
} else if (storageType == w.NumType.i32) {
b.i32_eq();
} else if (storageType == w.NumType.i64) {
b.i64_eq();
} else {
throw UnsupportedError('Could not find eq for $storageType');
}
}
@override
Never visitAuxiliaryConstant(AuxiliaryConstant node) {
throw UnsupportedError('Cannot canonicalize auxiliary constants.');
}
@override
void visitBoolConstant(BoolConstant node) {
_canonicalizeInstance(translator.boxedBoolClass);
}
@override
void visitConstructorTearOffConstant(ConstructorTearOffConstant node) {
_canonicalizeInstance(translator.closureClass);
}
@override
void visitDoubleConstant(DoubleConstant node) {
_canonicalizeInstance(translator.boxedDoubleClass);
}
@override
void visitInstanceConstant(InstanceConstant node) {
if (node.classNode == translator.wasmArrayClass) {
final dartElementType = node.typeArguments.single;
_canonicalizeArray(true, dartElementType);
} else if (node.classNode == translator.immutableWasmArrayClass) {
final dartElementType = node.typeArguments.single;
_canonicalizeArray(false, dartElementType);
} else {
_canonicalizeInstance(node.classNode);
}
}
@override
void visitInstantiationConstant(InstantiationConstant node) {
_canonicalizeInstance(translator.closureClass);
}
@override
void visitIntConstant(IntConstant node) {
_canonicalizeInstance(translator.boxedIntClass);
}
@override
void visitListConstant(ListConstant node) {
_canonicalizeInstance(translator.immutableListClass);
}
@override
void visitMapConstant(MapConstant node) {
_canonicalizeInstance(translator.immutableMapClass);
}
@override
void visitNullConstant(NullConstant node) {}
@override
void visitRecordConstant(RecordConstant node) {
_canonicalizeInstance(translator.coreTypes.recordClass);
}
@override
void visitRedirectingFactoryTearOffConstant(
RedirectingFactoryTearOffConstant node) {
_canonicalizeInstance(translator.closureClass);
}
@override
void visitSetConstant(SetConstant node) {
_canonicalizeInstance(translator.immutableSetClass);
}
@override
void visitStaticTearOffConstant(StaticTearOffConstant node) {
_canonicalizeInstance(translator.closureClass);
}
@override
void visitStringConstant(StringConstant node) {
_canonicalizeInstance(translator.jsStringClass);
}
@override
void visitSymbolConstant(SymbolConstant node) {
return _canonicalizeInstance(translator.symbolClass);
}
@override
void visitTypeLiteralConstant(TypeLiteralConstant node) {
_canonicalizeInstance(translator.typeClass);
}
@override
Never visitTypedefTearOffConstant(TypedefTearOffConstant node) {
throw UnsupportedError('Cannot canonicalize typedef tearoff constants.');
}
@override
Never visitUnevaluatedConstant(UnevaluatedConstant node) {
throw UnsupportedError('Cannot canonicalize unevaluated constants.');
}
}
/// Populates [DirectCallMetadata] for a visited component.
class _Devirtualization extends CHADevirtualization {
final CoreTypes coreTypes;
_Devirtualization(
this.coreTypes,
Component component,
ClosedWorldClassHierarchy hierarchy,
ClassHierarchySubtypes hierarchySubtype)
: super(coreTypes, component, hierarchy, hierarchySubtype);
@override
void makeDirectCall(
TreeNode node, Member? target, DirectCallMetadata directCall) {
if (target != null && target.isDynamicSubmoduleOverridable(coreTypes)) {
return;
}
super.makeDirectCall(node, target, directCall);
}
}