feat(reconcile): minimal word-level diff (Myers) with a bounded edit distance
Prefix/suffix trimmed first; adjacent deletes and inserts read as one Replace; beyond 3,000 edits (two texts sharing almost nothing) the middle is one Replace instead of gigabytes of trace. Mutation-checked: a wrong tie-break, missing replace coalescing, wrong suffix accounting and a punctuation-bearing key each turn tests red. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user