Skip to main content

structured_zstd/dictionary/
legacy.rs

1//! The reference's original dictionary trainer (`zdict.c`,
2//! `ZDICT_trainFromBuffer_legacy`), which `zstd --train-legacy` runs.
3//!
4//! It walks the suffix array of the whole corpus and, for every position not
5//! yet covered, measures how many other positions share a prefix of at least
6//! [`MIN_MATCH_LENGTH`] bytes with it. A prefix repeated often enough becomes a
7//! candidate segment, scored by the bytes it would save; overlapping candidates
8//! merge, and the best of them, up to the requested size, are the dictionary.
9//! Selectivity sets how often "often enough" is: a candidate needs at least
10//! `samples >> selectivity` repetitions, and never fewer than [`MIN_RATIO`].
11
12use alloc::vec;
13use alloc::vec::Vec;
14
15use super::suffix_array::suffix_array;
16use crate::encoding::fastpath::{
17    FastpathKernel, dispatch_common_prefix_len_ptr_with_kernel, select_kernel,
18};
19
20/// Fewest repetitions that make a prefix a candidate (`MINRATIO`).
21const MIN_RATIO: u32 = 4;
22/// Shortest prefix counted as a repetition (`MINMATCHLENGTH`).
23const MIN_MATCH_LENGTH: usize = 7;
24/// Longest repetition length told apart when scoring (`LLIMIT`).
25const LENGTH_LIMIT: usize = 64;
26/// Smallest candidate table (`DICTLISTSIZE_DEFAULT`).
27const DICT_LIST_SIZE_DEFAULT: usize = 10_000;
28/// Bytes of noise after the corpus, so a comparison running off its end stops
29/// on a mismatch (`NOISELENGTH`).
30const NOISE_LENGTH: usize = 32;
31/// Most corpus the trainer reads; whole samples beyond it are dropped
32/// (`ZDICT_MAX_SAMPLES_SIZE`).
33const MAX_SAMPLES_SIZE: usize = 2000 << 20;
34/// Smallest dictionary content the trainer returns (`ZDICT_CONTENTSIZE_MIN`).
35const CONTENT_SIZE_MIN: usize = 128;
36/// Smallest corpus worth training on (`ZDICT_MIN_SAMPLES_SIZE`).
37pub(crate) const MIN_SAMPLES_SIZE: usize = CONTENT_SIZE_MIN * MIN_RATIO as usize;
38/// Selectivity the reference uses when none is given (`g_selectivity_default`).
39pub const DEFAULT_SELECTIVITY: u32 = 9;
40
41/// Smallest dictionary the trainer is asked for (`ZDICT_DICTSIZE_MIN`).
42pub(crate) const DICT_SIZE_MIN: usize = super::MIN_TRAINED_DICT_SIZE;
43
44/// What was too small for the legacy trainer to produce a dictionary.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub(crate) enum TooSmall {
47    /// The requested size is below [`DICT_SIZE_MIN`].
48    Dictionary,
49    /// The corpus is smaller than [`MIN_SAMPLES_SIZE`].
50    Corpus,
51    /// The corpus repeats too little to fill [`CONTENT_SIZE_MIN`] bytes.
52    Content,
53}
54
55/// A candidate segment (`dictItem`). In the table, entry 0 is a header whose
56/// `pos` counts the entries in use, the header included, and whose `savings`
57/// is the largest value, so the sorted insert stops on it.
58#[derive(Debug, Clone, Copy, Default)]
59struct DictItem {
60    pos: u32,
61    length: u32,
62    savings: u32,
63}
64
65/// The reference's noise band (`ZDICT_fillNoise`), the same bytes on every run.
66fn noise_band() -> [u8; NOISE_LENGTH] {
67    let mut band = [0u8; NOISE_LENGTH];
68    let mut acc: u32 = 2_654_435_761;
69    for byte in &mut band {
70        acc = acc.wrapping_mul(2_246_822_519);
71        *byte = (acc >> 21) as u8;
72    }
73    band
74}
75
76/// The corpus followed by its noise band, read through bounds-checked
77/// accessors that treat anything past the band as a mismatch.
78///
79/// The samples are borrowed and the band kept apart, and the accessors join
80/// the two. The reference copies the whole corpus into a buffer one band
81/// longer (`ZDICT_trainFromBuffer_legacy`), a second copy of the samples while
82/// the caller still holds them for the finalizer.
83struct Corpus<'a> {
84    samples: &'a [u8],
85    noise: [u8; NOISE_LENGTH],
86    /// The compare kernel for this CPU, resolved once for the whole run.
87    kernel: FastpathKernel,
88}
89
90impl<'a> Corpus<'a> {
91    fn new(samples: &'a [u8]) -> Self {
92        Self {
93            samples,
94            noise: noise_band(),
95            kernel: select_kernel(),
96        }
97    }
98
99    /// Length of the samples and the band together.
100    #[inline]
101    fn len(&self) -> usize {
102        self.samples.len() + NOISE_LENGTH
103    }
104
105    #[inline]
106    fn byte(&self, at: usize) -> Option<u8> {
107        match self.samples.get(at) {
108            Some(&byte) => Some(byte),
109            None => self.noise.get(at - self.samples.len()).copied(),
110        }
111    }
112
113    /// `N` bytes from `at`, or `None` past the band.
114    #[inline(always)]
115    fn read<const N: usize>(&self, at: usize) -> Option<[u8; N]> {
116        if let Some(bytes) = self.samples.get(at..at + N) {
117            return Some(bytes.try_into().expect("N bytes"));
118        }
119        self.read_across(at)
120    }
121
122    /// [`Self::read`] for a read that reaches the band, which only the last
123    /// few positions of the corpus make.
124    #[cold]
125    #[inline(never)]
126    fn read_across<const N: usize>(&self, at: usize) -> Option<[u8; N]> {
127        let mut out = [0u8; N];
128        for (i, slot) in out.iter_mut().enumerate() {
129            *slot = self.byte(at + i)?;
130        }
131        Some(out)
132    }
133
134    #[inline]
135    fn read16(&self, at: usize) -> Option<u16> {
136        self.read::<2>(at).map(u16::from_le_bytes)
137    }
138
139    #[inline]
140    fn read64(&self, at: usize) -> Option<u64> {
141        self.read::<8>(at).map(u64::from_le_bytes)
142    }
143
144    /// Bytes `a` and `b` have in common (`ZDICT_count`): through the CPU's
145    /// vector compare while both runs lie in the samples, where the corpus'
146    /// long repeats make the comparison most of the analysis, then a byte at
147    /// a time across into the band.
148    #[inline]
149    fn common(&self, a: usize, b: usize) -> usize {
150        let samples = self.samples;
151        // Every position compared is a suffix, below the samples' length, or
152        // the noise slot, at it; one at the slot compares nothing in the
153        // samples and goes straight to the band.
154        debug_assert!(a.max(b) <= samples.len());
155        let in_samples = samples.len() - a.max(b);
156        let base = samples.as_ptr();
157        // SAFETY: `a, b <= max(a, b)` and `in_samples == samples.len() -
158        // max(a, b)`, so both runs of `in_samples` bytes lie in the samples.
159        let n = unsafe {
160            dispatch_common_prefix_len_ptr_with_kernel(
161                self.kernel,
162                base.add(a),
163                base.add(b),
164                in_samples,
165            )
166        };
167        if n < in_samples {
168            return n;
169        }
170        self.common_tail(a, b, n)
171    }
172
173    /// The bytes of [`Self::common`] from `n` on, where the shorter run has
174    /// reached the end of the samples: on into the band.
175    #[cold]
176    #[inline(never)]
177    fn common_tail(&self, a: usize, b: usize, mut n: usize) -> usize {
178        let Some(limit) = self.len().checked_sub(a.max(b)) else {
179            return 0;
180        };
181        while n < limit && self.byte(a + n) == self.byte(b + n) {
182            n += 1;
183        }
184        n
185    }
186
187    /// Copy the `out.len()` bytes from `from` into `out`.
188    fn copy_to(&self, from: usize, out: &mut [u8]) {
189        if let Some(bytes) = self.samples.get(from..from + out.len()) {
190            out.copy_from_slice(bytes);
191            return;
192        }
193        for (i, slot) in out.iter_mut().enumerate() {
194            *slot = self
195                .byte(from + i)
196                .expect("a segment lies inside the corpus");
197        }
198    }
199}
200
201/// How many text positions ahead the analysis warms the suffix-array line
202/// around a position's rank.
203const FAR_AHEAD: usize = 16;
204
205/// How many text positions ahead the analysis warms the text of a position's
206/// two neighbours in suffix order.
207const NEAR_AHEAD: usize = 8;
208
209/// The suffix array with one extra slot on each side, both pointing into the
210/// noise band, as the reference lays it out (`suffix0[0]` and
211/// `suffix[bufferSize]`): a walk off either end compares against noise and
212/// stops there.
213struct Suffixes {
214    sa: Vec<u32>,
215    noise: usize,
216}
217
218impl Suffixes {
219    fn new(sa: Vec<u32>, len: usize) -> Self {
220        Self { sa, noise: len }
221    }
222
223    /// Ranks in the array; rank `ranks()` is the upper noise slot.
224    #[inline]
225    fn ranks(&self) -> i64 {
226        self.sa.len() as i64
227    }
228
229    /// `suffix[at]`, where ranks `-1` and `len` are the noise band (the walks
230    /// stop at those two slots). A negative rank wraps past every real one,
231    /// so a single compare tells the two apart.
232    #[inline]
233    fn at(&self, at: i64) -> usize {
234        match self.sa.get(at as u64 as usize) {
235            Some(&pos) => pos as usize,
236            None => self.noise,
237        }
238    }
239
240    /// Warm what the analysis of text position `cursor` will read, ahead of
241    /// reaching it: its rank is random, so the array around that rank and the
242    /// text of the two neighbours there miss the cache every time. The array
243    /// line is warmed for the position [`FAR_AHEAD`] on, the neighbours' text
244    /// for the one [`NEAR_AHEAD`] on, whose array line that earlier hint has
245    /// brought in. A hint only; ranks at either end read a clamped slot.
246    #[inline(always)]
247    fn prefetch_for(&self, rank: &[u32], samples: &[u8], cursor: usize) {
248        use crate::decoding::prefetch::prefetch_l1_at;
249        let len = self.sa.len();
250        if let Some(&far) = rank.get(cursor + FAR_AHEAD) {
251            prefetch_l1_at(self.sa.as_ptr().wrapping_add(far as usize).cast());
252        }
253        if let Some(&near) = rank.get(cursor + NEAR_AHEAD) {
254            let near = near as usize;
255            // Rank 0 wraps past the end and clamps with the top rank.
256            let below = near.wrapping_sub(1).min(len - 1);
257            let above = (near + 1).min(len - 1);
258            // SAFETY: both are clamped below `len == self.sa.len()`.
259            let (below, above) = unsafe {
260                (
261                    *self.sa.get_unchecked(below) as usize,
262                    *self.sa.get_unchecked(above) as usize,
263                )
264            };
265            prefetch_l1_at(samples.as_ptr().wrapping_add(below));
266            prefetch_l1_at(samples.as_ptr().wrapping_add(above));
267        }
268    }
269}
270
271/// Train dictionary content from `samples`, the concatenation of samples whose
272/// lengths are `sample_sizes`, keeping at most `dict_size` bytes.
273///
274/// The segments are laid out best last, so the most valuable content sits
275/// nearest the data and is reached with the smallest offsets. `selectivity` of
276/// zero is [`DEFAULT_SELECTIVITY`].
277pub(crate) fn train_legacy_raw(
278    samples: &[u8],
279    sample_sizes: &[usize],
280    dict_size: usize,
281    selectivity: u32,
282) -> Result<Vec<u8>, TooSmall> {
283    let total: usize = sample_sizes.iter().sum();
284    debug_assert_eq!(total, samples.len(), "the sizes describe the corpus");
285    // The corpus first, as upstream checks it before anything else
286    // (zdict.c, `ZDICT_trainFromBuffer_legacy`).
287    if total < MIN_SAMPLES_SIZE {
288        return Err(TooSmall::Corpus);
289    }
290    if dict_size < DICT_SIZE_MIN {
291        return Err(TooSmall::Dictionary);
292    }
293    let selectivity = if selectivity == 0 {
294        DEFAULT_SELECTIVITY
295    } else {
296        selectivity
297    };
298    let nb_samples = sample_sizes.len();
299    let min_rep = if selectivity > 30 {
300        MIN_RATIO
301    } else {
302        // A repetition count past u32 is more than a u32-indexed corpus has
303        // positions, so nothing could qualify.
304        u32::try_from(nb_samples >> selectivity).map_err(|_| TooSmall::Content)?
305    };
306    let list_size = DICT_LIST_SIZE_DEFAULT.max(nb_samples).max(dict_size / 16);
307    let mut list = vec![DictItem::default(); list_size];
308    list[0] = DictItem {
309        pos: 1,
310        length: 0,
311        savings: u32::MAX,
312    };
313
314    // Whole samples past the size limit are left out, last first.
315    let mut used = total;
316    let mut kept = nb_samples;
317    while used > MAX_SAMPLES_SIZE {
318        kept -= 1;
319        used -= sample_sizes[kept];
320    }
321    let corpus = Corpus::new(&samples[..used]);
322    find_segments(&mut list, &corpus, used, min_rep);
323
324    let segments = list[0].pos as usize;
325    let content_size: usize = list[1..segments].iter().map(|d| d.length as usize).sum();
326    if content_size < CONTENT_SIZE_MIN {
327        return Err(TooSmall::Content);
328    }
329    // Keep the best segments that fit, in rank order.
330    let mut fit = 1;
331    let mut size = 0usize;
332    while fit < segments {
333        let next = size + list[fit].length as usize;
334        if next > dict_size {
335            break;
336        }
337        size = next;
338        fit += 1;
339    }
340    // Filled from the back, as the reference fills its buffer: rank 1 last.
341    let mut content = vec![0u8; size];
342    let mut end = size;
343    for item in &list[1..fit] {
344        let length = item.length as usize;
345        let start = end - length;
346        let from = item.pos as usize;
347        corpus.copy_to(from, &mut content[start..end]);
348        end = start;
349    }
350    debug_assert_eq!(end, 0, "the kept segments fill the content exactly");
351    Ok(content)
352}
353
354/// `ZDICT_trainBuffer_legacy`: walk every uncovered position of the corpus in
355/// text order and insert the segment its suffix neighbourhood yields.
356fn find_segments(list: &mut [DictItem], corpus: &Corpus<'_>, len: usize, min_rep: u32) {
357    let min_ratio = min_rep.max(MIN_RATIO);
358    debug_assert_eq!(corpus.samples.len(), len);
359    let sa = suffix_array(corpus.samples);
360    let mut rank = vec![0u32; len];
361    for (at, &pos) in sa.iter().enumerate() {
362        rank[pos as usize] = at as u32;
363    }
364    let suffixes = Suffixes::new(sa, len);
365    // Slack past the corpus, as the reference allocates it: a covered run may
366    // be marked into the noise band.
367    let mut done = vec![false; len + 16];
368    let max_size = list.len() as u32;
369
370    let mut cursor = 0usize;
371    while cursor < len {
372        suffixes.prefetch_for(&rank, corpus.samples, cursor);
373        if done[cursor] {
374            cursor += 1;
375            continue;
376        }
377        let solution = analyze_position(
378            &mut done,
379            &suffixes,
380            i64::from(rank[cursor]),
381            corpus,
382            min_ratio,
383        );
384        if solution.length == 0 {
385            cursor += 1;
386            continue;
387        }
388        insert_item(list, max_size, solution, corpus);
389        cursor += solution.length as usize;
390    }
391}
392
393/// Mark `at` covered, if it lies inside the marks.
394#[inline]
395fn mark(done: &mut [bool], at: usize) {
396    if let Some(slot) = done.get_mut(at) {
397        *slot = true;
398    }
399}
400
401/// Mark `len` positions from `at` covered, as far as the marks reach. Both
402/// are bounded by the corpus length, so the sum cannot overflow.
403#[inline]
404fn mark_run(done: &mut [bool], at: usize, len: usize) {
405    let end = (at + len).min(done.len());
406    if at < end {
407        done[at..end].fill(true);
408    }
409}
410
411/// `ZDICT_analyzePos`: the segment the suffix of rank `start` leads to, or an
412/// empty one. Every position the analysis settles is marked in `done`.
413fn analyze_position(
414    done: &mut [bool],
415    suffixes: &Suffixes,
416    mut start: i64,
417    corpus: &Corpus<'_>,
418    min_ratio: u32,
419) -> DictItem {
420    let mut pos = suffixes.at(start);
421    let mut end = start;
422    let empty = DictItem::default();
423    mark(done, pos);
424
425    // A run of one repeated pair is skipped whole: it compresses without a
426    // dictionary.
427    let repeats = |a: usize, b: usize| matches!((corpus.read16(a), corpus.read16(b)), (Some(x), Some(y)) if x == y);
428    if repeats(pos, pos + 2) || repeats(pos + 1, pos + 3) || repeats(pos + 2, pos + 4) {
429        let pattern = corpus.read16(pos + 4);
430        let mut pattern_end = 6usize;
431        while pattern.is_some() && corpus.read16(pos + pattern_end) == pattern {
432            pattern_end += 2;
433        }
434        if corpus.byte(pos + pattern_end).is_some()
435            && corpus.byte(pos + pattern_end) == corpus.byte(pos + pattern_end - 1)
436        {
437            pattern_end += 1;
438        }
439        mark_run(done, pos + 1, pattern_end - 1);
440        return empty;
441    }
442
443    // The neighbours sharing at least the minimum length, forward then back.
444    // Each walk stops at its noise slot as well as on a short match: every
445    // rank past the array reads the same band, so a corpus that repeats the
446    // band would otherwise walk forever. The reference reads past its two
447    // slots there, so it has no result to keep; on every other corpus the
448    // slot holds noise that ends the walk anyway.
449    loop {
450        end += 1;
451        if end >= suffixes.ranks() || corpus.common(pos, suffixes.at(end)) < MIN_MATCH_LENGTH {
452            break;
453        }
454    }
455    while start > 0 && corpus.common(pos, suffixes.at(start - 1)) >= MIN_MATCH_LENGTH {
456        start -= 1;
457    }
458
459    if ((end - start) as u32) < min_ratio {
460        for id in start..end {
461            mark(done, suffixes.at(id));
462        }
463        return empty;
464    }
465
466    // Lengthen the shared prefix one byte at a time while the most common
467    // continuation still repeats often enough.
468    let mut refined_start = start;
469    let mut refined_end = end;
470    let mut mml = MIN_MATCH_LENGTH;
471    loop {
472        let mut current_char = 0u8;
473        let mut current_count = 0u32;
474        let mut current_id = refined_start;
475        let mut selected_count = 0u32;
476        let mut selected_id = current_id;
477        for id in refined_start..refined_end {
478            let c = corpus.byte(suffixes.at(id) + mml).unwrap_or(0);
479            if c != current_char {
480                if current_count > selected_count {
481                    selected_count = current_count;
482                    selected_id = current_id;
483                }
484                current_id = id;
485                current_char = c;
486                current_count = 0;
487            }
488            current_count += 1;
489        }
490        if current_count > selected_count {
491            selected_count = current_count;
492            selected_id = current_id;
493        }
494        if selected_count < min_ratio {
495            break;
496        }
497        refined_start = selected_id;
498        refined_end = refined_start + i64::from(selected_count);
499        mml += 1;
500    }
501
502    // Measure the refined segment's neighbourhood.
503    start = refined_start;
504    pos = suffixes.at(refined_start);
505    end = start;
506    let mut lengths = [0u32; LENGTH_LIMIT];
507    loop {
508        end += 1;
509        let length = corpus.common(pos, suffixes.at(end)).min(LENGTH_LIMIT - 1);
510        lengths[length] += 1;
511        // The upper noise slot is counted, as the reference counts it, and
512        // ends the walk (see the first walk above).
513        if length < MIN_MATCH_LENGTH || end >= suffixes.ranks() {
514            break;
515        }
516    }
517    {
518        let mut length = MIN_MATCH_LENGTH;
519        while length >= MIN_MATCH_LENGTH && start > 0 {
520            length = corpus
521                .common(pos, suffixes.at(start - 1))
522                .min(LENGTH_LIMIT - 1);
523            lengths[length] += 1;
524            if length >= MIN_MATCH_LENGTH {
525                start -= 1;
526            }
527        }
528    }
529
530    // The longest length still shared by enough neighbours.
531    let mut cumulative = [0u32; LENGTH_LIMIT];
532    cumulative[LENGTH_LIMIT - 1] = lengths[LENGTH_LIMIT - 1];
533    for i in (0..LENGTH_LIMIT - 1).rev() {
534        cumulative[i] = cumulative[i + 1] + lengths[i];
535    }
536    let mut max_length = LENGTH_LIMIT - 1;
537    while max_length >= MIN_MATCH_LENGTH && cumulative[max_length] < min_ratio {
538        max_length -= 1;
539    }
540    // Do not end inside a run of the last byte.
541    {
542        let last = corpus.byte(pos + max_length - 1);
543        let mut l = max_length;
544        while l >= 2 && corpus.byte(pos + l - 2) == last {
545            l -= 1;
546        }
547        max_length = l;
548    }
549    if max_length < MIN_MATCH_LENGTH {
550        return empty;
551    }
552
553    let mut savings = [0u32; LENGTH_LIMIT];
554    for i in MIN_MATCH_LENGTH..=max_length {
555        savings[i] = savings[i - 1].wrapping_add(lengths[i].wrapping_mul((i - 3) as u32));
556    }
557    let solution = DictItem {
558        pos: pos as u32,
559        length: max_length as u32,
560        savings: savings[max_length],
561    };
562
563    for id in start..end {
564        let tested = suffixes.at(id);
565        let length = if tested == pos {
566            max_length
567        } else {
568            corpus.common(pos, tested).min(max_length)
569        };
570        mark_run(done, tested, length);
571    }
572    solution
573}
574
575/// Whether the `length` bytes at `a` equal those at `b` (`isIncluded`); both
576/// runs must lie inside the corpus. Compares `length` bytes and no more, where
577/// `common` would run on to the end of the shared prefix.
578fn is_included(corpus: &Corpus<'_>, a: usize, b: usize, length: usize) -> bool {
579    let samples = corpus.samples;
580    if let (Some(x), Some(y)) = (samples.get(a..a + length), samples.get(b..b + length)) {
581        return x == y;
582    }
583    a.max(b) + length <= corpus.len()
584        && (0..length).all(|i| corpus.byte(a + i) == corpus.byte(b + i))
585}
586
587/// Move entry `at` towards the front while its savings beat its predecessor's.
588fn promote(list: &mut [DictItem], mut at: usize) -> usize {
589    let item = list[at];
590    while at > 1 && list[at - 1].savings < item.savings {
591        list[at] = list[at - 1];
592        at -= 1;
593    }
594    list[at] = item;
595    at
596}
597
598/// `ZDICT_tryMerge`: fold `elt` into an entry it overlaps, skipping entry
599/// `skip`. Returns the merged entry's index, or 0 when nothing merged.
600fn try_merge(list: &mut [DictItem], elt: DictItem, skip: usize, corpus: &Corpus<'_>) -> usize {
601    let size = list[0].pos as usize;
602    let elt_end = elt.pos + elt.length;
603
604    // An existing entry starts inside `elt`: extend it backwards. The scans
605    // walk the table as a slice, and the rare skipped entry is tested last.
606    let mut hit = 0;
607    for (u, item) in list[..size].iter().enumerate().skip(1) {
608        if item.pos > elt.pos && item.pos <= elt_end && u != skip {
609            hit = u;
610            break;
611        }
612    }
613    if hit != 0 {
614        let item = &mut list[hit];
615        let added = item.pos - elt.pos;
616        item.length += added;
617        item.pos = elt.pos;
618        item.savings = item
619            .savings
620            .wrapping_add(elt.savings.wrapping_mul(added) / elt.length);
621        item.savings = item.savings.wrapping_add(elt.length / 8);
622        return promote(list, hit);
623    }
624
625    // `elt` starts inside an existing entry, or right after a copy of it. The
626    // eight bytes after `elt`'s start are the same for every entry, so they
627    // are read once rather than per entry as the reference reads them.
628    let elt_head = corpus.read64(elt.pos as usize + 1);
629    for u in 1..size {
630        let item = list[u];
631        if u == skip {
632            continue;
633        }
634        if item.pos + item.length >= elt.pos && item.pos < elt.pos {
635            let added = elt_end as i64 - i64::from(item.pos + item.length);
636            let item = &mut list[u];
637            item.savings = item.savings.wrapping_add(elt.length / 8);
638            if added > 0 {
639                item.length += added as u32;
640                item.savings = item
641                    .savings
642                    .wrapping_add(elt.savings.wrapping_mul(added as u32) / elt.length);
643            }
644            return promote(list, u);
645        }
646        if elt_head.is_some()
647            && corpus.read64(item.pos as usize) == elt_head
648            && is_included(
649                corpus,
650                item.pos as usize,
651                elt.pos as usize + 1,
652                item.length as usize,
653            )
654        {
655            // The reference takes this product at pointer width, where it does
656            // not wrap, unlike the two above.
657            let added = (i64::from(elt.length) - i64::from(item.length)).max(1) as u64;
658            let item = &mut list[u];
659            item.pos = elt.pos;
660            item.savings = item
661                .savings
662                .wrapping_add((u64::from(elt.savings) * added / u64::from(elt.length)) as u32);
663            item.length = elt.length.min(item.length + 1);
664            return u;
665        }
666    }
667    0
668}
669
670/// `ZDICT_removeDictItem`.
671fn remove_item(list: &mut [DictItem], id: usize) {
672    if id == 0 {
673        return;
674    }
675    let max = list[0].pos as usize;
676    for u in id..max - 1 {
677        list[u] = list[u + 1];
678    }
679    list[0].pos -= 1;
680}
681
682/// `ZDICT_insertDictItem`: merge `elt` into the table if it overlaps an entry,
683/// and keep merging while the merged entry overlaps another; otherwise insert
684/// it in savings order, dropping the last entry when the table is full.
685fn insert_item(list: &mut [DictItem], max_size: u32, elt: DictItem, corpus: &Corpus<'_>) {
686    let mut merge_id = try_merge(list, elt, 0, corpus);
687    if merge_id != 0 {
688        loop {
689            let merged = try_merge(list, list[merge_id], merge_id, corpus);
690            if merged == 0 {
691                return;
692            }
693            remove_item(list, merge_id);
694            merge_id = merged;
695        }
696    }
697    let next = list[0].pos.min(max_size - 1) as usize;
698    let mut current = next - 1;
699    while list[current].savings < elt.savings {
700        list[current + 1] = list[current];
701        current -= 1;
702    }
703    list[current + 1] = elt;
704    list[0].pos = next as u32 + 1;
705}
706
707#[cfg(test)]
708mod tests;