java-topology/docs/tickets/0001-tarjan-ov2-stack-contains.md
russell@unturf.com db29a08762 undefect. CWE-407 — 92 sites, 42 ecosystems
B&W print-friendly diagrams + tinkerpop-0001 + wave-3 proof sections.
Squash of 94 local commits onto remote master.
2026-03-26 19:48:18 -04:00

3.9 KiB
Raw Blame History

0001 — Tarjan SCC: O(V²) stack membership via stack.contains(n)

Status: open Severity: performance — high Component: jdk.compiler / com.sun.tools.javac.util.GraphUtils Affects: comp/Infer.java (type inference), comp/DeferredAttr.java (stuck expression resolution)


Root Cause

GraphUtils.java:186 — inner class Tarjan.findSCC():

// DEFECTIVE — O(n) per edge traversal
} else if (stack.contains(n)) {
    v.lowlink = Math.min(v.lowlink, n.index);
}

stack is a ListBuffer<N>. ListBuffer.contains() performs a full linear scan.

TarjanNode (line 132148) already carries an active field:

public abstract static class TarjanNode<D, N extends TarjanNode<D, N>> {
    boolean active;   // set true in visitNode(), false in addSCC()
    ...
}

active is set true when a node is pushed (line 202) and false when popped (line 210). It exists precisely to be the O(1) on-stack check. It is never read.

Fix — one line:

// FIXED — O(1)
} else if (n.active) {
    v.lowlink = Math.min(v.lowlink, n.index);
}

Complexity Analysis

Metric Defective Fixed
Stack membership check O(|stack|) per edge O(1) per edge
Overall Tarjan O(V²) worst case O(V+E)
Growth pattern Quadratic Linear

Worst case is a path graph: V₀→V₁→→…→Vₙ→V₀ (one back edge). Each forward step adds one node to the stack. When the back edge is reached, stack.contains() scans all V nodes. Total comparisons: V + (V-1) + … + 1 = V(V+1)/2.


Call Sites

comp/Infer.java:1908 — type inference graph solver

for (List<? extends Node> conSubGraph : GraphUtils.tarjan(nodes)) {

Called once per GraphSolver.solve() invocation — which fires for every method call site with unresolved inference variables. Complex generics (streams, collectors, builders) accumulate many inference variables per call site.

comp/DeferredAttr.java:675 — stuck expression resolver

List<? extends StuckNode> csn = GraphUtils.tarjan(stuckGraph).get(0);

Called in pickDeferredNode() during each stuck-expression resolution attempt. Firing frequency scales with lambda/method-reference density.


Diagrams

Source for all diagrams is in docs/tickets/diagrams/. Render with:

dot -Tsvg diagrams/0001-inference-graph.dot -o diagrams/0001-inference-graph.svg
dot -Tsvg diagrams/0001-tarjan-defect.dot   -o diagrams/0001-tarjan-defect.svg
dot -Tsvg diagrams/0001-stack-scan.dot      -o diagrams/0001-stack-scan.svg

Diagram 1 — Inference Variable Dependency Graph

Shows the graph structure that Tarjan processes during type inference. Cycles indicate mutually-dependent inference variables that must be merged into super-nodes.

See: diagrams/0001-inference-graph.dot

Diagram 2 — Tarjan Execution With Defect Highlighted

Shows the DFS traversal, stack growth, and the O(n) scan that fires when a back edge is encountered.

See: diagrams/0001-tarjan-defect.dot

Diagram 3 — Linear Scan vs O(1) Check

Side-by-side comparison of the defective and fixed stack membership check, showing the work done per edge traversal.

See: diagrams/0001-stack-scan.dot


Tests

Test File Purpose
Unit tests/unit/TarjanComplexityTest.java Proves O(V²) vs O(V+E) operation counts
Integration tests/integration/InferenceGraphScalingTest.java Proves scaling impact on type inference graph
Functional tests/functional/CompilerBenchmarkTest.java Proves end-to-end compilation latency impact

Run all: make -C tests


Fix Location

src/jdk.compiler/share/classes/com/sun/tools/javac/util/GraphUtils.java
line 186: stack.contains(n)  →  n.active

No other changes required. The active field is already correctly maintained by visitNode() and addSCC().