1use ahash::AHashMap;
2
3#[derive(Clone, Debug)]
4pub struct MatchModel {
6 hash_bits: usize,
7 min_len: usize,
8 max_len: usize,
9 stride_min: usize,
10 stride_max: usize,
11 base_mix: f64,
12 confidence_scale: f64,
13 history: Vec<u8>,
14 frozen_anchor: usize,
15 tables: Vec<AHashMap<u64, (usize, usize)>>,
16 pdf: [f64; 256],
17 cdf: [f64; 257],
18 valid: bool,
19 cdf_valid: bool,
20 predicted: Option<u8>,
21 match_len: usize,
22}
23
24#[derive(Clone, Debug)]
25pub(crate) struct MatchModelLifecycleSnapshot {
26 history: Vec<u8>,
27 frozen_anchor: usize,
28 pdf: [f64; 256],
29 cdf: [f64; 257],
30 valid: bool,
31 cdf_valid: bool,
32 predicted: Option<u8>,
33 match_len: usize,
34}
35
36impl MatchModel {
37 pub fn new(
39 hash_bits: usize,
40 min_len: usize,
41 max_len: usize,
42 gap_min: usize,
43 gap_max: usize,
44 base_mix: f64,
45 confidence_scale: f64,
46 ) -> Self {
47 let stride_min = gap_min.saturating_add(1);
48 let stride_max = gap_max.saturating_add(1).max(stride_min);
49 let mut tables = Vec::new();
50 for _ in stride_min..=stride_max {
51 tables.push(AHashMap::new());
52 }
53 Self {
54 hash_bits,
55 min_len: min_len.max(1),
56 max_len: max_len.max(min_len.max(1)),
57 stride_min,
58 stride_max,
59 base_mix: base_mix.clamp(1e-6, 0.99),
60 confidence_scale: confidence_scale.max(0.0),
61 history: Vec::new(),
62 frozen_anchor: 0,
63 tables,
64 pdf: [1.0 / 256.0; 256],
65 cdf: uniform_cdf(),
66 valid: false,
67 cdf_valid: false,
68 predicted: None,
69 match_len: 0,
70 }
71 }
72
73 pub fn new_contiguous(
75 hash_bits: usize,
76 min_len: usize,
77 max_len: usize,
78 base_mix: f64,
79 confidence_scale: f64,
80 ) -> Self {
81 Self::new(
82 hash_bits,
83 min_len,
84 max_len,
85 0,
86 0,
87 base_mix,
88 confidence_scale,
89 )
90 }
91
92 pub fn fill_pdf(&mut self, out: &mut [f64; 256]) {
94 self.ensure_pdf_inner(false);
95 out.copy_from_slice(&self.pdf);
96 }
97
98 pub fn pdf(&mut self) -> &[f64; 256] {
100 self.ensure_pdf_inner(false);
101 &self.pdf
102 }
103
104 pub fn cdf(&mut self) -> &[f64; 257] {
106 self.ensure_pdf_inner(true);
107 &self.cdf
108 }
109
110 pub fn log_prob(&mut self, symbol: u8, min_prob: f64) -> f64 {
112 self.ensure_pdf_inner(false);
113 self.pdf[symbol as usize].max(min_prob).ln()
114 }
115
116 pub fn update(&mut self, symbol: u8) {
118 if self.frozen_anchor > 0 {
119 self.frozen_anchor = 0;
120 }
121 self.history.push(symbol);
122 for stride in self.stride_min..=self.stride_max {
123 if let Some(key) = self.suffix_key(stride) {
124 let end = self.history.len() - 1;
125 self.tables[stride - self.stride_min]
126 .entry(key)
127 .and_modify(|entry| {
128 entry.1 = entry.0;
129 entry.0 = end;
130 })
131 .or_insert((end, usize::MAX));
132 }
133 }
134 self.valid = false;
135 self.cdf_valid = false;
136 }
137
138 pub fn reset_history(&mut self) {
140 if self.frozen_anchor > 0 {
141 self.history.truncate(self.frozen_anchor);
142 } else {
143 self.frozen_anchor = self.history.len();
144 }
145 self.valid = false;
146 self.cdf_valid = false;
147 self.predicted = None;
148 self.match_len = 0;
149 self.pdf.fill(1.0 / 256.0);
150 self.cdf = uniform_cdf();
151 }
152
153 pub(crate) fn lifecycle_snapshot(&self) -> MatchModelLifecycleSnapshot {
154 MatchModelLifecycleSnapshot {
155 history: self.history.clone(),
156 frozen_anchor: self.frozen_anchor,
157 pdf: self.pdf,
158 cdf: self.cdf,
159 valid: self.valid,
160 cdf_valid: self.cdf_valid,
161 predicted: self.predicted,
162 match_len: self.match_len,
163 }
164 }
165
166 pub(crate) fn restore_lifecycle_snapshot(&mut self, snapshot: MatchModelLifecycleSnapshot) {
167 self.history = snapshot.history;
168 self.frozen_anchor = snapshot.frozen_anchor;
169 self.pdf = snapshot.pdf;
170 self.cdf = snapshot.cdf;
171 self.valid = snapshot.valid;
172 self.cdf_valid = snapshot.cdf_valid;
173 self.predicted = snapshot.predicted;
174 self.match_len = snapshot.match_len;
175 }
176
177 pub fn update_history_only(&mut self, symbol: u8) {
179 if self.frozen_anchor == 0 {
180 self.frozen_anchor = self.history.len();
181 }
182 self.history.push(symbol);
183 self.valid = false;
184 self.cdf_valid = false;
185 }
186
187 pub fn match_len(&mut self) -> usize {
189 self.ensure_pdf_inner(false);
190 self.match_len
191 }
192
193 pub fn predicted_byte(&mut self) -> Option<u8> {
195 self.ensure_pdf_inner(false);
196 self.predicted
197 }
198
199 fn ensure_pdf_inner(&mut self, want_cdf: bool) {
200 if self.valid {
201 if want_cdf && !self.cdf_valid {
202 build_cdf_from_pdf(&self.pdf, &mut self.cdf);
203 self.cdf_valid = true;
204 }
205 return;
206 }
207 self.predicted = None;
208 self.match_len = 0;
209 self.pdf.fill(1.0 / 256.0);
210 let active_len = self.history.len().saturating_sub(self.frozen_anchor);
211 if active_len < self.min_len {
212 self.valid = true;
213 if want_cdf {
214 self.cdf = uniform_cdf();
215 self.cdf_valid = true;
216 } else {
217 self.cdf_valid = false;
218 }
219 return;
220 }
221
222 let mut best = None;
223 let history_limit = if self.frozen_anchor > 0 {
224 self.frozen_anchor
225 } else {
226 self.history.len()
227 };
228 for stride in self.stride_min..=self.stride_max {
229 let Some(key) = self.suffix_key(stride) else {
230 continue;
231 };
232 let Some(&(latest, previous)) = self.tables[stride - self.stride_min].get(&key) else {
233 continue;
234 };
235 let current_end = self.history.len() - 1;
236 let candidate_end = if latest == current_end {
237 previous
238 } else {
239 latest
240 };
241 if self.frozen_anchor > 0 && candidate_end >= self.frozen_anchor {
242 continue;
243 }
244 if candidate_end == usize::MAX || candidate_end + stride >= history_limit {
245 continue;
246 }
247 let matched = self.extend_match(candidate_end, stride);
248 if matched < self.min_len {
249 continue;
250 }
251 let predicted = self.history[candidate_end + stride];
252 match best {
253 Some((best_len, _, _)) if matched <= best_len => {}
254 _ => best = Some((matched, predicted, stride)),
255 }
256 }
257
258 if let Some((match_len, predicted, _stride)) = best {
259 self.predicted = Some(predicted);
260 self.match_len = match_len;
261 let base = 1.0 / 256.0;
262 let span = self.max_len.saturating_sub(self.min_len).max(1);
263 let covered = match_len.saturating_sub(self.min_len).min(span);
264 let confidence = ((covered as f64) / (span as f64)).sqrt() * self.confidence_scale;
265 let p_copy = (base + confidence.clamp(0.0, 1.0) * ((1.0 - self.base_mix) - base))
266 .clamp(base, 1.0 - self.base_mix);
267 let rest = ((1.0 - p_copy) / 255.0).max(0.0);
268 self.pdf.fill(rest);
269 self.pdf[predicted as usize] = p_copy;
270 }
271 if want_cdf {
272 build_cdf_from_pdf(&self.pdf, &mut self.cdf);
273 }
274 self.valid = true;
275 self.cdf_valid = want_cdf;
276 }
277
278 fn suffix_key(&self, stride: usize) -> Option<u64> {
279 let need = self
280 .min_len
281 .checked_sub(1)?
282 .saturating_mul(stride)
283 .saturating_add(1);
284 if self.history.len().saturating_sub(self.frozen_anchor) < need {
285 return None;
286 }
287 let mut h = 0x517C_C1B7_2722_0A95u64;
288 let mut idx = self.history.len() - 1;
289 for step in 0..self.min_len {
290 h ^= self.history[idx] as u64;
291 h = h.rotate_left(7).wrapping_mul(0x9E37_79B1);
292 if step + 1 == self.min_len {
293 break;
294 }
295 let boundary = self.frozen_anchor.saturating_add(stride);
296 if idx < boundary {
297 return None;
298 }
299 idx -= stride;
300 }
301 let bits = self.hash_bits.clamp(4, 63);
302 Some(h & ((1u64 << bits) - 1))
303 }
304
305 fn extend_match(&self, candidate_end: usize, stride: usize) -> usize {
306 let current_end = self.history.len() - 1;
307 let mut matched = self.min_len;
308 while matched < self.max_len {
309 let step = matched.saturating_mul(stride);
310 let Some(current_idx) = current_end.checked_sub(step) else {
311 break;
312 };
313 if current_idx < self.frozen_anchor {
314 break;
315 }
316 let Some(candidate_idx) = candidate_end.checked_sub(step) else {
317 break;
318 };
319 if self.history[current_idx] != self.history[candidate_idx] {
320 break;
321 }
322 matched += 1;
323 }
324 matched
325 }
326}
327
328#[inline]
329fn uniform_cdf() -> [f64; 257] {
330 let mut cdf = [0.0; 257];
331 let inv = 1.0 / 256.0;
332 for (i, slot) in cdf.iter_mut().enumerate() {
333 *slot = (i as f64) * inv;
334 }
335 cdf
336}
337
338#[inline]
339fn build_cdf_from_pdf(pdf: &[f64; 256], cdf: &mut [f64; 257]) {
340 cdf[0] = 0.0;
341 for i in 0..256 {
342 cdf[i + 1] = cdf[i] + pdf[i];
343 }
344}
345
346#[cfg(test)]
347mod tests {
348 use super::MatchModel;
349
350 #[test]
351 fn reset_history_preserves_fit_corpus_for_frozen_conditioning() {
352 let mut model = MatchModel::new_contiguous(32, 3, 16, 0.02, 1.0);
353 for &b in b"abcabcX" {
354 model.update(b);
355 }
356
357 model.reset_history();
358 for &b in b"abcabc" {
359 model.update_history_only(b);
360 }
361
362 assert_eq!(model.predicted_byte(), Some(b'X'));
363 assert!(model.match_len() >= 3);
364 }
365
366 #[test]
367 fn reset_history_drops_previous_conditioning() {
368 let mut model = MatchModel::new_contiguous(32, 3, 16, 0.02, 1.0);
369 for &b in b"abcabcX" {
370 model.update(b);
371 }
372
373 model.reset_history();
374 for &b in b"abcabc" {
375 model.update_history_only(b);
376 }
377 assert_eq!(model.predicted_byte(), Some(b'X'));
378
379 model.reset_history();
380 assert_eq!(model.predicted_byte(), None);
381 }
382}