From 224310d7a6a6a27b41ab9d82b060978738ce03da Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 23 Jul 2026 20:47:31 +0900 Subject: [PATCH] Fix nondeterministic highlight positions in FuzzyMatchV2 backtrace Phase 3 writes C rows only from column F[r], but tie-breaking in the backtrace could read row i+1 at column j+1 left of F[i+1]. With reused slab, that cell holds data from previously processed item, so highlight positions in equal-score ties depended on processing order. --- src/algo/algo.go | 6 +++++- src/algo/algo_test.go | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/algo/algo.go b/src/algo/algo.go index 7748a070..2fddcab3 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -630,6 +630,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util. s2 = H[I+j0-1] } + row := i if s > s1 && (s > s2 || s == s2 && preferMatch) { *pos = append(*pos, j+minIdx) if i == 0 { @@ -637,7 +638,10 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util. } i-- } - preferMatch = C[I+j0] > 1 || I+width+j0+1 < len(C) && C[I+width+j0+1] > 0 + // Row below is only written from column F[row+1]; don't read + // stale slab data left of it + preferMatch = C[I+j0] > 1 || + row+1 < M && j < lastIdx && int32(j+1) >= F[row+1] && C[I+width+j0+1] > 0 j-- } } diff --git a/src/algo/algo_test.go b/src/algo/algo_test.go index cd214f5d..45578521 100644 --- a/src/algo/algo_test.go +++ b/src/algo/algo_test.go @@ -218,3 +218,26 @@ func TestLongStringWithNormalize(t *testing.T) { unicodeString := string(bytes) + " MinĂ­mal example" assertMatch2(t, FuzzyMatchV1, false, true, false, unicodeString, "minim", 30001, 30006, 140) } + +func TestResultPositionsWithReusedSlab(t *testing.T) { + // Backtrace positions in equal-score ties must not depend on data + // a previous match left in the slab + pattern := []rune("co/") + target := util.ToChars([]byte("core_color/view/server.txt")) + _, freshPos := FuzzyMatchV2(false, false, true, &target, pattern, true, util.MakeSlab(100*1024, 2048)) + + slab := util.MakeSlab(100*1024, 2048) + dirty := util.ToChars([]byte("completion/keybinding/client/handler/writer_index.txt")) + FuzzyMatchV2(false, false, true, &dirty, pattern, true, slab) + _, reusedPos := FuzzyMatchV2(false, false, true, &target, pattern, true, slab) + + if len(*freshPos) != len(*reusedPos) { + t.Fatalf("position count mismatch: %v vs %v", *freshPos, *reusedPos) + } + for i := range *freshPos { + if (*freshPos)[i] != (*reusedPos)[i] { + t.Errorf("positions differ with reused slab: %v vs %v", *freshPos, *reusedPos) + break + } + } +}