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.
264 lines
11 KiB
Java
264 lines
11 KiB
Java
package unit;
|
||
|
||
import java.util.*;
|
||
|
||
/**
|
||
* Unit test for solc-0001: CallGraphCycleFinder CWE-407.
|
||
*
|
||
* Defect: CallGraphCycleFinder.visit() uses std::find on currentPath (a vector)
|
||
* to detect whether a function is already on the DFS stack. That is an O(D)
|
||
* membership test performed once per edge visited. With F functions each calling
|
||
* D others, total comparisons are O(F × D²).
|
||
*
|
||
* File: libyul/optimiser/CallGraphGenerator.cpp:49
|
||
* Symbol: CallGraphCycleFinder::visit — `std::find(currentPath.begin(), currentPath.end(), _function)`
|
||
*
|
||
* Fix: Carry a parallel std::set<FunctionHandle> (currentPathSet) alongside
|
||
* the currentPath vector. The set gives O(log D) membership, effectively
|
||
* O(1) compared to O(D) at practical depths.
|
||
*
|
||
* Modeled here in Java:
|
||
* - YulString ≡ String (function name)
|
||
* - currentPath ≡ List<String>
|
||
* - currentPathSet ≡ Set<String>
|
||
* - comparisons counted at the membership-test site
|
||
*
|
||
* Expected at depth=32, functions=32:
|
||
* defective ≈ F × D×(D+1)/2 = 32 × 528 = 16896
|
||
* fixed ≈ F × D = 32 × 32 = 1024
|
||
* ratio ≈ 16.5×
|
||
*/
|
||
public class SolcCallGraphCycleTest {
|
||
|
||
// ── Defective: linear scan of currentPath for cycle detection ─────────────
|
||
|
||
static class DefectiveCycleFinder {
|
||
final Map<String, List<String>> callGraph;
|
||
final Set<String> containedInCycle = new HashSet<>();
|
||
final Set<String> visited = new HashSet<>();
|
||
final List<String> currentPath = new ArrayList<>();
|
||
long comparisons = 0;
|
||
|
||
DefectiveCycleFinder(Map<String, List<String>> callGraph) {
|
||
this.callGraph = callGraph;
|
||
}
|
||
|
||
void visit(String function) {
|
||
if (visited.contains(function)) return;
|
||
|
||
// O(D) linear scan — the defect
|
||
int cycleStart = -1;
|
||
for (int i = 0; i < currentPath.size(); i++) {
|
||
comparisons++;
|
||
if (currentPath.get(i).equals(function)) {
|
||
cycleStart = i;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (cycleStart >= 0) {
|
||
for (int i = cycleStart; i < currentPath.size(); i++)
|
||
containedInCycle.add(currentPath.get(i));
|
||
} else {
|
||
currentPath.add(function);
|
||
List<String> callees = callGraph.getOrDefault(function, Collections.emptyList());
|
||
for (String child : callees)
|
||
visit(child);
|
||
currentPath.remove(currentPath.size() - 1);
|
||
visited.add(function);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Fixed: parallel HashSet for O(1) membership test ─────────────────────
|
||
|
||
static class FixedCycleFinder {
|
||
final Map<String, List<String>> callGraph;
|
||
final Set<String> containedInCycle = new HashSet<>();
|
||
final Set<String> visited = new HashSet<>();
|
||
final List<String> currentPath = new ArrayList<>();
|
||
final Set<String> currentPathSet = new HashSet<>();
|
||
long comparisons = 0;
|
||
|
||
FixedCycleFinder(Map<String, List<String>> callGraph) {
|
||
this.callGraph = callGraph;
|
||
}
|
||
|
||
void visit(String function) {
|
||
if (visited.contains(function)) return;
|
||
|
||
// O(1) hash lookup — the fix
|
||
comparisons++;
|
||
if (currentPathSet.contains(function)) {
|
||
// still need to walk currentPath to collect cycle members
|
||
boolean inCycle = false;
|
||
for (String f : currentPath) {
|
||
if (f.equals(function)) inCycle = true;
|
||
if (inCycle) containedInCycle.add(f);
|
||
}
|
||
} else {
|
||
currentPath.add(function);
|
||
currentPathSet.add(function);
|
||
List<String> callees = callGraph.getOrDefault(function, Collections.emptyList());
|
||
for (String child : callees)
|
||
visit(child);
|
||
currentPath.remove(currentPath.size() - 1);
|
||
currentPathSet.remove(function);
|
||
visited.add(function);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Graph builders ────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Build a linear chain: f0→f1→f2→…→f(D-1), no cycles.
|
||
* Each function is visited once; the path grows to depth D.
|
||
* Defective finder scans 0+1+2+…+(D-1) = D*(D-1)/2 times per function.
|
||
* With F independent chains total defective comparisons ≈ F * D*(D-1)/2.
|
||
*/
|
||
public static Map<String, List<String>> chainGraph(int F, int D) {
|
||
Map<String, List<String>> g = new LinkedHashMap<>();
|
||
for (int f = 0; f < F; f++) {
|
||
String prefix = "fn" + f + "_";
|
||
for (int d = 0; d < D - 1; d++) {
|
||
g.computeIfAbsent(prefix + d, k -> new ArrayList<>())
|
||
.add(prefix + (d + 1));
|
||
}
|
||
g.computeIfAbsent(prefix + (D - 1), k -> new ArrayList<>());
|
||
}
|
||
return g;
|
||
}
|
||
|
||
/**
|
||
* Build a star graph: root calls all F leaf functions.
|
||
* Path depth is 2 for each leaf. Membership test on each leaf sees
|
||
* currentPath = [root] → 1 comparison (defective) vs O(1) (fixed).
|
||
* This isolates the per-visit cost pattern.
|
||
*/
|
||
static Map<String, List<String>> starGraph(int F) {
|
||
Map<String, List<String>> g = new LinkedHashMap<>();
|
||
List<String> children = new ArrayList<>();
|
||
for (int i = 0; i < F; i++) {
|
||
String leaf = "leaf_" + i;
|
||
children.add(leaf);
|
||
g.put(leaf, Collections.emptyList());
|
||
}
|
||
g.put("root", children);
|
||
return g;
|
||
}
|
||
|
||
// ── Simulation helpers ────────────────────────────────────────────────────
|
||
|
||
public static long simulateDefective(Map<String, List<String>> g) {
|
||
DefectiveCycleFinder finder = new DefectiveCycleFinder(g);
|
||
for (String fn : g.keySet()) finder.visit(fn);
|
||
return finder.comparisons;
|
||
}
|
||
|
||
public static long simulateFixed(Map<String, List<String>> g) {
|
||
FixedCycleFinder finder = new FixedCycleFinder(g);
|
||
for (String fn : g.keySet()) finder.visit(fn);
|
||
return finder.comparisons;
|
||
}
|
||
|
||
// ── Tests ─────────────────────────────────────────────────────────────────
|
||
|
||
static void testCorrectnessNoCycle() {
|
||
Map<String, List<String>> g = chainGraph(4, 5);
|
||
DefectiveCycleFinder def = new DefectiveCycleFinder(g);
|
||
FixedCycleFinder fix = new FixedCycleFinder(g);
|
||
for (String fn : g.keySet()) { def.visit(fn); fix.visit(fn); }
|
||
assert def.containedInCycle.equals(fix.containedInCycle)
|
||
: "cycle sets differ (no-cycle graph)";
|
||
assert def.containedInCycle.isEmpty()
|
||
: "expected no cycles in chain graph";
|
||
System.out.println("PASS testCorrectnessNoCycle");
|
||
}
|
||
|
||
static void testCorrectnessCycleDetection() {
|
||
// f0 → f1 → f2 → f0 (cycle), f3 standalone
|
||
Map<String, List<String>> g = new LinkedHashMap<>();
|
||
g.put("f0", Arrays.asList("f1"));
|
||
g.put("f1", Arrays.asList("f2"));
|
||
g.put("f2", Arrays.asList("f0"));
|
||
g.put("f3", Collections.emptyList());
|
||
|
||
DefectiveCycleFinder def = new DefectiveCycleFinder(g);
|
||
FixedCycleFinder fix = new FixedCycleFinder(g);
|
||
for (String fn : g.keySet()) { def.visit(fn); fix.visit(fn); }
|
||
|
||
assert def.containedInCycle.containsAll(Arrays.asList("f0", "f1", "f2"))
|
||
: "defective missed cycle members: " + def.containedInCycle;
|
||
assert fix.containedInCycle.containsAll(Arrays.asList("f0", "f1", "f2"))
|
||
: "fixed missed cycle members: " + fix.containedInCycle;
|
||
assert !def.containedInCycle.contains("f3")
|
||
: "defective falsely included f3";
|
||
assert !fix.containedInCycle.contains("f3")
|
||
: "fixed falsely included f3";
|
||
assert def.containedInCycle.equals(fix.containedInCycle)
|
||
: "cycle sets differ between defective and fixed";
|
||
System.out.println("PASS testCorrectnessCycleDetection");
|
||
}
|
||
|
||
static void testDefectiveGrowsQuadratically() {
|
||
// Comparison count in chain graph grows quadratically with depth D
|
||
long prev = -1;
|
||
for (int D : new int[]{8, 16, 32}) {
|
||
long c = simulateDefective(chainGraph(1, D));
|
||
if (prev > 0) {
|
||
double ratio = (double) c / prev;
|
||
assert ratio > 2.5
|
||
: "defective should grow >2.5x when D doubles; got " + ratio + " at D=" + D;
|
||
}
|
||
prev = c;
|
||
}
|
||
System.out.println("PASS testDefectiveGrowsQuadratically");
|
||
}
|
||
|
||
static void testFixedGrowsLinearly() {
|
||
long prev = -1;
|
||
for (int D : new int[]{8, 16, 32}) {
|
||
long c = simulateFixed(chainGraph(1, D));
|
||
if (prev > 0) {
|
||
double ratio = (double) c / prev;
|
||
assert ratio < 2.3
|
||
: "fixed should grow ~2x when D doubles; got " + ratio + " at D=" + D;
|
||
}
|
||
prev = c;
|
||
}
|
||
System.out.println("PASS testFixedGrowsLinearly");
|
||
}
|
||
|
||
static void testRatioAtScale() {
|
||
// F=32 independent chains of depth D=32
|
||
// defective: each of the 32 chains accumulates 0+1+…+31 = 496 comparisons → 32*496 = 15872
|
||
// fixed: each chain accumulates 32 comparisons → 32*32 = 1024
|
||
// The spec says ≈16896 (uses D*(D+1)/2) and 1024; ratio ≈16.5×
|
||
int F = 32, D = 32;
|
||
Map<String, List<String>> g = chainGraph(F, D);
|
||
long defComp = simulateDefective(g);
|
||
long fixComp = simulateFixed(g);
|
||
double ratio = (double) defComp / fixComp;
|
||
|
||
assert defComp >= 15000 && defComp <= 18000
|
||
: "defective comparisons out of expected range: " + defComp;
|
||
assert fixComp == (long) F * D
|
||
: "fixed comparisons should be F*D=" + (F * D) + "; got " + fixComp;
|
||
assert ratio > 10.0
|
||
: "ratio should be >10x at F=32,D=32; got " + ratio;
|
||
|
||
System.out.printf(
|
||
"PASS testRatioAtScale (defective=%d, fixed=%d, ratio=%.1fx)%n",
|
||
defComp, fixComp, ratio);
|
||
}
|
||
|
||
public static void main(String[] args) {
|
||
testCorrectnessNoCycle();
|
||
testCorrectnessCycleDetection();
|
||
testDefectiveGrowsQuadratically();
|
||
testFixedGrowsLinearly();
|
||
testRatioAtScale();
|
||
System.out.println("All solc-0001 tests passed.");
|
||
}
|
||
}
|