1use crate::lcp::{LcpDispatch, Symbol};
9use std::num::NonZeroUsize;
10
11const DEFAULT_PROBE: usize = 256;
12const DEFAULT_MIN_LCP: usize = 1_024;
13const DEFAULT_CAPACITY: usize = 4_096;
14const DEFAULT_ACTIVATE_ENTRIES: usize = 64;
15
16#[non_exhaustive]
21#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
22pub enum LcpMemoizationPolicy {
23 #[default]
25 Disabled,
26 Geometric(GeometricMemoizationConfig),
28}
29
30impl LcpMemoizationPolicy {
31 pub const fn geometric() -> Self {
37 Self::Geometric(GeometricMemoizationConfig::DEFAULT)
38 }
39}
40
41impl From<GeometricMemoizationConfig> for LcpMemoizationPolicy {
42 fn from(config: GeometricMemoizationConfig) -> Self {
43 Self::Geometric(config)
44 }
45}
46
47#[non_exhaustive]
63#[derive(Copy, Clone, Debug, PartialEq, Eq)]
64pub struct GeometricMemoizationConfig {
65 probe_symbols: NonZeroUsize,
66 min_lcp_symbols: NonZeroUsize,
67 activate_after_entries: NonZeroUsize,
68 max_entries_per_partition: NonZeroUsize,
69}
70
71impl GeometricMemoizationConfig {
72 pub const DEFAULT: Self = Self {
75 probe_symbols: NonZeroUsize::new(DEFAULT_PROBE).unwrap(),
76 min_lcp_symbols: NonZeroUsize::new(DEFAULT_MIN_LCP).unwrap(),
77 activate_after_entries: NonZeroUsize::new(DEFAULT_ACTIVATE_ENTRIES).unwrap(),
78 max_entries_per_partition: NonZeroUsize::new(DEFAULT_CAPACITY).unwrap(),
79 };
80
81 pub const fn probe_symbols(&self) -> usize {
83 self.probe_symbols.get()
84 }
85
86 pub const fn with_probe_symbols(mut self, probe_symbols: NonZeroUsize) -> Self {
91 self.probe_symbols = probe_symbols;
92 self
93 }
94
95 pub const fn min_lcp_symbols(&self) -> usize {
98 self.min_lcp_symbols.get()
99 }
100
101 pub const fn with_min_lcp_symbols(mut self, min_lcp_symbols: NonZeroUsize) -> Self {
103 self.min_lcp_symbols = min_lcp_symbols;
104 self
105 }
106
107 pub const fn activate_after_entries(&self) -> usize {
110 self.activate_after_entries.get()
111 }
112
113 pub const fn with_activate_after_entries(
118 mut self,
119 activate_after_entries: NonZeroUsize,
120 ) -> Self {
121 self.activate_after_entries = activate_after_entries;
122 self
123 }
124
125 pub const fn max_entries_per_partition(&self) -> usize {
127 self.max_entries_per_partition.get()
128 }
129
130 pub const fn with_max_entries_per_partition(
133 mut self,
134 max_entries_per_partition: NonZeroUsize,
135 ) -> Self {
136 self.max_entries_per_partition = max_entries_per_partition;
137 self
138 }
139}
140
141impl Default for GeometricMemoizationConfig {
142 fn default() -> Self {
143 Self::DEFAULT
144 }
145}
146
147#[derive(Copy, Clone, Debug, PartialEq, Eq)]
149pub(crate) struct MemoConfig {
150 pub(crate) probe: usize,
151 pub(crate) min_lcp: usize,
152 pub(crate) capacity: usize,
153 pub(crate) activate_entries: usize,
154}
155
156impl From<GeometricMemoizationConfig> for MemoConfig {
157 fn from(config: GeometricMemoizationConfig) -> Self {
158 Self {
159 probe: config.probe_symbols.get(),
160 min_lcp: config.min_lcp_symbols.get(),
161 capacity: config.max_entries_per_partition.get(),
162 activate_entries: config.activate_after_entries.get(),
163 }
164 }
165}
166
167#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
169pub(crate) struct MemoStats {
170 pub(crate) tables: u64,
171 pub(crate) active_tables: u64,
172 pub(crate) tables_0_15: u64,
173 pub(crate) tables_16_31: u64,
174 pub(crate) tables_32_63: u64,
175 pub(crate) tables_64_127: u64,
176 pub(crate) tables_128_255: u64,
177 pub(crate) tables_256_plus: u64,
178 pub(crate) calls: u64,
179 pub(crate) cold_direct: u64,
180 pub(crate) probe_resolved: u64,
181 pub(crate) lookups: u64,
182 pub(crate) direct_hits: u64,
183 pub(crate) gap_hits: u64,
184 pub(crate) misses: u64,
185 pub(crate) inserts: u64,
186 pub(crate) extensions: u64,
187 pub(crate) capacity_rejects: u64,
188 pub(crate) scanned_matches: u64,
189 pub(crate) skipped_matches: u64,
190 pub(crate) final_entries: u64,
191 pub(crate) max_entries: u64,
192 pub(crate) unique_diagonals: u64,
193 pub(crate) singleton_diagonals: u64,
194 pub(crate) max_entries_per_diagonal: u64,
195 pub(crate) lookup_steps: u64,
196 pub(crate) insert_steps: u64,
197 pub(crate) insert_shifts: u64,
198 pub(crate) gap_mismatches: u64,
199 pub(crate) gap_caps: u64,
200}
201
202impl MemoStats {
203 pub(crate) fn add_assign(&mut self, other: Self) {
204 self.tables = self.tables.saturating_add(other.tables);
205 self.active_tables = self.active_tables.saturating_add(other.active_tables);
206 self.tables_0_15 = self.tables_0_15.saturating_add(other.tables_0_15);
207 self.tables_16_31 = self.tables_16_31.saturating_add(other.tables_16_31);
208 self.tables_32_63 = self.tables_32_63.saturating_add(other.tables_32_63);
209 self.tables_64_127 = self.tables_64_127.saturating_add(other.tables_64_127);
210 self.tables_128_255 = self.tables_128_255.saturating_add(other.tables_128_255);
211 self.tables_256_plus = self.tables_256_plus.saturating_add(other.tables_256_plus);
212 self.calls = self.calls.saturating_add(other.calls);
213 self.cold_direct = self.cold_direct.saturating_add(other.cold_direct);
214 self.probe_resolved = self.probe_resolved.saturating_add(other.probe_resolved);
215 self.lookups = self.lookups.saturating_add(other.lookups);
216 self.direct_hits = self.direct_hits.saturating_add(other.direct_hits);
217 self.gap_hits = self.gap_hits.saturating_add(other.gap_hits);
218 self.misses = self.misses.saturating_add(other.misses);
219 self.inserts = self.inserts.saturating_add(other.inserts);
220 self.extensions = self.extensions.saturating_add(other.extensions);
221 self.capacity_rejects = self.capacity_rejects.saturating_add(other.capacity_rejects);
222 self.scanned_matches = self.scanned_matches.saturating_add(other.scanned_matches);
223 self.skipped_matches = self.skipped_matches.saturating_add(other.skipped_matches);
224 self.final_entries = self.final_entries.saturating_add(other.final_entries);
225 self.max_entries = self.max_entries.max(other.max_entries);
226 self.unique_diagonals = self.unique_diagonals.saturating_add(other.unique_diagonals);
227 self.singleton_diagonals = self
228 .singleton_diagonals
229 .saturating_add(other.singleton_diagonals);
230 self.max_entries_per_diagonal = self
231 .max_entries_per_diagonal
232 .max(other.max_entries_per_diagonal);
233 self.lookup_steps = self.lookup_steps.saturating_add(other.lookup_steps);
234 self.insert_steps = self.insert_steps.saturating_add(other.insert_steps);
235 self.insert_shifts = self.insert_shifts.saturating_add(other.insert_shifts);
236 self.gap_mismatches = self.gap_mismatches.saturating_add(other.gap_mismatches);
237 self.gap_caps = self.gap_caps.saturating_add(other.gap_caps);
238 }
239}
240
241#[derive(Copy, Clone, Debug, PartialEq, Eq)]
242struct MemoEntry {
243 diagonal: usize,
244 end: usize,
245 start: usize,
246}
247
248pub(crate) struct GeometricMemo {
250 config: MemoConfig,
251 entries: Vec<MemoEntry>,
252 stats: MemoStats,
253}
254
255impl GeometricMemo {
256 pub(crate) fn new(config: MemoConfig) -> Self {
257 let initial_capacity = config.capacity.min(256);
258 Self {
259 config,
260 entries: Vec::with_capacity(initial_capacity),
261 stats: MemoStats::default(),
262 }
263 }
264
265 pub(crate) fn finish(mut self) -> MemoStats {
266 self.stats.tables = 1;
267 self.stats.final_entries = self.entries.len() as u64;
268 self.stats.active_tables = u64::from(self.is_active());
269 match self.entries.len() {
270 0..=15 => self.stats.tables_0_15 = 1,
271 16..=31 => self.stats.tables_16_31 = 1,
272 32..=63 => self.stats.tables_32_63 = 1,
273 64..=127 => self.stats.tables_64_127 = 1,
274 128..=255 => self.stats.tables_128_255 = 1,
275 _ => self.stats.tables_256_plus = 1,
276 }
277 let mut group_start = 0usize;
278 while group_start < self.entries.len() {
279 let diagonal = self.entries[group_start].diagonal;
280 let mut group_end = group_start + 1;
281 while group_end < self.entries.len() && self.entries[group_end].diagonal == diagonal {
282 group_end += 1;
283 }
284 let group_len = group_end - group_start;
285 self.stats.unique_diagonals += 1;
286 self.stats.singleton_diagonals += u64::from(group_len == 1);
287 self.stats.max_entries_per_diagonal =
288 self.stats.max_entries_per_diagonal.max(group_len as u64);
289 group_start = group_end;
290 }
291 self.stats
292 }
293
294 #[inline]
295 pub(crate) fn is_active(&self) -> bool {
296 self.entries.len() >= self.config.activate_entries
297 }
298
299 #[inline]
300 pub(crate) fn probe(&self, max_ext: usize) -> usize {
301 max_ext.min(self.config.probe)
302 }
303
304 #[cfg(test)]
308 #[inline]
309 pub(crate) fn lcp<S: Symbol>(
310 &mut self,
311 text: &[S],
312 dispatch: LcpDispatch,
313 p: usize,
314 q: usize,
315 known: usize,
316 max_ext: usize,
317 ) -> usize {
318 if self.is_active() {
319 self.lcp_active_impl::<S, false>(text, dispatch, p, q, known, max_ext)
320 } else {
321 let got = dispatch.lcp(text, p + known, q + known, max_ext);
322 self.observe_training_impl::<false>(p, q, known, got, max_ext);
323 got
324 }
325 }
326
327 #[cfg(test)]
330 #[inline]
331 pub(crate) fn lcp_profiled<S: Symbol>(
332 &mut self,
333 text: &[S],
334 dispatch: LcpDispatch,
335 p: usize,
336 q: usize,
337 known: usize,
338 max_ext: usize,
339 ) -> usize {
340 if self.is_active() {
341 self.lcp_active_impl::<S, true>(text, dispatch, p, q, known, max_ext)
342 } else {
343 let got = dispatch.lcp(text, p + known, q + known, max_ext);
344 self.observe_training_impl::<true>(p, q, known, got, max_ext);
345 got
346 }
347 }
348
349 #[inline]
350 pub(crate) fn observe_training(
351 &mut self,
352 p: usize,
353 q: usize,
354 known: usize,
355 got: usize,
356 max_ext: usize,
357 ) {
358 self.observe_training_impl::<false>(p, q, known, got, max_ext);
359 }
360
361 #[inline]
362 pub(crate) fn observe_training_profiled(
363 &mut self,
364 p: usize,
365 q: usize,
366 known: usize,
367 got: usize,
368 max_ext: usize,
369 ) {
370 self.observe_training_impl::<true>(p, q, known, got, max_ext);
371 }
372
373 #[inline]
374 fn observe_training_impl<const STATS: bool>(
375 &mut self,
376 p: usize,
377 q: usize,
378 known: usize,
379 got: usize,
380 max_ext: usize,
381 ) {
382 if STATS {
383 self.stats.calls = self.stats.calls.saturating_add(1);
384 self.stats.cold_direct = self.stats.cold_direct.saturating_add(1);
385 self.stats.scanned_matches = self.stats.scanned_matches.saturating_add(got as u64);
386 }
387 let total = known + got;
388 if total >= self.config.min_lcp && got < max_ext && !self.is_active() {
392 let (base, other) = if p < q { (p, q) } else { (q, p) };
393 self.insert_exact::<STATS>(base, other - base, total);
394 }
395 }
396
397 #[cfg(test)]
398 #[inline]
399 fn lcp_active_impl<S: Symbol, const STATS: bool>(
400 &mut self,
401 text: &[S],
402 dispatch: LcpDispatch,
403 p: usize,
404 q: usize,
405 known: usize,
406 max_ext: usize,
407 ) -> usize {
408 if STATS {
409 self.stats.calls = self.stats.calls.saturating_add(1);
410 }
411 if max_ext == 0 || p == q {
412 return dispatch.lcp(
413 text,
414 p.saturating_add(known),
415 q.saturating_add(known),
416 max_ext,
417 );
418 }
419
420 let scan_p = p.saturating_add(known);
421 let scan_q = q.saturating_add(known);
422
423 let probe = self.probe(max_ext);
425 let got = dispatch.lcp(text, scan_p, scan_q, probe);
426 self.add_scanned::<STATS>(got);
427 if got < probe {
428 if STATS {
429 self.stats.probe_resolved = self.stats.probe_resolved.saturating_add(1);
430 }
431 return got;
432 }
433 if probe == max_ext {
434 if STATS {
435 self.stats.probe_resolved = self.stats.probe_resolved.saturating_add(1);
436 }
437 return got;
438 }
439
440 self.lcp_after_probe_impl::<S, STATS>(text, dispatch, p, q, known, probe, max_ext)
441 }
442
443 #[inline]
444 #[allow(clippy::too_many_arguments)]
445 pub(crate) fn lcp_after_probe<S: Symbol>(
446 &mut self,
447 text: &[S],
448 dispatch: LcpDispatch,
449 p: usize,
450 q: usize,
451 known: usize,
452 probe: usize,
453 max_ext: usize,
454 ) -> usize {
455 debug_assert!(self.is_active());
456 self.lcp_after_probe_impl::<S, false>(text, dispatch, p, q, known, probe, max_ext)
457 }
458
459 #[inline]
460 #[allow(clippy::too_many_arguments)]
461 pub(crate) fn lcp_after_probe_profiled<S: Symbol>(
462 &mut self,
463 text: &[S],
464 dispatch: LcpDispatch,
465 p: usize,
466 q: usize,
467 known: usize,
468 probe: usize,
469 max_ext: usize,
470 ) -> usize {
471 debug_assert!(self.is_active());
472 self.lcp_after_probe_impl::<S, true>(text, dispatch, p, q, known, probe, max_ext)
473 }
474
475 #[inline]
476 pub(crate) fn record_probe_profiled(&mut self, got: usize, probe: usize, max_ext: usize) {
477 self.stats.calls = self.stats.calls.saturating_add(1);
478 self.stats.scanned_matches = self.stats.scanned_matches.saturating_add(got as u64);
479 if got < probe || probe == max_ext {
480 self.stats.probe_resolved = self.stats.probe_resolved.saturating_add(1);
481 }
482 }
483
484 #[allow(clippy::too_many_arguments)]
485 fn lcp_after_probe_impl<S: Symbol, const STATS: bool>(
486 &mut self,
487 text: &[S],
488 dispatch: LcpDispatch,
489 p: usize,
490 q: usize,
491 known: usize,
492 probe: usize,
493 max_ext: usize,
494 ) -> usize {
495 let scan_p = p.saturating_add(known);
496 let scan_q = q.saturating_add(known);
497 let (base, other) = if p < q { (p, q) } else { (q, p) };
498 let diagonal = other - base;
499
500 let query = base.saturating_add(known).saturating_add(probe);
501 let remaining = max_ext - probe;
502
503 if STATS {
504 self.stats.lookups = self.stats.lookups.saturating_add(1);
505 }
506 let successor_index = if STATS {
507 let mut steps = 0u64;
508 let index = self.entries.partition_point(|entry| {
509 steps += 1;
510 (entry.diagonal, entry.end) < (diagonal, query)
511 });
512 self.stats.lookup_steps = self.stats.lookup_steps.saturating_add(steps);
513 index
514 } else {
515 self.entries
516 .partition_point(|entry| (entry.diagonal, entry.end) < (diagonal, query))
517 };
518 let successor = self.entries.get(successor_index).and_then(|entry| {
519 (entry.diagonal == diagonal).then_some((successor_index, entry.end, entry.start))
520 });
521
522 let Some((successor_index, end, start)) = successor else {
523 if STATS {
524 self.stats.misses = self.stats.misses.saturating_add(1);
525 }
526 return probe
527 + self.scan_tail_and_insert::<S, STATS>(
528 text, dispatch, p, q, base, diagonal, known, probe, remaining,
529 );
530 };
531
532 if start <= query {
533 let available = end - query;
536 let skipped = available.min(remaining);
537 if STATS {
538 self.stats.direct_hits = self.stats.direct_hits.saturating_add(1);
539 }
540 self.add_skipped::<STATS>(skipped);
541 self.extend_start::<STATS>(successor_index, base);
542 return probe + skipped;
543 }
544
545 let gap = start - query;
549 let gap_cap = gap.min(remaining);
550 let gap_lcp = dispatch.lcp(
551 text,
552 scan_p.saturating_add(probe),
553 scan_q.saturating_add(probe),
554 gap_cap,
555 );
556 self.add_scanned::<STATS>(gap_lcp);
557 if gap_lcp < gap_cap {
558 if STATS {
559 self.stats.gap_mismatches = self.stats.gap_mismatches.saturating_add(1);
560 }
561 self.insert_exact::<STATS>(
562 base,
563 diagonal,
564 known.saturating_add(probe).saturating_add(gap_lcp),
565 );
566 return probe + gap_lcp;
567 }
568 if gap_cap == remaining {
569 if STATS {
570 self.stats.gap_caps = self.stats.gap_caps.saturating_add(1);
571 }
572 return max_ext;
573 }
574
575 let interval_len = end - start;
576 let skipped = interval_len.min(remaining - gap);
577 if STATS {
578 self.stats.gap_hits = self.stats.gap_hits.saturating_add(1);
579 }
580 self.add_skipped::<STATS>(skipped);
581 self.extend_start::<STATS>(successor_index, base);
582 probe + gap + skipped
583 }
584
585 #[allow(clippy::too_many_arguments)]
586 fn scan_tail_and_insert<S: Symbol, const STATS: bool>(
587 &mut self,
588 text: &[S],
589 dispatch: LcpDispatch,
590 p: usize,
591 q: usize,
592 base: usize,
593 diagonal: usize,
594 known: usize,
595 already_scanned: usize,
596 remaining: usize,
597 ) -> usize {
598 let got = dispatch.lcp(
599 text,
600 p.saturating_add(known).saturating_add(already_scanned),
601 q.saturating_add(known).saturating_add(already_scanned),
602 remaining,
603 );
604 self.add_scanned::<STATS>(got);
605 if got < remaining {
606 self.insert_exact::<STATS>(
607 base,
608 diagonal,
609 known.saturating_add(already_scanned).saturating_add(got),
610 );
611 }
612 got
613 }
614
615 fn insert_exact<const STATS: bool>(&mut self, base: usize, diagonal: usize, lcp: usize) {
616 if lcp < self.config.min_lcp {
617 return;
618 }
619 let end = base.saturating_add(lcp);
620 let key = (diagonal, end);
621 let index = if STATS {
622 let mut steps = 0u64;
623 let index = self.entries.partition_point(|entry| {
624 steps += 1;
625 (entry.diagonal, entry.end) < key
626 });
627 self.stats.insert_steps = self.stats.insert_steps.saturating_add(steps);
628 index
629 } else {
630 self.entries
631 .partition_point(|entry| (entry.diagonal, entry.end) < key)
632 };
633 if let Some(entry) = self
634 .entries
635 .get_mut(index)
636 .filter(|entry| (entry.diagonal, entry.end) == key)
637 {
638 if base < entry.start {
639 entry.start = base;
640 if STATS {
641 self.stats.extensions = self.stats.extensions.saturating_add(1);
642 }
643 }
644 return;
645 }
646 if self.entries.len() >= self.config.capacity {
647 if STATS {
648 self.stats.capacity_rejects = self.stats.capacity_rejects.saturating_add(1);
649 }
650 return;
651 }
652 if STATS {
653 self.stats.insert_shifts = self
654 .stats
655 .insert_shifts
656 .saturating_add((self.entries.len() - index) as u64);
657 }
658 self.entries.insert(
659 index,
660 MemoEntry {
661 diagonal,
662 end,
663 start: base,
664 },
665 );
666 if STATS {
667 self.stats.inserts = self.stats.inserts.saturating_add(1);
668 self.stats.max_entries = self.stats.max_entries.max(self.entries.len() as u64);
669 }
670 }
671
672 fn extend_start<const STATS: bool>(&mut self, index: usize, start: usize) {
673 let Some(entry) = self.entries.get_mut(index) else {
674 return;
675 };
676 if start < entry.start {
677 entry.start = start;
678 if STATS {
679 self.stats.extensions = self.stats.extensions.saturating_add(1);
680 }
681 }
682 }
683
684 fn add_scanned<const STATS: bool>(&mut self, value: usize) {
685 if STATS {
686 self.stats.scanned_matches = self.stats.scanned_matches.saturating_add(value as u64);
687 }
688 }
689
690 fn add_skipped<const STATS: bool>(&mut self, value: usize) {
691 if STATS {
692 self.stats.skipped_matches = self.stats.skipped_matches.saturating_add(value as u64);
693 }
694 }
695}
696
697#[cfg(test)]
698mod tests {
699 use super::*;
700
701 fn entry_start(memo: &GeometricMemo, diagonal: usize, end: usize) -> Option<usize> {
702 memo.entries
703 .iter()
704 .find(|entry| entry.diagonal == diagonal && entry.end == end)
705 .map(|entry| entry.start)
706 }
707
708 fn config() -> MemoConfig {
709 MemoConfig {
710 probe: 64,
711 min_lcp: 128,
712 capacity: 64,
713 activate_entries: 1,
714 }
715 }
716
717 fn naive_lcp<S: Symbol>(text: &[S], p: usize, q: usize, cap: usize) -> usize {
718 let lim = text
719 .len()
720 .saturating_sub(p)
721 .min(text.len().saturating_sub(q))
722 .min(cap);
723 (0..lim).take_while(|&i| text[p + i] == text[q + i]).count()
724 }
725
726 fn repeated_blocks() -> Vec<u8> {
727 let mut text = vec![b'A'; 6_100];
728 text[2_000] = b'C';
729 text[5_000] = b'G';
730 text
731 }
732
733 #[test]
734 fn direct_hit_returns_exact_subsumed_lcp() {
735 let text = repeated_blocks();
736 let dispatch = LcpDispatch::detect();
737 let mut memo = GeometricMemo::new(config());
738 assert_eq!(
739 memo.lcp_profiled(&text, dispatch, 0, 3_000, 0, 2_500),
740 2_000
741 );
742 assert_eq!(
743 memo.lcp_profiled(&text, dispatch, 500, 3_500, 0, 2_000),
744 1_500
745 );
746 let stats = memo.finish();
747 assert_eq!(stats.inserts, 1);
748 assert_eq!(stats.direct_hits, 1);
749 assert!(stats.skipped_matches >= 1_400);
750 }
751
752 #[test]
753 fn gap_hit_extends_existing_endpoint() {
754 let text = repeated_blocks();
755 let dispatch = LcpDispatch::detect();
756 let mut memo = GeometricMemo::new(config());
757 assert_eq!(
758 memo.lcp_profiled(&text, dispatch, 500, 3_500, 0, 2_000),
759 1_500
760 );
761 assert_eq!(
762 memo.lcp_profiled(&text, dispatch, 0, 3_000, 0, 2_500),
763 2_000
764 );
765 assert_eq!(entry_start(&memo, 3_000, 2_000), Some(0));
766 let stats = memo.finish();
767 assert_eq!(stats.gap_hits, 1);
768 assert_eq!(stats.extensions, 1);
769 }
770
771 #[test]
772 fn capped_match_is_not_admitted_as_exact() {
773 let text = repeated_blocks();
774 let dispatch = LcpDispatch::detect();
775 let mut memo = GeometricMemo::new(config());
776 assert_eq!(memo.lcp(&text, dispatch, 0, 3_000, 0, 512), 512);
777 assert!(memo.entries.is_empty());
778 }
779
780 #[test]
781 fn known_prefix_is_included_in_admitted_interval() {
782 let text = repeated_blocks();
783 let dispatch = LcpDispatch::detect();
784 let mut memo = GeometricMemo::new(config());
785 assert_eq!(memo.lcp(&text, dispatch, 0, 3_000, 1_000, 1_500), 1_000);
786 assert_eq!(entry_start(&memo, 3_000, 2_000), Some(0));
787 }
788
789 #[test]
790 fn randomized_queries_match_naive_for_u8_and_i8() {
791 let mut state = 0xd1b5_4a32_d192_ed03u64;
792 let mut next = || {
793 state ^= state << 13;
794 state ^= state >> 7;
795 state ^= state << 17;
796 state
797 };
798 let text_u8: Vec<u8> = (0..16_384).map(|_| (next() % 5) as u8).collect();
799 let text_i8: Vec<i8> = text_u8.iter().map(|&x| x as i8 - 2).collect();
800 check_randomized(&text_u8, &mut next);
801 check_randomized(&text_i8, &mut next);
802 }
803
804 fn check_randomized<S: Symbol>(text: &[S], next: &mut impl FnMut() -> u64) {
805 let dispatch = LcpDispatch::detect();
806 let mut memo = GeometricMemo::new(MemoConfig {
807 probe: 4,
808 min_lcp: 8,
809 capacity: 1_024,
810 activate_entries: 1,
811 });
812 for _ in 0..20_000 {
813 let p = next() as usize % (text.len() - 1);
814 let mut q = next() as usize % (text.len() - 1);
815 if q == p {
816 q = (q + 1) % (text.len() - 1);
817 }
818 let full = naive_lcp(text, p, q, usize::MAX);
819 let known = if full == 0 {
820 0
821 } else {
822 next() as usize % (full + 1)
823 };
824 let cap = 1 + next() as usize % 64;
825 let want = naive_lcp(text, p + known, q + known, cap);
826 let got = memo.lcp(text, dispatch, p, q, known, cap);
827 assert_eq!(got, want, "p={p} q={q} known={known} cap={cap}");
828 }
829 }
830
831 #[test]
832 fn capacity_is_strict_but_existing_endpoint_can_extend() {
833 let text = repeated_blocks();
834 let dispatch = LcpDispatch::detect();
835 let mut memo = GeometricMemo::new(MemoConfig {
836 capacity: 1,
837 ..config()
838 });
839 assert_eq!(memo.lcp(&text, dispatch, 500, 3_500, 0, 2_000), 1_500);
840 let _ = memo.lcp(&text, dispatch, 0, 3_001, 0, 2_500);
842 assert_eq!(memo.lcp(&text, dispatch, 0, 3_000, 0, 2_500), 2_000);
844 assert_eq!(memo.entries.len(), 1);
845 assert_eq!(entry_start(&memo, 3_000, 2_000), Some(0));
846 }
847}