stride-align 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/NOTICE +255 -0
  4. data/README.md +107 -0
  5. data/data/bmpm_data/gen_approx_any.txt +131 -0
  6. data/data/bmpm_data/gen_approx_arabic.txt +26 -0
  7. data/data/bmpm_data/gen_approx_common.txt +233 -0
  8. data/data/bmpm_data/gen_approx_cyrillic.txt +18 -0
  9. data/data/bmpm_data/gen_approx_czech.txt +18 -0
  10. data/data/bmpm_data/gen_approx_dutch.txt +18 -0
  11. data/data/bmpm_data/gen_approx_english.txt +47 -0
  12. data/data/bmpm_data/gen_approx_french.txt +25 -0
  13. data/data/bmpm_data/gen_approx_german.txt +73 -0
  14. data/data/bmpm_data/gen_approx_greek.txt +18 -0
  15. data/data/bmpm_data/gen_approx_greeklatin.txt +20 -0
  16. data/data/bmpm_data/gen_approx_hebrew.txt +18 -0
  17. data/data/bmpm_data/gen_approx_hungarian.txt +18 -0
  18. data/data/bmpm_data/gen_approx_italian.txt +18 -0
  19. data/data/bmpm_data/gen_approx_polish.txt +84 -0
  20. data/data/bmpm_data/gen_approx_portuguese.txt +18 -0
  21. data/data/bmpm_data/gen_approx_romanian.txt +18 -0
  22. data/data/bmpm_data/gen_approx_russian.txt +48 -0
  23. data/data/bmpm_data/gen_approx_spanish.txt +21 -0
  24. data/data/bmpm_data/gen_approx_turkish.txt +18 -0
  25. data/data/bmpm_data/gen_exact_any.txt +40 -0
  26. data/data/bmpm_data/gen_exact_approx_common.txt +79 -0
  27. data/data/bmpm_data/gen_exact_arabic.txt +18 -0
  28. data/data/bmpm_data/gen_exact_common.txt +32 -0
  29. data/data/bmpm_data/gen_exact_cyrillic.txt +18 -0
  30. data/data/bmpm_data/gen_exact_czech.txt +18 -0
  31. data/data/bmpm_data/gen_exact_dutch.txt +18 -0
  32. data/data/bmpm_data/gen_exact_english.txt +18 -0
  33. data/data/bmpm_data/gen_exact_french.txt +18 -0
  34. data/data/bmpm_data/gen_exact_german.txt +18 -0
  35. data/data/bmpm_data/gen_exact_greek.txt +18 -0
  36. data/data/bmpm_data/gen_exact_greeklatin.txt +18 -0
  37. data/data/bmpm_data/gen_exact_hebrew.txt +18 -0
  38. data/data/bmpm_data/gen_exact_hungarian.txt +18 -0
  39. data/data/bmpm_data/gen_exact_italian.txt +18 -0
  40. data/data/bmpm_data/gen_exact_polish.txt +23 -0
  41. data/data/bmpm_data/gen_exact_portuguese.txt +18 -0
  42. data/data/bmpm_data/gen_exact_romanian.txt +18 -0
  43. data/data/bmpm_data/gen_exact_russian.txt +19 -0
  44. data/data/bmpm_data/gen_exact_spanish.txt +19 -0
  45. data/data/bmpm_data/gen_exact_turkish.txt +18 -0
  46. data/data/bmpm_data/gen_hebrew_common.txt +113 -0
  47. data/data/bmpm_data/gen_lang.txt +295 -0
  48. data/data/bmpm_data/gen_languages.txt +36 -0
  49. data/data/bmpm_data/gen_rules_any.txt +367 -0
  50. data/data/bmpm_data/gen_rules_arabic.txt +76 -0
  51. data/data/bmpm_data/gen_rules_cyrillic.txt +99 -0
  52. data/data/bmpm_data/gen_rules_czech.txt +67 -0
  53. data/data/bmpm_data/gen_rules_dutch.txt +78 -0
  54. data/data/bmpm_data/gen_rules_english.txt +113 -0
  55. data/data/bmpm_data/gen_rules_french.txt +114 -0
  56. data/data/bmpm_data/gen_rules_german.txt +129 -0
  57. data/data/bmpm_data/gen_rules_greek.txt +97 -0
  58. data/data/bmpm_data/gen_rules_greeklatin.txt +118 -0
  59. data/data/bmpm_data/gen_rules_hebrew.txt +62 -0
  60. data/data/bmpm_data/gen_rules_hungarian.txt +83 -0
  61. data/data/bmpm_data/gen_rules_italian.txt +77 -0
  62. data/data/bmpm_data/gen_rules_polish.txt +185 -0
  63. data/data/bmpm_data/gen_rules_portuguese.txt +105 -0
  64. data/data/bmpm_data/gen_rules_romanian.txt +64 -0
  65. data/data/bmpm_data/gen_rules_russian.txt +142 -0
  66. data/data/bmpm_data/gen_rules_spanish.txt +85 -0
  67. data/data/bmpm_data/gen_rules_turkish.txt +50 -0
  68. data/data/keyboard_data/qwerty.npy +0 -0
  69. data/data/matrix_data/BLOSUM100 +31 -0
  70. data/data/matrix_data/BLOSUM30 +31 -0
  71. data/data/matrix_data/BLOSUM35 +31 -0
  72. data/data/matrix_data/BLOSUM40 +31 -0
  73. data/data/matrix_data/BLOSUM45 +25 -0
  74. data/data/matrix_data/BLOSUM50 +25 -0
  75. data/data/matrix_data/BLOSUM55 +31 -0
  76. data/data/matrix_data/BLOSUM60 +31 -0
  77. data/data/matrix_data/BLOSUM62 +25 -0
  78. data/data/matrix_data/BLOSUM65 +31 -0
  79. data/data/matrix_data/BLOSUM70 +31 -0
  80. data/data/matrix_data/BLOSUM75 +31 -0
  81. data/data/matrix_data/BLOSUM80 +25 -0
  82. data/data/matrix_data/BLOSUM85 +31 -0
  83. data/data/matrix_data/BLOSUM90 +25 -0
  84. data/data/matrix_data/NUC.4.4 +25 -0
  85. data/data/matrix_data/PAM10 +34 -0
  86. data/data/matrix_data/PAM100 +34 -0
  87. data/data/matrix_data/PAM110 +34 -0
  88. data/data/matrix_data/PAM120 +34 -0
  89. data/data/matrix_data/PAM130 +34 -0
  90. data/data/matrix_data/PAM140 +34 -0
  91. data/data/matrix_data/PAM150 +34 -0
  92. data/data/matrix_data/PAM160 +34 -0
  93. data/data/matrix_data/PAM170 +34 -0
  94. data/data/matrix_data/PAM180 +34 -0
  95. data/data/matrix_data/PAM190 +34 -0
  96. data/data/matrix_data/PAM20 +34 -0
  97. data/data/matrix_data/PAM200 +34 -0
  98. data/data/matrix_data/PAM210 +34 -0
  99. data/data/matrix_data/PAM220 +34 -0
  100. data/data/matrix_data/PAM230 +34 -0
  101. data/data/matrix_data/PAM240 +34 -0
  102. data/data/matrix_data/PAM250 +25 -0
  103. data/data/matrix_data/PAM260 +34 -0
  104. data/data/matrix_data/PAM270 +34 -0
  105. data/data/matrix_data/PAM280 +34 -0
  106. data/data/matrix_data/PAM290 +34 -0
  107. data/data/matrix_data/PAM30 +25 -0
  108. data/data/matrix_data/PAM300 +34 -0
  109. data/data/matrix_data/PAM310 +34 -0
  110. data/data/matrix_data/PAM320 +34 -0
  111. data/data/matrix_data/PAM330 +34 -0
  112. data/data/matrix_data/PAM340 +34 -0
  113. data/data/matrix_data/PAM350 +34 -0
  114. data/data/matrix_data/PAM360 +34 -0
  115. data/data/matrix_data/PAM370 +34 -0
  116. data/data/matrix_data/PAM380 +34 -0
  117. data/data/matrix_data/PAM390 +34 -0
  118. data/data/matrix_data/PAM40 +34 -0
  119. data/data/matrix_data/PAM400 +34 -0
  120. data/data/matrix_data/PAM410 +34 -0
  121. data/data/matrix_data/PAM420 +34 -0
  122. data/data/matrix_data/PAM430 +34 -0
  123. data/data/matrix_data/PAM440 +34 -0
  124. data/data/matrix_data/PAM450 +34 -0
  125. data/data/matrix_data/PAM460 +34 -0
  126. data/data/matrix_data/PAM470 +34 -0
  127. data/data/matrix_data/PAM480 +34 -0
  128. data/data/matrix_data/PAM490 +34 -0
  129. data/data/matrix_data/PAM50 +34 -0
  130. data/data/matrix_data/PAM500 +34 -0
  131. data/data/matrix_data/PAM60 +34 -0
  132. data/data/matrix_data/PAM70 +25 -0
  133. data/data/matrix_data/PAM80 +34 -0
  134. data/data/matrix_data/PAM90 +34 -0
  135. data/ext/stride_align/backend_avx2.cpp +2 -0
  136. data/ext/stride_align/backend_avx512bwvl.cpp +2 -0
  137. data/ext/stride_align/backend_generic.cpp +3 -0
  138. data/ext/stride_align/backend_impl.hpp +983 -0
  139. data/ext/stride_align/backend_lasx.cpp +2 -0
  140. data/ext/stride_align/backend_lsx.cpp +2 -0
  141. data/ext/stride_align/backend_neon.cpp +2 -0
  142. data/ext/stride_align/backend_rvv.cpp +2 -0
  143. data/ext/stride_align/backend_sse41.cpp +2 -0
  144. data/ext/stride_align/backend_sve.cpp +2 -0
  145. data/ext/stride_align/backend_sve2.cpp +2 -0
  146. data/ext/stride_align/backend_vsx.cpp +2 -0
  147. data/ext/stride_align/beider_morse_impl.cpp +5 -0
  148. data/ext/stride_align/cpu_detect.cpp +172 -0
  149. data/ext/stride_align/cpu_detect.hpp +6 -0
  150. data/ext/stride_align/extconf.rb +114 -0
  151. data/ext/stride_align/target_profile.hpp +82 -0
  152. data/ext/stride_align/vendor/beider_morse_impl.cpp +1467 -0
  153. data/ext/stride_align/vendor/stride_align/alignment.hpp +199 -0
  154. data/ext/stride_align/vendor/stride_align/batch.hpp +812 -0
  155. data/ext/stride_align/vendor/stride_align/beider_morse.hpp +121 -0
  156. data/ext/stride_align/vendor/stride_align/caverphone.hpp +222 -0
  157. data/ext/stride_align/vendor/stride_align/cologne_phonetic.hpp +202 -0
  158. data/ext/stride_align/vendor/stride_align/core.hpp +731 -0
  159. data/ext/stride_align/vendor/stride_align/daitch_mokotoff.hpp +631 -0
  160. data/ext/stride_align/vendor/stride_align/double_metaphone.hpp +796 -0
  161. data/ext/stride_align/vendor/stride_align/dtw.hpp +300 -0
  162. data/ext/stride_align/vendor/stride_align/encoded.hpp +235 -0
  163. data/ext/stride_align/vendor/stride_align/hamming.hpp +55 -0
  164. data/ext/stride_align/vendor/stride_align/indel.hpp +1200 -0
  165. data/ext/stride_align/vendor/stride_align/jaro.hpp +517 -0
  166. data/ext/stride_align/vendor/stride_align/lcs.hpp +159 -0
  167. data/ext/stride_align/vendor/stride_align/levenshtein.hpp +1247 -0
  168. data/ext/stride_align/vendor/stride_align/levenshtein_prepared.hpp +193 -0
  169. data/ext/stride_align/vendor/stride_align/match_rating.hpp +168 -0
  170. data/ext/stride_align/vendor/stride_align/metaphone.hpp +291 -0
  171. data/ext/stride_align/vendor/stride_align/ngram.hpp +176 -0
  172. data/ext/stride_align/vendor/stride_align/nysiis.hpp +199 -0
  173. data/ext/stride_align/vendor/stride_align/pairwise_alignment.hpp +465 -0
  174. data/ext/stride_align/vendor/stride_align/partial_ratio.hpp +486 -0
  175. data/ext/stride_align/vendor/stride_align/ratcliff_obershelp.hpp +101 -0
  176. data/ext/stride_align/vendor/stride_align/soundex.hpp +108 -0
  177. data/ext/stride_align/vendor/stride_align/token_ratios.hpp +445 -0
  178. data/ext/stride_align/vendor/stride_align/types.hpp +16 -0
  179. data/ext/stride_align/vendor/stride_align/utf8.hpp +512 -0
  180. data/ext/stride_align/vendor/stride_align/wratio.hpp +363 -0
  181. data/lib/stride_align/algorithms.rb +296 -0
  182. data/lib/stride_align/alignment_path.rb +217 -0
  183. data/lib/stride_align/backend.rb +47 -0
  184. data/lib/stride_align/batch.rb +705 -0
  185. data/lib/stride_align/core.rb +180 -0
  186. data/lib/stride_align/keyboard.rb +200 -0
  187. data/lib/stride_align/matrices.rb +403 -0
  188. data/lib/stride_align/version.rb +5 -0
  189. data/lib/stride_align.rb +87 -0
  190. metadata +231 -0
