blob: 1bd334f6d5bea4dc05c5b6e5145eb3b78fd639fe [file] [log] [blame]
// Copyright (c) 2021, 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 'nodes.dart';
import '../../elements/entities.dart';
/// A [Constraint] is a node in a constraint graph which wraps an
/// [ImportEntity].
class Constraint {
/// The name of the [NamedNode] this [Constraint] was created to
/// represent.
final String name;
/// The [CombinerType] which should be used to combine [imports]. Either
/// [imports] will be a singleton, or [combinerType] will be non-null.
final CombinerType combinerType;
/// The [ImportEntity]s underlying this [Constraint].
final Set<ImportEntity> imports;
/// Imports which load after [import].
final Set<Constraint> successors = {};
/// Imports which load before [import].
final Set<Constraint> predecessors = {};
/// Whether or not this [Constraint] should always apply transitions as
/// opposed to conditionally applying transitions.
bool get alwaysApplyTransitions {
return combinerType == null || combinerType == CombinerType.and;
}
Constraint(this.name, this.imports, this.combinerType) {
assert((this.imports.length == 1 && combinerType == null) ||
(this.imports.length > 1 && combinerType != null));
}
@override
String toString() {
var predecessorNames =
predecessors.map((constraint) => constraint.name).join(', ');
var successorNames =
successors.map((constraint) => constraint.name).join(', ');
return 'Constraint(imports=$imports, predecessors={$predecessorNames}, '
'successors={$successorNames})';
}
}
/// [_WorkItem] is an private class used to compute the transitive closure of
/// transitions.
class _WorkItem {
/// The [Constraint] to process.
final Constraint child;
/// The set of [ImportEntity]s guaranteed to be loaded after [child]
/// transitively.
final Set<ImportEntity> transitiveChildren;
_WorkItem(this.child, {this.transitiveChildren = const {}});
}
/// [Builder] is converts parsed [Node] objects into transitions which
/// can be applied while splitting a program.
class Builder {
/// The [ConstraintData] object which result from parsing json constraints.
final ConstraintData nodes;
Builder(this.nodes);
/// Builds [ProgramSplitConstraints] which can be applied by an
/// [ImportSetLattice] when generating [ImportSet]s.
ProgramSplitConstraints build(Iterable<ImportEntity> imports) {
// 1) Create a map of uri#prefix to [ImportEntity].
Map<Uri, Map<String, ImportEntity>> importsByUriAndPrefix = {};
for (var import in imports) {
var libraryUri = import.enclosingLibraryUri;
var prefix = import.name;
var uriNodes = importsByUriAndPrefix[libraryUri] ??= {};
uriNodes[prefix] = import;
}
// A helper function for looking up an [ImportEntity] from a
// [ReferenceNode].
ImportEntity _lookupReference(ReferenceNode node) {
var uri = node.uri;
if (!importsByUriAndPrefix.containsKey(uri)) {
throw 'Uri for constraint not found $uri';
}
var prefix = node.prefix;
if (!importsByUriAndPrefix[uri].containsKey(prefix)) {
throw 'Prefix: $prefix not found for uri: $uri';
}
return importsByUriAndPrefix[uri][prefix];
}
// 2) Create a [Constraint] for each [NamedNode]. Also,
// index each [Constraint] by [NamedNode].
Map<NamedNode, Constraint> nodeToConstraintMap = {};
for (var constraint in nodes.named) {
CombinerType combinerType = null;
Set<ImportEntity> imports = {};
if (constraint is ReferenceNode) {
imports.add(_lookupReference(constraint));
} else if (constraint is CombinerNode) {
combinerType = constraint.type;
for (var child in constraint.nodes) {
imports.add(_lookupReference(child));
}
} else {
throw 'Unexpected Node Type $constraint';
}
nodeToConstraintMap[constraint] =
Constraint(constraint.name, imports, combinerType);
}
// 3) Build a graph of [Constraint]s by processing user constraints and
// intializing each [Constraint]'s predecessor / successor members.
void createEdge(NamedNode successorNode, NamedNode predecessorNode) {
var successor = nodeToConstraintMap[successorNode];
var predecessor = nodeToConstraintMap[predecessorNode];
successor.predecessors.add(predecessor);
predecessor.successors.add(successor);
}
for (var constraint in nodes.ordered) {
if (constraint is RelativeOrderNode) {
createEdge(constraint.successor, constraint.predecessor);
} else if (constraint is FuseNode) {
// Fuse nodes are just syntactic sugar for generating cycles in the
// ordering graph.
for (var node1 in constraint.nodes) {
for (var node2 in constraint.nodes) {
if (node1 != node2) {
createEdge(node1, node2);
}
}
}
}
}
// 4) Compute the transitive closure of constraints. This gives us a map of
// transitiveTransitions, where each key is a parent [ImportEntity] and each
// value represents the transitive set of child [ImportEntity]s which are
// always loaded after the parent.
Map<ImportEntity, Set<ImportEntity>> singletonTransitions = {};
Map<Constraint, SetTransition> setTransitions = {};
Map<Constraint, Set<ImportEntity>> processed = {};
Queue<_WorkItem> queue =
Queue.from(nodeToConstraintMap.values.map((node) => _WorkItem(node)));
while (queue.isNotEmpty) {
var item = queue.removeFirst();
var constraint = item.child;
var imports = constraint.imports;
// Update [transitiveTransitions] with reachable transitions for this
// [_WorkItem]
var transitiveChildren = item.transitiveChildren;
// We only add singletonTransitions for a given [ImportEntity] when it is
// guaranteed to dominate another [ImportEntity]. Some nodes such as 'or'
// nodes do not have this property.
if (constraint.alwaysApplyTransitions) {
for (var import in imports) {
// We insert an implicit 'self' transition for every import.
var transitions = singletonTransitions[import] ??= {import};
transitions.addAll(transitiveChildren);
}
} else {
assert(constraint.combinerType == CombinerType.or);
var setTransition =
setTransitions[constraint] ??= SetTransition(constraint.imports);
setTransition.transitions.addAll(transitiveChildren);
}
// Propagate constraints transitively to the parent.
var predecessorTransitiveChildren = {
...imports,
...transitiveChildren,
};
for (var predecessor in constraint.predecessors) {
// We allow cycles in the constraint graph, so we need to support
// reprocessing constraints when we need to consider new transitive
// children.
if (processed.containsKey(predecessor) &&
processed[predecessor].containsAll(predecessorTransitiveChildren)) {
continue;
}
(processed[predecessor] ??= {}).addAll(predecessorTransitiveChildren);
queue.add(_WorkItem(predecessor,
transitiveChildren: predecessorTransitiveChildren));
}
}
return ProgramSplitConstraints(
singletonTransitions, setTransitions.values.toList());
}
}
/// A [SetTransition] is a set of [ImportEntity] transitions which can only be
/// applied when all of the [ImportEntity]s in a given [source] are present in a
/// given [ImportSet].
class SetTransition {
/// The [Set<ImportEntity>] which, if present in a given [ImportSet] means
/// [transitions] should be applied.
final Set<ImportEntity> source;
/// The [Set<ImportEntity>] which is applied if [source] is present in a
/// given [ImportSet].
final Set<ImportEntity> transitions = {};
SetTransition(this.source);
}
/// [ProgramSplitConstraints] is a holder for transitions which should be
/// applied while splitting a program.
class ProgramSplitConstraints {
/// Transitions which apply when a singleton [ImportEntity] is present.
final Map<ImportEntity, Set<ImportEntity>> singletonTransitions;
/// Transitions which apply only when a set of [ImportEntity]s is present.
final List<SetTransition> setTransitions;
ProgramSplitConstraints(this.singletonTransitions, this.setTransitions);
}