java-topology/tests/support/TarjanAlgorithm.java
russell@unturf.com 0a580b313d undefect. CWE-407 — 63 sites patched across 27 ecosystems
Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com

Patches, unit tests, benchmarks, whitepaper, and outreach briefs.
Public domain — no copyright claimed. Use freely.
2026-03-26 17:11:57 -04:00

244 lines
9.1 KiB
Java

package support;
import java.util.ArrayList;
import java.util.List;
/**
* Self-contained Tarjan SCC implementations — defective and fixed.
*
* These are verbatim ports of GraphUtils.Tarjan (the private inner class)
* with one difference: the defective version uses list.contains(n) and the
* fixed version uses n.onStack. An operation counter is exposed so tests can
* assert exact comparison counts without relying on timing.
*
* This class has no dependency on jdk.compiler internals.
*
* KEY TOPOLOGY — starWithBackEdges(V):
* N0 → N1, N0 → N2, ..., N0 → N(v-1) (tree edges from N0)
* N_k → N0 for k=1..v-1 (back edges to already-visited N0)
*
* DFS visits N0 first, then each N_k in order. N0 is never popped until
* all children finish (lowlink=0 != index=k for each child). So when N_k
* fires its back edge to N0, the stack is [N_k, N_(k-1), ..., N1, N0]:
* - defective: listContains scans k+1 elements to find N0 at the bottom
* - fixed: n.onStack read in 1 operation
*
* Exact comparison counts:
* defective: 2+3+...+V = V*(V+1)/2 - 1
* fixed: 1*(V-1) = V-1
*
* Growth when V doubles: defective ≈4x (quadratic), fixed ≈2x (linear).
*/
public class TarjanAlgorithm {
// ─── Shared node base ───────────────────────────────────────────────────
public static class Node {
public final String label;
public final List<Node> deps = new ArrayList<>();
// Tarjan bookkeeping
int index = -1;
int lowlink = -1;
boolean onStack = false; // used by FIXED version
public Node(String label) { this.label = label; }
public void addDep(Node dep) { deps.add(dep); }
@Override public String toString() { return label; }
}
// ─── Result ─────────────────────────────────────────────────────────────
public static class Result {
public final List<List<Node>> sccs;
/**
* Number of element-level comparisons made during stack membership checks.
* Defective: each call to listContains() walks the list until it finds the target.
* Fixed: each call to n.onStack counts as exactly 1 comparison.
*/
public final long comparisons;
Result(List<List<Node>> sccs, long comparisons) {
this.sccs = sccs;
this.comparisons = comparisons;
}
}
// ─── DEFECTIVE implementation ───────────────────────────────────────────
// Mirrors GraphUtils.java:186 — uses list.contains(n) for on-stack check.
// We count each element-level comparison inside the linear scan.
public static Result tarjanDefective(List<Node> nodes) {
return new DefectiveTarjan().run(nodes);
}
private static class DefectiveTarjan {
int index = 0;
long comparisons = 0;
final List<Node> stack = new ArrayList<>();
final List<List<Node>> sccs = new ArrayList<>();
Result run(List<Node> nodes) {
for (Node n : nodes) {
if (n.index == -1) visit(n);
}
return new Result(sccs, comparisons);
}
void visit(Node v) {
v.index = index;
v.lowlink = index;
index++;
stack.add(0, v); // prepend — matches ListBuffer.prepend()
for (Node n : v.deps) {
if (n.index == -1) {
visit(n);
v.lowlink = Math.min(v.lowlink, n.lowlink);
} else {
// DEFECT (GraphUtils.java:186): linear scan through the stack
if (listContains(stack, n)) {
v.lowlink = Math.min(v.lowlink, n.index);
}
}
}
if (v.lowlink == v.index) {
List<Node> scc = new ArrayList<>();
Node n;
do {
n = stack.remove(0);
n.onStack = false;
scc.add(n);
} while (n != v);
sccs.add(scc);
}
}
/**
* Counts every element comparison made during the linear scan.
* This is what ListBuffer.contains() does internally.
*/
private boolean listContains(List<Node> list, Node target) {
for (Node n : list) {
comparisons++; // each element access = one comparison
if (n == target) return true;
}
return false;
}
}
// ─── FIXED implementation ────────────────────────────────────────────────
// Uses n.onStack for O(1) membership check — the correct approach.
// Exactly one comparison per non-tree edge: read n.onStack.
public static Result tarjanFixed(List<Node> nodes) {
return new FixedTarjan().run(nodes);
}
private static class FixedTarjan {
int index = 0;
long comparisons = 0;
final List<Node> stack = new ArrayList<>();
final List<List<Node>> sccs = new ArrayList<>();
Result run(List<Node> nodes) {
for (Node n : nodes) {
if (n.index == -1) visit(n);
}
return new Result(sccs, comparisons);
}
void visit(Node v) {
v.index = index;
v.lowlink = index;
index++;
stack.add(0, v);
v.onStack = true;
for (Node n : v.deps) {
if (n.index == -1) {
visit(n);
v.lowlink = Math.min(v.lowlink, n.lowlink);
} else {
comparisons++; // exactly one operation: read n.onStack
if (n.onStack) {
v.lowlink = Math.min(v.lowlink, n.index);
}
}
}
if (v.lowlink == v.index) {
List<Node> scc = new ArrayList<>();
Node n;
do {
n = stack.remove(0);
n.onStack = false;
scc.add(n);
} while (n != v);
sccs.add(scc);
}
}
}
// ─── Graph factory ───────────────────────────────────────────────────────
/**
* Star-with-back-edges graph on V nodes.
*
* Edges:
* Tree: N0→N1, N0→N2, ..., N0→N(v-1)
* Back: N_k→N0 for k=1..v-1
*
* This is ONE large SCC (all nodes reachable from N0 and back).
*
* Exact defective comparisons: V*(V+1)/2 - 1 (quadratic in V)
* Exact fixed comparisons: V-1 (linear in V)
*
* Why defective is quadratic: N0 is pushed first and stays on the stack
* until all children finish. When N_k fires its back edge N_k→N0, the
* stack is [N_k, N_(k-1), ..., N1, N0]. listContains must scan all k+1
* elements to find N0 at the bottom. Total: sum(k+1, k=1..V-1) = V*(V+1)/2 - 1.
*/
public static List<Node> starWithBackEdges(int v) {
List<Node> nodes = new ArrayList<>();
for (int i = 0; i < v; i++) nodes.add(new Node("N" + i));
// tree edges N0 → N1..N(v-1)
for (int k = 1; k < v; k++) nodes.get(0).addDep(nodes.get(k));
// back edges N_k → N0
for (int k = 1; k < v; k++) nodes.get(k).addDep(nodes.get(0));
return nodes;
}
/**
* Simple cycle (path with one back edge): N0→N1→…→N(v-1)→N0
*
* ONE SCC of size V. Only ONE non-tree edge fires (the back edge N(v-1)→N0).
*
* Defective comparisons: V (scan entire stack to find N0 at bottom)
* Fixed comparisons: 1 (read N0.onStack)
*
* Demonstrates O(V) vs O(1) per back-edge, not the full O(V²) pattern.
* Use starWithBackEdges() for the O(V²) proof.
*/
public static List<Node> pathWithBackEdge(int v) {
List<Node> nodes = new ArrayList<>();
for (int i = 0; i < v; i++) nodes.add(new Node("N" + i));
for (int i = 0; i < v - 1; i++) nodes.get(i).addDep(nodes.get(i + 1));
nodes.get(v - 1).addDep(nodes.get(0));
return nodes;
}
/**
* Linear path with no cycles: N0→N1→…→N(v-1)
*
* V singleton SCCs. No back edges → the defective `else` branch never fires.
* comparisons = 0 for both versions. Used for correctness checks only.
*/
public static List<Node> linearPath(int v) {
List<Node> nodes = new ArrayList<>();
for (int i = 0; i < v; i++) nodes.add(new Node("N" + i));
for (int i = 0; i < v - 1; i++) nodes.get(i).addDep(nodes.get(i + 1));
return nodes;
}
}