Skip to main content

fsst/
builder.rs

1//! Functions and types used for building a [`Compressor`] from a corpus of text.
2//!
3//! This module implements the logic from Algorithm 3 of the [FSST Paper].
4//!
5//! [FSST Paper]: https://www.vldb.org/pvldb/vol13/p2649-boncz.pdf
6
7use crate::{
8    Code, Compressor, FSST_CODE_BASE, FSST_CODE_MASK, Symbol, advance_8byte_word, compare_masked,
9    lossy_pht::LossyPHT,
10};
11use rustc_hash::{FxBuildHasher, FxHashMap};
12use std::cmp::Ordering;
13use std::collections::BinaryHeap;
14
15/// Bitmap that only works for values up to 512
16#[derive(Clone, Copy, Debug, Default)]
17struct CodesBitmap {
18    codes: [u64; 8],
19}
20
21assert_sizeof!(CodesBitmap => 64);
22
23impl CodesBitmap {
24    /// Set the indicated bit. Must be between 0 and [`FSST_CODE_MASK`][crate::FSST_CODE_MASK].
25    pub(crate) fn set(&mut self, index: usize) {
26        debug_assert!(
27            index <= FSST_CODE_MASK as usize,
28            "code cannot exceed {FSST_CODE_MASK}"
29        );
30
31        let map = index >> 6;
32        self.codes[map] |= 1 << (index % 64);
33    }
34
35    /// Check if `index` is present in the bitmap
36    pub(crate) fn is_set(&self, index: usize) -> bool {
37        debug_assert!(
38            index <= FSST_CODE_MASK as usize,
39            "code cannot exceed {FSST_CODE_MASK}"
40        );
41
42        let map = index >> 6;
43        self.codes[map] & (1 << (index % 64)) != 0
44    }
45
46    /// Get all codes set in this bitmap
47    pub(crate) fn codes(&self) -> CodesIterator<'_> {
48        CodesIterator {
49            inner: self,
50            index: 0,
51            block: self.codes[0],
52            reference: 0,
53        }
54    }
55
56    /// Clear the bitmap of all entries.
57    pub(crate) fn clear(&mut self) {
58        self.codes[0] = 0;
59        self.codes[1] = 0;
60        self.codes[2] = 0;
61        self.codes[3] = 0;
62        self.codes[4] = 0;
63        self.codes[5] = 0;
64        self.codes[6] = 0;
65        self.codes[7] = 0;
66    }
67}
68
69struct CodesIterator<'a> {
70    inner: &'a CodesBitmap,
71    index: usize,
72    block: u64,
73    reference: usize,
74}
75
76impl Iterator for CodesIterator<'_> {
77    type Item = u16;
78
79    fn next(&mut self) -> Option<Self::Item> {
80        // If current is zero, advance to next non-zero block
81        while self.block == 0 {
82            self.index += 1;
83            if self.index >= 8 {
84                return None;
85            }
86            self.block = self.inner.codes[self.index];
87            self.reference = self.index * 64;
88        }
89
90        // Find the next set bit in the current block.
91        let position = self.block.trailing_zeros() as usize;
92        let code = self.reference + position;
93
94        if code >= 511 {
95            return None;
96        }
97
98        // The next iteration will calculate with reference to the returned code + 1
99        self.reference = code + 1;
100        self.block = if position == 63 {
101            0
102        } else {
103            self.block >> (1 + position)
104        };
105
106        Some(code as u16)
107    }
108}
109
110#[derive(Debug, Clone)]
111struct Counter {
112    /// Frequency count for each code.
113    counts1: Vec<usize>,
114
115    /// Frequency count for each code-pair.
116    counts2: Vec<usize>,
117
118    /// Bitmap index for codes that appear in counts1
119    code1_index: CodesBitmap,
120
121    /// Bitmap index of pairs that have been set.
122    ///
123    /// `pair_index[code1].codes()` yields an iterator that can
124    /// be used to find all possible codes that follow `codes1`.
125    pair_index: Vec<CodesBitmap>,
126}
127
128const COUNTS1_SIZE: usize = (FSST_CODE_MASK + 1) as usize;
129
130// NOTE: in Rust, creating a 1D vector of length N^2 is ~4x faster than creating a 2-D vector,
131//  because `vec!` has a specialization for zero.
132//
133// We also include +1 extra row at the end so that we can do writes into the counters without a branch
134// for the first iteration.
135const COUNTS2_SIZE: usize = COUNTS1_SIZE * COUNTS1_SIZE;
136
137impl Counter {
138    fn new() -> Self {
139        let mut counts1 = Vec::with_capacity(COUNTS1_SIZE);
140        let mut counts2 = Vec::with_capacity(COUNTS2_SIZE);
141        // SAFETY: all accesses to the vector go through the bitmap to ensure no uninitialized
142        //  data is ever read from these vectors.
143        unsafe {
144            counts1.set_len(COUNTS1_SIZE);
145            counts2.set_len(COUNTS2_SIZE);
146        }
147
148        Self {
149            counts1,
150            counts2,
151            code1_index: CodesBitmap::default(),
152            pair_index: vec![CodesBitmap::default(); COUNTS1_SIZE],
153        }
154    }
155
156    #[inline]
157    fn record_count1(&mut self, code1: u16) {
158        // If not set, we want to start at one.
159        let base = if self.code1_index.is_set(code1 as usize) {
160            self.counts1[code1 as usize]
161        } else {
162            0
163        };
164
165        self.counts1[code1 as usize] = base + 1;
166        self.code1_index.set(code1 as usize);
167    }
168
169    #[inline]
170    fn record_count2(&mut self, code1: u16, code2: u16) {
171        debug_assert!(code1 == FSST_CODE_MASK || self.code1_index.is_set(code1 as usize));
172        debug_assert!(self.code1_index.is_set(code2 as usize));
173
174        let idx = (code1 as usize) * COUNTS1_SIZE + (code2 as usize);
175        if self.pair_index[code1 as usize].is_set(code2 as usize) {
176            self.counts2[idx] += 1;
177        } else {
178            self.counts2[idx] = 1;
179        }
180        self.pair_index[code1 as usize].set(code2 as usize);
181    }
182
183    #[inline]
184    fn count1(&self, code1: u16) -> usize {
185        debug_assert!(self.code1_index.is_set(code1 as usize));
186
187        self.counts1[code1 as usize]
188    }
189
190    #[inline]
191    fn count2(&self, code1: u16, code2: u16) -> usize {
192        debug_assert!(self.code1_index.is_set(code1 as usize));
193        debug_assert!(self.code1_index.is_set(code2 as usize));
194        debug_assert!(self.pair_index[code1 as usize].is_set(code2 as usize));
195
196        let idx = (code1 as usize) * 512 + (code2 as usize);
197        self.counts2[idx]
198    }
199
200    /// Returns an ordered iterator over the codes that were observed
201    /// in a call to [`Self::count1`].
202    fn first_codes(&self) -> CodesIterator<'_> {
203        self.code1_index.codes()
204    }
205
206    /// Returns an iterator over the codes that have been observed
207    /// to follow `code1`.
208    ///
209    /// This is the set of all values `code2` where there was
210    /// previously a call to `self.record_count2(code1, code2)`.
211    fn second_codes(&self, code1: u16) -> CodesIterator<'_> {
212        self.pair_index[code1 as usize].codes()
213    }
214
215    /// Clear the counters.
216    /// Note that this just touches the bitmaps and sets them all to invalid.
217    fn clear(&mut self) {
218        self.code1_index.clear();
219        for index in &mut self.pair_index {
220            index.clear();
221        }
222    }
223}
224
225/// Entrypoint for building a new `Compressor`.
226pub struct CompressorBuilder {
227    /// Table mapping codes to symbols.
228    ///
229    /// The entries 0-255 are setup in some other way here
230    symbols: Vec<Symbol>,
231
232    /// The number of entries in the symbol table that have been populated, not counting
233    /// the escape values.
234    n_symbols: u8,
235
236    /// Counts for number of symbols of each length.
237    ///
238    /// `len_histogram[len-1]` = count of the symbols of length `len`.
239    len_histogram: [u8; 8],
240
241    /// Inverted index mapping 1-byte symbols to codes.
242    ///
243    /// This is only used for building, not used by the final `Compressor`.
244    codes_one_byte: Vec<Code>,
245
246    /// Inverted index mapping 2-byte symbols to codes
247    codes_two_byte: Vec<Code>,
248
249    /// Lossy perfect hash table for looking up codes to symbols that are 3 bytes or more
250    lossy_pht: LossyPHT,
251}
252
253impl CompressorBuilder {
254    /// Create a new builder.
255    pub fn new() -> Self {
256        // NOTE: `vec!` has a specialization for building a new vector of `0u64`. Because Symbol and u64
257        //  have the same bit pattern, we can allocate as u64 and transmute. If we do `vec![Symbol::EMPTY; N]`,
258        // that will create a new Vec and call `Symbol::EMPTY.clone()` `N` times which is considerably slower.
259        let symbols = vec![Symbol::ZERO; 511];
260
261        let mut table = Self {
262            symbols,
263            n_symbols: 0,
264            len_histogram: [0; 8],
265            codes_two_byte: Vec::with_capacity(65_536),
266            codes_one_byte: Vec::with_capacity(512),
267            lossy_pht: LossyPHT::new(),
268        };
269
270        // Populate the escape byte entries.
271        for byte in 0..=255 {
272            let symbol = Symbol::from_u8(byte);
273            table.symbols[byte as usize] = symbol;
274        }
275
276        // Fill codes_one_byte with pseudocodes for each byte.
277        for byte in 0..=255 {
278            // Push pseudocode for single-byte escape.
279            table.codes_one_byte.push(Code::new_escape(byte));
280        }
281
282        // Fill codes_two_byte with pseudocode of first byte.
283        //
284        // The pseudocode only depends on the low byte of the index, so the 65,536 entries are
285        // 256 copies of the 256-entry `codes_one_byte` row we just built. Copying the row is
286        // considerably cheaper than pushing every element individually.
287        for _ in 0..256 {
288            table
289                .codes_two_byte
290                .extend_from_slice(&table.codes_one_byte);
291        }
292
293        table
294    }
295}
296
297impl Default for CompressorBuilder {
298    fn default() -> Self {
299        Self::new()
300    }
301}
302
303impl CompressorBuilder {
304    /// Attempt to insert a new symbol at the end of the table.
305    ///
306    /// # Panics
307    ///
308    /// Panics if the table is already full.
309    ///
310    /// # Returns
311    ///
312    /// Returns true if the symbol was inserted successfully, or false if it conflicted
313    /// with an existing symbol.
314    pub fn insert(&mut self, symbol: Symbol, len: usize) -> bool {
315        assert!(self.n_symbols < 255, "cannot insert into full symbol table");
316        assert_eq!(len, symbol.len(), "provided len must equal symbol.len()");
317
318        if len == 2 {
319            // shortCodes
320            self.codes_two_byte[symbol.first2() as usize] =
321                Code::new_symbol_building(self.n_symbols, 2);
322        } else if len == 1 {
323            // byteCodes
324            self.codes_one_byte[symbol.first_byte() as usize] =
325                Code::new_symbol_building(self.n_symbols, 1);
326        } else {
327            // Symbols of 3 or more bytes go into the hash table
328            if !self.lossy_pht.insert(symbol, len, self.n_symbols) {
329                return false;
330            }
331        }
332
333        // Increment length histogram.
334        self.len_histogram[len - 1] += 1;
335
336        // Insert successfully stored symbol at end of the symbol table
337        // Note the rescaling from range [0-254] -> [256, 510].
338        self.symbols[256 + (self.n_symbols as usize)] = symbol;
339        self.n_symbols += 1;
340        true
341    }
342
343    /// Clear all set items from the compressor.
344    ///
345    /// This is considerably faster than building a new Compressor from scratch for each
346    /// iteration of the `train` loop.
347    fn clear(&mut self) {
348        // Eliminate every observed code from the table.
349        for code in 0..(256 + self.n_symbols as usize) {
350            let symbol = self.symbols[code];
351            if symbol.len() == 1 {
352                // Reset the entry from the codes_one_byte array.
353                self.codes_one_byte[symbol.first_byte() as usize] =
354                    Code::new_escape(symbol.first_byte());
355            } else if symbol.len() == 2 {
356                // Reset the entry from the codes_two_byte array.
357                self.codes_two_byte[symbol.first2() as usize] =
358                    Code::new_escape(symbol.first_byte());
359            } else {
360                // Clear the hashtable entry
361                self.lossy_pht.remove(symbol);
362            }
363        }
364
365        // Reset len histogram
366        for i in 0..=7 {
367            self.len_histogram[i] = 0;
368        }
369
370        self.n_symbols = 0;
371    }
372
373    /// Finalizing the table is done once building is complete to prepare for efficient
374    /// compression.
375    ///
376    /// When we finalize the table, the following modifications are made in-place:
377    ///
378    /// 1. The codes are renumbered so that all symbols are ordered by length (order 23456781).
379    ///    During this process, the two byte symbols are separated into a byte_lim and a suffix_lim,
380    ///    so we know that we don't need to check the suffix limitations instead.
381    /// 2. The 1-byte symbols index is merged into the 2-byte symbols index to allow for use of only
382    ///    a single index in front of the hash table.
383    ///
384    /// # Returns
385    ///
386    /// Returns the `suffix_lim`, which is the index of the two-byte code before where we know
387    /// there are no longer suffixies in the symbol table.
388    ///
389    /// Also returns the lengths vector, which is of length `n_symbols` and contains the
390    /// length for each of the values.
391    fn finalize(&mut self) -> (u8, [u8; 255]) {
392        // Create a cumulative sum of each of the elements of the input line numbers.
393        // Do a map that includes the previously seen value as well.
394        // Regroup symbols based on their lengths.
395        // Space at the end of the symbol table reserved for the one-byte codes.
396        let byte_lim = self.n_symbols - self.len_histogram[0];
397
398        // Start code for each length.
399        // Length 1: at the end of symbol table.
400        // Length 2: starts at 0. Split into before/after suffixLim.
401        let mut codes_by_length = [0u8; 8];
402        codes_by_length[0] = byte_lim;
403        codes_by_length[1] = 0;
404
405        // codes for lengths 3..=8 start where the previous ones end.
406        for i in 1..7 {
407            codes_by_length[i + 1] = codes_by_length[i] + self.len_histogram[i];
408        }
409
410        // no_suffix_code is the lowest code for a symbol that does not have a longer 3+ byte
411        // suffix in the table.
412        // This value starts at 0 and extends up.
413        let mut no_suffix_code = 0;
414
415        // The codes that do not have a suffix begin just before the range of the 3-byte codes.
416        let mut has_suffix_code = codes_by_length[2];
417
418        // Assign each symbol a new code ordered by lengths, in the order
419        // 2(no suffix) | 2 (suffix) | 3 | 4 | 5 | 6 | 7 | 8 | 1
420        let mut new_codes = [0u8; FSST_CODE_BASE as usize];
421
422        let mut symbol_lens = [0u8; FSST_CODE_BASE as usize];
423
424        // The `codes_two_byte` slots claimed by a two-byte symbol, as (slot, new code) pairs
425        // recorded in old-code (i.e. insertion) order. Replaying them in that order below
426        // reproduces the last-write-wins behaviour of `insert` in the case where the same
427        // two-byte symbol was inserted more than once.
428        let mut two_byte_slots = [(0u16, 0u8); FSST_CODE_BASE as usize];
429        let mut n_two_byte_slots = 0usize;
430
431        for i in 0..(self.n_symbols as usize) {
432            let symbol = self.symbols[256 + i];
433            let len = symbol.len();
434            if len == 2 {
435                let has_suffix = self
436                    .symbols
437                    .iter()
438                    .skip(FSST_CODE_BASE as usize)
439                    .enumerate()
440                    .any(|(k, other)| i != k && symbol.first2() == other.first2());
441
442                if has_suffix {
443                    // Symbols that have a longer suffix are inserted at the end of the 2-byte range
444                    has_suffix_code -= 1;
445                    new_codes[i] = has_suffix_code;
446                } else {
447                    // Symbols that do not have a longer suffix are inserted at the start of
448                    // the 2-byte range.
449                    new_codes[i] = no_suffix_code;
450                    no_suffix_code += 1;
451                }
452
453                two_byte_slots[n_two_byte_slots] = (symbol.first2(), new_codes[i]);
454                n_two_byte_slots += 1;
455            } else {
456                // Assign new code based on the next code available for the given length symbol
457                new_codes[i] = codes_by_length[len - 1];
458                codes_by_length[len - 1] += 1;
459            }
460
461            // Write the symbol into the front half of the symbol table.
462            // We are reusing the space that was previously occupied by escapes.
463            self.symbols[new_codes[i] as usize] = symbol;
464            symbol_lens[new_codes[i] as usize] = len as u8;
465        }
466
467        // Truncate the symbol table to only include the "true" symbols.
468        self.symbols.truncate(FSST_CODE_BASE as usize - 1);
469
470        // Rewrite the codes_one_byte table to point at the new code values.
471        // Replace pseudocodes with escapes.
472        for byte in 0..=255 {
473            let one_byte = self.codes_one_byte[byte];
474            if one_byte.extended_code() >= FSST_CODE_BASE {
475                let new_code = new_codes[one_byte.code() as usize];
476                self.codes_one_byte[byte] = Code::new_symbol(new_code, 1);
477            } else {
478                // After finalize: codes_one_byte contains the unused value
479                self.codes_one_byte[byte] = Code::UNUSED;
480            }
481        }
482
483        // Rewrite the codes_two_byte table to point at the new code values.
484        // Replace pseudocodes with escapes.
485        //
486        // A slot holds a symbol code if and only if some two-byte symbol was inserted with that
487        // slot as its `first2()`; every other slot falls back to the one-byte code for its low
488        // byte. Rather than visiting all 65,536 slots, bulk-copy the (already rewritten)
489        // `codes_one_byte` row 256 times and then patch the at most 255 symbol slots.
490        self.codes_two_byte.clear();
491        for _ in 0..256 {
492            self.codes_two_byte.extend_from_slice(&self.codes_one_byte);
493        }
494        for &(slot, new_code) in &two_byte_slots[..n_two_byte_slots] {
495            self.codes_two_byte[slot as usize] = Code::new_symbol(new_code, 2);
496        }
497
498        // Reset values in the hash table as well.
499        self.lossy_pht.renumber(&new_codes);
500
501        // Pre-compute the lengths
502        let mut lengths = [0u8; 255];
503        for (len, symbol) in lengths.iter_mut().zip(&self.symbols) {
504            *len = symbol.len() as u8;
505        }
506
507        (has_suffix_code, lengths)
508    }
509
510    /// Build into the final hash table.
511    pub fn build(mut self) -> Compressor {
512        // finalize the symbol table by inserting the codes_twobyte values into
513        // the relevant parts of the `codes_onebyte` set.
514
515        let (has_suffix_code, lengths) = self.finalize();
516
517        Compressor {
518            symbols: self
519                .symbols
520                .try_into()
521                .expect("Symbol table should be exactly 255 elements in length"),
522            lengths,
523            n_symbols: self.n_symbols,
524            has_suffix_code,
525            codes_two_byte: self.codes_two_byte,
526            lossy_pht: self.lossy_pht,
527        }
528    }
529}
530
531/// The number of generations used for training. This is taken from the [FSST paper].
532///
533/// [FSST paper]: https://www.vldb.org/pvldb/vol13/p2649-boncz.pdf
534const GENERATIONS: [usize; 5] = [8usize, 38, 68, 98, 128];
535
536const FSST_SAMPLETARGET: usize = 1 << 14;
537const FSST_SAMPLELINE: usize = 512;
538
539/// Create a sample from a set of strings in the input.
540///
541/// The sample is picked based on criteria from the C++ implementation, and it
542/// is a vector of subranges of the input strings `str_in`.
543fn make_sample<'a>(str_in: &[&'a [u8]], tot_size: usize) -> Vec<&'a [u8]> {
544    let mut sample: Vec<&[u8]> = Vec::new();
545
546    if tot_size < FSST_SAMPLETARGET {
547        return str_in.to_vec();
548    }
549
550    let mut sample_rnd = fsst_hash(4637947);
551    let sample_lim = FSST_SAMPLETARGET;
552
553    let mut sample_size = 0;
554
555    while sample_size < sample_lim {
556        sample_rnd = fsst_hash(sample_rnd);
557        let line_nr = (sample_rnd as usize) % str_in.len();
558
559        // Find the first non-empty chunk starting at line_nr, wrapping around if
560        // necessary.
561        let Some(line) = (line_nr..str_in.len())
562            .chain(0..line_nr)
563            .map(|line_nr| str_in[line_nr])
564            .find(|line| !line.is_empty())
565        else {
566            return sample;
567        };
568
569        let chunks = 1 + ((line.len() - 1) / FSST_SAMPLELINE);
570        sample_rnd = fsst_hash(sample_rnd);
571        let chunk = FSST_SAMPLELINE * ((sample_rnd as usize) % chunks);
572
573        let len = FSST_SAMPLELINE.min(line.len() - chunk);
574
575        sample.push(&line[chunk..chunk + len]);
576        sample_size += len;
577    }
578
579    sample
580}
581
582/// Hash function used in various components of the library.
583///
584/// This is equivalent to the FSST_HASH macro from the C++ implementation.
585#[inline]
586pub(crate) fn fsst_hash(value: u64) -> u64 {
587    value.wrapping_mul(2971215073) ^ value.wrapping_shr(15)
588}
589
590impl Compressor {
591    /// Build and train a `Compressor` from a sample corpus of text.
592    ///
593    /// This function implements the generational algorithm described in the [FSST paper] Section
594    /// 4.3. Starting with an empty symbol table, it iteratively compresses the corpus, then attempts
595    /// to merge symbols when doing so would yield better compression than leaving them unmerged. The
596    /// resulting table will have at most 255 symbols (the 256th symbol is reserved for the escape
597    /// code).
598    ///
599    /// [FSST paper]: https://www.vldb.org/pvldb/vol13/p2649-boncz.pdf
600    pub fn train(values: &Vec<&[u8]>) -> Self {
601        let mut builder = CompressorBuilder::new();
602
603        if values.is_empty() {
604            return builder.build();
605        }
606
607        let mut counters = Counter::new();
608        let mut pqueue = BinaryHeap::with_capacity(65_536);
609
610        let tot_size: usize = values.iter().map(|s| s.len()).sum();
611        let sampled = tot_size >= FSST_SAMPLETARGET;
612        let sample = make_sample(values, tot_size);
613        for sample_frac in GENERATIONS {
614            for (i, line) in sample.iter().enumerate() {
615                if sample_frac < 128 && ((fsst_hash(i as u64) & 127) as usize) > sample_frac {
616                    continue;
617                }
618
619                builder.compress_count(line, &mut counters);
620            }
621
622            // Clear the heap before we use it again
623            pqueue.clear();
624            let prune = sample_frac >= 128 && !sampled;
625            builder.optimize(&counters, sample_frac, &mut pqueue, prune);
626            counters.clear();
627        }
628
629        builder.build()
630    }
631}
632
633impl CompressorBuilder {
634    /// Find the longest symbol using the hash table and the codes_one_byte and codes_two_byte indexes.
635    fn find_longest_symbol(&self, word: u64) -> Code {
636        // Probe the hash table first to see if we have a long match
637        let entry = self.lossy_pht.lookup(word);
638        let ignored_bits = entry.ignored_bits;
639
640        // If the entry is valid, return the code
641        if !entry.is_unused() && compare_masked(word, entry.symbol.to_u64(), ignored_bits) {
642            return entry.code;
643        }
644
645        // Try and match first two bytes
646        let twobyte = self.codes_two_byte[word as u16 as usize];
647        if twobyte.extended_code() >= FSST_CODE_BASE {
648            return twobyte;
649        }
650
651        // Fall back to single-byte match
652        self.codes_one_byte[word as u8 as usize]
653    }
654
655    /// Compress the text using the current symbol table. Count the code occurrences
656    /// and code-pair occurrences, calculating total gain using the current compressor.
657    ///
658    /// NOTE: this is largely an unfortunate amount of copy-paste from `compress`, just to make sure
659    /// we can do all the counting in a single pass.
660    fn compress_count(&self, sample: &[u8], counter: &mut Counter) -> usize {
661        let mut gain = 0;
662        if sample.is_empty() {
663            return gain;
664        }
665
666        let mut in_ptr = sample.as_ptr();
667
668        // SAFETY: `end` will point just after the end of the `plaintext` slice.
669        let in_end = unsafe { in_ptr.byte_add(sample.len()) };
670        let in_end_sub8 = in_end as usize - 8;
671
672        let mut prev_code: u16 = FSST_CODE_MASK;
673
674        while (in_ptr as usize) < (in_end_sub8) {
675            // SAFETY: ensured in-bounds by loop condition.
676            let word: u64 = unsafe { std::ptr::read_unaligned(in_ptr as *const u64) };
677            let code = self.find_longest_symbol(word);
678            let code_u16 = code.extended_code();
679
680            // Gain increases by the symbol length if a symbol matches, or 0
681            // if an escape is emitted.
682            gain += (code.len() as usize) - ((code_u16 < 256) as usize);
683
684            // Record the single and pair counts
685            counter.record_count1(code_u16);
686            counter.record_count2(prev_code, code_u16);
687
688            // Also record the count for just extending by a single byte, but only if
689            // the symbol is not itself a single byte.
690            if code.len() > 1 {
691                let code_first_byte = self.symbols[code_u16 as usize].first_byte() as u16;
692                counter.record_count1(code_first_byte);
693                counter.record_count2(prev_code, code_first_byte);
694            }
695
696            // SAFETY: pointer bound is checked in loop condition before any access is made.
697            in_ptr = unsafe { in_ptr.byte_add(code.len() as usize) };
698
699            prev_code = code_u16;
700        }
701
702        let remaining_bytes = unsafe { in_end.byte_offset_from(in_ptr) };
703        assert!(
704            remaining_bytes.is_positive(),
705            "in_ptr exceeded in_end, should not be possible"
706        );
707        let remaining_bytes = remaining_bytes as usize;
708
709        // Load the last `remaining_byte`s of data into a final world. We then replicate the loop above,
710        // but shift data out of this word rather than advancing an input pointer and potentially reading
711        // unowned memory
712        let mut bytes = [0u8; 8];
713        unsafe {
714            // SAFETY: it is safe to read up to remaining_bytes from in_ptr, and remaining_bytes
715            //  will be <= 8 bytes.
716            std::ptr::copy_nonoverlapping(in_ptr, bytes.as_mut_ptr(), remaining_bytes);
717        }
718        let mut last_word = u64::from_le_bytes(bytes);
719
720        let mut remaining_bytes = remaining_bytes;
721
722        while remaining_bytes > 0 {
723            // SAFETY: ensured in-bounds by loop condition.
724            let code = self.find_longest_symbol(last_word);
725            let code_u16 = code.extended_code();
726
727            // Gain increases by the symbol length if a symbol matches, or 0
728            // if an escape is emitted.
729            gain += (code.len() as usize) - ((code_u16 < 256) as usize);
730
731            // Record the single and pair counts
732            counter.record_count1(code_u16);
733            counter.record_count2(prev_code, code_u16);
734
735            // Also record the count for just extending by a single byte, but only if
736            // the symbol is not itself a single byte.
737            if code.len() > 1 {
738                let code_first_byte = self.symbols[code_u16 as usize].first_byte() as u16;
739                counter.record_count1(code_first_byte);
740                counter.record_count2(prev_code, code_first_byte);
741            }
742
743            // Advance our last_word "input pointer" by shifting off the covered values.
744            let advance = code.len() as usize;
745            remaining_bytes -= advance;
746            last_word = advance_8byte_word(last_word, advance);
747
748            prev_code = code_u16;
749        }
750
751        gain
752    }
753
754    /// Using a set of counters and the existing set of symbols, build a new
755    /// set of symbols/codes that optimizes the gain over the distribution in `counter`.
756    fn optimize(
757        &mut self,
758        counters: &Counter,
759        sample_frac: usize,
760        pqueue: &mut BinaryHeap<Candidate>,
761        prune: bool,
762    ) {
763        // Use a HashMap to deduplicate candidates by symbol content, combining gains
764        // when the same symbol is encountered via different codes.
765        // This matches the C++ implementation's use of unordered_set<QSymbol> with addOrInc.
766        // NOTE: we use fxhash since that is the best Rust hasher for 64-bit ints.
767        let mut candidates = FxHashMap::with_capacity_and_hasher(256, FxBuildHasher);
768
769        for code1 in counters.first_codes() {
770            let symbol1 = self.symbols[code1 as usize];
771            let symbol1_len = symbol1.len();
772            let count = counters.count1(code1);
773
774            // From the c++ impl:
775            // "improves both compression speed (less candidates), but also quality!!"
776            // When pruning (final pass, exact counts), lower the threshold to 1
777            // so the pruning check can decide based on cost/benefit.
778            let min_count = if prune { 1 } else { 5 * sample_frac / 128 };
779            if count < min_count {
780                continue;
781            }
782
783            let mut gain = count * symbol1_len;
784            // NOTE: use heuristic from C++ implementation to boost the gain of single-byte symbols.
785            // This helps to reduce exception counts.
786            if symbol1_len == 1 {
787                gain *= 8;
788            }
789
790            // Add or combine gain for this symbol
791            *candidates.entry(symbol1).or_insert(0) += gain;
792
793            // Skip merges on last round, or when symbol cannot be extended.
794            if sample_frac >= 128 || symbol1_len == 8 {
795                continue;
796            }
797
798            for code2 in counters.second_codes(code1) {
799                let symbol2 = self.symbols[code2 as usize];
800
801                // If merging would yield a symbol of length greater than 8, skip.
802                if symbol1_len + symbol2.len() > 8 {
803                    continue;
804                }
805                let new_symbol = symbol1.concat(symbol2);
806                let gain = counters.count2(code1, code2) * new_symbol.len();
807
808                // Add or combine gain for this merged symbol
809                *candidates.entry(new_symbol).or_insert(0) += gain;
810            }
811        }
812
813        // Transfer deduplicated candidates to the priority queue
814        for (symbol, gain) in candidates {
815            pqueue.push(Candidate { symbol, gain });
816        }
817
818        // clear self in advance of inserting the symbols.
819        self.clear();
820
821        // Pop the 255 best symbols.
822        let mut n_symbols = 0;
823        while !pqueue.is_empty() && n_symbols < 255 {
824            let candidate = pqueue.pop().unwrap();
825            if prune {
826                let symbol_len = candidate.symbol.len();
827                let saves = if symbol_len == 1 {
828                    candidate.gain / 8 // undo the 8x single-byte boost
829                } else {
830                    candidate.gain
831                };
832                if saves <= symbol_len + 1 {
833                    continue;
834                }
835            }
836            if self.insert(candidate.symbol, candidate.symbol.len()) {
837                n_symbols += 1;
838            }
839        }
840    }
841}
842
843/// A candidate for inclusion in a symbol table.
844///
845/// This is really only useful for the `optimize` step of training.
846#[derive(Copy, Clone, Debug)]
847struct Candidate {
848    gain: usize,
849    symbol: Symbol,
850}
851
852impl Candidate {
853    fn comparable_form(&self) -> (usize, usize) {
854        (self.gain, self.symbol.len())
855    }
856}
857
858impl Eq for Candidate {}
859
860impl PartialEq<Self> for Candidate {
861    fn eq(&self, other: &Self) -> bool {
862        self.comparable_form().eq(&other.comparable_form())
863    }
864}
865
866impl PartialOrd<Self> for Candidate {
867    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
868        Some(self.cmp(other))
869    }
870}
871
872impl Ord for Candidate {
873    fn cmp(&self, other: &Self) -> Ordering {
874        let self_ord = (self.gain, self.symbol.len());
875        let other_ord = (other.gain, other.symbol.len());
876
877        self_ord.cmp(&other_ord)
878    }
879}
880
881#[cfg(test)]
882mod test {
883    use crate::{Compressor, ESCAPE_CODE, builder::CodesBitmap};
884
885    #[test]
886    fn test_builder() {
887        // Train a Compressor on the toy string
888        let text = b"hello hello hello hello hello";
889
890        // count of 5 is the cutoff for including a symbol in the table.
891        let table = Compressor::train(&vec![text, text, text, text, text]);
892
893        // Use the table to compress a string, see the values
894        let compressed = table.compress(text);
895
896        // Ensure that the compressed string has no escape bytes
897        assert!(compressed.iter().all(|b| *b != ESCAPE_CODE));
898
899        // Ensure that we can compress a string with no values seen at training time, with escape bytes
900        let compressed = table.compress("xyz123".as_bytes());
901        let decompressed = table.decompressor().decompress(&compressed);
902        assert_eq!(&decompressed, b"xyz123");
903        assert_eq!(
904            compressed,
905            vec![
906                ESCAPE_CODE,
907                b'x',
908                ESCAPE_CODE,
909                b'y',
910                ESCAPE_CODE,
911                b'z',
912                ESCAPE_CODE,
913                b'1',
914                ESCAPE_CODE,
915                b'2',
916                ESCAPE_CODE,
917                b'3',
918            ]
919        );
920    }
921
922    #[test]
923    fn test_bitmap() {
924        let mut map = CodesBitmap::default();
925        map.set(10);
926        map.set(100);
927        map.set(500);
928
929        let codes: Vec<u16> = map.codes().collect();
930        assert_eq!(codes, vec![10u16, 100, 500]);
931
932        // empty case
933        let map = CodesBitmap::default();
934        assert!(map.codes().collect::<Vec<_>>().is_empty());
935
936        // edge case: first bit in each block is set
937        let mut map = CodesBitmap::default();
938        (0..8).for_each(|i| map.set(64 * i));
939        assert_eq!(
940            map.codes().collect::<Vec<_>>(),
941            (0u16..8).map(|i| 64 * i).collect::<Vec<_>>(),
942        );
943
944        // Full bitmap case. There are only 512 values, so test them all
945        let mut map = CodesBitmap::default();
946        for i in 0..512 {
947            map.set(i);
948        }
949        assert_eq!(
950            map.codes().collect::<Vec<_>>(),
951            (0u16..511u16).collect::<Vec<_>>()
952        );
953    }
954
955    #[test]
956    #[should_panic(expected = "code cannot exceed")]
957    fn test_bitmap_invalid() {
958        let mut map = CodesBitmap::default();
959        map.set(512);
960    }
961
962    #[test]
963    fn test_no_duplicate_symbols() {
964        // Train on data that is likely to produce duplicate 1-byte and 2-byte candidates.
965        let text = b"aababcabcdabcde";
966        let corpus: Vec<&[u8]> = std::iter::repeat_n(text.as_slice(), 100).collect();
967        let compressor = Compressor::train(&corpus);
968
969        let symbols = &compressor.symbol_table()[0..compressor.n_symbols()];
970        let lengths = &compressor.symbol_lengths()[0..compressor.n_symbols()];
971
972        // Collect all 1-byte symbols and check for duplicates.
973        let one_byte: Vec<u8> = symbols
974            .iter()
975            .zip(lengths.iter())
976            .filter(|&(_, &len)| len == 1)
977            .map(|(sym, _)| sym.first_byte())
978            .collect();
979        let mut one_byte_sorted = one_byte.clone();
980        one_byte_sorted.sort();
981        one_byte_sorted.dedup();
982        assert_eq!(
983            one_byte.len(),
984            one_byte_sorted.len(),
985            "duplicate 1-byte symbols found"
986        );
987
988        // Collect all 2-byte symbols and check for duplicates.
989        let two_byte: Vec<u16> = symbols
990            .iter()
991            .zip(lengths.iter())
992            .filter(|&(_, &len)| len == 2)
993            .map(|(sym, _)| sym.first2())
994            .collect();
995        let mut two_byte_sorted = two_byte.clone();
996        two_byte_sorted.sort();
997        two_byte_sorted.dedup();
998        assert_eq!(
999            two_byte.len(),
1000            two_byte_sorted.len(),
1001            "duplicate 2-byte symbols found"
1002        );
1003    }
1004}