// Copyright (c) 2024, 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' show JsonEncoder; import 'dart:io'; import 'package:_fe_analyzer_shared/src/util/relativize.dart' show relativizeUri; import 'package:compiler/src/deferred_load/program_split_constraints/nodes.dart'; import 'package:compiler/src/deferred_load/program_split_constraints/parser.dart'; import 'package:kernel/ast.dart'; import 'package:kernel/class_hierarchy.dart'; import 'package:kernel/core_types.dart'; import 'await_transformer.dart' as await_transformer; import 'compiler_options.dart'; import 'deferred_load/partition.dart'; import 'io_util.dart'; import 'modules.dart'; import 'target.dart'; import 'util.dart' show addPragma, getPragma; class DeferredLoadingModuleStrategy extends ModuleStrategy { final Component component; final WasmCompilerOptions options; final WasmTarget kernelTarget; final CoreTypes coreTypes; final CompilerPhaseInputOutputManager ioManager; late final ModuleOutputData moduleOutputData; DeferredLoadingModuleStrategy( this.component, this.options, this.kernelTarget, this.coreTypes, this.ioManager, ); @override void addEntryPoints() {} @override void prepareComponent() {} @override Future processComponentAfterTfa( DeferredModuleLoadingMap loadingMap, ) async { ConstraintData? constraints; if (options.programSplitConstraintsUri != null) { final json = await ioManager.readString( options.programSplitConstraintsUri!, ); constraints = Parser().read(json); } final partition = partitionAppplication( coreTypes, component, options.translatorOptions.enableAsserts, loadingMap, findWasmRoots(coreTypes, component), constraints: constraints, ); final builder = ModuleMetadataBuilder(options); final moduleMetadata = {}; for (final part in partition.parts) { moduleMetadata[part] = builder.buildModuleMetadata(); } final referenceToModuleMetadata = {}; partition.referenceToPart.forEach((reference, output) { referenceToModuleMetadata[reference] = moduleMetadata[output]!; }); final constantToModuleMetadata = {}; partition.constantToPart.forEach((constant, output) { constantToModuleMetadata[constant] = moduleMetadata[output]!; }); partition.deferredImportToParts.forEach((deferredImport, parts) { final wasmModules = [for (final o in parts) moduleMetadata[o]!]; loadingMap.addModuleToLibraryImport( deferredImport.enclosingLibrary, deferredImport.name!, wasmModules, ); }); // Some elements may not have gotten a module assigned in the above // procedure. This can have a varity of reasons: // // - A class that's never really used but still in the AST because TFA // left it there (this happens occasionally because we enable RTA before // TFA, RTA is less precised and may mark a class as allocated but TFA // later on optimizes usages away which leave the class as non-abstract // but unused). // - A class is only used in type expressions // - ... // // The code generator still requires every library to have a corresponding // module, so we make an artificial one here. final dummyModule = builder.buildModuleMetadata(); moduleOutputData = ModuleOutputData.fineGrainedSplit( [...moduleMetadata.values, dummyModule], referenceToModuleMetadata, constantToModuleMetadata, dummyModule, ); } @override ModuleOutputData buildModuleOutputData() => moduleOutputData; } class StressTestModuleStrategy extends ModuleStrategy { final Component component; final CoreTypes coreTypes; final WasmTarget kernelTarget; final ClassHierarchy classHierarchy; final WasmCompilerOptions options; late final ModuleOutputData moduleOutputData; /// 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), ), }; StressTestModuleStrategy( this.component, this.coreTypes, this.options, this.kernelTarget, this.classHierarchy, ); @override void addEntryPoints() {} /// 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. @override void prepareComponent() { final initLibraries = _testModeMainLibraries; final internalLib = coreTypes.index.getLibrary('dart:_internal'); 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 import = LibraryDependency.deferredImport( library, '${library.importUri}', ); internalLib.addDependency(import); loadStatements.add( ExpressionStatement(AwaitExpression(LoadLibrary(import))), ); } invokeMain.function.asyncMarker = AsyncMarker.Async; invokeMain.function.emittedValueType = const VoidType(); final oldBody = invokeMain.function.body!; // Add print of 'unittest-suite-wait-for-done' to indicate to test harnesses // that the test contains async work. Any test 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, ); } @override Future processComponentAfterTfa( DeferredModuleLoadingMap loadingMap, ) async { final moduleBuilder = ModuleMetadataBuilder(options); final mainModule = moduleBuilder.buildModuleMetadata(); final initLibraries = _testModeMainLibraries; final modules = []; final importMap = >{}; final internalLib = coreTypes.index.getLibrary('dart:_internal'); // Put each library in a separate module. final libraryMap = {}; for (final library in component.libraries) { if (initLibraries.contains(library)) { libraryMap[library] = mainModule; continue; } final module = moduleBuilder.buildModuleMetadata(); modules.add(module); libraryMap[library] = module; final importName = '${library.importUri}'; importMap[importName] = [module]; loadingMap.addModuleToLibraryImport(internalLib, importName, [module]); } moduleOutputData = ModuleOutputData.librarySplit( [mainModule, ...modules], libraryMap, null, ); } @override ModuleOutputData buildModuleOutputData() => moduleOutputData; } Future writeDeferredMapFile( Component component, CoreTypes coreTypes, WasmCompilerOptions options, DeferredModuleLoadingMap loadingMap, ) async { final file = File.fromUri(options.deferredMapUri!); await file.create(recursive: true); await file.writeAsString( _generateDeferredMapJson( component, component.mainMethod!.enclosingLibrary.importUri, loadingMap, ), ); } String _generateDeferredMapJson( Component component, Uri rootLibraryUri, DeferredModuleLoadingMap loadingMap, ) { final output = {}; loadingMap.loadIds.forEach((tuple, loadId) { final modules = loadingMap.moduleMap[loadId]; final (library, prefix) = tuple; final libOutput = output[relativizeUri(rootLibraryUri, library.importUri, false)] ??= { 'name': library.name ?? '', 'imports': >{}, 'importPrefixToLoadId': {}, }; // For consistency with dart2js we use 1-based indexing in the generated // json file. final dart2jsLoadId = loadId + 1; final dart2jsLoadIdStr = dart2jsLoadId.toString(); libOutput['imports']![dart2jsLoadIdStr] = modules .map((m) => m.moduleName) .toList(); libOutput['importPrefixToLoadId'][prefix] = dart2jsLoadIdStr; }); return const JsonEncoder.withIndent(' ').convert(output); } class DeferredLoadingLowering extends Transformer { final CoreTypes coreTypes; final DeferredModuleLoadingMap loadingMap; // These will only exist if the [Component] has actual deferred libraries. So // access them lazily. late final Procedure _loadLibraryFromLoadId = coreTypes.index .getTopLevelProcedure('dart:_internal', 'loadLibraryFromLoadId'); late final Procedure _checkLibraryIsLoadedFromLoadId = coreTypes.index .getTopLevelProcedure('dart:_internal', 'checkLibraryIsLoadedFromLoadId'); Map _libraryLoadIds = {}; DeferredLoadingLowering(this.coreTypes, this.loadingMap); static void markRuntimeFunctionsAsEntrypoints(CoreTypes coreTypes) { addEntryPointPragma( coreTypes, coreTypes.index.getTopLevelProcedure( 'dart:_internal', 'loadLibraryFromLoadId', ), ); addEntryPointPragma( coreTypes, coreTypes.index.getTopLevelProcedure( 'dart:_internal', 'checkLibraryIsLoadedFromLoadId', ), ); } @override TreeNode visitComponent(Component node) { // Assign a load ID to each deferred import. _libraryLoadIds = {}; for (final library in node.libraries) { for (final dep in library.dependencies) { if (!dep.isDeferred) continue; final name = dep.name!; _libraryLoadIds[dep] = loadingMap.loadIds[(library, name)]!; } } return super.visitComponent(node); } @override TreeNode visitLoadLibrary(LoadLibrary node) { final loadId = _libraryLoadIds[node.import]!; return StaticInvocation( _loadLibraryFromLoadId, Arguments([IntLiteral(loadId)]), ); } @override TreeNode visitCheckLibraryIsLoaded(CheckLibraryIsLoaded node) { final loadId = _libraryLoadIds[node.import]!; return StaticInvocation( _checkLibraryIsLoadedFromLoadId, Arguments([IntLiteral(loadId)]), ); } static void addEntryPointPragma(CoreTypes coreTypes, Annotatable node) { addPragma(node, 'wasm:entry-point', coreTypes, value: BoolConstant(true)); } } Set findWasmRoots(CoreTypes coreTypes, Component component) { final exports = {}; final trueConstant = BoolConstant(true); bool check(Annotatable node) { if (getPragma(coreTypes, node, 'wasm:export') != null || getPragma( coreTypes, node, 'wasm:entry-point', defaultValue: trueConstant, ) != null) { return true; } return false; } for (final library in component.libraries) { for (final member in library.members) { if (check(member)) exports.add(member.reference); } for (final klass in library.classes) { if (check(klass)) exports.add(klass.reference); for (final member in klass.members) { if (check(member)) { if (member is Field) { exports.add(member.getterReference); if (member.hasSetter) { exports.add(member.setterReference!); } } else { exports.add(member.reference); } } } } } return exports; }