From 465837f3ada01be75e98a40abdf689418cfedc93 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 8 Aug 2026 19:10:37 +0900 Subject: [PATCH] Skip the normalization map for runes that cannot normalize normalizeRune guarded with 0x00C0..0xFF61, which does not exclude Hangul, CJK or Cyrillic, so every rune of those scripts hashed into the map only to miss. Every key of the map folds to ASCII, so the bitmap added for the rune prefilter rejects them without a lookup. - Non-ASCII queries 1.21x where every line is CJK, 1.05x on mostly-ASCII paths behind a Hangul prefix - ASCII queries unchanged, the prefilter already skips Phase 2 for them - Normalization share of query time for a non-ASCII query: 17.2% -> 0% --- src/algo/algo.go | 4 +++- src/algo/runeprefilter_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/algo/algo.go b/src/algo/algo.go index 45433c03..c3a4ca7d 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -303,7 +303,9 @@ func bonusAt(input *util.Chars, idx int) int16 { } func normalizeRune(r rune) rune { - if r < 0x00C0 || r > 0xFF61 { + // Every key of the map folds to ASCII, so a rune the bitmap rejects cannot + // be in it. TestNormalizedKeysAreFlagged pins that. + if !util.MayFoldToAscii(r) { return r } diff --git a/src/algo/runeprefilter_test.go b/src/algo/runeprefilter_test.go index 8277eb40..59c2d283 100644 --- a/src/algo/runeprefilter_test.go +++ b/src/algo/runeprefilter_test.go @@ -226,6 +226,35 @@ func TestRunePrefilterEquivalenceV1(t *testing.T) { disableRunePrefilter = false } +// normalizeRune skips the map when util.MayFoldToAscii rejects the rune. That +// is only sound if every key of the map is flagged, since a flagged-false rune +// is returned unchanged. +func TestNormalizedKeysAreFlagged(t *testing.T) { + for k := range normalized { + if !util.MayFoldToAscii(k) { + t.Errorf("normalized key U+%04X (%c) is not flagged by MayFoldToAscii", k, k) + } + } +} + +// Guarding normalizeRune must not change what it returns, for any rune. +func TestNormalizeRuneUnchangedByGuard(t *testing.T) { + for r := rune(0); r <= unicode.MaxRune; r++ { + if r >= 0xD800 && r <= 0xDFFF { + continue + } + exp := r + if r >= 0x00C0 && r <= 0xFF61 { + if n := normalized[r]; n > 0 { + exp = n + } + } + if got := normalizeRune(r); got != exp { + t.Fatalf("normalizeRune(U+%04X) = U+%04X, expected U+%04X", r, got, exp) + } + } +} + // preparePattern mirrors what pattern.go guarantees the algo functions: // lowercased when case-insensitive, normalized when normalize is on. func preparePattern(pat string, caseSensitive, normalize bool) []rune {