From a4a4ca8a412262c0b47c31fbd4235af309ddec00 Mon Sep 17 00:00:00 2001 From: Nate Biggs Date: Tue, 11 Feb 2025 13:59:46 -0800 Subject: [PATCH] [dart2wasm] Dynamic modules Missing from this implementation: - Closure/dynamic calls with differing signatures - Overrides with extra optional parameters - Records with same shape defined in different dynamic modules - Avoiding running TFA on dynamic module. - Recompilation of only updateable functions from main module. - Persist wasm def types from main module. Testing is currently done locally via the dynamic_modules package test suite: dart pkg/dynamic_modules/test/runner/main.dart --runtime=dart2wasm Immediately after this lands we can introduce a new step to one of the wasm test matrix configurations that runs the above test suite (the VM has a similar configuration). Change-Id: I3386d84be11b773842d45f4268a62a54c47e352b Tested: Tested via new tests in dynamic_modules package. Tests run locally but will add to existing config. Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/397721 Reviewed-by: Martin Kustermann Commit-Queue: Nate Biggs --- pkg/dart2wasm/bin/run_wasm.js | 3 + pkg/dart2wasm/lib/class_info.dart | 283 +++- pkg/dart2wasm/lib/closures.dart | 4 +- pkg/dart2wasm/lib/code_generator.dart | 177 ++- pkg/dart2wasm/lib/compile.dart | 115 +- pkg/dart2wasm/lib/constant_evaluator.dart | 12 +- pkg/dart2wasm/lib/constants.dart | 125 +- pkg/dart2wasm/lib/deferred_loading.dart | 304 ++--- pkg/dart2wasm/lib/dispatch_table.dart | 492 ++++--- pkg/dart2wasm/lib/dynamic_forwarders.dart | 27 +- .../lib/dynamic_module_kernel_metadata.dart | 256 ++++ pkg/dart2wasm/lib/dynamic_modules.dart | 1163 +++++++++++++++++ pkg/dart2wasm/lib/functions.dart | 192 ++- pkg/dart2wasm/lib/generate_wasm.dart | 5 +- pkg/dart2wasm/lib/globals.dart | 3 +- pkg/dart2wasm/lib/intrinsics.dart | 121 +- pkg/dart2wasm/lib/js/runtime_blob.dart | 39 +- pkg/dart2wasm/lib/kernel_nodes.dart | 81 ++ pkg/dart2wasm/lib/modules.dart | 170 +++ pkg/dart2wasm/lib/option.dart | 4 +- pkg/dart2wasm/lib/record_class_generator.dart | 13 + pkg/dart2wasm/lib/serialization.dart | 138 ++ pkg/dart2wasm/lib/sync_star.dart | 2 +- pkg/dart2wasm/lib/translator.dart | 220 +++- pkg/dart2wasm/lib/types.dart | 118 +- pkg/dynamic_modules/test/common/testing.dart | 3 +- .../closure_invocation/dynamic_interface.yaml | 16 + .../test/data/closure_invocation/main.dart | 14 + .../closure_invocation/modules/entry1.dart | 18 + .../closure_invocation/shared/shared.dart | 5 + .../dynamic_interface.yaml | 34 + .../data/dyn_module_type_checks/main.dart | 19 + .../modules/entry1.dart | 25 + .../modules/entry2.dart | 25 + .../dyn_module_type_checks/shared/shared.dart | 9 + .../dynamic_interface.yaml | 23 + .../test/data/extend_class_dyn_only/main.dart | 14 + .../extend_class_dyn_only/modules/entry1.dart | 30 + .../dynamic_interface.yaml | 27 + .../test/data/extend_class_generics/main.dart | 20 + .../extend_class_generics/modules/entry1.dart | 16 + .../extend_class_generics/shared/shared.dart | 9 + .../multiple_classes/dynamic_interface.yaml | 5 + .../test/data/multiple_classes/main.dart | 2 - .../dynamic_interface.yaml | 29 + .../test/data/override_extra_params/main.dart | 16 + .../override_extra_params/modules/entry1.dart | 13 + .../override_extra_params/shared/shared.dart | 7 + .../reshape_selectors/dynamic_interface.yaml | 29 + .../test/data/reshape_selectors/main.dart | 17 + .../reshape_selectors/modules/entry1.dart | 13 + .../data/reshape_selectors/shared/shared.dart | 11 + .../same_record_shape/dynamic_interface.yaml | 14 + .../test/data/same_record_shape/main.dart | 17 + .../same_record_shape/modules/entry1.dart | 6 + .../same_record_shape/modules/entry2.dart | 6 + .../dynamic_interface.yaml | 27 + .../data/tearoff_no_concrete_impl/main.dart | 15 + .../modules/entry1.dart | 15 + .../shared/shared.dart | 18 + .../lib/src/kernel/constant_evaluator.dart | 6 +- pkg/kernel/lib/kernel.dart | 6 +- .../transformations/mixin_deduplication.dart | 12 +- pkg/vm/lib/transformations/pragma.dart | 47 +- .../unreachable_code_elimination.dart | 9 + .../lib/src/builder/functions.dart | 1 + pkg/wasm_builder/lib/src/builder/table.dart | 1 + sdk/lib/_internal/wasm/lib/class_id.dart | 69 + sdk/lib/_internal/wasm/lib/core_patch.dart | 6 + .../_internal/wasm/lib/dynamic_module.dart | 199 +++ .../_internal/wasm/lib/internal_patch.dart | 25 +- sdk/lib/_internal/wasm/lib/type.dart | 148 ++- 72 files changed, 4367 insertions(+), 796 deletions(-) create mode 100644 pkg/dart2wasm/lib/dynamic_module_kernel_metadata.dart create mode 100644 pkg/dart2wasm/lib/dynamic_modules.dart create mode 100644 pkg/dart2wasm/lib/modules.dart create mode 100644 pkg/dart2wasm/lib/serialization.dart create mode 100644 pkg/dynamic_modules/test/data/closure_invocation/dynamic_interface.yaml create mode 100644 pkg/dynamic_modules/test/data/closure_invocation/main.dart create mode 100644 pkg/dynamic_modules/test/data/closure_invocation/modules/entry1.dart create mode 100644 pkg/dynamic_modules/test/data/closure_invocation/shared/shared.dart create mode 100644 pkg/dynamic_modules/test/data/dyn_module_type_checks/dynamic_interface.yaml create mode 100644 pkg/dynamic_modules/test/data/dyn_module_type_checks/main.dart create mode 100644 pkg/dynamic_modules/test/data/dyn_module_type_checks/modules/entry1.dart create mode 100644 pkg/dynamic_modules/test/data/dyn_module_type_checks/modules/entry2.dart create mode 100644 pkg/dynamic_modules/test/data/dyn_module_type_checks/shared/shared.dart create mode 100644 pkg/dynamic_modules/test/data/extend_class_dyn_only/dynamic_interface.yaml create mode 100644 pkg/dynamic_modules/test/data/extend_class_dyn_only/main.dart create mode 100644 pkg/dynamic_modules/test/data/extend_class_dyn_only/modules/entry1.dart create mode 100644 pkg/dynamic_modules/test/data/extend_class_generics/dynamic_interface.yaml create mode 100644 pkg/dynamic_modules/test/data/extend_class_generics/main.dart create mode 100644 pkg/dynamic_modules/test/data/extend_class_generics/modules/entry1.dart create mode 100644 pkg/dynamic_modules/test/data/extend_class_generics/shared/shared.dart create mode 100644 pkg/dynamic_modules/test/data/override_extra_params/dynamic_interface.yaml create mode 100644 pkg/dynamic_modules/test/data/override_extra_params/main.dart create mode 100644 pkg/dynamic_modules/test/data/override_extra_params/modules/entry1.dart create mode 100644 pkg/dynamic_modules/test/data/override_extra_params/shared/shared.dart create mode 100644 pkg/dynamic_modules/test/data/reshape_selectors/dynamic_interface.yaml create mode 100644 pkg/dynamic_modules/test/data/reshape_selectors/main.dart create mode 100644 pkg/dynamic_modules/test/data/reshape_selectors/modules/entry1.dart create mode 100644 pkg/dynamic_modules/test/data/reshape_selectors/shared/shared.dart create mode 100644 pkg/dynamic_modules/test/data/same_record_shape/dynamic_interface.yaml create mode 100644 pkg/dynamic_modules/test/data/same_record_shape/main.dart create mode 100644 pkg/dynamic_modules/test/data/same_record_shape/modules/entry1.dart create mode 100644 pkg/dynamic_modules/test/data/same_record_shape/modules/entry2.dart create mode 100644 pkg/dynamic_modules/test/data/tearoff_no_concrete_impl/dynamic_interface.yaml create mode 100644 pkg/dynamic_modules/test/data/tearoff_no_concrete_impl/main.dart create mode 100644 pkg/dynamic_modules/test/data/tearoff_no_concrete_impl/modules/entry1.dart create mode 100644 pkg/dynamic_modules/test/data/tearoff_no_concrete_impl/shared/shared.dart create mode 100644 sdk/lib/_internal/wasm/lib/dynamic_module.dart diff --git a/pkg/dart2wasm/bin/run_wasm.js b/pkg/dart2wasm/bin/run_wasm.js index 59049559d01..46868b3341d 100644 --- a/pkg/dart2wasm/bin/run_wasm.js +++ b/pkg/dart2wasm/bin/run_wasm.js @@ -416,6 +416,9 @@ const main = async () => { loadDeferredWasm: async (moduleName) => { let filename = wasmFilename.replace('.wasm', `_${moduleName}.wasm`); return readBytes(filename); + }, + loadDynamicModule: async (uri) => { + return readBytes(uri); } }); diff --git a/pkg/dart2wasm/lib/class_info.dart b/pkg/dart2wasm/lib/class_info.dart index 5dabc82e364..a3b8841bf50 100644 --- a/pkg/dart2wasm/lib/class_info.dart +++ b/pkg/dart2wasm/lib/class_info.dart @@ -7,6 +7,7 @@ import 'dart:math'; import 'package:kernel/ast.dart'; import 'package:wasm_builder/wasm_builder.dart' as w; +import 'dynamic_modules.dart'; import 'translator.dart'; /// Wasm struct field indices for fields that are accessed explicitly from Wasm @@ -43,6 +44,7 @@ class FieldIndex { static const instantiationContextInner = 0; static const instantiationContextTypeArgumentsBase = 1; static const typeIsDeclaredNullable = 2; + static const interfaceTypeClassId = 3; static const interfaceTypeTypeArguments = 4; static const functionTypeNamedParameters = 9; static const recordTypeNames = 3; @@ -154,14 +156,14 @@ class ClassInfo { final Class? cls; /// The Class ID of this class, stored in every instance of the class. - int get classId { - if (_classId == anonymousMixinClassId) { + ClassId get classId { + if (_classId._localValue == anonymousMixinClassId) { throw 'Tried to access class ID of anonymous mixin $cls'; } return _classId; } - final int _classId; + final ClassId _classId; /// Depth of this class in the Wasm type hierarchy. final int depth; @@ -216,6 +218,12 @@ class ClassInfo { for (var fieldType in struct.fields.skip(FieldIndex.objectFieldBase)) fieldType.type.unpacked ]; + + void forEachClassFieldIndex(void Function(int index, w.FieldType type) f) { + for (int i = FieldIndex.objectFieldBase; i < struct.fields.length; i++) { + f(i, struct.fields[i]); + } + } } ClassInfo upperBound(ClassInfo a, ClassInfo b) { @@ -301,11 +309,11 @@ class ClassInfoCollector { void _createStructForClassTop(int classCount) { final w.StructType struct = translator.typesBuilder.defineStruct("#Top"); - topInfo = ClassInfo(null, 0, 0, struct, null); + topInfo = ClassInfo(null, AbsoluteClassId(0), 0, struct, null); translator.classForHeapType[struct] = topInfo; } - void _createStructForClass(Map classIds, Class cls) { + void _createStructForClass(Map classIds, Class cls) { ClassInfo? info = translator.classInfo[cls]; if (info != null) return; @@ -364,16 +372,22 @@ class ClassInfoCollector { .defineStruct(cls.name, superType: superInfo.struct); info = ClassInfo(cls, classId, superInfo.depth + 1, struct, superInfo, typeParameterMatch: typeParameterMatch); + if (translator.dynamicModuleSupportEnabled && + cls.isDynamicModuleExtendable(translator.coreTypes)) { + // If a class is extendable in a dynamic module then we have to be + // conservative and mark it as not being final. + struct.hasAnySubtypes = true; + } } translator.classesSupersFirst.add(info); translator.classInfo[cls] = info; translator.classForHeapType.putIfAbsent(info.struct, () => info!); - if (classId != anonymousMixinClassId) { - translator.classes[classId] = info; + if (classId._localValue != anonymousMixinClassId) { + translator.classes[classId._localValue] = info; } } - void _createStructForRecordClass(Map classIds, Class cls) { + void _createStructForRecordClass(Map classIds, Class cls) { final numFields = cls.fields.length; final struct = _recordStructs.putIfAbsent( @@ -390,7 +404,7 @@ class ClassInfoCollector { ClassInfo(cls, classId, superInfo.depth + 1, struct, superInfo); translator.classesSupersFirst.add(info); - translator.classes[classId] = info; + translator.classes[classId._localValue] = info; translator.classInfo[cls] = info; translator.classForHeapType.putIfAbsent(info.struct, () => info); } @@ -471,8 +485,11 @@ class ClassInfoCollector { // Class infos by class-id, will be populated by the calls to // [_createStructForClass] and [_createStructForRecordClass] below. - translator.classes = - List.filled(classIdNumbering.maxClassId + 1, topInfo); + translator.classes = List.filled( + (classIdNumbering.maxDynamicModuleClassId ?? + classIdNumbering.maxClassId) + + 1, + topInfo); // Class infos in different order: Infos of super class and super interfaces // before own info. @@ -504,15 +521,43 @@ class ClassInfoCollector { // represent objects of that dart type). for (final cls in dfsOrder) { ClassInfo? representation; - for (final range in classIdNumbering.getConcreteClassIdRanges(cls)) { - for (int classId = range.start; classId <= range.end; ++classId) { - final current = translator.classes[classId]; - if (representation == null) { - representation = current; - continue; + if (translator.dynamicModuleSupportEnabled && + cls.isDynamicModuleExtendable(translator.coreTypes)) { + assert(!translator.builtinTypes.containsKey(cls)); + + // If a class is extendable in a dynamic module then we have to be + // conservative and assume it might be a subclass of Object. The Object + // class maps to topInfo because boxed values are a subtype of Object in + // Dart but not of the object struct. + representation = cls == translator.coreTypes.objectClass + ? translator.topInfo + : translator.objectInfo; + } else { + void addRanges(List ranges) { + for (final range in ranges) { + for (int classId = range.start; classId <= range.end; ++classId) { + final current = translator.classes[classId]; + if (representation == null) { + representation = current; + continue; + } + representation = upperBound(representation!, current); + } } - representation = upperBound(representation, current); } + + final mainModuleConcreteRange = + classIdNumbering.getConcreteClassIdRangeForMainModule(cls); + final dynamicModuleConcreteRange = + classIdNumbering.getConcreteClassIdRangeForDynamicModule(cls); + + // Only non-extendable classes can get here so they should only have + // concrete implementations in either the main module or the dynamic + // module, not both. + assert(mainModuleConcreteRange.isEmpty || + dynamicModuleConcreteRange.isEmpty); + addRanges(mainModuleConcreteRange); + addRanges(dynamicModuleConcreteRange); } final info = translator.classInfo[cls]!; info._repr = representation ?? info; @@ -537,25 +582,35 @@ class ClassInfoCollector { } class ClassIdNumbering { + final Translator translator; final Map> _subclasses; final Map> _implementors; - final Map _concreteSubclassIdRange; + final Map> _concreteSubclassIdRange; + final Map> _concreteSubclassIdRangeForDynamicModule; final Set _masqueraded; final List dfsOrder; - final Map classIds; + final Map classIds; final int maxConcreteClassId; final int maxClassId; + final int? maxDynamicModuleConcreteClassId; + final int? maxDynamicModuleClassId; + + int get firstDynamicModuleClassId => maxClassId + 1; ClassIdNumbering._( + this.translator, this._subclasses, this._implementors, this._concreteSubclassIdRange, + this._concreteSubclassIdRangeForDynamicModule, this._masqueraded, this.dfsOrder, this.classIds, this.maxConcreteClassId, - this.maxClassId); + this.maxClassId, + this.maxDynamicModuleConcreteClassId, + this.maxDynamicModuleClassId); final Map> _transitiveImplementors = {}; Set _getTransitiveImplementors(Class klass) { @@ -581,30 +636,58 @@ class ClassIdNumbering { return _transitiveImplementors[klass] = transitiveImplementors; } - // Maps a class to a list of class id ranges that implement/extend the given - // class directly or transitively. + /// Maps a class to a list of class id ranges that implement/extend the given + /// class directly or transitively. + /// + /// If this function is invoked from a dynamic module enabled build then it + /// should be wrapped with [DynamicModuleInfo.callClassIdBranch] so that the + /// checked range will be updated. final Map> _concreteClassIdRanges = {}; - List getConcreteClassIdRanges(Class klass) { - var ranges = _concreteClassIdRanges[klass]; + List getConcreteClassIdRangeForMainModule(Class klass) { + return _getConcreteClassIdRange( + klass, _concreteClassIdRanges, _concreteSubclassIdRange); + } + + final Map> _concreteClassIdRangesForDynamicModule = {}; + List getConcreteClassIdRangeForDynamicModule(Class klass) { + return _getConcreteClassIdRange( + klass, + _concreteClassIdRangesForDynamicModule, + _concreteSubclassIdRangeForDynamicModule); + } + + List getConcreteClassIdRangeForCurrentModule(Class klass) { + return translator.isDynamicModule + ? getConcreteClassIdRangeForDynamicModule(klass) + : getConcreteClassIdRangeForMainModule(klass); + } + + List _getConcreteClassIdRange(Class klass, + Map> cache, Map> subclasses) { + var ranges = cache[klass]; if (ranges != null) return ranges; ranges = []; final transitiveImplementors = _getTransitiveImplementors(klass); - final range = _concreteSubclassIdRange[klass]!; - if (!range.isEmpty) ranges.add(range); + final subclassRanges = subclasses[klass] ?? const []; + for (final range in subclassRanges) { + ranges.add(range); + } for (final implementor in transitiveImplementors) { - final range = _concreteSubclassIdRange[implementor]!; - if (!range.isEmpty) ranges.add(range); + final implementorRanges = subclasses[implementor] ?? const []; + for (final range in implementorRanges) { + ranges.add(range); + } } ranges.normalize(); - return _concreteClassIdRanges[klass] = ranges; + return cache[klass] = ranges; } late final int firstNonMasqueradedInterfaceClassCid = (() { int lastMasqueradedClassId = 0; for (final cls in _masqueraded) { - final ranges = getConcreteClassIdRanges(cls); + final ranges = getConcreteClassIdRangeForMainModule(cls); if (ranges.isNotEmpty) { lastMasqueradedClassId = max(lastMasqueradedClassId, ranges.last.end); } @@ -616,20 +699,40 @@ class ClassIdNumbering { Translator translator, Set masqueraded, int firstClassId) { // Make graph from class to its subclasses. late final Class root; + int? savedMaxConcreteClassId; + int? savedMaxClassId; final subclasses = >{}; final implementors = >{}; + final classIds = {}; + + final savedMapping = translator.dynamicModuleInfo?.classIdMapping; + if (savedMapping != null) { + savedMapping.forEach((cls, classId) { + classIds[cls] = AbsoluteClassId(classId); + savedMaxClassId = max(savedMaxClassId ?? -2, classId); + if (!cls.isAbstract && !cls.isAnonymousMixin) { + savedMaxConcreteClassId = max(savedMaxConcreteClassId ?? -2, classId); + } + }); + } + int concreteClassCount = 0; int abstractClassCount = 0; int anonymousMixinClassCount = 0; + int alreadyAssignedCount = 0; for (final library in translator.component.libraries) { for (final cls in library.classes) { - if (cls.isAnonymousMixin) { - assert(cls.isAbstract); - anonymousMixinClassCount++; - } else if (cls.isAbstract) { - abstractClassCount++; + if (!classIds.containsKey(cls)) { + if (cls.isAnonymousMixin) { + assert(cls.isAbstract); + anonymousMixinClassCount++; + } else if (cls.isAbstract) { + abstractClassCount++; + } else { + concreteClassCount++; + } } else { - concreteClassCount++; + alreadyAssignedCount++; } final superClass = cls.superclass; if (superClass == null) { @@ -700,51 +803,125 @@ class ClassIdNumbering { } // Make a list of the depth-first pre-order traversal. - final dfsOrder = []; - final classIds = {}; + final dfsOrder = + translator.dynamicModuleInfo?.dfsOrderClassIds ?? []; + final inDfsOrder = {...dfsOrder}; // Maps any class to a dense range of concrete class ids that are subclasses // of that class. - final concreteSubclassRange = {}; + final concreteSubclassRanges = >{}; + final concreteSubclassRangesForDynamicModule = >{}; + + int nextConcreteClassId = (savedMaxClassId ?? (firstClassId - 1)) + 1; + int nextAbstractClassId = nextConcreteClassId + concreteClassCount; + + if (classIds.isNotEmpty) { + // Assumes that saved IDs form a contiguous region at the top of the + // subclass tree. So if we encounter a node without a saved ID, then we do + // not need to explore its children for saved IDs. + Range? addSavedRanges(Class cls) { + final savedClassId = classIds[cls]; + if (savedClassId == null) return null; + final children = subclasses[cls] ?? const []; + final isConcrete = !cls.isAbstract && !cls.isAnonymousMixin; + Range? savedRange = isConcrete + ? Range(savedClassId._localValue, savedClassId._localValue) + : null; + for (final child in children) { + final childRange = addSavedRanges(child); + if (childRange != null) { + savedRange = savedRange == null + ? Range(childRange.start, childRange.end) + : Range(savedRange.start, max(savedRange.end, childRange.end)); + } + } + if (savedRange != null) { + (concreteSubclassRanges[cls] ??= []).add(savedRange); + } + return savedRange; + } + + addSavedRanges(root); + } + + final subclassesRangesToBuild = savedMaxClassId != null + ? concreteSubclassRangesForDynamicModule + : concreteSubclassRanges; - int nextConcreteClassId = firstClassId; - int nextAbstractClassId = firstClassId + concreteClassCount; dfs(root, (Class cls) { - dfsOrder.add(cls); + if (!inDfsOrder.contains(cls)) { + dfsOrder.add(cls); + } + if (classIds.containsKey(cls)) return nextConcreteClassId; if (cls.isAnonymousMixin) { - classIds[cls] = anonymousMixinClassId; + classIds[cls] = AbsoluteClassId(anonymousMixinClassId); return nextConcreteClassId; } if (cls.isAbstract) { var classId = classIds[cls]; - if (classId == null) classIds[cls] = nextAbstractClassId++; + if (classId == null) { + classIds[cls] = AbsoluteClassId(nextAbstractClassId++); + } return nextConcreteClassId; } assert(classIds[cls] == null); - classIds[cls] = nextConcreteClassId++; + final classId = nextConcreteClassId++; + classIds[cls] = savedMaxClassId != null + ? RelativeClassId(classId) + : AbsoluteClassId(classId); return nextConcreteClassId - 1; }, (Class cls, int firstClassId) { final range = Range(firstClassId, nextConcreteClassId - 1); - concreteSubclassRange[cls] = range; + if (!range.isEmpty) { + (subclassesRangesToBuild[cls] ??= []).add(range); + } }); assert(dfsOrder.length == - (concreteClassCount + abstractClassCount + anonymousMixinClassCount)); + (concreteClassCount + + abstractClassCount + + anonymousMixinClassCount + + alreadyAssignedCount)); return ClassIdNumbering._( + translator, subclasses, implementors, - concreteSubclassRange, + concreteSubclassRanges, + concreteSubclassRangesForDynamicModule, masqueraded, dfsOrder, classIds, - firstClassId + concreteClassCount - 1, - firstClassId + concreteClassCount + abstractClassCount - 1); + savedMaxConcreteClassId ?? nextConcreteClassId - 1, + savedMaxClassId ?? nextAbstractClassId - 1, + savedMaxConcreteClassId == null ? null : nextConcreteClassId - 1, + savedMaxClassId == null ? null : nextAbstractClassId - 1); } - Range getConcreteSubclassRange(Class klass) => - _concreteSubclassIdRange[klass]!; + List getConcreteSubclassRanges(Class klass) => + _concreteSubclassIdRange[klass] ?? const []; +} + +sealed class ClassId { + int get _localValue; +} + +final class AbsoluteClassId extends ClassId { + final int value; + + @override + int get _localValue => value; + + AbsoluteClassId(this.value); +} + +final class RelativeClassId extends ClassId { + final int relativeValue; + @override + int get _localValue => relativeValue; + + RelativeClassId(this.relativeValue); } // A range of class ids, both ends inclusive. diff --git a/pkg/dart2wasm/lib/closures.dart b/pkg/dart2wasm/lib/closures.dart index ecb5221fa5b..0f069c42de8 100644 --- a/pkg/dart2wasm/lib/closures.dart +++ b/pkg/dart2wasm/lib/closures.dart @@ -414,6 +414,8 @@ class ClosureLayouter extends RecursiveVisitor { ClosureRepresentation? parent, Map? indexOfCombination, Iterable paramCounts) { + // TODO(natebiggs): Add logic to allow for changing signatures in a dynamic + // module. List nameTags = ["$typeCount", "$positionalCount", ...names]; String vtableName = ["#Vtable", ...nameTags].join("-"); String closureName = ["#Closure", ...nameTags].join("-"); @@ -723,7 +725,7 @@ class ClosureLayouter extends RecursiveVisitor { w.Local typeParam(int i) => instantiationFunction.locals[1 + i]; // Header for the closure struct - b.pushObjectHeaderFields(translator.closureInfo); + b.pushObjectHeaderFields(translator, translator.closureInfo); // Context for the instantiated closure, containing the original closure and // the type arguments diff --git a/pkg/dart2wasm/lib/code_generator.dart b/pkg/dart2wasm/lib/code_generator.dart index a09765e1425..d4fed4634a8 100644 --- a/pkg/dart2wasm/lib/code_generator.dart +++ b/pkg/dart2wasm/lib/code_generator.dart @@ -1542,9 +1542,15 @@ abstract class AstCodeGenerator } Member _lookupSuperTarget(Member interfaceTarget, {required bool setter}) { - return translator.hierarchy.getDispatchTarget( + final staticTarget = translator.hierarchy.getDispatchTarget( enclosingMember.enclosingClass!.superclass!, interfaceTarget.name, - setter: setter)!; + setter: setter); + if (staticTarget != null) return staticTarget; + + // During dynamic module compilation a mixin might include a super call to + // an abstract class with no implementations yet. + assert(translator.dynamicModuleSupportEnabled); + return interfaceTarget; } @override @@ -1868,33 +1874,46 @@ abstract class AstCodeGenerator pushReceiver(selector.signature); - final targets = selector.targets(unchecked: useUncheckedEntry); + SelectorTargets targets; - if (targets.targetRanges.length == 1) { - // TODO(natebiggs): Ensure dynamic modules exclude this. - - assert(targets.staticDispatchRanges.length == 1); - final target = targets.targetRanges.single.target; - final signature = translator.signatureForDirectCall(target); - final paramInfo = translator.paramInfoForDirectCall(target); - pushArguments(signature, paramInfo); - return translator.outputOrVoid(call(target)); + if (!translator.dynamicModuleSupportEnabled || + b.module == translator.mainModule) { + targets = + selector.targets(unchecked: useUncheckedEntry, dynamicModule: false); + } else { + targets = + selector.targets(unchecked: useUncheckedEntry, dynamicModule: true); } - if (targets.targetRanges.isEmpty) { - // TODO(natebiggs): Ensure dynamic modules exclude this. - - // Unreachable call - b.comment("Virtual call of $name with no targets" - " at ${node.location}"); - pushArguments(selector.signature, selector.paramInfo); - for (int i = 0; i < selector.signature.inputs.length; ++i) { - b.drop(); + final isDynamicModuleOverrideable = selector.isDynamicModuleOverrideable; + if (!isDynamicModuleOverrideable) { + w.ValueType? checkRanges(List<({Range range, Reference target})> ranges) { + if (ranges.length == 1) { + final target = translator.getFunctionEntry(ranges[0].target, + uncheckedEntry: useUncheckedEntry); + final signature = translator.signatureForDirectCall(target); + final paramInfo = translator.paramInfoForDirectCall(target); + pushArguments(signature, paramInfo); + return translator.outputOrVoid(call(target)); + } + if (ranges.isEmpty) { + // Unreachable call + b.comment("Virtual call of $name with no targets" + " at ${node.location}"); + pushArguments(selector.signature, selector.paramInfo); + for (int i = 0; i < selector.signature.inputs.length; ++i) { + b.drop(); + } + b.block(const [], selector.signature.outputs); + b.unreachable(); + b.end(); + return translator.outputOrVoid(selector.signature.outputs); + } + return null; } - b.block(const [], selector.signature.outputs); - b.unreachable(); - b.end(); - return translator.outputOrVoid(selector.signature.outputs); + + final result = checkRanges(targets.targetRanges); + if (result != null) return result; } // Receiver is already on stack. @@ -1904,8 +1923,7 @@ abstract class AstCodeGenerator pushArguments(selector.signature, selector.paramInfo); if (targets.staticDispatchRanges.isNotEmpty) { - // TODO(natebiggs): Ensure dynamic modules exclude this. - + assert(!translator.dynamicModuleSupportEnabled); b.invoke(translator .getPolymorphicDispatchersForModule(b.module) .getPolymorphicDispatcher(selector, @@ -1914,6 +1932,7 @@ abstract class AstCodeGenerator b.comment("Instance $kind of '$name'"); b.local_get(receiverVar); translator.callDispatchTable(b, selector, + interfaceTarget: interfaceTarget, useUncheckedEntry: useUncheckedEntry); } @@ -2020,7 +2039,7 @@ abstract class AstCodeGenerator if (target is Procedure && !target.isGetter) { // Super tear-off w.StructType closureStruct = _pushClosure( - translator.getTearOffClosure(target), + translator.getTearOffClosure(target, b.module), translator.getTearOffType(target), () => visitThis(w.RefType.struct(nullable: false))); return w.RefType.def(closureStruct, nullable: false); @@ -2321,6 +2340,7 @@ abstract class AstCodeGenerator ClosureImplementation closure = translator.getClosure( functionNode, lambda.function, + b.module, ParameterInfo.fromLocalFunction(functionNode), "closure wrapper at ${functionNode.location}"); return _pushClosure( @@ -2336,7 +2356,7 @@ abstract class AstCodeGenerator ClassInfo info = translator.closureInfo; translator.functions.recordClassAllocation(info.classId); - b.pushObjectHeaderFields(info); + b.pushObjectHeaderFields(translator, info); pushContext(); translator.globals.readGlobal(b, closure.vtable); types.makeType(this, functionType); @@ -2947,7 +2967,7 @@ abstract class AstCodeGenerator translator.getRecordClassInfo(node.recordType); translator.functions.recordClassAllocation(recordClassInfo.classId); - b.pushObjectHeaderFields(recordClassInfo); + b.pushObjectHeaderFields(translator, recordClassInfo); for (Expression positional in node.positional) { translateExpression(positional, translator.topInfo.nullableType); } @@ -3351,13 +3371,14 @@ class TearOffCodeGenerator extends AstCodeGenerator { _initializeThis(member.reference); Procedure procedure = member as Procedure; DartType functionType = translator.getTearOffType(procedure); - ClosureImplementation closure = translator.getTearOffClosure(procedure); + ClosureImplementation closure = + translator.getTearOffClosure(procedure, b.module); w.StructType struct = closure.representation.closureStruct; ClassInfo info = translator.closureInfo; translator.functions.recordClassAllocation(info.classId); - b.pushObjectHeaderFields(info); + b.pushObjectHeaderFields(translator, info); b.local_get(paramLocals[0]); // `this` as context // The closure requires a struct value so box `this` if necessary. translator.convertType(b, paramLocals[0].type, @@ -3837,7 +3858,7 @@ class ConstructorAllocatorCodeGenerator extends AstCodeGenerator { } // Set field values - b.pushObjectHeaderFields(info); + b.pushObjectHeaderFields(translator, info); for (w.Local local in orderedFieldLocals.reversed) { b.local_get(local); @@ -4400,6 +4421,80 @@ extension MacroAssembler on w.InstructionsBuilder { return outputs; } + void incrementingLoop( + {required void Function() pushStart, + required void Function() pushLimit, + required void Function(w.Local) genBody, + int step = 1}) { + final endLoop = block(); + final limitVar = addLocal(w.NumType.i32); + final loopVar = addLocal(w.NumType.i32); + pushLimit(); + local_set(limitVar); + pushStart(); + local_set(loopVar); + + final loopLabel = loop(); + local_get(loopVar); + local_get(limitVar); + i32_ge_u(); + br_if(endLoop); + + genBody(loopVar); + local_get(loopVar); + i32_const(step); + i32_add(); + local_set(loopVar); + br(loopLabel); + end(); + end(); + } + + /// [ref Array] [ref Array] -> [ref Array] + /// + /// Takes the two arrays on the stack and concatenates them into a single + /// array. They both must have the same type provided as [arrayRefType]. + /// Uses [pushDefaultElement] as the filler element that holds space in the + /// array until values are copied over. + void concatenateWasmArrays(w.ArrayType arrayType, + {required void Function( + w.InstructionsBuilder b, w.Local oldArray, w.Local newArray) + pushDefaultElement}) { + final arrayRefType = w.RefType(arrayType, nullable: false); + final newArray = addLocal(arrayRefType); + final oldArray = addLocal(arrayRefType); + final newArrayLen = addLocal(w.NumType.i32); + final oldArrayLen = addLocal(w.NumType.i32); + final joinedArray = addLocal(arrayRefType); + + local_set(newArray); + local_set(oldArray); + pushDefaultElement(this, oldArray, newArray); + local_get(newArray); + array_len(); + local_set(newArrayLen); + local_get(oldArray); + array_len(); + local_tee(oldArrayLen); + local_get(newArrayLen); + i32_add(); + array_new(arrayType); + local_tee(joinedArray); + i32_const(0); + local_get(oldArray); + i32_const(0); + local_get(oldArrayLen); + array_copy(arrayType, arrayType); + local_get(joinedArray); + local_get(oldArrayLen); + local_get(newArray); + i32_const(0); + local_get(newArrayLen); + array_copy(arrayType, arrayType); + local_get(joinedArray); + end(); + } + /// `[i32] -> [i32]` /// /// Consumes a `i32` class ID, leaves an `i32` as `bool` for whether @@ -4733,11 +4828,21 @@ extension MacroAssembler on w.InstructionsBuilder { } /// Pushes fields common to all Dart objects (class id, id hash). - void pushObjectHeaderFields(ClassInfo classInfo) { - // TODO(natebiggs): Adjust class ID for dynamic module if appropriate. - i32_const(classInfo.classId); + void pushObjectHeaderFields(Translator translator, ClassInfo classInfo) { + pushClassIdToStack(translator, classInfo.classId); i32_const(initialIdentityHash); } + + void pushClassIdToStack(Translator translator, ClassId classId) { + switch (classId) { + case AbsoluteClassId(): + i32_const(classId.value); + case RelativeClassId(): + i32_const(classId.relativeValue); + translator.pushModuleId(this); + translator.callReference(translator.globalizeClassId.reference, this); + } + } } /// A call target that may be called with a direct call or may be inlined. diff --git a/pkg/dart2wasm/lib/compile.dart b/pkg/dart2wasm/lib/compile.dart index b3dfa7f9a0f..55e92da4ebb 100644 --- a/pkg/dart2wasm/lib/compile.dart +++ b/pkg/dart2wasm/lib/compile.dart @@ -2,10 +2,11 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:io' show File; import 'dart:typed_data'; import 'package:build_integration/file_system/multi_root.dart' - show MultiRootFileSystem; + show MultiRootFileSystem, MultiRootFileSystemEntity; import 'package:front_end/src/api_prototype/standard_file_system.dart' show StandardFileSystem; import 'package:front_end/src/api_unstable/vm.dart' @@ -19,12 +20,14 @@ import 'package:front_end/src/api_unstable/vm.dart' import 'package:kernel/ast.dart'; import 'package:kernel/class_hierarchy.dart'; import 'package:kernel/core_types.dart'; -import 'package:kernel/kernel.dart' show writeComponentToText; +import 'package:kernel/kernel.dart' + show writeComponentToBinary, writeComponentToText; import 'package:kernel/library_index.dart'; import 'package:kernel/verifier.dart'; +import 'package:path/path.dart' as path show setExtension; import 'package:vm/kernel_front_end.dart' show writeDepfile; import 'package:vm/transformations/mixin_deduplication.dart' - as mixin_deduplication show transformComponent; + as mixin_deduplication show transformLibraries; import 'package:vm/transformations/to_string_transformer.dart' as to_string_transformer; import 'package:vm/transformations/type_flow/transformer.dart' as globalTypeFlow @@ -35,10 +38,14 @@ import 'package:wasm_builder/wasm_builder.dart' show Serializer; import 'compiler_options.dart' as compiler; import 'constant_evaluator.dart'; -import 'deferred_loading.dart' as deferred_loading; +import 'deferred_loading.dart'; +import 'dynamic_module_kernel_metadata.dart'; +import 'dynamic_modules.dart'; import 'js/runtime_generator.dart' as js; +import 'modules.dart'; import 'record_class_generator.dart'; import 'records.dart'; +import 'serialization.dart'; import 'target.dart' as wasm show Mode; import 'target.dart' hide Mode; import 'translator.dart'; @@ -47,7 +54,7 @@ sealed class CompilationResult {} class CompilationSuccess extends CompilationResult { final Map wasmModules; - final String jsRuntime; + final String? jsRuntime; final String supportJs; CompilationSuccess(this.wasmModules, this.jsRuntime, this.supportJs); @@ -138,15 +145,50 @@ Future compileToModule( StandardFileSystem.instance); } + Future resolveUri(Uri? uri) async { + if (uri == null) return null; + var fileSystemEntity = compilerOptions.fileSystem.entityForUri(uri); + if (fileSystemEntity is MultiRootFileSystemEntity) { + fileSystemEntity = await fileSystemEntity.delegate; + } + return fileSystemEntity.uri; + } + if (options.platformPath != null) { compilerOptions.sdkSummary = options.platformPath; } else { compilerOptions.compileSdk = true; } + DynamicModuleMetadata? dynamicModuleMetadata; + final dynamicMainModuleUri = options.dynamicModuleMainUri; + if (dynamicMainModuleUri != null && options.dynamicInterfaceUri == null) { + final filename = options.dynamicModuleMetadataFile ?? + Uri.parse( + path.setExtension(dynamicMainModuleUri.toFilePath(), '.dyndata')); + final dynamicModuleMetadataBytes = + await File.fromUri(filename).readAsBytes(); + final source = BinaryDataSource(dynamicModuleMetadataBytes); + dynamicModuleMetadata = DynamicModuleMetadata.deserialize(source); + } + + final dynamicModuleMainUri = await resolveUri(options.dynamicModuleMainUri); + final dynamicInterfaceUri = await resolveUri(options.dynamicInterfaceUri); + final isDynamicMainModule = + dynamicModuleMainUri != null && dynamicInterfaceUri != null; + final isDynamicModule = + dynamicModuleMainUri != null && dynamicInterfaceUri == null; + if (isDynamicModule) { + dynamicModuleMetadata!.verifyDynamicModuleOptions(options); + compilerOptions.additionalDills.add(dynamicModuleMainUri); + } else if (isDynamicMainModule) { + DynamicModuleMetadata.verifyMainModuleOptions(options); + } + CompilerResult? compilerResult; try { - compilerResult = await kernelForProgram(options.mainUri, compilerOptions); + compilerResult = await kernelForProgram(options.mainUri, compilerOptions, + requireMain: !isDynamicModule); } catch (e, s) { return CFECrashError(e, s); } @@ -183,18 +225,38 @@ Future compileToModule( component, options.deleteToStringPackageUri); } - if (options.translatorOptions.enableMultiModuleStressTestMode) { - deferred_loading.transformComponentForTestMode( - component, classHierarchy, coreTypes, target); + ModuleStrategy moduleStrategy; + if (options.translatorOptions.enableDeferredLoading) { + moduleStrategy = + DeferredLoadingModuleStrategy(component, options, target, coreTypes); + } else if (options.translatorOptions.enableMultiModuleStressTestMode) { + moduleStrategy = + StressTestModuleStrategy(component, coreTypes, target, classHierarchy); + } else if (isDynamicMainModule) { + moduleStrategy = DynamicMainModuleStrategy( + component, + coreTypes, + classHierarchy, + File.fromUri(dynamicInterfaceUri).readAsStringSync(), + options.dynamicInterfaceUri!); + } else if (isDynamicModule) { + moduleStrategy = DynamicModuleStrategy(component, options, target, + coreTypes, classHierarchy, dynamicModuleMainUri); + } else { + moduleStrategy = DefaultModuleStrategy(component); } + final librariesToTransform = isDynamicModule + ? component.getMainModuleLibraries(coreTypes) + : component.libraries; ConstantEvaluator constantEvaluator = ConstantEvaluator( options, target, component, coreTypes, classHierarchy, libraryIndex); - unreachable_code_elimination.transformComponent(target, component, + unreachable_code_elimination.transformLibraries(target, librariesToTransform, constantEvaluator, options.translatorOptions.enableAsserts); - js.RuntimeFinalizer jsRuntimeFinalizer = - js.createRuntimeFinalizer(component, coreTypes, classHierarchy); + js.RuntimeFinalizer? jsRuntimeFinalizer = isDynamicModule + ? null + : js.createRuntimeFinalizer(component, coreTypes, classHierarchy); final Map recordClasses = generateRecordClasses(component, coreTypes); @@ -204,7 +266,14 @@ Future compileToModule( writeComponentToText(component, path: options.dumpKernelBeforeTfa!); } - mixin_deduplication.transformComponent(component); + mixin_deduplication.transformLibraries(librariesToTransform); + + moduleStrategy.prepareComponent(); + + if (isDynamicMainModule) { + writeComponentToBinary(component, dynamicModuleMainUri.path, + includeSource: false); + } // Patch `dart:_internal`s `mainTearOff` getter. final internalLib = component.libraries @@ -217,6 +286,7 @@ Future compileToModule( // Keep the flags in-sync with // pkg/vm/test/transformations/type_flow/transformer_test.dart + // TODO(natebiggs): Only run TFA on main module when dynamic modules enabled. globalTypeFlow.transformComponent(target, coreTypes, component, useRapidTypeAnalysis: false); @@ -231,11 +301,12 @@ Future compileToModule( return true; }()); - final moduleOutputData = deferred_loading.modulesForComponent( - component, options, target, coreTypes); + final moduleOutputData = moduleStrategy.buildModuleOutputData(); var translator = Translator(component, coreTypes, libraryIndex, recordClasses, - moduleOutputData, options.translatorOptions); + moduleOutputData, options.translatorOptions, + dynamicModuleMetadata: dynamicModuleMetadata, + enableDynamicModules: dynamicModuleMainUri != null); String? depFile = options.depFile; if (depFile != null) { @@ -247,6 +318,7 @@ Future compileToModule( final modules = translator.translate(sourceMapUrlGenerator); final wasmModules = {}; modules.forEach((moduleOutput, module) { + if (moduleOutput.skipEmit) return; final serializer = Serializer(); module.serialize(serializer); final wasmModuleSerialized = serializer.data; @@ -257,13 +329,22 @@ Future compileToModule( (moduleBytes: wasmModuleSerialized, sourceMap: sourceMap); }); - String jsRuntime = jsRuntimeFinalizer.generate( + final jsRuntime = jsRuntimeFinalizer?.generate( translator.functions.translatedProcedures, translator.internalizedStringsForJSRuntime, translator.options.requireJsStringBuiltin, mode); final supportJs = _generateSupportJs(options.translatorOptions); + if (isDynamicMainModule) { + final filename = options.dynamicModuleMetadataFile ?? + Uri.parse( + path.setExtension(dynamicMainModuleUri!.toFilePath(), '.dyndata')); + final sink = BinaryDataSink(); + translator.dynamicModuleInfo!.toMetadata(options).serialize(sink); + await File.fromUri(filename).writeAsBytes(sink.takeBytes()); + } + return CompilationSuccess(wasmModules, jsRuntime, supportJs); } diff --git a/pkg/dart2wasm/lib/constant_evaluator.dart b/pkg/dart2wasm/lib/constant_evaluator.dart index 115ad043fb9..aa38c6dbd24 100644 --- a/pkg/dart2wasm/lib/constant_evaluator.dart +++ b/pkg/dart2wasm/lib/constant_evaluator.dart @@ -17,9 +17,11 @@ class ConstantEvaluator extends kernel.ConstantEvaluator implements VMConstantEvaluator { final bool _checkBounds; final bool _minify; + final bool _hasDynamicModuleSupport; final Procedure _dartInternalCheckBoundsGetter; final Procedure _dartInternalMinifyGetter; + final Procedure _dartInternalHasDynamicModuleSupportGetter; ConstantEvaluator( WasmCompilerOptions options, @@ -30,10 +32,14 @@ class ConstantEvaluator extends kernel.ConstantEvaluator LibraryIndex libraryIndex) : _checkBounds = !options.translatorOptions.omitBoundsChecks, _minify = options.translatorOptions.minify, + _hasDynamicModuleSupport = options.dynamicModuleMainUri != null, _dartInternalCheckBoundsGetter = libraryIndex.getTopLevelProcedure( "dart:_internal", "get:checkBounds"), _dartInternalMinifyGetter = libraryIndex.getTopLevelProcedure("dart:_internal", "get:minify"), + _dartInternalHasDynamicModuleSupportGetter = + libraryIndex.getTopLevelProcedure( + "dart:_internal", "get:hasDynamicModuleSupport"), super( target.dartLibrarySupport, target.constantsBackend, @@ -56,6 +62,9 @@ class ConstantEvaluator extends kernel.ConstantEvaluator if (target == _dartInternalMinifyGetter) { return canonicalize(BoolConstant(_minify)); } + if (target == _dartInternalHasDynamicModuleSupportGetter) { + return canonicalize(BoolConstant(_hasDynamicModuleSupport)); + } return super.visitStaticGet(node); } @@ -67,5 +76,6 @@ class ConstantEvaluator extends kernel.ConstantEvaluator @override bool shouldEvaluateMember(Member node) => node == _dartInternalCheckBoundsGetter || - node == _dartInternalMinifyGetter; + node == _dartInternalMinifyGetter || + node == _dartInternalHasDynamicModuleSupportGetter; } diff --git a/pkg/dart2wasm/lib/constants.dart b/pkg/dart2wasm/lib/constants.dart index 66764e1602b..9f81e3fbb4c 100644 --- a/pkg/dart2wasm/lib/constants.dart +++ b/pkg/dart2wasm/lib/constants.dart @@ -13,6 +13,7 @@ import 'package:wasm_builder/wasm_builder.dart' as w; import 'class_info.dart'; import 'closures.dart'; import 'code_generator.dart'; +import 'dynamic_modules.dart'; import 'param_info.dart'; import 'translator.dart'; import 'types.dart'; @@ -69,6 +70,7 @@ typedef ConstantCodeGenerator = void Function(w.InstructionsBuilder); class Constants { final Translator translator; final Map constantInfo = {}; + final Map dynamicModuleConstantInfo = {}; w.DataSegmentBuilder? oneByteStringSegment; w.DataSegmentBuilder? twoByteStringSegment; late final ClassInfo typeInfo = translator.classInfo[translator.typeClass]!; @@ -114,6 +116,11 @@ class Constants { {translator.wasmI32Value.fieldReference: IntConstant(value)}); } + // Used as an indicator for interface types that the enclosed class ID must be + // globalized on instantiation. Resolves to a normal _InterfaceType. + static final Class _relativeInterfaceTypeIndicator = + Class(name: '', fileUri: Uri()); + /// Makes a `WasmArray<_Type>` [InstanceConstant]. InstanceConstant makeTypeArray(Iterable types) { return makeArrayOf( @@ -160,8 +167,8 @@ class Constants { /// Sub-constants must have Wasm globals assigned before the global for the /// composite constant is assigned, since global initializers can only refer /// to earlier globals. - ConstantInfo? ensureConstant(Constant constant) { - return ConstantCreator(this).ensureConstant(constant); + ConstantInfo? ensureConstant(Constant constant, w.ModuleBuilder module) { + return ConstantCreator(this, module).ensureConstant(constant); } /// Emit code to push a constant onto the stack. @@ -228,9 +235,22 @@ class Constants { } InstanceConstant _makeInterfaceTypeConstant(InterfaceType type) { - return _makeTypeConstant(translator.interfaceTypeClass, type.nullability, { - translator.interfaceTypeClassIdField.fieldReference: - makeWasmI32(translator.classIdNumbering.classIds[type.classNode]!), + final wrappedClassId = + translator.classIdNumbering.classIds[type.classNode]!; + final (typeClass, classId) = switch (wrappedClassId) { + RelativeClassId() => ( + _relativeInterfaceTypeIndicator, + wrappedClassId.relativeValue + ), + AbsoluteClassId() => ( + translator.interfaceTypeClass, + wrappedClassId.value + ), + }; + // If the class ID is relative we will detect that when the constant is + // emitted and adjust it accordingly. + return _makeTypeConstant(typeClass, type.nullability, { + translator.interfaceTypeClassIdField.fieldReference: makeWasmI32(classId), translator.interfaceTypeTypeArguments.fieldReference: makeTypeArray(type.typeArguments), }); @@ -354,7 +374,8 @@ class ConstantInstantiator extends ConstantVisitor @override w.ValueType defaultConstant(Constant constant) { - ConstantInfo info = ConstantCreator(constants).ensureConstant(constant)!; + ConstantInfo info = + ConstantCreator(constants, b.module).ensureConstant(constant)!; return info.readConstant(translator, b); } @@ -375,6 +396,10 @@ class ConstantInstantiator extends ConstantVisitor @override w.ValueType visitNullConstant(NullConstant node) { + if (expectedType == w.RefType.func(nullable: true)) { + b.ref_null((expectedType as w.RefType).heapType); + return expectedType; + } b.ref_null(w.HeapType.none); return const w.RefType.none(nullable: true); } @@ -435,8 +460,12 @@ class ConstantInstantiator extends ConstantVisitor class ConstantCreator extends ConstantVisitor with ConstantVisitorDefaultMixin { final Constants constants; + final w.ModuleBuilder targetModule; - ConstantCreator(this.constants); + ConstantCreator(this.constants, w.ModuleBuilder module) + : targetModule = constants.translator.isDynamicModule + ? module + : constants.translator.mainModule; Translator get translator => constants.translator; Types get types => translator.types; @@ -453,11 +482,15 @@ class ConstantCreator extends ConstantVisitor constant = constants._lowerTypeConstant(type); } - ConstantInfo? info = constants.constantInfo[constant]; + final cache = translator.dynamicModuleSupportEnabled && + !translator.isMainModule(targetModule) + ? constants.dynamicModuleConstantInfo + : constants.constantInfo; + ConstantInfo? info = cache[constant]; if (info == null) { info = constant.accept(this); if (info != null) { - constants.constantInfo[constant] = info; + cache[constant] = info; } } return info; @@ -467,18 +500,21 @@ class ConstantCreator extends ConstantVisitor Constant constant, w.RefType type, ConstantCodeGenerator generator, {bool lazy = false}) { assert(!type.nullable); - final mainModule = translator.mainModule; - if (lazy) { + if (lazy || translator.dynamicModuleSupportEnabled) { // Create uninitialized global and function to initialize it. final global = - mainModule.globals.define(w.GlobalType(type.withNullability(true))); + targetModule.globals.define(w.GlobalType(type.withNullability(true))); global.initializer.ref_null(w.HeapType.none); global.initializer.end(); w.FunctionType ftype = translator.typesBuilder.defineFunction(const [], [type]); - final function = mainModule.functions.define(ftype, "$constant"); + final function = targetModule.functions.define(ftype, "$constant"); final b2 = function.body; generator(b2); + if (translator.dynamicModuleSupportEnabled) { + final valueLocal = b2.addLocal(type); + constant.accept(ConstantCanonicalizer(translator, b2, valueLocal)); + } w.Local temp = b2.addLocal(type); b2.local_tee(temp); b2.global_set(global); @@ -491,7 +527,7 @@ class ConstantCreator extends ConstantVisitor assert(!constants.currentlyCreating); constants.currentlyCreating = true; final global = - mainModule.globals.define(w.GlobalType(type, mutable: false)); + targetModule.globals.define(w.GlobalType(type, mutable: false)); generator(global.initializer); global.initializer.end(); constants.currentlyCreating = false; @@ -507,7 +543,7 @@ class ConstantCreator extends ConstantVisitor ConstantInfo? visitBoolConstant(BoolConstant constant) { ClassInfo info = translator.classInfo[translator.boxedBoolClass]!; return createConstant(constant, info.nonNullableType, (b) { - b.i32_const(info.classId); + b.i32_const((info.classId as AbsoluteClassId).value); b.i32_const(constant.value ? 1 : 0); b.struct_new(info.struct); }); @@ -517,7 +553,7 @@ class ConstantCreator extends ConstantVisitor ConstantInfo? visitIntConstant(IntConstant constant) { ClassInfo info = translator.classInfo[translator.boxedIntClass]!; return createConstant(constant, info.nonNullableType, (b) { - b.i32_const(info.classId); + b.i32_const((info.classId as AbsoluteClassId).value); b.i64_const(constant.value); b.struct_new(info.struct); }); @@ -527,7 +563,7 @@ class ConstantCreator extends ConstantVisitor ConstantInfo? visitDoubleConstant(DoubleConstant constant) { ClassInfo info = translator.classInfo[translator.boxedDoubleClass]!; return createConstant(constant, info.nonNullableType, (b) { - b.i32_const(info.classId); + b.i32_const((info.classId as AbsoluteClassId).value); b.f64_const(constant.value); b.struct_new(info.struct); }); @@ -538,7 +574,7 @@ class ConstantCreator extends ConstantVisitor if (translator.options.jsCompatibility) { ClassInfo info = translator.classInfo[translator.jsStringClass]!; return createConstant(constant, info.nonNullableType, (b) { - b.pushObjectHeaderFields(info); + b.pushObjectHeaderFields(translator, info); translator.globals.readGlobal(b, translator.getInternalizedStringGlobal(b.module, constant.value)); b.struct_new(info.struct); @@ -556,19 +592,19 @@ class ConstantCreator extends ConstantVisitor (info.struct.fields[FieldIndex.stringArray].type as w.RefType) .heapType as w.ArrayType; - b.pushObjectHeaderFields(info); + b.pushObjectHeaderFields(translator, info); if (lazy) { // Initialize string contents from passive data segment. w.DataSegmentBuilder segment; Uint8List bytes; if (isOneByte) { segment = constants.oneByteStringSegment ??= - translator.mainModule.dataSegments.define(); + targetModule.dataSegments.define(); bytes = Uint8List.fromList(constant.value.codeUnits); } else { assert(Endian.host == Endian.little); segment = constants.twoByteStringSegment ??= - translator.mainModule.dataSegments.define(); + targetModule.dataSegments.define(); bytes = Uint16List.fromList(constant.value.codeUnits) .buffer .asUint8List(); @@ -592,6 +628,7 @@ class ConstantCreator extends ConstantVisitor @override ConstantInfo? visitInstanceConstant(InstanceConstant constant) { Class cls = constant.classNode; + bool isRelativeInterfaceType = false; if (cls == translator.wasmArrayClass) { return _makeWasmArrayLiteral(constant, mutable: true); } @@ -602,17 +639,25 @@ class ConstantCreator extends ConstantVisitor return null; } + if (cls == Constants._relativeInterfaceTypeIndicator) { + cls = translator.interfaceTypeClass; + isRelativeInterfaceType = true; + } + ClassInfo info = translator.classInfo[cls]!; translator.functions.recordClassAllocation(info.classId); w.RefType type = info.nonNullableType; // Collect sub-constants for field values. - const int baseFieldCount = 2; int fieldCount = info.struct.fields.length; List subConstants = List.filled(fieldCount, null); - bool lazy = false; + // Relative class IDs will get adjusted at runtime based on the local + // class ID base for the enclosing module. This must be done lazily + // since the global is not const. + bool lazy = isRelativeInterfaceType; constant.fieldValues.forEach((reference, subConstant) { - int index = translator.fieldIndex[reference.asField]!; + final field = reference.asField; + int index = translator.fieldIndex[field]!; assert(subConstants[index] == null); subConstants[index] = subConstant; lazy |= ensureConstant(subConstant)?.isLazy ?? false; @@ -638,11 +683,16 @@ class ConstantCreator extends ConstantVisitor } return createConstant(constant, type, lazy: lazy, (b) { - b.pushObjectHeaderFields(info); - for (int i = baseFieldCount; i < fieldCount; i++) { + b.pushObjectHeaderFields(translator, info); + for (int i = FieldIndex.objectFieldBase; i < fieldCount; i++) { Constant subConstant = subConstants[i]!; constants.instantiateConstant( b, subConstant, info.struct.fields[i].type.unpacked); + if (isRelativeInterfaceType && i == FieldIndex.interfaceTypeClassId) { + assert(translator.isDynamicModule); + translator.pushModuleId(b); + translator.callReference(translator.globalizeClassId.reference, b); + } } b.struct_new(info.struct); }); @@ -746,7 +796,7 @@ class ConstantCreator extends ConstantVisitor w.ArrayType arrayType = translator.listArrayType; w.ValueType elementType = arrayType.elementType.type.unpacked; int length = constant.entries.length; - b.pushObjectHeaderFields(info); + b.pushObjectHeaderFields(translator, info); constants.instantiateConstant( b, typeArgConstant, constants.typeInfo.nullableType); b.i64_const(length); @@ -853,24 +903,25 @@ class ConstantCreator extends ConstantVisitor Constant functionTypeConstant = constants._lowerTypeConstant(translator.getTearOffType(member)); ensureConstant(functionTypeConstant); - ClosureImplementation closure = translator.getTearOffClosure(member); + ClosureImplementation closure = + translator.getTearOffClosure(member, targetModule); w.StructType struct = closure.representation.closureStruct; w.RefType type = w.RefType.def(struct, nullable: false); // The vtable for the target will be stored on a global in the target's // module. - final isLazy = !translator - .isMainModule(translator.moduleForReference(constant.targetReference)); + final isLazy = + translator.moduleForReference(constant.targetReference) != targetModule; // The dummy struct must be declared before the constant global so that the // constant's initializer can reference it. final dummyStructGlobal = translator - .getDummyValuesCollectorForModule(translator.mainModule) + .getDummyValuesCollectorForModule(targetModule) .dummyStructGlobal; return createConstant(constant, type, (b) { ClassInfo info = translator.closureInfo; translator.functions.recordClassAllocation(info.classId); - b.pushObjectHeaderFields(info); + b.pushObjectHeaderFields(translator, info); translator.globals.readGlobal(b, dummyStructGlobal); // Dummy context translator.globals.readGlobal(b, closure.vtable); constants.instantiateConstant( @@ -896,7 +947,7 @@ class ConstantCreator extends ConstantVisitor constants._lowerTypeConstant(instantiatedFunctionType); ensureConstant(functionTypeConstant); ClosureImplementation tearOffClosure = - translator.getTearOffClosure(tearOffProcedure); + translator.getTearOffClosure(tearOffProcedure, targetModule); int positionalCount = tearOffConstant.function.positionalParameters.length; List names = tearOffConstant.function.namedParameters.map((p) => p.name!).toList(); @@ -911,7 +962,7 @@ class ConstantCreator extends ConstantVisitor final tearOffConstantInfo = ensureConstant(tearOffConstant)!; w.BaseFunction makeDynamicCallEntry() { - final function = translator.mainModule.functions.define( + final function = targetModule.functions.define( translator.dynamicCallVtableEntryFunctionType, "dynamic call entry"); final b = function.body; @@ -1030,7 +1081,7 @@ class ConstantCreator extends ConstantVisitor b.struct_new(instantiationOfTearOffRepresentation.vtableStruct); } - b.pushObjectHeaderFields(info); + b.pushObjectHeaderFields(translator, info); // Context is not used by the vtable functions, but it's needed for // closure equality checks to work (`_Closure._equals`). @@ -1062,7 +1113,7 @@ class ConstantCreator extends ConstantVisitor StringConstant nameConstant = StringConstant(constant.name); bool lazy = ensureConstant(nameConstant)?.isLazy ?? false; return createConstant(constant, info.nonNullableType, lazy: lazy, (b) { - b.pushObjectHeaderFields(info); + b.pushObjectHeaderFields(translator, info); constants.instantiateConstant(b, nameConstant, stringType); b.struct_new(info.struct); }); @@ -1084,7 +1135,7 @@ class ConstantCreator extends ConstantVisitor return createConstant(constant, recordClassInfo.nonNullableType, lazy: lazy, (b) { - b.pushObjectHeaderFields(recordClassInfo); + b.pushObjectHeaderFields(translator, recordClassInfo); for (Constant argument in arguments) { constants.instantiateConstant( b, argument, translator.topInfo.nullableType); diff --git a/pkg/dart2wasm/lib/deferred_loading.dart b/pkg/dart2wasm/lib/deferred_loading.dart index ffc863c405d..bb41eb4b825 100644 --- a/pkg/dart2wasm/lib/deferred_loading.dart +++ b/pkg/dart2wasm/lib/deferred_loading.dart @@ -6,62 +6,12 @@ import 'package:collection/collection.dart'; import 'package:kernel/ast.dart'; import 'package:kernel/class_hierarchy.dart'; import 'package:kernel/core_types.dart'; -import 'package:kernel/target/targets.dart'; import 'await_transformer.dart' as await_transformer; import 'compiler_options.dart'; import 'generate_wasm.dart'; -import 'util.dart'; - -const _mainModuleId = 0; - -Library? _enclosingLibraryForReference(Reference reference) { - TreeNode? current = reference.node; - // References generated for constants will not have a node attached. - if (reference.node == null) return null; - while (current != null) { - if (current is Library) return current; - current = current.parent; - } - throw ArgumentError('Could not find enclosing library for ${reference.node}'); -} - -/// Deferred loading metadata for a single dart2wasm output module. -/// -/// Each [ModuleOutput] will map to a single wasm module emitted by the -/// compiler. The separation of modules is guided by the deferred imports -/// defined in the source code. -/// -/// A module may contain code at any level of granularity. Code may be grouped -/// by library, by class or neither. [containsReference] should be used to -/// determine if a module contains a given class/member reference. -class ModuleOutput { - /// The ID for the module which will be included in the emitted name. - final int _id; - - /// The set of libraries contained in this module. - final Set _libraries = {}; - - bool get isMain => _id == _mainModuleId; - - /// The name used to import and export this module. - String get moduleImportName => 'module$_id'; - - /// The name added to the wasm output file for this module. - String get moduleName => isMain ? '' : moduleImportName; - - ModuleOutput._(this._id); - - /// Whether or not the provided kernel [Reference] is included in this module. - bool containsReference(Reference reference) { - final enclosingLibrary = _enclosingLibraryForReference(reference); - if (enclosingLibrary == null) return false; - return _libraries.contains(enclosingLibrary); - } - - @override - String toString() => '$moduleImportName($_libraries)'; -} +import 'modules.dart'; +import 'target.dart'; /// The root of a deferred import subgraph. /// @@ -125,60 +75,21 @@ class _RootSet { /// To support the actual process of loading the deferred wasm modules, we also /// collect a mapping from each import site (i.e. a library and deferred import /// name pair) to the load list needed at that import site. -class _LibraryAnalysis { +class DeferredLoadingModuleStrategy extends DefaultModuleStrategy { final WasmCompilerOptions options; - final Target kernelTarget; - final Component component; + final WasmTarget kernelTarget; final CoreTypes coreTypes; - _LibraryAnalysis( - this.component, this.options, this.kernelTarget, this.coreTypes); - - ModuleOutputData _buildModuleOutputDataForTestModule() { - int moduleIdCounter = _mainModuleId; - final mainModule = ModuleOutput._(moduleIdCounter++); - final initLibraries = - _getTestModeMainLibraries(component, coreTypes, kernelTarget); - mainModule._libraries.addAll(initLibraries); - final modules = []; - final importMap = >{}; - - // Put each library in a separate module. - for (final library in component.libraries) { - if (initLibraries.contains(library)) continue; - final module = ModuleOutput._(moduleIdCounter++); - modules.add(module); - module._libraries.add(library); - final importName = '${library.importUri}'; - importMap[importName] = [module]; - } - - final invokeMain = - coreTypes.index.getTopLevelProcedure('dart:_internal', '_invokeMain'); - return ModuleOutputData( - [mainModule, ...modules], {invokeMain.enclosingLibrary: importMap}); - } - - ModuleOutputData _buildModuleOutputDataDisabled() { -// If deferred loading is not enabled then put every library in the main - // module. - final mainModule = ModuleOutput._(_mainModuleId); - mainModule._libraries.addAll(component.libraries); - return ModuleOutputData([mainModule], const {}); - } + DeferredLoadingModuleStrategy( + super.component, this.options, this.kernelTarget, this.coreTypes); + @override ModuleOutputData buildModuleOutputData() { - if (options.translatorOptions.enableMultiModuleStressTestMode) { - return _buildModuleOutputDataForTestModule(); - } else if (!options.translatorOptions.enableDeferredLoading) { - return _buildModuleOutputDataDisabled(); - } - final (libraryToRootSet, importTargetMap) = _buildLibraryToImports(); - int moduleIdCounter = _mainModuleId; + final moduleBuilder = ModuleOutputBuilder(); // Dedupe root sets combining equal sets into a single ModuleOutput. - final mainModule = ModuleOutput._(moduleIdCounter++); + final mainModule = moduleBuilder.buildModule(); final Map<_RootSet, ModuleOutput> rootSetToModule = {}; final Map> rootToModules = {}; libraryToRootSet.forEach((targetLibrary, rootSet) { @@ -190,15 +101,15 @@ class _LibraryAnalysis { if (module != null) { // We've already seen a library required by the same roots so added it // to the same module. - module._libraries.add(targetLibrary); + module.libraries.add(targetLibrary); return; } // This library is used by a new set of roots so create a new module for // it. Each root that needs this library should depend on this module. - module = rootSetToModule[rootSet] = ModuleOutput._(moduleIdCounter++); + module = rootSetToModule[rootSet] = moduleBuilder.buildModule(); - module._libraries.add(targetLibrary); + module.libraries.add(targetLibrary); for (final root in rootSet.libraries) { (rootToModules[root] ??= []).add(module); } @@ -244,7 +155,7 @@ class _LibraryAnalysis { // dependencies on these. Also add libraries containing 'wasm:export' // since embedders might need access to these from the main module. for (final lib in component.libraries) { - if (_containsExport(coreTypes, lib) || _isRequiredLibrary(lib)) { + if (containsWasmExport(coreTypes, lib) || _isRequiredLibrary(lib)) { if (enqueuedEagerLibraries.add(lib)) { eagerWorkStack.add(lib); } @@ -286,132 +197,89 @@ class _LibraryAnalysis { } } -/// Data needed to create deferred modules. -class ModuleOutputData { - /// All [ModuleOutput]s generated for the program. - final List modules; +class StressTestModuleStrategy extends ModuleStrategy { + final Component component; + final CoreTypes coreTypes; + final WasmTarget kernelTarget; + final ClassHierarchy classHierarchy; - final Map>> _importMap; + /// We load all 'dart:*' libraries since just doing the deferred load of modules + /// requires a significant portion of the SDK libraries. + late final Set _testModeMainLibraries = { + ...component.libraries.where( + (l) => l.importUri.scheme == 'dart' || containsWasmExport(coreTypes, l)) + }; - ModuleOutputData(this.modules, this._importMap) : assert(modules[0].isMain); + StressTestModuleStrategy( + this.component, this.coreTypes, this.kernelTarget, this.classHierarchy); - ModuleOutput get mainModule => modules[0]; - Iterable get deferredModules => modules.skip(1); - - bool get hasMultipleModules => modules.length > 1; - - /// Mapping from deferred library import to the 'load list' of module names - /// needed for that import. + /// Augments the `_invokeMain` JS->WASM entry point with test mode setup. /// - /// If library L is required (either directly or indirectly) by two separate - /// imports, then L will be in its own module. That module will be included in - /// the load list for both those imports. - Map>> generateModuleImportMap() { - final result = >>{}; - _importMap.forEach((lib, importMapping) { - final nameMapping = >{}; - importMapping.forEach((importName, modules) { - nameMapping[importName] = - modules.map((o) => o.moduleImportName).toList(); - }); - result[lib.importUri.toString()] = nameMapping; - }); - return result; - } - - /// Returns the module that contains [reference]. - ModuleOutput moduleForReference(Reference reference) { - return modules.firstWhere((e) => e.containsReference(reference)); - } -} - -/// Generates module data for the libraries contained in the provided -/// [Component]. -ModuleOutputData modulesForComponent(Component component, - WasmCompilerOptions options, Target kernelTarget, CoreTypes coreTypes) { - return _LibraryAnalysis(component, options, kernelTarget, coreTypes) - .buildModuleOutputData(); -} - -Set _getReachableLibraries( - Component component, CoreTypes coreTypes, Target kernelTarget) { - final entryPoint = component.mainMethod!.enclosingLibrary; - final List queue = [entryPoint]; - final Set reachable = {entryPoint}; - while (queue.isNotEmpty) { - final current = queue.removeLast(); - for (final dep in current.dependencies) { - final importedLib = dep.targetLibrary; - if (reachable.add(importedLib)) { - queue.add(importedLib); - } + /// Choosing to augment `_invokeMain` allows us to defer the user-defined + /// `main` into a second module ensuring that we always have at least 2 + /// modules in test mode. + @override + void prepareComponent() { + final initLibraries = _testModeMainLibraries; + final loadLibrary = + coreTypes.index.getTopLevelProcedure('dart:_internal', 'loadLibrary'); + final invokeMain = + coreTypes.index.getTopLevelProcedure('dart:_internal', '_invokeMain'); + final loadStatements = []; + for (final library in getReachableLibraries( + component.mainMethod!.enclosingLibrary, coreTypes, kernelTarget)) { + if (initLibraries.contains(library)) continue; + final loadLibraryCall = StaticInvocation( + loadLibrary, + Arguments([ + StringLiteral('${invokeMain.enclosingLibrary.importUri}'), + StringLiteral('${library.importUri}') + ])); + loadStatements.add(ExpressionStatement(AwaitExpression(loadLibraryCall))); } - } - return reachable; -} -bool _hasWasmExportPragma(CoreTypes coreTypes, Member m) => - getPragma(coreTypes, m, 'wasm:export', defaultValue: m.name.text) != null; + invokeMain.function.asyncMarker = AsyncMarker.Async; + invokeMain.function.emittedValueType = const VoidType(); -bool _containsExport(CoreTypes coreTypes, Library lib) { - if (lib.members.any((m) => _hasWasmExportPragma(coreTypes, m))) { - return true; - } - return lib.classes - .any((c) => c.members.any((m) => _hasWasmExportPragma(coreTypes, m))); -} + final oldBody = invokeMain.function.body!; -/// Augments the `_invokeMain` JS->WASM entry point with test mode setup. -/// -/// Choosing to augment `_invokeMain` allows us to defer the user-defined `main` -/// into a second module ensuring that we always have at least 2 modules in test -/// mode. -void transformComponentForTestMode(Component component, - ClassHierarchy classHierarchy, CoreTypes coreTypes, Target kernelTarget) { - final initLibraries = - _getTestModeMainLibraries(component, coreTypes, kernelTarget); - final loadLibrary = - coreTypes.index.getTopLevelProcedure('dart:_internal', 'loadLibrary'); - final invokeMain = - coreTypes.index.getTopLevelProcedure('dart:_internal', '_invokeMain'); - final loadStatements = []; - for (final library - in _getReachableLibraries(component, coreTypes, kernelTarget)) { - if (initLibraries.contains(library)) continue; - final loadLibraryCall = StaticInvocation( - loadLibrary, - Arguments([ - StringLiteral('${invokeMain.enclosingLibrary.importUri}'), - StringLiteral('${library.importUri}') - ])); - loadStatements.add(ExpressionStatement(AwaitExpression(loadLibraryCall))); + // Add print of 'unittest-suite-wait-for-done' to indicate to test harnesses + // that the test contains async work. Any test using test most must therefore + // also include a concluding 'unittest-suite-done' message. Usually via calls + // to `asyncStart` and `asyncEnd` helpers. + final asyncStart = ExpressionStatement(StaticInvocation( + coreTypes.printProcedure, + Arguments([StringLiteral('unittest-suite-wait-for-done')]))); + invokeMain.function.body = Block([asyncStart, ...loadStatements, oldBody]); + + // The await transformer runs modularly before this transform so we need to + // rerun it on the transformed `_invokeMain` method. + await_transformer.transformLibraries( + [invokeMain.enclosingLibrary], classHierarchy, coreTypes); } - invokeMain.function.asyncMarker = AsyncMarker.Async; - invokeMain.function.emittedValueType = const VoidType(); + @override + ModuleOutputData buildModuleOutputData() { + final moduleBuilder = ModuleOutputBuilder(); + final mainModule = moduleBuilder.buildModule(); + final initLibraries = _testModeMainLibraries; + mainModule.libraries.addAll(initLibraries); + final modules = []; + final importMap = >{}; - final oldBody = invokeMain.function.body!; + // Put each library in a separate module. + for (final library in component.libraries) { + if (initLibraries.contains(library)) continue; + final module = moduleBuilder.buildModule(); + modules.add(module); + module.libraries.add(library); + final importName = '${library.importUri}'; + importMap[importName] = [module]; + } - // Add print of 'unittest-suite-wait-for-done' to indicate to test harnesses - // that the test contains async work. Any test using test most must therefore - // also include a concluding 'unittest-suite-done' message. Usually via calls - // to `asyncStart` and `asyncEnd` helpers. - final asyncStart = ExpressionStatement(StaticInvocation( - coreTypes.printProcedure, - Arguments([StringLiteral('unittest-suite-wait-for-done')]))); - invokeMain.function.body = Block([asyncStart, ...loadStatements, oldBody]); - - // The await transformer runs modularly before this transform so we need to - // rerun it on the transformed `_invokeMain` method. - await_transformer.transformLibraries( - [invokeMain.enclosingLibrary], classHierarchy, coreTypes); + final invokeMain = + coreTypes.index.getTopLevelProcedure('dart:_internal', '_invokeMain'); + return ModuleOutputData( + [mainModule, ...modules], {invokeMain.enclosingLibrary: importMap}); + } } - -/// We load all 'dart:*' libraries since just doing the deferred load of modules -/// requires a significant portion of the SDK libraries. -Set _getTestModeMainLibraries( - Component component, CoreTypes coreTypes, Target kernelTarget) => - { - ...component.libraries.where( - (l) => l.importUri.scheme == 'dart' || _containsExport(coreTypes, l)) - }; diff --git a/pkg/dart2wasm/lib/dispatch_table.dart b/pkg/dart2wasm/lib/dispatch_table.dart index 506c24452b2..fb8cc2e50fb 100644 --- a/pkg/dart2wasm/lib/dispatch_table.dart +++ b/pkg/dart2wasm/lib/dispatch_table.dart @@ -10,10 +10,17 @@ import 'package:vm/metadata/table_selector.dart'; import 'package:wasm_builder/wasm_builder.dart' as w; import 'class_info.dart'; +import 'dynamic_modules.dart'; import 'param_info.dart'; import 'reference_extensions.dart'; import 'translator.dart'; +typedef ModuleSelectorTargets = ({ + SelectorTargets? checked, + SelectorTargets? unchecked, + SelectorTargets? normal +}); + /// Information for a dispatch table selector. /// /// A selector encapsulates information to generate code that selects the right @@ -30,6 +37,10 @@ class SelectorInfo { /// Unique ID of the selector. final int id; + // The ID of the selector in the main module. Only populated for dynamic + // modules. + final Map _mainModuleIds = {}; + /// Number of use sites of the selector. final int callCount; @@ -45,14 +56,14 @@ class SelectorInfo { /// performs type checks on the passed arguments. bool useMultipleEntryPoints = false; + bool isDynamicModuleOverrideable = false; + bool isDynamicModuleCallable = false; + /// Wasm function type for the selector. /// /// This should be read after all targets have been added to the selector. late final w.FunctionType signature = _computeSignature(); - /// Number of concrete classes that provide this selector. - late final int concreteClasses; - /// The selector's member's name. final String name; @@ -61,24 +72,34 @@ class SelectorInfo { /// `noSuchMethod` overrides to the dispatch table. final bool isNoSuchMethod; - late final SelectorTargets? _normal; - late final SelectorTargets? _checked; - late final SelectorTargets? _unchecked; + late final ModuleSelectorTargets _mainModuleTargets; + late final ModuleSelectorTargets _dynamicModuleTargets; - SelectorTargets targets({required bool unchecked}) { + SelectorTargets targets( + {required bool unchecked, required bool dynamicModule}) { + final selectorTargets = + dynamicModule ? _dynamicModuleTargets : _mainModuleTargets; if (useMultipleEntryPoints) { - assert(_checked!.targetRanges.length == _unchecked!.targetRanges.length); - assert(_checked!.staticDispatchRanges.length == - _unchecked!.staticDispatchRanges.length); - return unchecked ? _unchecked! : _checked!; + assert(selectorTargets.checked!.targetRanges.length == + selectorTargets.unchecked!.targetRanges.length); + return (unchecked ? selectorTargets.unchecked : selectorTargets.checked)!; } - assert(_checked == null && _unchecked == null); - return _normal!; + assert( + selectorTargets.checked == null && selectorTargets.unchecked == null); + return selectorTargets.normal!; } SelectorInfo._( - this.translator, this.id, this.name, this.callCount, this.paramInfo, - {required this.isSetter, required this.isNoSuchMethod}); + this.translator, + this.id, + this.name, + this.callCount, + this.paramInfo, { + required this.isSetter, + required this.isNoSuchMethod, + }); + + int mainModuleIdForTarget(Member member) => _mainModuleIds[member]!; String entryPointName(bool unchecked) { if (!useMultipleEntryPoints) return name; @@ -99,7 +120,13 @@ class SelectorInfo { List.generate(1 + paramInfo.paramCount, (_) => {}); List> outputSets = List.generate(returnCount, (_) => {}); List ensureBoxed = List.filled(1 + paramInfo.paramCount, false); - for (final (range: _, :target) in targets(unchecked: false).targetRanges) { + Iterable<({Reference target, Range range})> targetRanges = + targets(unchecked: false, dynamicModule: false).targetRanges; + if (translator.isDynamicModule) { + targetRanges = targetRanges.followedBy( + targets(unchecked: false, dynamicModule: true).targetRanges); + } + for (final (range: _, :target) in targetRanges) { Member member = target.asMember; DartType receiver = InterfaceType(member.enclosingClass!, Nullability.nonNullable); @@ -219,9 +246,22 @@ class SelectorInfo { return w.RefType.def(heapTypes.single, nullable: nullable); } - late final Set targetSet = useMultipleEntryPoints - ? {..._unchecked!._targetSet, ..._checked!._targetSet} - : _normal!._targetSet; + late final Set _targetSet = useMultipleEntryPoints + ? { + ..._mainModuleTargets.checked!._targetSet, + ..._mainModuleTargets.unchecked!._targetSet, + if (translator.isDynamicModule) ...{ + ..._dynamicModuleTargets.checked!._targetSet, + ..._dynamicModuleTargets.unchecked!._targetSet, + } + } + : { + ..._mainModuleTargets.normal!._targetSet, + if (translator.isDynamicModule) + ..._dynamicModuleTargets.normal!._targetSet + }; + + bool containsTarget(Reference target) => _targetSet.contains(target); } class SelectorTargets { @@ -245,7 +285,7 @@ class SelectorTargets { /// /// For a class in [targetRanges], `class ID + offset` gives the offset of the /// class member for this selector. - late final int offset; + int? offset; SelectorTargets(this.targetRanges, this.staticDispatchRanges); @@ -275,11 +315,15 @@ class DispatchTable { /// Contents of [_definedWasmTable]. For a selector with ID S and a target /// class of the selector with ID C, `table[S + C]` gives the reference to the /// class member for the selector. - late final List _table; + late final List table; + List? _dynamicModuleTable; late final w.TableBuilder _definedWasmTable; final WasmTableImporter _importedWasmTables; + w.TableBuilder? _dynamicModuleDefinedWasmTable; + w.Table get dynamicModuleDefinedWasmTable => _dynamicModuleDefinedWasmTable!; + /// The Wasm table for the dispatch table. w.Table getWasmTable(w.ModuleBuilder module) => _importedWasmTables.get(_definedWasmTable, module); @@ -323,6 +367,9 @@ class DispatchTable { metadata.methodOrSetterCalledDynamically || member.name.text == "call"); + final isDynamicModuleOverrideable = + member.isDynamicModuleOverrideable(translator.coreTypes); + final selector = _selectorInfo.putIfAbsent( selectorId, () => SelectorInfo._(translator, selectorId, member.name.text, @@ -331,10 +378,15 @@ class DispatchTable { isNoSuchMethod: member == translator.objectNoSuchMethod)); assert(selector.isSetter == isSetter); final useMultipleEntryPoints = !member.isAbstract && + !member.isExternal && !target.isGetter && !target.isTearOffReference && translator.needToCheckTypesFor(member); selector.useMultipleEntryPoints |= useMultipleEntryPoints; + selector.isDynamicModuleOverrideable |= isDynamicModuleOverrideable; + selector.isDynamicModuleCallable |= + member.isDynamicModuleCallable(translator.coreTypes); + selector.paramInfo.merge(paramInfo); if (calledDynamically) { if (isGetter) { @@ -345,6 +397,13 @@ class DispatchTable { (_dynamicMethods[member.name.text] ??= {}).add(selector); } } + final mainModuleIds = translator.dynamicModuleInfo?.selectorIds?[member]; + if (mainModuleIds != null) { + selector._mainModuleIds[member] = + isGetter ? mainModuleIds.$1 : mainModuleIds.$2; + } else { + selector._mainModuleIds[member] = selectorId; + } return selector; } @@ -425,10 +484,21 @@ class DispatchTable { } else if (member is Procedure) { final target = member.reference; addMember(target, staticDispatch); + final procedureMetadata = + translator.procedureAttributeMetadata[member]!; // `hasTearOffUses` can be true for operators as well, even though // it's not possible to tear-off an operator. (no syntax for it) if (member.kind == ProcedureKind.Method && - translator.procedureAttributeMetadata[member]!.hasTearOffUses) { + (procedureMetadata.hasTearOffUses || + // Only concrete members have 'hasTearOffUse' but for dynamic + // module compilations, there may be any concrete + // implementations yet. We check if the member is dynamically + // overrideable at the tearoff sites as they might not be + // tree-shaken. We still need a selector at the tearoff site. + (translator.dynamicModuleSupportEnabled && + _selectorMetadata[ + procedureMetadata.methodOrSetterSelectorId] + .tornOff))) { addMember(member.tearOffReference, staticDispatch); } } @@ -436,179 +506,238 @@ class DispatchTable { selectorsInClass[cls] = selectors; } - final selectorTargets = >{}; - final maxConcreteClassId = translator.classIdNumbering.maxConcreteClassId; - for (int classId = 0; classId <= maxConcreteClassId; ++classId) { - final cls = translator.classes[classId].cls; - if (cls != null) { - selectorsInClass[cls]!.forEach((selectorInfo, target) { - if (!target.asMember.isAbstract) { - selectorTargets.putIfAbsent(selectorInfo, () => {})[classId] = - target; - } - }); - } - } - - selectorTargets - .forEach((SelectorInfo selector, Map targets) { - selector.concreteClasses = targets.length; - - final List<({Range range, Reference target})> ranges = targets.entries - .map((entry) => - (range: Range(entry.key, entry.key), target: entry.value)) - .toList() - ..sort((a, b) => a.range.start.compareTo(b.range.start)); - assert(ranges.isNotEmpty); - int writeIndex = 0; - for (int readIndex = 1; readIndex < ranges.length; ++readIndex) { - final current = ranges[writeIndex]; - final next = ranges[readIndex]; - assert(next.range.length == 1); - if ((current.range.end + 1) == next.range.start && - identical(current.target, next.target)) { - ranges[writeIndex] = ( - range: Range(current.range.start, next.range.end), - target: current.target - ); - } else { - ranges[++writeIndex] = next; + List processTargets(int start, int end, bool isDynamicModule) { + final selectorTargets = >{}; + for (int classId = start; classId <= end; ++classId) { + final cls = translator.classes[classId].cls; + if (cls != null) { + selectorsInClass[cls]!.forEach((selectorInfo, target) { + if (!target.asMember.isAbstract) { + selectorTargets.putIfAbsent(selectorInfo, () => {})[classId] = + target; + } + }); } } - ranges.length = writeIndex + 1; - final staticDispatchRanges = (translator - .options.polymorphicSpecialization || - ranges.length == 1) - ? ranges - : ranges - .where((range) => staticDispatchPragmas.contains(range.target)) - .toList(); - - if (selector.useMultipleEntryPoints) { - ({Range range, Reference target}) getChecked( - ({Range range, Reference target}) targetRange, - bool unchecked, - ) => - ( - range: targetRange.range, - target: translator.getFunctionEntry(targetRange.target, - uncheckedEntry: unchecked) + selectorTargets + .forEach((SelectorInfo selector, Map targets) { + final List<({Range range, Reference target})> ranges = targets.entries + .map((entry) => + (range: Range(entry.key, entry.key), target: entry.value)) + .toList() + ..sort((a, b) => a.range.start.compareTo(b.range.start)); + assert(ranges.isNotEmpty); + int writeIndex = 0; + for (int readIndex = 1; readIndex < ranges.length; ++readIndex) { + final current = ranges[writeIndex]; + final next = ranges[readIndex]; + assert(next.range.length == 1); + if ((current.range.end + 1) == next.range.start && + identical(current.target, next.target)) { + ranges[writeIndex] = ( + range: Range(current.range.start, next.range.end), + target: current.target ); - - selector._normal = null; - selector._checked = SelectorTargets( - ranges.map((r) => getChecked(r, false)).toList(), - staticDispatchRanges.map((r) => getChecked(r, false)).toList(), - ); - selector._unchecked = SelectorTargets( - ranges.map((r) => getChecked(r, true)).toList(), - staticDispatchRanges.map((r) => getChecked(r, true)).toList(), - ); - } else { - selector._normal = SelectorTargets(ranges, staticDispatchRanges); - selector._checked = null; - selector._unchecked = null; - } - }); - - _selectorInfo.forEach((_, selector) { - if (!selectorTargets.containsKey(selector)) { - // There are no concrete implementations for the given [selector]. - // But there may be an abstract interface target which is targed by a - // call. In this case the call should be unreachable. - if (selector.useMultipleEntryPoints) { - selector._normal = null; - selector._checked = SelectorTargets([], []); - selector._unchecked = SelectorTargets([], []); - } else { - selector._normal = SelectorTargets([], []); - selector._checked = null; - selector._unchecked = null; + } else { + ranges[++writeIndex] = next; + } } - } - }); + ranges.length = writeIndex + 1; - // Assign selector offsets + final staticDispatchRanges = selector.isDynamicModuleOverrideable + ? const <({Range range, Reference target})>[] + : (translator.options.polymorphicSpecialization || + ranges.length == 1) + ? ranges + : ranges + .where( + (range) => staticDispatchPragmas.contains(range.target)) + .toList(); + if (selector.useMultipleEntryPoints) { + ({Range range, Reference target}) getChecked( + ({Range range, Reference target}) targetRange, + bool unchecked, + ) => + ( + range: targetRange.range, + target: translator.getFunctionEntry(targetRange.target, + uncheckedEntry: unchecked) + ); + final checkedTargets = SelectorTargets( + ranges.map((r) => getChecked(r, false)).toList(), + staticDispatchRanges.map((r) => getChecked(r, false)).toList(), + ); + final uncheckedTargets = SelectorTargets( + ranges.map((r) => getChecked(r, true)).toList(), + staticDispatchRanges.map((r) => getChecked(r, true)).toList(), + ); + final targets = ( + normal: null, + checked: checkedTargets, + unchecked: uncheckedTargets + ); + if (isDynamicModule) { + selector._dynamicModuleTargets = targets; + } else { + selector._mainModuleTargets = targets; + } + } else { + final normalTargets = SelectorTargets(ranges, staticDispatchRanges); + final targets = + (normal: normalTargets, checked: null, unchecked: null); + if (isDynamicModule) { + selector._dynamicModuleTargets = targets; + } else { + selector._mainModuleTargets = targets; + } + } + }); + + // Assign selector offsets + + bool isUsedViaDispatchTableCall(SelectorInfo selector) { + // Special case for `objectNoSuchMethod`: we introduce instance + // invocations of `objectNoSuchMethod` in dynamic calls, so keep it alive + // even if there was no references to it from the Dart code. + if (selector.isNoSuchMethod) { + return true; + } + if (selector.isDynamicModuleCallable) return true; + + if (selector.callCount == 0) { + return false; + } + if (selector.isDynamicModuleOverrideable) return true; + + final targets = + selector.targets(unchecked: false, dynamicModule: isDynamicModule); + + if (targets.targetRanges.length <= 1) return false; + if (!isDynamicModule && + targets.staticDispatchRanges.length == + targets.targetRanges.length) { + return false; + } - bool isUsedViaDispatchTableCall(SelectorInfo selector) { - if (selector.isNoSuchMethod) { return true; } - if (selector.callCount == 0) return false; - final targets = selector.targets(unchecked: false); + final List selectors = + selectorTargets.keys.where(isUsedViaDispatchTableCall).toList(); - if (targets.targetRanges.length <= 1) return false; - if (targets.staticDispatchRanges.length == targets.targetRanges.length) { - return false; + // Sort the selectors based on number of targets and number of use sites. + // This is a heuristic to keep the table small. + // + // Place selectors with more targets first as they are less likely to fit + // into the gaps left by selectors placed earlier. + // + // Among the selectors with approximately same number of targets, place + // more used ones first, as the smaller selector offset will have a smaller + // instruction encoding. + int selectorSortWeight(SelectorInfo selector) => + selectorTargets[selector]!.length * 10 + selector.callCount; + + selectors.sort((a, b) => selectorSortWeight(b) - selectorSortWeight(a)); + + final rows = >[]; + for (final selector in selectors) { + Row buildRow( + List<({Range range, Reference target})> targetRanges) { + final rowValues = <({int index, Reference value})>[]; + for (final (:range, :target) in targetRanges) { + for (int classId = range.start; classId <= range.end; ++classId) { + final adjustedClassId = classId - start; + rowValues.add((index: adjustedClassId, value: target)); + } + } + rowValues.sort((a, b) => a.index.compareTo(b.index)); + return Row(rowValues); + } + + final selectorTargets = isDynamicModule + ? selector._dynamicModuleTargets + : selector._mainModuleTargets; + + if (selector.useMultipleEntryPoints) { + rows.add(buildRow((selectorTargets.checked!.targetRanges))); + rows.add(buildRow((selectorTargets.unchecked!.targetRanges))); + } else { + rows.add(buildRow((selectorTargets.normal!.targetRanges))); + } } - return true; - } - final List selectors = - selectorTargets.keys.where(isUsedViaDispatchTableCall).toList(); + final table = buildRowDisplacementTable(rows); - // Sort the selectors based on number of targets and number of use sites. - // This is a heuristic to keep the table small. - // - // Place selectors with more targets first as they are less likely to fit - // into the gaps left by selectors placed earlier. - // - // Among the selectors with approximately same number of targets, place - // more used ones first, as the smaller selector offset will have a smaller - // instruction encoding. - int selectorSortWeight(SelectorInfo selector) => - selector.concreteClasses * 10 + selector.callCount; + int rowIndex = 0; + for (final selector in selectors) { + final selectorTargets = isDynamicModule + ? selector._dynamicModuleTargets + : selector._mainModuleTargets; + if (selector.useMultipleEntryPoints) { + selectorTargets.checked!.offset = rows[rowIndex++].offset; + selectorTargets.unchecked!.offset = rows[rowIndex++].offset; + } else { + selectorTargets.normal!.offset = rows[rowIndex++].offset; + } + } - selectors.sort((a, b) => selectorSortWeight(b) - selectorSortWeight(a)); - - final rows = >[]; - for (final selector in selectors) { - Row buildRow( - List<({Range range, Reference target})> targetRanges) { - final rowValues = <({int index, Reference value})>[]; - for (final (:range, :target) in targetRanges) { - for (int classId = range.start; classId <= range.end; ++classId) { - rowValues.add((index: classId, value: target)); + _selectorInfo.forEach((_, selector) { + if (!selectorTargets.containsKey(selector)) { + // There are no concrete implementations for the given [selector]. + // But there may be an abstract interface target which is targed by a + // call. In this case the call should be unreachable. + final targets = selector.useMultipleEntryPoints + ? ( + normal: null, + checked: SelectorTargets(const [], const []), + unchecked: SelectorTargets(const [], const []) + ) + : ( + normal: SelectorTargets(const [], const []), + checked: null, + unchecked: null, + ); + if (isDynamicModule) { + selector._dynamicModuleTargets = targets; + } else { + selector._mainModuleTargets = targets; } } - rowValues.sort((a, b) => a.index.compareTo(b.index)); - return Row(rowValues); - } + }); - if (selector.useMultipleEntryPoints) { - rows.add(buildRow(selector._checked!.targetRanges)); - rows.add(buildRow(selector._unchecked!.targetRanges)); - } else { - rows.add(buildRow(selector._normal!.targetRanges)); - } + return table; } - _table = buildRowDisplacementTable(rows); - - int rowIndex = 0; - for (final selector in selectors) { - if (selector.useMultipleEntryPoints) { - selector._checked!.offset = rows[rowIndex++].offset; - selector._unchecked!.offset = rows[rowIndex++].offset; - } else { - selector._normal!.offset = rows[rowIndex++].offset; - } - } + table = processTargets( + 0, translator.classIdNumbering.maxConcreteClassId, false); _definedWasmTable = - translator.mainModule.tables.define(_functionType, _table.length); - for (final module in translator.modules) { - // Ensure the dispatch table is imported into every module as the first - // table. - getWasmTable(module); + translator.mainModule.tables.define(_functionType, table.length); + if (!translator.dynamicModuleSupportEnabled) { + // Dynamic modules don't need direct access to the main module's table. + // Accesses are routed through global dispatch function refs. + for (final module in translator.modules) { + // Ensure the dispatch table is imported into every module as the first + // table. + getWasmTable(module); + } + } + + if (translator.isDynamicModule) { + _dynamicModuleTable = processTargets( + translator.classIdNumbering.firstDynamicModuleClassId, + translator.classIdNumbering.maxDynamicModuleConcreteClassId!, + true); + + _dynamicModuleDefinedWasmTable = translator.dynamicModule.tables + .define(_functionType, _dynamicModuleTable!.length); } } void output() { - for (int i = 0; i < _table.length; i++) { - Reference? target = _table[i]; + for (int i = 0; i < table.length; i++) { + Reference? target = table[i]; if (target != null) { w.BaseFunction? fun = translator.functions.getExistingFunction(target); // Any call to the dispatch table is guaranteed to hit a target. @@ -621,7 +750,7 @@ class DispatchTable { // module must've been loaded to call the constructor. if (fun != null) { final targetModule = translator.moduleForReference(target); - if (translator.isMainModule(targetModule)) { + if (targetModule == _definedWasmTable.enclosingModule) { _definedWasmTable.setElement(i, fun); } else { // This will generate the imported table if it doesn't already @@ -632,6 +761,29 @@ class DispatchTable { } } } + final dynamicModuleDefinedWasmTable = _dynamicModuleDefinedWasmTable; + if (dynamicModuleDefinedWasmTable != null) { + final dynamicModuleTable = _dynamicModuleTable!; + final targetModule = dynamicModuleDefinedWasmTable.enclosingModule; + for (int i = 0; i < dynamicModuleTable.length; i++) { + Reference? target = dynamicModuleTable[i]; + if (target != null) { + w.BaseFunction? fun = + translator.functions.getExistingFunction(target); + if (fun != null) { + if (fun.enclosingModule != targetModule) { + if (target.asMember + .isDynamicModuleCallable(translator.coreTypes)) { + fun = translator.functions.importFunctionToDynamicModule(fun); + } else { + continue; + } + } + dynamicModuleDefinedWasmTable.setElement(i, fun); + } + } + } + } } } diff --git a/pkg/dart2wasm/lib/dynamic_forwarders.dart b/pkg/dart2wasm/lib/dynamic_forwarders.dart index c1419084320..3c6ca81ea90 100644 --- a/pkg/dart2wasm/lib/dynamic_forwarders.dart +++ b/pkg/dart2wasm/lib/dynamic_forwarders.dart @@ -98,9 +98,11 @@ class Forwarder { void _generateGetterCode(Translator translator) { final selectors = translator.dispatchTable.dynamicGetterSelectors(memberName); + final isDynamicModule = translator.isDynamicModule && + function.enclosingModule == translator.dynamicModule; final ranges = selectors .expand((selector) => selector - .targets(unchecked: false) + .targets(unchecked: false, dynamicModule: isDynamicModule) .targetRanges .map((r) => (range: r.range, value: r.target))) .toList(); @@ -146,9 +148,11 @@ class Forwarder { void _generateSetterCode(Translator translator) { final selectors = translator.dispatchTable.dynamicSetterSelectors(memberName); + final isDynamicModule = translator.isDynamicModule && + function.enclosingModule == translator.dynamicModule; final ranges = selectors .expand((selector) => selector - .targets(unchecked: false) + .targets(unchecked: false, dynamicModule: isDynamicModule) .targetRanges .map((r) => (range: r.range, value: r.target))) .toList(); @@ -201,8 +205,11 @@ class Forwarder { for (final selector in methodSelectors) { // Accumulates all class ID ranges that have the same target. final Map> targets = {}; - for (final (:range, :target) - in selector.targets(unchecked: false).targetRanges) { + final isDynamicModule = translator.isDynamicModule && + function.enclosingModule == translator.dynamicModule; + for (final (:range, :target) in selector + .targets(unchecked: false, dynamicModule: isDynamicModule) + .targetRanges) { targets.putIfAbsent(target, () => []).add(range); } @@ -481,9 +488,12 @@ class Forwarder { final getterSelectors = translator.dispatchTable.dynamicGetterSelectors(memberName); final getterValueLocal = b.addLocal(translator.topInfo.nullableType); + final isDynamicModule = translator.isDynamicModule && + function.enclosingModule == translator.dynamicModule; for (final selector in getterSelectors) { - for (final (:range, :target) - in selector.targets(unchecked: false).targetRanges) { + for (final (:range, :target) in selector + .targets(unchecked: false, dynamicModule: isDynamicModule) + .targetRanges) { for (int classId = range.start; classId <= range.end; ++classId) { final targetMember = target.asMember; // This loop checks getters and fields. Methods are considered in the @@ -528,7 +538,8 @@ class Forwarder { // Invoke "call" if the value is not a closure b.struct_get(translator.topInfo.struct, FieldIndex.classId); - b.i32_const(translator.closureInfo.classId); + b.i32_const( + (translator.closureInfo.classId as AbsoluteClassId).value); b.i32_ne(); b.if_(); // Value is not a closure @@ -780,7 +791,7 @@ void generateNoSuchMethodCall( // Get class id for virtual call pushReceiver(); translator.callDispatchTable(b, noSuchMethodSelector, - useUncheckedEntry: false); + interfaceTarget: translator.objectNoSuchMethod, useUncheckedEntry: false); } class ClassIdRange { diff --git a/pkg/dart2wasm/lib/dynamic_module_kernel_metadata.dart b/pkg/dart2wasm/lib/dynamic_module_kernel_metadata.dart new file mode 100644 index 00000000000..d7afdb63ed8 --- /dev/null +++ b/pkg/dart2wasm/lib/dynamic_module_kernel_metadata.dart @@ -0,0 +1,256 @@ +// 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 'compiler_options.dart'; +import 'serialization.dart'; +import 'translator.dart'; + +/// Repository for kernel global entity IDs. +/// +/// Each class and member gets annotated with a unique ID that will allow us to +/// persist metadata about that entity across compilations. +class DynamicModuleGlobalIdRepository extends MetadataRepository { + static const repositoryTag = 'wasm.dynamic-modules.globalId'; + + @override + final String tag = repositoryTag; + + @override + final Map mapping = {}; + + @override + int readFromBinary(Node node, BinarySource source) { + throw UnsupportedError(''); + } + + @override + void writeToBinary(int globalId, Node node, BinarySink sink) {} +} + +/// Metadata produced by the main module. +/// +/// This data will get serialized as part of the main module compilation process +/// and will be provided as an input to be deserialized by subsequent dynamic +/// module compilations. +class DynamicModuleMetadata { + /// Global kernel class ID to dart2wasm class hierarchy class ID. + final Map classIds; + + /// Global kernel member ID to getter and setter/method selector ID. + final Map selectorIds; + + /// Global kernel class IDs in class hierarchy dfs order. + final List dfsOrderClassIds; + + /// References for all targets callable from the main module represented as + /// member global ID and reference type. + final List<(int, int)> callableReferences; + + /// Key names of updateable functions defined in the main module. + final Map updateableFunctionsInMain; + + /// Saved flags from the main module to verify that settings have not changed + /// between main module invocation and dynamic module invocation. + final TranslatorOptions mainModuleTranslatorOptions; + final Map mainModuleEnvironment; + + DynamicModuleMetadata( + this.classIds, + this.selectorIds, + this.dfsOrderClassIds, + this.callableReferences, + this.updateableFunctionsInMain, + this.mainModuleTranslatorOptions, + this.mainModuleEnvironment); + + void serialize(BinaryDataSink sink) { + sink.writeInt(classIds.length); + classIds.forEach((globalClassId, classId) { + sink.writeInt(globalClassId); + sink.writeClassId(classId); + }); + sink.writeInt(selectorIds.length); + selectorIds.forEach((globalMemberId, selectorIds) { + sink.writeInt(globalMemberId); + sink.writeInt(selectorIds.$1); + sink.writeInt(selectorIds.$2); + }); + sink.writeInt(dfsOrderClassIds.length); + for (final classId in dfsOrderClassIds) { + sink.writeClassId(classId); + } + sink.writeInt(callableReferences.length); + for (final callableMemberId in callableReferences) { + sink.writeInt(callableMemberId.$1); + sink.writeInt(callableMemberId.$2); + } + sink.writeInt(updateableFunctionsInMain.length); + updateableFunctionsInMain.forEach((stringKey, key) { + sink.writeString(stringKey); + sink.writeInt(key); + }); + + mainModuleTranslatorOptions.serialize(sink); + sink.writeInt(mainModuleEnvironment.length); + mainModuleEnvironment.forEach((k, v) { + sink.writeString(k); + sink.writeString(v); + }); + } + + static DynamicModuleMetadata deserialize(BinaryDataSource source) { + final classIdMappingLength = source.readInt(); + final classIds = {}; + for (int i = 0; i < classIdMappingLength; i++) { + final globalClassId = source.readInt(); + final classId = source.readClassId(); + classIds[globalClassId] = classId; + } + + final selectorIdMappingLength = source.readInt(); + final selectorIds = {}; + for (int i = 0; i < selectorIdMappingLength; i++) { + final globalMemberId = source.readInt(); + final getterSelectorId = source.readInt(); + final setterOrMethodSelectorId = source.readInt(); + selectorIds[globalMemberId] = + (getterSelectorId, setterOrMethodSelectorId); + } + final dfsOrderClassIdsLength = source.readInt(); + final dfsOrderClassIds = []; + for (int i = 0; i < dfsOrderClassIdsLength; i++) { + dfsOrderClassIds.add(source.readClassId()); + } + final callableMemberIdsLength = source.readInt(); + final callableMemberIds = <(int, int)>[]; + for (int i = 0; i < callableMemberIdsLength; i++) { + callableMemberIds.add((source.readInt(), source.readInt())); + } + final updateableFunctionsInMainLength = source.readInt(); + final updateableFunctionsInMain = {}; + for (int i = 0; i < updateableFunctionsInMainLength; i++) { + final stringKey = source.readString(); + final key = source.readInt(); + updateableFunctionsInMain[stringKey] = key; + } + final mainModuleTranslatorOptions = TranslatorOptions.deserialize(source); + final mainModuleEnvironmentLength = source.readInt(); + final mainModuleEnvironment = {}; + for (int i = 0; i < mainModuleEnvironmentLength; i++) { + final key = source.readString(); + final value = source.readString(); + mainModuleEnvironment[key] = value; + } + + return DynamicModuleMetadata( + classIds, + selectorIds, + dfsOrderClassIds, + callableMemberIds, + updateableFunctionsInMain, + mainModuleTranslatorOptions, + mainModuleEnvironment); + } + + static void verifyMainModuleOptions(WasmCompilerOptions options) { + final translatorOptions = options.translatorOptions; + if (translatorOptions.enableDeferredLoading) { + throw StateError( + 'Cannot use enable-deferred-loading with dynamic modules.'); + } + if (translatorOptions.enableMultiModuleStressTestMode) { + throw StateError( + 'Cannot use multi-module-stress-test-mode with dynamic modules.'); + } + if (translatorOptions.enableMultiModuleStressTestMode) { + throw StateError( + 'Cannot use multi-module-stress-test-mode with dynamic modules.'); + } + } + + void verifyDynamicModuleOptions(WasmCompilerOptions options) { + final translatorOptions = options.translatorOptions; + + Never fail(String optionName) { + throw StateError( + 'Inconsistent flag for dynamic module compilation: $optionName'); + } + + // TODO(natebiggs): Disallow certain flags from being used in conjunction + // with dynamic modules. + + if (translatorOptions.enableAsserts != + mainModuleTranslatorOptions.enableAsserts) { + fail('enable-asserts'); + } + if (translatorOptions.importSharedMemory != + mainModuleTranslatorOptions.importSharedMemory) { + fail('import-shared-memory'); + } + if (translatorOptions.inlining != translatorOptions.inlining) { + fail('inlining'); + } + if (translatorOptions.jsCompatibility != + mainModuleTranslatorOptions.jsCompatibility) { + fail('js-compatibility'); + } + if (translatorOptions.omitImplicitTypeChecks != + mainModuleTranslatorOptions.omitImplicitTypeChecks) { + fail('omit-implicit-checks'); + } + if (translatorOptions.omitExplicitTypeChecks != + mainModuleTranslatorOptions.omitExplicitTypeChecks) { + fail('omit-explicit-checks'); + } + if (translatorOptions.omitBoundsChecks != + mainModuleTranslatorOptions.omitBoundsChecks) { + fail('omit-bounds-checks'); + } + if (translatorOptions.polymorphicSpecialization != + mainModuleTranslatorOptions.polymorphicSpecialization) { + fail('polymorphic-specialization'); + } + // Skip printKernel + // Skip printWasm + if (translatorOptions.minify != mainModuleTranslatorOptions.minify) { + fail('minify'); + } + if (translatorOptions.verifyTypeChecks != + mainModuleTranslatorOptions.verifyTypeChecks) { + fail('verify-type-checks'); + } + // Skip verbose + if (translatorOptions.enableExperimentalFfi != + mainModuleTranslatorOptions.enableExperimentalFfi) { + fail('enable-experimental-ffi'); + } + if (translatorOptions.enableExperimentalWasmInterop != + mainModuleTranslatorOptions.enableExperimentalWasmInterop) { + fail('enable-experimental-wasm-interop'); + } + // Skip generate source maps + if (translatorOptions.enableDeferredLoading != + mainModuleTranslatorOptions.enableDeferredLoading) { + fail('enable-deferred-loading'); + } + if (translatorOptions.enableMultiModuleStressTestMode != + mainModuleTranslatorOptions.enableMultiModuleStressTestMode) { + fail('enable-multi-module-stress-test-mode'); + } + if (translatorOptions.inliningLimit != + mainModuleTranslatorOptions.inliningLimit) { + fail('inlining-limit'); + } + if (translatorOptions.sharedMemoryMaxPages != + mainModuleTranslatorOptions.sharedMemoryMaxPages) { + fail('shared-memory-max-pages'); + } + + if (!mapEquals(options.environment, mainModuleEnvironment)) { + fail('environment mismatch'); + } + } +} diff --git a/pkg/dart2wasm/lib/dynamic_modules.dart b/pkg/dart2wasm/lib/dynamic_modules.dart new file mode 100644 index 00000000000..428cc81ee22 --- /dev/null +++ b/pkg/dart2wasm/lib/dynamic_modules.dart @@ -0,0 +1,1163 @@ +// 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/procedure_attributes.dart' + show ProcedureAttributesMetadataRepository; +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 '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 _dynamicModuleEntryPoint = + Expando(); + + Procedure? get dynamicModuleEntryPoint => _dynamicModuleEntryPoint[this]; + List getMainModuleLibraries(CoreTypes coreTypes) => + [...libraries.where((l) => hasPragma(coreTypes, l, _mainLibPragma))]; +} + +extension DynamicModuleClass on Class { + bool isDynamicModuleExtendable(CoreTypes coreTypes) => + hasPragma(coreTypes, this, kDynModuleExtendablePragmaName) || + hasPragma(coreTypes, this, kDynModuleImplicitlyExtendablePragmaName); +} + +extension DynamicModuleMember on Member { + bool isDynamicModuleCallable(CoreTypes coreTypes) => + hasPragma(coreTypes, this, kDynModuleCallablePragmaName) || + hasPragma(coreTypes, this, kDynModuleImplicitlyCallablePragmaName); + + bool isDynamicModuleOverrideable(CoreTypes coreTypes) => + hasPragma(coreTypes, this, kDynModuleCanBeOverriddenPragmaName) || + hasPragma(coreTypes, this, kDynModuleCanBeOverriddenImplicitlyPragmaName); +} + +class DynamicMainModuleStrategy extends DefaultModuleStrategy with KernelNodes { + @override + final CoreTypes coreTypes; + @override + final LibraryIndex index; + final ClassHierarchy hierarchy; + final Uri dynamicInterfaceSpecificationBaseUri; + final String dynamicInterfaceSpecification; + + DynamicMainModuleStrategy( + super.component, + this.coreTypes, + this.hierarchy, + 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(); + _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 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); + } + + // 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 ClassHierarchy hierarchy; + final Set _mainModuleLibraries = {}; + final Set _dynamicModuleLibraries = {}; + + DynamicModuleStrategy(super.component, this.options, this.kernelTarget, + this.coreTypes, this.hierarchy, 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, component.mode); + } + } + } + + 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 ModuleOutputData([mainModule, dynamicModule], const {}); + } +} + +enum BuiltinUpdatableFunctions { + recordId; + + static int _keyOffset = values.length; +} + +class DynamicModuleInfo { + final Translator translator; + Procedure? get dynamicEntryPoint => + translator.component.dynamicModuleEntryPoint; + bool get isDynamicModule => dynamicEntryPoint != null; + late final w.FunctionBuilder initFunction; + + Map? _classIdMapping; + Map? get classIdMapping => _classIdMapping; + + Map? _selectorIds; + Map? get selectorIds => _selectorIds; + + List? _callableReferences; + List? get callableReferences => _callableReferences; + + List? _dfsOrderClassIds; + List? get dfsOrderClassIds => _dfsOrderClassIds; + + late final w.Global moduleIdGlobal; + + final Map _updateableDispatchKeys; + // null is used to indicate that skipDynamic was passed for this key. + final Map _updateableFunctions = {}; + + final Map> + _constantCacheCheckers = {}; + final Map> + _mutableArrayConstantCacheCheckers = {}; + final Map> + _immutableArrayConstantCacheCheckers = {}; + + late final w.ModuleBuilder dynamicModule = + translator.modules.firstWhere((m) => m != translator.mainModule); + + DynamicModuleInfo(this.translator, DynamicModuleMetadata? serializedMetadata) + : _updateableDispatchKeys = + serializedMetadata?.updateableFunctionsInMain ?? {} { + if (serializedMetadata != null) { + assert(isDynamicModule); + final globalIdRepository = translator + .component.metadata[DynamicModuleGlobalIdRepository.repositoryTag] + as DynamicModuleGlobalIdRepository; + final Map globalIdToNode = {}; + globalIdRepository.mapping.forEach((node, globalId) { + globalIdToNode[globalId] = node; + }); + + final assignedClassIds = {}; + serializedMetadata.classIds.forEach((globalId, classId) { + assignedClassIds[globalIdToNode[globalId] as Class] = classId; + }); + _classIdMapping = assignedClassIds; + + final assignedSelectorIds = {}; + serializedMetadata.selectorIds.forEach((globalId, selectorIds) { + assignedSelectorIds[globalIdToNode[globalId] as Member] = selectorIds; + }); + _selectorIds = assignedSelectorIds; + + _dfsOrderClassIds = [ + for (final globalId in serializedMetadata.dfsOrderClassIds) + globalIdToNode[globalId] as Class + ]; + + _callableReferences = [ + for (final reference in serializedMetadata.callableReferences) + _referenceForFlag( + globalIdToNode[reference.$1] as Member, reference.$2) + ]; + } + } + + 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(); + } + } + + 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 finishDynamicModule() { + _registerModuleRefs( + isDynamicModule ? initFunction.body : translator.initFunction.body); + + initFunction.body.end(); + } + + void _registerModuleRefs(w.InstructionsBuilder b) { + final numKeys = _updateableFunctions.length; + assert(numKeys < maxArrayNewFixedLength); + for (int key = 0; key < numKeys; key++) { + final function = _updateableFunctions[key]; + 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(); + } + + void _maybeCreateUpdateableFunction(int key, w.FunctionType type, + {required void Function(w.FunctionBuilder function) buildMain, + required void Function(w.FunctionBuilder function) buildDynamic, + bool skipDynamic = false, + String? name}) { + if (!_updateableFunctions.containsKey(key)) { + final mainFunction = + translator.mainModule.functions.define(type, name ?? '$key main'); + translator.mainModule.functions.declare(mainFunction); + _updateableFunctions[key] = mainFunction; + buildMain(mainFunction); + + if (isDynamicModule) { + if (skipDynamic) { + _updateableFunctions[key] = null; + } else { + final dynamicModuleFunction = + dynamicModule.functions.define(type, name ?? '$key dyn'); + dynamicModule.functions.declare(dynamicModuleFunction); + _updateableFunctions[key] = dynamicModuleFunction; + buildDynamic(dynamicModuleFunction); + } + } + } + } + + void _callClassIdBranch( + int key, w.InstructionsBuilder b, w.FunctionType signature, + {required void Function(w.FunctionBuilder b) buildMainMatch, + required void Function(w.FunctionBuilder b) buildDynamicMatch, + bool skipDynamic = false, + 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; + _maybeCreateUpdateableFunction(key, signature, + buildMain: buildMainMatch, + buildDynamic: buildDynamicMatch, + skipDynamic: canSkipDynamicBranch, + name: name); + + translator.callReference(translator.classIdToModuleId.reference, b); + b.i64_const(key); + // 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, w.FunctionType signature, + {required void Function(w.FunctionBuilder b) buildMainMatch, + required void Function(w.FunctionBuilder b) buildDynamicMatch, + bool skipDynamic = false}) { + _callClassIdBranch(key.index, b, signature, + buildMainMatch: buildMainMatch, + buildDynamicMatch: buildDynamicMatch, + name: '#r${key.index}_${key.name}', + skipDynamic: skipDynamic); + } + + void callUpdateableDispatch( + w.InstructionsBuilder b, SelectorInfo selector, Member interfaceTarget, + {required bool useUncheckedEntry}) { + final signature = selector.signature; + // The shared entry point to this selector has to use 'any' because the + // selector's signature may change between compilations. The entry points + // must maintain the same type though. + // TODO(natebiggs): This doesn't account for overrides with extra + // parameters. + final updatedSignature = 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)) + ]); + final mainModuleId = selector.mainModuleIdForTarget(interfaceTarget); + + final name = + selector.isNoSuchMethod ? '#nsm' : '#$mainModuleId-${selector.name}'; + + // If any input is not a RefType (i.e. it's an unboxed value) then wrap it + // so the updated signature works. + if (signature.inputs.any((i) => i is! w.RefType)) { + final receiverLocal = b.addLocal(translator.topInfo.nullableType); + b.local_set(receiverLocal); + final locals = []; + for (final input in signature.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 key = _updateableDispatchKeys[name] ??= + _updateableDispatchKeys.length + BuiltinUpdatableFunctions._keyOffset; + + void Function(w.FunctionBuilder) buildSelectorBranch(bool dynamicModule) { + return (w.FunctionBuilder function) { + final ib = function.body; + + final offset = selector + .targets(unchecked: useUncheckedEntry, dynamicModule: dynamicModule) + .offset; + + if (offset == null) { + ib.unreachable(); + ib.end(); + return; + } + + for (int i = 0; i < ib.locals.length - 1; i++) { + ib.local_get(ib.locals[i]); + translator.convertType( + ib, updatedSignature.inputs[i], signature.inputs[i]); + } + ib.local_get(ib.locals.last); + if (dynamicModule) { + translator.callReference(translator.scopeClassId.reference, ib); + } + if (offset != 0) { + ib.i32_const(offset); + ib.i32_add(); + } + final table = dynamicModule + ? translator.dispatchTable.dynamicModuleDefinedWasmTable + : translator.dispatchTable.getWasmTable(translator.mainModule); + ib.call_indirect(signature, table); + translator.convertType( + ib, signature.outputs.single, updatedSignature.outputs.single); + ib.end(); + }; + } + + _callClassIdBranch(key, b, updatedSignature, + name: '#s${key}_$name', + buildMainMatch: buildSelectorBranch(false), + buildDynamicMatch: buildSelectorBranch(true), + skipDynamic: translator.isDynamicModule && + selector + .targets(unchecked: useUncheckedEntry, dynamicModule: true) + .targetRanges + .isEmpty); + translator.convertType( + b, updatedSignature.outputs.single, signature.outputs.single); + } + + DynamicModuleMetadata toMetadata(WasmCompilerOptions options) { + assert(!isDynamicModule); + final globalIdRepository = translator + .component.metadata[DynamicModuleGlobalIdRepository.repositoryTag] + as DynamicModuleGlobalIdRepository; + + final classIdMapping = {}; + translator.classIdNumbering.classIds.forEach((cls, id) { + classIdMapping[globalIdRepository.mapping[cls]!] = + (id as AbsoluteClassId).value; + }); + + final selectorIdMapping = {}; + final procedureMetadata = + (translator.component.metadata["vm.procedure-attributes.metadata"] + as ProcedureAttributesMetadataRepository) + .mapping; + for (final library in translator.libraries) { + for (final cls in library.classes) { + for (final member in cls.procedures) { + if (!member.isInstanceMember) continue; + final globalId = globalIdRepository.mapping[member]; + // TFA might add placeholders for removed members. We can ignore + // these. + if (globalId == null) continue; + selectorIdMapping[globalId] = ( + procedureMetadata[member]!.getterSelectorId, + procedureMetadata[member]!.methodOrSetterSelectorId + ); + } + for (final member in cls.fields) { + if (!member.isInstanceMember) continue; + final globalId = globalIdRepository.mapping[member]; + // TFA might add placeholders for removed members. We can ignore + // these. + if (globalId == null) continue; + selectorIdMapping[globalId] = ( + procedureMetadata[member]!.getterSelectorId, + procedureMetadata[member]!.methodOrSetterSelectorId + ); + } + } + } + + final dfsOrderClassIds = []; + for (final cls in translator.classIdNumbering.dfsOrder) { + dfsOrderClassIds.add(globalIdRepository.mapping[cls]!); + } + + final callableMemberIds = <(int, int)>[]; + for (final export in translator.functions.dynamicModuleCallable) { + final exportMember = export.asMember; + final flag = _flagForReference(export); + callableMemberIds.add((globalIdRepository.mapping[exportMember]!, flag)); + } + return DynamicModuleMetadata( + classIdMapping, + selectorIdMapping, + dfsOrderClassIds, + callableMemberIds, + _updateableDispatchKeys, + options.translatorOptions, + options.environment); + } + + static int _flagForReference(Reference reference) { + if (reference.isImplicitGetter) return 0; + if (reference.isImplicitSetter) return 1; + if (reference.isTearOffReference) return 2; + if (reference.isConstructorBodyReference) return 3; + if (reference.isInitializerReference) return 4; + if (reference.isTypeCheckerReference) return 5; + if (reference.isCheckedEntryReference) return 6; + if (reference.isUncheckedEntryReference) return 7; + if (reference.isBodyReference) return 8; + assert(reference == reference.asMember.reference); + return 9; + } + + static Reference _referenceForFlag(Member member, int flag) { + if (flag == 0) return (member as Field).getterReference; + if (flag == 1) return (member as Field).setterReference!; + if (flag == 2) return (member as Procedure).tearOffReference; + if (flag == 3) return (member as Constructor).constructorBodyReference; + if (flag == 4) return (member as Constructor).initializerReference; + if (flag == 5) return member.typeCheckerReference; + if (flag == 6) return member.checkedEntryReference; + if (flag == 7) return member.uncheckedEntryReference; + if (flag == 8) return member.bodyReference; + assert(flag == 9); + return member.reference; + } +} + +/// 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 { + 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 _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 _boxedClasses = { + translator.boxedIntClass, + translator.boxedDoubleClass, + translator.boxedBoolClass, + }; + + /// Values of these types are canonicalized by their == function. + late final Set _equalsCheckerClasses = { + if (translator.options.jsCompatibility) translator.jsStringClass, + if (!translator.options.jsCompatibility) translator.oneByteStringClass, + if (!translator.options.jsCompatibility) translator.twoByteStringClass, + translator.symbolClass, + translator.closureClass, + }; + + /// These iterable classes contain lazily initialized data that should not be + /// considered in comparisons. + late final Set _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, + useUncheckedEntry: true); + } + + /// 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? 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.options.jsCompatibility + ? translator.jsStringClass + : (node.value.codeUnits.every((c) => c <= 255) + ? translator.oneByteStringClass + : translator.twoByteStringClass)); + } + + @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.'); + } +} diff --git a/pkg/dart2wasm/lib/functions.dart b/pkg/dart2wasm/lib/functions.dart index e0065a49549..192ccef6188 100644 --- a/pkg/dart2wasm/lib/functions.dart +++ b/pkg/dart2wasm/lib/functions.dart @@ -3,12 +3,16 @@ // BSD-style license that can be found in the LICENSE file. import 'package:kernel/ast.dart'; +import 'package:vm/metadata/procedure_attributes.dart' + show ProcedureAttributesMetadataRepository; 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 'intrinsics.dart'; import 'reference_extensions.dart'; import 'translator.dart'; @@ -33,6 +37,12 @@ class FunctionCollector { // if an allocation of that class is encountered final Map> _pendingAllocation = {}; + /// Collection of references marked as callable from dynamic modules. + Set dynamicModuleCallable = {}; + + late final WasmFunctionImporter _importedDynamicModuleFunctions = + WasmFunctionImporter(translator, '#dmf'); + FunctionCollector(this.translator); void _collectImportsAndExports() { @@ -80,9 +90,32 @@ class FunctionCollector { /// 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) { @@ -108,6 +141,10 @@ class FunctionCollector { } } + if (translator.dynamicModuleSupportEnabled) { + _generateDynamicModuleCallableReferences(); + } + // Value classes are always implicitly allocated. recordClassAllocation( translator.classInfo[translator.boxedBoolClass]!.classId); @@ -117,6 +154,98 @@ class FunctionCollector { translator.classInfo[translator.boxedDoubleClass]!.classId); } + void _generateDynamicModuleCallableReferences() { + final references = dynamicModuleCallable = translator.isDynamicModule + ? translator.dynamicModuleInfo!.callableReferences!.toSet() + : _generateCallableReferences(); + + for (final reference in references) { + final member = reference.asMember; + + if (member.isInstanceMember) { + final selector = translator.dispatchTable.selectorForTarget(reference); + final targetRanges = selector + .targets(unchecked: false, dynamicModule: false) + .targetRanges + .followedBy(selector + .targets(unchecked: true, dynamicModule: false) + .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) { + _recordClassTargetUse(classId, target); + } + } + } else { + // Generate static members immediately since they are unconditionally + // callable. + getFunction(reference); + } + } + } + + Set _generateCallableReferences() { + assert( + translator.dynamicModuleSupportEnabled && !translator.isDynamicModule); + + final exports = {}; + 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; + } + exports.add(reference); + } + + final procedureAttributeMetadata = + (translator.component.metadata["vm.procedure-attributes.metadata"] + as ProcedureAttributesMetadataRepository) + .mapping; + void collectCallableReferences(Member member) { + if (member is Procedure) { + collectCallableReference(member.reference); + if (member.isInstanceMember && + member.kind == ProcedureKind.Method && + procedureAttributeMetadata[member]!.hasTearOffUses) { + collectCallableReference(member.tearOffReference); + } + } else if (member is Field) { + collectCallableReference(member.getterReference); + if (member.hasSetter) { + collectCallableReference(member.setterReference!); + } + } else if (member is Constructor) { + if (translator.classInfo[member.enclosingClass]!.struct + .isSubtypeOf(translator.objectInfo.struct)) { + collectCallableReference(member.reference); + collectCallableReference(member.initializerReference); + collectCallableReference(member.constructorBodyReference); + } + } + } + + for (final lib in translator.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); + } + } + } + + return exports; + } + w.BaseFunction? getExistingFunction(Reference target) { return _functions[target]; } @@ -128,6 +257,15 @@ class FunctionCollector { 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 && + dynamicModuleCallable.contains(target)) { + assert(translator.dynamicModuleSupportEnabled); + _importedDynamicModuleFunctions.get(function, translator.dynamicModule, + exportOnly: true); + } return function; }); } @@ -177,8 +315,7 @@ class FunctionCollector { if (target.isTearOffReference) { assert(!translator.dispatchTable .selectorForTarget(target) - .targetSet - .contains(target)); + .containsTarget(target)); return translator.signatureForDirectCall(target); } @@ -236,25 +373,44 @@ class FunctionCollector { final set = useUncheckedEntry ? _calledUncheckedSelectors : _calledSelectors; if (set.add(selector.id)) { - for (final (:range, :target) - in selector.targets(unchecked: useUncheckedEntry).targetRanges) { + for (final (:range, :target) in selector + .targets(unchecked: useUncheckedEntry, dynamicModule: false) + .targetRanges) { for (int classId = range.start; classId <= range.end; ++classId) { - 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); + _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 recordClassAllocation(int classId) { - if (_allocatedClasses.add(classId)) { + 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[classId] ?? const []) { + for (Reference target in _pendingAllocation[id] ?? const []) { getFunction(target); } } @@ -278,8 +434,7 @@ class _FunctionTypeGenerator extends MemberVisitor1 { } assert(!translator.dispatchTable .selectorForTarget(target) - .targetSet - .contains(target)); + .containsTarget(target)); final receiverType = target.asMember.enclosingClass! .getThisType(translator.coreTypes, Nullability.nonNullable); @@ -289,15 +444,16 @@ class _FunctionTypeGenerator extends MemberVisitor1 { @override w.FunctionType visitProcedure(Procedure node, Reference target) { - assert(!node.isAbstract); + // 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) - .targetSet - .contains(target)); + .containsTarget(target)); final receiverType = target.asMember.enclosingClass! .getThisType(translator.coreTypes, Nullability.nonNullable); diff --git a/pkg/dart2wasm/lib/generate_wasm.dart b/pkg/dart2wasm/lib/generate_wasm.dart index 09986a62334..6469905db4a 100644 --- a/pkg/dart2wasm/lib/generate_wasm.dart +++ b/pkg/dart2wasm/lib/generate_wasm.dart @@ -114,7 +114,10 @@ Future generateWasm(WasmCompilerOptions options, await Future.wait(writeFutures); final jsFile = path.setExtension(options.outputFile, '.mjs'); - await File(jsFile).writeAsString(result.jsRuntime); + final jsRuntime = result.jsRuntime; + if (jsRuntime != null) { + await File(jsFile).writeAsString(jsRuntime); + } final supportJsFile = path.setExtension(options.outputFile, '.support.js'); await File(supportJsFile).writeAsString(result.supportJs); diff --git a/pkg/dart2wasm/lib/globals.dart b/pkg/dart2wasm/lib/globals.dart index eb2a0672ba4..8f99b930dfc 100644 --- a/pkg/dart2wasm/lib/globals.dart +++ b/pkg/dart2wasm/lib/globals.dart @@ -76,7 +76,8 @@ class Globals { final module = translator.moduleForReference(field.fieldReference); final memberName = field.toString(); if (init != null && - !(translator.constants.ensureConstant(init)?.isLazy ?? false)) { + !(translator.constants.ensureConstant(init, module)?.isLazy ?? + false)) { // Initialized to a constant final global = module.globals.define( w.GlobalType(fieldType, mutable: !field.isFinal), memberName); diff --git a/pkg/dart2wasm/lib/intrinsics.dart b/pkg/dart2wasm/lib/intrinsics.dart index 4ec3bb5e590..ca9512cad70 100644 --- a/pkg/dart2wasm/lib/intrinsics.dart +++ b/pkg/dart2wasm/lib/intrinsics.dart @@ -10,6 +10,7 @@ import 'abi.dart' show kWasmAbiEnumIndex; import 'class_info.dart'; import 'code_generator.dart'; import 'dynamic_forwarders.dart'; +import 'dynamic_modules.dart'; import 'translator.dart'; import 'types.dart'; import 'util.dart'; @@ -187,7 +188,8 @@ enum StaticIntrinsic { storeFloat('dart:ffi', null, '_storeFloat'), storeFloatUnaligned('dart:ffi', null, '_storeFloatUnaligned'), storeDouble('dart:ffi', null, '_storeDouble'), - storeDoubleUnaligned('dart:ffi', null, '_storeDoubleUnaligned'); + storeDoubleUnaligned('dart:ffi', null, '_storeDoubleUnaligned'), + ; final String library; final String? cls; @@ -764,6 +766,10 @@ class Intrinsifier { // ClassID getters if (cls?.name == 'ClassID') { + if (target.name.text == 'maxClassId') { + codeGen.b.i32_const(translator.classIdNumbering.maxClassId); + return w.NumType.i32; + } final libAndClassName = translator.getPragma(target, "wasm:class-id"); if (libAndClassName != null) { List libAndClassNameParts = libAndClassName.split("#"); @@ -778,8 +784,8 @@ class Intrinsifier { orElse: () => throw 'Class $className not found in library $lib ' '(${target.location})'); - int classId = translator.classInfo[cls]!.classId; - b.i32_const(classId); + ClassId classId = translator.classInfo[cls]!.classId; + b.pushClassIdToStack(translator, classId); return w.NumType.i32; } @@ -790,6 +796,13 @@ class Intrinsifier { } } + if (target.enclosingLibrary.name == 'dart._internal') { + if (target.name.text == '_numClassesForConstCaches') { + b.i64_const(translator.classIdNumbering.maxClassId); + return w.NumType.i64; + } + } + if (node.target.enclosingLibrary == translator.coreTypes.coreLibrary) { switch (target.name.text) { case "_isIntrinsified": @@ -801,41 +814,12 @@ class Intrinsifier { case "_noSubstitutionIndex": b.i32_const(RuntimeTypeInformation.noSubstitutionIndex); return w.NumType.i32; - case "_typeRowDisplacementOffsets": - final type = translator - .translateStorageType( - types.rtt.typeRowDisplacementOffsets.interfaceType) - .unpacked; + case "_mainModuleRtt": + final moduleRttType = translator.translateType( + InterfaceType(translator.moduleRtt, Nullability.nonNullable)); translator.constants.instantiateConstant( - b, types.rtt.typeRowDisplacementOffsets, type); - return type; - case "_typeRowDisplacementTable": - final type = translator - .translateStorageType( - types.rtt.typeRowDisplacementTable.interfaceType) - .unpacked; - translator.constants - .instantiateConstant(b, types.rtt.typeRowDisplacementTable, type); - return type; - case "_typeRowDisplacementSubstTable": - final type = translator - .translateStorageType( - types.rtt.typeRowDisplacementSubstTable.interfaceType) - .unpacked; - translator.constants.instantiateConstant( - b, types.rtt.typeRowDisplacementSubstTable, type); - return type; - case "_typeNames": - final type = translator - .translateStorageType(types.rtt.typeNames.interfaceType) - .unpacked; - if (translator.options.minify) { - b.ref_null((type as w.RefType).heapType); - } else { - translator.constants - .instantiateConstant(b, types.rtt.typeNames, type); - } - return type; + b, translator.types.rtt.mainModuleRtt, moduleRttType); + return moduleRttType; } } @@ -1065,8 +1049,9 @@ class Intrinsifier { case StaticIntrinsic.isObjectClassId: final classId = node.arguments.positional.single; - final objectClassId = translator - .classIdNumbering.classIds[translator.coreTypes.objectClass]!; + final objectClassId = (translator.classIdNumbering + .classIds[translator.coreTypes.objectClass] as AbsoluteClassId) + .value; codeGen.translateExpression(classId, w.NumType.i32); b.emitClassIdRangeCheck([Range(objectClassId, objectClassId)]); @@ -1075,21 +1060,54 @@ class Intrinsifier { final classId = node.arguments.positional.single; final ranges = translator.classIdNumbering - .getConcreteClassIdRanges(translator.coreTypes.functionClass); + .getConcreteClassIdRangeForMainModule( + translator.coreTypes.functionClass); assert(ranges.length <= 1); codeGen.translateExpression(classId, w.NumType.i32); b.emitClassIdRangeCheck(ranges); + return w.NumType.i32; case StaticIntrinsic.isRecordClassId: final classId = node.arguments.positional.single; - final ranges = translator.classIdNumbering - .getConcreteClassIdRanges(translator.coreTypes.recordClass); + .getConcreteClassIdRangeForMainModule( + translator.coreTypes.recordClass); assert(ranges.length <= 1); - codeGen.translateExpression(classId, w.NumType.i32); - b.emitClassIdRangeCheck(ranges); + if (translator.dynamicModuleSupportEnabled) { + final dynamicModuleRanges = translator.classIdNumbering + .getConcreteClassIdRangeForDynamicModule( + translator.coreTypes.recordClass); + final classIdLocal = b.addLocal(w.NumType.i32); + codeGen.translateExpression(classId, w.NumType.i32); + b.local_tee(classIdLocal); + b.local_get(classIdLocal); + translator.dynamicModuleInfo!.callClassIdBranchBuiltIn( + BuiltinUpdatableFunctions.recordId, + b, + translator.typesBuilder + .defineFunction(const [w.NumType.i32], const [w.NumType.i32]), + skipDynamic: dynamicModuleRanges.isEmpty, + buildMainMatch: (w.FunctionBuilder f) { + final ib = f.body; + ib.local_get(ib.locals[0]); + ib.emitClassIdRangeCheck(ranges); + ib.end(); + }, buildDynamicMatch: (w.FunctionBuilder f) { + final ib = f.body; + if (dynamicModuleRanges.isEmpty) { + ib.i32_const(0); + } else { + ib.local_get(ib.locals[0]); + ib.emitClassIdRangeCheck(dynamicModuleRanges); + } + ib.end(); + }); + } else { + codeGen.translateExpression(classId, w.NumType.i32); + b.emitClassIdRangeCheck(ranges); + } return w.NumType.i32; // dart:_object_helper static functions. @@ -1438,13 +1456,13 @@ class Intrinsifier { case StaticIntrinsic.isSubClassOf: final baseClass = (node.arguments.types.single as InterfaceType).classNode; - final range = - translator.classIdNumbering.getConcreteSubclassRange(baseClass); + final ranges = + translator.classIdNumbering.getConcreteSubclassRanges(baseClass); final object = node.arguments.positional.single; codeGen.translateExpression(object, w.RefType.any(nullable: false)); b.struct_get(translator.topInfo.struct, FieldIndex.classId); - b.emitClassIdRangeCheck([range]); + b.emitClassIdRangeCheck(ranges); return w.NumType.i32; } } @@ -1604,7 +1622,7 @@ class Intrinsifier { // Both bool? b.local_get(cid); - b.i32_const(boolInfo.classId); + b.i32_const((boolInfo.classId as AbsoluteClassId).value); b.i32_eq(); b.if_(); b.local_get(first); @@ -1619,7 +1637,7 @@ class Intrinsifier { // Both int? b.local_get(cid); - b.i32_const(intInfo.classId); + b.i32_const((intInfo.classId as AbsoluteClassId).value); b.i32_eq(); b.if_(); b.local_get(first); @@ -1634,7 +1652,7 @@ class Intrinsifier { // Both double? b.local_get(cid); - b.i32_const(doubleInfo.classId); + b.i32_const((doubleInfo.classId as AbsoluteClassId).value); b.i32_eq(); b.if_(); b.local_get(first); @@ -1666,7 +1684,8 @@ class Intrinsifier { final w.Local nonNullArg = b.addLocal(translator.topInfo.nonNullableType); final List classIds = translator.valueClasses.keys - .map((cls) => translator.classInfo[cls]!.classId) + .map((cls) => + (translator.classInfo[cls]!.classId as AbsoluteClassId).value) .toList() ..sort(); diff --git a/pkg/dart2wasm/lib/js/runtime_blob.dart b/pkg/dart2wasm/lib/js/runtime_blob.dart index aab2fd2b03f..2d31f4e728c 100644 --- a/pkg/dart2wasm/lib/js/runtime_blob.dart +++ b/pkg/dart2wasm/lib/js/runtime_blob.dart @@ -51,7 +51,7 @@ class CompiledApp { // wasm file produced by the dart2wasm compiler and returns the bytes to // load the module. These bytes can be in either a format supported by // `WebAssembly.compile` or `WebAssembly.compileStreaming`. - async instantiate(additionalImports, {loadDeferredWasm} = {}) { + async instantiate(additionalImports, {loadDeferredWasm, loadDynamicModule} = {}) { let dartInstance; // Prints to the console @@ -111,12 +111,18 @@ class CompiledApp { <> - const deferredLibraryHelper = { - "loadModule": async (moduleName) => { - if (!loadDeferredWasm) { - throw "No implementation of loadDeferredWasm provided."; - } - const source = await Promise.resolve(loadDeferredWasm(moduleName)); + const loadModuleFromBytes = async (bytes) => { + const module = await WebAssembly.compile(bytes, this.builtins); + return await WebAssembly.instantiate(module, { + ...baseImports, + ...additionalImports, + "wasm:js-string": jsStringPolyfill, + "module0": dartInstance.exports, + }); + } + + const loadModule = async (loader, loaderArgument) => { + const source = await Promise.resolve(loader(loaderArgument)); const module = await ((source instanceof Response) ? WebAssembly.compileStreaming(source, this.builtins) : WebAssembly.compile(source, this.builtins)); @@ -126,6 +132,25 @@ class CompiledApp { <> "module0": dartInstance.exports, }); + } + + const deferredLibraryHelper = { + "loadModule": async (moduleName) => { + if (!loadDeferredWasm) { + throw "No implementation of loadDeferredWasm provided."; + } + return await loadModule(loadDeferredWasm, moduleName); + }, + "loadDynamicModuleFromUri": async (uri) => { + if (!loadDynamicModule) { + throw "No implementation of loadDynamicModule provided."; + } + const loadedModule = await loadModule(loadDynamicModule, uri); + return loadedModule.exports.$invokeEntryPoint; + }, + "loadDynamicModuleFromBytes": async (bytes) => { + const loadedModule = await loadModuleFromBytes(loadDynamicModule, uri); + return loadedModule.exports.$invokeEntryPoint; }, }; diff --git a/pkg/dart2wasm/lib/kernel_nodes.dart b/pkg/dart2wasm/lib/kernel_nodes.dart index 366b73a9a37..12072a1c466 100644 --- a/pkg/dart2wasm/lib/kernel_nodes.dart +++ b/pkg/dart2wasm/lib/kernel_nodes.dart @@ -3,6 +3,7 @@ // 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'; /// Kernel nodes for classes and members referenced specifically by the @@ -12,6 +13,8 @@ mixin KernelNodes { LibraryIndex get index; + CoreTypes get coreTypes; + // dart:_internal classes late final Class symbolClass = index.getClass("dart:_internal", "Symbol"); @@ -121,6 +124,17 @@ mixin KernelNodes { index.getField("dart:core", "_RecordType", "fieldTypes"); late final Field recordTypeNamesField = index.getField("dart:core", "_RecordType", "names"); + late final Class moduleRtt = index.getClass("dart:core", "_ModuleRtt"); + late final Field moduleRttOffsets = + index.getField("dart:core", "_ModuleRtt", "typeRowDisplacementOffsets"); + late final Field moduleRttDisplacementTable = + index.getField("dart:core", "_ModuleRtt", "typeRowDisplacementTable"); + late final Field moduleRttSubstTable = index.getField( + "dart:core", "_ModuleRtt", "typeRowDisplacementSubstTable"); + late final Field moduleRttTypeNames = + index.getField("dart:core", "_ModuleRtt", "typeNames"); + late final Procedure registerModuleRtt = + index.getTopLevelProcedure("dart:core", "_registerModuleRtt"); // dart:core sync* support classes late final Class suspendStateClass = @@ -383,6 +397,50 @@ mixin KernelNodes { late final Procedure systemHashCombine = index.getProcedure("dart:_internal", "SystemHash", "combine"); + // Dynamic module helpers + late final Class constCacheClass = + index.getClass('dart:_internal', 'WasmConstCache'); + late final Constructor constCacheInit = + index.getConstructor('dart:_internal', 'WasmConstCache', ''); + late final Procedure constCacheCanonicalize = index.getProcedure( + 'dart:_internal', 'WasmConstCache', 'canonicalizeValue'); + late final Procedure constCacheArrayCanonicalize = index.getProcedure( + 'dart:_internal', 'WasmArrayConstCache', 'canonicalizeArrayValue'); + late final Procedure registerUpdateableFuncRefs = index.getTopLevelProcedure( + 'dart:_internal', 'registerUpdateableFuncRefs'); + late final Procedure getUpdateableFuncRef = + index.getTopLevelProcedure('dart:_internal', 'getUpdateableFuncRef'); + late final Procedure classIdToModuleId = + index.getTopLevelProcedure('dart:_internal', 'classIdToModuleId'); + late final Procedure localizeClassId = + index.getTopLevelProcedure('dart:_internal', 'localizeClassId'); + late final Procedure scopeClassId = + index.getTopLevelProcedure('dart:_internal', 'scopeClassId'); + late final Procedure globalizeClassId = + index.getTopLevelProcedure('dart:_internal', 'globalizeClassId'); + late final Procedure registerModuleClassRange = + index.getTopLevelProcedure('dart:_internal', 'registerModuleClassRange'); + late final Procedure constCacheGetter = + index.getTopLevelProcedure('dart:_internal', 'getConstCache'); + late final Field objectConstArrayCache = + index.getTopLevelField('dart:_internal', 'objectConstArray'); + late final Field stringConstArrayCache = + index.getTopLevelField('dart:_internal', 'stringConstArray'); + late final Field stringConstImmutableArrayCache = + index.getTopLevelField('dart:_internal', 'stringConstImmutableArray'); + late final Field typeConstArrayCache = + index.getTopLevelField('dart:_internal', 'typeConstArray'); + late final Field typeArrayConstArrayCache = + index.getTopLevelField('dart:_internal', 'typeArrayConstArray'); + late final Field namedParameterConstArrayCache = + index.getTopLevelField('dart:_internal', 'nameParameterConstArray'); + late final Field i8ConstImmutableArrayCache = + index.getTopLevelField('dart:_internal', 'i8ConstImmutableArray'); + late final Field i32ConstArrayCache = + index.getTopLevelField('dart:_internal', 'i32ConstArray'); + late final Field i64ConstImmutableArrayCache = + index.getTopLevelField('dart:_internal', 'i64ConstImmutableArray'); + // Debugging late final Procedure printToConsole = index.getTopLevelProcedure("dart:_internal", "printToConsole"); @@ -390,6 +448,29 @@ mixin KernelNodes { late final Map _extensionCache = {}; + late final Map wasmArrayConstCache = { + _makeElementType(coreTypes.objectClass, nullable: true): + objectConstArrayCache, + _makeElementType(typeClass): typeConstArrayCache, + _makeElementType(namedParameterClass): namedParameterConstArrayCache, + _makeElementType(coreTypes.stringClass): stringConstArrayCache, + _makeElementType(wasmI32Class): i32ConstArrayCache, + _makeElementType(wasmArrayClass, + typeArguments: [_makeElementType(typeClass)]): typeArrayConstArrayCache, + }; + late final Map immutableWasmArrayConstCache = { + _makeElementType(coreTypes.stringClass): stringConstImmutableArrayCache, + _makeElementType(wasmI8Class): i8ConstImmutableArrayCache, + _makeElementType(wasmI64Class): i64ConstImmutableArrayCache, + }; + + InterfaceType _makeElementType(Class c, + {bool nullable = false, List? typeArguments}) => + InterfaceType( + c, + nullable ? Nullability.nullable : Nullability.nonNullable, + typeArguments); + (Extension, ExtensionMemberDescriptor) extensionOfMember(Member member) { return _extensionCache.putIfAbsent(member, () { assert(member.isExtensionMember); diff --git a/pkg/dart2wasm/lib/modules.dart b/pkg/dart2wasm/lib/modules.dart new file mode 100644 index 00000000000..18d36901844 --- /dev/null +++ b/pkg/dart2wasm/lib/modules.dart @@ -0,0 +1,170 @@ +// 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 'target.dart'; +import 'util.dart'; + +const _mainModuleId = 0; + +Library? _enclosingLibraryForReference(Reference reference) { + TreeNode? current = reference.node; + // References generated for constants will not have a node attached. + if (reference.node == null) return null; + while (current != null) { + if (current is Library) return current; + current = current.parent; + } + throw ArgumentError('Could not find enclosing library for ${reference.node}'); +} + +Class? enclosingClassForReference(Reference reference) { + TreeNode? current = reference.node; + // References generated for constants will not have a node attached. + if (reference.node == null) return null; + while (current != null) { + if (current is Class) return current; + current = current.parent; + } + return null; +} + +class ModuleOutputBuilder { + int _counter = _mainModuleId; + + ModuleOutput buildModule({bool emitAsMain = false, bool skipEmit = false}) => + ModuleOutput._(_counter++, emitAsMain: emitAsMain, skipEmit: skipEmit); +} + +/// Deferred loading metadata for a single dart2wasm output module. +/// +/// Each [ModuleOutput] will map to a single wasm module emitted by the +/// compiler. The separation of modules is guided by the deferred imports +/// defined in the source code. +/// +/// A module may contain code at any level of granularity. Code may be grouped +/// by library, by class or neither. [containsReference] should be used to +/// determine if a module contains a given class/member reference. +class ModuleOutput { + /// The ID for the module which will be included in the emitted name. + final int _id; + + /// The set of libraries contained in this module. + final Set libraries = {}; + + bool get isMain => _id == _mainModuleId; + + /// The name used to import and export this module. + String get moduleImportName => 'module$_id'; + + /// The name added to the wasm output file for this module. + final String moduleName; + + /// Whether or not a wasm file should be emitted for this module. + final bool skipEmit; + + ModuleOutput._(this._id, {this.skipEmit = false, bool emitAsMain = false}) + : moduleName = emitAsMain || _id == _mainModuleId ? '' : 'module$_id'; + + /// Whether or not the provided kernel [Reference] is included in this module. + bool containsReference(Reference reference) { + final enclosingLibrary = _enclosingLibraryForReference(reference); + if (enclosingLibrary == null) return false; + return libraries.contains(enclosingLibrary); + } + + @override + String toString() => '$moduleImportName($libraries)'; +} + +/// Data needed to create deferred modules. +class ModuleOutputData { + /// All [ModuleOutput]s generated for the program. + final List modules; + + final Map>> _importMap; + + ModuleOutputData(this.modules, this._importMap) : assert(modules[0].isMain); + + ModuleOutput get mainModule => modules[0]; + Iterable get deferredModules => modules.skip(1); + + bool get hasMultipleModules => modules.length > 1; + + /// Mapping from deferred library import to the 'load list' of module names + /// needed for that import. + /// + /// If library L is required (either directly or indirectly) by two separate + /// imports, then L will be in its own module. That module will be included in + /// the load list for both those imports. + Map>> generateModuleImportMap() { + final result = >>{}; + _importMap.forEach((lib, importMapping) { + final nameMapping = >{}; + importMapping.forEach((importName, modules) { + nameMapping[importName] = + modules.map((o) => o.moduleImportName).toList(); + }); + result[lib.importUri.toString()] = nameMapping; + }); + return result; + } + + /// Returns the module that contains [reference]. + ModuleOutput moduleForReference(Reference reference) => + modules.firstWhere((e) => e.containsReference(reference)); +} + +/// Module strategy that puts all libraries into a single module. +class DefaultModuleStrategy extends ModuleStrategy { + final Component component; + + DefaultModuleStrategy(this.component); + + @override + ModuleOutputData buildModuleOutputData() { + // If deferred loading is not enabled then put every library in the main + // module. + final mainModule = ModuleOutput._(_mainModuleId); + mainModule.libraries.addAll(component.libraries); + return ModuleOutputData([mainModule], const {}); + } + + @override + void prepareComponent() {} +} + +bool _hasWasmExportPragma(CoreTypes coreTypes, Member m) => + hasPragma(coreTypes, m, 'wasm:export'); + +bool containsWasmExport(CoreTypes coreTypes, Library lib) { + if (lib.members.any((m) => _hasWasmExportPragma(coreTypes, m))) { + return true; + } + return lib.classes + .any((c) => c.members.any((m) => _hasWasmExportPragma(coreTypes, m))); +} + +abstract class ModuleStrategy { + void prepareComponent(); + ModuleOutputData buildModuleOutputData(); +} + +Set getReachableLibraries( + Library entryPoint, CoreTypes coreTypes, WasmTarget kernelTarget) { + final List queue = [entryPoint]; + final Set reachable = {entryPoint}; + while (queue.isNotEmpty) { + final current = queue.removeLast(); + for (final dep in current.dependencies) { + final importedLib = dep.targetLibrary; + if (reachable.add(importedLib)) { + queue.add(importedLib); + } + } + } + return reachable; +} diff --git a/pkg/dart2wasm/lib/option.dart b/pkg/dart2wasm/lib/option.dart index fc55746890d..e56567457fd 100644 --- a/pkg/dart2wasm/lib/option.dart +++ b/pkg/dart2wasm/lib/option.dart @@ -2,8 +2,6 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -import 'dart:io'; - import 'package:args/args.dart'; import 'package:front_end/src/api_unstable/vm.dart' show resolveInputUri; @@ -106,6 +104,6 @@ class UriMultiOption extends MultiValueOption { UriMultiOption( name, void Function(WasmCompilerOptions o, List v) applyToOptions, {Iterable? defaultsTo}) - : super(name, applyToOptions, (v) => Uri.file(Directory(v).absolute.path), + : super(name, applyToOptions, (v) => Uri.base.resolve(v), defaultsTo: defaultsTo); } diff --git a/pkg/dart2wasm/lib/record_class_generator.dart b/pkg/dart2wasm/lib/record_class_generator.dart index 90b6a2bd225..5a17e3f9a34 100644 --- a/pkg/dart2wasm/lib/record_class_generator.dart +++ b/pkg/dart2wasm/lib/record_class_generator.dart @@ -182,6 +182,14 @@ class _RecordClassGenerator { Library get library => coreTypes.coreLibrary; + late final Map _existingCoreClassNames = (() { + final map = {}; + for (final cls in library.classes) { + map[cls.name] = cls; + } + return map; + })(); + _RecordClassGenerator(this.classes, this.coreTypes); void generateClassForRecordType(RecordType recordType) { @@ -198,6 +206,11 @@ class _RecordClassGenerator { className = '${className}_${shape.names.join('_')}'; } + // If this is a dynamic module the loaded main module may already contain + // this class. + final existingClass = _existingCoreClassNames[className]; + if (existingClass != null) return existingClass; + final cls = addWasmEntryPointPragma( Class( name: className, diff --git a/pkg/dart2wasm/lib/serialization.dart b/pkg/dart2wasm/lib/serialization.dart new file mode 100644 index 00000000000..d124d5067ff --- /dev/null +++ b/pkg/dart2wasm/lib/serialization.dart @@ -0,0 +1,138 @@ +// 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 'dart:convert'; +import 'dart:typed_data'; + +class BinaryDataSink { + static const int _initSinkSize = 50 * 1024; + + Uint8List _data = Uint8List(_initSinkSize); + int _length = 0; + + BinaryDataSink(); + + int get length => _length; + + void _ensure(int size) { + // Ensure space for at least `size` additional bytes. + if (_data.length < _length + size) { + int newLength = _data.length * 2; + while (newLength < _length + size) { + newLength *= 2; + } + _data = Uint8List(newLength)..setRange(0, _data.length, _data); + } + } + + void writeByte(int byte) { + assert(byte == byte & 0xFF); + _ensure(1); + _data[_length++] = byte; + } + + void writeBytes(Uint8List bytes) { + _ensure(bytes.length); + _data.setRange(_length, _length += bytes.length, bytes); + } + + void writeString(String value) { + final bytes = utf8.encode(value); + writeInt(bytes.length); + writeBytes(bytes); + } + + void writeBool(bool value) { + writeByte(value ? 1 : 0); + } + + void writeInt(int value) { + assert(value >= 0 && value >> 30 == 0); + if (value < 0x80) { + writeByte(value); + } else if (value < 0x4000) { + writeByte((value >> 8) | 0x80); + writeByte(value & 0xFF); + } else { + writeByte((value >> 24) | 0xC0); + writeByte((value >> 16) & 0xFF); + writeByte((value >> 8) & 0xFF); + writeByte(value & 0xFF); + } + } + + void writeClassId(int value) { + // Add 1 since some class IDs are -1. + writeInt(value + 1); + } + + void writeEnum(E value) { + writeInt(value.index); + } + + Uint8List takeBytes() { + final result = Uint8List.sublistView(_data, 0, _length); + // Free the reference to the large data list so it can potentially be + // tree-shaken. + _data = Uint8List(0); + return result; + } +} + +class BinaryDataSource { + int _byteOffset = 0; + final Uint8List _bytes; + + BinaryDataSource(this._bytes); + + void begin(String tag) {} + + void end(String tag) {} + + int _readByte() => _bytes[_byteOffset++]; + + String readString() { + int length = readInt(); + return utf8.decode( + Uint8List.sublistView(_bytes, _byteOffset, _byteOffset += length)); + } + + bool readBool() { + return _readByte() != 0; + } + + int readInt() { + var byte = _readByte(); + if (byte & 0x80 == 0) { + // 0xxxxxxx + return byte; + } else if (byte & 0x40 == 0) { + // 10xxxxxx + return ((byte & 0x3F) << 8) | _readByte(); + } else { + // 11xxxxxx + return ((byte & 0x3F) << 24) | + (_readByte() << 16) | + (_readByte() << 8) | + _readByte(); + } + } + + int readClassId() { + // Subtract 1 since some class IDs are -1. + return readInt() - 1; + } + + E readEnum(List values) { + int index = readInt(); + assert( + 0 <= index && index < values.length, + "Invalid data kind index. " + "Expected one of $values, found index $index."); + return values[index]; + } + + int get length => _bytes.length; + int get currentOffset => _byteOffset; +} diff --git a/pkg/dart2wasm/lib/sync_star.dart b/pkg/dart2wasm/lib/sync_star.dart index 1b84a2c0467..e295ea232ed 100644 --- a/pkg/dart2wasm/lib/sync_star.dart +++ b/pkg/dart2wasm/lib/sync_star.dart @@ -27,7 +27,7 @@ mixin SyncStarCodeGeneratorMixin on StateMachineEntryAstCodeGenerator { // function for this `sync*` function. DartType elementType = functionNode.emittedValueType!; translator.functions.recordClassAllocation(syncStarIterableInfo.classId); - b.pushObjectHeaderFields(syncStarIterableInfo); + b.pushObjectHeaderFields(translator, syncStarIterableInfo); types.makeType(this, elementType); if (context != null) { assert(!context.isEmpty); diff --git a/pkg/dart2wasm/lib/translator.dart b/pkg/dart2wasm/lib/translator.dart index 0753d20e12c..357de979bfe 100644 --- a/pkg/dart2wasm/lib/translator.dart +++ b/pkg/dart2wasm/lib/translator.dart @@ -19,15 +19,18 @@ import 'class_info.dart'; import 'closures.dart'; import 'code_generator.dart'; import 'constants.dart'; -import 'deferred_loading.dart'; import 'dispatch_table.dart'; import 'dynamic_forwarders.dart'; +import 'dynamic_module_kernel_metadata.dart'; +import 'dynamic_modules.dart'; import 'functions.dart'; import 'globals.dart'; import 'kernel_nodes.dart'; +import 'modules.dart'; import 'param_info.dart'; import 'records.dart'; import 'reference_extensions.dart'; +import 'serialization.dart'; import 'static_dispatch_table.dart'; import 'tags.dart'; import 'types.dart'; @@ -57,6 +60,57 @@ class TranslatorOptions { int? sharedMemoryMaxPages; bool requireJsStringBuiltin = false; List watchPoints = []; + + void serialize(BinaryDataSink sink) { + sink.writeBool(enableAsserts); + sink.writeBool(importSharedMemory); + sink.writeBool(inlining); + sink.writeBool(jsCompatibility); + sink.writeBool(omitImplicitTypeChecks); + sink.writeBool(omitExplicitTypeChecks); + sink.writeBool(omitBoundsChecks); + sink.writeBool(polymorphicSpecialization); + sink.writeBool(printKernel); + sink.writeBool(printWasm); + sink.writeBool(minify); + sink.writeBool(verifyTypeChecks); + sink.writeBool(verbose); + sink.writeBool(enableExperimentalFfi); + sink.writeBool(enableExperimentalWasmInterop); + sink.writeBool(generateSourceMaps); + sink.writeBool(enableDeferredLoading); + sink.writeBool(enableMultiModuleStressTestMode); + sink.writeInt(inliningLimit); + sink.writeInt( + sharedMemoryMaxPages == null ? 0 : (sharedMemoryMaxPages! + 1)); + } + + static TranslatorOptions deserialize(BinaryDataSource source) { + final TranslatorOptions options = TranslatorOptions(); + options.enableAsserts = source.readBool(); + options.importSharedMemory = source.readBool(); + options.inlining = source.readBool(); + options.jsCompatibility = source.readBool(); + options.omitImplicitTypeChecks = source.readBool(); + options.omitExplicitTypeChecks = source.readBool(); + options.omitBoundsChecks = source.readBool(); + options.polymorphicSpecialization = source.readBool(); + options.printKernel = source.readBool(); + options.printWasm = source.readBool(); + options.minify = source.readBool(); + options.verifyTypeChecks = source.readBool(); + options.verbose = source.readBool(); + options.enableExperimentalFfi = source.readBool(); + options.enableExperimentalWasmInterop = source.readBool(); + options.generateSourceMaps = source.readBool(); + options.enableDeferredLoading = source.readBool(); + options.enableMultiModuleStressTestMode = source.readBool(); + options.inliningLimit = source.readInt(); + final int sharedMemoryMaxPages = source.readInt(); + options.sharedMemoryMaxPages = + sharedMemoryMaxPages == 0 ? null : (sharedMemoryMaxPages - 1); + return options; + } } /// The main entry point for the translation from kernel to Wasm and the hub for @@ -72,6 +126,7 @@ class Translator with KernelNodes { @override final Component component; final List libraries; + @override final CoreTypes coreTypes; late final TypeEnvironment typeEnvironment; final ClosedWorldClassHierarchy hierarchy; @@ -151,7 +206,8 @@ class Translator with KernelNodes { final Set membersBeingGenerated = {}; final Map constructorClosures = {}; late final w.FunctionBuilder initFunction; - late final w.ValueType voidMarker; + late final w.ValueType voidMarker = + w.RefType.def(w.StructType("void"), nullable: true); // Lazily import FFI memory if used. late final w.Memory ffiMemory = mainModule.memories.import("ffi", "memory", options.importSharedMemory, 0, options.sharedMemoryMaxPages); @@ -164,9 +220,11 @@ class Translator with KernelNodes { final Map immutableArrayTypeCache = {}; final Map mutableArrayTypeCache = {}; final Map functionRefCache = {}; - final Map tearOffFunctionCache = {}; + final Map> + tearOffFunctionCache = {}; - final Map closureImplementations = {}; + final Map> + closureImplementations = {}; // Some convenience accessors for commonly used values. late final ClassInfo topInfo = classes[0]; @@ -325,6 +383,11 @@ class Translator with KernelNodes { final Map _builderToOutput = {}; bool get hasMultipleModules => _moduleOutputData.hasMultipleModules; + DynamicModuleInfo? dynamicModuleInfo; + bool get dynamicModuleSupportEnabled => dynamicModuleInfo != null; + bool get isDynamicModule => dynamicModuleInfo?.isDynamicModule ?? false; + w.ModuleBuilder get dynamicModule => dynamicModuleInfo!.dynamicModule; + w.ModuleBuilder moduleForReference(Reference reference) => _outputToBuilder[_moduleOutputData.moduleForReference(reference)]!; @@ -343,7 +406,9 @@ class Translator with KernelNodes { : Closures(this, member, findCaptures: false); Translator(this.component, this.coreTypes, this.index, this.recordClasses, - this._moduleOutputData, this.options) + this._moduleOutputData, this.options, + {bool enableDynamicModules = false, + DynamicModuleMetadata? dynamicModuleMetadata}) : libraries = component.libraries, hierarchy = ClassHierarchy(component, coreTypes) as ClosedWorldClassHierarchy { @@ -357,6 +422,9 @@ class Translator with KernelNodes { functions = FunctionCollector(this); types = Types(this); exceptionTag = ExceptionTag(this); + if (enableDynamicModules) { + dynamicModuleInfo = DynamicModuleInfo(this, dynamicModuleMetadata); + } } void _initLoadLibraryImportMap() { @@ -397,29 +465,36 @@ class Translator with KernelNodes { } } + void drainCompletionQueue() { + while (!compilationQueue.isEmpty) { + final task = compilationQueue.pop(); + task.run(this, options.printKernel, options.printWasm); + } + } + Map translate( Uri Function(String moduleName)? sourceMapUrlGenerator) { _initLoadLibraryImportMap(); _initModules(sourceMapUrlGenerator); - voidMarker = w.RefType.def(w.StructType("void"), nullable: true); - - closureLayouter.collect(); - classInfoCollector.collect(); - initFunction = mainModule.functions .define(typesBuilder.defineFunction(const [], const []), "#init"); mainModule.functions.start = initFunction; + closureLayouter.collect(); + classInfoCollector.collect(); + globals = Globals(this); constants = Constants(this); dispatchTable.build(); functions.initialize(); - while (!compilationQueue.isEmpty) { - final task = compilationQueue.pop(); - task.run(this, options.printKernel, options.printWasm); - } + + dynamicModuleInfo?.initDynamicModule(); + + drainCompletionQueue(); + + dynamicModuleInfo?.finishDynamicModule(); constructorClosures.clear(); dispatchTable.output(); @@ -463,7 +538,12 @@ class Translator with KernelNodes { /// [callFunction]. List callReference( Reference reference, w.InstructionsBuilder b) { - return callFunction(functions.getFunction(reference), b); + final function = functions.getFunction(reference); + final targetModule = function.enclosingModule; + if (targetModule == b.module) { + return b.invoke(directCallTarget(reference)); + } + return callFunction(function, b); } late final WasmFunctionImporter _importedFunctions = @@ -476,9 +556,14 @@ class Translator with KernelNodes { List callFunction( w.BaseFunction function, w.InstructionsBuilder b) { final targetModule = function.enclosingModule; - // TODO(natebiggs): Consider inlining function body in some scenarios. if (targetModule == b.module) { b.call(function); + } else if (dynamicModuleSupportEnabled) { + // This is a function that the dynamic interface spec has indicated is + // callable from the dynamic module. + final importedFunction = + functions.importFunctionToDynamicModule(function); + b.call(importedFunction); } else if (isMainModule(targetModule)) { final importedFunction = _importedFunctions.get(function, b.module); b.call(importedFunction); @@ -493,17 +578,28 @@ class Translator with KernelNodes { } void callDispatchTable(w.InstructionsBuilder b, SelectorInfo selector, - {required bool useUncheckedEntry}) { - final offset = selector.targets(unchecked: useUncheckedEntry).offset; + {Member? interfaceTarget, required bool useUncheckedEntry}) { + if (dynamicModuleSupportEnabled && selector.isDynamicModuleOverrideable) { + dynamicModuleInfo!.callUpdateableDispatch(b, selector, interfaceTarget!, + useUncheckedEntry: useUncheckedEntry); + } else { + b.struct_get(topInfo.struct, FieldIndex.classId); + final offset = selector + .targets(unchecked: useUncheckedEntry, dynamicModule: false) + .offset; + if (offset == null) { + b.unreachable(); + b.end(); + return; + } - // TODO(natebiggs): Handle dispatch to dynamic module overrideable members. - b.struct_get(topInfo.struct, FieldIndex.classId); - if (offset != 0) { - b.i32_const(offset); - b.i32_add(); + if (offset != 0) { + b.i32_const(offset); + b.i32_add(); + } + b.call_indirect(selector.signature, dispatchTable.getWasmTable(b.module)); + b.emitUnreachableIfNoResult(selector.signature.outputs); } - b.call_indirect(selector.signature, dispatchTable.getWasmTable(b.module)); - b.emitUnreachableIfNoResult(selector.signature.outputs); functions.recordSelectorUse(selector, useUncheckedEntry); } @@ -515,6 +611,14 @@ class Translator with KernelNodes { : coreTypes.objectClass; } + void pushModuleId(w.InstructionsBuilder b) { + if (!isDynamicModule || b.module != dynamicModule) { + b.i64_const(0); + } else { + b.global_get(dynamicModuleInfo!.moduleIdGlobal); + } + } + /// Compute the runtime type of a tear-off. This is the signature of the /// method with the types of all covariant parameters replaced by `Object?`. FunctionType getTearOffType(Procedure method) { @@ -792,19 +896,25 @@ class Translator with KernelNodes { }); } - ClosureImplementation getTearOffClosure(Procedure member) { - return tearOffFunctionCache.putIfAbsent(member, () { + ClosureImplementation getTearOffClosure( + Procedure member, w.ModuleBuilder closureModule) { + final innerCache = tearOffFunctionCache.putIfAbsent(member, () => {}); + return innerCache.putIfAbsent(closureModule, () { assert(member.kind == ProcedureKind.Method); final reference = getFunctionEntry(member.reference, uncheckedEntry: false); w.BaseFunction target = functions.getFunction(reference); - return getClosure(member.function, target, + return getClosure(member.function, target, closureModule, paramInfoForDirectCall(reference), "$member tear-off"); }); } - ClosureImplementation getClosure(FunctionNode functionNode, - w.BaseFunction target, ParameterInfo paramInfo, String name) { + ClosureImplementation getClosure( + FunctionNode functionNode, + w.BaseFunction target, + w.ModuleBuilder closureModule, + ParameterInfo paramInfo, + String name) { // We compile a block multiple times in try-catch, to catch Dart exceptions // and then again to catch JS exceptions. We may also ask for // `ClosureImplementation` for a local function multiple times as we see @@ -817,13 +927,12 @@ class Translator with KernelNodes { // will be the value returned by `paramInfoForDirectCall`. So the key for // this cache can be just `FunctionNode`, instead of `(FunctionNode, // ParameterInfo)`. - final existingImplementation = closureImplementations[functionNode]; + final existingImplementation = + closureImplementations[functionNode]?[closureModule]; if (existingImplementation != null) { return existingImplementation; } - final targetModule = target.enclosingModule; - // Look up the closure representation for the signature. int typeCount = functionNode.typeParameters.length; int positionalCount = functionNode.positionalParameters.length; @@ -895,7 +1004,7 @@ class Translator with KernelNodes { w.BaseFunction makeTrampoline( w.FunctionType signature, int posArgCount, List argNames) { final trampoline = - targetModule.functions.define(signature, "$name trampoline"); + closureModule.functions.define(signature, "$name trampoline"); compilationQueue.add(CompilationTask( trampoline, _ClosureTrampolineGenerator(this, trampoline, target, typeCount, @@ -904,7 +1013,7 @@ class Translator with KernelNodes { } w.BaseFunction makeDynamicCallEntry() { - final function = targetModule.functions.define( + final function = closureModule.functions.define( dynamicCallVtableEntryFunctionType, "$name dynamic call entry"); compilationQueue.add(CompilationTask( function, @@ -927,7 +1036,7 @@ class Translator with KernelNodes { ib.ref_func(function); } - final vtable = targetModule.globals.define(w.GlobalType( + final vtable = closureModule.globals.define(w.GlobalType( w.RefType.def(representation.vtableStruct, nullable: false), mutable: false)); final ib = vtable.initializer; @@ -950,8 +1059,9 @@ class Translator with KernelNodes { ib.end(); final implementation = ClosureImplementation(representation, functions, - dynamicCallEntry, vtable, targetModule, paramInfo); - closureImplementations[functionNode] = implementation; + dynamicCallEntry, vtable, closureModule, paramInfo); + (closureImplementations[functionNode] ??= {})[closureModule] = + implementation; return implementation; } @@ -964,6 +1074,7 @@ class Translator with KernelNodes { } void convertType(w.InstructionsBuilder b, w.ValueType from, w.ValueType to) { + if (identical(from, to)) return; if (from == voidMarker || to == voidMarker) { if (from != voidMarker) { b.drop(); @@ -1006,7 +1117,7 @@ class Translator with KernelNodes { w.Local temp = b.addLocal(from); b.local_set(temp); - b.i32_const(info.classId); + b.i32_const((info.classId as AbsoluteClassId).value); b.local_get(temp); b.struct_new(info.struct); } else if (from is w.RefType) { @@ -1181,7 +1292,7 @@ class Translator with KernelNodes { w.FunctionType signatureForDirectCall(Reference target) { if (target.asMember.isInstanceMember && !target.isBodyReference) { final selector = dispatchTable.selectorForTarget(target); - if (selector.targetSet.contains(target)) { + if (selector.containsTarget(target)) { return selector.signature; } } @@ -1191,7 +1302,7 @@ class Translator with KernelNodes { ParameterInfo paramInfoForDirectCall(Reference target) { if (target.asMember.isInstanceMember) { final selector = dispatchTable.selectorForTarget(target); - if (selector.targetSet.contains(target)) { + if (selector.containsTarget(target)) { return selector.paramInfo; } } @@ -2166,8 +2277,11 @@ class PolymorphicDispatchers { CallTarget getPolymorphicDispatcher(SelectorInfo selector, {required bool useUncheckedEntry}) { - assert( - selector.targets(unchecked: useUncheckedEntry).targetRanges.length > 1); + assert(selector + .targets(unchecked: useUncheckedEntry, dynamicModule: false) + .targetRanges + .length > + 1); return (useUncheckedEntry && selector.useMultipleEntryPoints ? uncheckedCache : cache) @@ -2186,7 +2300,8 @@ class PolymorphicDispatcherCallTarget extends CallTarget { PolymorphicDispatcherCallTarget(this.translator, this.selector, this.callingModule, this.useUncheckedEntry) - : super(selector.signature); + : assert(!selector.isDynamicModuleOverrideable), + super(selector.signature); @override String get name => '${selector.name} (polymorphic dispatcher)'; @@ -2197,7 +2312,7 @@ class PolymorphicDispatcherCallTarget extends CallTarget { @override bool get shouldInline => selector - .targets(unchecked: useUncheckedEntry) + .targets(unchecked: useUncheckedEntry, dynamicModule: false) .staticDispatchRanges .length <= 2; @@ -2223,14 +2338,18 @@ class PolymorphicDispatcherCodeGenerator implements CodeGenerator { final bool useUncheckedEntry; PolymorphicDispatcherCodeGenerator( - this.translator, this.selector, this.useUncheckedEntry); + this.translator, this.selector, this.useUncheckedEntry) + : assert(!selector.isDynamicModuleOverrideable); @override void generate(w.InstructionsBuilder b, List paramLocals, w.Label? returnLabel) { final signature = selector.signature; - final targets = selector.targets(unchecked: useUncheckedEntry); + final targets = selector.targets( + unchecked: useUncheckedEntry, + dynamicModule: + translator.isDynamicModule && b.module == translator.dynamicModule); final targetRanges = targets.staticDispatchRanges .map((entry) => (range: entry.range, value: entry.target)) @@ -2399,18 +2518,23 @@ abstract class _WasmImporter { Iterable get imports => _map.values.expand((v) => v.values); - T get(T key, w.ModuleBuilder module) { + T get(T key, w.ModuleBuilder module, {bool exportOnly = false}) { if (key.enclosingModule == module) return key; final innerMap = _map.putIfAbsent(key, () { key.enclosingModule.exports.export('$_exportPrefix${_map.length}', key); return {}; }); + if (exportOnly) return key; return innerMap.putIfAbsent(module, () { return _import(module, key, _translator.nameForModule(key.enclosingModule), key.exportedName); }); } + + bool has(T key) { + return _map.containsKey(key); + } } class WasmFunctionImporter extends _WasmImporter { diff --git a/pkg/dart2wasm/lib/types.dart b/pkg/dart2wasm/lib/types.dart index 4a0d82e898e..3e9f75c0083 100644 --- a/pkg/dart2wasm/lib/types.dart +++ b/pkg/dart2wasm/lib/types.dart @@ -12,6 +12,7 @@ import 'package:wasm_builder/wasm_builder.dart' as w; import 'class_info.dart'; import 'code_generator.dart'; import 'dispatch_table.dart' show Row, buildRowDisplacementTable; +import 'dynamic_modules.dart'; import 'translator.dart'; /// Values for the `_kind` field in `_TopType`. Must match the definitions in @@ -44,6 +45,10 @@ class Types { late final ClassInfo typeClassInfo = translator.classInfo[translator.typeClass]!; + /// Class info for `_NamedParameter` + late final ClassInfo namedParameterClassInfo = + translator.classInfo[translator.namedParameterClass]!; + /// Wasm value type of `List<_Type>` late final w.ValueType typeListExpectedType = translator.classInfo[translator.listBaseClass]!.nonNullableType; @@ -193,7 +198,7 @@ class Types { final b = codeGen.b; ClassInfo typeInfo = translator.classInfo[type.classNode]!; b.i32_const(encodedNullability(type)); - b.i32_const(typeInfo.classId); + b.pushClassIdToStack(translator, typeInfo.classId); _makeTypeArray(codeGen, type.typeArguments); } @@ -346,7 +351,7 @@ class Types { } translator.functions.recordClassAllocation(info.classId); - b.pushObjectHeaderFields(info); + b.pushObjectHeaderFields(translator, info); if (type is InterfaceType) { _makeInterfaceType(codeGen, type); } else if (type is FunctionType) { @@ -422,7 +427,7 @@ class Types { codeGen.call(translator.isNullabilityCheck.reference); } else { b.i32_const(encodedNullability(testedAgainstType)); - b.i32_const(typeClassInfo.classId); + b.pushClassIdToStack(translator, typeClassInfo.classId); if (typeArguments.isEmpty) { codeGen.call(translator.isInterfaceSubtype0.reference); } else if (typeArguments.length == 1) { @@ -500,7 +505,7 @@ class Types { final typeClassInfo = translator.classInfo[testedAgainstType.classNode]!; final typeArguments = testedAgainstType.typeArguments; b.i32_const(encodedNullability(testedAgainstType)); - b.i32_const(typeClassInfo.classId); + b.pushClassIdToStack(translator, typeClassInfo.classId); if (typeArguments.isEmpty) { outputsToDrop = codeGen.call(translator.asInterfaceSubtype0.reference); } else if (typeArguments.length == 1) { @@ -551,6 +556,10 @@ abstract class _TypeCheckers { if (testedAgainstType is! InterfaceType) { return (null, checkArguments: false); } + if (testedAgainstType.classNode + .isDynamicModuleExtendable(rtt.translator.coreTypes)) { + return (null, checkArguments: false); + } if (_hasOnlyDefaultTypeArguments(testedAgainstType)) { return (testedAgainstType, checkArguments: false); @@ -726,7 +735,9 @@ class IsCheckerCallTarget extends CallTarget { this.testedAgainstType, this.operandIsNullable, this.checkArguments, - this.argumentCount); + this.argumentCount) + : assert(!testedAgainstType.classNode + .isDynamicModuleExtendable(translator.coreTypes)); @override String get name { @@ -755,8 +766,8 @@ class IsCheckerCallTarget extends CallTarget { // Always inline single class-id range checks (no branching, simply loads, // arithmetic and unsigned compare). - final ranges = - translator.classIdNumbering.getConcreteClassIdRanges(interfaceClass); + final ranges = translator.classIdNumbering + .getConcreteClassIdRangeForCurrentModule(interfaceClass); return ranges.length <= 1; } @@ -872,7 +883,7 @@ class IsCheckerCodeGenerator implements CodeGenerator { b.ref_test(translator.closureInfo.nonNullableType); } else { final ranges = translator.classIdNumbering - .getConcreteClassIdRanges(interfaceClass); + .getConcreteClassIdRangeForCurrentModule(interfaceClass); b.local_get(operand); b.struct_get(translator.topInfo.struct, FieldIndex.classId); b.emitClassIdRangeCheck(ranges); @@ -955,7 +966,9 @@ class AsCheckerCodeGenerator implements CodeGenerator { this.testedAgainstType, this.operandIsNullable, this.checkArguments, - this.argumentCount); + this.argumentCount) + : assert(!testedAgainstType.classNode + .isDynamicModuleExtendable(translator.coreTypes)); @override void generate(w.InstructionsBuilder b, List paramLocals, @@ -984,7 +997,7 @@ class AsCheckerCodeGenerator implements CodeGenerator { translator.classInfo[testedAgainstType.classNode]!.classId; b.local_get(b.locals[0]); b.i32_const(encodedNullability(testedAgainstType)); - b.i32_const(testedAgainstClassId); + b.pushClassIdToStack(translator, testedAgainstClassId); if (argumentCount == 1) { b.local_get(b.locals[1]); translator.callReference( @@ -1039,15 +1052,6 @@ class RuntimeTypeInformation { /// not have to substitute anything. static const int noSubstitutionIndex = 0; - /// Table of type names indexed by class id. - late final InstanceConstant typeNames; - - /// See sdk/lib/_internal/wasm/lib/type.dart:_typeRowDisplacement* - /// for what this contains and how it's used for substitution. - late final InstanceConstant typeRowDisplacementOffsets; - late final InstanceConstant typeRowDisplacementTable; - late final InstanceConstant typeRowDisplacementSubstTable; - CoreTypes get coreTypes => translator.coreTypes; Types get types => translator.types; @@ -1056,24 +1060,25 @@ class RuntimeTypeInformation { late final Map _substitutionTable; late final List _substitutionTableByIndex; + /// Object containing RTT info for the main module. See + /// sdk/lib/_internal/wasm/lib/type.dart for what this contains and how it's + /// used. + late final InstanceConstant mainModuleRtt = getModuleRtt(isMainModule: true); + final Map _requiresSubstitutionForSubclasses = {}; RuntimeTypeInformation(this.translator) { _buildTypeRules(); - - // Data structure to tell whether two types are related and if so how to - // translate type arguments from one class to that of a super class. - _initTypeRowDiplacementTable(); - - // The class name table of type WasmArray - _initTypeNames(); } bool requiresTypeArgumentSubstitution(Class superclass) { final superclassId = translator.classIdNumbering.classIds[superclass]!; - return _requiresSubstitutionForSubclasses.putIfAbsent(superclassId, () { - final subclassSubstitutions = - _substitutionSuperclassToSubclass[superclassId]; + final id = switch (superclassId) { + RelativeClassId() => superclassId.relativeValue, + AbsoluteClassId() => superclassId.value, + }; + return _requiresSubstitutionForSubclasses.putIfAbsent(id, () { + final subclassSubstitutions = _substitutionSuperclassToSubclass[id]; if (subclassSubstitutions == null) return false; for (final entry in subclassSubstitutions.entries) { @@ -1146,11 +1151,22 @@ class RuntimeTypeInformation { substitutionIndex = index; } - final subclassId = translator.classInfo[subtype.classNode]!.classId; - (_substitutionSubclassToSuperclass[subclassId] ??= - {})[superclassInfo.classId] = substitutionIndex; - (_substitutionSuperclassToSubclass[superclassInfo.classId] ??= - {})[subclassId] = substitutionIndex; + final subclassIdWrapped = + translator.classInfo[subtype.classNode]!.classId; + final subclassId = switch (subclassIdWrapped) { + RelativeClassId() => subclassIdWrapped.relativeValue, + AbsoluteClassId() => subclassIdWrapped.value, + }; + final superclassIdWrapped = superclassInfo.classId; + final superclassId = switch (superclassIdWrapped) { + RelativeClassId() => superclassIdWrapped.relativeValue, + AbsoluteClassId() => superclassIdWrapped.value, + }; + + (_substitutionSubclassToSuperclass[subclassId] ??= {})[superclassId] = + substitutionIndex; + (_substitutionSuperclassToSubclass[superclassId] ??= {})[subclassId] = + substitutionIndex; } } } @@ -1184,7 +1200,7 @@ class RuntimeTypeInformation { return true; } - void _initTypeRowDiplacementTable() { + InstanceConstant getModuleRtt({required bool isMainModule}) { final rowForSuperclass = List.filled(translator.classes.length, null); final rows = >[]; final ranges = _buildRanges(_substitutionSuperclassToSubclass); @@ -1217,42 +1233,60 @@ class RuntimeTypeInformation { rows.sort((Row a, Row b) => -weight(a).compareTo(weight(b))); final table = buildRowDisplacementTable(rows, firstAvailable: 1); - typeRowDisplacementTable = translator.constants.makeArrayOf(wasmI32, [ + final typeRowDisplacementTable = translator.constants.makeArrayOf(wasmI32, [ for (final entry in table) translator.constants.makeWasmI32(entry == null ? 0 : (entry.$2 == noSubstitutionIndex ? -entry.$1 : entry.$1)), ]); - - typeRowDisplacementSubstTable = + final typeRowDisplacementSubstTable = translator.constants.makeArrayOf(arrayOfType, [ for (final entry in table) _substitutionTableByIndex[ entry == null ? noSubstitutionIndex : entry.$2], ]); - typeRowDisplacementOffsets = translator.constants.makeArrayOf(wasmI32, [ + final typeRowDisplacementOffsets = + translator.constants.makeArrayOf(wasmI32, [ for (int classId = 0; classId < translator.classes.length; ++classId) translator.constants .makeWasmI32(rowForSuperclass[classId]?.offset ?? -1), ]); + + final typeNames = translator.options.minify + ? NullConstant() + : _getTypeNames(isMainModule); + + return InstanceConstant(translator.moduleRtt.reference, const [], { + translator.moduleRttOffsets.fieldReference: typeRowDisplacementOffsets, + translator.moduleRttDisplacementTable.fieldReference: + typeRowDisplacementTable, + translator.moduleRttSubstTable.fieldReference: + typeRowDisplacementSubstTable, + translator.moduleRttTypeNames.fieldReference: typeNames, + }); } - void _initTypeNames() { + InstanceConstant _getTypeNames(bool isMainModule) { final stringType = translator.coreTypes.stringRawType(Nullability.nonNullable); final emptyString = StringConstant(''); List nameConstants = []; + List dynamicModuleNameConstants = []; for (ClassInfo classInfo in translator.classes) { Class? cls = classInfo.cls; if (cls == null || cls.isAnonymousMixin) { nameConstants.add(emptyString); } else { - nameConstants.add(StringConstant(cls.name)); + final constantList = classInfo.classId is RelativeClassId + ? dynamicModuleNameConstants + : nameConstants; + constantList.add(StringConstant(cls.name)); } } - typeNames = translator.constants.makeArrayOf(stringType, nameConstants); + return translator.constants.makeArrayOf( + stringType, isMainModule ? nameConstants : dynamicModuleNameConstants); } Map> _buildRanges(Map> map) { diff --git a/pkg/dynamic_modules/test/common/testing.dart b/pkg/dynamic_modules/test/common/testing.dart index 4f87d95500a..6c8cc4d3fda 100644 --- a/pkg/dynamic_modules/test/common/testing.dart +++ b/pkg/dynamic_modules/test/common/testing.dart @@ -33,8 +33,7 @@ Future load(String moduleName, return loadModuleFromBytes(bytes); } // Dart2wasm implementation - return loadModuleFromUri( - Uri(scheme: '', path: 'modules/${moduleName}_module1.wasm')); + return loadModuleFromUri(Uri(scheme: '', path: 'modules/$moduleName.wasm')); } /// Notify the test harness that the test has run to completion. diff --git a/pkg/dynamic_modules/test/data/closure_invocation/dynamic_interface.yaml b/pkg/dynamic_modules/test/data/closure_invocation/dynamic_interface.yaml new file mode 100644 index 00000000000..b33f9707446 --- /dev/null +++ b/pkg/dynamic_modules/test/data/closure_invocation/dynamic_interface.yaml @@ -0,0 +1,16 @@ +# 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. +callable: + - library: 'shared/shared.dart' + # TODO(sigmund): This should be included by default + - library: 'dart:core' + class: 'pragma' + member: '_' + - library: 'dart:core' + class: 'Object' + - library: 'dart:core' + class: 'int' + - library: 'dart:core' + class: 'String' + diff --git a/pkg/dynamic_modules/test/data/closure_invocation/main.dart b/pkg/dynamic_modules/test/data/closure_invocation/main.dart new file mode 100644 index 00000000000..fa56d261b94 --- /dev/null +++ b/pkg/dynamic_modules/test/data/closure_invocation/main.dart @@ -0,0 +1,14 @@ +// 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 '../../common/testing.dart' as helper; +import 'package:expect/expect.dart'; + +import 'shared/shared.dart' as shared; + +main() async { + await helper.load('entry1.dart'); + Expect.equals('dynamic module 1: hello', shared.topLevelClosure!('hello')); + helper.done(); +} diff --git a/pkg/dynamic_modules/test/data/closure_invocation/modules/entry1.dart b/pkg/dynamic_modules/test/data/closure_invocation/modules/entry1.dart new file mode 100644 index 00000000000..93a46a4180b --- /dev/null +++ b/pkg/dynamic_modules/test/data/closure_invocation/modules/entry1.dart @@ -0,0 +1,18 @@ +// 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 '../shared/shared.dart'; + +String Function(String s, {int i})? _localTopLevelClosure; + +@pragma('dyn-module:entry-point') +void dynamicModuleEntrypoint() { + String f(T s, {int? i}) => 'dynamic module 1: $s'; + String g(String s, {int? i}) => 'dynamic module 2: $s'; + _localTopLevelClosure = f; + topLevelClosure = _localTopLevelClosure; + _localTopLevelClosure!('a', i: 1); + _localTopLevelClosure = g; + _localTopLevelClosure!('b', i: 2); +} diff --git a/pkg/dynamic_modules/test/data/closure_invocation/shared/shared.dart b/pkg/dynamic_modules/test/data/closure_invocation/shared/shared.dart new file mode 100644 index 00000000000..6f3af2f76bb --- /dev/null +++ b/pkg/dynamic_modules/test/data/closure_invocation/shared/shared.dart @@ -0,0 +1,5 @@ +// 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. + +String Function(String s)? topLevelClosure; diff --git a/pkg/dynamic_modules/test/data/dyn_module_type_checks/dynamic_interface.yaml b/pkg/dynamic_modules/test/data/dyn_module_type_checks/dynamic_interface.yaml new file mode 100644 index 00000000000..fa54f77bb4d --- /dev/null +++ b/pkg/dynamic_modules/test/data/dyn_module_type_checks/dynamic_interface.yaml @@ -0,0 +1,34 @@ +# 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. +extendable: + - library: 'shared/shared.dart' + class: 'Base' + +can-be-overridden: + - library: 'shared/shared.dart' + class: 'Base' + member: 'method1' + +# TODO(sigmund): consider implying this for all extendable types. +callable: + - library: 'shared/shared.dart' + class: 'Base' + - library: 'shared/shared.dart' + class: 'Base' + member: 'x' + - library: 'shared/shared.dart' + class: 'Base' + member: 'method1' + # TODO(sigmund): This should be included by default + - library: 'dart:core' + class: 'Object' + - library: 'dart:core' + class: 'int' + - library: 'dart:core' + class: 'pragma' + member: '_' + - library: 'dart:core' + member: 'override' + - library: 'dart:core' + class: 'num' diff --git a/pkg/dynamic_modules/test/data/dyn_module_type_checks/main.dart b/pkg/dynamic_modules/test/data/dyn_module_type_checks/main.dart new file mode 100644 index 00000000000..d94d6dbee06 --- /dev/null +++ b/pkg/dynamic_modules/test/data/dyn_module_type_checks/main.dart @@ -0,0 +1,19 @@ +// 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 '../../common/testing.dart' as helper; +import 'package:expect/expect.dart'; + +import 'shared/shared.dart' show Base; + +/// A dynamic module is allowed to extend a class in the dynamic interface and +/// override its members. +main() async { + Expect.equals(100, Base().method1(0)); + final o1 = (await helper.load('entry1.dart')); + final o2 = (await helper.load('entry2.dart')); + Expect.equals(1, o1); + Expect.equals(3, o2); + helper.done(); +} diff --git a/pkg/dynamic_modules/test/data/dyn_module_type_checks/modules/entry1.dart b/pkg/dynamic_modules/test/data/dyn_module_type_checks/modules/entry1.dart new file mode 100644 index 00000000000..968192fa1a4 --- /dev/null +++ b/pkg/dynamic_modules/test/data/dyn_module_type_checks/modules/entry1.dart @@ -0,0 +1,25 @@ +// 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 '../shared/shared.dart'; + +class Child1 extends Base { + @override + int method1(int i) => i + 1; +} + +class Child2 extends Base { + @override + int method1(int i) => i + 2; +} + +@pragma('dyn-module:entry-point') +Object? dynamicModuleEntrypoint() { + Base.x = Child1(); + final x = Base.x; + if (x is Base) { + return x.method1(0); + } + throw 'bad'; +} diff --git a/pkg/dynamic_modules/test/data/dyn_module_type_checks/modules/entry2.dart b/pkg/dynamic_modules/test/data/dyn_module_type_checks/modules/entry2.dart new file mode 100644 index 00000000000..1b2a21f54a9 --- /dev/null +++ b/pkg/dynamic_modules/test/data/dyn_module_type_checks/modules/entry2.dart @@ -0,0 +1,25 @@ +// 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 '../shared/shared.dart'; + +class Child3 extends Base { + @override + int method1(int i) => i + 3; +} + +class Child4 extends Base { + @override + int method1(int i) => i + 4; +} + +@pragma('dyn-module:entry-point') +Object? dynamicModuleEntrypoint() { + Base.x = Child3(); + final x = Base.x; + if (x is Base) { + return x.method1(0); + } + throw 'bad'; +} diff --git a/pkg/dynamic_modules/test/data/dyn_module_type_checks/shared/shared.dart b/pkg/dynamic_modules/test/data/dyn_module_type_checks/shared/shared.dart new file mode 100644 index 00000000000..53bf2c02c47 --- /dev/null +++ b/pkg/dynamic_modules/test/data/dyn_module_type_checks/shared/shared.dart @@ -0,0 +1,9 @@ +// 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. + +class Base { + int method1(int i) => i + 100; + + static Object? x; +} diff --git a/pkg/dynamic_modules/test/data/extend_class_dyn_only/dynamic_interface.yaml b/pkg/dynamic_modules/test/data/extend_class_dyn_only/dynamic_interface.yaml new file mode 100644 index 00000000000..e44add4d28a --- /dev/null +++ b/pkg/dynamic_modules/test/data/extend_class_dyn_only/dynamic_interface.yaml @@ -0,0 +1,23 @@ +# 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. + +callable: + - library: 'dart:core' + class: 'Object' + - library: 'dart:core' + class: 'int' + - library: 'dart:core' + class: 'pragma' + member: '_' + - library: 'dart:core' + member: 'override' + - library: 'dart:core' + class: 'bool' + - library: 'dart:core' + class: 'num' + +extendable: + - library: 'dart:core' + class: 'Object' + diff --git a/pkg/dynamic_modules/test/data/extend_class_dyn_only/main.dart b/pkg/dynamic_modules/test/data/extend_class_dyn_only/main.dart new file mode 100644 index 00000000000..90c80c5d4db --- /dev/null +++ b/pkg/dynamic_modules/test/data/extend_class_dyn_only/main.dart @@ -0,0 +1,14 @@ +// 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 '../../common/testing.dart' as helper; +import 'package:expect/expect.dart'; + +/// A dynamic module is allowed to extend a class in the dynamic interface and +/// override its members. +main() async { + final o = (await helper.load('entry1.dart')) as int; + Expect.equals(1, o); + helper.done(); +} diff --git a/pkg/dynamic_modules/test/data/extend_class_dyn_only/modules/entry1.dart b/pkg/dynamic_modules/test/data/extend_class_dyn_only/modules/entry1.dart new file mode 100644 index 00000000000..5a13b722bac --- /dev/null +++ b/pkg/dynamic_modules/test/data/extend_class_dyn_only/modules/entry1.dart @@ -0,0 +1,30 @@ +// 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. + +abstract class Base { + int method1(); +} + +class Child extends Base { + @override + int method1() => 1; +} + +dynamic getChild(bool x) { + if (x) { + return Child(); + } + return 5; +} + +bool getOpaqueTrue() => 5 < 20; + +@pragma('dyn-module:entry-point') +Object? dynamicModuleEntrypoint() { + final x = getChild(getOpaqueTrue()); + if (x is Base) { + return x.method1(); + } + return 0; +} diff --git a/pkg/dynamic_modules/test/data/extend_class_generics/dynamic_interface.yaml b/pkg/dynamic_modules/test/data/extend_class_generics/dynamic_interface.yaml new file mode 100644 index 00000000000..ea1a9f2e0fe --- /dev/null +++ b/pkg/dynamic_modules/test/data/extend_class_generics/dynamic_interface.yaml @@ -0,0 +1,27 @@ +# 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. +extendable: + - library: 'shared/shared.dart' + class: 'Base' + +can-be-overridden: + - library: 'shared/shared.dart' + class: 'Base' + member: 'method1' + +# TODO(sigmund): consider implying this for all extendable types. +callable: + - library: 'shared/shared.dart' + class: 'Base' + member: '' + # TODO(sigmund): This should be included by default + - library: 'dart:core' + class: 'Object' + - library: 'dart:core' + class: 'int' + - library: 'dart:core' + class: 'pragma' + member: '_' + - library: 'dart:core' + member: 'override' diff --git a/pkg/dynamic_modules/test/data/extend_class_generics/main.dart b/pkg/dynamic_modules/test/data/extend_class_generics/main.dart new file mode 100644 index 00000000000..35f0e21f8eb --- /dev/null +++ b/pkg/dynamic_modules/test/data/extend_class_generics/main.dart @@ -0,0 +1,20 @@ +// 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 '../../common/testing.dart' as helper; +import 'package:expect/expect.dart'; + +import 'shared/shared.dart' show Base, SuperBase; + +/// A dynamic module is allowed to extend a class in the dynamic interface and +/// override its members. +main() async { + final o = (await helper.load('entry1.dart')) as Base; + if (o is SuperBase) { + Expect.equals(3, o.method1()); + } else { + Expect.fail('Missed type check'); + } + helper.done(); +} diff --git a/pkg/dynamic_modules/test/data/extend_class_generics/modules/entry1.dart b/pkg/dynamic_modules/test/data/extend_class_generics/modules/entry1.dart new file mode 100644 index 00000000000..52e0c1198d5 --- /dev/null +++ b/pkg/dynamic_modules/test/data/extend_class_generics/modules/entry1.dart @@ -0,0 +1,16 @@ +// 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 '../shared/shared.dart'; + +class Child extends Base { + final T t; + Child(this.t); + + @override + T method1() => t; +} + +@pragma('dyn-module:entry-point') +Object? dynamicModuleEntrypoint() => Child(3); diff --git a/pkg/dynamic_modules/test/data/extend_class_generics/shared/shared.dart b/pkg/dynamic_modules/test/data/extend_class_generics/shared/shared.dart new file mode 100644 index 00000000000..f5fa20e96e4 --- /dev/null +++ b/pkg/dynamic_modules/test/data/extend_class_generics/shared/shared.dart @@ -0,0 +1,9 @@ +// 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. + +abstract class SuperBase {} + +abstract class Base extends SuperBase { + T method1(); +} diff --git a/pkg/dynamic_modules/test/data/multiple_classes/dynamic_interface.yaml b/pkg/dynamic_modules/test/data/multiple_classes/dynamic_interface.yaml index b7736cefb26..562cff357bb 100644 --- a/pkg/dynamic_modules/test/data/multiple_classes/dynamic_interface.yaml +++ b/pkg/dynamic_modules/test/data/multiple_classes/dynamic_interface.yaml @@ -12,6 +12,11 @@ extendable: callable: # TODO(sigmund): This should be included by default - library: 'dart:core' + class: 'Object' + - library: 'dart:core' + class: 'String' + - library: 'dart:core' + member: 'override' - library: 'dart:core' class: 'pragma' member: '_' diff --git a/pkg/dynamic_modules/test/data/multiple_classes/main.dart b/pkg/dynamic_modules/test/data/multiple_classes/main.dart index 7310c4e5baf..c2fe2505085 100644 --- a/pkg/dynamic_modules/test/data/multiple_classes/main.dart +++ b/pkg/dynamic_modules/test/data/multiple_classes/main.dart @@ -7,8 +7,6 @@ import 'package:expect/expect.dart'; import '../../common/testing.dart' as helper; import 'modules/common.dart'; -// It is an error to load a module that provides a second definition for -// a library that already exists in the application. main() async { final a1 = await helper.load('entry1.dart') as A; final a2 = await helper.load('entry2.dart') as A; diff --git a/pkg/dynamic_modules/test/data/override_extra_params/dynamic_interface.yaml b/pkg/dynamic_modules/test/data/override_extra_params/dynamic_interface.yaml new file mode 100644 index 00000000000..6218f55a427 --- /dev/null +++ b/pkg/dynamic_modules/test/data/override_extra_params/dynamic_interface.yaml @@ -0,0 +1,29 @@ +# 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. +extendable: + - library: 'shared/shared.dart' + class: 'Base' + +can-be-overridden: + - library: 'shared/shared.dart' + class: 'Base' + member: 'method1' + +# TODO(sigmund): consider implying this for all extendable types. +callable: + - library: 'shared/shared.dart' + class: 'Base' + member: '' + # TODO(sigmund): This should be included by default + - library: 'dart:core' + class: 'Object' + - library: 'dart:core' + class: 'int' + - library: 'dart:core' + class: 'pragma' + member: '_' + - library: 'dart:core' + member: 'override' + - library: 'dart:core' + class: 'num' diff --git a/pkg/dynamic_modules/test/data/override_extra_params/main.dart b/pkg/dynamic_modules/test/data/override_extra_params/main.dart new file mode 100644 index 00000000000..8d0c2cfbf23 --- /dev/null +++ b/pkg/dynamic_modules/test/data/override_extra_params/main.dart @@ -0,0 +1,16 @@ +// 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 '../../common/testing.dart' as helper; +import 'package:expect/expect.dart'; + +import 'shared/shared.dart' show Base; + +/// A dynamic module is allowed to extend a class in the dynamic interface and +/// override its members. +main() async { + final o = (await helper.load('entry1.dart')) as Base; + Expect.equals(1, o.method1(1)); + helper.done(); +} diff --git a/pkg/dynamic_modules/test/data/override_extra_params/modules/entry1.dart b/pkg/dynamic_modules/test/data/override_extra_params/modules/entry1.dart new file mode 100644 index 00000000000..364eefae25f --- /dev/null +++ b/pkg/dynamic_modules/test/data/override_extra_params/modules/entry1.dart @@ -0,0 +1,13 @@ +// 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 '../shared/shared.dart'; + +class Child extends Base { + @override + int method1(int i, [int? j]) => i + (j ?? 0); +} + +@pragma('dyn-module:entry-point') +Object? dynamicModuleEntrypoint() => Child(); diff --git a/pkg/dynamic_modules/test/data/override_extra_params/shared/shared.dart b/pkg/dynamic_modules/test/data/override_extra_params/shared/shared.dart new file mode 100644 index 00000000000..110b8dcf892 --- /dev/null +++ b/pkg/dynamic_modules/test/data/override_extra_params/shared/shared.dart @@ -0,0 +1,7 @@ +// 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. + +abstract class Base { + int method1(int i) => i; +} diff --git a/pkg/dynamic_modules/test/data/reshape_selectors/dynamic_interface.yaml b/pkg/dynamic_modules/test/data/reshape_selectors/dynamic_interface.yaml new file mode 100644 index 00000000000..4730c5ac72e --- /dev/null +++ b/pkg/dynamic_modules/test/data/reshape_selectors/dynamic_interface.yaml @@ -0,0 +1,29 @@ +# 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. +extendable: + - library: 'shared/shared.dart' + class: ['A', 'B'] + - library: 'dart:core' + class: 'Object' + +can-be-overridden: + - library: 'shared/shared.dart' + class: 'A' + member: 'foo' + - library: 'shared/shared.dart' + class: 'B' + member: 'foo' + +# TODO(sigmund): consider implying this for all extendable types. +callable: + # TODO(sigmund): This should be included by default + - library: 'dart:core' + class: 'Object' + - library: 'dart:core' + class: 'pragma' + member: '_' + - library: 'dart:core' + member: 'override' + - library: 'dart:core' + member: 'print' diff --git a/pkg/dynamic_modules/test/data/reshape_selectors/main.dart b/pkg/dynamic_modules/test/data/reshape_selectors/main.dart new file mode 100644 index 00000000000..fbb2754d695 --- /dev/null +++ b/pkg/dynamic_modules/test/data/reshape_selectors/main.dart @@ -0,0 +1,17 @@ +// 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 '../../common/testing.dart' as helper; + +import 'shared/shared.dart' show A, B; + +main() async { + final o = (await helper.load('entry1.dart')) as A; + final l = [A(), B()]; + for (final entry in l) { + entry.foo(); + } + o.foo(); + helper.done(); +} diff --git a/pkg/dynamic_modules/test/data/reshape_selectors/modules/entry1.dart b/pkg/dynamic_modules/test/data/reshape_selectors/modules/entry1.dart new file mode 100644 index 00000000000..dbc4189576e --- /dev/null +++ b/pkg/dynamic_modules/test/data/reshape_selectors/modules/entry1.dart @@ -0,0 +1,13 @@ +// 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 '../shared/shared.dart'; + +class C implements A, B { + @override + void foo() => print('C.foo'); +} + +@pragma('dyn-module:entry-point') +Object? dynamicModuleEntrypoint() => C(); diff --git a/pkg/dynamic_modules/test/data/reshape_selectors/shared/shared.dart b/pkg/dynamic_modules/test/data/reshape_selectors/shared/shared.dart new file mode 100644 index 00000000000..5412b395b20 --- /dev/null +++ b/pkg/dynamic_modules/test/data/reshape_selectors/shared/shared.dart @@ -0,0 +1,11 @@ +// 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. + +class A { + void foo() => print('A.foo'); +} + +class B { + void foo() => print('B.foo'); +} diff --git a/pkg/dynamic_modules/test/data/same_record_shape/dynamic_interface.yaml b/pkg/dynamic_modules/test/data/same_record_shape/dynamic_interface.yaml new file mode 100644 index 00000000000..ed584dee376 --- /dev/null +++ b/pkg/dynamic_modules/test/data/same_record_shape/dynamic_interface.yaml @@ -0,0 +1,14 @@ +# 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. + +callable: + # TODO(sigmund): This should be included by default + - library: 'dart:core' + class: 'Object' + - library: 'dart:core' + class: 'pragma' + member: '_' + - library: 'dart:core' + class: 'int' + diff --git a/pkg/dynamic_modules/test/data/same_record_shape/main.dart b/pkg/dynamic_modules/test/data/same_record_shape/main.dart new file mode 100644 index 00000000000..a87980b2fa8 --- /dev/null +++ b/pkg/dynamic_modules/test/data/same_record_shape/main.dart @@ -0,0 +1,17 @@ +// 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 '../../common/testing.dart' as helper; +import 'package:expect/expect.dart'; + +// Similar to `isolated_shared`, constant canonicalization distinguishes +// two constnats, even if they are created from a common library that was +// not part of the original application. +main() async { + final c1 = (await helper.load('entry1.dart')); + final c2 = (await helper.load('entry2.dart')); + + Expect.equals(c1, c2); + helper.done(); +} diff --git a/pkg/dynamic_modules/test/data/same_record_shape/modules/entry1.dart b/pkg/dynamic_modules/test/data/same_record_shape/modules/entry1.dart new file mode 100644 index 00000000000..ebe02283a97 --- /dev/null +++ b/pkg/dynamic_modules/test/data/same_record_shape/modules/entry1.dart @@ -0,0 +1,6 @@ +// 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. + +@pragma('dyn-module:entry-point') +Object? dynamicModuleEntrypoint() => (a: 3, 4); diff --git a/pkg/dynamic_modules/test/data/same_record_shape/modules/entry2.dart b/pkg/dynamic_modules/test/data/same_record_shape/modules/entry2.dart new file mode 100644 index 00000000000..ebe02283a97 --- /dev/null +++ b/pkg/dynamic_modules/test/data/same_record_shape/modules/entry2.dart @@ -0,0 +1,6 @@ +// 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. + +@pragma('dyn-module:entry-point') +Object? dynamicModuleEntrypoint() => (a: 3, 4); diff --git a/pkg/dynamic_modules/test/data/tearoff_no_concrete_impl/dynamic_interface.yaml b/pkg/dynamic_modules/test/data/tearoff_no_concrete_impl/dynamic_interface.yaml new file mode 100644 index 00000000000..bc18119149a --- /dev/null +++ b/pkg/dynamic_modules/test/data/tearoff_no_concrete_impl/dynamic_interface.yaml @@ -0,0 +1,27 @@ +# 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. +extendable: + - library: 'shared/shared.dart' + class: ['C'] + - library: 'dart:core' + class: 'Object' + +can-be-overridden: + - library: 'shared/shared.dart' + class: 'C' + member: 'foo' + +# TODO(sigmund): consider implying this for all extendable types. +callable: + # TODO(sigmund): This should be included by default + - library: 'dart:core' + class: 'Object' + - library: 'dart:core' + class: 'pragma' + member: '_' + - library: 'dart:core' + member: 'override' + - library: 'dart:core' + member: 'print' + diff --git a/pkg/dynamic_modules/test/data/tearoff_no_concrete_impl/main.dart b/pkg/dynamic_modules/test/data/tearoff_no_concrete_impl/main.dart new file mode 100644 index 00000000000..d1db943c6d0 --- /dev/null +++ b/pkg/dynamic_modules/test/data/tearoff_no_concrete_impl/main.dart @@ -0,0 +1,15 @@ +// 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 '../../common/testing.dart' as helper; + +import 'shared/shared.dart' show A, B, C; + +main() async { + final c = await helper.load('entry1.dart') as C?; + if (c != null) { + A(B(c).c.foo); + } + helper.done(); +} diff --git a/pkg/dynamic_modules/test/data/tearoff_no_concrete_impl/modules/entry1.dart b/pkg/dynamic_modules/test/data/tearoff_no_concrete_impl/modules/entry1.dart new file mode 100644 index 00000000000..93fdb632de9 --- /dev/null +++ b/pkg/dynamic_modules/test/data/tearoff_no_concrete_impl/modules/entry1.dart @@ -0,0 +1,15 @@ +// 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 '../shared/shared.dart'; + +class C1 implements C { + @override + void foo() { + print('foo'); + } +} + +@pragma('dyn-module:entry-point') +Object? dynamicModuleEntrypoint() => C1(); diff --git a/pkg/dynamic_modules/test/data/tearoff_no_concrete_impl/shared/shared.dart b/pkg/dynamic_modules/test/data/tearoff_no_concrete_impl/shared/shared.dart new file mode 100644 index 00000000000..e23448b252f --- /dev/null +++ b/pkg/dynamic_modules/test/data/tearoff_no_concrete_impl/shared/shared.dart @@ -0,0 +1,18 @@ +// 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. + +abstract class C { + void foo(); +} + +class B { + final C c; + + B(this.c); +} + +class A { + final void Function() f; + A(this.f); +} diff --git a/pkg/front_end/lib/src/kernel/constant_evaluator.dart b/pkg/front_end/lib/src/kernel/constant_evaluator.dart index fd26d782a35..905f24e24e9 100644 --- a/pkg/front_end/lib/src/kernel/constant_evaluator.dart +++ b/pkg/front_end/lib/src/kernel/constant_evaluator.dart @@ -6066,21 +6066,19 @@ abstract class ErrorReporter { bool get hasSeenError; } +// Coverage-ignore(suite): Not run. class SimpleErrorReporter implements ErrorReporter { const SimpleErrorReporter(); @override - // Coverage-ignore(suite): Not run. bool get supportsTrackingReportedErrors => false; @override - // Coverage-ignore(suite): Not run. bool get hasSeenError { return unsupported("SimpleErrorReporter.hasSeenError", -1, null); } @override - // Coverage-ignore(suite): Not run. void report(LocatedMessage message, [List? context]) { _report(message); if (context != null) { @@ -6090,12 +6088,10 @@ class SimpleErrorReporter implements ErrorReporter { } } - // Coverage-ignore(suite): Not run. void _report(LocatedMessage message) { reportMessage(message.uri, message.charOffset, message.problemMessage); } - // Coverage-ignore(suite): Not run. void reportMessage(Uri? uri, int offset, String message) { io.exitCode = 42; io.stderr.writeln('$uri:$offset Constant evaluation error: $message'); diff --git a/pkg/kernel/lib/kernel.dart b/pkg/kernel/lib/kernel.dart index bbbe2a23f01..3d9c85e3902 100644 --- a/pkg/kernel/lib/kernel.dart +++ b/pkg/kernel/lib/kernel.dart @@ -41,7 +41,8 @@ Component loadComponentSourceFromBytes(Uint8List bytes, return component; } -Future writeComponentToBinary(Component component, String path) { +Future writeComponentToBinary(Component component, String path, + {bool includeSource = true}) { IOSink sink; if (path == 'null' || path == 'stdout') { sink = stdout.nonBlocking; @@ -51,7 +52,8 @@ Future writeComponentToBinary(Component component, String path) { Future future; try { - new BinaryPrinter(sink).writeComponentFile(component); + new BinaryPrinter(sink, includeSources: includeSource) + .writeComponentFile(component); } finally { if (sink == stdout.nonBlocking) { future = sink.flush(); diff --git a/pkg/vm/lib/transformations/mixin_deduplication.dart b/pkg/vm/lib/transformations/mixin_deduplication.dart index 68f6d5f2f91..d90351117de 100644 --- a/pkg/vm/lib/transformations/mixin_deduplication.dart +++ b/pkg/vm/lib/transformations/mixin_deduplication.dart @@ -6,14 +6,13 @@ import 'package:kernel/ast.dart'; import 'package:kernel/type_algebra.dart'; /// De-duplication of identical mixin applications. -void transformComponent(Component component) { +void transformLibraries(List libraries) { final deduplicateMixins = new DeduplicateMixinsTransformer(); final referenceUpdater = ReferenceUpdater(deduplicateMixins); // Deduplicate mixins and re-resolve super initializers. // (this is a shallow transformation) - component.libraries - .forEach((library) => deduplicateMixins.visitLibrary(library, null)); + libraries.forEach((library) => deduplicateMixins.visitLibrary(library, null)); // Do a deep transformation to update references to the removed mixin // application classes in the interface targets and types. @@ -32,7 +31,12 @@ void transformComponent(Component component) { // TODO(dartbug.com/39375): Remove this extra O(N) pass over the AST if the // CFE decides to consistently let the interface target point to the mixin // class (instead of mixin application). - component.libraries.forEach(referenceUpdater.visitLibrary); + libraries.forEach(referenceUpdater.visitLibrary); +} + +/// De-duplication of identical mixin applications. +void transformComponent(Component component) { + transformLibraries(component.libraries); } class _DeduplicateMixinKey { diff --git a/pkg/vm/lib/transformations/pragma.dart b/pkg/vm/lib/transformations/pragma.dart index f98aaf21c68..064ca36b7a5 100644 --- a/pkg/vm/lib/transformations/pragma.dart +++ b/pkg/vm/lib/transformations/pragma.dart @@ -97,6 +97,30 @@ class ConstantPragmaAnnotationParser implements PragmaAnnotationParser { ConstantPragmaAnnotationParser(this.coreTypes, this.target); + ParsedEntryPointPragma? getEntryPointTypeFromOptions( + Constant options, String pragmaName) { + PragmaEntryPointType? type; + if (options is NullConstant) { + type = PragmaEntryPointType.Default; + } else if (options is BoolConstant && options.value == true) { + type = PragmaEntryPointType.Default; + } else if (options is StringConstant) { + if (options.value == "get") { + type = PragmaEntryPointType.GetterOnly; + } else if (options.value == "set") { + type = PragmaEntryPointType.SetterOnly; + } else if (options.value == "call") { + type = PragmaEntryPointType.CallOnly; + } else { + throw "Error: string directive to " + "@pragma('$pragmaName', ...) " + "must be either 'get' or 'set' for fields " + "or 'get' or 'call' for procedures."; + } + } + return type != null ? ParsedEntryPointPragma(type) : null; + } + ParsedPragma? parsePragma(Expression annotation) { InstanceConstant? pragmaConstant; if (annotation is ConstantExpression) { @@ -131,26 +155,7 @@ class ConstantPragmaAnnotationParser implements PragmaAnnotationParser { switch (pragmaName) { case kVmEntryPointPragmaName: - PragmaEntryPointType? type; - if (options is NullConstant) { - type = PragmaEntryPointType.Default; - } else if (options is BoolConstant && options.value == true) { - type = PragmaEntryPointType.Default; - } else if (options is StringConstant) { - if (options.value == "get") { - type = PragmaEntryPointType.GetterOnly; - } else if (options.value == "set") { - type = PragmaEntryPointType.SetterOnly; - } else if (options.value == "call") { - type = PragmaEntryPointType.CallOnly; - } else { - throw "Error: string directive to " - "@pragma('$kVmEntryPointPragmaName', ...) " - "must be either 'get' or 'set' for fields " - "or 'get' or 'call' for procedures."; - } - } - return type != null ? ParsedEntryPointPragma(type) : null; + return getEntryPointTypeFromOptions(options, pragmaName); case kVmExactResultTypePragmaName: if (options is TypeLiteralConstant) { return ParsedResultTypeByTypePragma(options.type, false); @@ -207,7 +212,7 @@ class ConstantPragmaAnnotationParser implements PragmaAnnotationParser { PragmaEntryPointType.CanBeOverridden); case kDynModuleCallablePragmaName: case kDynModuleImplicitlyCallablePragmaName: - return const ParsedEntryPointPragma(PragmaEntryPointType.Default); + return getEntryPointTypeFromOptions(options, pragmaName); case kDynModuleEntryPointPragmaName: return const ParsedDynModuleEntryPointPragma(); default: diff --git a/pkg/vm/lib/transformations/unreachable_code_elimination.dart b/pkg/vm/lib/transformations/unreachable_code_elimination.dart index 1fe0706d87b..f1875c6d227 100644 --- a/pkg/vm/lib/transformations/unreachable_code_elimination.dart +++ b/pkg/vm/lib/transformations/unreachable_code_elimination.dart @@ -21,6 +21,15 @@ Component transformComponent(Target target, Component component, return component; } +List transformLibraries(Target target, List libraries, + VMConstantEvaluator evaluator, bool enableAsserts) { + for (final library in libraries) { + SimpleUnreachableCodeElimination(evaluator, enableAsserts: enableAsserts) + .visitLibrary(library, null); + } + return libraries; +} + class SimpleUnreachableCodeElimination extends RemovingTransformer { final bool enableAsserts; final VMConstantEvaluator constantEvaluator; diff --git a/pkg/wasm_builder/lib/src/builder/functions.dart b/pkg/wasm_builder/lib/src/builder/functions.dart index c10c5ddfe0e..f8eb306359e 100644 --- a/pkg/wasm_builder/lib/src/builder/functions.dart +++ b/pkg/wasm_builder/lib/src/builder/functions.dart @@ -65,6 +65,7 @@ class FunctionsBuilder with Builder { /// Declare [function] as a module element so it can be used in a constant /// context. void declare(ir.BaseFunction function) { + assert(function.enclosingModule == _module); _declaredFunctions.add(function); } diff --git a/pkg/wasm_builder/lib/src/builder/table.dart b/pkg/wasm_builder/lib/src/builder/table.dart index aa4598d083a..fdf46bd7721 100644 --- a/pkg/wasm_builder/lib/src/builder/table.dart +++ b/pkg/wasm_builder/lib/src/builder/table.dart @@ -17,6 +17,7 @@ class TableBuilder extends ir.Table with IndexableBuilder { "Elements are only supported for funcref tables"); assert(maxSize == null || index < maxSize!, 'Index $index greater than max table size $maxSize'); + assert(function.enclosingModule == enclosingModule); if (index >= elements.length) { elements.length = index + 1; } diff --git a/sdk/lib/_internal/wasm/lib/class_id.dart b/sdk/lib/_internal/wasm/lib/class_id.dart index 2250d330eab..d29b8205e94 100644 --- a/sdk/lib/_internal/wasm/lib/class_id.dart +++ b/sdk/lib/_internal/wasm/lib/class_id.dart @@ -78,4 +78,73 @@ class ClassID { // Dummy, only used by VM-specific hash table code. static final WasmI32 numPredefinedCids = 1.toWasmI32(); + + // The maximum class ID in the main module of the program. + external static WasmI32 get maxClassId; +} + +const int mainModuleId = 0; + +/// The ith entry in this array is the max global class ID for module i. +WasmArray _moduleMaxClassId = WasmArray.filled(1, ClassID.maxClassId); + +/// Gets the module ID for a given global class ID. +@pragma('dyn-module:callable') +int classIdToModuleId(WasmI32 classId) { + if (!hasDynamicModuleSupport) { + assert( + _moduleMaxClassId.length == 1 && + classId <= _moduleMaxClassId[mainModuleId], + ); + return mainModuleId; + } + // NOTE: This could be made into binary search if many modules are getting + // registered. For now we expect few so a linear search is fine. + final array = _moduleMaxClassId; + for (int i = 0; i < array.length; i++) { + if (classId <= array[i]) return i; + } + throw ArgumentError(); +} + +/// Registers a new dynamic module based on the size of its new class ID range. +@pragma('dyn-module:callable') +int registerModuleClassRange(WasmI32 rangeSize) { + final oldRanges = _moduleMaxClassId; + final moduleId = oldRanges.length; + + final newRanges = WasmArray.filled(moduleId + 1, 0.toWasmI32()); + newRanges.copy(0, oldRanges, 0, oldRanges.length); + newRanges[moduleId] = rangeSize + oldRanges[moduleId - 1]; + _moduleMaxClassId = newRanges; + return moduleId; +} + +/// Scopes a class ID to the enclosing module giving an ID relative to only +/// classes defined in the module defining the class. +@pragma('dyn-module:callable') +WasmI32 scopeClassId(WasmI32 classId) { + final moduleId = classIdToModuleId(classId); + if (moduleId == 0) return classId; + return classId - (_moduleMaxClassId[moduleId - 1] + 1.toWasmI32()); +} + +/// Produces a localized class ID from a global class ID. A local ID is offset +/// relative to the main module rather than the global ID space. The compiler +/// produces localized IDs since it can't track global IDs. Multiple classes +/// can map to the same localized class ID. +@pragma('dyn-module:callable') +WasmI32 localizeClassId(WasmI32 classId) { + final moduleId = classIdToModuleId(classId); + if (moduleId == 0) return classId; + return classId - _moduleMaxClassId[moduleId - 1] + ClassID.maxClassId; +} + +/// Produces a global class ID from a local class ID. A global ID is offset +/// relative to all registered dynamic modules. Each class will have a unique +/// global class ID. +@pragma('dyn-module:callable') +WasmI32 globalizeClassId(WasmI32 classId, int moduleId) { + if (moduleId == 0) return classId; + return classId - ClassID.maxClassId + _moduleMaxClassId[moduleId - 1]; } diff --git a/sdk/lib/_internal/wasm/lib/core_patch.dart b/sdk/lib/_internal/wasm/lib/core_patch.dart index b9c6e678a69..5f129f5fc9f 100644 --- a/sdk/lib/_internal/wasm/lib/core_patch.dart +++ b/sdk/lib/_internal/wasm/lib/core_patch.dart @@ -5,15 +5,19 @@ import "dart:_internal" show ClassID, + classIdToModuleId, CodeUnits, doubleToIntBits, EfficientLengthIterable, FixedLengthListMixin, + hasDynamicModuleSupport, intBitsToDouble, IterableElementError, jsonEncode, + localizeClassId, ListIterator, Lists, + mainModuleId, minify, mix64, patch, @@ -63,6 +67,8 @@ import 'dart:_string_helper'; import 'dart:_wasm'; +import 'internal_patch.dart'; + part "closure.dart"; part "double_patch.dart"; part "errors_patch.dart"; diff --git a/sdk/lib/_internal/wasm/lib/dynamic_module.dart b/sdk/lib/_internal/wasm/lib/dynamic_module.dart new file mode 100644 index 00000000000..f72cb4a02fc --- /dev/null +++ b/sdk/lib/_internal/wasm/lib/dynamic_module.dart @@ -0,0 +1,199 @@ +// 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. + +part of "internal_patch.dart"; + +@pragma("wasm:import", "deferredLibraryHelper.loadDynamicModuleFromUri") +external WasmExternRef _loadModuleFromUri(WasmExternRef moduleUri); + +@pragma("wasm:import", "deferredLibraryHelper.loadDynamicModuleFromBytes") +external WasmExternRef _loadModuleFromBytes(WasmExternRef moduleUri); + +@patch +Future loadDynamicModule({Uri? uri, Uint8List? bytes}) { + JSPromise loadPromise; + if (uri != null) { + final uriString = '$uri'; + loadPromise = + (_loadModuleFromUri(uriString.toJS.toExternRef!).toJS as JSPromise); + } else if (bytes != null) { + loadPromise = + (_loadModuleFromBytes(bytes.buffer.toJS.toExternRef!).toJS + as JSPromise); + } else { + throw ArgumentError( + 'Must provide either `uri` or `bytes` to `loadDynamicModule`', + ); + } + return loadPromise.toDart.then( + (entryPoint) => + dartifyRaw(((entryPoint as JSFunction).callAsFunction())?.toExternRef), + ); +} + +/// Only classes defined in the main module require runtime canonicalization. +/// Classes defined in the dynamic modules cannot be shared outside of that +/// module so can be canonicalized at compile time. So this will always only be +/// the number of classes in the main module. +external int get _numClassesForConstCaches; + +/// Stores caches to canonicalize instances of objects. The i'th entry contains +/// a cache of entities for type Class_i where Class_i is the class with +/// ID i. +@pragma('dyn-module:callable') +final WasmArray _constCacheByType = WasmArray.filled( + _numClassesForConstCaches, + null, +); + +@pragma('dyn-module:callable') +WasmConstCache getConstCache(int classId) { + return _constCacheByType[classId] ??= WasmConstCache(); +} + +/// Runtime cache containing constant values for a particular class. +/// +/// Contains growable wasm arrays to store values in. Doesn't use a Dart +/// growable List since Lists require a runtime type to be instantiated. This +/// would cause an instantiation loop since runtime types are constants. +/// +/// Values of type WasmArray are stored separately from values of type T, +/// so that the appropriate equality function can be used to compare them. +/// +/// Note: The functions in here avoid polymorphic helpers as this would require +/// instantiating Type constants and we cannot use constants in code to create +/// constants. +class WasmConstCache { + int _nextIndex = 0; + WasmArray _data = WasmArray.filled( + 2, + WasmAnyRef.fromObject(Object()), + ); + + @pragma('dyn-module:callable') + WasmConstCache(); + + @pragma('dyn-module:callable', 'call') + Object canonicalizeValue( + Object value, + WasmFunction check, + ) { + for (int i = 0; i < _nextIndex; i++) { + final cachedValue = _data[i]; + if (check.call(value, cachedValue)) { + return cachedValue; + } + } + if (_data.length == _nextIndex) { + final newCache = WasmArray.filled(_data.length * 2, _data[0]); + newCache.copy(0, _data, 0, _data.length); + _data = newCache; + } + _data[_nextIndex++] = value; + return value; + } +} + +@pragma('dyn-module:callable') +final objectConstArray = WasmArrayConstCache(); +@pragma('dyn-module:callable') +final stringConstArray = WasmArrayConstCache(); +@pragma('dyn-module:callable') +final stringConstImmutableArray = WasmArrayConstCache(); +@pragma('dyn-module:callable') +final typeConstArray = WasmArrayConstCache(); +@pragma('dyn-module:callable') +final typeArrayConstArray = WasmArrayConstCache(); +@pragma('dyn-module:callable') +final nameParameterConstArray = WasmArrayConstCache(); +@pragma('dyn-module:callable') +final i8ConstImmutableArray = WasmArrayConstCache(); +@pragma('dyn-module:callable') +final i32ConstArray = WasmArrayConstCache(); +@pragma('dyn-module:callable') +final i64ConstImmutableArray = WasmArrayConstCache(); + +class WasmArrayConstCache { + // Guaranteed by construction to contain only arrays with the same type. + WasmArray? _data; + int _nextIndex = 0; + + WasmArrayConstCache(); + + @pragma('dyn-module:callable', 'call') + WasmArrayRef canonicalizeArrayValue( + WasmArrayRef value, + WasmFunction check, + ) { + var data = + _data ??= WasmArray.filled( + 2, + WasmArray.filled(0, WasmAnyRef.fromObject(Object())), + ); + for (int i = 0; i < _nextIndex; i++) { + final cachedValue = data[i]; + if (value.length != cachedValue.length) continue; + // The cachedValue must be the second value here so that it's type is + // verified. + if (check.call(value, cachedValue)) { + return cachedValue; + } + } + if (data.length == _nextIndex) { + final newCache = WasmArray.filled(data.length * 2, data[0]); + newCache.copy(0, data, 0, data.length); + data = _data = newCache; + } + data[_nextIndex++] = value; + return value; + } +} + +/// A table where there is one row per module and each column represents the +/// updateable function for the corresponding allocated key index. The compiler +/// tracks updateable functions via a unique string key which is converted to an +/// integer index at either compile time or runtime. Each module may have an +/// implementation of that function key. +WasmArray> _updateableRefs = WasmArray.literal( + const [], +); + +/// Get the function reference implementation for allocated index [key] as +/// defined by module [moduleId]. +@pragma('dyn-module:callable') +WasmFuncRef? getUpdateableFuncRef(int moduleId, int key) { + final moduleRefs = _updateableRefs[moduleId]; + if (key >= moduleRefs.length) return null; + return moduleRefs[key]; +} + +/// Register the updateable function ref implementations into a new module. +/// [refs] contains implementations of pre-allocated (i.e. defined in main) +/// keys. [stringKeys] and [stringRefs] contain runtime allocated refs and their +/// keys. If any of the entries in [stringKeys] are new, an index will be +/// allocated for them. +@pragma('dyn-module:callable') +void registerUpdateableFuncRefs(WasmArray refs) { + final oldUpdateableRefs = _updateableRefs; + final oldSize = oldUpdateableRefs.length; + final newUpdateableRefs = WasmArray>.filled( + oldSize + 1, + refs, + ); + newUpdateableRefs.copy(0, oldUpdateableRefs, 0, oldSize); + _updateableRefs = newUpdateableRefs; +} + +Set _loadedLibraryUris = {}; + +@pragma('dyn-module:callable') +void registerLibraryUris(List uris) { + for (final uri in uris) { + if (!_loadedLibraryUris.add(uri)) { + throw StateError( + 'Cannot define the same library twice in dynamic modules.', + ); + } + } +} diff --git a/sdk/lib/_internal/wasm/lib/internal_patch.dart b/sdk/lib/_internal/wasm/lib/internal_patch.dart index 47ad8d92a8e..7c92aa43474 100644 --- a/sdk/lib/_internal/wasm/lib/internal_patch.dart +++ b/sdk/lib/_internal/wasm/lib/internal_patch.dart @@ -4,25 +4,35 @@ import 'dart:async'; import "dart:_js_helper" - show JS, JSAnyToExternRef, jsStringFromDartString, jsStringToDartString; + show + JS, + JSAnyToExternRef, + jsStringFromDartString, + jsStringToDartString, + jsUint8ArrayFromDartUint8List; import "dart:_js_types" show JSStringImpl; import 'dart:_string'; import 'dart:js_interop' show + ByteBufferToJSArrayBuffer, JSArray, + JSFunction, + JSFunctionUtilExtension, JSString, JSArrayToList, JSStringToString, JSPromise, JSPromiseToFuture, StringToJSString; -import 'dart:_js_helper' show JSValue; +import 'dart:_js_helper' show dartifyRaw, JSValue; import 'dart:_js_types'; import 'dart:_wasm'; +import 'dart:math'; import 'dart:typed_data' show Uint8List; part "class_id.dart"; part "deferred.dart"; +part "dynamic_module.dart"; part "print_patch.dart"; part "symbol_patch.dart"; @@ -200,9 +210,14 @@ external bool get checkBounds; /// evaluator, and its value depends on `--minify`. external bool get minify; -@patch -Future loadDynamicModule({Uri? uri, Uint8List? bytes}) => - throw 'Unsupported operation'; +/// Whether dynamic module support is enabled for this build. +/// +/// Enables shortcuts in some runtime logic if it is known that no support is +/// needed for dynamic modules. +/// +/// Reads of this variable is evaluated before the TFA by the constant +/// evaluator, and its value depends on `--dynamic-module-main`. +external bool get hasDynamicModuleSupport; /// Compiler intrinsic to push an element to a Wasm array in a class field or /// variable. diff --git a/sdk/lib/_internal/wasm/lib/type.dart b/sdk/lib/_internal/wasm/lib/type.dart index 28b7d41c6c8..ef41d09d99a 100644 --- a/sdk/lib/_internal/wasm/lib/type.dart +++ b/sdk/lib/_internal/wasm/lib/type.dart @@ -337,8 +337,10 @@ class _InterfaceType extends _Type { @override String toString() { StringBuffer s = StringBuffer(); - final int index = classId.toIntSigned(); - s.write(_typeNames?[index] ?? 'minified:Class$index'); + final int index = scopeClassId(classId).toIntSigned(); + s.write( + _moduleRttForClassId(classId).typeNames?[index] ?? 'minified:Class$index', + ); if (typeArguments.isNotEmpty) { s.write("<"); for (int i = 0; i < typeArguments.length; i++) { @@ -654,40 +656,81 @@ class _RecordType extends _Type { identical(names, other.names); } -/// Maps each class id representing a type to the offset of that type-checker -/// row in [_typeRowDisplacementTable]. -external WasmArray get _typeRowDisplacementOffsets; - -/// Tells whether a class `Sub` is a subclass of another class `Base. -/// -/// Used via -/// ``` -/// baseOffset = _typeRowDisplacementOffsets[Base.classId]` -/// index = baseOffset + Sub.classId -/// value = _typeRowDisplacementTable[index] -/// if (value == Base.classId) { -/// // => Sub.classId is a subclass of Base.classId -/// // => Can use `index` into `_typeRowDisplacementSubstTable[index]` -/// } -///``` -external WasmArray get _typeRowDisplacementTable; - -/// Holds the canonical type argument substitution index for matching table -/// entries (see above). -/// -/// If `index` matches in [_typeRowDisplacementTable] then the same index can be -/// used in this array to find the type argument substitution array for -/// translating type arguments from a base class to a direct/indirect class. -external WasmArray> get _typeRowDisplacementSubstTable; - -/// The names of all classes (indexed by class id) or null (if `--minify` was -/// used) -external WasmArray? get _typeNames; - -/// The non-negative index into [_typeRowDisplacementSubstTable] that represents -/// that no substitution is needed. +/// The non-negative index into [_ModuleRtt.typeRowDisplacementSubstTable] that +/// represents that no substitution is needed. external WasmI32 get _noSubstitutionIndex; +external _ModuleRtt get _mainModuleRtt; + +WasmArray<_ModuleRtt> _rttInfoForModule = WasmArray.filled(1, _mainModuleRtt); + +@pragma('wasm:entry-point') +void _registerModuleRtt(int moduleId, _ModuleRtt moduleRtt) { + if (moduleId >= _rttInfoForModule.length) { + final oldArray = _rttInfoForModule; + final newArray = WasmArray.filled(moduleId + 1, moduleRtt); + newArray.copy(0, oldArray, 0, oldArray.length); + _rttInfoForModule = newArray; + } + _rttInfoForModule[moduleId] = moduleRtt; +} + +_ModuleRtt _moduleRttForClassId(WasmI32 classId) { + if (!hasDynamicModuleSupport) { + assert(classId <= ClassID.maxClassId); + return _mainModuleRtt; + } + return _rttInfoForModule[classIdToModuleId(classId)]; +} + +class _ModuleRtt { + /// Maps each class id representing a type to the offset of that type-checker + /// row in [typeRowDisplacementTable]. + final WasmArray typeRowDisplacementOffsets; + + /// Tells whether a class `Sub` is a subclass of another class `Base. + /// + /// Used via + /// ``` + /// baseOffset = _typeRowDisplacementOffsets[Base.classId]` + /// index = baseOffset + Sub.classId + /// value = _typeRowDisplacementTable[index] + /// if (value == Base.classId) { + /// // => Sub.classId is a subclass of Base.classId + /// // => Can use `index` into `typeRowDisplacementSubstTable[index]` + /// } + ///``` + /// + /// Takes two class IDs of classes to be queried. For dynamic modules this will + /// allow the compiler to scope the query to a specific module. The class IDs + /// are ignored for all other compilations. + final WasmArray typeRowDisplacementTable; + + /// Holds the canonical type argument substitution index for matching table + /// entries (see above). + /// + /// If `index` matches in [typeRowDisplacementTable] then the same index can be + /// used in this array to find the type argument substitution array for + /// translating type arguments from a base class to a direct/indirect class. + /// + /// Takes two class IDs of classes to be queried. For dynamic modules this will + /// allow the compiler to scope the query to a specific module. The class IDs + /// are ignored for all other compilations. + final WasmArray> typeRowDisplacementSubstTable; + + /// The names of all classes (indexed by class id) or null (if `--minify` was + /// used) + final WasmArray? typeNames; + + @pragma("wasm:entry-point") + const _ModuleRtt( + this.typeRowDisplacementOffsets, + this.typeRowDisplacementTable, + this.typeRowDisplacementSubstTable, + this.typeNames, + ); +} + /// Type parameter environment used while comparing function types. /// /// In the case of nested function types, the environment refers to the @@ -973,6 +1016,9 @@ abstract class _TypeUniverse { tTypeArguments, tEnv, substitutionIndex, + // Since these are already proved to be subclasses, sId and tId are in the + // same module or sId's module contains info for both. + _moduleRttForClassId(sId).typeRowDisplacementSubstTable, ); } @@ -998,6 +1044,9 @@ abstract class _TypeUniverse { sTypeArguments, tTypeArgument0, substitutionIndex, + // Since these are already proved to be subclasses, sId and tId are in the + // same module or sId's module contains info for both. + _moduleRttForClassId(sId).typeRowDisplacementSubstTable, ); } @@ -1020,6 +1069,9 @@ abstract class _TypeUniverse { tTypeArgument0, tTypeArgument1, substitutionIndex, + // Since these are already proved to be subclasses, sId and tId are in the + // same module or sId's module contains info for both. + _moduleRttForClassId(sId).typeRowDisplacementSubstTable, ); } @@ -1046,6 +1098,9 @@ abstract class _TypeUniverse { tTypeArguments, null, substitutionIndex, + // Since these are already proved to be subclasses, sId and tId are in the + // same module or sId's module contains info for both. + _moduleRttForClassId(sId).typeRowDisplacementSubstTable, ); } @@ -1053,6 +1108,7 @@ abstract class _TypeUniverse { WasmArray<_Type> sTypeArguments, _Type tTypeArgument0, WasmI32 substitutionIndex, + WasmArray> substTable, ) { // Check individual type arguments without substitution (fast case). if (substitutionIndex == _noSubstitutionIndex) { @@ -1060,8 +1116,7 @@ abstract class _TypeUniverse { } // Substitue each argument before performing the subtype check (slow case). - final substitutions = - _typeRowDisplacementSubstTable[substitutionIndex.toIntSigned()]; + final substitutions = substTable[substitutionIndex.toIntSigned()]; assert(substitutions.length == 1); final sArgForClassT = substituteTypeArgument( substitutions[0], @@ -1076,6 +1131,7 @@ abstract class _TypeUniverse { _Type tTypeArgument0, _Type tTypeArgument1, WasmI32 substitutionIndex, + WasmArray> substTable, ) { // Check individual type arguments without substitution (fast case). if (substitutionIndex == _noSubstitutionIndex) { @@ -1084,8 +1140,7 @@ abstract class _TypeUniverse { } // Substitue each argument before performing the subtype check (slow case). - final substitutions = - _typeRowDisplacementSubstTable[substitutionIndex.toIntSigned()]; + final substitutions = substTable[substitutionIndex.toIntSigned()]; assert(substitutions.length == 2); final sArg1ForClassT = substituteTypeArgument( substitutions[1], @@ -1107,6 +1162,7 @@ abstract class _TypeUniverse { WasmArray<_Type> tTypeArguments, _Environment? tEnv, WasmI32 substitutionIndex, + WasmArray> substTable, ) { // Check individual type arguments without substitution (fast case). if (substitutionIndex == _noSubstitutionIndex) { @@ -1119,8 +1175,7 @@ abstract class _TypeUniverse { } // Substitute each argument before performing the subtype check (slow case). - final substitutions = - _typeRowDisplacementSubstTable[substitutionIndex.toIntSigned()]; + final substitutions = substTable[substitutionIndex.toIntSigned()]; assert(substitutions.length == tTypeArguments.length); for (int i = 0; i < tTypeArguments.length; i++) { final sArgForClassT = substituteTypeArgument( @@ -1155,8 +1210,17 @@ abstract class _TypeUniverse { @pragma('wasm:prefer-inline') static WasmI32 _checkSubclassRelationshipViaTable(WasmI32 sId, WasmI32 tId) { - final offset = _typeRowDisplacementOffsets; - final table = _typeRowDisplacementTable; + final sModuleId = classIdToModuleId(sId); + final tModuleId = classIdToModuleId(tId); + if (tModuleId != mainModuleId) { + if (sModuleId != tModuleId) return -1.toWasmI32(); + } + + final rtt = _rttInfoForModule[sModuleId]; + final offset = rtt.typeRowDisplacementOffsets; + final table = rtt.typeRowDisplacementTable; + sId = localizeClassId(sId); + tId = localizeClassId(tId); final WasmI32 index = offset[tId.toIntSigned()] + sId; if (index.geU(table.length.toWasmI32())) return (-1).toWasmI32();