blob: dc4696778ecc1ce83fa0a8d544ad0001b4fd0595 [file]
// 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();
}