From d227eb7404d99629259b39c3bd8dfad0c6547133 Mon Sep 17 00:00:00 2001 From: rohit Date: Tue, 3 Nov 2026 09:26:13 +0000 Subject: [PATCH] fix(diff): keep finished boxes when the myers budget runs out When the budgeted search over the whole input ran out of work, solve cleared every mark before splitting the boxes it had left. The boxes it had already finished were marked for good and never searched again, so their changes were lost: the two sides ended up with different counts of unchanged items, and changes() emitted misaligned ranges, some past the end of the old side. diff_texts then underflowed computing the trailing gap. Search::run only marks boxes it finishes, and the boxes it leaves are unmarked and disjoint from them, so the clear was never needed. Drop it, as Anchors::run already does for the regions it re-searches. --- crates/diffy-diff/src/myers.rs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/crates/diffy-diff/src/myers.rs b/crates/diffy-diff/src/myers.rs index 1542d2c4..d7fee4c4 100644 --- a/crates/diffy-diff/src/myers.rs +++ b/crates/diffy-diff/src/myers.rs @@ -244,8 +244,7 @@ fn solve( if search.run(whole, removed, added).is_ok() { return; } - removed.fill(false); - added.fill(false); + // The boxes it finished stay marked; only those it left are split. let left = std::mem::take(&mut search.left); Anchors::new(ids).run(&mut search, left, removed, added, budget); } @@ -827,6 +826,28 @@ mod tests { row[b.len()] } + // Catches a search that runs out of budget after finishing some boxes + // dropping their marks: those boxes are not searched again, so the + // script would misalign the sides. Every budget from none to plenty + // must give a valid script. + #[test] + fn budget_spent_midway_keeps_finished_boxes() { + let a: Vec = [0, 1, 2, 0, 1, 1, 0].repeat(4); + let b: Vec = [2, 1, 0, 1, 0, 2].repeat(4); + for base in 0..1_024 { + let budget = Budget { + per_item: 0, + base, + small: 0, + }; + let changes = diff_with(&a, &b, budget); + let removed: usize = changes.iter().map(|c| c.old.len()).sum(); + let added: usize = changes.iter().map(|c| c.new.len()).sum(); + assert_eq!(a.len() + added, b.len() + removed, "budget {base}"); + assert_eq!(apply(&a, &b, &changes), b, "budget {base}"); + } + } + proptest! { #[test] fn changes_rebuild_new_with_a_minimal_script(