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        for idx in 0..=65_535 {
284            table.codes_two_byte.push(Code::new_escape(idx as u8));
285        }
286
287        table
288    }
289}
290
291impl Default for CompressorBuilder {
292    fn default() -> Self {
293        Self::new()
294    }
295}
296
297impl CompressorBuilder {
298    /// Attempt to insert a new symbol at the end of the table.
299    ///
300    /// # Panics
301    ///
302    /// Panics if the table is already full.
303    ///
304    /// # Returns
305    ///
306    /// Returns true if the symbol was inserted successfully, or false if it conflicted
307    /// with an existing symbol.
308    pub fn insert(&mut self, symbol: Symbol, len: usize) -> bool {
309        assert!(self.n_symbols < 255, "cannot insert into full symbol table");
310        assert_eq!(len, symbol.len(), "provided len must equal symbol.len()");
311
312        if len == 2 {
313            // shortCodes
314            self.codes_two_byte[symbol.first2() as usize] =
315                Code::new_symbol_building(self.n_symbols, 2);
316        } else if len == 1 {
317            // byteCodes
318            self.codes_one_byte[symbol.first_byte() as usize] =
319                Code::new_symbol_building(self.n_symbols, 1);
320        } else {
321            // Symbols of 3 or more bytes go into the hash table
322            if !self.lossy_pht.insert(symbol, len, self.n_symbols) {
323                return false;
324            }
325        }
326
327        // Increment length histogram.
328        self.len_histogram[len - 1] += 1;
329
330        // Insert successfully stored symbol at end of the symbol table
331        // Note the rescaling from range [0-254] -> [256, 510].
332        self.symbols[256 + (self.n_symbols as usize)] = symbol;
333        self.n_symbols += 1;
334        true
335    }
336
337    /// Clear all set items from the compressor.
338    ///
339    /// This is considerably faster than building a new Compressor from scratch for each
340    /// iteration of the `train` loop.
341    fn clear(&mut self) {
342        // Eliminate every observed code from the table.
343        for code in 0..(256 + self.n_symbols as usize) {
344            let symbol = self.symbols[code];
345            if symbol.len() == 1 {
346                // Reset the entry from the codes_one_byte array.
347                self.codes_one_byte[symbol.first_byte() as usize] =
348                    Code::new_escape(symbol.first_byte());
349            } else if symbol.len() == 2 {
350                // Reset the entry from the codes_two_byte array.
351                self.codes_two_byte[symbol.first2() as usize] =
352                    Code::new_escape(symbol.first_byte());
353            } else {
354                // Clear the hashtable entry
355                self.lossy_pht.remove(symbol);
356            }
357        }
358
359        // Reset len histogram
360        for i in 0..=7 {
361            self.len_histogram[i] = 0;
362        }
363
364        self.n_symbols = 0;
365    }
366
367    /// Finalizing the table is done once building is complete to prepare for efficient
368    /// compression.
369    ///
370    /// When we finalize the table, the following modifications are made in-place:
371    ///
372    /// 1. The codes are renumbered so that all symbols are ordered by length (order 23456781).
373    ///    During this process, the two byte symbols are separated into a byte_lim and a suffix_lim,
374    ///    so we know that we don't need to check the suffix limitations instead.
375    /// 2. The 1-byte symbols index is merged into the 2-byte symbols index to allow for use of only
376    ///    a single index in front of the hash table.
377    ///
378    /// # Returns
379    ///
380    /// Returns the `suffix_lim`, which is the index of the two-byte code before where we know
381    /// there are no longer suffixies in the symbol table.
382    ///
383    /// Also returns the lengths vector, which is of length `n_symbols` and contains the
384    /// length for each of the values.
385    fn finalize(&mut self) -> (u8, Vec<u8>) {
386        // Create a cumulative sum of each of the elements of the input line numbers.
387        // Do a map that includes the previously seen value as well.
388        // Regroup symbols based on their lengths.
389        // Space at the end of the symbol table reserved for the one-byte codes.
390        let byte_lim = self.n_symbols - self.len_histogram[0];
391
392        // Start code for each length.
393        // Length 1: at the end of symbol table.
394        // Length 2: starts at 0. Split into before/after suffixLim.
395        let mut codes_by_length = [0u8; 8];
396        codes_by_length[0] = byte_lim;
397        codes_by_length[1] = 0;
398
399        // codes for lengths 3..=8 start where the previous ones end.
400        for i in 1..7 {
401            codes_by_length[i + 1] = codes_by_length[i] + self.len_histogram[i];
402        }
403
404        // no_suffix_code is the lowest code for a symbol that does not have a longer 3+ byte
405        // suffix in the table.
406        // This value starts at 0 and extends up.
407        let mut no_suffix_code = 0;
408
409        // The codes that do not have a suffix begin just before the range of the 3-byte codes.
410        let mut has_suffix_code = codes_by_length[2];
411
412        // Assign each symbol a new code ordered by lengths, in the order
413        // 2(no suffix) | 2 (suffix) | 3 | 4 | 5 | 6 | 7 | 8 | 1
414        let mut new_codes = [0u8; FSST_CODE_BASE as usize];
415
416        let mut symbol_lens = [0u8; FSST_CODE_BASE as usize];
417
418        for i in 0..(self.n_symbols as usize) {
419            let symbol = self.symbols[256 + i];
420            let len = symbol.len();
421            if len == 2 {
422                let has_suffix = self
423                    .symbols
424                    .iter()
425                    .skip(FSST_CODE_BASE as usize)
426                    .enumerate()
427                    .any(|(k, other)| i != k && symbol.first2() == other.first2());
428
429                if has_suffix {
430                    // Symbols that have a longer suffix are inserted at the end of the 2-byte range
431                    has_suffix_code -= 1;
432                    new_codes[i] = has_suffix_code;
433                } else {
434                    // Symbols that do not have a longer suffix are inserted at the start of
435                    // the 2-byte range.
436                    new_codes[i] = no_suffix_code;
437                    no_suffix_code += 1;
438                }
439            } else {
440                // Assign new code based on the next code available for the given length symbol
441                new_codes[i] = codes_by_length[len - 1];
442                codes_by_length[len - 1] += 1;
443            }
444
445            // Write the symbol into the front half of the symbol table.
446            // We are reusing the space that was previously occupied by escapes.
447            self.symbols[new_codes[i] as usize] = symbol;
448            symbol_lens[new_codes[i] as usize] = len as u8;
449        }
450
451        // Truncate the symbol table to only include the "true" symbols.
452        self.symbols.truncate(self.n_symbols as usize);
453
454        // Rewrite the codes_one_byte table to point at the new code values.
455        // Replace pseudocodes with escapes.
456        for byte in 0..=255 {
457            let one_byte = self.codes_one_byte[byte];
458            if one_byte.extended_code() >= FSST_CODE_BASE {
459                let new_code = new_codes[one_byte.code() as usize];
460                self.codes_one_byte[byte] = Code::new_symbol(new_code, 1);
461            } else {
462                // After finalize: codes_one_byte contains the unused value
463                self.codes_one_byte[byte] = Code::UNUSED;
464            }
465        }
466
467        // Rewrite the codes_two_byte table to point at the new code values.
468        // Replace pseudocodes with escapes.
469        for two_bytes in 0..=65_535 {
470            let two_byte = self.codes_two_byte[two_bytes];
471            if two_byte.extended_code() >= FSST_CODE_BASE {
472                let new_code = new_codes[two_byte.code() as usize];
473                self.codes_two_byte[two_bytes] = Code::new_symbol(new_code, 2);
474            } else {
475                // The one-byte code for the given code number here...
476                self.codes_two_byte[two_bytes] = self.codes_one_byte[two_bytes & 0xFF];
477            }
478        }
479
480        // Reset values in the hash table as well.
481        self.lossy_pht.renumber(&new_codes);
482
483        // Pre-compute the lengths
484        let mut lengths = Vec::with_capacity(self.n_symbols as usize);
485        for symbol in &self.symbols {
486            lengths.push(symbol.len() as u8);
487        }
488
489        (has_suffix_code, lengths)
490    }
491
492    /// Build into the final hash table.
493    pub fn build(mut self) -> Compressor {
494        // finalize the symbol table by inserting the codes_twobyte values into
495        // the relevant parts of the `codes_onebyte` set.
496
497        let (has_suffix_code, lengths) = self.finalize();
498
499        Compressor {
500            symbols: self.symbols,
501            lengths,
502            n_symbols: self.n_symbols,
503            has_suffix_code,
504            codes_two_byte: self.codes_two_byte,
505            lossy_pht: self.lossy_pht,
506        }
507    }
508}
509
510/// The number of generations used for training. This is taken from the [FSST paper].
511///
512/// [FSST paper]: https://www.vldb.org/pvldb/vol13/p2649-boncz.pdf
513#[cfg(not(miri))]
514const GENERATIONS: [usize; 5] = [8usize, 38, 68, 98, 128];
515#[cfg(miri)]
516const GENERATIONS: [usize; 3] = [8usize, 38, 128];
517
518const FSST_SAMPLETARGET: usize = 1 << 14;
519const FSST_SAMPLELINE: usize = 512;
520
521/// Create a sample from a set of strings in the input.
522///
523/// The sample is picked based on criteria from the C++ implementation, and it
524/// is a vector of subranges of the input strings `str_in`.
525fn make_sample<'a>(str_in: &[&'a [u8]], tot_size: usize) -> Vec<&'a [u8]> {
526    let mut sample: Vec<&[u8]> = Vec::new();
527
528    if tot_size < FSST_SAMPLETARGET {
529        return str_in.to_vec();
530    }
531
532    let mut sample_rnd = fsst_hash(4637947);
533    let sample_lim = FSST_SAMPLETARGET;
534
535    let mut sample_size = 0;
536
537    while sample_size < sample_lim {
538        sample_rnd = fsst_hash(sample_rnd);
539        let line_nr = (sample_rnd as usize) % str_in.len();
540
541        // Find the first non-empty chunk starting at line_nr, wrapping around if
542        // necessary.
543        let Some(line) = (line_nr..str_in.len())
544            .chain(0..line_nr)
545            .map(|line_nr| str_in[line_nr])
546            .find(|line| !line.is_empty())
547        else {
548            return sample;
549        };
550
551        let chunks = 1 + ((line.len() - 1) / FSST_SAMPLELINE);
552        sample_rnd = fsst_hash(sample_rnd);
553        let chunk = FSST_SAMPLELINE * ((sample_rnd as usize) % chunks);
554
555        let len = FSST_SAMPLELINE.min(line.len() - chunk);
556
557        sample.push(&line[chunk..chunk + len]);
558        sample_size += len;
559    }
560
561    sample
562}
563
564/// Hash function used in various components of the library.
565///
566/// This is equivalent to the FSST_HASH macro from the C++ implementation.
567#[inline]
568pub(crate) fn fsst_hash(value: u64) -> u64 {
569    value.wrapping_mul(2971215073) ^ value.wrapping_shr(15)
570}
571
572impl Compressor {
573    /// Build and train a `Compressor` from a sample corpus of text.
574    ///
575    /// This function implements the generational algorithm described in the [FSST paper] Section
576    /// 4.3. Starting with an empty symbol table, it iteratively compresses the corpus, then attempts
577    /// to merge symbols when doing so would yield better compression than leaving them unmerged. The
578    /// resulting table will have at most 255 symbols (the 256th symbol is reserved for the escape
579    /// code).
580    ///
581    /// [FSST paper]: https://www.vldb.org/pvldb/vol13/p2649-boncz.pdf
582    pub fn train(values: &Vec<&[u8]>) -> Self {
583        let mut builder = CompressorBuilder::new();
584
585        if values.is_empty() {
586            return builder.build();
587        }
588
589        let mut counters = Counter::new();
590        let mut pqueue = BinaryHeap::with_capacity(65_536);
591
592        let tot_size: usize = values.iter().map(|s| s.len()).sum();
593        let sampled = tot_size >= FSST_SAMPLETARGET;
594        let sample = make_sample(values, tot_size);
595        for sample_frac in GENERATIONS {
596            for (i, line) in sample.iter().enumerate() {
597                if sample_frac < 128 && ((fsst_hash(i as u64) & 127) as usize) > sample_frac {
598                    continue;
599                }
600
601                builder.compress_count(line, &mut counters);
602            }
603
604            // Clear the heap before we use it again
605            pqueue.clear();
606            let prune = sample_frac >= 128 && !sampled;
607            builder.optimize(&counters, sample_frac, &mut pqueue, prune);
608            counters.clear();
609        }
610
611        builder.build()
612    }
613}
614
615impl CompressorBuilder {
616    /// Find the longest symbol using the hash table and the codes_one_byte and codes_two_byte indexes.
617    fn find_longest_symbol(&self, word: u64) -> Code {
618        // Probe the hash table first to see if we have a long match
619        let entry = self.lossy_pht.lookup(word);
620        let ignored_bits = entry.ignored_bits;
621
622        // If the entry is valid, return the code
623        if !entry.is_unused() && compare_masked(word, entry.symbol.to_u64(), ignored_bits) {
624            return entry.code;
625        }
626
627        // Try and match first two bytes
628        let twobyte = self.codes_two_byte[word as u16 as usize];
629        if twobyte.extended_code() >= FSST_CODE_BASE {
630            return twobyte;
631        }
632
633        // Fall back to single-byte match
634        self.codes_one_byte[word as u8 as usize]
635    }
636
637    /// Compress the text using the current symbol table. Count the code occurrences
638    /// and code-pair occurrences, calculating total gain using the current compressor.
639    ///
640    /// NOTE: this is largely an unfortunate amount of copy-paste from `compress`, just to make sure
641    /// we can do all the counting in a single pass.
642    fn compress_count(&self, sample: &[u8], counter: &mut Counter) -> usize {
643        let mut gain = 0;
644        if sample.is_empty() {
645            return gain;
646        }
647
648        let mut in_ptr = sample.as_ptr();
649
650        // SAFETY: `end` will point just after the end of the `plaintext` slice.
651        let in_end = unsafe { in_ptr.byte_add(sample.len()) };
652        let in_end_sub8 = in_end as usize - 8;
653
654        let mut prev_code: u16 = FSST_CODE_MASK;
655
656        while (in_ptr as usize) < (in_end_sub8) {
657            // SAFETY: ensured in-bounds by loop condition.
658            let word: u64 = unsafe { std::ptr::read_unaligned(in_ptr as *const u64) };
659            let code = self.find_longest_symbol(word);
660            let code_u16 = code.extended_code();
661
662            // Gain increases by the symbol length if a symbol matches, or 0
663            // if an escape is emitted.
664            gain += (code.len() as usize) - ((code_u16 < 256) as usize);
665
666            // Record the single and pair counts
667            counter.record_count1(code_u16);
668            counter.record_count2(prev_code, code_u16);
669
670            // Also record the count for just extending by a single byte, but only if
671            // the symbol is not itself a single byte.
672            if code.len() > 1 {
673                let code_first_byte = self.symbols[code_u16 as usize].first_byte() as u16;
674                counter.record_count1(code_first_byte);
675                counter.record_count2(prev_code, code_first_byte);
676            }
677
678            // SAFETY: pointer bound is checked in loop condition before any access is made.
679            in_ptr = unsafe { in_ptr.byte_add(code.len() as usize) };
680
681            prev_code = code_u16;
682        }
683
684        let remaining_bytes = unsafe { in_end.byte_offset_from(in_ptr) };
685        assert!(
686            remaining_bytes.is_positive(),
687            "in_ptr exceeded in_end, should not be possible"
688        );
689        let remaining_bytes = remaining_bytes as usize;
690
691        // Load the last `remaining_byte`s of data into a final world. We then replicate the loop above,
692        // but shift data out of this word rather than advancing an input pointer and potentially reading
693        // unowned memory
694        let mut bytes = [0u8; 8];
695        unsafe {
696            // SAFETY: it is safe to read up to remaining_bytes from in_ptr, and remaining_bytes
697            //  will be <= 8 bytes.
698            std::ptr::copy_nonoverlapping(in_ptr, bytes.as_mut_ptr(), remaining_bytes);
699        }
700        let mut last_word = u64::from_le_bytes(bytes);
701
702        let mut remaining_bytes = remaining_bytes;
703
704        while remaining_bytes > 0 {
705            // SAFETY: ensured in-bounds by loop condition.
706            let code = self.find_longest_symbol(last_word);
707            let code_u16 = code.extended_code();
708
709            // Gain increases by the symbol length if a symbol matches, or 0
710            // if an escape is emitted.
711            gain += (code.len() as usize) - ((code_u16 < 256) as usize);
712
713            // Record the single and pair counts
714            counter.record_count1(code_u16);
715            counter.record_count2(prev_code, code_u16);
716
717            // Also record the count for just extending by a single byte, but only if
718            // the symbol is not itself a single byte.
719            if code.len() > 1 {
720                let code_first_byte = self.symbols[code_u16 as usize].first_byte() as u16;
721                counter.record_count1(code_first_byte);
722                counter.record_count2(prev_code, code_first_byte);
723            }
724
725            // Advance our last_word "input pointer" by shifting off the covered values.
726            let advance = code.len() as usize;
727            remaining_bytes -= advance;
728            last_word = advance_8byte_word(last_word, advance);
729
730            prev_code = code_u16;
731        }
732
733        gain
734    }
735
736    /// Using a set of counters and the existing set of symbols, build a new
737    /// set of symbols/codes that optimizes the gain over the distribution in `counter`.
738    fn optimize(
739        &mut self,
740        counters: &Counter,
741        sample_frac: usize,
742        pqueue: &mut BinaryHeap<Candidate>,
743        prune: bool,
744    ) {
745        // Use a HashMap to deduplicate candidates by symbol content, combining gains
746        // when the same symbol is encountered via different codes.
747        // This matches the C++ implementation's use of unordered_set<QSymbol> with addOrInc.
748        // NOTE: we use fxhash since that is the best Rust hasher for 64-bit ints.
749        let mut candidates = FxHashMap::with_capacity_and_hasher(256, FxBuildHasher);
750
751        for code1 in counters.first_codes() {
752            let symbol1 = self.symbols[code1 as usize];
753            let symbol1_len = symbol1.len();
754            let count = counters.count1(code1);
755
756            // From the c++ impl:
757            // "improves both compression speed (less candidates), but also quality!!"
758            // When pruning (final pass, exact counts), lower the threshold to 1
759            // so the pruning check can decide based on cost/benefit.
760            let min_count = if prune { 1 } else { 5 * sample_frac / 128 };
761            if count < min_count {
762                continue;
763            }
764
765            let mut gain = count * symbol1_len;
766            // NOTE: use heuristic from C++ implementation to boost the gain of single-byte symbols.
767            // This helps to reduce exception counts.
768            if symbol1_len == 1 {
769                gain *= 8;
770            }
771
772            // Add or combine gain for this symbol
773            *candidates.entry(symbol1).or_insert(0) += gain;
774
775            // Skip merges on last round, or when symbol cannot be extended.
776            if sample_frac >= 128 || symbol1_len == 8 {
777                continue;
778            }
779
780            for code2 in counters.second_codes(code1) {
781                let symbol2 = self.symbols[code2 as usize];
782
783                // If merging would yield a symbol of length greater than 8, skip.
784                if symbol1_len + symbol2.len() > 8 {
785                    continue;
786                }
787                let new_symbol = symbol1.concat(symbol2);
788                let gain = counters.count2(code1, code2) * new_symbol.len();
789
790                // Add or combine gain for this merged symbol
791                *candidates.entry(new_symbol).or_insert(0) += gain;
792            }
793        }
794
795        // Transfer deduplicated candidates to the priority queue
796        for (symbol, gain) in candidates {
797            pqueue.push(Candidate { symbol, gain });
798        }
799
800        // clear self in advance of inserting the symbols.
801        self.clear();
802
803        // Pop the 255 best symbols.
804        let mut n_symbols = 0;
805        while !pqueue.is_empty() && n_symbols < 255 {
806            let candidate = pqueue.pop().unwrap();
807            if prune {
808                let symbol_len = candidate.symbol.len();
809                let saves = if symbol_len == 1 {
810                    candidate.gain / 8 // undo the 8x single-byte boost
811                } else {
812                    candidate.gain
813                };
814                if saves <= symbol_len + 1 {
815                    continue;
816                }
817            }
818            if self.insert(candidate.symbol, candidate.symbol.len()) {
819                n_symbols += 1;
820            }
821        }
822    }
823}
824
825/// A candidate for inclusion in a symbol table.
826///
827/// This is really only useful for the `optimize` step of training.
828#[derive(Copy, Clone, Debug)]
829struct Candidate {
830    gain: usize,
831    symbol: Symbol,
832}
833
834impl Candidate {
835    fn comparable_form(&self) -> (usize, usize) {
836        (self.gain, self.symbol.len())
837    }
838}
839
840impl Eq for Candidate {}
841
842impl PartialEq<Self> for Candidate {
843    fn eq(&self, other: &Self) -> bool {
844        self.comparable_form().eq(&other.comparable_form())
845    }
846}
847
848impl PartialOrd<Self> for Candidate {
849    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
850        Some(self.cmp(other))
851    }
852}
853
854impl Ord for Candidate {
855    fn cmp(&self, other: &Self) -> Ordering {
856        let self_ord = (self.gain, self.symbol.len());
857        let other_ord = (other.gain, other.symbol.len());
858
859        self_ord.cmp(&other_ord)
860    }
861}
862
863#[cfg(test)]
864mod test {
865    use crate::{Compressor, ESCAPE_CODE, builder::CodesBitmap};
866
867    #[test]
868    fn test_builder() {
869        // Train a Compressor on the toy string
870        let text = b"hello hello hello hello hello";
871
872        // count of 5 is the cutoff for including a symbol in the table.
873        let table = Compressor::train(&vec![text, text, text, text, text]);
874
875        // Use the table to compress a string, see the values
876        let compressed = table.compress(text);
877
878        // Ensure that the compressed string has no escape bytes
879        assert!(compressed.iter().all(|b| *b != ESCAPE_CODE));
880
881        // Ensure that we can compress a string with no values seen at training time, with escape bytes
882        let compressed = table.compress("xyz123".as_bytes());
883        let decompressed = table.decompressor().decompress(&compressed);
884        assert_eq!(&decompressed, b"xyz123");
885        assert_eq!(
886            compressed,
887            vec![
888                ESCAPE_CODE,
889                b'x',
890                ESCAPE_CODE,
891                b'y',
892                ESCAPE_CODE,
893                b'z',
894                ESCAPE_CODE,
895                b'1',
896                ESCAPE_CODE,
897                b'2',
898                ESCAPE_CODE,
899                b'3',
900            ]
901        );
902    }
903
904    #[test]
905    fn test_bitmap() {
906        let mut map = CodesBitmap::default();
907        map.set(10);
908        map.set(100);
909        map.set(500);
910
911        let codes: Vec<u16> = map.codes().collect();
912        assert_eq!(codes, vec![10u16, 100, 500]);
913
914        // empty case
915        let map = CodesBitmap::default();
916        assert!(map.codes().collect::<Vec<_>>().is_empty());
917
918        // edge case: first bit in each block is set
919        let mut map = CodesBitmap::default();
920        (0..8).for_each(|i| map.set(64 * i));
921        assert_eq!(
922            map.codes().collect::<Vec<_>>(),
923            (0u16..8).map(|i| 64 * i).collect::<Vec<_>>(),
924        );
925
926        // Full bitmap case. There are only 512 values, so test them all
927        let mut map = CodesBitmap::default();
928        for i in 0..512 {
929            map.set(i);
930        }
931        assert_eq!(
932            map.codes().collect::<Vec<_>>(),
933            (0u16..511u16).collect::<Vec<_>>()
934        );
935    }
936
937    #[test]
938    #[should_panic(expected = "code cannot exceed")]
939    fn test_bitmap_invalid() {
940        let mut map = CodesBitmap::default();
941        map.set(512);
942    }
943
944    #[test]
945    fn test_no_duplicate_symbols() {
946        // Train on data that is likely to produce duplicate 1-byte and 2-byte candidates.
947        let text = b"aababcabcdabcde";
948        let corpus: Vec<&[u8]> = std::iter::repeat_n(text.as_slice(), 100).collect();
949        let compressor = Compressor::train(&corpus);
950
951        let symbols = compressor.symbol_table();
952        let lengths = compressor.symbol_lengths();
953
954        // Collect all 1-byte symbols and check for duplicates.
955        let one_byte: Vec<u8> = symbols
956            .iter()
957            .zip(lengths.iter())
958            .filter(|&(_, &len)| len == 1)
959            .map(|(sym, _)| sym.first_byte())
960            .collect();
961        let mut one_byte_sorted = one_byte.clone();
962        one_byte_sorted.sort();
963        one_byte_sorted.dedup();
964        assert_eq!(
965            one_byte.len(),
966            one_byte_sorted.len(),
967            "duplicate 1-byte symbols found"
968        );
969
970        // Collect all 2-byte symbols and check for duplicates.
971        let two_byte: Vec<u16> = symbols
972            .iter()
973            .zip(lengths.iter())
974            .filter(|&(_, &len)| len == 2)
975            .map(|(sym, _)| sym.first2())
976            .collect();
977        let mut two_byte_sorted = two_byte.clone();
978        two_byte_sorted.sort();
979        two_byte_sorted.dedup();
980        assert_eq!(
981            two_byte.len(),
982            two_byte_sorted.len(),
983            "duplicate 2-byte symbols found"
984        );
985    }
986}