Skip to main content

rucc_headers/
diff.rs

1//! Lining two releases of the same header up, so the merge can see what moved and what did not.
2//!
3//! The merge needs, for two sequences of code lines, the pairs that are the same line. What it does
4//! with them is in `merge.rs`; this is only the lining up, and it is the part where an easy
5//! implementation is too slow on the files that matter. `elf.h` is four thousand lines and changes
6//! at every release, and the textbook table of one cell per pair of lines is sixteen million cells
7//! for one file and one release, eight times over.
8//!
9//! So three steps, cheapest first, which is what every diff worth using does:
10//!
11//! 1. The common prefix and the common suffix. Two releases of a header agree about almost all of
12//!    it, and agreeing at the ends is free to notice.
13//! 2. The lines that appear exactly once in each of what is left. Those are anchors: a line that is
14//!    unique on both sides and in increasing order on both sides cannot be anything but itself.
15//!    This is Bram Cohen's patience diff, and the reason it is right for header text rather than
16//!    merely fast is that it refuses to match the eighth `#endif` with the third one.
17//! 3. The table, for what is left between two anchors, which after the first two steps is small.
18//!    A region with no unique line at all and more cells than the cap is left unmatched, which
19//!    makes the merge write that region out per release. That is coarser and never wrong.
20//!
21//! Every step narrows the problem and no step can match two lines that are not equal, so the worst
22//! this can do is a bigger tree than necessary.
23
24/// The most cells the table is allowed, which is four megabytes of `u32`.
25///
26/// A region this big with no line unique on both sides is not a header that moved, it is two
27/// different files, and merging those line by line produces something nobody can review.
28const CELLS: usize = 1 << 20;
29
30/// The pairs of positions that hold the same line, in increasing order on both sides.
31pub fn aligned(a: &[&str], b: &[&str]) -> Vec<(usize, usize)> {
32    let mut out = Vec::new();
33    let mut work = vec![(0, a.len(), 0, b.len())];
34    while let Some((mut a0, mut a1, mut b0, mut b1)) = work.pop() {
35        while a0 < a1 && b0 < b1 && a[a0] == b[b0] {
36            out.push((a0, b0));
37            a0 += 1;
38            b0 += 1;
39        }
40        while a1 > a0 && b1 > b0 && a[a1 - 1] == b[b1 - 1] {
41            a1 -= 1;
42            b1 -= 1;
43            out.push((a1, b1));
44        }
45        if a0 >= a1 || b0 >= b1 {
46            continue;
47        }
48        let anchors = anchors(&a[a0..a1], &b[b0..b1]);
49        if anchors.is_empty() {
50            if (a1 - a0).saturating_mul(b1 - b0) <= CELLS {
51                out.extend(
52                    table(&a[a0..a1], &b[b0..b1]).into_iter().map(|(x, y)| (a0 + x, b0 + y)),
53                );
54            }
55            continue;
56        }
57        let (mut at, mut bt) = (a0, b0);
58        for (x, y) in anchors {
59            let (ax, by) = (a0 + x, b0 + y);
60            work.push((at, ax, bt, by));
61            out.push((ax, by));
62            at = ax + 1;
63            bt = by + 1;
64        }
65        work.push((at, a1, bt, b1));
66    }
67    out.sort_unstable();
68    out
69}
70
71/// The lines that appear exactly once on each side, in an order both sides agree about.
72fn anchors(a: &[&str], b: &[&str]) -> Vec<(usize, usize)> {
73    let once_in_a = once(a);
74    let once_in_b = once(b);
75    let mut pairs: Vec<(usize, usize)> = Vec::new();
76    for (x, line) in a.iter().enumerate() {
77        if once_in_a.get(line) == Some(&Some(x)) {
78            if let Some(&Some(y)) = once_in_b.get(line) {
79                pairs.push((x, y));
80            }
81        }
82    }
83    increasing(&pairs)
84}
85
86/// Where each line is, for the lines that are there once, and `None` for the rest.
87fn once<'a>(lines: &[&'a str]) -> std::collections::HashMap<&'a str, Option<usize>> {
88    let mut seen: std::collections::HashMap<&str, Option<usize>> = std::collections::HashMap::new();
89    for (at, line) in lines.iter().enumerate() {
90        seen.entry(line).and_modify(|e| *e = None).or_insert(Some(at));
91    }
92    seen
93}
94
95/// The longest run of pairs that increases on the second side as well as the first.
96///
97/// Patience sorting, with a back pointer per pair, which is the standard way and is here because
98/// two lines unique on both sides can still have swapped places between releases, and taking both
99/// of them would claim an order the file does not have.
100fn increasing(pairs: &[(usize, usize)]) -> Vec<(usize, usize)> {
101    if pairs.is_empty() {
102        return Vec::new();
103    }
104    // `piles[k]` is the index into `pairs` of the smallest second element ending a run of k + 1.
105    let mut piles: Vec<usize> = Vec::new();
106    let mut came_from: Vec<Option<usize>> = vec![None; pairs.len()];
107    for (at, &(_, y)) in pairs.iter().enumerate() {
108        let pile = piles.partition_point(|&p| pairs[p].1 < y);
109        came_from[at] = if pile == 0 { None } else { Some(piles[pile - 1]) };
110        if pile == piles.len() {
111            piles.push(at);
112        } else {
113            piles[pile] = at;
114        }
115    }
116    let mut run = Vec::with_capacity(piles.len());
117    let mut at = piles.last().copied();
118    while let Some(i) = at {
119        run.push(pairs[i]);
120        at = came_from[i];
121    }
122    run.reverse();
123    run
124}
125
126/// The longest common subsequence of two short sequences, by the table.
127fn table(a: &[&str], b: &[&str]) -> Vec<(usize, usize)> {
128    let (rows, cols) = (a.len() + 1, b.len() + 1);
129    let mut best = vec![0u32; rows * cols];
130    for x in (0..a.len()).rev() {
131        for y in (0..b.len()).rev() {
132            best[x * cols + y] = if a[x] == b[y] {
133                best[(x + 1) * cols + y + 1] + 1
134            } else {
135                best[(x + 1) * cols + y].max(best[x * cols + y + 1])
136            };
137        }
138    }
139    let mut out = Vec::new();
140    let (mut x, mut y) = (0, 0);
141    while x < a.len() && y < b.len() {
142        if a[x] == b[y] {
143            out.push((x, y));
144            x += 1;
145            y += 1;
146        } else if best[(x + 1) * cols + y] >= best[x * cols + y + 1] {
147            x += 1;
148        } else {
149            y += 1;
150        }
151    }
152    out
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    fn lines(text: &str) -> Vec<&str> {
160        text.split_whitespace().collect()
161    }
162
163    /// The pairs are in increasing order on both sides and every pair is two equal lines, which is
164    /// what the merge assumes and is the only thing that makes the output legal rather than small.
165    fn sound(a: &[&str], b: &[&str], pairs: &[(usize, usize)]) {
166        for (n, &(x, y)) in pairs.iter().enumerate() {
167            assert_eq!(a[x], b[y], "pair {n} is not two equal lines");
168            if n > 0 {
169                assert!(pairs[n - 1].0 < x && pairs[n - 1].1 < y, "pair {n} goes backwards");
170            }
171        }
172    }
173
174    #[test]
175    fn two_equal_files_line_up_entirely() {
176        let (a, b) = (lines("one two three"), lines("one two three"));
177        let pairs = aligned(&a, &b);
178        sound(&a, &b, &pairs);
179        assert_eq!(pairs, vec![(0, 0), (1, 1), (2, 2)]);
180    }
181
182    #[test]
183    fn a_line_added_in_the_middle_is_the_only_thing_unmatched() {
184        let (a, b) = (lines("one two five"), lines("one two three five"));
185        let pairs = aligned(&a, &b);
186        sound(&a, &b, &pairs);
187        assert_eq!(pairs, vec![(0, 0), (1, 1), (2, 3)]);
188    }
189
190    #[test]
191    fn nothing_in_common_matches_nothing() {
192        let (a, b) = (lines("one two"), lines("three four"));
193        assert_eq!(aligned(&a, &b), Vec::new());
194    }
195
196    #[test]
197    fn an_empty_side_matches_nothing() {
198        assert_eq!(aligned(&lines("one"), &[]), Vec::new());
199        assert_eq!(aligned(&[], &lines("one")), Vec::new());
200    }
201
202    /// The case patience diff is for: the repeated line is not an anchor, so the unique ones
203    /// decide, and the `#endif` that matches is the one in the same place rather than the first.
204    #[test]
205    fn a_repeated_line_does_not_drag_the_alignment_out_of_order() {
206        let a = lines("#if A x #endif #if B y #endif");
207        let b = lines("#if B y #endif");
208        let pairs = aligned(&a, &b);
209        sound(&a, &b, &pairs);
210        let matched: Vec<&str> = pairs.iter().map(|&(x, _)| a[x]).collect();
211        assert_eq!(matched, vec!["#if", "B", "y", "#endif"]);
212    }
213
214    /// Two lines that swapped places cannot both be matched, or the pairs would not increase.
215    #[test]
216    fn a_swap_keeps_one_of_the_two() {
217        let (a, b) = (lines("head one two tail"), lines("head two one tail"));
218        let pairs = aligned(&a, &b);
219        sound(&a, &b, &pairs);
220        assert_eq!(pairs.len(), 3);
221    }
222
223    #[test]
224    fn a_file_that_grew_at_both_ends_keeps_its_middle() {
225        let (a, b) = (lines("middle"), lines("before middle after"));
226        let pairs = aligned(&a, &b);
227        sound(&a, &b, &pairs);
228        assert_eq!(pairs, vec![(0, 1)]);
229    }
230
231    /// Long enough that the table is not what did the work, with a change in the middle.
232    #[test]
233    fn a_long_file_with_one_change_in_the_middle() {
234        let left: Vec<String> = (0..5000).map(|n| format!("line {n}")).collect();
235        let mut right = left.clone();
236        right[2500] = "line changed".to_owned();
237        let a: Vec<&str> = left.iter().map(String::as_str).collect();
238        let b: Vec<&str> = right.iter().map(String::as_str).collect();
239        let pairs = aligned(&a, &b);
240        sound(&a, &b, &pairs);
241        assert_eq!(pairs.len(), 4999);
242    }
243
244    /// A region with no line unique on both sides and more cells than the cap is left alone, which
245    /// the merge turns into one branch per release rather than into a guess.
246    #[test]
247    fn a_region_past_the_cap_with_no_anchor_is_left_unmatched() {
248        let side: Vec<String> = (0..2000).map(|n| format!("{}", n % 2)).collect();
249        let other: Vec<String> = (0..2000).map(|n| format!("{}", (n + 1) % 2)).collect();
250        let a: Vec<&str> = side.iter().map(String::as_str).collect();
251        let b: Vec<&str> = other.iter().map(String::as_str).collect();
252        assert!(a.len() * b.len() > CELLS);
253        assert_eq!(aligned(&a, &b), Vec::new());
254    }
255}