diff --git a/internal/reconcile/diff.go b/internal/reconcile/diff.go new file mode 100644 index 0000000..b558f85 --- /dev/null +++ b/internal/reconcile/diff.go @@ -0,0 +1,187 @@ +package reconcile + +// OpKind is the kind of one run in an edit script. +type OpKind int + +const ( + // Equal: A[AStart:AEnd] and B[BStart:BEnd] are the same words. + Equal OpKind = iota + // Replace: different words on both sides. + Replace + // Delete: words only in A. + Delete + // Insert: words only in B. + Insert +) + +// Op is one run of the edit script. The ops of a Diff tile both inputs: each op starts +// where the previous one ended, in A and in B. +type Op struct { + Kind OpKind + AStart, AEnd int + BStart, BEnd int +} + +// maxEdits bounds the Myers search. Memory grows with the square of the edit +// distance, so two texts that share almost nothing (one engine produced garbage) +// are reported as one Replace instead of spending gigabytes proving it. A real +// hour disagrees in roughly 400 places, i.e. about 800 edits, far below this. +const maxEdits = 3000 + +// Diff returns a minimal edit script from a to b (Myers, O(ND)). Adjacent deletions +// and insertions are reported as one Replace. +func Diff(a, b []string) []Op { + pre := 0 + for pre < len(a) && pre < len(b) && a[pre] == b[pre] { + pre++ + } + suf := 0 + for suf < len(a)-pre && suf < len(b)-pre && a[len(a)-1-suf] == b[len(b)-1-suf] { + suf++ + } + midA, midB := a[pre:len(a)-suf], b[pre:len(b)-suf] + + steps, ok := myers(midA, midB) + if !ok { + steps = steps[:0] + for range midA { + steps = append(steps, stepDel) + } + for range midB { + steps = append(steps, stepIns) + } + } + + var ops []Op + ai, bi := 0, 0 + emit := func(k OpKind, da, db int) { + if da == 0 && db == 0 { + return + } + if k == Equal && len(ops) > 0 && ops[len(ops)-1].Kind == Equal { + ops[len(ops)-1].AEnd += da + ops[len(ops)-1].BEnd += db + } else { + ops = append(ops, Op{Kind: k, AStart: ai, AEnd: ai + da, BStart: bi, BEnd: bi + db}) + } + ai += da + bi += db + } + emit(Equal, pre, pre) + for i := 0; i < len(steps); { + if steps[i] == stepEq { + j := i + for j < len(steps) && steps[j] == stepEq { + j++ + } + emit(Equal, j-i, j-i) + i = j + continue + } + dels, inss := 0, 0 + for i < len(steps) && steps[i] != stepEq { + if steps[i] == stepDel { + dels++ + } else { + inss++ + } + i++ + } + switch { + case dels > 0 && inss > 0: + emit(Replace, dels, inss) + case dels > 0: + emit(Delete, dels, 0) + default: + emit(Insert, 0, inss) + } + } + emit(Equal, suf, suf) + return ops +} + +type step int8 + +const ( + stepEq step = iota + stepDel + stepIns +) + +// myers returns one step per element of the edit script, in order. ok is false when +// the edit distance exceeds maxEdits. +func myers(a, b []string) ([]step, bool) { + n, m := len(a), len(b) + if n == 0 && m == 0 { + return nil, true + } + limit := min(n+m, maxEdits) + off := limit + 1 + v := make([]int32, 2*limit+3) + var trace [][]int32 // trace[d] is v as it stood at the start of round d, for k in [-d, d] + + for d := 0; d <= limit; d++ { + snap := make([]int32, 2*d+1) + for k := -d; k <= d; k++ { + snap[k+d] = v[off+k] + } + trace = append(trace, snap) + for k := -d; k <= d; k += 2 { + var x int + if k == -d || (k != d && v[off+k-1] < v[off+k+1]) { + x = int(v[off+k+1]) + } else { + x = int(v[off+k-1]) + 1 + } + y := x - k + for x < n && y < m && a[x] == b[y] { + x++ + y++ + } + v[off+k] = int32(x) + if x >= n && y >= m { + return backtrack(trace, n, m, d), true + } + } + } + return nil, false +} + +func backtrack(trace [][]int32, n, m, d int) []step { + var rev []step + x, y := n, m + for ; d >= 0; d-- { + snap := trace[d] + at := func(k int) int { return int(snap[k+d]) } + k := x - y + var prevK int + if k == -d || (k != d && at(k-1) < at(k+1)) { + prevK = k + 1 + } else { + prevK = k - 1 + } + prevX := 0 + if d > 0 { + prevX = at(prevK) + } + prevY := prevX - prevK + for x > prevX && y > prevY { + rev = append(rev, stepEq) + x-- + y-- + } + if d > 0 { + if x == prevX { + rev = append(rev, stepIns) + } else { + rev = append(rev, stepDel) + } + } + x, y = prevX, prevY + } + out := make([]step, len(rev)) + for i, s := range rev { + out[len(rev)-1-i] = s + } + return out +}