113 lines
4.1 KiB
Java
113 lines
4.1 KiB
Java
package unit;
|
||
|
||
import java.util.ArrayList;
|
||
import java.util.HashSet;
|
||
import java.util.List;
|
||
import java.util.Set;
|
||
|
||
/**
|
||
* ScalaRefChecksIntersectionTest — CWE-407 unit test for scala-0002
|
||
*
|
||
* Models the O(D²) defect in RefChecks.checkAllOverrides:
|
||
*
|
||
* def intersectionIsEmpty(syms1: List[Symbol], syms2: List[Symbol]) =
|
||
* !syms1.exists(syms2.contains)
|
||
*
|
||
* syms1 = member.extendedOverriddenSymbols (O(D) for hierarchy depth D)
|
||
* syms2 = other.extendedOverriddenSymbols (O(D) for hierarchy depth D)
|
||
* syms2.contains = List[Symbol].contains (O(D) linear scan)
|
||
*
|
||
* So per invocation: O(D) × O(D) = O(D²).
|
||
* With M overriding pairs per class: O(M × D²) total per compilation unit.
|
||
*
|
||
* Fix: val s2 = syms2.toSet; !syms1.exists(s2.contains)
|
||
* Build cost O(D), lookup cost O(1) → O(D) per invocation.
|
||
*
|
||
* slow(): simulates original — nested list scan.
|
||
* fast(): simulates fix — build HashSet once, then O(1) membership.
|
||
*
|
||
* Uses D=200 (deep trait hierarchy) to show measurable ratio.
|
||
* We assert slow() uses >= 10x more element operations than fast().
|
||
*/
|
||
public class ScalaRefChecksIntersectionTest {
|
||
|
||
static final int D = 200; // hierarchy depth — length of overriddenSymbols lists
|
||
static final int M = 50; // number of overriding method pairs (typical complex class)
|
||
static final int N = 10; // minimum speedup factor required
|
||
|
||
/**
|
||
* Simulates: !syms1.exists(syms2.contains)
|
||
* where syms1 and syms2 are List[Symbol] of length D.
|
||
* Counts total element comparisons across M pairs.
|
||
*/
|
||
static long slow() {
|
||
long ops = 0;
|
||
for (int pair = 0; pair < M; pair++) {
|
||
// syms1: symbols 0..D-1
|
||
List<Integer> syms1 = new ArrayList<>();
|
||
for (int i = 0; i < D; i++) syms1.add(i);
|
||
|
||
// syms2: same symbols in reverse order (worst case: match at end of each scan)
|
||
List<Integer> syms2 = new ArrayList<>();
|
||
for (int i = D - 1; i >= 0; i--) syms2.add(i);
|
||
|
||
// syms1.exists(syms2.contains) — for each sym in syms1, do O(D) scan of syms2
|
||
outer:
|
||
for (Integer sym : syms1) {
|
||
for (int j = 0; j < syms2.size(); j++) {
|
||
ops++;
|
||
if (syms2.get(j).equals(sym)) {
|
||
// found — but exists() keeps going (checking all of syms1)
|
||
// Actually exists() short-circuits on first true match.
|
||
// Here intersection is NON-empty (all symbols match),
|
||
// so first sym in syms1 will match syms2 at position D-1.
|
||
break; // simulate the inner scan stopping at match
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
/**
|
||
* Simulates: val s2 = syms2.toSet; !syms1.exists(s2.contains)
|
||
* Build s2 once per invocation (O(D)), then O(1) per lookup.
|
||
*/
|
||
static long fast() {
|
||
long ops = 0;
|
||
for (int pair = 0; pair < M; pair++) {
|
||
List<Integer> syms1 = new ArrayList<>();
|
||
for (int i = 0; i < D; i++) syms1.add(i);
|
||
|
||
List<Integer> syms2 = new ArrayList<>();
|
||
for (int i = D - 1; i >= 0; i--) syms2.add(i);
|
||
|
||
// Build set — O(D) cost
|
||
Set<Integer> s2 = new HashSet<>(syms2);
|
||
ops += D; // count build cost
|
||
|
||
// O(1) per lookup
|
||
for (Integer sym : syms1) {
|
||
ops++;
|
||
if (s2.contains(sym)) break; // same short-circuit as exists()
|
||
}
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
public static void main(String[] args) {
|
||
long sOps = slow();
|
||
long fOps = fast();
|
||
long ratio = fOps > 0 ? sOps / fOps : sOps;
|
||
|
||
System.out.println("slow ops: " + sOps);
|
||
System.out.println("fast ops: " + fOps);
|
||
System.out.println("ratio: " + sOps + "/" + fOps + " = " + ratio + "x");
|
||
|
||
if (ratio < N) {
|
||
System.out.println("1/1 FAIL — expected slowOps >= " + N + "x fastOps, got ratio=" + ratio);
|
||
System.exit(1);
|
||
}
|
||
System.out.println("1/1 PASS");
|
||
}
|
||
}
|