java-topology/defects/javac/unit/NotifyChangeAlgorithm.java

208 lines
8 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 unit;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Models InferenceContext.notifyChange() diff-hoist defect — defective vs fixed.
*
* DEFECT (javac-0007): inferencevars.diff(inferredVars) is recomputed on
* every iteration of the freeTypeListeners loop.
*
* void notifyChange(List<Type> inferredVars) {
* for (entry : freeTypeListeners.entrySet()) {
* if (!Type.containsAny(entry.getValue(),
* inferencevars.diff(inferredVars))) { // ← rebuilt L times
* ...
* }
* }
* }
*
* List.diff() iterates inferredVars (M elements) and for each scans
* inferencevars (N elements) → O(N×M) per call. Called L times: O(L×N×M).
*
* FIX: hoist the diff() before the loop.
*
* List<Type> remainingVars = inferencevars.diff(inferredVars); // once
* for (entry : freeTypeListeners.entrySet()) {
* if (!Type.containsAny(entry.getValue(), remainingVars)) {
* ...
* }
* }
*
* Complexity:
* Defective: O(L × N × M)
* Fixed: O(N×M + L×V)
*
* Test topology:
* N = inferencevars (type-variable list length)
* M = inferredVars (resolved subset — set to N/2 for worst-case diff work)
* L = listener count
*
* The op-counter measures diff() recomputation cost:
* SLOW: diff() called L times → L × N × M ops
* FAST: diff() called once → N × M ops (then L cheap list-scan checks)
*/
public class NotifyChangeAlgorithm {
// ── Result ───────────────────────────────────────────────────────────────
public static class Result {
public final int notifiedCount; // how many listeners fired
public final long ops;
public Result(int notifiedCount, long ops) {
this.notifiedCount = notifiedCount;
this.ops = ops;
}
}
// ── helpers ──────────────────────────────────────────────────────────────
/**
* Simulates List.diff(that): iterates every element of 'from' (N items),
* for each does a linear scan of 'that' (M items). Returns elements of
* 'from' not present in 'that'.
*/
static List<String> diff(List<String> from, List<String> that, long[] ops) {
List<String> result = new ArrayList<>();
for (String f : from) {
boolean found = false;
for (String t : that) {
ops[0]++;
if (f.equals(t)) {
found = true;
break;
}
}
if (!found) result.add(f);
}
return result;
}
/**
* Simulates Type.containsAny(ts1, ts2): returns true if any element of
* ts1 is present in ts2.
*/
static boolean containsAny(List<String> ts1, List<String> ts2) {
for (String t : ts1) {
for (String s : ts2) {
if (t.equals(s)) return true;
}
}
return false;
}
// ── Defective ─────────────────────────────────────────────────────────
/**
* diff() recomputed inside every listener iteration.
*/
public static Result slow(List<String> inferencevars,
List<String> inferredVars,
Map<String, List<String>> listeners) {
long[] ops = {0};
int notified = 0;
for (Map.Entry<String, List<String>> entry : listeners.entrySet()) {
// diff recomputed every iteration
List<String> remaining = diff(inferencevars, inferredVars, ops);
if (!containsAny(entry.getValue(), remaining)) {
notified++;
}
}
return new Result(notified, ops[0]);
}
// ── Fixed ─────────────────────────────────────────────────────────────
/**
* diff() hoisted before the loop.
*/
public static Result fast(List<String> inferencevars,
List<String> inferredVars,
Map<String, List<String>> listeners) {
long[] ops = {0};
int notified = 0;
List<String> remaining = diff(inferencevars, inferredVars, ops); // once
for (Map.Entry<String, List<String>> entry : listeners.entrySet()) {
if (!containsAny(entry.getValue(), remaining)) {
notified++;
}
}
return new Result(notified, ops[0]);
}
// ── Test harness ─────────────────────────────────────────────────────
static List<String> makeVars(String prefix, int n) {
List<String> vars = new ArrayList<>(n);
for (int i = 0; i < n; i++) vars.add(prefix + i);
return vars;
}
/**
* Build listener map: L listeners each watching V variables from
* inferencevars. We pick variables that are NOT in inferredVars so
* that containsAny returns false (listener fires).
*/
static Map<String, List<String>> makeListeners(List<String> inferencevars,
List<String> inferredVars,
int l, int v) {
// build set of "remaining" vars (not inferred) to register listeners on
List<String> remaining = new ArrayList<>();
for (String iv : inferencevars) {
if (!inferredVars.contains(iv)) remaining.add(iv);
}
Map<String, List<String>> map = new LinkedHashMap<>();
for (int i = 0; i < l; i++) {
List<String> watched = new ArrayList<>();
// each listener watches v vars from remaining (cycling)
for (int j = 0; j < v; j++) {
watched.add(remaining.get((i + j) % remaining.size()));
}
map.put("listener_" + i, watched);
}
return map;
}
public static void main(String[] args) {
// N=inferencevars, M=inferredVars (=N/2), L=listeners
int[][] configs = {
{40, 20, 40}, // N=40, M=20, L=40
{80, 40, 80}, // N=80, M=40, L=80
{160, 80, 160}, // N=160, M=80, L=160
};
int passes = 0;
int fails = 0;
for (int[] c : configs) {
int n = c[0], m = c[1], l = c[2];
List<String> inferencevars = makeVars("T", n);
List<String> inferredVars = makeVars("T", m); // T0..T(m-1) inferred
Map<String, List<String>> listeners = makeListeners(inferencevars, inferredVars, l, 3);
Result slow = slow(inferencevars, inferredVars, listeners);
Result fast = fast(inferencevars, inferredVars, listeners);
// correctness: same notification count
boolean correct = slow.notifiedCount == fast.notifiedCount;
// ratio: slow should be >> fast
double ratio = (double) slow.ops / Math.max(fast.ops, 1);
boolean ratioOk = ratio >= 5.0;
if (correct && ratioOk) {
System.out.printf("PASS N=%4d M=%4d L=%4d slow=%10d fast=%8d ratio=%6.1fx%n",
n, m, l, slow.ops, fast.ops, ratio);
passes++;
} else {
System.out.printf("FAIL N=%4d M=%4d L=%4d correct=%b ratio=%.1fx (need>=5)%n",
n, m, l, correct, ratio);
fails++;
}
}
System.out.printf("%n%d/%d PASS%n", passes, passes + fails);
if (fails > 0) System.exit(1);
}
}