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 (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 * - currentPathSet ≡ Set * - 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> callGraph; final Set containedInCycle = new HashSet<>(); final Set visited = new HashSet<>(); final List currentPath = new ArrayList<>(); long comparisons = 0; DefectiveCycleFinder(Map> 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 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> callGraph; final Set containedInCycle = new HashSet<>(); final Set visited = new HashSet<>(); final List currentPath = new ArrayList<>(); final Set currentPathSet = new HashSet<>(); long comparisons = 0; FixedCycleFinder(Map> 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 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> chainGraph(int F, int D) { Map> 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> starGraph(int F) { Map> g = new LinkedHashMap<>(); List 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> g) { DefectiveCycleFinder finder = new DefectiveCycleFinder(g); for (String fn : g.keySet()) finder.visit(fn); return finder.comparisons; } public static long simulateFixed(Map> g) { FixedCycleFinder finder = new FixedCycleFinder(g); for (String fn : g.keySet()) finder.visit(fn); return finder.comparisons; } // ── Tests ───────────────────────────────────────────────────────────────── static void testCorrectnessNoCycle() { Map> 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> 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> 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."); } }