java-topology/tests/support/ClosureAlgorithm.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

119 lines
4.6 KiB
Java

package support;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Reference implementations of InferenceGraph.Node.closure() — defective and fixed.
*
* DEFECT 0002b (Infer.java:1747): closure() recomputes the DFS reachability
* set on every call — no caching. Called K times from canInfluence() within
* the O(N²) loop in buildStuckGraph(). With the O(N) findNode scan (0002a),
* the combined total = O(N³).
*
* Fix: cache the closure result after the first computation.
*
* Exact node-visit counts when calling closure(N0) K times on a V-node chain:
* defective: K * V (each of K calls runs a full O(V) DFS)
* fixed: V + (K - 1) (first call = V visits; each cache hit = 1)
*
* Derivation:
* Chain N0→N1→...→N(V-1). closure(N0) = all V nodes.
* Defective: K independent DFS traversals each visiting V nodes = K*V.
* Fixed: 1st call DFS visits V nodes and stores result. Calls 2..K each
* find the cached set in one check: 1 visit each. Total = V + (K-1).
*
* Growth when K doubles (V fixed): defective ≈2x (linear in K), fixed ≈1x
* (nearly constant: (V+2K-1)/(V+K-1) → 1 as V grows or K≪V).
*/
public class ClosureAlgorithm {
public static class Node {
public final String label;
public final List<Node> deps = new ArrayList<>();
// used by fixed version
Set<Node> cachedClosure = null;
public Node(String label) { this.label = label; }
public void addDep(Node dep) { deps.add(dep); }
@Override public String toString() { return label; }
}
public static class Result {
/** The closure set returned on each of the K calls (all identical). */
public final Set<Node> closure;
/**
* Total node visits across all K calls.
* Defective: K * V. Fixed: V + (K-1).
*/
public final long nodeVisits;
Result(Set<Node> closure, long nodeVisits) {
this.closure = closure;
this.nodeVisits = nodeVisits;
}
}
// ─── DEFECTIVE: uncached DFS on every call ───────────────────────────────
// Mirrors Infer.java:1747 — computes closure fresh each time it is called.
public static Result closureDefective(Node start, int calls) {
long[] visits = {0};
Set<Node> last = null;
for (int i = 0; i < calls; i++) {
Set<Node> closure = new HashSet<>();
dfs(start, closure, visits);
last = closure;
}
return new Result(last, visits[0]);
}
private static void dfs(Node n, Set<Node> visited, long[] visits) {
if (visited.contains(n)) return;
visits[0]++;
visited.add(n);
for (Node dep : n.deps) dfs(dep, visited, visits);
}
// ─── FIXED: cached DFS ───────────────────────────────────────────────────
// First call runs the DFS and stores the result on the node.
// Subsequent calls find the cached result in one check.
public static Result closureFixed(Node start, int calls) {
long[] visits = {0};
Set<Node> last = null;
for (int i = 0; i < calls; i++) {
last = closureCached(start, visits);
}
return new Result(last, visits[0]);
}
private static Set<Node> closureCached(Node n, long[] visits) {
if (n.cachedClosure != null) {
visits[0]++; // one operation: read the cached field
return n.cachedClosure;
}
Set<Node> closure = new HashSet<>();
dfs(n, closure, visits); // counts V visits
n.cachedClosure = closure;
return closure;
}
// ─── Factory ─────────────────────────────────────────────────────────────
/**
* Linear chain of V nodes: N0→N1→...→N(V-1).
* closure(N0) = all V nodes.
* Returns N0 (the start node for closure queries).
*
* Creates fresh nodes — call this separately for defective vs fixed tests
* so the fixed cache does not leak between test runs.
*/
public static Node buildLinearChain(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.get(0);
}
}