implements PR-19.2 dependency graph analiser extraction
This commit is contained in:
parent
82d2bfafb8
commit
b190227b53
@ -6,12 +6,9 @@ import p.studio.compiler.models.DependencyContext;
|
||||
import p.studio.compiler.models.ProjectInfoId;
|
||||
import p.studio.compiler.source.identifiers.ProjectId;
|
||||
import p.studio.compiler.workspaces.DependencyPhase;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
import p.studio.utilities.structures.DependencyGraphAnaliser;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
|
||||
import static java.util.stream.Collectors.joining;
|
||||
|
||||
@ -21,62 +18,11 @@ public final class StackPhase implements DependencyPhase {
|
||||
*/
|
||||
@Override
|
||||
public BuildingIssueSink run(final DependencyContext ctx) {
|
||||
final int n = ctx.projectTable.size();
|
||||
final int[] indeg = new int[n];
|
||||
for (int from = 0; from < n; from++) {
|
||||
for (final ProjectId to : ctx.dependenciesByProject.get(from)) {
|
||||
indeg[to.getIndex()]++;
|
||||
}
|
||||
}
|
||||
final var analysis = new DependencyGraphAnaliser<ProjectId>(java.util.Comparator.comparingInt(ProjectId::getIndex))
|
||||
.analyse(buildDependencyGraph(ctx));
|
||||
|
||||
final Deque<Integer> q = new ArrayDeque<>();
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (indeg[i] == 0) q.add(i);
|
||||
}
|
||||
|
||||
final List<ProjectId> travesalOrder = new ArrayList<>(n);
|
||||
// Performs topological sort using Kahn's algorithm
|
||||
while (!q.isEmpty()) {
|
||||
final int u = q.removeFirst();
|
||||
//travesalOrder.add(ctx.projectDescriptors.get(u).getProjectId());
|
||||
travesalOrder.add(new ProjectId(u));
|
||||
for (final ProjectId v : ctx.dependenciesByProject.get(u)) {
|
||||
if (--indeg[v.getId()] == 0) {
|
||||
q.addLast(v.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (travesalOrder.size() != n) {
|
||||
boolean[] stuck = new boolean[n];
|
||||
for (int i = 0; i < n; i++) stuck[i] = indeg[i] > 0;
|
||||
|
||||
var tarjan = new TarjanScc(n, ctx.dependenciesByProject);
|
||||
final List<List<Integer>> sccs = tarjan.run(stuck);
|
||||
|
||||
final List<List<ProjectId>> cycles = new ArrayList<>();
|
||||
for (var scc : sccs) {
|
||||
if (scc.size() > 1) {
|
||||
final var cycle = scc.stream().map(ProjectId::new).toList();
|
||||
cycles.add(cycle);
|
||||
} else {
|
||||
// size==1: cycle only if self-loop exists
|
||||
final var u = scc.getFirst();
|
||||
final var projectId = new ProjectId(u);
|
||||
boolean selfLoop = false;
|
||||
for (final var pu : ctx.dependenciesByProject.get(u)) {
|
||||
if (pu.getIndex() == u) {
|
||||
selfLoop = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (selfLoop) {
|
||||
cycles.add(List.of(projectId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final var msg = "[DEPS]: cycle(s) detected:\n" + cycles.stream()
|
||||
if (!analysis.acyclic()) {
|
||||
final var msg = "[DEPS]: cycle(s) detected:\n" + analysis.cycleComponents().stream()
|
||||
.map(projectIds -> " * " + projectIds.stream()
|
||||
.map(pId -> ctx.projectInfoTable.get(new ProjectInfoId(pId.getIndex())).manifest.name())
|
||||
.collect(joining(" -> "))
|
||||
@ -87,65 +33,16 @@ public final class StackPhase implements DependencyPhase {
|
||||
.report(builder -> builder.error(true).message(msg));
|
||||
}
|
||||
|
||||
ctx.stack = new BuildStack(ReadOnlyList.wrap(travesalOrder));
|
||||
ctx.stack = new BuildStack(analysis.traversalOrder());
|
||||
|
||||
return BuildingIssueSink.empty();
|
||||
}
|
||||
|
||||
static final class TarjanScc {
|
||||
private final int n;
|
||||
private final List<List<ProjectId>> adj;
|
||||
|
||||
private int time = 0;
|
||||
private final int[] disc;
|
||||
private final int[] low;
|
||||
private final boolean[] inStack;
|
||||
private final Deque<Integer> stack = new ArrayDeque<>();
|
||||
|
||||
TarjanScc(int n, List<List<ProjectId>> adj) {
|
||||
this.n = n;
|
||||
this.adj = adj;
|
||||
this.disc = new int[n];
|
||||
this.low = new int[n];
|
||||
this.inStack = new boolean[n];
|
||||
java.util.Arrays.fill(disc, -1);
|
||||
}
|
||||
|
||||
List<List<Integer>> run(boolean[] allowed) {
|
||||
List<List<Integer>> sccs = new ArrayList<>();
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (allowed[i] && disc[i] == -1) dfs(i, allowed, sccs);
|
||||
}
|
||||
return sccs;
|
||||
}
|
||||
|
||||
private void dfs(int u, boolean[] allowed, List<List<Integer>> out) {
|
||||
disc[u] = low[u] = time++;
|
||||
stack.push(u);
|
||||
inStack[u] = true;
|
||||
|
||||
for (ProjectId pid : adj.get(u)) {
|
||||
int v = pid.getIndex();
|
||||
if (!allowed[v]) continue;
|
||||
|
||||
if (disc[v] == -1) {
|
||||
dfs(v, allowed, out);
|
||||
low[u] = Math.min(low[u], low[v]);
|
||||
} else if (inStack[v]) {
|
||||
low[u] = Math.min(low[u], disc[v]);
|
||||
}
|
||||
}
|
||||
|
||||
if (low[u] == disc[u]) {
|
||||
List<Integer> scc = new ArrayList<>();
|
||||
while (true) {
|
||||
int w = stack.pop();
|
||||
inStack[w] = false;
|
||||
scc.add(w);
|
||||
if (w == u) break;
|
||||
}
|
||||
out.add(scc);
|
||||
}
|
||||
private LinkedHashMap<ProjectId, LinkedHashSet<ProjectId>> buildDependencyGraph(final DependencyContext ctx) {
|
||||
final var graph = new LinkedHashMap<ProjectId, LinkedHashSet<ProjectId>>();
|
||||
for (int index = 0; index < ctx.projectTable.size(); index++) {
|
||||
graph.put(new ProjectId(index), new LinkedHashSet<>(ctx.dependenciesByProject.get(index)));
|
||||
}
|
||||
return graph;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,72 @@
|
||||
package p.studio.compiler.workspaces.phases;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import p.studio.compiler.messages.DependencyConfig;
|
||||
import p.studio.compiler.models.DependencyContext;
|
||||
import p.studio.compiler.models.ProjectDescriptor;
|
||||
import p.studio.compiler.models.ProjectInfo;
|
||||
import p.studio.compiler.models.PrometeuManifest;
|
||||
import p.studio.compiler.source.identifiers.ProjectId;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class StackPhaseTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldBuildDeterministicTopologicalOrder() {
|
||||
final var ctx = DependencyContext.basedOn(new DependencyConfig(false, tempDir));
|
||||
registerProject(ctx, "app");
|
||||
registerProject(ctx, "core");
|
||||
registerProject(ctx, "util");
|
||||
|
||||
ctx.dependenciesByProject.add(List.of(new ProjectId(1), new ProjectId(2)));
|
||||
ctx.dependenciesByProject.add(List.of(new ProjectId(2)));
|
||||
ctx.dependenciesByProject.add(List.of());
|
||||
|
||||
final var issues = new StackPhase().run(ctx);
|
||||
|
||||
assertTrue(!issues.hasErrors());
|
||||
assertEquals(ReadOnlyList.wrap(List.of(new ProjectId(2), new ProjectId(1), new ProjectId(0))), ctx.stack.topologicalOrder);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReportCycles() {
|
||||
final var ctx = DependencyContext.basedOn(new DependencyConfig(false, tempDir));
|
||||
registerProject(ctx, "a");
|
||||
registerProject(ctx, "b");
|
||||
registerProject(ctx, "c");
|
||||
|
||||
ctx.dependenciesByProject.add(List.of(new ProjectId(1)));
|
||||
ctx.dependenciesByProject.add(List.of(new ProjectId(2)));
|
||||
ctx.dependenciesByProject.add(List.of(new ProjectId(0)));
|
||||
|
||||
final var issues = new StackPhase().run(ctx);
|
||||
|
||||
assertTrue(issues.hasErrors());
|
||||
assertTrue(issues.asCollection().stream().anyMatch(issue -> issue.getMessage().contains("a -> b -> c")));
|
||||
}
|
||||
|
||||
private void registerProject(final DependencyContext ctx, final String name) {
|
||||
final var root = tempDir.resolve(name);
|
||||
ctx.projectTable.register(ProjectDescriptor.builder()
|
||||
.rootPath(root)
|
||||
.name(name)
|
||||
.version("1.0.0")
|
||||
.sourceRoots(ReadOnlyList.wrap(List.of()))
|
||||
.build());
|
||||
ctx.projectInfoTable.register(ProjectInfo.builder()
|
||||
.rootDirectory(root)
|
||||
.manifestPath(root.resolve("prometeu.json"))
|
||||
.manifest(new PrometeuManifest(name, "1.0.0", "pbs", 1, ReadOnlyList.wrap(List.of())))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,154 @@
|
||||
package p.studio.utilities.structures;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.PriorityQueue;
|
||||
import java.util.Set;
|
||||
|
||||
public final class DependencyGraphAnaliser<N> {
|
||||
private final Comparator<? super N> nodeOrder;
|
||||
|
||||
public DependencyGraphAnaliser(final Comparator<? super N> nodeOrder) {
|
||||
this.nodeOrder = nodeOrder;
|
||||
}
|
||||
|
||||
public Analysis<N> analyse(final Map<N, ? extends Collection<N>> dependenciesByNode) {
|
||||
final var canonicalGraph = canonicalize(dependenciesByNode);
|
||||
final var indegree = new HashMap<N, Integer>();
|
||||
final var reverse = new HashMap<N, LinkedHashSet<N>>();
|
||||
for (final var node : canonicalGraph.keySet()) {
|
||||
indegree.put(node, 0);
|
||||
reverse.put(node, new LinkedHashSet<>());
|
||||
}
|
||||
|
||||
for (final var entry : canonicalGraph.entrySet()) {
|
||||
for (final var dependency : entry.getValue()) {
|
||||
indegree.put(entry.getKey(), indegree.get(entry.getKey()) + 1);
|
||||
reverse.get(dependency).add(entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
final var ready = new PriorityQueue<N>(nodeOrder);
|
||||
for (final var entry : indegree.entrySet()) {
|
||||
if (entry.getValue() == 0) {
|
||||
ready.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
final var traversalOrder = new ArrayList<N>(canonicalGraph.size());
|
||||
while (!ready.isEmpty()) {
|
||||
final var current = ready.remove();
|
||||
traversalOrder.add(current);
|
||||
for (final var dependent : reverse.getOrDefault(current, new LinkedHashSet<>())) {
|
||||
final var next = indegree.get(dependent) - 1;
|
||||
indegree.put(dependent, next);
|
||||
if (next == 0) {
|
||||
ready.add(dependent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final var stronglyConnectedComponents = tarjan(canonicalGraph);
|
||||
final var cycleComponents = new ArrayList<ReadOnlyList<N>>();
|
||||
for (final var scc : stronglyConnectedComponents) {
|
||||
if (scc.size() > 1) {
|
||||
cycleComponents.add(scc);
|
||||
continue;
|
||||
}
|
||||
final var node = scc.getFirst();
|
||||
if (canonicalGraph.getOrDefault(node, new LinkedHashSet<>()).contains(node)) {
|
||||
cycleComponents.add(scc);
|
||||
}
|
||||
}
|
||||
|
||||
return new Analysis<>(
|
||||
ReadOnlyList.wrap(traversalOrder),
|
||||
ReadOnlyList.wrap(stronglyConnectedComponents),
|
||||
ReadOnlyList.wrap(cycleComponents),
|
||||
traversalOrder.size() == canonicalGraph.size());
|
||||
}
|
||||
|
||||
private LinkedHashMap<N, LinkedHashSet<N>> canonicalize(final Map<N, ? extends Collection<N>> dependenciesByNode) {
|
||||
final var graph = new LinkedHashMap<N, LinkedHashSet<N>>();
|
||||
for (final var entry : dependenciesByNode.entrySet()) {
|
||||
graph.computeIfAbsent(entry.getKey(), ignored -> new LinkedHashSet<>());
|
||||
for (final var dependency : entry.getValue()) {
|
||||
graph.computeIfAbsent(dependency, ignored -> new LinkedHashSet<>());
|
||||
graph.get(entry.getKey()).add(dependency);
|
||||
}
|
||||
}
|
||||
return graph;
|
||||
}
|
||||
|
||||
private List<ReadOnlyList<N>> tarjan(final LinkedHashMap<N, LinkedHashSet<N>> graph) {
|
||||
final var indexByNode = new HashMap<N, Integer>();
|
||||
final var lowLinkByNode = new HashMap<N, Integer>();
|
||||
final var inStack = new HashSet<N>();
|
||||
final var stack = new ArrayDeque<N>();
|
||||
final var components = new ArrayList<ReadOnlyList<N>>();
|
||||
final var indexRef = new int[]{0};
|
||||
|
||||
for (final var node : graph.keySet()) {
|
||||
if (!indexByNode.containsKey(node)) {
|
||||
strongConnect(node, graph, indexByNode, lowLinkByNode, inStack, stack, components, indexRef);
|
||||
}
|
||||
}
|
||||
|
||||
return components;
|
||||
}
|
||||
|
||||
private void strongConnect(
|
||||
final N node,
|
||||
final Map<N, LinkedHashSet<N>> graph,
|
||||
final Map<N, Integer> indexByNode,
|
||||
final Map<N, Integer> lowLinkByNode,
|
||||
final Set<N> inStack,
|
||||
final Deque<N> stack,
|
||||
final List<ReadOnlyList<N>> components,
|
||||
final int[] indexRef) {
|
||||
indexByNode.put(node, indexRef[0]);
|
||||
lowLinkByNode.put(node, indexRef[0]);
|
||||
indexRef[0]++;
|
||||
stack.push(node);
|
||||
inStack.add(node);
|
||||
|
||||
for (final var dependency : graph.getOrDefault(node, new LinkedHashSet<>())) {
|
||||
if (!indexByNode.containsKey(dependency)) {
|
||||
strongConnect(dependency, graph, indexByNode, lowLinkByNode, inStack, stack, components, indexRef);
|
||||
lowLinkByNode.put(node, Math.min(lowLinkByNode.get(node), lowLinkByNode.get(dependency)));
|
||||
} else if (inStack.contains(dependency)) {
|
||||
lowLinkByNode.put(node, Math.min(lowLinkByNode.get(node), indexByNode.get(dependency)));
|
||||
}
|
||||
}
|
||||
|
||||
if (lowLinkByNode.get(node).equals(indexByNode.get(node))) {
|
||||
final var component = new ArrayList<N>();
|
||||
while (true) {
|
||||
final var current = stack.pop();
|
||||
inStack.remove(current);
|
||||
component.add(current);
|
||||
if (current.equals(node)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
component.sort(nodeOrder);
|
||||
components.add(ReadOnlyList.wrap(component));
|
||||
}
|
||||
}
|
||||
|
||||
public record Analysis<N>(
|
||||
ReadOnlyList<N> traversalOrder,
|
||||
ReadOnlyList<ReadOnlyList<N>> stronglyConnectedComponents,
|
||||
ReadOnlyList<ReadOnlyList<N>> cycleComponents,
|
||||
boolean acyclic) {
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
package p.studio.utilities.structures;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class DependencyGraphAnaliserTest {
|
||||
|
||||
@Test
|
||||
void shouldTopologicallyOrderSimpleDag() {
|
||||
final var graph = new LinkedHashMap<String, Set<String>>();
|
||||
graph.put("app", Set.of("core", "util"));
|
||||
graph.put("core", Set.of("util"));
|
||||
graph.put("util", Set.of());
|
||||
|
||||
final var analysis = new DependencyGraphAnaliser<>(String::compareTo).analyse(graph);
|
||||
|
||||
assertTrue(analysis.acyclic());
|
||||
assertEquals(ReadOnlyList.wrap(List.of("util", "core", "app")), analysis.traversalOrder());
|
||||
assertTrue(analysis.cycleComponents().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDetectCycle() {
|
||||
final var graph = new LinkedHashMap<String, Set<String>>();
|
||||
graph.put("a", Set.of("b"));
|
||||
graph.put("b", Set.of("c"));
|
||||
graph.put("c", Set.of("a"));
|
||||
|
||||
final var analysis = new DependencyGraphAnaliser<>(String::compareTo).analyse(graph);
|
||||
|
||||
assertFalse(analysis.acyclic());
|
||||
assertEquals(1, analysis.cycleComponents().size());
|
||||
assertEquals(ReadOnlyList.wrap(List.of("a", "b", "c")), analysis.cycleComponents().getFirst());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldProduceDeterministicOrderForDisconnectedGraph() {
|
||||
final var graph = new LinkedHashMap<String, Set<String>>();
|
||||
graph.put("zeta", Set.of());
|
||||
graph.put("alpha", Set.of());
|
||||
graph.put("beta", Set.of("alpha"));
|
||||
|
||||
final var analysis = new DependencyGraphAnaliser<>(String::compareTo).analyse(graph);
|
||||
|
||||
assertTrue(analysis.acyclic());
|
||||
assertEquals(ReadOnlyList.wrap(List.of("alpha", "beta", "zeta")), analysis.traversalOrder());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldTreatSelfLoopAsCycle() {
|
||||
final var graph = new LinkedHashMap<String, Set<String>>();
|
||||
graph.put("self", Set.of("self"));
|
||||
|
||||
final var analysis = new DependencyGraphAnaliser<>(String::compareTo).analyse(graph);
|
||||
|
||||
assertFalse(analysis.acyclic());
|
||||
assertEquals(1, analysis.cycleComponents().size());
|
||||
assertEquals(ReadOnlyList.wrap(List.of("self")), analysis.cycleComponents().getFirst());
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user