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

411 lines
16 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package support;
import java.util.*;
/**
* SwiplAlgorithm — models three CWE-407 sites in SWI-Prolog library code.
*
* ── swipl-0001 library/ugraphs.pl:509-510 (HIGH) ──────────────────────────
*
* DEFECT: In top_sort/5 (Kahn's topological sort), decr_zero_neighbors/7
* looks up each zero-in-degree vertex in the graph by linear scan:
*
* decr_zero_neighbors([Zero|Zeros], Graph, Vertices, ...) :-
* graph_memberchk(Zero-Neibs, Graph), % O(|V|) scan — DEFECT
* decr_list(Neibs, Vertices, ...),
* decr_zero_neighbors(Zeros, ...).
*
* graph_memberchk(Element1-Edges, [Element2-Edges2|_]) :-
* Element1 == Element2, !, Edges = Edges2.
* graph_memberchk(Element, [_|Rest]) :-
* graph_memberchk(Element, Rest).
*
* The graph is a sorted vertex-edges association list. For each vertex in Zeros
* (up to |V| vertices), graph_memberchk does an O(|V|) scan from the beginning.
* Total: O(|V|²). Correct Kahn's algorithm complexity: O(|V| + |E|).
*
* Fix: Pass a pre-built HashMap<Vertex, List<Vertex>> alongside Graph.
* O(1) lookup per zero-vertex. Total becomes O(|V| + |E|).
*
* ── swipl-0002 library/aggregate.pl:673 (MEDIUM) ──────────────────────────
*
* DEFECT: free_variables/4 builds a VarList accumulator and checks each new
* candidate variable with list_is_free_of/2:
*
* free_variables(Term, Bound, VarList, [Term|VarList]) :-
* var(Term),
* term_is_free_of(Bound, Term),
* list_is_free_of(VarList, Term), % O(n) scan — DEFECT
* !.
*
* list_is_free_of([Head|Tail], Var) :- Head \== Var, !, list_is_free_of(Tail, Var).
* list_is_free_of([], _).
*
* VarList grows by one entry per unique free variable. With N unique variables,
* list_is_free_of is called N times (once per candidate), each scanning O(n)
* entries already in VarList. Total: 0+1+2+...+(N-1) = N*(N-1)/2 = O(N²).
*
* Even the maintainer flagged it: @tbd "Exploit our built-in term_variables/2
* at some places?" — term_variables/2 is a C built-in that is O(N).
*
* Fix: Use a HashSet<Integer> (modeling variable identity) alongside VarList.
* Check HashSet.contains() — O(1) — before adding to VarList.
*
* ── swipl-0003 library/clp/clp_distinct.pl:173-174 (MEDIUM) ───────────────
*
* DEFECT: attr_unify_hook/2 fires during unification of CLP(distinct)-
* constrained variables. It checks:
*
* attr_unify_hook(dom_neq(Dom, Lefts, Rights), Y) :-
* ( ground(Y) -> ...
* ;
* \+ lists_contain(Lefts, Y), % O(K×N) — DEFECT
* \+ lists_contain(Rights, Y), % O(K×N) — DEFECT
*
* lists_contain([X|Xs], Y) :-
* ( list_contains(X, Y) -> true ; lists_contain(Xs, Y) ).
* list_contains([X|Xs], Y) :-
* ( X == Y -> true ; list_contains(Xs, Y) ).
*
* Lefts/Rights are lists of variable lists accumulated per constraint group.
* With K constraint groups each of average N variables, each unification hook
* call costs O(K×N). For constraints on many variables, this becomes significant.
*
* Fix: Replace list-of-lists with a flat HashSet<Integer> per constraint group,
* built once when the constraint is posted. Check HashSet.contains() — O(1).
*
* Comparison counts:
* 0001 defective: |V|² — O(|V|) scan per vertex in Zeros
* 0001 fixed: |V| — O(1) HashMap lookup per vertex
* 0002 defective: N*(N-1)/2 — triangular accumulator scan
* 0002 fixed: N — one O(1) set check per variable
* 0003 defective: K × N — nested list scan per unification
* 0003 fixed: 1 — one O(1) hash lookup per unification
*/
public class SwiplAlgorithm {
// ── swipl-0001: ugraphs.pl top_sort graph_memberchk ──────────────────────
public static final class TopoResult {
public final List<List<Integer>> layers; // topological layers (Kahn's)
public final long comparisons;
public TopoResult(List<List<Integer>> layers, long comparisons) {
this.layers = layers;
this.comparisons = comparisons;
}
}
/**
* Defective: graph_memberchk — O(|V|) scan per zero-vertex.
* Graph: sorted list of (vertex → neighbors) pairs modeled as array of lists.
*/
public static TopoResult defectiveTopSort(List<List<Integer>> adjList) {
int V = adjList.size();
int[] indegree = new int[V];
for (int u = 0; u < V; u++)
for (int w : adjList.get(u)) indegree[w]++;
long comparisons = 0;
List<Integer> zeros = new ArrayList<>();
for (int v = 0; v < V; v++) if (indegree[v] == 0) zeros.add(v);
List<List<Integer>> layers = new ArrayList<>();
while (!zeros.isEmpty()) {
layers.add(new ArrayList<>(zeros));
List<Integer> nextZeros = new ArrayList<>();
for (int zero : zeros) {
// graph_memberchk: scan sorted graph list from beginning — O(|V|)
List<Integer> neibs = null;
for (int v = 0; v < V; v++) { // O(|V|) scan
comparisons++;
if (v == zero) { neibs = adjList.get(v); break; }
}
if (neibs != null) {
for (int w : neibs) {
if (--indegree[w] == 0) nextZeros.add(w);
}
}
}
zeros = nextZeros;
}
return new TopoResult(layers, comparisons);
}
/**
* Fixed: HashMap<vertex, neighbors> — O(1) lookup per zero-vertex.
* Pre-build a HashMap alongside the graph, as the fix prescribes.
*/
public static TopoResult fixedTopSort(List<List<Integer>> adjList) {
int V = adjList.size();
int[] indegree = new int[V];
// Build HashMap once — O(|V|)
Map<Integer, List<Integer>> graphMap = new HashMap<>(V * 2);
for (int u = 0; u < V; u++) {
graphMap.put(u, adjList.get(u));
for (int w : adjList.get(u)) indegree[w]++;
}
long comparisons = 0;
List<Integer> zeros = new ArrayList<>();
for (int v = 0; v < V; v++) if (indegree[v] == 0) zeros.add(v);
List<List<Integer>> layers = new ArrayList<>();
while (!zeros.isEmpty()) {
layers.add(new ArrayList<>(zeros));
List<Integer> nextZeros = new ArrayList<>();
for (int zero : zeros) {
comparisons++; // O(1) HashMap lookup
List<Integer> neibs = graphMap.get(zero);
if (neibs != null) {
for (int w : neibs) {
if (--indegree[w] == 0) nextZeros.add(w);
}
}
}
zeros = nextZeros;
}
return new TopoResult(layers, comparisons);
}
// ── swipl-0002: aggregate.pl free_variables/4 list_is_free_of ────────────
public static final class FreeVarsResult {
public final List<Integer> varList; // accumulated unique free variables
public final long comparisons;
public FreeVarsResult(List<Integer> varList, long comparisons) {
this.varList = varList;
this.comparisons = comparisons;
}
}
/**
* Defective: list_is_free_of — O(n) scan per candidate variable.
* Models the accumulator-with-linear-check pattern.
*
* @param candidates variable IDs to process (some duplicates, models term walk)
* @param bound set of bound variables to exclude
*/
public static FreeVarsResult defectiveFreeVariables(
List<Integer> candidates, Set<Integer> bound) {
List<Integer> varList = new ArrayList<>();
long comparisons = 0;
for (int term : candidates) {
if (bound.contains(term)) continue; // term_is_free_of(Bound, Term)
// list_is_free_of: scan varList for identity
boolean free = true;
for (int existing : varList) {
comparisons++;
if (existing == term) { free = false; break; }
}
if (free) varList.add(term);
}
return new FreeVarsResult(varList, comparisons);
}
/**
* Fixed: HashSet — O(1) per candidate variable.
* Models term_variables/2 built-in or HashSet-backed accumulator.
*/
public static FreeVarsResult fixedFreeVariables(
List<Integer> candidates, Set<Integer> bound) {
List<Integer> varList = new ArrayList<>();
Set<Integer> seen = new HashSet<>();
long comparisons = 0;
for (int term : candidates) {
if (bound.contains(term)) continue;
comparisons++; // O(1) hash lookup
if (!seen.contains(term)) {
varList.add(term);
seen.add(term);
}
}
return new FreeVarsResult(varList, comparisons);
}
// ── swipl-0003: clp_distinct.pl lists_contain in attr_unify_hook ─────────
public static final class UnifyHookResult {
public final boolean compatible; // could the unification proceed?
public final long comparisons;
public UnifyHookResult(boolean compatible, long comparisons) {
this.compatible = compatible;
this.comparisons = comparisons;
}
}
/**
* Defective: lists_contain — O(K × N) nested list scan per unification.
*
* @param leftsGroups K constraint groups (each a list of variable IDs)
* @param candidate the variable Y being unified
*/
public static UnifyHookResult defectiveUnifyHook(
List<List<Integer>> leftsGroups, int candidate) {
long comparisons = 0;
// lists_contain: for each group, scan its list for candidate
for (List<Integer> group : leftsGroups) {
for (int v : group) {
comparisons++;
if (v == candidate) {
return new UnifyHookResult(false, comparisons);
}
}
}
return new UnifyHookResult(true, comparisons);
}
/**
* Fixed: pre-built flat HashSet — O(1) per unification hook call.
*
* @param flatSet pre-built set of all variables in all constraint groups
* @param candidate the variable Y being unified
*/
public static UnifyHookResult fixedUnifyHook(
Set<Integer> flatSet, int candidate) {
long comparisons = 1; // O(1) hash lookup
boolean compatible = !flatSet.contains(candidate);
return new UnifyHookResult(compatible, comparisons);
}
// ── Test data builders ────────────────────────────────────────────────────
/** Builds a random DAG with V vertices and E edges (no self-loops, no back-edges). */
public static List<List<Integer>> buildDAG(int V, int E, Random rng) {
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < V; i++) adj.add(new ArrayList<>());
int added = 0;
while (added < E) {
int u = rng.nextInt(V - 1);
int w = u + 1 + rng.nextInt(V - u - 1);
if (!adj.get(u).contains(w)) {
adj.get(u).add(w);
added++;
}
}
for (List<Integer> nb : adj) Collections.sort(nb);
return adj;
}
/** Builds a list of candidates with some duplicates (models term walk). */
public static List<Integer> buildCandidates(int uniqueVars, int duplicates, Random rng) {
List<Integer> cands = new ArrayList<>();
for (int i = 0; i < uniqueVars; i++) cands.add(i);
for (int i = 0; i < duplicates; i++) cands.add(rng.nextInt(uniqueVars));
Collections.shuffle(cands, rng);
return cands;
}
// ── Self-test ─────────────────────────────────────────────────────────────
public static void main(String[] args) {
System.out.println("SwiplAlgorithm — swipl-0001 + swipl-0002 + swipl-0003");
System.out.println();
// swipl-0001: top_sort graph_memberchk
System.out.println("── swipl-0001: ugraphs.pl top_sort (graph_memberchk) ──");
System.out.println("Defect: graph_memberchk O(|V|) scan per zero-vertex in Kahn loop");
System.out.println("Fix: HashMap<vertex,neighbors> built once — O(1) per lookup");
System.out.println();
System.out.printf("%-8s %-8s %-14s %-10s %s%n",
"|V|", "|E|", "Defective ops", "Fixed ops", "Speedup");
System.out.println("".repeat(52));
for (int V : new int[]{10, 20, 50, 100, 200, 500}) {
int E = V * 2; // sparse graph
List<List<Integer>> dag = buildDAG(V, Math.min(E, V * (V - 1) / 2),
new Random(42));
TopoResult def = defectiveTopSort(dag);
TopoResult fix = fixedTopSort(dag);
assert def.layers.equals(fix.layers)
: "layer mismatch at V=" + V;
System.out.printf("%-8d %-8d %-14d %-10d %.1fx%n",
V, E, def.comparisons, fix.comparisons,
(double) def.comparisons / Math.max(1, fix.comparisons));
}
System.out.println();
// swipl-0002: free_variables/4 accumulator
System.out.println("── swipl-0002: aggregate.pl free_variables/4 (list_is_free_of) ──");
System.out.println("Defect: list_is_free_of O(n) scan per candidate — O(N²) total");
System.out.println("Fix: HashSet<Var> — O(1) per candidate");
System.out.println();
System.out.printf("%-10s %-10s %-14s %-10s %s%n",
"N unique", "candidates", "Defective ops", "Fixed ops", "Speedup");
System.out.println("".repeat(56));
for (int N : new int[]{10, 50, 100, 500, 1000}) {
List<Integer> cands = buildCandidates(N, N / 2, new Random(42));
Set<Integer> bound = Collections.emptySet();
FreeVarsResult def = defectiveFreeVariables(cands, bound);
FreeVarsResult fix = fixedFreeVariables(cands, bound);
assert def.varList.size() == fix.varList.size()
: "varList size mismatch at N=" + N;
System.out.printf("%-10d %-10d %-14d %-10d %.1fx%n",
N, cands.size(), def.comparisons, fix.comparisons,
(double) def.comparisons / Math.max(1, fix.comparisons));
}
System.out.println();
// swipl-0003: clp_distinct attr_unify_hook
System.out.println("── swipl-0003: clp_distinct.pl attr_unify_hook (lists_contain) ──");
System.out.println("Defect: lists_contain O(K×N) nested scan per unification hook");
System.out.println("Fix: pre-built flat HashSet — O(1) per hook call");
System.out.println();
System.out.printf("%-8s %-8s %-14s %-10s %s%n",
"K groups", "N per grp", "Defective ops", "Fixed ops", "Speedup");
System.out.println("".repeat(52));
Random rng = new Random(42);
for (int K : new int[]{2, 5, 10, 20, 50}) {
int N = 20;
List<List<Integer>> groups = new ArrayList<>();
Set<Integer> flat = new HashSet<>();
for (int k = 0; k < K; k++) {
List<Integer> grp = new ArrayList<>();
for (int i = 0; i < N; i++) {
int v = k * N + i;
grp.add(v);
flat.add(v);
}
groups.add(grp);
}
int candidate = rng.nextInt(K * N + 10); // sometimes not in groups
UnifyHookResult def = defectiveUnifyHook(groups, candidate);
UnifyHookResult fix = fixedUnifyHook(flat, candidate);
assert def.compatible == fix.compatible
: "compatibility mismatch at K=" + K;
System.out.printf("%-8d %-8d %-14d %-10d %.1fx%n",
K, N, def.comparisons, fix.comparisons,
(double) def.comparisons / Math.max(1, fix.comparisons));
}
}
}