@@ -0,0 +1,1200 @@
1
+ #pragma once
2
+
3
+ // Indel distance — Levenshtein restricted to insertions and deletions
4
+ // (no substitutions). Equivalently:
5
+ //
6
+ // indel(a, b) = |a| + |b| - 2 * LCS(a, b)
7
+ //
8
+ // Normalized similarity:
9
+ //
10
+ // sim(a, b) = 1 - indel(a, b) / (|a| + |b|)
11
+ // = 2 * LCS(a, b) / (|a| + |b|)
12
+ //
13
+ // The bit-parallel kernel uses the Allison-Dix (1986) LCS recurrence:
14
+ // V starts as the all-ones m-bit vector.
15
+ // For each text character c:
16
+ // U = V & PEQ[c]
17
+ // V = ((V + U) | (V - U)) & MASK
18
+ // LCS = m - popcount(V).
19
+ //
20
+ // Hyyrö (2004) gives a cleaner derivation and the multi-word
21
+ // generalization; rapidfuzz uses the same recurrence.
22
+
23
+ #include <algorithm>
24
+ #include <array>
25
+ #include <bit>
26
+ #include <cstddef>
27
+ #include <cstdint>
28
+ #include <limits>
29
+ #include <span>
30
+ #include <type_traits>
31
+ #include <unordered_map>
32
+ #include <vector>
33
+
34
+ #include "stride_align/alignment.hpp"
35
+
36
+ namespace stride_align::indel {
37
+
38
+ inline constexpr std::size_t kNoCutoff = std::numeric_limits<std::size_t>::max();
39
+
40
+ // Scalar DP reference. O(m*n) time, O(m) space (rolling rows).
41
+ // Correctness oracle for the bit-parallel paths.
42
+ template <typename Token>
43
+ inline std::size_t indel_dp(
44
+ std::span<const Token> pattern,
45
+ std::span<const Token> text) {
46
+ const std::size_t m = pattern.size();
47
+ const std::size_t n = text.size();
48
+ if (m == 0U) return n;
49
+ if (n == 0U) return m;
50
+
51
+ std::vector<std::size_t> prev(m + 1U);
52
+ std::vector<std::size_t> curr(m + 1U);
53
+ for (std::size_t i = 0; i <= m; ++i) {
54
+ prev[i] = i;
55
+ }
56
+ for (std::size_t j = 1; j <= n; ++j) {
57
+ curr[0] = j;
58
+ for (std::size_t i = 1; i <= m; ++i) {
59
+ if (pattern[i - 1U] == text[j - 1U]) {
60
+ curr[i] = prev[i - 1U];
61
+ } else {
62
+ // No substitution: only insertion or deletion.
63
+ curr[i] = std::min(prev[i] + 1U, curr[i - 1U] + 1U);
64
+ }
65
+ }
66
+ std::swap(prev, curr);
67
+ }
68
+ return prev[m];
69
+ }
70
+
71
+ // Bit-parallel single-word indel for uint8 patterns of length <= 64.
72
+ //
73
+ // ``cutoff`` is the rapidfuzz-style score-cutoff: if the caller knows
74
+ // the result will only be used when distance <= cutoff, the kernel
75
+ // bails out of the per-character loop when its lower-bound estimate
76
+ // of the final distance exceeds the cutoff, returning ``cutoff + 1``
77
+ // (any value strictly greater than the cutoff carries the same
78
+ // "doesn't qualify" signal). The lower-bound formula: after
79
+ // processing ``j`` of ``n`` text chars with ``lcs_so_far = m -
80
+ // popcount(V)``, the final LCS can grow by at most ``n - j`` (each
81
+ // remaining text char can match at most one unused pattern slot), so
82
+ // the lower bound on final indel is
83
+ // m + n - 2·(lcs_so_far + (n - j)) = m - n + 2j - 2·lcs_so_far.
84
+ // Equivalently the bail condition simplifies to
85
+ // ``2*(j + popcount(V)) < m + n - cutoff``.
86
+ inline std::size_t indel_single_word_u8(
87
+ std::span<const std::uint8_t> pattern,
88
+ std::span<const std::uint8_t> text,
89
+ std::size_t cutoff = kNoCutoff) noexcept {
90
+ const std::size_t m = pattern.size();
91
+ const std::size_t n = text.size();
92
+ if (m == 0U) return n;
93
+ if (n == 0U) return m;
94
+ // indel = m + n - 2·LCS ≥ |m - n|. Bail before PEQ build when a
95
+ // finite cutoff already rules the pair out.
96
+ if (cutoff != kNoCutoff) {
97
+ const std::size_t len_diff = m > n ? m - n : n - m;
98
+ if (len_diff > cutoff) return cutoff + 1U;
99
+ }
100
+
101
+ std::uint64_t peq[256] = {0};
102
+ const std::uint64_t one = 1U;
103
+ for (std::size_t i = 0; i < m; ++i) {
104
+ peq[pattern[i]] |= one << i;
105
+ }
106
+
107
+ const std::uint64_t mask =
108
+ (m == 64U) ? ~std::uint64_t{0} : ((one << m) - 1U);
109
+ std::uint64_t V = mask;
110
+
111
+ // Dual loops: the common no-cutoff path skips per-char popcount.
112
+ if (cutoff == kNoCutoff) {
113
+ for (const std::uint8_t c : text) {
114
+ const std::uint64_t U = V & peq[c];
115
+ V = ((V + U) | (V - U)) & mask;
116
+ }
117
+ } else {
118
+ // Pre-compute the bail threshold once. The lower bound on final
119
+ // indel after processing ``j`` of ``n`` text chars is
120
+ // 2*(j + popcount(V)) - m - n
121
+ // (derivation: final LCS ≤ (m - popcount(V)) + (n - j), so final
122
+ // indel = m + n - 2·final_lcs ≥ 2j + 2·popcount(V) - m - n).
123
+ // Bail when this lower bound exceeds the cutoff, i.e. when
124
+ // 2*(j + popcount(V)) > m + n + cutoff.
125
+ const std::size_t bail_threshold = m + n + cutoff;
126
+ std::size_t j = 0;
127
+ for (const std::uint8_t c : text) {
128
+ const std::uint64_t U = V & peq[c];
129
+ V = ((V + U) | (V - U)) & mask;
130
+ ++j;
131
+ const std::size_t lower_bound_score =
132
+ 2U * (j + static_cast<std::size_t>(std::popcount(V)));
133
+ if (lower_bound_score > bail_threshold) {
134
+ return cutoff + 1U;
135
+ }
136
+ }
137
+ }
138
+ const std::size_t lcs =
139
+ m - static_cast<std::size_t>(std::popcount(V));
140
+ return m + n - 2U * lcs;
141
+ }
142
+
143
+ // Forward declarations: the dispatch helpers above and the
144
+ // templated ``indel_distance`` below need these visible before the
145
+ // concrete definitions further down in the file.
146
+ inline std::size_t indel_distance_k2_u8(
147
+ std::span<const std::uint8_t> pattern,
148
+ std::span<const std::uint8_t> text,
149
+ std::size_t cutoff = kNoCutoff) noexcept;
150
+ inline std::size_t indel_distance_k3_u8(
151
+ std::span<const std::uint8_t> pattern,
152
+ std::span<const std::uint8_t> text,
153
+ std::size_t cutoff = kNoCutoff) noexcept;
154
+ inline std::size_t indel_distance_k4_u8(
155
+ std::span<const std::uint8_t> pattern,
156
+ std::span<const std::uint8_t> text,
157
+ std::size_t cutoff = kNoCutoff) noexcept;
158
+ inline std::size_t indel_distance_k5_u8(
159
+ std::span<const std::uint8_t> pattern,
160
+ std::span<const std::uint8_t> text,
161
+ std::size_t cutoff = kNoCutoff) noexcept;
162
+ inline std::size_t indel_distance_k6_u8(
163
+ std::span<const std::uint8_t> pattern,
164
+ std::span<const std::uint8_t> text,
165
+ std::size_t cutoff = kNoCutoff) noexcept;
166
+ inline std::size_t indel_distance_k7_u8(
167
+ std::span<const std::uint8_t> pattern,
168
+ std::span<const std::uint8_t> text,
169
+ std::size_t cutoff = kNoCutoff) noexcept;
170
+ inline std::size_t indel_distance_k8_u8(
171
+ std::span<const std::uint8_t> pattern,
172
+ std::span<const std::uint8_t> text,
173
+ std::size_t cutoff = kNoCutoff) noexcept;
174
+
175
+ inline std::size_t indel_distance_multi_word_u8(
176
+ std::span<const std::uint8_t> pattern,
177
+ std::span<const std::uint8_t> text,
178
+ std::size_t cutoff = kNoCutoff);
179
+
180
+ template <typename Token>
181
+ inline std::size_t indel_distance_multi_word(
182
+ std::span<const Token> pattern,
183
+ std::span<const Token> text,
184
+ std::size_t cutoff = kNoCutoff);
185
+
186
+ // Token-generic bit-parallel for patterns of any length. Patterns of
187
+ // up to 64 elements use the cheap single-word path; longer patterns
188
+ // fall through to the multi-word Hyyrö generalisation. Same cutoff
189
+ // semantics as ``indel_single_word_u8``.
190
+ template <typename Token>
191
+ inline std::size_t indel_distance(
192
+ std::span<const Token> pattern,
193
+ std::span<const Token> text,
194
+ std::size_t cutoff = kNoCutoff) {
195
+ static_assert(std::is_integral_v<Token> || std::is_unsigned_v<Token>);
196
+ const std::size_t m = pattern.size();
197
+ const std::size_t n = text.size();
198
+ if (m == 0U) return n;
199
+ if (n == 0U) return m;
200
+ if (cutoff != kNoCutoff) {
201
+ const std::size_t len_diff = m > n ? m - n : n - m;
202
+ if (len_diff > cutoff) return cutoff + 1U;
203
+ }
204
+ if (m > 64U) {
205
+ return indel_distance_multi_word<Token>(pattern, text, cutoff);
206
+ }
207
+
208
+ std::unordered_map<Token, std::uint64_t> peq;
209
+ const std::uint64_t one = 1U;
210
+ for (std::size_t i = 0; i < m; ++i) {
211
+ peq[pattern[i]] |= one << i;
212
+ }
213
+
214
+ const std::uint64_t mask =
215
+ (m == 64U) ? ~std::uint64_t{0} : ((one << m) - 1U);
216
+ std::uint64_t V = mask;
217
+
218
+ if (cutoff == kNoCutoff) {
219
+ for (const auto c : text) {
220
+ auto it = peq.find(c);
221
+ const std::uint64_t pm = (it == peq.end()) ? 0U : it->second;
222
+ const std::uint64_t U = V & pm;
223
+ V = ((V + U) | (V - U)) & mask;
224
+ }
225
+ } else {
226
+ const std::size_t bail_threshold = m + n + cutoff;
227
+ std::size_t j = 0;
228
+ for (const auto c : text) {
229
+ auto it = peq.find(c);
230
+ const std::uint64_t pm = (it == peq.end()) ? 0U : it->second;
231
+ const std::uint64_t U = V & pm;
232
+ V = ((V + U) | (V - U)) & mask;
233
+ ++j;
234
+ const std::size_t lower_bound_score =
235
+ 2U * (j + static_cast<std::size_t>(std::popcount(V)));
236
+ if (lower_bound_score > bail_threshold) {
237
+ return cutoff + 1U;
238
+ }
239
+ }
240
+ }
241
+ const std::size_t lcs =
242
+ m - static_cast<std::size_t>(std::popcount(V));
243
+ return m + n - 2U * lcs;
244
+ }
245
+
246
+ // Convenience dispatch for u8 patterns: single-word bit-parallel for
247
+ // short patterns, scalar DP for longer. Cutoff is honoured by the
248
+ // bit-parallel path; the scalar DP path doesn't implement early-
249
+ // exit yet, so a cutoff there is informational only (the full
250
+ // distance is computed and returned).
251
+ inline std::size_t indel_distance_u8(
252
+ std::span<const std::uint8_t> pattern,
253
+ std::span<const std::uint8_t> text,
254
+ std::size_t cutoff = kNoCutoff) {
255
+ const std::size_t m = pattern.size();
256
+ const std::size_t n = text.size();
257
+ if (m == 0U) return n;
258
+ if (n == 0U) return m;
259
+ if (cutoff != kNoCutoff) {
260
+ const std::size_t len_diff = m > n ? m - n : n - m;
261
+ if (len_diff > cutoff) return cutoff + 1U;
262
+ }
263
+ if (m > 0U && m <= 64U) {
264
+ return indel_single_word_u8(pattern, text, cutoff);
265
+ }
266
+ if (m > 64U && m <= 128U) {
267
+ return indel_distance_k2_u8(pattern, text, cutoff);
268
+ }
269
+ if (m > 128U && m <= 192U) {
270
+ return indel_distance_k3_u8(pattern, text, cutoff);
271
+ }
272
+ if (m > 192U && m <= 256U) {
273
+ return indel_distance_k4_u8(pattern, text, cutoff);
274
+ }
275
+ if (m > 256U && m <= 320U) {
276
+ return indel_distance_k5_u8(pattern, text, cutoff);
277
+ }
278
+ if (m > 320U && m <= 384U) {
279
+ return indel_distance_k6_u8(pattern, text, cutoff);
280
+ }
281
+ if (m > 384U && m <= 448U) {
282
+ return indel_distance_k7_u8(pattern, text, cutoff);
283
+ }
284
+ if (m > 448U && m <= 512U) {
285
+ return indel_distance_k8_u8(pattern, text, cutoff);
286
+ }
287
+ return indel_distance_multi_word_u8(pattern, text, cutoff);
288
+ }
289
+
290
+ // Byte-alphabet multi-word Indel. Specialised for ``std::uint8_t``
291
+ // (and the codepoint-fits-in-byte fast path) — the 256-entry PEQ
292
+ // lives in a single contiguous allocation instead of the per-unique-
293
+ // char ``std::vector`` allocations that the generic ``unordered_map``
294
+ // path needs. For long ASCII / Latin-1 inputs this is the difference
295
+ // between a single 16 KB malloc and ~30 separate small allocations,
296
+ // which dominate the per-call cost for long patterns.
297
+ // Per-thread scratch buffers shared across the multi-word u8 kernel.
298
+ // The kernel only ever resizes the buffers upward, so reuse across
299
+ // calls amortises away the heap-allocation cost that was the
300
+ // dominant per-call expense for long patterns.
301
+ //
302
+ // PEQ clearing uses a dirty-symbol list: after the first call we only
303
+ // zero the alphabet rows the previous pattern touched (typically ≪ 256
304
+ // for ASCII), instead of ``fill(256 * K)`` every time.
305
+ struct MultiWordU8Scratch {
306
+ std::vector<std::uint64_t> peq; // 256 * K entries
307
+ std::vector<std::uint64_t> mask; // K entries
308
+ std::vector<std::uint64_t> V; // K entries
309
+ std::vector<std::uint64_t> sum; // K entries
310
+ std::vector<std::uint64_t> diff; // K entries
311
+ std::vector<std::uint64_t> U; // K entries
312
+
313
+ // Dirty-PEQ bookkeeping.
314
+ std::vector<std::uint8_t> peq_dirty;
315
+ std::array<std::uint32_t, 256> peq_touch{};
316
+ std::uint32_t peq_gen = 0;
317
+ std::size_t peq_layout_k = 0;
318
+
319
+ void resize_for(std::size_t K) {
320
+ if (peq.size() < 256U * K) {
321
+ peq.assign(256U * K, 0U);
322
+ peq_dirty.clear();
323
+ peq_layout_k = K;
324
+ peq_touch.fill(0);
325
+ peq_gen = 0;
326
+ }
327
+ if (mask.size() < K) mask.resize(K);
328
+ if (V.size() < K) V.resize(K);
329
+ if (sum.size() < K) sum.resize(K);
330
+ if (diff.size() < K) diff.resize(K);
331
+ if (U.size() < K) U.resize(K);
332
+ }
333
+
334
+ // Prepare peq for a rebuild at layout ``K``. Zeros only previously
335
+ // dirtied symbol rows when the layout is unchanged.
336
+ void peq_begin(std::size_t K) {
337
+ resize_for(K);
338
+ if (peq_layout_k != K) {
339
+ std::fill_n(peq.data(), 256U * K, std::uint64_t{0});
340
+ peq_dirty.clear();
341
+ peq_layout_k = K;
342
+ } else {
343
+ for (const std::uint8_t s : peq_dirty) {
344
+ std::fill_n(
345
+ peq.data() + static_cast<std::size_t>(s) * K, K, std::uint64_t{0});
346
+ }
347
+ peq_dirty.clear();
348
+ }
349
+ if (++peq_gen == 0U) {
350
+ peq_touch.fill(0);
351
+ peq_gen = 1U;
352
+ }
353
+ }
354
+
355
+ void peq_or_bit(std::uint8_t c, std::size_t bit_index, std::size_t K) {
356
+ if (peq_touch[c] != peq_gen) {
357
+ peq_touch[c] = peq_gen;
358
+ peq_dirty.push_back(c);
359
+ }
360
+ peq[static_cast<std::size_t>(c) * K + (bit_index >> 6U)] |=
361
+ std::uint64_t{1} << (bit_index & 63U);
362
+ }
363
+
364
+ void peq_build(std::span<const std::uint8_t> pattern, std::size_t K) {
365
+ peq_begin(K);
366
+ for (std::size_t i = 0; i < pattern.size(); ++i) {
367
+ peq_or_bit(pattern[i], i, K);
368
+ }
369
+ }
370
+ };
371
+
372
+ inline MultiWordU8Scratch& multi_word_u8_scratch() {
373
+ thread_local MultiWordU8Scratch s;
374
+ return s;
375
+ }
376
+
377
+ inline std::size_t indel_distance_multi_word_u8(
378
+ std::span<const std::uint8_t> pattern,
379
+ std::span<const std::uint8_t> text,
380
+ std::size_t cutoff) {
381
+ const std::size_t m = pattern.size();
382
+ const std::size_t n = text.size();
383
+ if (m == 0U) return n;
384
+ if (n == 0U) return m;
385
+ if (cutoff != kNoCutoff) {
386
+ const std::size_t len_diff = m > n ? m - n : n - m;
387
+ if (len_diff > cutoff) return cutoff + 1U;
388
+ }
389
+ const std::size_t K = (m + 63U) / 64U;
390
+ const std::uint64_t one = 1U;
391
+
392
+ MultiWordU8Scratch& scr = multi_word_u8_scratch();
393
+ scr.peq_build(pattern, K);
394
+
395
+ std::uint64_t* const mask = scr.mask.data();
396
+ for (std::size_t k = 0; k < K; ++k) mask[k] = ~std::uint64_t{0};
397
+ if (m % 64U != 0U) {
398
+ mask[K - 1] = (one << (m % 64U)) - 1U;
399
+ }
400
+
401
+ std::uint64_t* const V = scr.V.data();
402
+ for (std::size_t k = 0; k < K; ++k) V[k] = mask[k];
403
+
404
+ const bool has_cutoff = cutoff != kNoCutoff;
405
+ const std::size_t bail_threshold = m + n + (has_cutoff ? cutoff : 0U);
406
+
407
+ std::size_t j = 0;
408
+ for (const std::uint8_t c : text) {
409
+ const std::uint64_t* peq_row = scr.peq.data() + static_cast<std::size_t>(c) * K;
410
+
411
+ // Fused per-block recurrence. The Hyyrö invariant ``U <= V``
412
+ // bitwise means the per-block subtraction ``V - U`` never
413
+ // underflows, so the borrow chain across blocks is identically
414
+ // zero. Only the carry chain on the addition needs to
415
+ // propagate — handled by ``__builtin_add_overflow``.
416
+ std::uint64_t carry_in = 0;
417
+ for (std::size_t k = 0; k < K; ++k) {
418
+ const std::uint64_t Vk = V[k];
419
+ const std::uint64_t Uk = Vk & peq_row[k];
420
+ std::uint64_t s1;
421
+ const bool c1 = __builtin_add_overflow(Vk, Uk, &s1);
422
+ std::uint64_t s2;
423
+ const bool c2 = __builtin_add_overflow(s1, carry_in, &s2);
424
+ carry_in = static_cast<std::uint64_t>(c1) + static_cast<std::uint64_t>(c2);
425
+ V[k] = (s2 | (Vk - Uk)) & mask[k];
426
+ }
427
+
428
+ ++j;
429
+ if (has_cutoff) {
430
+ std::size_t pc = 0;
431
+ for (std::size_t k = 0; k < K; ++k) {
432
+ pc += static_cast<std::size_t>(std::popcount(V[k]));
433
+ }
434
+ const std::size_t lower_bound_score = 2U * (j + pc);
435
+ if (lower_bound_score > bail_threshold) {
436
+ return cutoff + 1U;
437
+ }
438
+ }
439
+ }
440
+
441
+ std::size_t lcs_unmatched = 0;
442
+ for (std::size_t k = 0; k < K; ++k) {
443
+ lcs_unmatched += static_cast<std::size_t>(std::popcount(V[k]));
444
+ }
445
+ const std::size_t lcs = m - lcs_unmatched;
446
+ return m + n - 2U * lcs;
447
+ }
448
+
449
+ // Hand-specialised K = 2 multi-word kernel for patterns 65..128
450
+ // characters. Architectural inspiration: rapidfuzz-cpp's
451
+ // ``lcs_unroll`` template specialisations (MIT, attribution in
452
+ // NOTICE). The two ideas inherited from their design are:
453
+ //
454
+ // 1. Per-block fused single-step Hyyrö recurrence —
455
+ // U[k] = V[k] & PEQ[c][k]
456
+ // sum = V[k] + U[k] + carry_in (carry propagates k → k+1)
457
+ // V[k] = (sum | (V[k] - U[k])) & mask[k]
458
+ // with NO separate borrow chain across blocks. The Hyyrö
459
+ // invariant ``U[k] <= V[k]`` bitwise guarantees that the
460
+ // per-block subtraction never underflows, so the cross-block
461
+ // borrow term is mathematically zero. Eliminates one of the
462
+ // four K-loops a naïve translation of Allison-Dix would emit.
463
+ // 2. State register-pinning. ``V0`` and ``V1`` are local
464
+ // ``uint64_t`` (no array, no intermediate buffers), so the
465
+ // compiler keeps them in registers across the entire text scan.
466
+ //
467
+ // Carry between block 0 and block 1 of the sum chain still needs
468
+ // propagation — handled with ``__builtin_add_overflow``, which the
469
+ // compiler lowers to ADCX/ADOX or equivalent.
470
+ //
471
+ // Correctness vs the generic multi-word kernel is verified on
472
+ // random fuzz (see tests/test_indel.py); the mathematical
473
+ // equivalence reduces to "the borrow chain is identically zero".
474
+ inline std::size_t indel_distance_k2_u8(
475
+ std::span<const std::uint8_t> pattern,
476
+ std::span<const std::uint8_t> text,
477
+ std::size_t cutoff) noexcept {
478
+ const std::size_t m = pattern.size();
479
+ const std::size_t n = text.size();
480
+ if (m == 0U) return n;
481
+ if (n == 0U) return m;
482
+ // Caller guarantees 64 < m <= 128.
483
+ constexpr std::size_t K = 2;
484
+ const std::uint64_t one = 1U;
485
+
486
+ MultiWordU8Scratch& scr = multi_word_u8_scratch();
487
+ scr.peq_build(pattern, K);
488
+
489
+ const std::uint64_t mask0 = ~std::uint64_t{0};
490
+ const std::uint64_t mask1 =
491
+ (m % 64U == 0U) ? ~std::uint64_t{0} : ((one << (m % 64U)) - 1U);
492
+
493
+ std::uint64_t V0 = mask0;
494
+ std::uint64_t V1 = mask1;
495
+
496
+ const std::uint64_t* const peq_base = scr.peq.data();
497
+
498
+ const bool has_cutoff = cutoff != kNoCutoff;
499
+ const std::size_t bail_threshold = m + n + (has_cutoff ? cutoff : 0U);
500
+
501
+ std::size_t j = 0;
502
+ for (const std::uint8_t c : text) {
503
+ const std::uint64_t* peq_row = peq_base + static_cast<std::size_t>(c) * K;
504
+
505
+ // Block 0: no carry in. The per-block fused expression.
506
+ const std::uint64_t U0 = V0 & peq_row[0];
507
+ std::uint64_t sum0;
508
+ const bool carry01 = __builtin_add_overflow(V0, U0, &sum0);
509
+ V0 = (sum0 | (V0 - U0)) & mask0;
510
+
511
+ // Block 1: carry in from block 0; no carry out needed (K = 2).
512
+ const std::uint64_t U1 = V1 & peq_row[1];
513
+ std::uint64_t sum1;
514
+ __builtin_add_overflow(V1, U1, &sum1);
515
+ sum1 += static_cast<std::uint64_t>(carry01);
516
+ V1 = (sum1 | (V1 - U1)) & mask1;
517
+
518
+ ++j;
519
+ if (has_cutoff) {
520
+ const std::size_t pc =
521
+ static_cast<std::size_t>(std::popcount(V0)) +
522
+ static_cast<std::size_t>(std::popcount(V1));
523
+ const std::size_t lower_bound_score = 2U * (j + pc);
524
+ if (lower_bound_score > bail_threshold) {
525
+ return cutoff + 1U;
526
+ }
527
+ }
528
+ }
529
+
530
+ const std::size_t lcs_unmatched =
531
+ static_cast<std::size_t>(std::popcount(V0)) +
532
+ static_cast<std::size_t>(std::popcount(V1));
533
+ const std::size_t lcs = m - lcs_unmatched;
534
+ return m + n - 2U * lcs;
535
+ }
536
+
537
+ // K = 3 hand-specialisation for ``129 <= m <= 192``. Same fused
538
+ // single-step recurrence as K=2, extended to three register-pinned
539
+ // blocks with a two-stage carry chain.
540
+ inline std::size_t indel_distance_k3_u8(
541
+ std::span<const std::uint8_t> pattern,
542
+ std::span<const std::uint8_t> text,
543
+ std::size_t cutoff) noexcept {
544
+ const std::size_t m = pattern.size();
545
+ const std::size_t n = text.size();
546
+ if (m == 0U) return n;
547
+ if (n == 0U) return m;
548
+ // Caller guarantees 128 < m <= 192.
549
+ constexpr std::size_t K = 3;
550
+ const std::uint64_t one = 1U;
551
+
552
+ MultiWordU8Scratch& scr = multi_word_u8_scratch();
553
+ scr.peq_build(pattern, K);
554
+
555
+ const std::uint64_t mask0 = ~std::uint64_t{0};
556
+ const std::uint64_t mask1 = ~std::uint64_t{0};
557
+ const std::uint64_t mask2 =
558
+ (m % 64U == 0U) ? ~std::uint64_t{0} : ((one << (m % 64U)) - 1U);
559
+
560
+ std::uint64_t V0 = mask0;
561
+ std::uint64_t V1 = mask1;
562
+ std::uint64_t V2 = mask2;
563
+
564
+ const std::uint64_t* const peq_base = scr.peq.data();
565
+
566
+ const bool has_cutoff = cutoff != kNoCutoff;
567
+ const std::size_t bail_threshold = m + n + (has_cutoff ? cutoff : 0U);
568
+
569
+ std::size_t j = 0;
570
+ for (const std::uint8_t c : text) {
571
+ const std::uint64_t* peq_row = peq_base + static_cast<std::size_t>(c) * K;
572
+
573
+ // Block 0: no carry in.
574
+ const std::uint64_t U0 = V0 & peq_row[0];
575
+ std::uint64_t sum0;
576
+ const bool carry01 = __builtin_add_overflow(V0, U0, &sum0);
577
+ V0 = (sum0 | (V0 - U0)) & mask0;
578
+
579
+ // Block 1: carry in from block 0, carry out to block 2.
580
+ const std::uint64_t U1 = V1 & peq_row[1];
581
+ std::uint64_t sum1a;
582
+ const bool carry_a = __builtin_add_overflow(V1, U1, &sum1a);
583
+ std::uint64_t sum1;
584
+ const bool carry_b =
585
+ __builtin_add_overflow(sum1a, static_cast<std::uint64_t>(carry01), &sum1);
586
+ const bool carry12 = carry_a || carry_b;
587
+ V1 = (sum1 | (V1 - U1)) & mask1;
588
+
589
+ // Block 2: carry in from block 1; no carry out (K = 3, last block).
590
+ const std::uint64_t U2 = V2 & peq_row[2];
591
+ std::uint64_t sum2;
592
+ __builtin_add_overflow(V2, U2, &sum2);
593
+ sum2 += static_cast<std::uint64_t>(carry12);
594
+ V2 = (sum2 | (V2 - U2)) & mask2;
595
+
596
+ ++j;
597
+ if (has_cutoff) {
598
+ const std::size_t pc =
599
+ static_cast<std::size_t>(std::popcount(V0)) +
600
+ static_cast<std::size_t>(std::popcount(V1)) +
601
+ static_cast<std::size_t>(std::popcount(V2));
602
+ const std::size_t lower_bound_score = 2U * (j + pc);
603
+ if (lower_bound_score > bail_threshold) {
604
+ return cutoff + 1U;
605
+ }
606
+ }
607
+ }
608
+
609
+ const std::size_t lcs_unmatched =
610
+ static_cast<std::size_t>(std::popcount(V0)) +
611
+ static_cast<std::size_t>(std::popcount(V1)) +
612
+ static_cast<std::size_t>(std::popcount(V2));
613
+ const std::size_t lcs = m - lcs_unmatched;
614
+ return m + n - 2U * lcs;
615
+ }
616
+
617
+ // K = 4 hand-specialisation for ``193 <= m <= 256``. Four register-
618
+ // pinned blocks, three-stage carry chain. Beyond K = 4 the register
619
+ // pressure on x86-64 starts to spill V into stack, so the dispatch
620
+ // switches to the generic kernel.
621
+ inline std::size_t indel_distance_k4_u8(
622
+ std::span<const std::uint8_t> pattern,
623
+ std::span<const std::uint8_t> text,
624
+ std::size_t cutoff) noexcept {
625
+ const std::size_t m = pattern.size();
626
+ const std::size_t n = text.size();
627
+ if (m == 0U) return n;
628
+ if (n == 0U) return m;
629
+ // Caller guarantees 192 < m <= 256.
630
+ constexpr std::size_t K = 4;
631
+ const std::uint64_t one = 1U;
632
+
633
+ MultiWordU8Scratch& scr = multi_word_u8_scratch();
634
+ scr.peq_build(pattern, K);
635
+
636
+ const std::uint64_t mask0 = ~std::uint64_t{0};
637
+ const std::uint64_t mask1 = ~std::uint64_t{0};
638
+ const std::uint64_t mask2 = ~std::uint64_t{0};
639
+ const std::uint64_t mask3 =
640
+ (m % 64U == 0U) ? ~std::uint64_t{0} : ((one << (m % 64U)) - 1U);
641
+
642
+ std::uint64_t V0 = mask0;
643
+ std::uint64_t V1 = mask1;
644
+ std::uint64_t V2 = mask2;
645
+ std::uint64_t V3 = mask3;
646
+
647
+ const std::uint64_t* const peq_base = scr.peq.data();
648
+
649
+ const bool has_cutoff = cutoff != kNoCutoff;
650
+ const std::size_t bail_threshold = m + n + (has_cutoff ? cutoff : 0U);
651
+
652
+ std::size_t j = 0;
653
+ for (const std::uint8_t c : text) {
654
+ const std::uint64_t* peq_row = peq_base + static_cast<std::size_t>(c) * K;
655
+
656
+ // Block 0: no carry in.
657
+ const std::uint64_t U0 = V0 & peq_row[0];
658
+ std::uint64_t sum0;
659
+ const bool carry01 = __builtin_add_overflow(V0, U0, &sum0);
660
+ V0 = (sum0 | (V0 - U0)) & mask0;
661
+
662
+ // Block 1: carry in from block 0, carry out to block 2.
663
+ const std::uint64_t U1 = V1 & peq_row[1];
664
+ std::uint64_t sum1a;
665
+ const bool c1a = __builtin_add_overflow(V1, U1, &sum1a);
666
+ std::uint64_t sum1;
667
+ const bool c1b =
668
+ __builtin_add_overflow(sum1a, static_cast<std::uint64_t>(carry01), &sum1);
669
+ const bool carry12 = c1a || c1b;
670
+ V1 = (sum1 | (V1 - U1)) & mask1;
671
+
672
+ // Block 2: carry in from block 1, carry out to block 3.
673
+ const std::uint64_t U2 = V2 & peq_row[2];
674
+ std::uint64_t sum2a;
675
+ const bool c2a = __builtin_add_overflow(V2, U2, &sum2a);
676
+ std::uint64_t sum2;
677
+ const bool c2b =
678
+ __builtin_add_overflow(sum2a, static_cast<std::uint64_t>(carry12), &sum2);
679
+ const bool carry23 = c2a || c2b;
680
+ V2 = (sum2 | (V2 - U2)) & mask2;
681
+
682
+ // Block 3: carry in from block 2; no carry out (K = 4, last block).
683
+ const std::uint64_t U3 = V3 & peq_row[3];
684
+ std::uint64_t sum3;
685
+ __builtin_add_overflow(V3, U3, &sum3);
686
+ sum3 += static_cast<std::uint64_t>(carry23);
687
+ V3 = (sum3 | (V3 - U3)) & mask3;
688
+
689
+ ++j;
690
+ if (has_cutoff) {
691
+ const std::size_t pc =
692
+ static_cast<std::size_t>(std::popcount(V0)) +
693
+ static_cast<std::size_t>(std::popcount(V1)) +
694
+ static_cast<std::size_t>(std::popcount(V2)) +
695
+ static_cast<std::size_t>(std::popcount(V3));
696
+ const std::size_t lower_bound_score = 2U * (j + pc);
697
+ if (lower_bound_score > bail_threshold) {
698
+ return cutoff + 1U;
699
+ }
700
+ }
701
+ }
702
+
703
+ const std::size_t lcs_unmatched =
704
+ static_cast<std::size_t>(std::popcount(V0)) +
705
+ static_cast<std::size_t>(std::popcount(V1)) +
706
+ static_cast<std::size_t>(std::popcount(V2)) +
707
+ static_cast<std::size_t>(std::popcount(V3));
708
+ const std::size_t lcs = m - lcs_unmatched;
709
+ return m + n - 2U * lcs;
710
+ }
711
+
712
+ // Templated K = 5..8 implementation. ``K`` is a compile-time
713
+ // constant, so the per-block loop body unrolls and V[0..K-1] /
714
+ // mask[0..K-1] become register-resident stack arrays the compiler can
715
+ // keep in GPRs across the full text scan. Same fused single-step
716
+ // Hyyrö recurrence as the K = 2/3/4 specialisations — kept in one
717
+ // templated function instead of four hand-expanded variants because
718
+ // the duplication offers no clarity at that point.
719
+ //
720
+ // Above K = 8 the register file (16 GPRs on x86-64) overflows and the
721
+ // templated form's advantage disappears; for K >= 9 the dispatch
722
+ // falls back to ``indel_distance_multi_word_u8``, which uses the same
723
+ // fused-recurrence body but with a runtime K loop and a heap-backed
724
+ // V / mask buffer.
725
+ template <std::size_t K>
726
+ inline std::size_t indel_distance_kN_u8(
727
+ std::span<const std::uint8_t> pattern,
728
+ std::span<const std::uint8_t> text,
729
+ std::size_t cutoff) noexcept {
730
+ static_assert(K >= 2 && K <= 8, "indel_distance_kN_u8 covers K=2..8");
731
+ const std::size_t m = pattern.size();
732
+ const std::size_t n = text.size();
733
+ if (m == 0U) return n;
734
+ if (n == 0U) return m;
735
+ if (cutoff != kNoCutoff) {
736
+ const std::size_t len_diff = m > n ? m - n : n - m;
737
+ if (len_diff > cutoff) return cutoff + 1U;
738
+ }
739
+ const std::uint64_t one = 1U;
740
+
741
+ MultiWordU8Scratch& scr = multi_word_u8_scratch();
742
+ scr.peq_build(pattern, K);
743
+
744
+ // Stack-resident per-block state. ``K`` is constexpr so these are
745
+ // fixed-size arrays the compiler can promote to registers.
746
+ std::uint64_t mask[K];
747
+ for (std::size_t k = 0; k < K - 1U; ++k) mask[k] = ~std::uint64_t{0};
748
+ mask[K - 1U] =
749
+ (m % 64U == 0U) ? ~std::uint64_t{0} : ((one << (m % 64U)) - 1U);
750
+
751
+ std::uint64_t V[K];
752
+ for (std::size_t k = 0; k < K; ++k) V[k] = mask[k];
753
+
754
+ const std::uint64_t* const peq_base = scr.peq.data();
755
+
756
+ const bool has_cutoff = cutoff != kNoCutoff;
757
+ const std::size_t bail_threshold = m + n + (has_cutoff ? cutoff : 0U);
758
+
759
+ std::size_t j = 0;
760
+ for (const std::uint8_t c : text) {
761
+ const std::uint64_t* peq_row = peq_base + static_cast<std::size_t>(c) * K;
762
+
763
+ // Block 0: no carry in. Carry-out feeds the next block.
764
+ std::uint64_t carry;
765
+ {
766
+ const std::uint64_t U = V[0] & peq_row[0];
767
+ std::uint64_t s;
768
+ const bool c1 = __builtin_add_overflow(V[0], U, &s);
769
+ carry = static_cast<std::uint64_t>(c1);
770
+ V[0] = (s | (V[0] - U)) & mask[0];
771
+ }
772
+
773
+ // Middle blocks: carry in and carry out. The loop is unrolled by
774
+ // the compiler because ``K`` is constexpr; the carry is a single
775
+ // ``uint64_t`` (always 0 or 1 by the Hyyrö U <= V invariant) so
776
+ // the chain lowers to ADCX/ADOX where available.
777
+ for (std::size_t k = 1; k < K - 1U; ++k) {
778
+ const std::uint64_t U = V[k] & peq_row[k];
779
+ std::uint64_t s1, s2;
780
+ const bool c1 = __builtin_add_overflow(V[k], U, &s1);
781
+ const bool c2 = __builtin_add_overflow(s1, carry, &s2);
782
+ carry = static_cast<std::uint64_t>(c1) + static_cast<std::uint64_t>(c2);
783
+ V[k] = (s2 | (V[k] - U)) & mask[k];
784
+ }
785
+
786
+ // Last block: carry in, no carry out needed.
787
+ {
788
+ constexpr std::size_t k = K - 1U;
789
+ const std::uint64_t U = V[k] & peq_row[k];
790
+ std::uint64_t s;
791
+ __builtin_add_overflow(V[k], U, &s);
792
+ s += carry;
793
+ V[k] = (s | (V[k] - U)) & mask[k];
794
+ }
795
+
796
+ ++j;
797
+ if (has_cutoff) {
798
+ std::size_t pc = 0;
799
+ for (std::size_t k = 0; k < K; ++k) {
800
+ pc += static_cast<std::size_t>(std::popcount(V[k]));
801
+ }
802
+ const std::size_t lower_bound_score = 2U * (j + pc);
803
+ if (lower_bound_score > bail_threshold) {
804
+ return cutoff + 1U;
805
+ }
806
+ }
807
+ }
808
+
809
+ std::size_t lcs_unmatched = 0;
810
+ for (std::size_t k = 0; k < K; ++k) {
811
+ lcs_unmatched += static_cast<std::size_t>(std::popcount(V[k]));
812
+ }
813
+ const std::size_t lcs = m - lcs_unmatched;
814
+ return m + n - 2U * lcs;
815
+ }
816
+
817
+ inline std::size_t indel_distance_k5_u8(
818
+ std::span<const std::uint8_t> pattern,
819
+ std::span<const std::uint8_t> text,
820
+ std::size_t cutoff) noexcept {
821
+ return indel_distance_kN_u8<5>(pattern, text, cutoff);
822
+ }
823
+ inline std::size_t indel_distance_k6_u8(
824
+ std::span<const std::uint8_t> pattern,
825
+ std::span<const std::uint8_t> text,
826
+ std::size_t cutoff) noexcept {
827
+ return indel_distance_kN_u8<6>(pattern, text, cutoff);
828
+ }
829
+ inline std::size_t indel_distance_k7_u8(
830
+ std::span<const std::uint8_t> pattern,
831
+ std::span<const std::uint8_t> text,
832
+ std::size_t cutoff) noexcept {
833
+ return indel_distance_kN_u8<7>(pattern, text, cutoff);
834
+ }
835
+ inline std::size_t indel_distance_k8_u8(
836
+ std::span<const std::uint8_t> pattern,
837
+ std::span<const std::uint8_t> text,
838
+ std::size_t cutoff) noexcept {
839
+ return indel_distance_kN_u8<8>(pattern, text, cutoff);
840
+ }
841
+
842
+ // Multi-word bit-parallel Indel for patterns of arbitrary length.
843
+ //
844
+ // Hyyrö (2004) generalisation: split the pattern into ``K = ceil(m / 64)``
845
+ // blocks of 64 bits each. The V vector becomes ``V[K]``; the per-text-
846
+ // character recurrence
847
+ // sum[k] = V[k] + (V[k] & PEQ[c][k]) (with carry between blocks)
848
+ // diff[k] = V[k] - (V[k] & PEQ[c][k]) (with borrow between blocks)
849
+ // V'[k] = (sum[k] | diff[k]) & mask[k]
850
+ // extends the single-word formula. The last block's ``mask`` masks
851
+ // off the bits beyond ``m % 64``.
852
+ //
853
+ // Memory: O(K) per call for the V buffer plus K-entry PEQ per
854
+ // observed alphabet character. PEQ is a hashmap for arbitrary
855
+ // ``Token``; the byte-alphabet specialisation stores a flat
856
+ // ``[256][K]`` array (rebuilt as a flat ``[256 * K]`` vector here so
857
+ // the same code path serves both).
858
+ template <typename Token>
859
+ inline std::size_t indel_distance_multi_word(
860
+ std::span<const Token> pattern,
861
+ std::span<const Token> text,
862
+ std::size_t cutoff) {
863
+ static_assert(std::is_integral_v<Token> || std::is_unsigned_v<Token>);
864
+ const std::size_t m = pattern.size();
865
+ const std::size_t n = text.size();
866
+ if (m == 0U) return n;
867
+ if (n == 0U) return m;
868
+ const std::size_t K = (m + 63U) / 64U;
869
+ const std::uint64_t one = 1U;
870
+
871
+ // PEQ as ``Token -> std::vector<uint64_t>(K)``.
872
+ std::unordered_map<Token, std::vector<std::uint64_t>> peq;
873
+ for (std::size_t i = 0; i < m; ++i) {
874
+ auto [it, inserted] = peq.try_emplace(pattern[i], K, std::uint64_t{0});
875
+ it->second[i >> 6U] |= one << (i & 63U);
876
+ }
877
+
878
+ // Per-block masks: full ``~0`` for all but the last block, which
879
+ // masks off the bits at indices >= ``m``.
880
+ std::vector<std::uint64_t> mask(K, ~std::uint64_t{0});
881
+ if (m % 64U != 0U) {
882
+ mask.back() = (one << (m % 64U)) - 1U;
883
+ }
884
+
885
+ // V starts as the all-ones pattern (every position "unmatched").
886
+ std::vector<std::uint64_t> V(K);
887
+ for (std::size_t k = 0; k < K; ++k) V[k] = mask[k];
888
+
889
+ std::vector<std::uint64_t> sum(K), diff(K), U(K);
890
+
891
+ // Cutoff bookkeeping. The single-word lower bound generalises:
892
+ // popcount(V) is summed across blocks.
893
+ const bool has_cutoff = cutoff != kNoCutoff;
894
+ const std::size_t bail_threshold = m + n + (has_cutoff ? cutoff : 0U);
895
+
896
+ std::size_t j = 0;
897
+ for (const auto c : text) {
898
+ const auto it = peq.find(c);
899
+ const std::uint64_t* peq_row = (it == peq.end()) ? nullptr : it->second.data();
900
+
901
+ // U[k] = V[k] & PEQ[c][k]
902
+ for (std::size_t k = 0; k < K; ++k) {
903
+ U[k] = peq_row == nullptr ? std::uint64_t{0} : V[k] & peq_row[k];
904
+ }
905
+
906
+ // sum[k] = V[k] + U[k] with carry between blocks.
907
+ std::uint64_t carry = 0;
908
+ for (std::size_t k = 0; k < K; ++k) {
909
+ const std::uint64_t a = V[k];
910
+ const std::uint64_t b = U[k];
911
+ const std::uint64_t s1 = a + b;
912
+ const std::uint64_t s2 = s1 + carry;
913
+ // Carry out: 1 if s1 < a (overflow on a+b) or (carry && s2 < s1).
914
+ carry = static_cast<std::uint64_t>(s1 < a) +
915
+ static_cast<std::uint64_t>(s2 < s1);
916
+ sum[k] = s2;
917
+ }
918
+
919
+ // diff[k] = V[k] - U[k] with borrow between blocks.
920
+ std::uint64_t borrow = 0;
921
+ for (std::size_t k = 0; k < K; ++k) {
922
+ const std::uint64_t a = V[k];
923
+ const std::uint64_t b = U[k];
924
+ const std::uint64_t d1 = a - b;
925
+ const std::uint64_t d2 = d1 - borrow;
926
+ // Borrow out: 1 if a < b OR (borrow && d1 == 0 && borrow > 0).
927
+ borrow = static_cast<std::uint64_t>(a < b) +
928
+ static_cast<std::uint64_t>(d1 < borrow);
929
+ diff[k] = d2;
930
+ }
931
+
932
+ // V'[k] = (sum[k] | diff[k]) & mask[k]
933
+ for (std::size_t k = 0; k < K; ++k) {
934
+ V[k] = (sum[k] | diff[k]) & mask[k];
935
+ }
936
+
937
+ ++j;
938
+ if (has_cutoff) {
939
+ std::size_t pc = 0;
940
+ for (std::size_t k = 0; k < K; ++k) {
941
+ pc += static_cast<std::size_t>(std::popcount(V[k]));
942
+ }
943
+ const std::size_t lower_bound_score = 2U * (j + pc);
944
+ if (lower_bound_score > bail_threshold) {
945
+ return cutoff + 1U;
946
+ }
947
+ }
948
+ }
949
+
950
+ std::size_t lcs_unmatched = 0;
951
+ for (std::size_t k = 0; k < K; ++k) {
952
+ lcs_unmatched += static_cast<std::size_t>(std::popcount(V[k]));
953
+ }
954
+ const std::size_t lcs = m - lcs_unmatched;
955
+ return m + n - 2U * lcs;
956
+ }
957
+
958
+ // Prepared bit-parallel pattern — PEQ built once, reused across many
959
+ // targets. The win is concentrated in batch / multi-window workloads
960
+ // (``partial_ratio`` evaluates the same pattern against multiple
961
+ // window slices) where the PEQ build cost would otherwise be paid
962
+ // per evaluation.
963
+ template <typename Token>
964
+ struct PreparedIndelPattern {
965
+ static constexpr bool kIsByte = std::is_same_v<Token, std::uint8_t>;
966
+ // Storage: 256-entry array for byte-alphabet (the indel_single_word_u8
967
+ // shape), arbitrary hashmap otherwise. Both are queried the same way
968
+ // via ``peq_of(c)``.
969
+ std::conditional_t<kIsByte,
970
+ std::array<std::uint64_t, 256>,
971
+ std::unordered_map<Token, std::uint64_t>> peq{};
972
+ std::uint64_t mask = 0;
973
+ std::size_t m = 0;
974
+ bool use_dp = false; // true when m > 64; falls back to scalar DP
975
+ std::span<const Token> pattern{}; // retained for the DP fallback path
976
+
977
+ std::uint64_t peq_of(Token c) const noexcept {
978
+ if constexpr (kIsByte) {
979
+ return peq[c];
980
+ } else {
981
+ const auto it = peq.find(c);
982
+ return it == peq.end() ? std::uint64_t{0} : it->second;
983
+ }
984
+ }
985
+ };
986
+
987
+ template <typename Token>
988
+ inline PreparedIndelPattern<Token> prepare_indel_pattern(
989
+ std::span<const Token> pattern) {
990
+ static_assert(std::is_integral_v<Token> || std::is_unsigned_v<Token>);
991
+ PreparedIndelPattern<Token> prepared;
992
+ prepared.m = pattern.size();
993
+ prepared.pattern = pattern;
994
+ if (prepared.m == 0U || prepared.m > 64U) {
995
+ // For empty or multi-word patterns we don't pre-build the single-
996
+ // word PEQ; the ``indel_distance_prepared`` path detects this and
997
+ // routes to the multi-word kernel (which builds its own per-block
998
+ // PEQ inline — see below for the prepared multi-word variant).
999
+ prepared.use_dp = true;
1000
+ return prepared;
1001
+ }
1002
+ const std::uint64_t one = 1U;
1003
+ for (std::size_t i = 0; i < prepared.m; ++i) {
1004
+ if constexpr (PreparedIndelPattern<Token>::kIsByte) {
1005
+ prepared.peq[pattern[i]] |= one << i;
1006
+ } else {
1007
+ prepared.peq[pattern[i]] |= one << i;
1008
+ }
1009
+ }
1010
+ prepared.mask =
1011
+ (prepared.m == 64U) ? ~std::uint64_t{0} : ((one << prepared.m) - 1U);
1012
+ return prepared;
1013
+ }
1014
+
1015
+ // Multi-word kernel that accepts a pre-built PEQ buffer instead of
1016
+ // rebuilding it from the pattern. The buffer layout is the standard
1017
+ // flat ``[256][K]`` row-major: ``peq[c * K + b]`` is block ``b`` of
1018
+ // the bitmap for character ``c``. Used by ``partial_ratio`` (and
1019
+ // other "fixed pattern, many text windows" loops) to amortise the
1020
+ // 256 * K * 8-byte PEQ fill across many calls.
1021
+ //
1022
+ // Carry chain + cutoff bookkeeping match ``indel_distance_multi_word_u8``;
1023
+ // only the PEQ source changes.
1024
+ inline std::size_t indel_distance_multi_word_u8_with_peq(
1025
+ const std::uint64_t* peq, std::size_t K,
1026
+ std::size_t m, // pattern length
1027
+ std::span<const std::uint8_t> text,
1028
+ std::size_t cutoff = kNoCutoff) {
1029
+ const std::size_t n = text.size();
1030
+ if (m == 0U) return n;
1031
+ if (n == 0U) return m;
1032
+ if (cutoff != kNoCutoff) {
1033
+ const std::size_t len_diff = m > n ? m - n : n - m;
1034
+ if (len_diff > cutoff) return cutoff + 1U;
1035
+ }
1036
+
1037
+ // Stack-resident V[K] when K small (K <= 8 covers up to m = 512;
1038
+ // partial_ratio's hot path is K <= 4). Heap fallback for K > 8.
1039
+ constexpr std::size_t kStackMaxK = 8U;
1040
+ std::uint64_t V_stack[kStackMaxK];
1041
+ std::vector<std::uint64_t> V_heap;
1042
+ std::uint64_t* V;
1043
+ if (K <= kStackMaxK) {
1044
+ V = V_stack;
1045
+ } else {
1046
+ V_heap.resize(K);
1047
+ V = V_heap.data();
1048
+ }
1049
+
1050
+ const std::uint64_t one = 1U;
1051
+ const std::size_t last_bits = m - (K - 1U) * 64U;
1052
+ const std::uint64_t mask_last =
1053
+ (last_bits == 64U) ? ~std::uint64_t{0} : ((one << last_bits) - 1U);
1054
+ for (std::size_t k = 0; k < K - 1U; ++k) V[k] = ~std::uint64_t{0};
1055
+ V[K - 1U] = mask_last;
1056
+
1057
+ const bool has_cutoff = cutoff != kNoCutoff;
1058
+ const std::size_t bail_threshold = m + n + (has_cutoff ? cutoff : 0U);
1059
+
1060
+ std::size_t j = 0;
1061
+ for (const std::uint8_t c : text) {
1062
+ const std::uint64_t* peq_row = peq + static_cast<std::size_t>(c) * K;
1063
+ std::uint64_t carry_in = 0;
1064
+ for (std::size_t k = 0; k < K; ++k) {
1065
+ const std::uint64_t Vk = V[k];
1066
+ const std::uint64_t Uk = Vk & peq_row[k];
1067
+ std::uint64_t s1;
1068
+ const bool c1 = __builtin_add_overflow(Vk, Uk, &s1);
1069
+ std::uint64_t s2;
1070
+ const bool c2 = __builtin_add_overflow(s1, carry_in, &s2);
1071
+ carry_in = static_cast<std::uint64_t>(c1) +
1072
+ static_cast<std::uint64_t>(c2);
1073
+ const std::uint64_t mask_k =
1074
+ (k == K - 1U) ? mask_last : ~std::uint64_t{0};
1075
+ V[k] = (s2 | (Vk - Uk)) & mask_k;
1076
+ }
1077
+ ++j;
1078
+ if (has_cutoff) {
1079
+ std::size_t pc = 0;
1080
+ for (std::size_t k = 0; k < K; ++k) {
1081
+ pc += static_cast<std::size_t>(std::popcount(V[k]));
1082
+ }
1083
+ const std::size_t lower_bound_score = 2U * (j + pc);
1084
+ if (lower_bound_score > bail_threshold) {
1085
+ return cutoff + 1U;
1086
+ }
1087
+ }
1088
+ }
1089
+ std::size_t lcs_unmatched = 0;
1090
+ for (std::size_t k = 0; k < K; ++k) {
1091
+ lcs_unmatched += static_cast<std::size_t>(std::popcount(V[k]));
1092
+ }
1093
+ return m + n - 2U * (m - lcs_unmatched);
1094
+ }
1095
+
1096
+ // Build a multi-word PEQ for a byte-pattern into a caller-provided
1097
+ // vector (sized 256 * K). Uses dirty-symbol clearing when the same
1098
+ // buffer is reused at the same K (partial_ratio's thread-local peq).
1099
+ inline void build_multi_word_peq_u8(
1100
+ std::span<const std::uint8_t> pattern,
1101
+ std::size_t K,
1102
+ std::vector<std::uint64_t>& peq) {
1103
+ // Per-thread dirty bookkeeping for the external peq buffer. Assumes
1104
+ // a single persistent buffer per thread (the partial_ratio path).
1105
+ thread_local std::vector<std::uint8_t> dirty;
1106
+ thread_local std::array<std::uint32_t, 256> touch{};
1107
+ thread_local std::uint32_t gen = 0;
1108
+ thread_local std::size_t layout_k = 0;
1109
+
1110
+ if (peq.size() < 256U * K) {
1111
+ peq.assign(256U * K, 0U);
1112
+ dirty.clear();
1113
+ layout_k = K;
1114
+ touch.fill(0);
1115
+ gen = 0;
1116
+ } else if (layout_k != K) {
1117
+ std::fill_n(peq.data(), 256U * K, std::uint64_t{0});
1118
+ dirty.clear();
1119
+ layout_k = K;
1120
+ } else {
1121
+ for (const std::uint8_t s : dirty) {
1122
+ std::fill_n(
1123
+ peq.data() + static_cast<std::size_t>(s) * K, K, std::uint64_t{0});
1124
+ }
1125
+ dirty.clear();
1126
+ }
1127
+ if (++gen == 0U) {
1128
+ touch.fill(0);
1129
+ gen = 1U;
1130
+ }
1131
+
1132
+ for (std::size_t i = 0; i < pattern.size(); ++i) {
1133
+ const std::uint8_t c = pattern[i];
1134
+ if (touch[c] != gen) {
1135
+ touch[c] = gen;
1136
+ dirty.push_back(c);
1137
+ }
1138
+ peq[static_cast<std::size_t>(c) * K + (i >> 6U)] |=
1139
+ std::uint64_t{1} << (i & 63U);
1140
+ }
1141
+ }
1142
+
1143
+ template <typename Token>
1144
+ inline std::size_t indel_distance_prepared(
1145
+ const PreparedIndelPattern<Token>& prepared,
1146
+ std::span<const Token> text,
1147
+ std::size_t cutoff = kNoCutoff) {
1148
+ const std::size_t m = prepared.m;
1149
+ const std::size_t n = text.size();
1150
+ if (m == 0U) return n;
1151
+ if (n == 0U) return m;
1152
+ if (cutoff != kNoCutoff) {
1153
+ const std::size_t len_diff = m > n ? m - n : n - m;
1154
+ if (len_diff > cutoff) return cutoff + 1U;
1155
+ }
1156
+ if (prepared.use_dp) {
1157
+ // Pattern too long for the single-word prepared PEQ: route to
1158
+ // the multi-word kernel. The multi-word PEQ build is amortised
1159
+ // over the text traversal already; pre-building it would require
1160
+ // a separate prepared-multi-word struct (TODO if profiling shows
1161
+ // it matters).
1162
+ return indel_distance_multi_word<Token>(prepared.pattern, text, cutoff);
1163
+ }
1164
+ std::uint64_t V = prepared.mask;
1165
+ if (cutoff == kNoCutoff) {
1166
+ for (const auto c : text) {
1167
+ const std::uint64_t U = V & prepared.peq_of(c);
1168
+ V = ((V + U) | (V - U)) & prepared.mask;
1169
+ }
1170
+ } else {
1171
+ const std::size_t bail_threshold = m + n + cutoff;
1172
+ std::size_t j = 0;
1173
+ for (const auto c : text) {
1174
+ const std::uint64_t U = V & prepared.peq_of(c);
1175
+ V = ((V + U) | (V - U)) & prepared.mask;
1176
+ ++j;
1177
+ const std::size_t lower_bound_score =
1178
+ 2U * (j + static_cast<std::size_t>(std::popcount(V)));
1179
+ if (lower_bound_score > bail_threshold) {
1180
+ return cutoff + 1U;
1181
+ }
1182
+ }
1183
+ }
1184
+ const std::size_t lcs =
1185
+ m - static_cast<std::size_t>(std::popcount(V));
1186
+ return m + n - 2U * lcs;
1187
+ }
1188
+
1189
+ // Normalized similarity. The denominator is |a| + |b| (NOT
1190
+ // max(|a|, |b|) — Indel can be up to a_len + b_len, e.g.
1191
+ // indel("aaa", "bbb") = 6).
1192
+ inline double normalize(
1193
+ std::size_t distance, std::size_t a_len, std::size_t b_len) noexcept {
1194
+ const std::size_t total = a_len + b_len;
1195
+ if (total == 0U) return 1.0;
1196
+ return 1.0 -
1197
+ static_cast<double>(distance) / static_cast<double>(total);
1198
+ }
1199
+
1200
+ } // namespace stride_align::indel