| // 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:collection'; |
| |
| import 'package:compiler/src/deferred_load/program_split_constraints/builder.dart' |
| as psc; |
| import 'package:compiler/src/deferred_load/program_split_constraints/nodes.dart'; |
| import 'package:kernel/class_hierarchy.dart'; |
| import 'package:kernel/core_types.dart'; |
| import 'package:kernel/kernel.dart' hide Node, NamedNode; |
| import 'package:vm/metadata/direct_call.dart'; |
| import 'package:vm/metadata/procedure_attributes.dart'; |
| import 'package:vm/metadata/table_selector.dart'; |
| |
| import '../modules.dart' show DeferredModuleLoadingMap; |
| import '../reference_extensions.dart'; |
| import 'dependencies.dart'; |
| import 'devirtualization_oracle.dart'; |
| import 'dominators.dart'; |
| import 'import_set.dart'; |
| |
| export 'import_set.dart' show Part; |
| |
| Partitioning partitionAppplication( |
| CoreTypes coreTypes, |
| Component component, |
| bool assertsEnabled, |
| DeferredModuleLoadingMap loadingMap, |
| Set<Reference> roots, { |
| ConstraintData? constraints, |
| }) { |
| final Map<TreeNode, DirectCallMetadata> directCallMetadata = |
| (component.metadata[DirectCallMetadataRepository.repositoryTag] |
| as DirectCallMetadataRepository) |
| .mapping; |
| |
| late final Map<TreeNode, ProcedureAttributesMetadata> |
| procedureAttributeMetadata = |
| (component.metadata[ProcedureAttributesMetadataRepository.repositoryTag] |
| as ProcedureAttributesMetadataRepository) |
| .mapping; |
| late final List<TableSelectorInfo> selectorMetadata = |
| (component.metadata[TableSelectorMetadataRepository.repositoryTag] |
| as TableSelectorMetadataRepository) |
| .mapping[component]! |
| .selectors; |
| |
| // Assume instance members which are marked as `@pragma('wasm:entry-point')` |
| // have interface calls from the outside (which may be e.g. backend code |
| // generator emitting dispatch table calls to it). |
| final selectorRoots = <int>{}; |
| for (final root in roots) { |
| final node = root.node; |
| if (node is Member && node.isInstanceMember && !node.isAbstract) { |
| final metadata = procedureAttributeMetadata[node]!; |
| if (root.isGetter) { |
| selectorRoots.add(metadata.getterSelectorId); |
| } else if (root.isSetter) { |
| selectorRoots.add(metadata.methodOrSetterSelectorId); |
| } else { |
| selectorRoots.add(metadata.methodOrSetterSelectorId); |
| selectorRoots.add(metadata.getterSelectorId); |
| } |
| } |
| } |
| |
| final classHierarchy = |
| ClassHierarchy(component, coreTypes) as ClosedWorldClassHierarchy; |
| final devirtualizionOracle = DevirtualizionOracle( |
| directCallMetadata, |
| procedureAttributeMetadata, |
| selectorMetadata, |
| ); |
| final depsCollector = DependenciesCollector( |
| procedureAttributeMetadata, |
| coreTypes, |
| classHierarchy, |
| devirtualizionOracle, |
| loadingMap, |
| assertsEnabled, |
| ); |
| final algorithm = _Algorithm( |
| component, |
| loadingMap.rootImport, |
| depsCollector, |
| constraints, |
| ); |
| return algorithm.run(roots, selectorRoots); |
| } |
| |
| class Partitioning { |
| final Part root; |
| final List<Part> parts; |
| final Map<Reference, Part> referenceToPart; |
| final Map<Constant, Part> constantToPart; |
| final Map<LibraryDependency, Set<Part>> deferredImportToParts; |
| // Maps each deferred import to a dedicated [Part] which is only used for that |
| // import. |
| // |
| // NOTE: Only non-leaves in the dominator tree are guaranteed to have one. |
| final Map<LibraryDependency, Part> deferredImportPart; |
| final Dominators dominators; |
| |
| Partitioning( |
| this.root, |
| this.parts, |
| this.referenceToPart, |
| this.constantToPart, |
| this.deferredImportToParts, |
| this.deferredImportPart, |
| this.dominators, |
| ); |
| |
| String toText(Uri baseUri, {bool includeRoot = false}) { |
| final output = StringBuffer(); |
| int partId = 0; |
| final partContents = computePartContents(); |
| |
| final sortedParts = |
| parts |
| .toList() |
| .where( |
| (p) => |
| (!p.isRoot || includeRoot) && |
| (partContents[p]!.references.isNotEmpty || |
| partContents[p]!.constants.isNotEmpty), |
| ) |
| .toList() |
| ..sort((a, b) { |
| final contentsA = partContents[a]!; |
| final contentsB = partContents[b]!; |
| return (contentsB.references.length + contentsA.constants.length) - |
| (contentsA.references.length + contentsB.constants.length); |
| }); |
| |
| for (int i = 0; i < sortedParts.length; ++i) { |
| final part = sortedParts[i]; |
| final isLast = i == (sortedParts.length - 1); |
| final contents = partContents[part]!; |
| |
| final sortedImports = |
| part.imports |
| .map((dep) => _stringifyDeferredImport(baseUri, dep)) |
| .toList() |
| ..sort(); |
| final sortedRefs = |
| contents.references |
| .map((ref) => _stringifyReference(baseUri, ref)) |
| .toList() |
| ..sort(); |
| final sortedConsts = contents.constants.map(_stringifyConstant).toList() |
| ..sort(); |
| |
| output.writeln('Part ${partId++}'); |
| output.writeln(' ImportSet'); |
| for (final i in sortedImports) { |
| output.writeln(' - $i'); |
| } |
| output.writeln(' References'); |
| for (final ref in sortedRefs) { |
| output.writeln(' - $ref'); |
| } |
| output.writeln(' Constants'); |
| for (final ref in sortedConsts) { |
| output.writeln(' - $ref'); |
| } |
| if (!isLast) output.writeln(''); |
| } |
| return '$output'; |
| } |
| |
| Map<Part, ({Set<Reference> references, Set<Constant> constants})> |
| computePartContents() { |
| final partRefs = <Part, Set<Reference>>{}; |
| final partConstants = <Part, Set<Constant>>{}; |
| referenceToPart.forEach((reference, part) { |
| (partRefs[part] ??= {}).add(reference); |
| }); |
| constantToPart.forEach((reference, part) { |
| (partConstants[part] ??= {}).add(reference); |
| }); |
| return { |
| for (final part in parts) |
| part: ( |
| references: partRefs[part] ?? {}, |
| constants: partConstants[part] ?? {}, |
| ), |
| }; |
| } |
| |
| static String _stringifyDeferredImport( |
| Uri baseUri, |
| LibraryDependency dependency, |
| ) => '${(dependency.parent as Library).importUri} prefix: ${dependency.name!}' |
| .replaceAll('$baseUri', ''); |
| |
| static String _stringifyReference(Uri baseUri, Reference reference) => |
| reference.canonicalName!.toStringInternal().replaceAll('$baseUri', ''); |
| |
| static String _stringifyConstant(Constant reference) => reference.toString(); |
| } |
| |
| class _Algorithm { |
| final Component component; |
| final LibraryDependency rootImport; |
| final DependenciesCollector depsCollector; |
| final ConstraintData? userConstraints; |
| |
| final ImportSetLattice importSets = ImportSetLattice(); |
| |
| // The work queues for propagating import set additions. |
| |
| late final referenceQueue = _WorkQueue<Reference>(importSets); |
| late final constantQueue = _WorkQueue<Constant>(importSets); |
| |
| // Caches of direct dependencies of [Reference]s/[Constants]s. |
| final Map<Reference, DirectReferenceDependencies> |
| directReferenceDependencies = {}; |
| final Map<Constant, DirectConstantDependencies> directConstantDependencies = |
| {}; |
| |
| // The [ImportSet] the given [Reference]/[Constant]s are needed for. |
| final Map<Reference, ImportSet> referenceToImportSet = {}; |
| final Map<Constant, ImportSet> constantToImportSet = {}; |
| |
| _Algorithm( |
| this.component, |
| this.rootImport, |
| this.depsCollector, |
| this.userConstraints, |
| ); |
| |
| Partitioning run(Set<Reference> roots, Set<int> selectorRoots) { |
| collectDependencies(roots); |
| |
| // Sentinel used to represent the artificial import of all roots. |
| deferSelectors(rootImport, roots, selectorRoots); |
| |
| final dominators = deferSelectors(rootImport, roots, selectorRoots); |
| |
| final allDeferredImportsIncludingRoot = dominators.allNodes |
| .map((n) => n.prefix) |
| .toSet(); |
| final rootPart = Part(true, allDeferredImportsIncludingRoot); |
| importSets.buildRootSet(rootImport, rootPart); |
| |
| final transitions = computeConstraints( |
| rootImport, |
| dominators, |
| allDeferredImportsIncludingRoot, |
| ); |
| importSets.buildInitialSets(transitions.singletonTransitions); |
| importSets.buildSetTransitions(transitions.setTransitions); |
| |
| enqueueRootsAndPropagate(roots); |
| applySetTransitions(); |
| |
| return createParitition(rootPart, rootImport, dominators); |
| } |
| |
| psc.ProgramSplitConstraints<LibraryDependency> computeConstraints( |
| LibraryDependency root, |
| Dominators dominators, |
| Set<LibraryDependency> allDeferredImportsIncludingRoot, |
| ) { |
| final namedNodes = ProgramSplitBuilder(); |
| final orderNodes = <OrderNode>[]; |
| |
| final filteredUserConstraints = _filterUserConstraints( |
| userConstraints, |
| allDeferredImportsIncludingRoot, |
| ); |
| |
| // If user provided constraints, initialize from them. |
| final existingNames = <String, NamedNode>{}; |
| if (filteredUserConstraints != null) { |
| for (final named in filteredUserConstraints.named) { |
| if (named is ReferenceNode) { |
| final import = UriAndPrefix(named.uri, named.prefix).toString(); |
| existingNames[import] = named; |
| } |
| namedNodes.namedNodes[named.name] = named; |
| } |
| for (final ordered in filteredUserConstraints.ordered) { |
| orderNodes.add(ordered); |
| } |
| } |
| |
| // Ensure we have named nodes for all deferred imports. |
| for (final deferredImport in allDeferredImportsIncludingRoot) { |
| final name = deferredImport.uriPrefix; |
| if (!existingNames.containsKey(name)) { |
| namedNodes.referenceNode(name); |
| } |
| } |
| |
| // Then add ordering constraints based on dominator tree. |
| dominators.allNodes.forEach((node) { |
| final dominator = node.dominator?.prefix; |
| if (dominator != null) { |
| orderNodes.add( |
| namedNodes.orderNode(dominator.uriPrefix, node.prefix.uriPrefix), |
| ); |
| } |
| }); |
| |
| // Now we can build the transitions. |
| final allConstraints = ConstraintData( |
| namedNodes.namedNodes.values.toList(), |
| orderNodes, |
| ); |
| return psc.KernelBuilder( |
| allConstraints, |
| ).build(allDeferredImportsIncludingRoot); |
| } |
| |
| ConstraintData? _filterUserConstraints( |
| ConstraintData? userConstraints, |
| Set<LibraryDependency> allDeferredImportsIncludingRoot, |
| ) { |
| if (userConstraints == null) return null; |
| |
| /// The set of all prefixes (even unreachable ones). |
| final allPrefixNames = <String>{ |
| for (final library in component.libraries) |
| for (final import in library.dependencies) |
| if (import.isDeferred) import.uriPrefix, |
| }; |
| |
| /// The set of reachable prefixes. |
| final validImportPrefixes = allDeferredImportsIncludingRoot |
| .map((d) => d.uriPrefix) |
| .toSet(); |
| |
| final newNamedNodes = <NamedNode, NamedNode>{}; |
| |
| // Find [ReferenceNode]s that exist. |
| for (final NamedNode node in userConstraints.named) { |
| if (node is ReferenceNode) { |
| final importPrefix = UriAndPrefix(node.uri, node.prefix).toString(); |
| if (validImportPrefixes.contains(importPrefix)) { |
| newNamedNodes[node] = node; |
| continue; |
| } |
| if (!allPrefixNames.contains(importPrefix)) { |
| throw StateError('The library $importPrefix is not known.'); |
| } |
| // The [importPrefix] is in the Kernel AST but unreachable. This can |
| // happen due to RTA+TFA leaving unreachable code behind. We therefore |
| // prune this node and simplify or remove depending constraint nodes |
| // below. |
| } |
| } |
| |
| // Prune or remove [CombinerNode]s. |
| for (final NamedNode node in userConstraints.named) { |
| if (node is CombinerNode) { |
| final Set<ReferenceNode> remaining = node.nodes |
| .where(newNamedNodes.containsKey) |
| .toSet(); |
| if (remaining.isEmpty) continue; |
| if (node.type == CombinerType.and && |
| remaining.length < node.nodes.length) { |
| continue; |
| } |
| if (remaining.length == 1) { |
| newNamedNodes[node] = remaining.first; |
| } else { |
| newNamedNodes[node] = CombinerNode(node.name, node.type, remaining); |
| } |
| } |
| } |
| |
| // Filter/prune [OrderNode]s. |
| final newOrderNodes = <OrderNode>[]; |
| for (final node in userConstraints.ordered) { |
| if (node is RelativeOrderNode) { |
| final predecessor = newNamedNodes[node.predecessor]; |
| final successor = newNamedNodes[node.successor]; |
| if (predecessor != null && successor != null) { |
| newOrderNodes.add( |
| RelativeOrderNode(predecessor: predecessor, successor: successor), |
| ); |
| } |
| continue; |
| } |
| if (node is FuseNode) { |
| final newNodes = <NamedNode>{}; |
| for (final node in node.nodes) { |
| final newNode = newNamedNodes[node]; |
| if (newNode == null) continue; |
| newNodes.add(newNode); |
| } |
| if (newNodes.length >= 2) { |
| newOrderNodes.add(FuseNode(newNodes)); |
| } |
| continue; |
| } |
| throw StateError('Unknown order node $node.'); |
| } |
| |
| return ConstraintData(newNamedNodes.values.toList(), newOrderNodes); |
| } |
| |
| void collectDependencies(Set<Reference> roots) { |
| for (final reference in roots) { |
| ensureReferenceDependencies(reference); |
| } |
| } |
| |
| Dominators deferSelectors( |
| LibraryDependency rootImport, |
| Set<Reference> roots, |
| Set<int> selectorRoots, |
| ) { |
| final dominators = computeDominators( |
| rootImport, |
| roots, |
| directReferenceDependencies, |
| directConstantDependencies, |
| ); |
| |
| final prefixRoots = computePrefixRoots( |
| rootImport, |
| roots, |
| selectorRoots, |
| directReferenceDependencies, |
| directConstantDependencies, |
| ); |
| final prefixDominatorUsages = computeTransitiveDominatorUsages( |
| dominators, |
| prefixRoots, |
| directReferenceDependencies, |
| directConstantDependencies, |
| ); |
| |
| final classDominators = computeClassDominators( |
| dominators, |
| prefixDominatorUsages, |
| ); |
| |
| final selectorDominators = computeSelectorDominators( |
| dominators, |
| prefixDominatorUsages, |
| ); |
| |
| // Defer instance methods. |
| dominators.root.visitDFS((dominatorNode) { |
| // The transitive usages via this prefix, minus the usages of the parent |
| // dominators. |
| final usages = prefixDominatorUsages.usages[dominatorNode.prefix]!; |
| |
| // Scan for all classes that we depend on & dominate, then move |
| // appliable methods down the tree. |
| for (final reference in usages.references) { |
| if (reference.node is! Class) continue; |
| |
| // We only consider moving methods down the tree if the class dominator |
| // actually uses the class. That means we are guaranteed to defer the |
| // methods of the class. |
| // |
| // If a class is not used by it's dominator this guarantee wouldn't be |
| // there. Imagine: |
| // |
| // Root |
| // / | \ |
| // D1 D2 D3 |
| // |
| // Further imagine D1 & D2 allocate `Foo` and D3 invokes selector |
| // `foo` provided by `Foo`. |
| // |
| // Here the `Root` is the class dominator of `Foo`. If we removed the |
| // `Foo -> Foo.foo` reference and pushed it down the tree we would |
| // make loading of `D3` also load `Foo.foo`. While this would be |
| // semantically correct, we would end up loading `Foo.foo` when `D3` |
| // is loaded, which may not need it at that moment yet. So we'd load |
| // more code than needed. |
| final classDominator = classDominators.classDominators[reference]!; |
| if (classDominator != dominatorNode) continue; |
| |
| final deps = directReferenceDependencies[reference]!; |
| final (deletions, moves) = _collectMethodsToMove( |
| selectorDominators, |
| usages, |
| dominatorNode, |
| deps, |
| ); |
| |
| // Remove all unused methods. |
| deps.references.removeAll(deletions); |
| |
| // Execute moves. |
| for (final (reference, selectorDominator, selectorId, selectorName) |
| in moves) { |
| deps.references.remove(reference); |
| final deferredUses = deps.deferredReferences[reference] ??= {}; |
| final before = deferredUses.length; |
| addDeferredMethodDependencyRecursive( |
| prefixDominatorUsages, |
| selectorDominator, |
| selectorId, |
| selectorName, |
| deferredUses, |
| ); |
| final after = deferredUses.length; |
| assert((after - before) > 0); |
| } |
| } |
| }); |
| |
| return dominators; |
| } |
| |
| ( |
| List<Reference>, |
| List<(Reference, DominatorNode<LibraryDependency>, int, Name)>, |
| ) |
| _collectMethodsToMove( |
| SelectorDominators selectorDominators, |
| PrefixUsages classDominatorUsages, |
| DominatorNode<LibraryDependency> classDominator, |
| DirectReferenceDependencies deps, |
| ) { |
| final deletions = <Reference>[]; |
| final moves = <(Reference, DominatorNode<LibraryDependency>, int, Name)>[]; |
| |
| for (final reference in deps.references) { |
| // Skip dependency on super class. |
| if (reference.node is Class) continue; |
| |
| final (selectorId, selectorName) = _getSelectorIdAndName(reference); |
| final selectorCallDominator = selectorDominators.selectorIds[selectorId]; |
| final dynamicCallDominator = |
| selectorDominators.selectorNames[selectorName]; |
| |
| if (selectorCallDominator == null && dynamicCallDominator == null) { |
| // There are no dynamic or interface based calls to the selector, |
| // which means even though the class is used (via constructor |
| // or constant), the method does not have to be enqueued |
| // automatically. All call sites (if any, **) are devirtualized and |
| // will have the [reference] in their [DirectReferenceDependencies]. |
| // |
| // (**) There may actually be no call sites at all: RTA+TFA can |
| // leave dead code behind. TFA may think that the selector is used - |
| // but the only usage site may be in dead code. |
| deletions.add(reference); |
| continue; |
| } |
| |
| // If the node that allocates the class also has calls to the selector we |
| // cannot move it. |
| if (classDominatorUsages.selectorIds.contains(selectorId) || |
| classDominatorUsages.selectorNames.contains(selectorName)) { |
| continue; |
| } |
| |
| final destination = selectorCallDominator == null |
| ? dynamicCallDominator! |
| : (dynamicCallDominator == null |
| ? selectorCallDominator |
| : selectorCallDominator.commonDominator(dynamicCallDominator)); |
| if (classDominator.dominates(destination)) { |
| // The class is defined but the selector is only used in deferred units. |
| // Let's defer loading the method to deferred units. |
| moves.add((reference, destination, selectorId, selectorName)); |
| continue; |
| } |
| } |
| return (deletions, moves); |
| } |
| |
| (int, Name) _getSelectorIdAndName(Reference reference) { |
| final member = reference.node as Member; |
| assert(member.isInstanceMember); |
| final metadata = depsCollector.procedureAttributeMetadata[member]!; |
| |
| assert(member.isInstanceMember); |
| if (member is Field) { |
| if (reference == member.getterReference) { |
| return (metadata.getterSelectorId, member.name); |
| } |
| assert(reference == member.setterReference); |
| return (metadata.methodOrSetterSelectorId, member.name); |
| } |
| member as Procedure; |
| assert(reference == member.reference); |
| return ( |
| ((member.kind == ProcedureKind.Getter) |
| ? metadata.getterSelectorId |
| : metadata.methodOrSetterSelectorId), |
| member.name, |
| ); |
| } |
| |
| void addDeferredMethodDependencyRecursive( |
| ProgramPrefixUsages prefixDominatorUsages, |
| DominatorNode<LibraryDependency> destination, |
| int selectorId, |
| Name selectorName, |
| Set<LibraryDependency> deferredUses, |
| ) { |
| // If [destination] has calls to the selector, that's where we stop. |
| final destinationUsages = prefixDominatorUsages.usages[destination.prefix]!; |
| if (destinationUsages.selectorIds.contains(selectorId) || |
| destinationUsages.selectorNames.contains(selectorName)) { |
| assert(destination.prefix != rootImport); |
| deferredUses.add(destination.prefix); |
| return; |
| } |
| |
| // Otherwise we defer to the [destination]s children. |
| for (final child in destination.children) { |
| addDeferredMethodDependencyRecursive( |
| prefixDominatorUsages, |
| child, |
| selectorId, |
| selectorName, |
| deferredUses, |
| ); |
| } |
| } |
| |
| void enqueueRootsAndPropagate(Set<Reference> roots) { |
| for (final root in roots) { |
| referenceQueue.enqueue(root, importSets.rootSet); |
| } |
| directReferenceDependencies.forEach((reference, deps) { |
| deps.deferredReferences.forEach((reference, imports) { |
| for (final import in imports) { |
| referenceQueue.enqueue(reference, importSets.initialSetOf(import)); |
| } |
| }); |
| deps.deferredConstants.forEach((constant, imports) { |
| for (final import in imports) { |
| constantQueue.enqueue(constant, importSets.initialSetOf(import)); |
| } |
| }); |
| }); |
| |
| while (referenceQueue.isNotEmpty || constantQueue.isNotEmpty) { |
| while (referenceQueue.isNotEmpty) { |
| final (reference, importsToAdd) = referenceQueue.dequeue(); |
| final oldSet = referenceToImportSet[reference] ?? importSets.emptySet; |
| final newSet = importSets.union(oldSet, importsToAdd); |
| updateReference(reference, oldSet, newSet); |
| } |
| while (constantQueue.isNotEmpty) { |
| final (constant, importsToAdd) = constantQueue.dequeue(); |
| final oldSet = constantToImportSet[constant] ?? importSets.emptySet; |
| final newSet = importSets.union(oldSet, importsToAdd); |
| updateConstant(constant, oldSet, newSet); |
| } |
| } |
| } |
| |
| /// Creates a [Partitioning] that maps [Reference]s/[Constant]s to the [Part] |
| /// they were assigned to. |
| Partitioning createParitition( |
| Part rootPart, |
| LibraryDependency rootImport, |
| Dominators dominators, |
| ) { |
| // Map [Reference]s/[Constant]s to the [Part] they were assigned to. |
| final referenceToPart = <Reference, Part>{}; |
| final constantToPart = <Constant, Part>{}; |
| final parts = <Part>[rootPart]; |
| referenceToImportSet.forEach((reference, importSet) { |
| Part? part = importSet.part; |
| if (part == null) { |
| part = Part(false, importSet.toSet()); |
| parts.add(importSet.part = part); |
| } |
| referenceToPart[reference] = part; |
| }); |
| constantToImportSet.forEach((constant, importSet) { |
| Part? part = importSet.part; |
| if (part == null) { |
| part = Part(false, importSet.toSet()); |
| parts.add(importSet.part = part); |
| } |
| constantToPart[constant] = part; |
| }); |
| |
| final deferredInputLoadingList = <LibraryDependency, Set<Part>>{}; |
| for (final part in parts) { |
| for (final deferredImport in part.imports) { |
| (deferredInputLoadingList[deferredImport] ??= {}).add(part); |
| } |
| } |
| |
| final deferredImportPart = <LibraryDependency, Part>{}; |
| dominators.root.visitDFS((node) { |
| if (node.children.isEmpty) return; |
| |
| final prefix = node.prefix; |
| final importSet = importSets.initialSets[prefix]!; |
| final part = importSet.part ??= Part(false, importSet.toSet()); |
| deferredImportPart[prefix] = part; |
| }); |
| |
| // Now we can prune the load lists: If a parent is guaranteed to have loaded |
| // a part, then there's no need to include that part in a child's load list. |
| final alreadyLoaded = <LibraryDependency, Set<Part>>{}; |
| dominators.root.visitDFS((node) { |
| final thisPrefix = node.prefix; |
| final thisLoadList = deferredInputLoadingList[thisPrefix] ?? {}; |
| final dominatorPrefix = node.dominator?.prefix; |
| final dominatorLoadList = alreadyLoaded[dominatorPrefix] ?? <Part>{}; |
| alreadyLoaded[thisPrefix] = {...dominatorLoadList, ...thisLoadList}; |
| thisLoadList.removeAll(dominatorLoadList); |
| }); |
| |
| deferredInputLoadingList.remove(rootImport); |
| |
| return Partitioning( |
| rootPart, |
| parts, |
| referenceToPart, |
| constantToPart, |
| deferredInputLoadingList, |
| deferredImportPart, |
| dominators, |
| ); |
| } |
| |
| /// Ensures we have all transitive direct dependencies of [reference] |
| /// cached and all transitive deferred dependencies of [reference] enqueued. |
| void ensureReferenceDependencies(Reference reference) { |
| if (directReferenceDependencies.containsKey(reference)) return; |
| |
| final deps = depsCollector.directReferenceDependencies(reference); |
| directReferenceDependencies[reference] = deps; |
| |
| deps.references.forEach(ensureReferenceDependencies); |
| deps.deferredReferences.forEach((reference, imports) { |
| ensureReferenceDependencies(reference); |
| }); |
| deps.constants.forEach(ensureConstantDependencies); |
| deps.deferredConstants.forEach((constant, imports) { |
| ensureConstantDependencies(constant); |
| }); |
| } |
| |
| /// Ensures we have all transitive dependencies of [constant] cached. |
| void ensureConstantDependencies(Constant constant) { |
| if (directConstantDependencies.containsKey(constant)) return; |
| |
| final deps = depsCollector.directConstantDependencies(constant); |
| directConstantDependencies[constant] = deps; |
| |
| final reference = deps.reference; |
| if (reference != null) { |
| ensureReferenceDependencies(reference); |
| } |
| deps.constants.forEach(ensureConstantDependencies); |
| } |
| |
| /// Processes each [ImportSet], applying [SetTransition]s if their |
| /// prerequisites are met. |
| void applySetTransitions() { |
| final imports = { |
| ...referenceToImportSet.values, |
| ...constantToImportSet.values, |
| }; |
| final finalTransitions = importSets.computeFinalTransitions(imports); |
| referenceToImportSet.updateAll( |
| (reference, importSet) => finalTransitions[importSet]!, |
| ); |
| constantToImportSet.updateAll( |
| (reference, importSet) => finalTransitions[importSet]!, |
| ); |
| } |
| |
| /// Given an [Reference], an [oldSet] and a [newSet], either ignore the |
| /// update, apply the update immediately if we can avoid unions, or apply the |
| /// update later if we cannot. For more detail on [oldSet] and [newSet], |
| /// please see the comment in [dart2js]. |
| /// |
| /// [dart2js] pkg/compiler/lib/src/deferred_load/deferred_load.dart |
| void updateReference( |
| Reference reference, |
| ImportSet oldSet, |
| ImportSet newSet, |
| ) { |
| final currentSet = referenceToImportSet[reference] ?? importSets.emptySet; |
| |
| // If [currentSet] == [newSet], then currentSet must include all of newSet. |
| if (currentSet == newSet) return; |
| |
| // Elements in the main output unit always remain there. |
| if (currentSet == importSets.rootSet) return; |
| |
| // If [currentSet] == [oldSet], then we can safely update the import set of |
| // the reference in a single assignment. |
| // Otherwise another union operation needs to be performed, which we do by |
| // enquing it into the queue. |
| if (currentSet == oldSet) { |
| // Continue recursively updating from [oldSet] to [newSet]. |
| referenceToImportSet[reference] = newSet; |
| _updateReferenceDependencies(reference, oldSet, newSet); |
| } else { |
| assert( |
| // Invariant: we must mark main before we mark any deferred import. |
| newSet != importSets.rootSet || oldSet != importSets.emptySet, |
| "Tried to assign to the main output unit, but it was assigned " |
| "to $currentSet.", |
| ); |
| // Recursively enqueue [reference]. |
| referenceQueue.enqueue(reference, newSet); |
| } |
| } |
| |
| void updateConstant(Constant constant, ImportSet oldSet, ImportSet newSet) { |
| final currentSet = constantToImportSet[constant] ?? importSets.emptySet; |
| |
| // If [currentSet] == [newSet], then currentSet must include all of newSet. |
| if (currentSet == newSet) return; |
| |
| // Elements in the main output unit always remain there. |
| if (currentSet == importSets.rootSet) return; |
| |
| // If [currentSet] == [oldSet], then we can safely update the import set of |
| // the constant in a single assignment. |
| // Otherwise another union operation needs to be performed, which we do by |
| // enquing it into the queue. |
| if (currentSet == oldSet) { |
| // Continue recursively updating from [oldSet] to [newSet]. |
| constantToImportSet[constant] = newSet; |
| _updateConstantDependencies(constant, oldSet, newSet); |
| } else { |
| assert( |
| // Invariant: we must mark main before we mark any deferred import. |
| newSet != importSets.rootSet || oldSet != importSets.emptySet, |
| "Tried to assign to the main output unit, but it was assigned " |
| "to $currentSet.", |
| ); |
| // Recursively enqueue [constant]. |
| constantQueue.enqueue(constant, newSet); |
| } |
| } |
| |
| /// Updates the dependencies of a given [Reference] from [oldSet] to |
| /// [newSet]. |
| void _updateReferenceDependencies( |
| Reference reference, |
| ImportSet oldSet, |
| ImportSet newSet, |
| ) { |
| final deps = directReferenceDependencies[reference]!; |
| for (final reference in deps.references) { |
| updateReference(reference, oldSet, newSet); |
| } |
| for (final constant in deps.constants) { |
| updateConstant(constant, oldSet, newSet); |
| } |
| } |
| |
| void _updateConstantDependencies( |
| Constant constant, |
| ImportSet oldSet, |
| ImportSet newSet, |
| ) { |
| final deps = directConstantDependencies[constant]!; |
| final reference = deps.reference; |
| if (reference != null) { |
| updateReference(reference, oldSet, newSet); |
| } |
| |
| for (final constant in deps.constants) { |
| updateConstant(constant, oldSet, newSet); |
| } |
| } |
| } |
| |
| /// Keeps track of a worklist of objects that need additional imports to be |
| /// added to them. |
| class _WorkQueue<T extends Object> { |
| final ImportSetLattice _importSets; |
| final Queue<T> _queue = Queue(); |
| final Map<T, ImportSet> _pendingWork = {}; |
| |
| _WorkQueue(this._importSets); |
| |
| bool get isNotEmpty => _queue.isNotEmpty; |
| |
| void enqueue(T key, ImportSet importsToAdd) { |
| final existingImportSet = _pendingWork[key]; |
| if (existingImportSet != null) { |
| _pendingWork[key] = _importSets.union(existingImportSet, importsToAdd); |
| return; |
| } |
| _pendingWork[key] = importsToAdd; |
| _queue.add(key); |
| } |
| |
| (T, ImportSet) dequeue() { |
| assert(isNotEmpty); |
| final object = _queue.removeFirst(); |
| final importSet = _pendingWork.remove(object)!; |
| return (object, importSet); |
| } |
| } |
| |
| extension on LibraryDependency { |
| String get uriPrefix => |
| UriAndPrefix((parent as Library).importUri, name!).toString(); |
| } |