Skip to main content

ashlar_diff/
myers.rs

1//! Sequence diff: Myers' O((N+M)·D) algorithm with a cost cap, and a coarse fallback past the cap.
2//!
3//! Elements are interned to `u32` first, so comparisons are integer compares. The common prefix
4//! and suffix are stripped before Myers runs. When the edit distance of what is left exceeds the
5//! cap, the input is split at lines that occur exactly once on both sides (the longest increasing
6//! run of them) and each piece is diffed again; past a small recursion depth, or with no such
7//! anchors, the piece becomes one delete and one insert. The result is then a valid but not
8//! necessarily minimal edit script, and [`Edit::coarse`] says so.
9
10use std::collections::HashMap;
11use std::hash::Hash;
12
13/// The default cost cap: the largest edit distance one Myers run may reach. Its trace takes about
14/// `cap² / 2` words, so the default keeps it near 4 MB.
15pub const DEFAULT_MAX_COST: usize = 1024;
16
17/// Hard ceiling on the cost cap, whatever the caller asks for (about 16 MB of trace).
18const MAX_COST_CEILING: usize = 2048;
19
20/// How many times the anchor fallback may split a piece that still exceeds the cap.
21const MAX_DEPTH: u32 = 6;
22
23/// One run of an edit script. Indices are 0-based positions in the old (`a`) and new (`b`) inputs.
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub enum Op {
26    /// `len` elements equal on both sides.
27    Equal { old: usize, new: usize, len: usize },
28    /// `len` elements of the old side removed.
29    Delete { old: usize, len: usize },
30    /// `len` elements of the new side added.
31    Insert { new: usize, len: usize },
32}
33
34/// An edit script from the old to the new sequence.
35#[derive(Clone, Debug, Default, PartialEq, Eq)]
36pub struct Edit {
37    /// Runs in order; together they cover both inputs exactly once.
38    pub ops: Vec<Op>,
39    /// The cost cap was hit somewhere: the script is valid but may not be minimal.
40    pub coarse: bool,
41}
42
43impl Edit {
44    /// Elements deleted plus elements inserted.
45    pub fn cost(&self) -> usize {
46        self.ops
47            .iter()
48            .map(|op| match op {
49                Op::Equal { .. } => 0,
50                Op::Delete { len, .. } | Op::Insert { len, .. } => *len,
51            })
52            .fold(0usize, usize::saturating_add)
53    }
54}
55
56#[derive(Clone, Copy, PartialEq, Eq)]
57enum Step {
58    Eq,
59    Del,
60    Ins,
61}
62
63/// Diffs two sequences. `max_cost` caps one Myers run (see the module docs); it is clamped to
64/// `1..=2048`.
65///
66/// ```
67/// use ashlar_diff::{diff_ops, Op};
68///
69/// let edit = diff_ops(&["a", "b", "c"], &["a", "c", "d"], 1024);
70/// assert_eq!(edit.cost(), 2);
71/// assert_eq!(edit.ops.first(), Some(&Op::Equal { old: 0, new: 0, len: 1 }));
72/// ```
73pub fn diff_ops<T: Eq + Hash>(a: &[T], b: &[T], max_cost: usize) -> Edit {
74    let mut ids: HashMap<&T, u32> = HashMap::new();
75    let mut ia = Vec::with_capacity(a.len());
76    for x in a {
77        let next = u32::try_from(ids.len()).unwrap_or(u32::MAX);
78        ia.push(*ids.entry(x).or_insert(next));
79    }
80    let mut ib = Vec::with_capacity(b.len());
81    for x in b {
82        let next = u32::try_from(ids.len()).unwrap_or(u32::MAX);
83        ib.push(*ids.entry(x).or_insert(next));
84    }
85    diff_ids(&ia, &ib, max_cost)
86}
87
88/// [`diff_ops`] on interned ids.
89pub(crate) fn diff_ids(a: &[u32], b: &[u32], max_cost: usize) -> Edit {
90    let cap = max_cost.clamp(1, MAX_COST_CEILING);
91    let mut steps = Vec::with_capacity(a.len().saturating_add(b.len()));
92    let mut coarse = false;
93    let mut scratch = Vec::new();
94    rec(a, b, cap, 0, &mut steps, &mut coarse, &mut scratch);
95    Edit { ops: compress(&steps), coarse }
96}
97
98fn compress(steps: &[Step]) -> Vec<Op> {
99    let mut ops: Vec<Op> = Vec::new();
100    let (mut x, mut y) = (0usize, 0usize);
101    for s in steps {
102        match (s, ops.last_mut()) {
103            (Step::Eq, Some(Op::Equal { len, .. })) | (Step::Del, Some(Op::Delete { len, .. })) | (Step::Ins, Some(Op::Insert { len, .. })) => {
104                *len = len.saturating_add(1);
105            }
106            (Step::Eq, _) => ops.push(Op::Equal { old: x, new: y, len: 1 }),
107            (Step::Del, _) => ops.push(Op::Delete { old: x, len: 1 }),
108            (Step::Ins, _) => ops.push(Op::Insert { new: y, len: 1 }),
109        }
110        match s {
111            Step::Eq => {
112                x = x.saturating_add(1);
113                y = y.saturating_add(1);
114            }
115            Step::Del => x = x.saturating_add(1),
116            Step::Ins => y = y.saturating_add(1),
117        }
118    }
119    ops
120}
121
122fn push_n(out: &mut Vec<Step>, step: Step, n: usize) {
123    out.extend(std::iter::repeat_n(step, n));
124}
125
126fn rec(a: &[u32], b: &[u32], cap: usize, depth: u32, out: &mut Vec<Step>, coarse: &mut bool, scratch: &mut Vec<i64>) {
127    let prefix = a.iter().zip(b).take_while(|(x, y)| x == y).count();
128    let (a, b) = (a.get(prefix..).unwrap_or(&[]), b.get(prefix..).unwrap_or(&[]));
129    let suffix = a.iter().rev().zip(b.iter().rev()).take_while(|(x, y)| x == y).count();
130    let (a, b) = (a.get(..a.len() - suffix).unwrap_or(&[]), b.get(..b.len() - suffix).unwrap_or(&[]));
131    push_n(out, Step::Eq, prefix);
132    if a.is_empty() || b.is_empty() {
133        push_n(out, Step::Del, a.len());
134        push_n(out, Step::Ins, b.len());
135    } else if !myers(a, b, cap, out, scratch) {
136        let anchors = if depth < MAX_DEPTH { unique_anchors(a, b) } else { Vec::new() };
137        if anchors.is_empty() {
138            *coarse = true;
139            push_n(out, Step::Del, a.len());
140            push_n(out, Step::Ins, b.len());
141        } else {
142            let (mut i, mut j) = (0usize, 0usize);
143            for (ai, bj) in anchors {
144                let (Some(pa), Some(pb)) = (a.get(i..ai), b.get(j..bj)) else { continue };
145                rec(pa, pb, cap, depth + 1, out, coarse, scratch);
146                out.push(Step::Eq);
147                i = ai + 1;
148                j = bj + 1;
149            }
150            rec(a.get(i..).unwrap_or(&[]), b.get(j..).unwrap_or(&[]), cap, depth + 1, out, coarse, scratch);
151        }
152    }
153    push_n(out, Step::Eq, suffix);
154}
155
156/// Marks a diagonal no path reaches in a round.
157const NONE: i64 = i64::MIN;
158
159/// Myers' greedy algorithm. Appends the steps and returns `true`, or returns `false` (appending
160/// nothing) when the edit distance exceeds `cap`.
161///
162/// `trace` keeps, for each round `d`, the furthest x on diagonals `-d, -d+2, …, d` (`d + 1`
163/// entries starting at `d·(d+1)/2`), so a path can be walked back without storing it.
164fn myers(a: &[u32], b: &[u32], cap: usize, out: &mut Vec<Step>, trace: &mut Vec<i64>) -> bool {
165    let (n, m) = (a.len() as i64, b.len() as i64);
166    let limit = (cap as i64).min(n + m);
167    trace.clear();
168    let snake = |mut x: i64, mut y: i64| {
169        while x < n && y < m && a.get(x as usize) == b.get(y as usize) {
170            x += 1;
171            y += 1;
172        }
173        x
174    };
175    for d in 0..=limit {
176        for j in 0..=d {
177            let k = 2 * j - d;
178            let x = if d == 0 { Some(0) } else { choose(trace, d, k, n, m).map(|(x, _)| x) };
179            let x = match x {
180                Some(x) => snake(x, x - k),
181                None => NONE,
182            };
183            trace.push(x);
184            if x == n && x - k == m {
185                backtrack(trace, d, n, m, out);
186                return true;
187            }
188        }
189    }
190    false
191}
192
193/// The furthest x a round-`d` path reaches on diagonal `k` before its snake, and whether it came
194/// down (an insertion) rather than right (a deletion). Reads round `d - 1` from `trace`.
195fn choose(trace: &[i64], d: i64, k: i64, n: i64, m: i64) -> Option<(i64, bool)> {
196    if k < -m || k > n {
197        return None;
198    }
199    let prev = d - 1;
200    let start = (prev * (prev + 1) / 2) as usize;
201    let at = |kk: i64| -> Option<i64> {
202        if kk < -prev || kk > prev {
203            return None;
204        }
205        let x = *trace.get(start + ((kk + prev) / 2) as usize)?;
206        (x != NONE).then_some(x)
207    };
208    let down = at(k + 1).filter(|x| x - k <= m);
209    let right = at(k - 1).map(|x| x + 1).filter(|x| *x <= n);
210    match (down, right) {
211        (Some(xd), Some(xr)) if xd >= xr => Some((xd, true)),
212        (_, Some(xr)) => Some((xr, false)),
213        (Some(xd), None) => Some((xd, true)),
214        (None, None) => None,
215    }
216}
217
218fn backtrack(trace: &[i64], last: i64, n: i64, m: i64, out: &mut Vec<Step>) {
219    let mut rev: Vec<Step> = Vec::new();
220    let (mut x, mut y) = (n, m);
221    let mut d = last;
222    while d > 0 {
223        let k = x - y;
224        let Some((mx, down)) = choose(trace, d, k, n, m) else { break };
225        // the snake from the move's end point to (x, y)
226        for _ in mx..x {
227            rev.push(Step::Eq);
228        }
229        if down {
230            rev.push(Step::Ins);
231            x = mx;
232            y = mx - (k + 1);
233        } else {
234            rev.push(Step::Del);
235            x = mx - 1;
236            y = x - (k - 1);
237        }
238        d -= 1;
239    }
240    for _ in 0..x.min(y) {
241        rev.push(Step::Eq);
242    }
243    out.extend(rev.into_iter().rev());
244}
245
246/// Pairs `(i, j)` with `a[i] == b[j]` where the element occurs exactly once in each side, in the
247/// longest run increasing on both sides.
248fn unique_anchors(a: &[u32], b: &[u32]) -> Vec<(usize, usize)> {
249    // per id: (count in a, count in b, position in b)
250    let mut seen: HashMap<u32, (u32, u32, usize)> = HashMap::new();
251    for x in a {
252        let e = seen.entry(*x).or_insert((0, 0, 0));
253        e.0 = e.0.saturating_add(1);
254    }
255    for (j, x) in b.iter().enumerate() {
256        if let Some(e) = seen.get_mut(x) {
257            e.1 = e.1.saturating_add(1);
258            e.2 = j;
259        }
260    }
261    let pairs: Vec<(usize, usize)> =
262        a.iter().enumerate().filter_map(|(i, x)| seen.get(x).filter(|e| e.0 == 1 && e.1 == 1).map(|e| (i, e.2))).collect();
263    longest_increasing(&pairs)
264}
265
266/// The longest subsequence of `pairs` (sorted by `.0`) increasing in `.1` (patience sorting).
267fn longest_increasing(pairs: &[(usize, usize)]) -> Vec<(usize, usize)> {
268    // tails[l] = index into pairs of the smallest tail of an increasing run of length l + 1
269    let mut tails: Vec<usize> = Vec::new();
270    let mut prev: Vec<Option<usize>> = vec![None; pairs.len()];
271    for (idx, (_, v)) in pairs.iter().enumerate() {
272        let pos = tails.partition_point(|&t| pairs.get(t).is_some_and(|p| p.1 < *v));
273        if let Some(p) = prev.get_mut(idx) {
274            *p = pos.checked_sub(1).and_then(|q| tails.get(q).copied());
275        }
276        if pos < tails.len() {
277            if let Some(t) = tails.get_mut(pos) {
278                *t = idx;
279            }
280        } else {
281            tails.push(idx);
282        }
283    }
284    let mut run = Vec::with_capacity(tails.len());
285    let mut cur = tails.last().copied();
286    while let Some(c) = cur {
287        if let Some(p) = pairs.get(c) {
288            run.push(*p);
289        }
290        cur = prev.get(c).copied().flatten();
291        if run.len() > pairs.len() {
292            break;
293        }
294    }
295    run.reverse();
296    run
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    fn apply(a: &[u32], b: &[u32], edit: &Edit) -> Vec<u32> {
304        let mut out = Vec::new();
305        for op in &edit.ops {
306            match *op {
307                Op::Equal { old, len, .. } => out.extend_from_slice(&a[old..old + len]),
308                Op::Delete { .. } => {}
309                Op::Insert { new, len } => out.extend_from_slice(&b[new..new + len]),
310            }
311        }
312        out
313    }
314
315    #[test]
316    fn simple_scripts() {
317        let a = [1, 2, 3, 4, 5];
318        let b = [1, 3, 4, 6, 5];
319        let e = diff_ids(&a, &b, 1024);
320        assert_eq!(e.cost(), 2);
321        assert_eq!(apply(&a, &b, &e), b);
322        assert!(!e.coarse);
323    }
324
325    #[test]
326    fn cap_falls_back_to_anchors_then_coarse() {
327        let a: Vec<u32> = (0..200).collect();
328        let b: Vec<u32> = (0..200).map(|i| if i % 2 == 0 { i } else { 1000 + i }).collect();
329        let e = diff_ids(&a, &b, 4);
330        assert!(e.coarse || e.cost() == 200);
331        assert_eq!(apply(&a, &b, &e), b);
332        // with anchors every other element, the pieces fit the cap and the result is minimal
333        assert_eq!(e.cost(), 200);
334        let c: Vec<u32> = (0..50).map(|i| 5000 + i).collect();
335        let e = diff_ids(&a[..50], &c, 4);
336        assert!(e.coarse);
337        assert_eq!(e.cost(), 100);
338    }
339
340    #[test]
341    fn lis_is_increasing() {
342        let pairs = [(0, 5), (1, 1), (2, 2), (3, 9), (4, 3), (5, 4)];
343        assert_eq!(longest_increasing(&pairs), vec![(1, 1), (2, 2), (4, 3), (5, 4)]);
344    }
345}