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    if dict_size < DICT_SIZE_MIN {
286        return Err(TooSmall::Dictionary);
287    }
288    if total < MIN_SAMPLES_SIZE {
289        return Err(TooSmall::Corpus);
290    }
291    let selectivity = if selectivity == 0 {
292        DEFAULT_SELECTIVITY
293    } else {
294        selectivity
295    };
296    let nb_samples = sample_sizes.len();
297    let min_rep = if selectivity > 30 {
298        MIN_RATIO
299    } else {
300        // A repetition count past u32 is more than a u32-indexed corpus has
301        // positions, so nothing could qualify.
302        u32::try_from(nb_samples >> selectivity).map_err(|_| TooSmall::Content)?
303    };
304    let list_size = DICT_LIST_SIZE_DEFAULT.max(nb_samples).max(dict_size / 16);
305    let mut list = vec![DictItem::default(); list_size];
306    list[0] = DictItem {
307        pos: 1,
308        length: 0,
309        savings: u32::MAX,
310    };
311
312    // Whole samples past the size limit are left out, last first.
313    let mut used = total;
314    let mut kept = nb_samples;
315    while used > MAX_SAMPLES_SIZE {
316        kept -= 1;
317        used -= sample_sizes[kept];
318    }
319    let corpus = Corpus::new(&samples[..used]);
320    find_segments(&mut list, &corpus, used, min_rep);
321
322    let segments = list[0].pos as usize;
323    let content_size: usize = list[1..segments].iter().map(|d| d.length as usize).sum();
324    if content_size < CONTENT_SIZE_MIN {
325        return Err(TooSmall::Content);
326    }
327    // Keep the best segments that fit, in rank order.
328    let mut fit = 1;
329    let mut size = 0usize;
330    while fit < segments {
331        let next = size + list[fit].length as usize;
332        if next > dict_size {
333            break;
334        }
335        size = next;
336        fit += 1;
337    }
338    // Filled from the back, as the reference fills its buffer: rank 1 last.
339    let mut content = vec![0u8; size];
340    let mut end = size;
341    for item in &list[1..fit] {
342        let length = item.length as usize;
343        let start = end - length;
344        let from = item.pos as usize;
345        corpus.copy_to(from, &mut content[start..end]);
346        end = start;
347    }
348    debug_assert_eq!(end, 0, "the kept segments fill the content exactly");
349    Ok(content)
350}
351
352/// `ZDICT_trainBuffer_legacy`: walk every uncovered position of the corpus in
353/// text order and insert the segment its suffix neighbourhood yields.
354fn find_segments(list: &mut [DictItem], corpus: &Corpus<'_>, len: usize, min_rep: u32) {
355    let min_ratio = min_rep.max(MIN_RATIO);
356    debug_assert_eq!(corpus.samples.len(), len);
357    let sa = suffix_array(corpus.samples);
358    let mut rank = vec![0u32; len];
359    for (at, &pos) in sa.iter().enumerate() {
360        rank[pos as usize] = at as u32;
361    }
362    let suffixes = Suffixes::new(sa, len);
363    // Slack past the corpus, as the reference allocates it: a covered run may
364    // be marked into the noise band.
365    let mut done = vec![false; len + 16];
366    let max_size = list.len() as u32;
367
368    let mut cursor = 0usize;
369    while cursor < len {
370        suffixes.prefetch_for(&rank, corpus.samples, cursor);
371        if done[cursor] {
372            cursor += 1;
373            continue;
374        }
375        let solution = analyze_position(
376            &mut done,
377            &suffixes,
378            i64::from(rank[cursor]),
379            corpus,
380            min_ratio,
381        );
382        if solution.length == 0 {
383            cursor += 1;
384            continue;
385        }
386        insert_item(list, max_size, solution, corpus);
387        cursor += solution.length as usize;
388    }
389}
390
391/// Mark `at` covered, if it lies inside the marks.
392#[inline]
393fn mark(done: &mut [bool], at: usize) {
394    if let Some(slot) = done.get_mut(at) {
395        *slot = true;
396    }
397}
398
399/// Mark `len` positions from `at` covered, as far as the marks reach. Both
400/// are bounded by the corpus length, so the sum cannot overflow.
401#[inline]
402fn mark_run(done: &mut [bool], at: usize, len: usize) {
403    let end = (at + len).min(done.len());
404    if at < end {
405        done[at..end].fill(true);
406    }
407}
408
409/// `ZDICT_analyzePos`: the segment the suffix of rank `start` leads to, or an
410/// empty one. Every position the analysis settles is marked in `done`.
411fn analyze_position(
412    done: &mut [bool],
413    suffixes: &Suffixes,
414    mut start: i64,
415    corpus: &Corpus<'_>,
416    min_ratio: u32,
417) -> DictItem {
418    let mut pos = suffixes.at(start);
419    let mut end = start;
420    let empty = DictItem::default();
421    mark(done, pos);
422
423    // A run of one repeated pair is skipped whole: it compresses without a
424    // dictionary.
425    let repeats = |a: usize, b: usize| matches!((corpus.read16(a), corpus.read16(b)), (Some(x), Some(y)) if x == y);
426    if repeats(pos, pos + 2) || repeats(pos + 1, pos + 3) || repeats(pos + 2, pos + 4) {
427        let pattern = corpus.read16(pos + 4);
428        let mut pattern_end = 6usize;
429        while pattern.is_some() && corpus.read16(pos + pattern_end) == pattern {
430            pattern_end += 2;
431        }
432        if corpus.byte(pos + pattern_end).is_some()
433            && corpus.byte(pos + pattern_end) == corpus.byte(pos + pattern_end - 1)
434        {
435            pattern_end += 1;
436        }
437        mark_run(done, pos + 1, pattern_end - 1);
438        return empty;
439    }
440
441    // The neighbours sharing at least the minimum length, forward then back.
442    // Each walk stops at its noise slot as well as on a short match: every
443    // rank past the array reads the same band, so a corpus that repeats the
444    // band would otherwise walk forever. The reference reads past its two
445    // slots there, so it has no result to keep; on every other corpus the
446    // slot holds noise that ends the walk anyway.
447    loop {
448        end += 1;
449        if end >= suffixes.ranks() || corpus.common(pos, suffixes.at(end)) < MIN_MATCH_LENGTH {
450            break;
451        }
452    }
453    while start > 0 && corpus.common(pos, suffixes.at(start - 1)) >= MIN_MATCH_LENGTH {
454        start -= 1;
455    }
456
457    if ((end - start) as u32) < min_ratio {
458        for id in start..end {
459            mark(done, suffixes.at(id));
460        }
461        return empty;
462    }
463
464    // Lengthen the shared prefix one byte at a time while the most common
465    // continuation still repeats often enough.
466    let mut refined_start = start;
467    let mut refined_end = end;
468    let mut mml = MIN_MATCH_LENGTH;
469    loop {
470        let mut current_char = 0u8;
471        let mut current_count = 0u32;
472        let mut current_id = refined_start;
473        let mut selected_count = 0u32;
474        let mut selected_id = current_id;
475        for id in refined_start..refined_end {
476            let c = corpus.byte(suffixes.at(id) + mml).unwrap_or(0);
477            if c != current_char {
478                if current_count > selected_count {
479                    selected_count = current_count;
480                    selected_id = current_id;
481                }
482                current_id = id;
483                current_char = c;
484                current_count = 0;
485            }
486            current_count += 1;
487        }
488        if current_count > selected_count {
489            selected_count = current_count;
490            selected_id = current_id;
491        }
492        if selected_count < min_ratio {
493            break;
494        }
495        refined_start = selected_id;
496        refined_end = refined_start + i64::from(selected_count);
497        mml += 1;
498    }
499
500    // Measure the refined segment's neighbourhood.
501    start = refined_start;
502    pos = suffixes.at(refined_start);
503    end = start;
504    let mut lengths = [0u32; LENGTH_LIMIT];
505    loop {
506        end += 1;
507        let length = corpus.common(pos, suffixes.at(end)).min(LENGTH_LIMIT - 1);
508        lengths[length] += 1;
509        // The upper noise slot is counted, as the reference counts it, and
510        // ends the walk (see the first walk above).
511        if length < MIN_MATCH_LENGTH || end >= suffixes.ranks() {
512            break;
513        }
514    }
515    {
516        let mut length = MIN_MATCH_LENGTH;
517        while length >= MIN_MATCH_LENGTH && start > 0 {
518            length = corpus
519                .common(pos, suffixes.at(start - 1))
520                .min(LENGTH_LIMIT - 1);
521            lengths[length] += 1;
522            if length >= MIN_MATCH_LENGTH {
523                start -= 1;
524            }
525        }
526    }
527
528    // The longest length still shared by enough neighbours.
529    let mut cumulative = [0u32; LENGTH_LIMIT];
530    cumulative[LENGTH_LIMIT - 1] = lengths[LENGTH_LIMIT - 1];
531    for i in (0..LENGTH_LIMIT - 1).rev() {
532        cumulative[i] = cumulative[i + 1] + lengths[i];
533    }
534    let mut max_length = LENGTH_LIMIT - 1;
535    while max_length >= MIN_MATCH_LENGTH && cumulative[max_length] < min_ratio {
536        max_length -= 1;
537    }
538    // Do not end inside a run of the last byte.
539    {
540        let last = corpus.byte(pos + max_length - 1);
541        let mut l = max_length;
542        while l >= 2 && corpus.byte(pos + l - 2) == last {
543            l -= 1;
544        }
545        max_length = l;
546    }
547    if max_length < MIN_MATCH_LENGTH {
548        return empty;
549    }
550
551    let mut savings = [0u32; LENGTH_LIMIT];
552    for i in MIN_MATCH_LENGTH..=max_length {
553        savings[i] = savings[i - 1].wrapping_add(lengths[i].wrapping_mul((i - 3) as u32));
554    }
555    let solution = DictItem {
556        pos: pos as u32,
557        length: max_length as u32,
558        savings: savings[max_length],
559    };
560
561    for id in start..end {
562        let tested = suffixes.at(id);
563        let length = if tested == pos {
564            max_length
565        } else {
566            corpus.common(pos, tested).min(max_length)
567        };
568        mark_run(done, tested, length);
569    }
570    solution
571}
572
573/// Whether the `length` bytes at `a` equal those at `b` (`isIncluded`); both
574/// runs must lie inside the corpus. Compares `length` bytes and no more, where
575/// `common` would run on to the end of the shared prefix.
576fn is_included(corpus: &Corpus<'_>, a: usize, b: usize, length: usize) -> bool {
577    let samples = corpus.samples;
578    if let (Some(x), Some(y)) = (samples.get(a..a + length), samples.get(b..b + length)) {
579        return x == y;
580    }
581    a.max(b) + length <= corpus.len()
582        && (0..length).all(|i| corpus.byte(a + i) == corpus.byte(b + i))
583}
584
585/// Move entry `at` towards the front while its savings beat its predecessor's.
586fn promote(list: &mut [DictItem], mut at: usize) -> usize {
587    let item = list[at];
588    while at > 1 && list[at - 1].savings < item.savings {
589        list[at] = list[at - 1];
590        at -= 1;
591    }
592    list[at] = item;
593    at
594}
595
596/// `ZDICT_tryMerge`: fold `elt` into an entry it overlaps, skipping entry
597/// `skip`. Returns the merged entry's index, or 0 when nothing merged.
598fn try_merge(list: &mut [DictItem], elt: DictItem, skip: usize, corpus: &Corpus<'_>) -> usize {
599    let size = list[0].pos as usize;
600    let elt_end = elt.pos + elt.length;
601
602    // An existing entry starts inside `elt`: extend it backwards. The scans
603    // walk the table as a slice, and the rare skipped entry is tested last.
604    let mut hit = 0;
605    for (u, item) in list[..size].iter().enumerate().skip(1) {
606        if item.pos > elt.pos && item.pos <= elt_end && u != skip {
607            hit = u;
608            break;
609        }
610    }
611    if hit != 0 {
612        let item = &mut list[hit];
613        let added = item.pos - elt.pos;
614        item.length += added;
615        item.pos = elt.pos;
616        item.savings = item
617            .savings
618            .wrapping_add(elt.savings.wrapping_mul(added) / elt.length);
619        item.savings = item.savings.wrapping_add(elt.length / 8);
620        return promote(list, hit);
621    }
622
623    // `elt` starts inside an existing entry, or right after a copy of it. The
624    // eight bytes after `elt`'s start are the same for every entry, so they
625    // are read once rather than per entry as the reference reads them.
626    let elt_head = corpus.read64(elt.pos as usize + 1);
627    for u in 1..size {
628        let item = list[u];
629        if u == skip {
630            continue;
631        }
632        if item.pos + item.length >= elt.pos && item.pos < elt.pos {
633            let added = elt_end as i64 - i64::from(item.pos + item.length);
634            let item = &mut list[u];
635            item.savings = item.savings.wrapping_add(elt.length / 8);
636            if added > 0 {
637                item.length += added as u32;
638                item.savings = item
639                    .savings
640                    .wrapping_add(elt.savings.wrapping_mul(added as u32) / elt.length);
641            }
642            return promote(list, u);
643        }
644        if elt_head.is_some()
645            && corpus.read64(item.pos as usize) == elt_head
646            && is_included(
647                corpus,
648                item.pos as usize,
649                elt.pos as usize + 1,
650                item.length as usize,
651            )
652        {
653            // The reference takes this product at pointer width, where it does
654            // not wrap, unlike the two above.
655            let added = (i64::from(elt.length) - i64::from(item.length)).max(1) as u64;
656            let item = &mut list[u];
657            item.pos = elt.pos;
658            item.savings = item
659                .savings
660                .wrapping_add((u64::from(elt.savings) * added / u64::from(elt.length)) as u32);
661            item.length = elt.length.min(item.length + 1);
662            return u;
663        }
664    }
665    0
666}
667
668/// `ZDICT_removeDictItem`.
669fn remove_item(list: &mut [DictItem], id: usize) {
670    if id == 0 {
671        return;
672    }
673    let max = list[0].pos as usize;
674    for u in id..max - 1 {
675        list[u] = list[u + 1];
676    }
677    list[0].pos -= 1;
678}
679
680/// `ZDICT_insertDictItem`: merge `elt` into the table if it overlaps an entry,
681/// and keep merging while the merged entry overlaps another; otherwise insert
682/// it in savings order, dropping the last entry when the table is full.
683fn insert_item(list: &mut [DictItem], max_size: u32, elt: DictItem, corpus: &Corpus<'_>) {
684    let mut merge_id = try_merge(list, elt, 0, corpus);
685    if merge_id != 0 {
686        loop {
687            let merged = try_merge(list, list[merge_id], merge_id, corpus);
688            if merged == 0 {
689                return;
690            }
691            remove_item(list, merge_id);
692            merge_id = merged;
693        }
694    }
695    let next = list[0].pos.min(max_size - 1) as usize;
696    let mut current = next - 1;
697    while list[current].savings < elt.savings {
698        list[current + 1] = list[current];
699        current -= 1;
700    }
701    list[current + 1] = elt;
702    list[0].pos = next as u32 + 1;
703}
704
705#[cfg(test)]
706mod tests;