use std::collections::HashMap;
use crate::tokenizer::TokenCounter;
const MAX_PHRASE_WORDS: usize = 32;
struct Candidate {
ids: Vec<u32>,
first: usize,
starts: Vec<usize>,
}
fn intern(segments: &[&str]) -> (Vec<u32>, Vec<String>) {
let mut vocab: Vec<String> = Vec::new();
let mut lookup: HashMap<&str, u32> = HashMap::new();
let mut ids: Vec<u32> = Vec::new();
let mut sentinel_slots: Vec<usize> = Vec::new();
for seg in segments {
for w in seg.split_whitespace() {
let id = *lookup.entry(w).or_insert_with(|| {
let id = vocab.len() as u32;
vocab.push(w.to_string());
id
});
ids.push(id);
}
sentinel_slots.push(ids.len());
ids.push(0); }
let base = vocab.len() as u32;
for (k, &pos) in sentinel_slots.iter().enumerate() {
ids[pos] = base + k as u32;
}
(ids, vocab)
}
fn suffix_array(s: &[u32]) -> Vec<u32> {
let n = s.len();
if n == 0 {
return Vec::new();
}
let mut sa: Vec<u32> = (0..n as u32).collect();
let mut rank: Vec<i64> = s.iter().map(|&c| c as i64).collect();
let mut tmp: Vec<i64> = vec![0; n];
let mut k = 1usize;
while k < n {
let key = |i: usize| -> (i64, i64) {
let second = if i + k < n { rank[i + k] } else { -1 };
(rank[i], second)
};
sa.sort_by_key(|&a| key(a as usize));
tmp[sa[0] as usize] = 0;
for i in 1..n {
let prev = sa[i - 1] as usize;
let cur = sa[i] as usize;
tmp[cur] = tmp[prev] + i64::from(key(prev) != key(cur));
}
std::mem::swap(&mut rank, &mut tmp);
if rank[sa[n - 1] as usize] == (n as i64 - 1) {
break; }
k <<= 1;
}
sa
}
fn kasai_lcp(s: &[u32], sa: &[u32]) -> Vec<usize> {
let n = s.len();
let mut rank = vec![0usize; n];
for (i, &p) in sa.iter().enumerate() {
rank[p as usize] = i;
}
let mut lcp = vec![0usize; n];
let mut h = 0usize;
for i in 0..n {
if rank[i] > 0 {
let j = sa[rank[i] - 1] as usize;
while i + h < n && j + h < n && s[i + h] == s[j + h] {
h += 1;
}
lcp[rank[i]] = h;
h = h.saturating_sub(1);
} else {
h = 0;
}
}
lcp
}
fn enumerate_repeats(s: &[u32], sa: &[u32], lcp: &[usize], sentinel_floor: u32) -> Vec<Candidate> {
let n = s.len();
let mut seen: HashMap<&[u32], usize> = HashMap::new();
let mut cands: Vec<Candidate> = Vec::new();
for i in 1..n {
let common = lcp[i].min(MAX_PHRASE_WORDS);
if common < 2 {
continue;
}
let a = sa[i - 1] as usize;
let b = sa[i] as usize;
for len in 2..=common {
let start = b;
let slice = &s[start..start + len];
if slice.iter().any(|&id| id >= sentinel_floor) {
break; }
match seen.get(slice).copied() {
Some(idx) => {
let c: &mut Candidate = &mut cands[idx];
c.starts.push(a);
c.starts.push(b);
}
None => {
let idx = cands.len();
seen.insert(slice, idx);
cands.push(Candidate {
ids: slice.to_vec(),
first: a.min(b),
starts: vec![a, b],
});
}
}
}
}
for c in cands.iter_mut() {
c.starts.sort_unstable();
c.starts.dedup();
let len = c.ids.len();
let mut kept: Vec<usize> = Vec::with_capacity(c.starts.len());
let mut last_end = 0usize;
let mut first_set = false;
for &st in &c.starts {
if !first_set || st >= last_end {
if !first_set {
c.first = st;
first_set = true;
}
kept.push(st);
last_end = st + len;
}
}
c.starts = kept;
}
cands.retain(|c| c.starts.len() >= 2);
cands
}
pub(crate) struct Selection {
pub phrase: String,
}
pub(crate) fn mine(
segments: &[&str],
max_entries: usize,
counter: &dyn TokenCounter,
placeholder_for: impl Fn(usize) -> String,
) -> Vec<Selection> {
if max_entries == 0 {
return Vec::new();
}
let (ids, vocab) = intern(segments);
if ids.len() < 3 {
return Vec::new();
}
let sentinel_floor = vocab.len() as u32;
let sa = suffix_array(&ids);
let lcp = kasai_lcp(&ids, &sa);
let cands = enumerate_repeats(&ids, &sa, &lcp, sentinel_floor);
if cands.is_empty() {
return Vec::new();
}
let phrase_text: Vec<String> = cands
.iter()
.map(|c| {
c.ids
.iter()
.map(|&id| vocab[id as usize].as_str())
.collect::<Vec<_>>()
.join(" ")
})
.collect();
let phrase_tokens: Vec<usize> = phrase_text.iter().map(|p| counter.count(p)).collect();
let mut claimed = vec![false; ids.len()];
let mut chosen: Vec<Selection> = Vec::new();
let mut used: Vec<bool> = vec![false; cands.len()];
let gain_of = |i: usize, next_index: usize, claimed: &[bool]| -> (i64, usize) {
let len = cands[i].ids.len();
let live: usize = cands[i]
.starts
.iter()
.filter(|&&st| !claimed[st..st + len].iter().any(|&c| c))
.count();
if live < 2 {
return (i64::MIN, live);
}
let ph = placeholder_for(next_index);
let ph_tokens = counter.count(&ph);
let legend_entry = format!("{ph}={}; ", phrase_text[i]);
let legend_tokens = counter.count(&legend_entry);
let per = phrase_tokens[i] as i64 - ph_tokens as i64;
let gain = (live as i64) * per - legend_tokens as i64;
(gain, live)
};
while chosen.len() < max_entries {
let next_index = chosen.len() + 1;
let mut best: Option<(usize, i64)> = None;
for i in 0..cands.len() {
if used[i] {
continue;
}
let (g, _live) = gain_of(i, next_index, &claimed);
if g <= 0 {
continue;
}
let take = match best {
None => true,
Some((bi, bg)) => {
g > bg
|| (g == bg
&& better_tiebreak(
cands[i].ids.len(),
cands[i].first,
&phrase_text[i],
cands[bi].ids.len(),
cands[bi].first,
&phrase_text[bi],
))
}
};
if take {
best = Some((i, g));
}
}
let Some((bi, _)) = best else {
break; };
used[bi] = true;
let len = cands[bi].ids.len();
for &st in &cands[bi].starts {
if !claimed[st..st + len].iter().any(|&c| c) {
for c in &mut claimed[st..st + len] {
*c = true;
}
}
}
chosen.push(Selection {
phrase: phrase_text[bi].clone(),
});
}
chosen
}
#[allow(clippy::too_many_arguments)]
fn better_tiebreak(
a_len: usize,
a_first: usize,
a_text: &str,
b_len: usize,
b_first: usize,
b_text: &str,
) -> bool {
(a_len, std::cmp::Reverse(a_first), std::cmp::Reverse(b_text))
> (b_len, std::cmp::Reverse(b_first), std::cmp::Reverse(a_text))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn suffix_array_matches_naive() {
let s: Vec<u32> = vec![3, 1, 2, 1, 2, 1, 0];
let sa = suffix_array(&s);
let mut naive: Vec<usize> = (0..s.len()).collect();
naive.sort_by(|&a, &b| s[a..].cmp(&s[b..]));
let sa_us: Vec<usize> = sa.iter().map(|&x| x as usize).collect();
assert_eq!(sa_us, naive, "doubling SA must equal the naive suffix sort");
}
#[test]
fn lcp_is_consistent_with_sa() {
let s: Vec<u32> = vec![2, 1, 2, 1, 2, 1, 0];
let sa = suffix_array(&s);
let lcp = kasai_lcp(&s, &sa);
for i in 1..s.len() {
let a = sa[i - 1] as usize;
let b = sa[i] as usize;
let mut h = 0;
while a + h < s.len() && b + h < s.len() && s[a + h] == s[b + h] {
h += 1;
}
assert_eq!(lcp[i], h, "Kasai LCP must match the direct prefix length");
}
}
#[test]
fn enumerate_finds_the_long_repeat() {
let seg = "a b c d e x a b c d e y a b c d e";
let (ids, vocab) = intern(&[seg]);
let floor = vocab.len() as u32;
let sa = suffix_array(&ids);
let lcp = kasai_lcp(&ids, &sa);
let cands = enumerate_repeats(&ids, &sa, &lcp, floor);
let abcde: Vec<u32> = ["a", "b", "c", "d", "e"]
.iter()
.map(|w| ids[seg.split_whitespace().position(|x| x == *w).unwrap()])
.collect();
let hit = cands
.iter()
.find(|c| c.ids == abcde)
.expect("full repeat found");
assert_eq!(hit.starts.len(), 3, "all three non-overlapping occurrences");
}
#[test]
fn no_repeat_spans_a_segment_boundary() {
let segs = ["foo bar z y", "z y baz qux"];
let (ids, vocab) = intern(&segs);
let floor = vocab.len() as u32;
let sa = suffix_array(&ids);
let lcp = kasai_lcp(&ids, &sa);
let cands = enumerate_repeats(&ids, &sa, &lcp, floor);
for c in &cands {
assert!(
c.ids.iter().all(|&id| id < floor),
"no phrase may contain a sentinel"
);
}
}
}