1use crate::Index;
34use crate::lcp::{LcpDispatch, Symbol};
35use crate::lcp_memo::GeometricMemo;
36use crate::limits::{LimitProvider, PlainText};
37use rayon::join;
38
39const PREFETCH_DISTANCE: usize = 8;
43
44#[inline(always)]
51fn prefetch_symbol<S>(text: &[S], at: usize) {
52 let _ = (text, at);
53 #[cfg(target_arch = "x86_64")]
54 unsafe {
55 std::arch::x86_64::_mm_prefetch(
56 text.as_ptr().add(at.min(text.len())) as *const i8,
57 std::arch::x86_64::_MM_HINT_T0,
58 );
59 }
60 #[cfg(target_arch = "aarch64")]
61 unsafe {
62 let p = text.as_ptr().add(at.min(text.len()));
65 std::arch::asm!("prfm pldl1keep, [{p}]", p = in(reg) p, options(nostack, readonly, preserves_flags));
66 }
67}
68
69#[derive(Clone, Debug)]
71pub struct Opts {
72 pub max_context: usize,
77}
78
79impl Default for Opts {
80 fn default() -> Self {
81 Self {
82 max_context: usize::MAX,
83 }
84 }
85}
86
87pub fn build_in_memory<S, I>(text: &[S]) -> Vec<I>
98where
99 S: Symbol,
100 I: Index,
101{
102 build_in_memory_with_opts(text, &Opts::default())
103}
104
105pub fn build_in_memory_with_opts<S, I>(text: &[S], opts: &Opts) -> Vec<I>
107where
108 S: Symbol,
109 I: Index,
110{
111 build_in_memory_with(text, &PlainText::new(text.len()), opts)
112}
113
114pub fn build_in_memory_with<S, I, L>(text: &[S], lp: &L, opts: &Opts) -> Vec<I>
119where
120 S: Symbol,
121 I: Index,
122 L: LimitProvider,
123{
124 let n = text.len();
125 let positions: Vec<I> = (0..n).map(I::from_usize).collect();
126 build_in_memory_for_positions_with(text, positions, lp, opts)
127}
128
129pub fn build_in_memory_for_positions<S, I>(text: &[S], positions: Vec<I>) -> Vec<I>
144where
145 S: Symbol,
146 I: Index,
147{
148 build_in_memory_for_positions_with_opts(text, positions, &Opts::default())
149}
150
151pub fn build_in_memory_for_positions_with_opts<S, I>(
153 text: &[S],
154 positions: Vec<I>,
155 opts: &Opts,
156) -> Vec<I>
157where
158 S: Symbol,
159 I: Index,
160{
161 build_in_memory_for_positions_with(text, positions, &PlainText::new(text.len()), opts)
162}
163
164pub fn build_in_memory_for_positions_with<S, I, L>(
169 text: &[S],
170 positions: Vec<I>,
171 lp: &L,
172 opts: &Opts,
173) -> Vec<I>
174where
175 S: Symbol,
176 I: Index,
177 L: LimitProvider,
178{
179 let n = positions.len();
180 if n == 0 {
181 return Vec::new();
182 }
183
184 let mut sa: Vec<I> = positions;
185 let mut sa_w: Vec<I> = vec![I::zero(); n];
186 let mut lcp_arr: Vec<I> = vec![I::zero(); n];
187 let mut lcp_w: Vec<I> = vec![I::zero(); n];
188
189 let dispatch = LcpDispatch::detect();
193
194 merge_sort(
195 text,
196 lp,
197 &mut sa,
198 &mut sa_w,
199 &mut lcp_arr,
200 &mut lcp_w,
201 opts.max_context,
202 dispatch,
203 );
204
205 sa
206}
207
208#[allow(clippy::too_many_arguments)] pub(crate) fn merge_sort<S, I, L>(
222 text: &[S],
223 lp: &L,
224 sa: &mut [I],
225 sa_w: &mut [I],
226 lcp_arr: &mut [I],
227 lcp_w: &mut [I],
228 max_ctx: usize,
229 dispatch: LcpDispatch,
230) where
231 S: Symbol,
232 I: Index,
233 L: LimitProvider,
234{
235 let n = sa.len();
236 debug_assert_eq!(sa_w.len(), n);
237 debug_assert_eq!(lcp_arr.len(), n);
238 debug_assert_eq!(lcp_w.len(), n);
239
240 if n <= 1 {
241 if n == 1 {
242 lcp_arr[0] = I::zero();
243 }
244 return;
245 }
246
247 let mid = n / 2;
248 let (sa_l, sa_r) = sa.split_at_mut(mid);
249 let (sa_w_l, sa_w_r) = sa_w.split_at_mut(mid);
250 let (lcp_l, lcp_r) = lcp_arr.split_at_mut(mid);
251 let (lcp_w_l, lcp_w_r) = lcp_w.split_at_mut(mid);
252
253 join(
254 || merge_sort(text, lp, sa_l, sa_w_l, lcp_l, lcp_w_l, max_ctx, dispatch),
255 || merge_sort(text, lp, sa_r, sa_w_r, lcp_r, lcp_w_r, max_ctx, dispatch),
256 );
257
258 merge(
262 text, lp, sa_l, sa_r, lcp_l, lcp_r, sa_w, lcp_w, max_ctx, dispatch,
263 );
264 sa.copy_from_slice(sa_w);
265 lcp_arr.copy_from_slice(lcp_w);
266}
267
268#[allow(clippy::too_many_arguments)]
274pub(crate) fn merge_sort_task_local<S, I, L>(
275 text: &[S],
276 lp: &L,
277 sa: &mut [I],
278 sa_w: &mut [I],
279 lcp_arr: &mut [I],
280 lcp_w: &mut [I],
281 max_ctx: usize,
282 dispatch: LcpDispatch,
283) where
284 S: Symbol,
285 I: Index,
286 L: LimitProvider,
287{
288 debug_assert_eq!(sa.len(), sa_w.len());
289 debug_assert_eq!(sa.len(), lcp_arr.len());
290 debug_assert_eq!(sa.len(), lcp_w.len());
291 if sa.is_empty() {
292 return;
293 }
294
295 sa_w.copy_from_slice(sa);
299 merge_sort_ping_pong(text, lp, sa_w, lcp_w, sa, lcp_arr, max_ctx, dispatch);
300}
301
302#[allow(clippy::too_many_arguments)]
303fn merge_sort_ping_pong<S, I, L>(
304 text: &[S],
305 lp: &L,
306 src_sa: &mut [I],
307 src_lcp: &mut [I],
308 dst_sa: &mut [I],
309 dst_lcp: &mut [I],
310 max_ctx: usize,
311 dispatch: LcpDispatch,
312) where
313 S: Symbol,
314 I: Index,
315 L: LimitProvider,
316{
317 let n = src_sa.len();
318 debug_assert_eq!(src_lcp.len(), n);
319 debug_assert_eq!(dst_sa.len(), n);
320 debug_assert_eq!(dst_lcp.len(), n);
321 if n <= 1 {
322 if n == 1 {
323 dst_sa[0] = src_sa[0];
324 dst_lcp[0] = I::zero();
325 }
326 return;
327 }
328
329 let mid = n / 2;
330 {
331 let (src_sa_l, src_sa_r) = src_sa.split_at_mut(mid);
332 let (src_lcp_l, src_lcp_r) = src_lcp.split_at_mut(mid);
333 let (dst_sa_l, dst_sa_r) = dst_sa.split_at_mut(mid);
334 let (dst_lcp_l, dst_lcp_r) = dst_lcp.split_at_mut(mid);
335 merge_sort_ping_pong(
336 text, lp, dst_sa_l, dst_lcp_l, src_sa_l, src_lcp_l, max_ctx, dispatch,
337 );
338 merge_sort_ping_pong(
339 text, lp, dst_sa_r, dst_lcp_r, src_sa_r, src_lcp_r, max_ctx, dispatch,
340 );
341 }
342
343 let (src_sa_l, src_sa_r) = src_sa.split_at(mid);
344 let (src_lcp_l, src_lcp_r) = src_lcp.split_at(mid);
345 merge(
346 text, lp, src_sa_l, src_sa_r, src_lcp_l, src_lcp_r, dst_sa, dst_lcp, max_ctx, dispatch,
347 );
348}
349
350macro_rules! merge_extension {
359 (direct, $text:expr, $dispatch:expr, $p:expr, $q:expr, $known:expr, $max_ext:expr) => {
360 $dispatch.lcp($text, $p + $known, $q + $known, $max_ext)
361 };
362 ((memo $memo:ident), $text:expr, $dispatch:expr, $p:expr, $q:expr, $known:expr, $max_ext:expr) => {{
363 let probe = $memo.probe($max_ext);
364 let got = $dispatch.lcp($text, $p + $known, $q + $known, probe);
365 if got < probe || probe == $max_ext {
366 got
367 } else {
368 $memo.lcp_after_probe($text, $dispatch, $p, $q, $known, probe, $max_ext)
369 }
370 }};
371 ((memo_profiled $memo:ident), $text:expr, $dispatch:expr, $p:expr, $q:expr, $known:expr, $max_ext:expr) => {{
372 let probe = $memo.probe($max_ext);
373 let got = $dispatch.lcp($text, $p + $known, $q + $known, probe);
374 $memo.record_probe_profiled(got, probe, $max_ext);
375 if got < probe || probe == $max_ext {
376 got
377 } else {
378 $memo.lcp_after_probe_profiled($text, $dispatch, $p, $q, $known, probe, $max_ext)
379 }
380 }};
381 ((training $memo:ident), $text:expr, $dispatch:expr, $p:expr, $q:expr, $known:expr, $max_ext:expr) => {{
382 let got = $dispatch.lcp($text, $p + $known, $q + $known, $max_ext);
383 $memo.observe_training($p, $q, $known, got, $max_ext);
384 got
385 }};
386 ((training_profiled $memo:ident), $text:expr, $dispatch:expr, $p:expr, $q:expr, $known:expr, $max_ext:expr) => {{
387 let got = $dispatch.lcp($text, $p + $known, $q + $known, $max_ext);
388 $memo.observe_training_profiled($p, $q, $known, got, $max_ext);
389 got
390 }};
391}
392
393macro_rules! merge_body {
398 ($lookup:tt; $text:ident, $lp:ident, $x:ident, $y:ident, $lcp_x:ident, $lcp_y:ident, $z:ident, $lcp_z:ident, $max_ctx:ident, $dispatch:ident) => {{
399 let len_x = $x.len();
400 let len_y = $y.len();
401 debug_assert_eq!($z.len(), len_x + len_y);
402 debug_assert_eq!($lcp_z.len(), len_x + len_y);
403
404 if len_x == 0 {
405 $z.copy_from_slice($y);
406 $lcp_z.copy_from_slice($lcp_y);
407 return;
408 }
409 if len_y == 0 {
410 $z.copy_from_slice($x);
411 $lcp_z.copy_from_slice($lcp_x);
412 return;
413 }
414
415 let mut arr_a: &[I] = $x;
418 let mut arr_b: &[I] = $y;
419 let mut lcp_a: &[I] = $lcp_x;
420 let mut lcp_b: &[I] = $lcp_y;
421 let mut len_a = len_x;
422 let mut len_b = len_y;
423 let mut i_a: usize = 0;
424 let mut i_b: usize = 0;
425 let mut m: usize = 0;
426 let mut k: usize = 0;
427 let mut lim_a_cache: Option<(usize, usize)> = None;
428 let mut lim_b_cache: Option<(usize, usize)> = None;
429
430 while i_a < len_a && i_b < len_b {
431 if i_a + PREFETCH_DISTANCE < len_a {
432 prefetch_symbol($text, arr_a[i_a + PREFETCH_DISTANCE].to_usize() + m);
433 }
434 if i_b + PREFETCH_DISTANCE < len_b {
435 prefetch_symbol($text, arr_b[i_b + PREFETCH_DISTANCE].to_usize() + m);
436 }
437
438 let l_a = lcp_a[i_a].to_usize();
439 let (output_a, lcp_for_output, new_m) = if l_a > m {
440 (true, l_a, m)
441 } else if l_a < m {
442 (false, m, l_a)
443 } else {
444 let p_a = arr_a[i_a].to_usize();
445 let p_b = arr_b[i_b].to_usize();
446 let lim_a = match lim_a_cache {
447 Some((idx, lim)) if idx == i_a => lim,
448 _ => {
449 let lim = $lp.lim_at(p_a);
450 lim_a_cache = Some((i_a, lim));
451 lim
452 }
453 };
454 let lim_b = match lim_b_cache {
455 Some((idx, lim)) if idx == i_b => lim,
456 _ => {
457 let lim = $lp.lim_at(p_b);
458 lim_b_cache = Some((i_b, lim));
459 lim
460 }
461 };
462 let cap = lim_a.min(lim_b).min($max_ctx);
463 let remaining_ctx = cap.saturating_sub(m);
464 let ext = merge_extension!($lookup, $text, $dispatch, p_a, p_b, m, remaining_ctx);
465 let total = m + ext;
466 let a_smaller = if total < cap {
472 $text[p_a + total] < $text[p_b + total]
473 } else {
474 $lp.boundary_order(p_a, lim_a, p_b, lim_b).is_lt()
475 };
476 (a_smaller, m, total)
477 };
478
479 if output_a {
480 $z[k] = arr_a[i_a];
481 $lcp_z[k] = I::from_usize(lcp_for_output);
482 i_a += 1;
483 lim_a_cache = None;
484 } else {
485 $z[k] = arr_b[i_b];
486 $lcp_z[k] = I::from_usize(lcp_for_output);
487 i_b += 1;
488 lim_b_cache = None;
489 std::mem::swap(&mut arr_a, &mut arr_b);
490 std::mem::swap(&mut lcp_a, &mut lcp_b);
491 std::mem::swap(&mut len_a, &mut len_b);
492 std::mem::swap(&mut i_a, &mut i_b);
493 std::mem::swap(&mut lim_a_cache, &mut lim_b_cache);
494 }
495 m = new_m;
496 k += 1;
497 }
498
499 drain(arr_a, lcp_a, i_a, len_a, $z, $lcp_z, &mut k, m);
500 drain(arr_b, lcp_b, i_b, len_b, $z, $lcp_z, &mut k, m);
501 }};
502}
503
504#[allow(clippy::too_many_arguments)] pub(crate) fn merge<S, I, L>(
506 text: &[S],
507 lp: &L,
508 x: &[I],
509 y: &[I],
510 lcp_x: &[I],
511 lcp_y: &[I],
512 z: &mut [I],
513 lcp_z: &mut [I],
514 max_ctx: usize,
515 dispatch: LcpDispatch,
516) where
517 S: Symbol,
518 I: Index,
519 L: LimitProvider,
520{
521 merge_body!(direct; text, lp, x, y, lcp_x, lcp_y, z, lcp_z, max_ctx, dispatch);
522}
523
524#[allow(clippy::too_many_arguments)]
527pub(crate) fn merge_memoized<S, I, L>(
528 text: &[S],
529 lp: &L,
530 x: &[I],
531 y: &[I],
532 lcp_x: &[I],
533 lcp_y: &[I],
534 z: &mut [I],
535 lcp_z: &mut [I],
536 max_ctx: usize,
537 dispatch: LcpDispatch,
538 memo: &mut GeometricMemo,
539) where
540 S: Symbol,
541 I: Index,
542 L: LimitProvider,
543{
544 merge_body!((memo memo); text, lp, x, y, lcp_x, lcp_y, z, lcp_z, max_ctx, dispatch);
545}
546
547#[allow(clippy::too_many_arguments)]
550pub(crate) fn merge_memoized_profiled<S, I, L>(
551 text: &[S],
552 lp: &L,
553 x: &[I],
554 y: &[I],
555 lcp_x: &[I],
556 lcp_y: &[I],
557 z: &mut [I],
558 lcp_z: &mut [I],
559 max_ctx: usize,
560 dispatch: LcpDispatch,
561 memo: &mut GeometricMemo,
562) where
563 S: Symbol,
564 I: Index,
565 L: LimitProvider,
566{
567 merge_body!((memo_profiled memo); text, lp, x, y, lcp_x, lcp_y, z, lcp_z, max_ctx, dispatch);
568}
569
570#[allow(clippy::too_many_arguments)]
571pub(crate) fn merge_memoized_training<S, I, L>(
572 text: &[S],
573 lp: &L,
574 x: &[I],
575 y: &[I],
576 lcp_x: &[I],
577 lcp_y: &[I],
578 z: &mut [I],
579 lcp_z: &mut [I],
580 max_ctx: usize,
581 dispatch: LcpDispatch,
582 memo: &mut GeometricMemo,
583) where
584 S: Symbol,
585 I: Index,
586 L: LimitProvider,
587{
588 merge_body!((training memo); text, lp, x, y, lcp_x, lcp_y, z, lcp_z, max_ctx, dispatch);
589}
590
591#[allow(clippy::too_many_arguments)]
592pub(crate) fn merge_memoized_training_profiled<S, I, L>(
593 text: &[S],
594 lp: &L,
595 x: &[I],
596 y: &[I],
597 lcp_x: &[I],
598 lcp_y: &[I],
599 z: &mut [I],
600 lcp_z: &mut [I],
601 max_ctx: usize,
602 dispatch: LcpDispatch,
603 memo: &mut GeometricMemo,
604) where
605 S: Symbol,
606 I: Index,
607 L: LimitProvider,
608{
609 merge_body!((training_profiled memo); text, lp, x, y, lcp_x, lcp_y, z, lcp_z, max_ctx, dispatch);
610}
611
612#[inline]
613#[allow(clippy::too_many_arguments)] fn drain<I: Index>(
615 arr: &[I],
616 lcp_src: &[I],
617 mut i: usize,
618 len: usize,
619 z: &mut [I],
620 lcp_z: &mut [I],
621 k: &mut usize,
622 boundary_m: usize,
623) {
624 let mut first = true;
625 while i < len {
626 z[*k] = arr[i];
627 lcp_z[*k] = if first {
628 I::from_usize(boundary_m)
629 } else {
630 lcp_src[i]
631 };
632 first = false;
633 i += 1;
634 *k += 1;
635 }
636}
637
638#[cfg(test)]
639mod tests {
640 use super::*;
641
642 fn brute_force_sa(text: &[u8]) -> Vec<u32> {
644 let mut sa: Vec<u32> = (0..text.len() as u32).collect();
645 sa.sort_by(|&a, &b| text[a as usize..].cmp(&text[b as usize..]));
646 sa
647 }
648
649 fn assert_matches_brute(text: &[u8]) {
650 let got: Vec<u32> = build_in_memory(text);
651 let want = brute_force_sa(text);
652 assert_eq!(got, want, "mismatch on text {text:?}");
653 }
654
655 fn build_sa_and_lcp(text: &[u8], max_ctx: usize) -> (Vec<u32>, Vec<u32>) {
663 let n = text.len();
664 let mut sa: Vec<u32> = (0..n as u32).collect();
665 let mut sa_w = vec![0u32; n];
666 let mut lcp_arr = vec![0u32; n];
667 let mut lcp_w = vec![0u32; n];
668 merge_sort(
669 text,
670 &PlainText::new(n),
671 &mut sa,
672 &mut sa_w,
673 &mut lcp_arr,
674 &mut lcp_w,
675 max_ctx,
676 LcpDispatch::detect(),
677 );
678 (sa, lcp_arr)
679 }
680
681 fn naive_lcp(text: &[u8], a: usize, b: usize, max_ctx: usize) -> usize {
683 let lim = (text.len() - a).min(text.len() - b).min(max_ctx);
684 (0..lim).take_while(|&i| text[a + i] == text[b + i]).count()
685 }
686
687 fn assert_lcp_valid(text: &[u8], max_ctx: usize) {
690 let (sa, lcp) = build_sa_and_lcp(text, max_ctx);
691 if sa.is_empty() {
692 return;
693 }
694 assert_eq!(lcp[0], 0, "lcp[0] must be 0 (text {text:?})");
695 for i in 1..sa.len() {
696 let want = naive_lcp(text, sa[i - 1] as usize, sa[i] as usize, max_ctx);
697 assert_eq!(
698 lcp[i] as usize,
699 want,
700 "lcp[{i}] wrong for sa[{}]={} vs sa[{i}]={} (text {text:?})",
701 i - 1,
702 sa[i - 1],
703 sa[i],
704 );
705 }
706 }
707
708 #[test]
709 fn lcp_array_matches_naive_on_fixtures() {
710 for text in [
711 b"banana".as_slice(),
712 b"mississippi",
713 b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
714 b"abababababababababababababab",
715 b"a",
716 b"",
717 ] {
718 assert_lcp_valid(text, usize::MAX);
719 }
720 }
721
722 #[test]
723 fn lcp_array_matches_naive_on_random() {
724 use rand::{RngExt, SeedableRng};
725 let mut rng = rand::rngs::StdRng::seed_from_u64(0x1CB0);
726 for &sigma in &[2u8, 4, 6, 255] {
727 for &n in &[2usize, 3, 7, 16, 17, 63, 64, 65, 200, 1000, 5000] {
728 let text: Vec<u8> = (0..n).map(|_| rng.random_range(0..sigma)).collect();
729 assert_lcp_valid(&text, usize::MAX);
730 }
731 }
732 }
733
734 #[test]
738 fn lcp_array_on_long_runs_and_periodic_text() {
739 assert_lcp_valid(&vec![7u8; 2000], usize::MAX);
740 let periodic: Vec<u8> = (0..2000).map(|i| (i % 3) as u8).collect();
741 assert_lcp_valid(&periodic, usize::MAX);
742 let mut mixed: Vec<u8> = (0..500).map(|i| (i % 4) as u8).collect();
744 mixed.extend(std::iter::repeat_n(4u8, 1500));
745 mixed.extend((0..500).map(|i| (i % 4) as u8));
746 assert_lcp_valid(&mixed, usize::MAX);
747 }
748
749 #[test]
750 fn lcp_array_respects_max_context() {
751 use rand::{RngExt, SeedableRng};
752 let mut rng = rand::rngs::StdRng::seed_from_u64(0xC7A);
753 for &max_ctx in &[1usize, 2, 4, 16] {
754 for &n in &[64usize, 500] {
755 let text: Vec<u8> = (0..n).map(|_| rng.random_range(0..3u8)).collect();
756 let (sa, lcp) = build_sa_and_lcp(&text, max_ctx);
757 for i in 1..sa.len() {
758 let want = naive_lcp(&text, sa[i - 1] as usize, sa[i] as usize, max_ctx);
759 assert_eq!(
760 lcp[i] as usize, want,
761 "lcp[{i}] wrong with max_ctx={max_ctx}"
762 );
763 }
764 }
765 }
766 }
767
768 #[test]
769 fn suffix_array_respects_finite_max_context() {
770 use rand::{RngExt, SeedableRng};
771
772 let dispatch = LcpDispatch::detect();
773 let mut rng = rand::rngs::StdRng::seed_from_u64(0x0F11_7EC7);
774 for &max_ctx in &[0usize, 1, 2, 4, 16] {
775 for &n in &[2usize, 3, 7, 64, 500] {
776 let text: Vec<u8> = (0..n).map(|_| rng.random_range(0..3u8)).collect();
777 let opts = Opts {
778 max_context: max_ctx,
779 };
780 let got: Vec<u32> = build_in_memory_with_opts(&text, &opts);
781 let mut want: Vec<u32> = (0..n as u32).collect();
782 want.sort_by(|&a, &b| dispatch.suffix_cmp(&text, a as usize, b as usize, max_ctx));
783 assert_eq!(
784 got, want,
785 "finite-context SA mismatch (n={n}, max_ctx={max_ctx})"
786 );
787 }
788 }
789 }
790
791 #[test]
792 fn empty_text() {
793 let sa: Vec<u32> = build_in_memory::<u8, u32>(&[]);
794 assert!(sa.is_empty());
795 }
796
797 #[test]
798 fn single_symbol() {
799 let sa: Vec<u32> = build_in_memory(&[7u8]);
800 assert_eq!(sa, vec![0]);
801 }
802
803 #[test]
804 fn banana() {
805 assert_matches_brute(b"banana");
806 }
807
808 #[test]
809 fn mississippi() {
810 assert_matches_brute(b"mississippi");
811 }
812
813 #[test]
814 fn small_distinct_sentinel() {
815 let text: Vec<u8> = vec![0, 1, 2, 0, 1, 5, 0, 2, 1, 6];
818 let got: Vec<u32> = build_in_memory(&text);
819 let want = brute_force_sa(&text);
820 assert_eq!(got, want);
821 }
822
823 #[test]
824 fn random_byte_texts() {
825 use rand::{RngExt, SeedableRng};
826 let mut rng = rand::rngs::StdRng::seed_from_u64(0xC0FFEE);
827 for &n in &[1usize, 2, 3, 7, 33, 200, 1000] {
828 let text: Vec<u8> = (0..n).map(|_| rng.random_range(0..6u8)).collect();
829 let got: Vec<u32> = build_in_memory(&text);
830 let want = brute_force_sa(&text);
831 assert_eq!(got, want, "mismatch on random text len={n}");
832 }
833 }
834
835 #[test]
836 fn for_positions_full_set_matches_build_in_memory() {
837 let text = b"banana";
839 let want: Vec<u32> = build_in_memory(text);
840 let positions: Vec<u32> = (0..text.len() as u32).collect();
841 let got = build_in_memory_for_positions(text, positions);
842 assert_eq!(got, want);
843 }
844
845 #[test]
846 fn for_positions_subset_matches_brute_force() {
847 let text = b"mississippi";
850 let positions: Vec<u32> = (0..text.len() as u32).step_by(2).collect();
851 let mut want = positions.clone();
852 want.sort_by(|&a, &b| text[a as usize..].cmp(&text[b as usize..]));
853 let got = build_in_memory_for_positions(text, positions);
854 assert_eq!(got, want);
855 }
856
857 #[test]
858 fn for_positions_random_subsets() {
859 use rand::{RngExt, SeedableRng};
860 let mut rng = rand::rngs::StdRng::seed_from_u64(0xFEED);
861 for &n in &[33usize, 200, 1000] {
862 let text: Vec<u8> = (0..n).map(|_| rng.random_range(0..6u8)).collect();
863 let mut positions: Vec<u32> = (0..n as u32).collect();
865 positions.retain(|_| rng.random_range(0..10) < 7);
867 let mut want = positions.clone();
868 want.sort_by(|&a, &b| text[a as usize..].cmp(&text[b as usize..]));
869 let got = build_in_memory_for_positions(&text, positions);
870 assert_eq!(got, want, "subset sort mismatch n={n}");
871 }
872 }
873
874 #[test]
875 fn random_with_unique_terminator() {
876 use rand::{RngExt, SeedableRng};
879 let mut rng = rand::rngs::StdRng::seed_from_u64(0xBEEF);
880 for &n in &[1usize, 50, 500] {
881 let mut text: Vec<u8> = (0..n).map(|_| rng.random_range(0..5u8)).collect();
882 text.push(250); let got: Vec<u32> = build_in_memory(&text);
884 let want = brute_force_sa(&text);
885 assert_eq!(got, want);
886 }
887 }
888
889 use crate::limits::SegmentedText;
892
893 fn segmented_cmp(text: &[u8], lp: &SegmentedText, a: usize, b: usize) -> std::cmp::Ordering {
896 use crate::limits::LimitProvider;
897 let lim_a = lp.lim_at(a);
898 let lim_b = lp.lim_at(b);
899 let lim = lim_a.min(lim_b);
900 for i in 0..lim {
901 if text[a + i] != text[b + i] {
902 return text[a + i].cmp(&text[b + i]);
903 }
904 }
905 lim_a.cmp(&lim_b)
906 }
907
908 fn assert_segmented_sa_valid(text: &[u8], lengths: &[usize], positions: &[u32], sa: &[u32]) {
918 let lp = SegmentedText::from_lengths(text.len(), lengths);
919 let mut expected = positions.to_vec();
920 expected.sort();
921 let mut got_sorted = sa.to_vec();
922 got_sorted.sort();
923 assert_eq!(got_sorted, expected, "sa is not a permutation of positions");
924 for w in sa.windows(2) {
925 let a = w[0] as usize;
926 let b = w[1] as usize;
927 let ord = segmented_cmp(text, &lp, a, b);
928 assert_ne!(
929 ord,
930 std::cmp::Ordering::Greater,
931 "out of order: pos {a} > pos {b} under segmented comparator",
932 );
933 }
934 }
935
936 #[test]
937 fn segmented_in_memory_matches_brute_force_small() {
938 let text: Vec<u8> = b"helloworldbananamississippi".to_vec();
940 let lengths = &[5usize, 5, 6, 11];
941 let lp = SegmentedText::from_lengths(text.len(), lengths);
942 let sa: Vec<u32> = build_in_memory_with(&text, &lp, &Opts::default());
943 let all_positions: Vec<u32> = (0..text.len() as u32).collect();
944 assert_segmented_sa_valid(&text, lengths, &all_positions, &sa);
945 }
946
947 #[test]
948 fn segmented_single_segment_equals_unsegmented() {
949 let text = b"mississippi";
953 let lp = SegmentedText::from_lengths(text.len(), &[text.len()]);
954 let got_segmented: Vec<u32> = build_in_memory_with(text, &lp, &Opts::default());
955 let got_plain: Vec<u32> = build_in_memory(text);
956 assert_eq!(got_segmented, got_plain);
957 }
958
959 #[test]
960 fn segmented_random_validity() {
961 use rand::{RngExt, SeedableRng};
962 let mut rng = rand::rngs::StdRng::seed_from_u64(0x5E6);
963 for _ in 0..20 {
964 let n_segments = rng.random_range(1..10usize);
965 let lengths: Vec<usize> = (0..n_segments)
966 .map(|_| rng.random_range(5..50usize))
967 .collect();
968 let n: usize = lengths.iter().sum();
969 let text: Vec<u8> = (0..n).map(|_| rng.random_range(0..3u8)).collect();
971 let lp = SegmentedText::from_lengths(n, &lengths);
972 let sa: Vec<u32> = build_in_memory_with(&text, &lp, &Opts::default());
973 let all_positions: Vec<u32> = (0..n as u32).collect();
974 assert_segmented_sa_valid(&text, &lengths, &all_positions, &sa);
975 }
976 }
977
978 #[test]
979 fn segmented_for_positions_subset_validity() {
980 let text: Vec<u8> = b"helloworldbananamississippi".to_vec();
982 let lengths = &[5usize, 5, 6, 11];
983 let positions: Vec<u32> = (0..text.len() as u32).step_by(2).collect();
984 let lp = SegmentedText::from_lengths(text.len(), lengths);
985 let sa =
986 build_in_memory_for_positions_with(&text, positions.clone(), &lp, &Opts::default());
987 assert_segmented_sa_valid(&text, lengths, &positions, &sa);
988 }
989
990 struct StarConvention {
999 inner: SegmentedText,
1000 }
1001
1002 impl crate::limits::LimitProvider for StarConvention {
1003 fn lim_at(&self, p: usize) -> usize {
1004 self.inner.lim_at(p)
1005 }
1006 fn boundary_order(
1007 &self,
1008 p_a: usize,
1009 lim_a: usize,
1010 p_b: usize,
1011 lim_b: usize,
1012 ) -> std::cmp::Ordering {
1013 lim_b.cmp(&lim_a).then(p_a.cmp(&p_b))
1014 }
1015 }
1016
1017 fn star_brute_force_sa(text: &[u8], lengths: &[usize]) -> Vec<u32> {
1022 use crate::limits::LimitProvider;
1023 let lp = SegmentedText::from_lengths(text.len(), lengths);
1024 let mut sa: Vec<u32> = (0..text.len() as u32).collect();
1025 sa.sort_by(|&a, &b| {
1026 let pa = a as usize;
1027 let pb = b as usize;
1028 let lim_a = lp.lim_at(pa);
1029 let lim_b = lp.lim_at(pb);
1030 let lim = lim_a.min(lim_b);
1031 for i in 0..lim {
1032 if text[pa + i] != text[pb + i] {
1033 return text[pa + i].cmp(&text[pb + i]);
1034 }
1035 }
1036 lim_b.cmp(&lim_a).then(pa.cmp(&pb))
1038 });
1039 sa
1040 }
1041
1042 #[test]
1043 fn star_convention_matches_brute_force_small() {
1044 let text: Vec<u8> = b"helloworldbananamississippi".to_vec();
1046 let lengths = &[5usize, 5, 6, 11];
1047 let lp = StarConvention {
1048 inner: SegmentedText::from_lengths(text.len(), lengths),
1049 };
1050 let got: Vec<u32> = build_in_memory_with(&text, &lp, &Opts::default());
1051 let want = star_brute_force_sa(&text, lengths);
1052 assert_eq!(got, want, "STAR-convention SA mismatch");
1053 }
1054
1055 #[test]
1060 fn star_convention_within_segment_longer_first() {
1061 let text = b"AAAA";
1062 let lp = StarConvention {
1063 inner: SegmentedText::from_lengths(text.len(), &[text.len()]),
1064 };
1065 let got: Vec<u32> = build_in_memory_with(text, &lp, &Opts::default());
1066 assert_eq!(got, vec![0u32, 1, 2, 3]);
1070 }
1071
1072 #[test]
1073 fn star_convention_random_matches_brute_force() {
1074 use rand::{RngExt, SeedableRng};
1075 let mut rng = rand::rngs::StdRng::seed_from_u64(0xCAFE);
1076 for _ in 0..20 {
1077 let n_segments = rng.random_range(1..10usize);
1078 let lengths: Vec<usize> = (0..n_segments)
1079 .map(|_| rng.random_range(5..50usize))
1080 .collect();
1081 let n: usize = lengths.iter().sum();
1082 let text: Vec<u8> = (0..n).map(|_| rng.random_range(0..3u8)).collect();
1084 let lp = StarConvention {
1085 inner: SegmentedText::from_lengths(n, &lengths),
1086 };
1087 let got: Vec<u32> = build_in_memory_with(&text, &lp, &Opts::default());
1088 let want = star_brute_force_sa(&text, &lengths);
1089 assert_eq!(
1090 got, want,
1091 "STAR-convention SA mismatch (lengths={lengths:?}, text={text:?})",
1092 );
1093 }
1094 }
1095}