package unit; import java.util.*; /** * vim-0001: ins_compl_add linked-list linear scan O(N²) in insert-mode completion. * * Models ins_compl_add_matches() calling ins_compl_add() for N candidates. * Each ins_compl_add() does a full O(M) walk of the existing completions linked * list to check for duplicates. Total work is O(N²). * * The fix replaces the linked-list scan with a HashSet lookup: O(N) total. */ public class InsComplDedupTest { // ----- SLOW path: linked-list dedup (mirrors Vim's insexpand.c) ----- static class ComplNode { String str; ComplNode next; ComplNode(String s) { this.str = s; } } /** Returns op count for adding one candidate to the linked list. */ static long slowAddCandidate(ComplNode head, String candidate, long[] ops) { // Walk linked list to check for duplicates — O(M) ComplNode cur = head; while (cur != null) { ops[0]++; if (cur.str.equals(candidate)) { return -1; // already present } cur = cur.next; } return 0; // not found — would be added } static long slowBuildCompletions(String[] candidates) { long[] ops = {0}; ComplNode head = null; ComplNode tail = null; List accepted = new ArrayList<>(); for (String c : candidates) { // Simulate the head-pointer (compl_first_match is not null) if (head == null) { head = new ComplNode(c); tail = head; accepted.add(c); continue; } long result = slowAddCandidate(head, c, ops); if (result == 0) { // Add to list ComplNode node = new ComplNode(c); tail.next = node; tail = node; accepted.add(c); } } return ops[0]; } // ----- FAST path: HashSet dedup ----- static long fastBuildCompletions(String[] candidates) { long ops = 0; Set seen = new HashSet<>(); for (String c : candidates) { ops++; // O(1) hash lookup seen.add(c); } return ops; } // ----- Test runner ----- public static void main(String[] args) { System.out.println("vim-0001: InsComplDedupTest"); System.out.println("=========================="); int passed = 0; int total = 0; // Test 1: correctness — both paths accept same unique elements { total++; String[] cands = {"alpha", "beta", "gamma", "alpha", "delta", "beta"}; // Slow path: collect accepted list long[] ops = {0}; ComplNode head = null; ComplNode tail = null; List slowAccepted = new ArrayList<>(); for (String c : cands) { if (head == null) { head = new ComplNode(c); tail = head; slowAccepted.add(c); continue; } if (slowAddCandidate(head, c, ops) == 0) { ComplNode node = new ComplNode(c); tail.next = node; tail = node; slowAccepted.add(c); } } Set fastAccepted = new HashSet<>(Arrays.asList(cands)); boolean ok = new HashSet<>(slowAccepted).equals(fastAccepted); System.out.println("Test 1 (correctness): " + (ok ? "PASS" : "FAIL")); if (ok) passed++; } // Test 2: op-count ratio >= 5x at N=200 unique candidates { total++; int N = 200; String[] candidates = new String[N]; for (int i = 0; i < N; i++) candidates[i] = "word_" + i; long slowOps = slowBuildCompletions(candidates); long fastOps = fastBuildCompletions(candidates); double ratio = (double) slowOps / fastOps; System.out.printf("Test 2 (N=%d unique): slow=%d ops, fast=%d ops, ratio=%.1fx%n", N, slowOps, fastOps, ratio); boolean ok = ratio >= 5.0; System.out.println("Test 2 (ratio >= 5x): " + (ok ? "PASS" : "FAIL")); if (ok) passed++; } // Test 3: op-count ratio >= 50x at N=500 unique candidates { total++; int N = 500; String[] candidates = new String[N]; for (int i = 0; i < N; i++) candidates[i] = "word_" + i; long slowOps = slowBuildCompletions(candidates); long fastOps = fastBuildCompletions(candidates); double ratio = (double) slowOps / fastOps; System.out.printf("Test 3 (N=%d unique): slow=%d ops, fast=%d ops, ratio=%.1fx%n", N, slowOps, fastOps, ratio); boolean ok = ratio >= 50.0; System.out.println("Test 3 (ratio >= 50x): " + (ok ? "PASS" : "FAIL")); if (ok) passed++; } // Test 4: op-count ratio >= 100x at N=1000 unique candidates { total++; int N = 1000; String[] candidates = new String[N]; for (int i = 0; i < N; i++) candidates[i] = "word_" + i; long slowOps = slowBuildCompletions(candidates); long fastOps = fastBuildCompletions(candidates); double ratio = (double) slowOps / fastOps; System.out.printf("Test 4 (N=%d unique): slow=%d ops, fast=%d ops, ratio=%.1fx%n", N, slowOps, fastOps, ratio); boolean ok = ratio >= 100.0; System.out.println("Test 4 (ratio >= 100x): " + (ok ? "PASS" : "FAIL")); if (ok) passed++; } // Test 5: with duplicates — correctness and ratio hold { total++; int N = 400; String[] candidates = new String[N]; for (int i = 0; i < N; i++) candidates[i] = "dup_" + (i % 50); // 50 unique, 8 dups each long slowOps = slowBuildCompletions(candidates); long fastOps = fastBuildCompletions(candidates); double ratio = (double) slowOps / fastOps; System.out.printf("Test 5 (N=%d w/dups, 50 unique): slow=%d ops, fast=%d ops, ratio=%.1fx%n", N, slowOps, fastOps, ratio); boolean ok = ratio >= 5.0; System.out.println("Test 5 (ratio >= 5x): " + (ok ? "PASS" : "FAIL")); if (ok) passed++; } System.out.println(); System.out.println(passed + "/" + total + " PASS"); assert passed == total : passed + "/" + total + " tests passed"; } }