1use alloc::vec::Vec;
9use super::CompressionLevel;
13use super::Matcher;
14use super::Sequence;
15use super::blocks::encode_offset_with_history;
16use super::bt::BtMatcher;
17#[cfg(test)]
18use super::cost_model::HC_MAX_LIT;
19use super::cost_model::{
20 HC_BITCOST_MULTIPLIER, HC_FORMAT_MINMATCH, HC_OPT_NODE_LEN, HC_OPT_NUM, HC_OPT_PRICE_ARENA_LEN,
21 HC_OPT_PRICE_STRIDE, HC_PREDEF_THRESHOLD, HcOptState, HcOptimalCostProfile,
22};
23#[cfg(test)]
24use super::cost_model::{HC_BLOCKSIZE_MAX, HC_MAX_LL, HC_MAX_ML, HC_MAX_OFF, HcOptPriceType};
25use super::dfast::DfastMatchGenerator;
26#[cfg(test)]
32use super::match_table::helpers::INCOMPRESSIBLE_SKIP_STEP;
33use super::match_table::helpers::MIN_MATCH_LEN;
34#[cfg(test)]
35use super::match_table::helpers::common_prefix_len;
36#[cfg(test)]
37use super::opt::ldm::HcRawSeq;
38use super::opt::ldm::{HcOptLdmState, HcRawSeqStore};
39use super::opt::types::{
40 HcCandidateQuery, HcOptimalNode, HcOptimalPlanBuffers, HcOptimalPlanState, HcOptimalSequence,
41 MatchCandidate,
42};
43use super::row::RowMatchGenerator;
44use super::simple::fast_matcher::{FAST_LEVEL_1_HASH_LOG, FAST_LEVEL_1_MLS, FastKernelMatcher};
45#[cfg(all(
46 test,
47 feature = "std",
48 target_arch = "aarch64",
49 target_endian = "little"
50))]
51use std::arch::is_aarch64_feature_detected;
52#[cfg(all(test, feature = "std", target_arch = "x86_64"))]
53use std::arch::is_x86_feature_detected;
54
55pub(crate) const DFAST_MIN_MATCH_LEN: usize = 5;
56pub(crate) const DFAST_SHORT_HASH_LOOKAHEAD: usize = 5;
60pub(crate) const ROW_MIN_MATCH_LEN: usize = 5;
61pub(crate) const DFAST_HASH_BITS: usize = 17;
84pub(crate) const DFAST_SHORT_HASH_BITS_DELTA: usize = 1;
90pub(crate) const DFAST_EMPTY_SLOT: u32 = 0;
98
99pub(crate) const DFAST_REBASE_GUARD_BAND: u32 = 1u32 << 30;
106pub(crate) const DFAST_SKIP_SEARCH_STRENGTH: usize = 6;
107pub(crate) const DFAST_SKIP_STEP_GROWTH_INTERVAL: usize = 1 << DFAST_SKIP_SEARCH_STRENGTH;
108pub(crate) const DFAST_MAX_SKIP_STEP: usize = 8;
109pub(crate) const DFAST_INCOMPRESSIBLE_SKIP_STEP: usize = 16;
110pub(crate) const ROW_HASH_BITS: usize = 20;
111pub(crate) const ROW_LOG: usize = 5;
112pub(crate) const ROW_SEARCH_DEPTH: usize = 16;
113pub(crate) const ROW_TARGET_LEN: usize = 48;
114pub(crate) const ROW_TAG_BITS: usize = 8;
115pub(crate) const ROW_EMPTY_SLOT: u32 = u32::MAX;
116pub(crate) const ROW_HASH_KEY_LEN: usize = 4;
117#[cfg(test)]
124use super::match_table::storage::{HC_PRIME3BYTES, HC_PRIME4BYTES};
125
126#[cfg(test)]
131use super::match_table::storage::HC_EMPTY;
132use super::match_table::storage::HC3_HASH_LOG;
133#[cfg(test)]
135use super::match_table::storage::{HC_CHAIN_LOG, HC_HASH_LOG};
136const HC_SEARCH_DEPTH: usize = 16;
141use super::hc::HC_MIN_MATCH_LEN;
144const HC_OPT_MIN_MATCH_LEN: usize = HC_FORMAT_MINMATCH;
145const HC_TARGET_LEN: usize = 48;
146
147use super::hc::MAX_HC_SEARCH_DEPTH;
149
150#[derive(Copy, Clone, PartialEq, Eq)]
158struct HcConfig {
159 hash_log: usize,
160 chain_log: usize,
161 search_depth: usize,
162 target_len: usize,
163 search_mls: usize,
170}
171
172#[derive(Copy, Clone, PartialEq, Eq)]
173pub(crate) struct RowConfig {
174 pub(crate) hash_bits: usize,
175 pub(crate) row_log: usize,
176 pub(crate) search_depth: usize,
177 pub(crate) target_len: usize,
178 pub(crate) mls: usize,
185}
186
187#[cfg(test)]
190const HC_CONFIG: HcConfig = HcConfig {
191 hash_log: HC_HASH_LOG,
192 chain_log: HC_CHAIN_LOG,
193 search_depth: HC_SEARCH_DEPTH,
194 target_len: HC_TARGET_LEN,
195 search_mls: 4,
196};
197
198const HC_OVERRIDE_DEFAULT: HcConfig = HcConfig {
204 hash_log: super::match_table::storage::HC_HASH_LOG,
205 chain_log: super::match_table::storage::HC_CHAIN_LOG,
206 search_depth: HC_SEARCH_DEPTH,
207 target_len: HC_TARGET_LEN,
208 search_mls: 4,
209};
210
211const BTULTRA2_HC_CONFIG: HcConfig = HcConfig {
212 hash_log: 24,
213 chain_log: 24,
214 search_depth: 512,
215 target_len: 256,
216 search_mls: 4,
217};
218
219const BTULTRA2_HC_CONFIG_L22: HcConfig = HcConfig {
220 hash_log: 25,
221 chain_log: 27,
222 search_depth: 512,
223 target_len: 999,
224 search_mls: 4,
225};
226
227const BTULTRA2_HC_CONFIG_L22_256K: HcConfig = HcConfig {
228 hash_log: 19,
229 chain_log: 19,
230 search_depth: 1 << 13,
231 target_len: 999,
232 search_mls: 4,
233};
234
235const BTULTRA2_HC_CONFIG_L22_128K: HcConfig = HcConfig {
236 hash_log: 17,
237 chain_log: 18,
238 search_depth: 1 << 11,
239 target_len: 999,
240 search_mls: 4,
241};
242
243const BTULTRA2_HC_CONFIG_L22_16K: HcConfig = HcConfig {
244 hash_log: 15,
245 chain_log: 15,
246 search_depth: 1 << 10,
247 target_len: 999,
248 search_mls: 4,
249};
250
251#[cfg(test)]
254const ROW_CONFIG: RowConfig = RowConfig {
255 hash_bits: ROW_HASH_BITS,
256 row_log: ROW_LOG,
257 search_depth: ROW_SEARCH_DEPTH,
258 target_len: ROW_TARGET_LEN,
259 mls: ROW_MIN_MATCH_LEN,
260};
261
262const ROW_L5: RowConfig = RowConfig {
279 hash_bits: 19,
280 row_log: 4,
281 search_depth: 8,
282 target_len: 2,
283 mls: ROW_MIN_MATCH_LEN,
284};
285
286const ROW_L6: RowConfig = RowConfig {
300 hash_bits: 19,
301 row_log: 4,
302 search_depth: 8,
303 target_len: 4,
304 mls: ROW_MIN_MATCH_LEN,
305};
306const ROW_L7: RowConfig = RowConfig {
307 hash_bits: 20,
308 row_log: 4,
309 search_depth: 16,
310 target_len: 8,
311 mls: ROW_MIN_MATCH_LEN,
312};
313const ROW_L8: RowConfig = RowConfig {
314 hash_bits: 20,
315 row_log: 4,
316 search_depth: 16,
317 target_len: 16,
318 mls: ROW_MIN_MATCH_LEN,
319};
320const ROW_L9: RowConfig = RowConfig {
321 hash_bits: 21,
322 row_log: 4,
323 search_depth: 16,
324 target_len: 16,
325 mls: ROW_MIN_MATCH_LEN,
326};
327const ROW_L10: RowConfig = RowConfig {
328 hash_bits: 22,
329 row_log: 5,
330 search_depth: 32,
331 target_len: 16,
332 mls: ROW_MIN_MATCH_LEN,
333};
334const ROW_L11: RowConfig = RowConfig {
335 hash_bits: 22,
336 row_log: 6,
337 search_depth: 64,
338 target_len: 16,
339 mls: ROW_MIN_MATCH_LEN,
340};
341const ROW_L12: RowConfig = RowConfig {
342 hash_bits: 23,
343 row_log: 6,
344 search_depth: 64,
345 target_len: 32,
346 mls: ROW_MIN_MATCH_LEN,
347};
348
349#[derive(Copy, Clone, PartialEq, Eq)]
357struct DfastConfig {
358 long_hash_log: u8,
359 short_hash_log: u8,
360}
361
362const DFAST_L3: DfastConfig = DfastConfig {
365 long_hash_log: 17,
366 short_hash_log: 16,
367};
368const DFAST_L4: DfastConfig = DfastConfig {
369 long_hash_log: 18,
370 short_hash_log: 18,
371};
372
373#[derive(Copy, Clone, PartialEq, Eq)]
378struct FastConfig {
379 hash_log: u32,
380 mls: u32,
381 step_size: usize,
382}
383
384const FAST_L1: FastConfig = FastConfig {
385 hash_log: 14,
386 mls: 7,
393 step_size: 2,
394};
395const FAST_L2: FastConfig = FastConfig {
396 hash_log: 16,
397 mls: 6,
398 step_size: 2,
399};
400
401#[derive(Copy, Clone, PartialEq, Eq)]
407struct LevelParams {
408 strategy_tag: super::strategy::StrategyTag,
409 search: super::strategy::SearchMethod,
415 window_log: u8,
416 lazy_depth: u8,
417 fast: Option<FastConfig>,
424 dfast: Option<DfastConfig>,
425 hc: Option<HcConfig>,
426 row: Option<RowConfig>,
427}
428
429impl LevelParams {
430 fn backend(&self) -> super::strategy::BackendTag {
435 self.search.backend()
436 }
437
438 fn parse(&self) -> super::strategy::ParseMode {
444 match self.search {
445 super::strategy::SearchMethod::BinaryTree => super::strategy::ParseMode::Optimal,
446 _ => super::strategy::ParseMode::from_lazy_depth(self.lazy_depth),
447 }
448 }
449
450 fn pre_split(&self) -> Option<u8> {
458 use super::strategy::StrategyTag;
459 Some(match self.strategy_tag {
472 StrategyTag::Fast | StrategyTag::Dfast => 0,
475 StrategyTag::Greedy => 0,
477 StrategyTag::Lazy => {
478 if self.lazy_depth >= 2 {
479 1 } else {
481 0 }
483 }
484 StrategyTag::Btlazy2 => 1, StrategyTag::BtOpt | StrategyTag::BtUltra | StrategyTag::BtUltra2 => 2,
486 })
487 }
488}
489
490fn apply_param_overrides(params: &mut LevelParams, ov: &super::parameters::ParamOverrides) {
498 use super::strategy::SearchMethod;
499
500 if let Some(strategy) = ov.strategy {
502 let tag = strategy.tag();
503 params.strategy_tag = tag;
504 params.search = tag.search();
505 params.lazy_depth = strategy.lazy_depth();
506 }
507
508 match params.search {
511 SearchMethod::Fast => {
512 params.fast.get_or_insert(FAST_L1);
513 }
514 SearchMethod::DoubleFast => {
515 params.dfast.get_or_insert(DFAST_L3);
516 }
517 SearchMethod::RowHash => {
518 params.row.get_or_insert(ROW_L5);
519 }
520 SearchMethod::HashChain | SearchMethod::BinaryTree => {
521 params.hc.get_or_insert(HcConfig {
526 search_mls: if matches!(params.strategy_tag, super::strategy::StrategyTag::Btlazy2)
527 {
528 5
529 } else {
530 HC_OVERRIDE_DEFAULT.search_mls
531 },
532 ..HC_OVERRIDE_DEFAULT
533 });
534 }
535 }
536
537 if let Some(window_log) = ov.window_log {
539 params.window_log = window_log;
540 }
541
542 match params.search {
546 SearchMethod::Fast => {
547 if let Some(fast) = params.fast.as_mut() {
548 if let Some(hash_log) = ov.hash_log {
549 fast.hash_log = hash_log;
550 }
551 if let Some(min_match) = ov.min_match {
552 fast.mls = min_match;
553 }
554 }
555 }
556 SearchMethod::DoubleFast => {
557 if let Some(dfast) = params.dfast.as_mut() {
558 if let Some(hash_log) = ov.hash_log {
562 dfast.long_hash_log = hash_log as u8;
563 }
564 if let Some(chain_log) = ov.chain_log {
565 dfast.short_hash_log = chain_log as u8;
566 }
567 }
568 }
569 SearchMethod::RowHash => {
570 if let Some(row) = params.row.as_mut() {
571 if let Some(hash_log) = ov.hash_log {
576 row.hash_bits = hash_log as usize;
577 }
578 if let Some(search_log) = ov.search_log {
579 let row_log = (search_log as usize).clamp(4, 6);
582 row.row_log = row_log;
583 row.search_depth = 1usize << (search_log as usize).min(row_log);
584 }
585 if let Some(target_length) = ov.target_length {
586 row.target_len = target_length as usize;
587 }
588 if let Some(min_match) = ov.min_match {
589 row.mls = min_match as usize;
590 }
591 }
592 }
593 SearchMethod::HashChain | SearchMethod::BinaryTree => {
594 if let Some(hc) = params.hc.as_mut() {
595 if let Some(hash_log) = ov.hash_log {
596 hc.hash_log = hash_log as usize;
597 }
598 if let Some(chain_log) = ov.chain_log {
599 hc.chain_log = chain_log as usize;
600 }
601 if let Some(search_log) = ov.search_log {
602 hc.search_depth = 1usize << search_log;
603 }
604 if let Some(target_length) = ov.target_length {
605 hc.target_len = target_length as usize;
606 }
607 if let Some(min_match) = ov.min_match {
608 hc.search_mls = (min_match as usize).clamp(4, 6);
613 }
614 }
615 }
616 }
617}
618
619#[cfg(feature = "hash")]
623fn ldm_strategy_ordinal(tag: super::strategy::StrategyTag, lazy_depth: u8) -> u32 {
624 use super::strategy::StrategyTag;
625 match tag {
626 StrategyTag::Fast => 1,
627 StrategyTag::Dfast => 2,
628 StrategyTag::Greedy => 3,
629 StrategyTag::Lazy => {
630 if lazy_depth >= 2 {
631 5
632 } else {
633 4
634 }
635 }
636 StrategyTag::Btlazy2 => 6,
638 StrategyTag::BtOpt => 7,
639 StrategyTag::BtUltra => 8,
640 StrategyTag::BtUltra2 => 9,
641 }
642}
643
644pub(crate) fn source_size_ceil_log(size: u64) -> u8 {
654 if size == 0 {
655 MIN_WINDOW_LOG
656 } else {
657 (64 - (size - 1).leading_zeros()) as u8
658 }
659}
660
661pub(crate) const FAST_ATTACH_DICT_CUTOFF_LOG: u8 = 31;
687
688pub(crate) const MAX_FAST_ATTACH_DICT_REGION: usize = 1 << 24;
696
697const DFAST_ATTACH_DICT_CUTOFF_LOG: u8 = 14;
706
707const ROW_ATTACH_DICT_CUTOFF_LOG: u8 = 15;
712
713const HC_ATTACH_DICT_CUTOFF_LOG: u8 = 15;
719
720const BT_OPT_ATTACH_DICT_CUTOFF_LOG: u8 = 15;
726
727const BT_ULTRA_ATTACH_DICT_CUTOFF_LOG: u8 = 13;
732
733fn dfast_hash_bits_for_window(max_window_size: usize) -> usize {
737 let window_log = (usize::BITS - 1 - max_window_size.leading_zeros()) as usize;
738 window_log.max(MIN_WINDOW_LOG as usize)
739}
740
741fn row_hash_bits_for_window(max_window_size: usize) -> usize {
742 let window_log = (usize::BITS - 1 - max_window_size.leading_zeros()) as usize;
751 (window_log + 1).max(MIN_WINDOW_LOG as usize)
752}
753
754fn hc_hash_bits_for_window(max_window_size: usize) -> usize {
759 let window_log = (usize::BITS - 1 - max_window_size.leading_zeros()) as usize;
760 window_log.max(MIN_WINDOW_LOG as usize)
761}
762
763#[rustfmt::skip]
772const LEVEL_TABLE: [LevelParams; 22] = [
773 LevelParams { strategy_tag: super::strategy::StrategyTag::Fast, search: super::strategy::SearchMethod::Fast, window_log: 19, lazy_depth: 0, fast: Some(FAST_L1), dfast: None, hc: None, row: None },
778 LevelParams { strategy_tag: super::strategy::StrategyTag::Fast, search: super::strategy::SearchMethod::Fast, window_log: 20, lazy_depth: 0, fast: Some(FAST_L2), dfast: None, hc: None, row: None },
779 LevelParams { strategy_tag: super::strategy::StrategyTag::Dfast, search: super::strategy::SearchMethod::DoubleFast, window_log: 21, lazy_depth: 1, fast: None, dfast: Some(DFAST_L3), hc: None, row: None },
780 LevelParams { strategy_tag: super::strategy::StrategyTag::Dfast, search: super::strategy::SearchMethod::DoubleFast, window_log: 21, lazy_depth: 1, fast: None, dfast: Some(DFAST_L4), hc: None, row: None },
781 LevelParams { strategy_tag: super::strategy::StrategyTag::Greedy, search: super::strategy::SearchMethod::RowHash, window_log: 21, lazy_depth: 0, fast: None, dfast: None, hc: None, row: Some(ROW_L5) },
789 LevelParams { strategy_tag: super::strategy::StrategyTag::Lazy, search: super::strategy::SearchMethod::RowHash, window_log: 21, lazy_depth: 1, fast: None, dfast: None, hc: None, row: Some(ROW_L6) },
798 LevelParams { strategy_tag: super::strategy::StrategyTag::Lazy, search: super::strategy::SearchMethod::RowHash, window_log: 21, lazy_depth: 1, fast: None, dfast: None, hc: None, row: Some(ROW_L7) },
799 LevelParams { strategy_tag: super::strategy::StrategyTag::Lazy, search: super::strategy::SearchMethod::RowHash, window_log: 21, lazy_depth: 2, fast: None, dfast: None, hc: None, row: Some(ROW_L8) },
800 LevelParams { strategy_tag: super::strategy::StrategyTag::Lazy, search: super::strategy::SearchMethod::RowHash, window_log: 22, lazy_depth: 2, fast: None, dfast: None, hc: None, row: Some(ROW_L9) },
801 LevelParams { strategy_tag: super::strategy::StrategyTag::Lazy, search: super::strategy::SearchMethod::RowHash, window_log: 22, lazy_depth: 2, fast: None, dfast: None, hc: None, row: Some(ROW_L10) },
802 LevelParams { strategy_tag: super::strategy::StrategyTag::Lazy, search: super::strategy::SearchMethod::RowHash, window_log: 22, lazy_depth: 2, fast: None, dfast: None, hc: None, row: Some(ROW_L11) },
803 LevelParams { strategy_tag: super::strategy::StrategyTag::Lazy, search: super::strategy::SearchMethod::RowHash, window_log: 22, lazy_depth: 2, fast: None, dfast: None, hc: None, row: Some(ROW_L12) },
804 LevelParams { strategy_tag: super::strategy::StrategyTag::Btlazy2, search: super::strategy::SearchMethod::BinaryTree, window_log: 22, lazy_depth: 2, fast: None, dfast: None, hc: Some(HcConfig { hash_log: 22, chain_log: 22, search_depth: 16, target_len: 32, search_mls: 5 }), row: None },
813 LevelParams { strategy_tag: super::strategy::StrategyTag::Btlazy2, search: super::strategy::SearchMethod::BinaryTree, window_log: 22, lazy_depth: 2, fast: None, dfast: None, hc: Some(HcConfig { hash_log: 23, chain_log: 22, search_depth: 32, target_len: 32, search_mls: 5 }), row: None },
814 LevelParams { strategy_tag: super::strategy::StrategyTag::Btlazy2, search: super::strategy::SearchMethod::BinaryTree, window_log: 22, lazy_depth: 2, fast: None, dfast: None, hc: Some(HcConfig { hash_log: 23, chain_log: 23, search_depth: 64, target_len: 32, search_mls: 5 }), row: None },
815 LevelParams { strategy_tag: super::strategy::StrategyTag::BtOpt, search: super::strategy::SearchMethod::BinaryTree, window_log: 22, lazy_depth: 2, fast: None, dfast: None, hc: Some(HcConfig { hash_log: 22, chain_log: 22, search_depth: 32, target_len: 48, search_mls: 5 }), row: None },
816 LevelParams { strategy_tag: super::strategy::StrategyTag::BtOpt, search: super::strategy::SearchMethod::BinaryTree, window_log: 23, lazy_depth: 2, fast: None, dfast: None, hc: Some(HcConfig { hash_log: 22, chain_log: 23, search_depth: 32, target_len: 64, search_mls: 4 }), row: None },
817 LevelParams { strategy_tag: super::strategy::StrategyTag::BtUltra, search: super::strategy::SearchMethod::BinaryTree, window_log: 23, lazy_depth: 2, fast: None, dfast: None, hc: Some(HcConfig { hash_log: 22, chain_log: 23, search_depth: 64, target_len: 64, search_mls: 4 }), row: None },
818 LevelParams { strategy_tag: super::strategy::StrategyTag::BtUltra2, search: super::strategy::SearchMethod::BinaryTree, window_log: 23, lazy_depth: 2, fast: None, dfast: None, hc: Some(HcConfig { hash_log: 22, chain_log: 24, search_depth: 128, target_len: 256, search_mls: 4 }), row: None },
819 LevelParams { strategy_tag: super::strategy::StrategyTag::BtUltra2, search: super::strategy::SearchMethod::BinaryTree, window_log: 25, lazy_depth: 2, fast: None, dfast: None, hc: Some(HcConfig { hash_log: 23, chain_log: 25, search_depth: 128, target_len: 256, search_mls: 4 }), row: None },
820 LevelParams { strategy_tag: super::strategy::StrategyTag::BtUltra2, search: super::strategy::SearchMethod::BinaryTree, window_log: 26, lazy_depth: 2, fast: None, dfast: None, hc: Some(BTULTRA2_HC_CONFIG), row: None },
821 LevelParams { strategy_tag: super::strategy::StrategyTag::BtUltra2, search: super::strategy::SearchMethod::BinaryTree, window_log: 27, lazy_depth: 2, fast: None, dfast: None, hc: Some(BTULTRA2_HC_CONFIG_L22), row: None },
822];
823
824fn cdict_table_logs(
831 window_log: u8,
832 hash_log: usize,
833 chain_log: usize,
834 uses_bt: bool,
835 dict_size: usize,
836) -> (usize, usize) {
837 let (h, c) = super::cparams::create_cdict_table_logs(
838 window_log,
839 hash_log as u32,
840 chain_log as u32,
841 uses_bt,
842 dict_size,
843 );
844 (h as usize, c as usize)
845}
846
847pub(crate) const MIN_WINDOW_LOG: u8 = 10;
849const MIN_HINTED_WINDOW_LOG: u8 = 14;
855
856fn cparams_tier(source_size: Option<u64>) -> usize {
869 match source_size {
870 Some(size) if size <= 16 * 1024 => 3,
871 Some(size) if size <= 128 * 1024 => 2,
872 Some(size) if size <= 256 * 1024 => 1,
873 _ => 0,
874 }
875}
876
877fn apply_cparams_tier(level: i32, source_size: Option<u64>, p: &mut LevelParams) {
886 let tier = cparams_tier(source_size);
887 match level {
892 1 => {
894 if let Some(f) = p.fast.as_mut() {
895 f.mls = super::cparams::default_cparams(tier, 1).min_match;
896 }
897 }
898 2 => {
900 if let Some(f) = p.fast.as_mut() {
901 let cp = super::cparams::default_cparams(tier, 2);
902 f.hash_log = cp.hash_log;
903 f.mls = cp.min_match;
904 }
905 }
906 3 => {
908 if let Some(d) = p.dfast.as_mut() {
909 let cp = super::cparams::default_cparams(tier, 3);
910 d.long_hash_log = cp.hash_log as u8;
911 d.short_hash_log = cp.chain_log as u8;
912 }
913 }
914 _ => {}
915 }
916}
917
918fn adjust_params_for_source_size(mut params: LevelParams, src_size: u64) -> LevelParams {
919 let raw_src_log = source_size_ceil_log(src_size);
929 let src_log = raw_src_log.max(MIN_WINDOW_LOG).max(MIN_HINTED_WINDOW_LOG);
930 if src_log < params.window_log {
931 params.window_log = src_log;
932 }
933 let table_log = raw_src_log.max(MIN_WINDOW_LOG);
944 let backend = params.backend();
945 if backend == super::strategy::BackendTag::HashChain {
946 let hc = params
947 .hc
948 .as_mut()
949 .expect("HashChain level row carries an HcConfig");
950 if (table_log + 2) < hc.hash_log as u8 {
951 hc.hash_log = (table_log + 2) as usize;
952 }
953 if (table_log + 1) < hc.chain_log as u8 {
954 hc.chain_log = (table_log + 1) as usize;
955 }
956 } else if backend == super::strategy::BackendTag::Row {
957 let row = params
958 .row
959 .as_mut()
960 .expect("Row level row carries a RowConfig");
961 let row_cap = (table_log + 1) as usize;
969 if row_cap < row.hash_bits {
970 row.hash_bits = row_cap;
971 }
972 } else if backend == super::strategy::BackendTag::Simple {
973 let fast = params
974 .fast
975 .as_mut()
976 .expect("Fast level row carries a FastConfig");
977 let fast_cap = (table_log + 1) as u32;
978 if fast_cap < fast.hash_log {
979 fast.hash_log = fast_cap;
980 }
981 }
982 params
983}
984
985fn level22_btultra2_params_for_source_size(source_size: Option<u64>) -> LevelParams {
986 let mut hc = match source_size {
987 Some(size) if size <= 16 * 1024 => BTULTRA2_HC_CONFIG_L22_16K,
988 Some(size) if size <= 128 * 1024 => BTULTRA2_HC_CONFIG_L22_128K,
989 Some(size) if size <= 256 * 1024 => BTULTRA2_HC_CONFIG_L22_256K,
990 _ => BTULTRA2_HC_CONFIG_L22,
991 };
992 let mut window_log = match source_size {
993 Some(size) if size <= 16 * 1024 => 14,
994 Some(size) if size <= 128 * 1024 => 17,
995 Some(size) if size <= 256 * 1024 => 18,
996 _ => 27,
997 };
998 if let Some(size) = source_size
999 && size > 256 * 1024
1000 {
1001 let src_log = source_size_ceil_log(size);
1002 window_log = window_log.min(src_log.max(MIN_WINDOW_LOG));
1003 let adjusted_table_log = window_log as usize + 1;
1004 hc.hash_log = hc.hash_log.min(adjusted_table_log);
1005 hc.chain_log = hc.chain_log.min(adjusted_table_log);
1006 }
1007 LevelParams {
1008 strategy_tag: super::strategy::StrategyTag::BtUltra2,
1009 search: super::strategy::SearchMethod::BinaryTree,
1010 window_log,
1011 lazy_depth: 2,
1012 fast: None,
1013 dfast: None,
1014 hc: Some(hc),
1015 row: None,
1016 }
1017}
1018
1019pub fn estimated_compression_workspace_bytes(level: CompressionLevel) -> usize {
1025 use super::strategy::StrategyTag;
1026 let params = resolve_level_params(level, None);
1027 let window = 1usize << params.window_log;
1028 let wants_hash3 = matches!(
1033 params.strategy_tag,
1034 StrategyTag::BtUltra | StrategyTag::BtUltra2
1035 );
1036 let uses_bt = matches!(
1037 params.strategy_tag,
1038 StrategyTag::Btlazy2 | StrategyTag::BtOpt | StrategyTag::BtUltra | StrategyTag::BtUltra2
1039 );
1040 let tables = params.fast.map(|f| 4usize << f.hash_log).unwrap_or(0)
1041 + params
1042 .dfast
1043 .map(|d| (4usize << d.long_hash_log) + (4usize << d.short_hash_log))
1044 .unwrap_or(0)
1045 + params
1046 .hc
1047 .map(|h| {
1048 let hash3 = if wants_hash3 {
1049 4usize
1050 << super::match_table::storage::HC3_HASH_LOG.min(params.window_log as usize)
1051 } else {
1052 0
1053 };
1054 (4usize << h.hash_log) + (4usize << h.chain_log) + hash3
1055 })
1056 .unwrap_or(0)
1057 + params
1058 .row
1059 .map(|r| (4usize << r.hash_bits) + (2usize << r.hash_bits))
1060 .unwrap_or(0);
1061 let bt = if uses_bt {
1064 super::bt::BtMatcher::estimated_workspace_bytes()
1065 } else {
1066 0
1067 };
1068 let staging = 3 * (128 * 1024);
1071 window + tables + bt + staging
1072}
1073
1074pub fn estimated_bt_strategy_extra_bytes(strategy_ordinal: u32, window_log: u32) -> usize {
1079 if !(6..=9).contains(&strategy_ordinal) {
1080 return 0;
1081 }
1082 let hash3 = if matches!(strategy_ordinal, 8 | 9) {
1083 4usize << super::match_table::storage::HC3_HASH_LOG.min(window_log as usize)
1084 } else {
1085 0
1086 };
1087 super::bt::BtMatcher::estimated_workspace_bytes() + hash3
1088}
1089
1090fn resolve_level_params(level: CompressionLevel, source_size: Option<u64>) -> LevelParams {
1131 if matches!(level, CompressionLevel::Level(22)) {
1132 return level22_btultra2_params_for_source_size(source_size);
1133 }
1134 let params = match level {
1135 CompressionLevel::Uncompressed => LevelParams {
1136 strategy_tag: super::strategy::StrategyTag::Fast,
1137 search: super::strategy::SearchMethod::Fast,
1138 window_log: 17,
1142 lazy_depth: 0,
1143 fast: Some(FastConfig {
1147 hash_log: 14,
1148 mls: 6,
1149 step_size: 2,
1150 }),
1151 dfast: None,
1152 hc: None,
1153 row: None,
1154 },
1155 CompressionLevel::Fastest => {
1156 let mut p = LEVEL_TABLE[0];
1163 p.fast = Some(FastConfig {
1164 hash_log: 14,
1165 mls: 6,
1166 step_size: 2,
1167 });
1168 p
1169 }
1170 CompressionLevel::Default => {
1171 let mut p = LEVEL_TABLE[CompressionLevel::DEFAULT_LEVEL as usize - 1];
1176 apply_cparams_tier(CompressionLevel::DEFAULT_LEVEL, source_size, &mut p);
1177 p
1178 }
1179 CompressionLevel::Better => LEVEL_TABLE[6],
1180 CompressionLevel::Best => LEVEL_TABLE[12],
1186 CompressionLevel::Level(n) => {
1187 if n > 0 {
1188 let idx = (n as usize).min(CompressionLevel::MAX_LEVEL as usize) - 1;
1189 let mut p = LEVEL_TABLE[idx];
1190 apply_cparams_tier(n, source_size, &mut p);
1204 p
1205 } else if n == 0 {
1206 let mut p = LEVEL_TABLE[CompressionLevel::DEFAULT_LEVEL as usize - 1];
1209 apply_cparams_tier(CompressionLevel::DEFAULT_LEVEL, source_size, &mut p);
1210 p
1211 } else {
1212 let clamped = n.max(CompressionLevel::MIN_LEVEL);
1222 let target_length = (-clamped) as usize;
1223 let step_size = target_length + 1;
1224 LevelParams {
1233 strategy_tag: super::strategy::StrategyTag::Fast,
1234 search: super::strategy::SearchMethod::Fast,
1235 window_log: 19,
1236 lazy_depth: 0,
1237 fast: Some(FastConfig {
1238 hash_log: 13,
1239 mls: 7,
1240 step_size,
1241 }),
1242 dfast: None,
1243 hc: None,
1244 row: None,
1245 }
1246 }
1247 }
1248 };
1249 if let Some(size) = source_size {
1250 adjust_params_for_source_size(params, size)
1251 } else {
1252 params
1253 }
1254}
1255
1256pub(crate) fn level_pre_split(level: CompressionLevel) -> Option<usize> {
1262 if matches!(level, CompressionLevel::Uncompressed) {
1268 return None;
1269 }
1270 resolve_level_params(level, None)
1271 .pre_split()
1272 .map(usize::from)
1273}
1274
1275#[derive(Clone)]
1293enum MatcherStorage {
1294 Simple(FastKernelMatcher),
1301 Dfast(DfastMatchGenerator),
1306 Row(RowMatchGenerator),
1310 HashChain(HcMatchGenerator),
1322}
1323
1324impl MatcherStorage {
1325 fn heap_size(&self) -> usize {
1327 match self {
1328 Self::Simple(m) => m.heap_size(),
1329 Self::Dfast(m) => m.heap_size(),
1330 Self::Row(m) => m.heap_size(),
1331 Self::HashChain(m) => m.heap_size(),
1332 }
1333 }
1334
1335 fn backend(&self) -> super::strategy::BackendTag {
1337 use super::strategy::BackendTag;
1338 match self {
1339 Self::Simple(_) => BackendTag::Simple,
1340 Self::Dfast(_) => BackendTag::Dfast,
1341 Self::Row(_) => BackendTag::Row,
1342 Self::HashChain(_) => BackendTag::HashChain,
1343 }
1344 }
1345}
1346
1347pub struct MatchGeneratorDriver {
1349 vec_pool: Vec<Vec<u8>>,
1350 storage: MatcherStorage,
1357 strategy_tag: super::strategy::StrategyTag,
1363 search: super::strategy::SearchMethod,
1369 parse: super::strategy::ParseMode,
1375 #[cfg(test)]
1379 config_override: Option<(super::strategy::SearchMethod, super::strategy::ParseMode)>,
1380 param_overrides: Option<super::parameters::ParamOverrides>,
1389 slice_size: usize,
1390 base_slice_size: usize,
1391 reported_window_size: usize,
1394 dictionary_retained_budget: usize,
1397 source_size_hint: Option<u64>,
1399 dictionary_size_hint: Option<usize>,
1407 reset_size_log: Option<u8>,
1416 reset_dict_attach_ok: bool,
1424 reset_shape: Option<(
1431 LevelParams,
1432 usize,
1433 bool,
1434 Option<super::parameters::LdmOverride>,
1435 )>,
1436 borrowed_pending: Option<(usize, usize)>,
1443 primed: Option<(MatcherStorage, usize, PrimedKey)>,
1455}
1456
1457#[derive(Clone, Copy, PartialEq, Eq)]
1498struct PrimedKey {
1499 level: super::CompressionLevel,
1500 params: LevelParams,
1501 table_bits: usize,
1502 fast_attach: bool,
1503 ldm: Option<super::parameters::LdmOverride>,
1512}
1513
1514impl MatchGeneratorDriver {
1515 pub(crate) fn new(slice_size: usize, max_slices_in_window: usize) -> Self {
1520 assert!(
1537 slice_size > 0,
1538 "MatchGeneratorDriver::new requires slice_size > 0 (got 0)",
1539 );
1540 assert!(
1541 max_slices_in_window > 0,
1542 "MatchGeneratorDriver::new requires max_slices_in_window > 0 (got 0)",
1543 );
1544 let max_window_size = max_slices_in_window
1545 .checked_mul(slice_size)
1546 .expect("MatchGeneratorDriver::new: slice_size * max_slices_in_window overflows usize");
1547 let next_pow2 = max_window_size.checked_next_power_of_two().expect(
1562 "MatchGeneratorDriver::new: max_window_size too large for \
1563 next_power_of_two without overflow",
1564 );
1565 let window_log_init = next_pow2.trailing_zeros() as u8;
1566 Self {
1567 vec_pool: Vec::new(),
1568 storage: MatcherStorage::Simple(FastKernelMatcher::with_params_deferred(
1574 window_log_init,
1575 FAST_LEVEL_1_HASH_LOG,
1576 FAST_LEVEL_1_MLS,
1577 2, )),
1579 strategy_tag: super::strategy::StrategyTag::Fast,
1580 search: super::strategy::SearchMethod::Fast,
1581 parse: super::strategy::ParseMode::Greedy,
1582 #[cfg(test)]
1583 config_override: None,
1584 param_overrides: None,
1585 slice_size,
1586 base_slice_size: slice_size,
1587 reported_window_size: next_pow2,
1596 reset_size_log: None,
1597 reset_dict_attach_ok: true,
1598 reset_shape: None,
1599 dictionary_retained_budget: 0,
1600 source_size_hint: None,
1601 dictionary_size_hint: None,
1602 borrowed_pending: None,
1603 primed: None,
1604 }
1605 }
1606
1607 fn level_params(level: CompressionLevel, source_size: Option<u64>) -> LevelParams {
1608 resolve_level_params(level, source_size)
1609 }
1610
1611 pub(crate) fn set_param_overrides(
1615 &mut self,
1616 overrides: Option<super::parameters::ParamOverrides>,
1617 ) {
1618 self.param_overrides = overrides;
1619 }
1620
1621 pub(crate) fn active_backend(&self) -> super::strategy::BackendTag {
1624 self.storage.backend()
1625 }
1626
1627 pub(crate) fn borrowed_supported(&self) -> bool {
1634 use super::strategy::{BackendTag, SearchMethod, StrategyTag};
1635 match self.active_backend() {
1636 BackendTag::Simple | BackendTag::Dfast | BackendTag::Row => true,
1637 BackendTag::HashChain => match self.search {
1649 SearchMethod::HashChain => true,
1650 SearchMethod::BinaryTree => matches!(self.strategy_tag, StrategyTag::Btlazy2),
1651 _ => false,
1652 },
1653 }
1654 }
1655
1656 pub(crate) fn borrowed_dict_supported(&self) -> bool {
1665 matches!(
1666 &self.storage,
1667 MatcherStorage::Simple(m) if m.dict_is_attached()
1668 )
1669 }
1670
1671 fn simple_mut(&mut self) -> &mut FastKernelMatcher {
1672 match &mut self.storage {
1673 MatcherStorage::Simple(m) => m,
1674 _ => panic!("simple backend must be initialized by reset() before use"),
1675 }
1676 }
1677
1678 fn recycle_simple_space(&mut self) {
1692 if let Some(space) = self.simple_mut().take_recycled_space() {
1693 self.vec_pool.push(space);
1705 }
1706 }
1707
1708 pub(crate) unsafe fn set_borrowed_window(&mut self, buffer: &[u8]) {
1718 match self.active_backend() {
1720 super::strategy::BackendTag::Simple => unsafe {
1721 self.simple_mut().set_borrowed_window(buffer)
1722 },
1723 super::strategy::BackendTag::Dfast => unsafe {
1724 self.dfast_matcher_mut().set_borrowed_window(buffer)
1725 },
1726 super::strategy::BackendTag::Row => unsafe {
1727 self.row_matcher_mut().set_borrowed_window(buffer)
1728 },
1729 super::strategy::BackendTag::HashChain => unsafe {
1730 self.hc_matcher_mut().set_borrowed_window(buffer)
1731 },
1732 }
1733 }
1734
1735 pub(crate) fn clear_borrowed_window(&mut self) {
1738 match self.active_backend() {
1739 super::strategy::BackendTag::Simple => self.simple_mut().clear_borrowed_window(),
1740 super::strategy::BackendTag::Dfast => self.dfast_matcher_mut().clear_borrowed_window(),
1741 super::strategy::BackendTag::Row => self.row_matcher_mut().clear_borrowed_window(),
1742 super::strategy::BackendTag::HashChain => self.hc_matcher_mut().clear_borrowed_window(),
1743 #[allow(unreachable_patterns)]
1744 _ => {}
1745 }
1746 self.borrowed_pending = None;
1747 }
1748
1749 pub(crate) fn set_borrowed_block(&mut self, block_start: usize, block_end: usize) {
1757 assert!(
1758 self.borrowed_supported(),
1759 "borrowed block staging is not supported for the active backend/search config",
1760 );
1761 assert!(
1762 block_start <= block_end,
1763 "borrowed block range must satisfy start <= end (start={block_start} end={block_end})",
1764 );
1765 self.borrowed_pending = Some((block_start, block_end));
1766 match self.active_backend() {
1772 super::strategy::BackendTag::Simple => self
1773 .simple_mut()
1774 .stage_borrowed_block(block_start, block_end),
1775 super::strategy::BackendTag::Dfast => self
1776 .dfast_matcher_mut()
1777 .stage_borrowed_block(block_start, block_end),
1778 super::strategy::BackendTag::Row => self
1779 .row_matcher_mut()
1780 .stage_borrowed_block(block_start, block_end),
1781 super::strategy::BackendTag::HashChain => self
1782 .hc_matcher_mut()
1783 .table
1784 .stage_borrowed_block(block_start, block_end),
1785 }
1786 }
1787
1788 #[cfg(test)]
1789 fn dfast_matcher(&self) -> &DfastMatchGenerator {
1790 match &self.storage {
1791 MatcherStorage::Dfast(m) => m,
1792 _ => panic!("dfast backend must be initialized by reset() before use"),
1793 }
1794 }
1795
1796 fn dfast_matcher_mut(&mut self) -> &mut DfastMatchGenerator {
1797 match &mut self.storage {
1798 MatcherStorage::Dfast(m) => m,
1799 _ => panic!("dfast backend must be initialized by reset() before use"),
1800 }
1801 }
1802
1803 #[cfg(test)]
1804 fn row_matcher(&self) -> &RowMatchGenerator {
1805 match &self.storage {
1806 MatcherStorage::Row(m) => m,
1807 _ => panic!("row backend must be initialized by reset() before use"),
1808 }
1809 }
1810
1811 fn row_matcher_mut(&mut self) -> &mut RowMatchGenerator {
1812 match &mut self.storage {
1813 MatcherStorage::Row(m) => m,
1814 _ => panic!("row backend must be initialized by reset() before use"),
1815 }
1816 }
1817
1818 #[cfg(test)]
1819 fn hc_matcher(&self) -> &HcMatchGenerator {
1820 match &self.storage {
1821 MatcherStorage::HashChain(m) => m,
1822 _ => panic!("hash chain backend must be initialized by reset() before use"),
1823 }
1824 }
1825
1826 fn hc_matcher_mut(&mut self) -> &mut HcMatchGenerator {
1827 match &mut self.storage {
1828 MatcherStorage::HashChain(m) => m,
1829 _ => panic!("hash chain backend must be initialized by reset() before use"),
1830 }
1831 }
1832
1833 #[must_use]
1842 fn retire_dictionary_budget(&mut self, evicted_bytes: usize) -> bool {
1843 let reclaimed = evicted_bytes.min(self.dictionary_retained_budget);
1844 if reclaimed == 0 {
1845 return false;
1846 }
1847 self.dictionary_retained_budget -= reclaimed;
1848 match self.active_backend() {
1849 super::strategy::BackendTag::Simple => {
1850 let matcher = self.simple_mut();
1851 matcher.max_window_size = matcher.max_window_size.saturating_sub(reclaimed);
1856 }
1857 super::strategy::BackendTag::Dfast => {
1858 let matcher = self.dfast_matcher_mut();
1859 matcher.max_window_size = matcher.max_window_size.saturating_sub(reclaimed);
1864 }
1865 super::strategy::BackendTag::Row => {
1866 let matcher = self.row_matcher_mut();
1867 matcher.max_window_size = matcher.max_window_size.saturating_sub(reclaimed);
1872 }
1873 super::strategy::BackendTag::HashChain => {
1874 let matcher = self.hc_matcher_mut();
1875 matcher.table.max_window_size =
1878 matcher.table.max_window_size.saturating_sub(reclaimed);
1879 }
1880 }
1881 true
1882 }
1883
1884 fn trim_after_budget_retire(&mut self) {
1885 loop {
1886 let mut evicted_bytes = 0usize;
1887 match self.active_backend() {
1888 super::strategy::BackendTag::Simple => {
1889 let MatcherStorage::Simple(m) = &mut self.storage else {
1898 unreachable!("active_backend() == Simple proven above");
1899 };
1900 evicted_bytes += m.trim_to_window();
1901 }
1902 super::strategy::BackendTag::Dfast => {
1903 let dfast = self.dfast_matcher_mut();
1912 let pre = dfast.window_size;
1913 dfast.trim_to_window();
1914 evicted_bytes += pre - dfast.window_size;
1915 }
1916 super::strategy::BackendTag::Row => {
1917 let row = self.row_matcher_mut();
1922 let pre = row.window_size;
1923 row.trim_to_window();
1924 evicted_bytes += pre - row.window_size;
1925 }
1926 super::strategy::BackendTag::HashChain => {
1927 let table = &mut self.hc_matcher_mut().table;
1932 let pre = table.window_size;
1933 table.trim_to_window();
1934 evicted_bytes += pre - table.window_size;
1935 }
1936 }
1937 if evicted_bytes == 0 {
1938 break;
1939 }
1940 let _ = self.retire_dictionary_budget(evicted_bytes);
1954 }
1955 }
1956
1957 fn hc_dict_attach_mode(&self) -> bool {
1968 let MatcherStorage::HashChain(hc) = &self.storage else {
1971 return true;
1972 };
1973 let cutoff = if hc.table.uses_bt {
1974 match hc.strategy_tag {
1975 super::strategy::StrategyTag::BtUltra | super::strategy::StrategyTag::BtUltra2 => {
1976 BT_ULTRA_ATTACH_DICT_CUTOFF_LOG
1977 }
1978 _ => BT_OPT_ATTACH_DICT_CUTOFF_LOG,
1979 }
1980 } else {
1981 HC_ATTACH_DICT_CUTOFF_LOG
1982 };
1983 self.reset_size_log.is_none_or(|log| log <= cutoff)
1984 }
1985
1986 fn skip_matching_for_dictionary_priming(&mut self) {
1987 match self.active_backend() {
1988 super::strategy::BackendTag::Simple => {
1989 let attach = self.reset_dict_attach_ok
2001 && self
2002 .reset_size_log
2003 .is_none_or(|log| log <= FAST_ATTACH_DICT_CUTOFF_LOG);
2004 if attach {
2005 self.simple_mut().skip_matching_for_dict_prime();
2006 } else {
2007 self.simple_mut().skip_matching_with_hint(Some(false));
2008 }
2009 self.recycle_simple_space();
2010 }
2011 super::strategy::BackendTag::Dfast => {
2012 let attach = self
2023 .reset_size_log
2024 .is_none_or(|log| log <= DFAST_ATTACH_DICT_CUTOFF_LOG);
2025 if attach {
2026 self.dfast_matcher_mut().skip_matching_for_dict_attach();
2027 } else {
2028 self.dfast_matcher_mut().invalidate_dict_cache();
2029 self.dfast_matcher_mut().skip_matching_dense();
2030 }
2031 }
2032 super::strategy::BackendTag::Row => {
2033 let attach = self
2040 .reset_size_log
2041 .is_none_or(|log| log <= ROW_ATTACH_DICT_CUTOFF_LOG);
2042 if attach {
2043 self.row_matcher_mut().prime_dict_attach_current_block();
2044 } else {
2045 self.row_matcher_mut().invalidate_dict_cache();
2046 self.row_matcher_mut().skip_matching_with_hint(Some(false));
2047 }
2048 }
2049 super::strategy::BackendTag::HashChain => {
2050 if self.hc_dict_attach_mode() {
2061 self.hc_matcher_mut().table.skip_matching_dict_bt();
2062 } else {
2063 self.hc_matcher_mut().skip_matching(Some(false));
2064 }
2065 }
2066 }
2067 }
2068}
2069
2070impl Matcher for MatchGeneratorDriver {
2071 fn supports_dictionary_priming(&self) -> bool {
2072 true
2073 }
2074
2075 fn set_source_size_hint(&mut self, size: u64) {
2076 self.source_size_hint = Some(size);
2077 }
2078
2079 fn set_dictionary_size_hint(&mut self, size: usize) {
2080 self.dictionary_size_hint = Some(size);
2081 }
2082
2083 fn block_samples_match_dict(&self, block: &[u8]) -> bool {
2098 match &self.storage {
2099 MatcherStorage::Simple(m) => m.block_samples_match_dict(block),
2100 _ => true,
2101 }
2102 }
2103
2104 fn heap_size(&self) -> usize {
2109 let pool: usize = self.vec_pool.capacity() * core::mem::size_of::<Vec<u8>>()
2110 + self.vec_pool.iter().map(Vec::capacity).sum::<usize>();
2111 let snapshot = self
2112 .primed
2113 .as_ref()
2114 .map_or(0, |(storage, _, _)| storage.heap_size());
2115 pool + self.storage.heap_size() + snapshot
2116 }
2117
2118 fn clear_param_overrides(&mut self) {
2119 self.param_overrides = None;
2120 }
2121
2122 fn reset(&mut self, level: CompressionLevel) {
2123 let hint = self.source_size_hint.take();
2124 let dict_hint = self.dictionary_size_hint.take();
2125 self.reset_size_log = hint.map(source_size_ceil_log);
2131 self.reset_dict_attach_ok =
2135 dict_hint.is_none_or(|size| size <= MAX_FAST_ATTACH_DICT_REGION);
2136 let hinted = hint.is_some();
2137 #[cfg_attr(not(test), allow(unused_mut))]
2138 let mut params = Self::level_params(level, hint);
2139 #[cfg(test)]
2147 if let Some((search, parse)) = self.config_override.take() {
2148 params.search = search;
2149 params.lazy_depth = parse.lazy_depth();
2150 use super::strategy::SearchMethod;
2155 match search {
2156 SearchMethod::Fast => {
2157 params.fast.get_or_insert(FAST_L1);
2158 }
2159 SearchMethod::DoubleFast => {
2160 params.dfast.get_or_insert(DFAST_L3);
2161 }
2162 SearchMethod::RowHash => {
2163 params.row.get_or_insert(ROW_CONFIG);
2164 }
2165 SearchMethod::HashChain | SearchMethod::BinaryTree => {
2166 params.hc.get_or_insert(HC_CONFIG);
2167 }
2168 }
2169 }
2170 if let Some(ov) = self.param_overrides
2176 && !ov.is_empty()
2177 {
2178 apply_param_overrides(&mut params, &ov);
2179 if let Some(hint_size) = hint {
2189 params = adjust_params_for_source_size(params, hint_size);
2190 if let Some(window_log) = ov.window_log {
2191 params.window_log = window_log;
2192 }
2193 }
2194 }
2195 if let Some(dict_size) = dict_hint.filter(|&size| size > 0) {
2214 let mut base_params = Self::level_params(level, None);
2228 if let Some(ov) = self.param_overrides
2229 && !ov.is_empty()
2230 {
2231 apply_param_overrides(&mut base_params, &ov);
2232 }
2233 if let (Some(hc), Some(base_hc)) = (params.hc.as_mut(), base_params.hc) {
2234 let uses_bt = matches!(
2235 params.strategy_tag,
2236 super::strategy::StrategyTag::Btlazy2
2237 | super::strategy::StrategyTag::BtOpt
2238 | super::strategy::StrategyTag::BtUltra
2239 | super::strategy::StrategyTag::BtUltra2
2240 );
2241 let (dict_hash_log, dict_chain_log) = cdict_table_logs(
2242 params.window_log,
2243 base_hc.hash_log,
2244 base_hc.chain_log,
2245 uses_bt,
2246 dict_size,
2247 );
2248 hc.hash_log = dict_hash_log;
2249 hc.chain_log = dict_chain_log;
2250 }
2251 }
2252 if params.search == super::strategy::SearchMethod::RowHash && params.window_log <= 14 {
2266 let row = params
2267 .row
2268 .expect("a RowHash level row must carry a RowConfig");
2269 params.search = super::strategy::SearchMethod::HashChain;
2270 let row_cdict_hash_bits = match dict_hint.filter(|&size| size > 0) {
2287 Some(_) => {
2288 let mut base_params = Self::level_params(level, None);
2289 if let Some(ov) = self.param_overrides
2290 && !ov.is_empty()
2291 {
2292 apply_param_overrides(&mut base_params, &ov);
2293 }
2294 base_params
2295 .row
2296 .map_or(row.hash_bits, |base_row| base_row.hash_bits)
2297 }
2298 None => row.hash_bits,
2299 };
2300 let explicit_chain_log = self
2320 .param_overrides
2321 .filter(|ov| !ov.is_empty())
2322 .and_then(|ov| ov.chain_log)
2323 .map(|chain_log| chain_log as usize);
2324 let row_cdict_chain_bits = explicit_chain_log.unwrap_or(row_cdict_hash_bits - 1);
2325 let (mut hash_log, mut chain_log) = match dict_hint.filter(|&size| size > 0) {
2326 Some(dict_size) => cdict_table_logs(
2327 params.window_log,
2328 row_cdict_hash_bits,
2329 row_cdict_chain_bits,
2330 false,
2331 dict_size,
2332 ),
2333 None => (
2334 row.hash_bits,
2335 explicit_chain_log.unwrap_or(row.hash_bits - 1),
2336 ),
2337 };
2338 if dict_hint.filter(|&size| size > 0).is_none() {
2345 let wlog = params.window_log as usize;
2346 hash_log = hash_log.min(wlog + 1);
2347 chain_log = chain_log.min(wlog);
2348 }
2349 params.hc = Some(HcConfig {
2350 hash_log,
2351 chain_log,
2352 search_depth: row.search_depth,
2353 target_len: row.target_len,
2354 search_mls: 4,
2355 });
2356 params.row = None;
2357 }
2358 let next_backend = params.backend();
2359 let max_window_size = 1usize << params.window_log;
2360 self.dictionary_retained_budget = 0;
2361 self.borrowed_pending = None;
2364 if self.active_backend() != next_backend {
2365 match &mut self.storage {
2371 MatcherStorage::Simple(_m) => {
2372 }
2379 MatcherStorage::Dfast(m) => {
2380 m.tables = Vec::new();
2393 m.reset();
2394 }
2395 MatcherStorage::Row(m) => {
2396 m.row_heads = Vec::new();
2397 m.row_positions = Vec::new();
2398 m.row_tags = Vec::new();
2399 m.reset();
2400 }
2401 MatcherStorage::HashChain(m) => {
2402 m.table.hash_table = Vec::new();
2410 m.table.chain_table = Vec::new();
2411 m.table.hash3_table = Vec::new();
2412 let vec_pool = &mut self.vec_pool;
2413 m.reset(|mut data| {
2414 data.resize(data.capacity(), 0);
2415 vec_pool.push(data);
2416 });
2417 }
2418 }
2419 self.storage = match next_backend {
2422 super::strategy::BackendTag::Simple => {
2423 let fast = params.fast.expect("Fast level row carries a FastConfig");
2429 MatcherStorage::Simple(FastKernelMatcher::with_params(
2430 params.window_log,
2431 fast.hash_log,
2432 fast.mls,
2433 fast.step_size,
2434 ))
2435 }
2436 super::strategy::BackendTag::Dfast => {
2437 MatcherStorage::Dfast(DfastMatchGenerator::new(max_window_size))
2438 }
2439 super::strategy::BackendTag::Row => {
2440 MatcherStorage::Row(RowMatchGenerator::new(max_window_size))
2441 }
2442 super::strategy::BackendTag::HashChain => {
2443 MatcherStorage::HashChain(HcMatchGenerator::new(max_window_size))
2444 }
2445 };
2446 }
2447
2448 self.strategy_tag = params.strategy_tag;
2454 self.search = params.search;
2455 self.parse = params.parse();
2456 self.slice_size = self.base_slice_size.min(max_window_size);
2457 self.reported_window_size = max_window_size;
2458 let strategy_tag = self.strategy_tag;
2459 let table_window_size = match hint {
2465 Some(h) => {
2466 let raw_log = source_size_ceil_log(h);
2467 let shift = raw_log.max(MIN_WINDOW_LOG).min(usize::BITS as u8 - 1);
2474 (1usize << shift).min(max_window_size)
2475 }
2476 None => max_window_size,
2477 };
2478 let mut resolved_table_bits: usize = 0;
2483 match &mut self.storage {
2484 MatcherStorage::Simple(m) => {
2485 let fast = params.fast.expect("Fast level row carries a FastConfig");
2489 let dict_attach_epoch = matches!(dict_hint, Some(size) if size > 0)
2499 && self.reset_dict_attach_ok
2500 && self
2501 .reset_size_log
2502 .is_none_or(|log| log <= FAST_ATTACH_DICT_CUTOFF_LOG);
2503 let table_overwritten_by_restore = matches!(dict_hint, Some(size) if size > 0)
2514 && !dict_attach_epoch
2515 && self.primed.as_ref().is_some_and(|(_, _, captured)| {
2516 *captured
2517 == PrimedKey {
2518 level,
2519 params,
2520 table_bits: 0,
2521 fast_attach: false,
2522 ldm: None,
2523 }
2524 });
2525 let hash_log = if dict_hint.is_some_and(|s| s > 0) {
2536 fast.hash_log
2537 } else {
2538 fast.hash_log.min(params.window_log as u32 + 1)
2539 };
2540 m.reset(
2541 params.window_log,
2542 hash_log,
2543 fast.mls,
2544 fast.step_size,
2545 dict_attach_epoch,
2546 table_overwritten_by_restore,
2547 );
2548 }
2549 MatcherStorage::Dfast(dfast) => {
2550 dfast.max_window_size = max_window_size;
2551 let dcfg = params
2552 .dfast
2553 .expect("Dfast level row must carry a DfastConfig");
2554 let long_bits = if hinted {
2558 dfast_hash_bits_for_window(table_window_size).min(dcfg.long_hash_log as usize)
2559 } else {
2560 dcfg.long_hash_log as usize
2561 };
2562 let short_bits = if hinted {
2563 dfast_hash_bits_for_window(table_window_size).min(dcfg.short_hash_log as usize)
2564 } else {
2565 dcfg.short_hash_log as usize
2566 };
2567 resolved_table_bits = long_bits;
2568 dfast.set_hash_bits(long_bits, short_bits);
2569 dfast.reset();
2573 }
2574 MatcherStorage::Row(row) => {
2575 row.max_window_size = max_window_size;
2576 row.lazy_depth = params.lazy_depth;
2577 let mut row_cfg = params.row.expect("Row level row carries a RowConfig");
2578 if hinted {
2579 row_cfg.hash_bits = row_cfg
2592 .hash_bits
2593 .min(row_hash_bits_for_window(table_window_size));
2594 }
2595 row.configure(row_cfg);
2596 resolved_table_bits = row.hash_bits();
2602 row.reset();
2603 }
2604 MatcherStorage::HashChain(hc) => {
2605 hc.table.max_window_size = max_window_size;
2606 hc.hc.lazy_depth = params.lazy_depth;
2607 let mut hc_cfg = params.hc.expect("HashChain level row carries an HcConfig");
2608 if hinted && !matches!(dict_hint, Some(size) if size > 0) {
2628 let wlog = hc_hash_bits_for_window(table_window_size);
2629 let uses_bt = matches!(
2630 strategy_tag,
2631 super::strategy::StrategyTag::Btlazy2
2632 | super::strategy::StrategyTag::BtOpt
2633 | super::strategy::StrategyTag::BtUltra
2634 | super::strategy::StrategyTag::BtUltra2
2635 );
2636 hc_cfg.hash_log = hc_cfg.hash_log.min(wlog + 1);
2637 hc_cfg.chain_log = hc_cfg.chain_log.min(if uses_bt { wlog + 1 } else { wlog });
2638 }
2639 hc.configure(hc_cfg, strategy_tag, params.window_log);
2640 let vec_pool = &mut self.vec_pool;
2641 hc.reset(|mut data| {
2642 data.resize(data.capacity(), 0);
2643 vec_pool.push(data);
2644 });
2645 if let Some(src) = hint {
2652 let src_hint = usize::try_from(src).unwrap_or(usize::MAX);
2659 let expected = src_hint.saturating_add(dict_hint.unwrap_or(0));
2660 hc.table.reserve_history(expected);
2661 }
2662 }
2663 }
2664 #[cfg(feature = "hash")]
2672 {
2673 let derived_ldm = self
2676 .param_overrides
2677 .as_ref()
2678 .and_then(|ov| ov.ldm)
2679 .map(|ldm_ov| {
2680 let strategy_ord = ldm_strategy_ordinal(params.strategy_tag, params.lazy_depth);
2681 let seed = super::ldm::params::LdmParams {
2688 window_log: params.window_log as u32,
2689 hash_log: ldm_ov.hash_log.unwrap_or(0),
2690 hash_rate_log: ldm_ov.hash_rate_log.unwrap_or(0),
2691 min_match_length: ldm_ov.min_match.unwrap_or(0),
2692 bucket_size_log: ldm_ov.bucket_size_log.unwrap_or(0),
2693 };
2694 seed.derive(strategy_ord)
2695 });
2696 if let MatcherStorage::HashChain(hc) = &mut self.storage {
2697 let producer = derived_ldm.map(|p| match hc.take_ldm_producer() {
2707 Some(mut existing) if existing.params() == p => {
2708 existing.clear();
2709 existing
2710 }
2711 _ => super::ldm::LdmProducer::new(p),
2712 });
2713 hc.set_ldm_producer(producer);
2714 }
2715 }
2716 let fast_attach = matches!(next_backend, super::strategy::BackendTag::Simple)
2728 && self.reset_dict_attach_ok
2729 && self
2730 .reset_size_log
2731 .is_none_or(|log| log <= FAST_ATTACH_DICT_CUTOFF_LOG);
2732 let active_ldm = if matches!(params.search, super::strategy::SearchMethod::BinaryTree) {
2741 self.param_overrides.and_then(|ov| ov.ldm)
2742 } else {
2743 None
2744 };
2745 self.reset_shape = Some((params, resolved_table_bits, fast_attach, active_ldm));
2746 }
2747
2748 fn dictionary_is_resident(&self) -> bool {
2749 match &self.storage {
2750 MatcherStorage::HashChain(hc) => hc.table.dict_resident,
2751 MatcherStorage::Simple(s) => s.dict_resident(),
2752 MatcherStorage::Dfast(d) => d.dict_resident(),
2753 _ => false,
2754 }
2755 }
2756
2757 fn reapply_resident_dictionary(&mut self, offset_hist: [u32; 3]) {
2758 match self.active_backend() {
2761 super::strategy::BackendTag::Simple => {
2762 self.simple_mut().prime_offset_history(offset_hist)
2763 }
2764 super::strategy::BackendTag::Dfast => {
2765 self.dfast_matcher_mut().offset_hist = offset_hist
2766 }
2767 super::strategy::BackendTag::Row => self.row_matcher_mut().offset_hist = offset_hist,
2768 super::strategy::BackendTag::HashChain => {
2769 let matcher = self.hc_matcher_mut();
2770 matcher.table.offset_hist = offset_hist;
2771 matcher.table.mark_dictionary_primed();
2772 }
2773 }
2774 let base = self.reported_window_size;
2786 let inflated = match self.active_backend() {
2787 super::strategy::BackendTag::Simple => self.simple_mut().max_window_size,
2788 super::strategy::BackendTag::Dfast => self.dfast_matcher_mut().max_window_size,
2789 super::strategy::BackendTag::Row => self.row_matcher_mut().max_window_size,
2790 super::strategy::BackendTag::HashChain => self.hc_matcher_mut().table.max_window_size,
2791 };
2792 self.dictionary_retained_budget = inflated.saturating_sub(base);
2793 }
2794
2795 fn prime_with_dictionary(&mut self, dict_content: &[u8], offset_hist: [u32; 3]) {
2796 match self.active_backend() {
2797 super::strategy::BackendTag::Simple => {
2798 self.simple_mut().prime_offset_history(offset_hist);
2807 }
2808 super::strategy::BackendTag::Dfast => {
2809 self.dfast_matcher_mut().offset_hist = offset_hist
2810 }
2811 super::strategy::BackendTag::Row => self.row_matcher_mut().offset_hist = offset_hist,
2812 super::strategy::BackendTag::HashChain => {
2813 let matcher = self.hc_matcher_mut();
2814 matcher.table.clear_chain_hash_tables();
2820 matcher.table.offset_hist = offset_hist;
2821 matcher.table.mark_dictionary_primed();
2822 }
2823 }
2824
2825 if dict_content.is_empty() {
2826 return;
2827 }
2828
2829 use super::match_table::storage::MAX_PRIMED_WINDOW_SIZE;
2844
2845 let requested_dict_budget = dict_content.len();
2859 let base_max_window_size = match self.active_backend() {
2860 super::strategy::BackendTag::Simple => self.simple_mut().max_window_size,
2861 super::strategy::BackendTag::Dfast => self.dfast_matcher_mut().max_window_size,
2862 super::strategy::BackendTag::Row => self.row_matcher_mut().max_window_size,
2863 super::strategy::BackendTag::HashChain => self.hc_matcher_mut().table.max_window_size,
2864 };
2865 match self.active_backend() {
2866 super::strategy::BackendTag::Simple => {
2867 let matcher = self.simple_mut();
2868 matcher.max_window_size = matcher
2869 .max_window_size
2870 .saturating_add(requested_dict_budget)
2871 .min(MAX_PRIMED_WINDOW_SIZE);
2872 }
2873 super::strategy::BackendTag::Dfast => {
2874 let matcher = self.dfast_matcher_mut();
2875 matcher.max_window_size = matcher
2876 .max_window_size
2877 .saturating_add(requested_dict_budget)
2878 .min(MAX_PRIMED_WINDOW_SIZE);
2879 }
2880 super::strategy::BackendTag::Row => {
2881 let matcher = self.row_matcher_mut();
2882 matcher.max_window_size = matcher
2883 .max_window_size
2884 .saturating_add(requested_dict_budget)
2885 .min(MAX_PRIMED_WINDOW_SIZE);
2886 }
2887 super::strategy::BackendTag::HashChain => {
2888 let matcher = self.hc_matcher_mut();
2889 matcher.table.max_window_size = matcher
2890 .table
2891 .max_window_size
2892 .saturating_add(requested_dict_budget)
2893 .min(MAX_PRIMED_WINDOW_SIZE);
2894 }
2895 }
2896
2897 let mut start = 0usize;
2898 let mut committed_dict_budget = 0usize;
2899 let min_primed_tail = match self.active_backend() {
2903 super::strategy::BackendTag::Simple => MIN_MATCH_LEN,
2904 super::strategy::BackendTag::Dfast
2905 | super::strategy::BackendTag::Row
2906 | super::strategy::BackendTag::HashChain => 4,
2907 };
2908 while start < dict_content.len() {
2909 let end = (start + self.slice_size).min(dict_content.len());
2910 if end - start < min_primed_tail {
2911 break;
2912 }
2913 let mut space = self.vec_pool.pop().unwrap_or_default();
2923 space.clear();
2924 space.extend_from_slice(&dict_content[start..end]);
2925 self.commit_space(space);
2926 self.skip_matching_for_dictionary_priming();
2927 committed_dict_budget += end - start;
2928 start = end;
2929 }
2930
2931 let capped_retained_budget = MAX_PRIMED_WINDOW_SIZE.saturating_sub(base_max_window_size);
2941 let granted_retained_budget = committed_dict_budget.min(capped_retained_budget);
2942 let final_max_window_size = base_max_window_size.saturating_add(granted_retained_budget);
2943 match self.active_backend() {
2944 super::strategy::BackendTag::Simple => {
2945 self.simple_mut().max_window_size = final_max_window_size;
2946 }
2947 super::strategy::BackendTag::Dfast => {
2948 self.dfast_matcher_mut().max_window_size = final_max_window_size;
2949 }
2950 super::strategy::BackendTag::Row => {
2951 self.row_matcher_mut().max_window_size = final_max_window_size;
2952 }
2953 super::strategy::BackendTag::HashChain => {
2954 self.hc_matcher_mut().table.max_window_size = final_max_window_size;
2955 }
2956 }
2957 if granted_retained_budget > 0 {
2958 self.dictionary_retained_budget = self
2959 .dictionary_retained_budget
2960 .saturating_add(granted_retained_budget);
2961 }
2962 if self.active_backend() == super::strategy::BackendTag::HashChain {
2963 let attach = self.hc_dict_attach_mode();
2996 let table = &mut self.hc_matcher_mut().table;
2997 table.set_dictionary_limit_from_primed_bytes(committed_dict_budget);
2998 if !attach {
3006 table.dms.invalidate();
3007 } else if table.uses_bt {
3008 table.prime_dms_bt(committed_dict_budget);
3009 } else {
3010 table.prime_dms_hc(committed_dict_budget);
3011 }
3012 }
3013 match self.active_backend() {
3019 super::strategy::BackendTag::Simple => self.simple_mut().mark_dict_primed(),
3020 super::strategy::BackendTag::Dfast => self.dfast_matcher_mut().mark_dict_primed(),
3021 super::strategy::BackendTag::Row => self.row_matcher_mut().mark_dict_primed(),
3022 _ => {}
3023 }
3024 }
3025
3026 fn restore_primed_dictionary(&mut self, level: super::CompressionLevel) -> bool {
3027 let Some((params, table_bits, fast_attach, ldm)) = self.reset_shape else {
3038 return false;
3039 };
3040 let key = PrimedKey {
3041 level,
3042 params,
3043 table_bits,
3044 fast_attach,
3045 ldm,
3046 };
3047 let Some((snapshot, budget, captured_key)) = &self.primed else {
3048 return false;
3049 };
3050 if *captured_key != key {
3051 return false;
3052 }
3053 let budget = *budget;
3054 match (&mut self.storage, snapshot) {
3055 (MatcherStorage::Simple(live), MatcherStorage::Simple(snap)) => {
3061 live.clone_from(snap);
3062 }
3063 (MatcherStorage::HashChain(live), MatcherStorage::HashChain(snap))
3072 if !snap.table.uses_bt =>
3073 {
3074 live.table.clone_from(&snap.table);
3075 live.hc.clone_from(&snap.hc);
3076 live.strategy_tag = snap.strategy_tag;
3077 }
3080 (live, snapshot_storage) => {
3081 let mut storage = snapshot_storage.clone();
3082 if let MatcherStorage::HashChain(hc) = &mut storage {
3095 hc.table.ensure_tables();
3096 }
3097 #[cfg(feature = "hash")]
3104 {
3105 let fresh_ldm = if let MatcherStorage::HashChain(hc) = live {
3106 hc.take_ldm_producer()
3107 } else {
3108 None
3109 };
3110 if let MatcherStorage::HashChain(hc) = &mut storage {
3111 hc.set_ldm_producer(fresh_ldm);
3112 }
3113 }
3114 *live = storage;
3115 }
3116 }
3117 self.dictionary_retained_budget = budget;
3118 true
3119 }
3120
3121 fn capture_primed_dictionary(&mut self, level: super::CompressionLevel) {
3122 let Some((params, table_bits, fast_attach, ldm)) = self.reset_shape else {
3125 return;
3126 };
3127 let key = PrimedKey {
3128 level,
3129 params,
3130 table_bits,
3131 fast_attach,
3132 ldm,
3133 };
3134 let bt_decoupled = matches!(
3153 &self.storage,
3154 MatcherStorage::HashChain(hc) if hc.table.uses_bt && hc.table.dms.is_primed()
3155 );
3156 if bt_decoupled {
3157 let MatcherStorage::HashChain(hc) = &mut self.storage else {
3158 unreachable!("bt_decoupled implies HashChain storage");
3159 };
3160 let hash_table = core::mem::take(&mut hc.table.hash_table);
3161 let chain_table = core::mem::take(&mut hc.table.chain_table);
3162 let hash3_table = core::mem::take(&mut hc.table.hash3_table);
3163 #[cfg(feature = "hash")]
3168 let ldm_producer = hc.take_ldm_producer();
3169 let snapshot = self.storage.clone();
3172 let MatcherStorage::HashChain(hc) = &mut self.storage else {
3174 unreachable!("storage variant is stable across the take/put");
3175 };
3176 hc.table.hash_table = hash_table;
3177 hc.table.chain_table = chain_table;
3178 hc.table.hash3_table = hash3_table;
3179 #[cfg(feature = "hash")]
3180 hc.set_ldm_producer(ldm_producer);
3181 self.primed = Some((snapshot, self.dictionary_retained_budget, key));
3182 } else {
3183 self.primed = Some((self.storage.clone(), self.dictionary_retained_budget, key));
3184 }
3185 }
3186
3187 fn invalidate_primed_dictionary(&mut self) {
3188 self.primed = None;
3189 match self.active_backend() {
3194 super::strategy::BackendTag::Simple => self.simple_mut().invalidate_dict_cache(),
3195 super::strategy::BackendTag::Dfast => self.dfast_matcher_mut().invalidate_dict_cache(),
3196 super::strategy::BackendTag::Row => self.row_matcher_mut().invalidate_dict_cache(),
3201 super::strategy::BackendTag::HashChain => {
3206 let table = &mut self.hc_matcher_mut().table;
3207 table.dms.invalidate();
3208 table.dictionary_active = false;
3219 }
3220 }
3221 }
3222
3223 fn seed_dictionary_entropy(
3224 &mut self,
3225 huff: Option<&crate::huff0::huff0_encoder::HuffmanTable>,
3226 ll: Option<&crate::fse::fse_encoder::FSETable>,
3227 ml: Option<&crate::fse::fse_encoder::FSETable>,
3228 of: Option<&crate::fse::fse_encoder::FSETable>,
3229 ) {
3230 if self.active_backend() == super::strategy::BackendTag::HashChain {
3231 self.hc_matcher_mut()
3232 .seed_dictionary_entropy(huff, ll, ml, of);
3233 }
3234 }
3235
3236 fn window_size(&self) -> u64 {
3237 self.reported_window_size as u64
3238 }
3239
3240 fn get_next_space(&mut self) -> Vec<u8> {
3241 if let Some(mut space) = self.vec_pool.pop() {
3242 if space.len() > self.slice_size {
3243 space.truncate(self.slice_size);
3244 }
3245 if space.len() < self.slice_size {
3246 space.resize(self.slice_size, 0);
3247 }
3248 return space;
3249 }
3250 alloc::vec![0; self.slice_size]
3251 }
3252
3253 fn get_last_space(&mut self) -> &[u8] {
3254 match &self.storage {
3255 MatcherStorage::Simple(m) => m.last_committed_space(),
3256 MatcherStorage::Dfast(m) => m.get_last_space(),
3257 MatcherStorage::Row(m) => m.get_last_space(),
3258 MatcherStorage::HashChain(m) => m.table.get_last_space(),
3259 }
3260 }
3261
3262 fn commit_space(&mut self, space: Vec<u8>) {
3263 let mut evicted_bytes = 0usize;
3264 let vec_pool = &mut self.vec_pool;
3270 match &mut self.storage {
3271 MatcherStorage::Simple(m) => {
3272 let pre = m.history_len_for_eviction_accounting();
3282 m.accept_data(space);
3283 let post = m.history_len_for_eviction_accounting();
3284 evicted_bytes += pre.saturating_sub(post);
3295 }
3296 MatcherStorage::Dfast(m) => {
3297 let pre = m.window_size;
3319 let space_len = space.len();
3320 m.add_data(space, |data| {
3321 vec_pool.push(data);
3329 });
3330 evicted_bytes += (pre + space_len).saturating_sub(m.window_size);
3333 }
3334 MatcherStorage::Row(m) => {
3335 let pre = m.window_size;
3344 let space_len = space.len();
3345 m.add_data(space, |data| {
3346 vec_pool.push(data);
3351 });
3352 evicted_bytes += (pre + space_len).saturating_sub(m.window_size);
3355 }
3356 MatcherStorage::HashChain(m) => {
3357 let pre = m.table.window_size;
3364 let space_len = space.len();
3365 m.table.add_data(space, |data| {
3366 vec_pool.push(data);
3376 });
3377 evicted_bytes += (pre + space_len).saturating_sub(m.table.window_size);
3380 }
3381 }
3382 if self.retire_dictionary_budget(evicted_bytes) {
3392 self.trim_after_budget_retire();
3393 }
3394 }
3395
3396 fn start_matching(&mut self, mut handle_sequence: impl for<'a> FnMut(Sequence<'a>)) {
3397 use super::strategy::{self, StrategyTag};
3398 if let Some((block_start, block_end)) = self.borrowed_pending.take() {
3404 match self.active_backend() {
3405 super::strategy::BackendTag::Simple => {
3406 let m = self.simple_mut();
3407 if m.dict_is_attached() {
3408 m.start_matching_borrowed_dict(
3412 block_start,
3413 block_end,
3414 &mut handle_sequence,
3415 );
3416 } else {
3417 m.start_matching_borrowed(block_start, block_end, &mut handle_sequence);
3418 }
3419 }
3420 super::strategy::BackendTag::Dfast => self
3421 .dfast_matcher_mut()
3422 .start_matching_borrowed(block_start, block_end, &mut handle_sequence),
3423 super::strategy::BackendTag::Row => {
3424 let greedy = self.parse == super::strategy::ParseMode::Greedy;
3426 self.row_matcher_mut().start_matching_borrowed(
3427 block_start,
3428 block_end,
3429 greedy,
3430 &mut handle_sequence,
3431 );
3432 }
3433 super::strategy::BackendTag::HashChain => match self.search {
3434 super::strategy::SearchMethod::HashChain => self
3435 .hc_matcher_mut()
3436 .start_matching_lazy_borrowed(block_start, block_end, &mut handle_sequence),
3437 super::strategy::SearchMethod::BinaryTree => {
3438 match self.strategy_tag {
3454 StrategyTag::Btlazy2 => self
3455 .hc_matcher_mut()
3456 .start_matching_btlazy2(&mut handle_sequence),
3457 other => unreachable!(
3458 "borrowed BinaryTree scan is only supported for Btlazy2, got {other:?}"
3459 ),
3460 }
3461 }
3462 other => {
3463 unreachable!("HashChain backend with unexpected search {other:?}")
3464 }
3465 },
3466 }
3467 return;
3468 }
3469 use super::strategy::SearchMethod;
3478 match self.search {
3479 SearchMethod::Fast => {
3480 self.simple_mut().start_matching(&mut handle_sequence);
3481 self.recycle_simple_space();
3482 }
3483 SearchMethod::DoubleFast => {
3484 self.dfast_matcher_mut()
3485 .start_matching(&mut handle_sequence);
3486 }
3487 SearchMethod::RowHash => {
3488 let greedy = self.parse == super::strategy::ParseMode::Greedy;
3494 let row = self.row_matcher_mut();
3495 if greedy {
3496 row.start_matching_greedy(&mut handle_sequence);
3497 } else {
3498 row.start_matching(&mut handle_sequence);
3499 }
3500 }
3501 SearchMethod::HashChain => {
3502 self.hc_matcher_mut()
3505 .start_matching_lazy(&mut handle_sequence);
3506 }
3507 SearchMethod::BinaryTree => match self.strategy_tag {
3508 StrategyTag::Btlazy2 => self
3509 .hc_matcher_mut()
3510 .start_matching_btlazy2(&mut handle_sequence),
3511 StrategyTag::BtOpt => self.compress_block::<strategy::BtOpt>(&mut handle_sequence),
3512 StrategyTag::BtUltra => {
3513 self.compress_block::<strategy::BtUltra>(&mut handle_sequence)
3514 }
3515 StrategyTag::BtUltra2 => {
3516 self.compress_block::<strategy::BtUltra2>(&mut handle_sequence)
3517 }
3518 _ => unreachable!(
3519 "SearchMethod::BinaryTree requires a BT strategy tag (Btlazy2/BtOpt/BtUltra/BtUltra2)"
3520 ),
3521 },
3522 }
3523 }
3524
3525 fn skip_matching(&mut self) {
3526 self.skip_matching_with_hint(None);
3527 }
3528
3529 fn skip_matching_with_hint(&mut self, incompressible_hint: Option<bool>) {
3530 if let Some((block_start, block_end)) = self.borrowed_pending.take() {
3535 match self.active_backend() {
3536 super::strategy::BackendTag::Simple => self.simple_mut().skip_matching_borrowed(
3537 block_start,
3538 block_end,
3539 incompressible_hint,
3540 ),
3541 super::strategy::BackendTag::Dfast => self
3542 .dfast_matcher_mut()
3543 .skip_matching_borrowed(block_start, block_end, incompressible_hint),
3544 super::strategy::BackendTag::Row => self.row_matcher_mut().skip_matching_borrowed(
3545 block_start,
3546 block_end,
3547 incompressible_hint,
3548 ),
3549 super::strategy::BackendTag::HashChain => self
3550 .hc_matcher_mut()
3551 .skip_matching_borrowed(block_start, block_end, incompressible_hint),
3552 }
3553 return;
3554 }
3555 match self.active_backend() {
3556 super::strategy::BackendTag::Simple => {
3557 self.simple_mut()
3558 .skip_matching_with_hint(incompressible_hint);
3559 self.recycle_simple_space();
3560 }
3561 super::strategy::BackendTag::Dfast => {
3562 self.dfast_matcher_mut().skip_matching(incompressible_hint)
3563 }
3564 super::strategy::BackendTag::Row => self
3565 .row_matcher_mut()
3566 .skip_matching_with_hint(incompressible_hint),
3567 super::strategy::BackendTag::HashChain => {
3568 self.hc_matcher_mut().skip_matching(incompressible_hint)
3569 }
3570 }
3571 }
3572}
3573
3574impl MatchGeneratorDriver {
3575 fn compress_block<S: super::strategy::Strategy>(
3585 &mut self,
3586 handle_sequence: &mut impl for<'a> FnMut(Sequence<'a>),
3587 ) {
3588 debug_assert_eq!(S::BACKEND, super::strategy::BackendTag::HashChain);
3589 debug_assert!(
3590 S::USE_BT,
3591 "compress_block only handles the optimal (BT) path"
3592 );
3593 self.hc_matcher_mut()
3594 .start_matching_strategy::<S>(handle_sequence);
3595 }
3596}
3597
3598#[derive(Clone)]
3612pub(crate) enum HcBackend {
3613 Hc,
3615 Bt(alloc::boxed::Box<super::bt::BtMatcher>),
3619}
3620
3621impl HcBackend {
3622 fn heap_size(&self) -> usize {
3625 match self {
3626 Self::Hc => 0,
3627 Self::Bt(bt) => core::mem::size_of::<super::bt::BtMatcher>() + bt.heap_size(),
3628 }
3629 }
3630
3631 #[inline(always)]
3638 pub(crate) fn bt_mut(&mut self) -> &mut super::bt::BtMatcher {
3639 match self {
3640 Self::Bt(bt) => bt,
3641 Self::Hc => unreachable!("BT-only accessor called in HC mode"),
3642 }
3643 }
3644}
3645
3646#[derive(Clone)]
3647struct HcMatchGenerator {
3648 table: super::match_table::storage::MatchTable,
3653 hc: super::hc::HcMatcher,
3657 backend: HcBackend,
3662 strategy_tag: super::strategy::StrategyTag,
3674}
3675
3676macro_rules! bt_insert_step_no_rebase_body {
3692 ($table:expr, $search_depth:expr, $abs_pos:ident, $current_abs_end:ident, $target_abs:ident, $cmf:path) => {{
3693 let idx = $abs_pos - $table.history_abs_start;
3694 let concat: &[u8] = unsafe {
3699 let lh = $table.live_history();
3700 core::slice::from_raw_parts(lh.as_ptr(), lh.len())
3701 };
3702 if idx + 8 > concat.len() {
3703 return 1;
3704 }
3705 debug_assert!(
3706 $abs_pos <= $current_abs_end,
3707 "BT walker called past current block end"
3708 );
3709 let tail_limit = $current_abs_end - $abs_pos;
3710 let hash = $crate::encoding::match_table::storage::MatchTable::hash_position_at(
3711 concat,
3712 idx,
3713 $table.hash_log,
3714 $table.search_mls,
3715 );
3716 #[cfg(all(
3724 target_feature = "sse",
3725 any(target_arch = "x86", target_arch = "x86_64")
3726 ))]
3727 {
3728 #[cfg(target_arch = "x86")]
3729 use core::arch::x86::{_MM_HINT_T0, _mm_prefetch};
3730 #[cfg(target_arch = "x86_64")]
3731 use core::arch::x86_64::{_MM_HINT_T0, _mm_prefetch};
3732 unsafe {
3735 _mm_prefetch($table.hash_table.as_ptr().add(hash).cast(), _MM_HINT_T0);
3736 }
3737 if idx + 1 + 8 <= concat.len() {
3743 let hash_next =
3744 $crate::encoding::match_table::storage::MatchTable::hash_position_at(
3745 concat,
3746 idx + 1,
3747 $table.hash_log,
3748 $table.search_mls,
3749 );
3750 unsafe {
3753 _mm_prefetch(
3754 $table.hash_table.as_ptr().add(hash_next).cast(),
3755 _MM_HINT_T0,
3756 );
3757 }
3758 }
3759 }
3760 let Some(relative_pos) = $table.relative_position($abs_pos) else {
3761 return 1;
3762 };
3763 let stored = relative_pos + 1;
3764 let bt_mask = $table.bt_mask();
3765 let bt_low = $abs_pos.saturating_sub(bt_mask);
3771 let chain_ptr = $table.chain_table.as_mut_ptr();
3775 debug_assert_eq!($table.chain_table.len(), 2 << $table.bt_log());
3776 let window_low = $table.window_low_abs_for_target($target_abs);
3777 let mut match_end_abs = $abs_pos + 9;
3786 let mut best_len = 8usize;
3787 let mut compares_left = $search_depth;
3788 let mut common_length_smaller = 0usize;
3789 let mut common_length_larger = 0usize;
3790 let pair_idx = $table.bt_pair_index_for_abs($abs_pos);
3791 let mut smaller_slot = pair_idx;
3792 let mut larger_slot = pair_idx + 1;
3793 let mut match_stored = $table.hash_table[hash];
3794 $table.hash_table[hash] = stored;
3795
3796 while compares_left > 0 {
3797 if match_stored == $crate::encoding::match_table::storage::HC_EMPTY {
3798 break;
3799 }
3800 let Some(candidate_abs) = ($table.position_base + (match_stored as usize - 1))
3810 .checked_sub($table.index_shift)
3811 else {
3812 break;
3813 };
3814 if candidate_abs < window_low || candidate_abs >= $abs_pos {
3815 break;
3816 }
3817 compares_left -= 1;
3818
3819 let next_pair_idx = $table.bt_pair_index_for_abs(candidate_abs);
3820 let next_smaller = unsafe { *chain_ptr.add(next_pair_idx) };
3824 let next_larger = unsafe { *chain_ptr.add(next_pair_idx + 1) };
3825 let seed_len = common_length_smaller.min(common_length_larger);
3826 let candidate_idx = candidate_abs - $table.history_abs_start;
3827 let match_len = unsafe { $cmf(concat, idx, candidate_idx, tail_limit, seed_len) };
3832
3833 if match_len > best_len {
3834 best_len = match_len;
3835 let candidate_end = candidate_abs + match_len;
3839 if candidate_end > match_end_abs {
3840 match_end_abs = candidate_end;
3841 }
3842 }
3843
3844 if match_len >= tail_limit {
3845 break;
3846 }
3847
3848 let candidate_next = candidate_idx + match_len;
3849 let current_next = idx + match_len;
3850 if unsafe {
3854 *concat.get_unchecked(candidate_next) < *concat.get_unchecked(current_next)
3855 } {
3856 unsafe { *chain_ptr.add(smaller_slot) = match_stored };
3860 common_length_smaller = match_len;
3861 if candidate_abs <= bt_low {
3862 smaller_slot = usize::MAX;
3863 break;
3864 }
3865 smaller_slot = next_pair_idx + 1;
3866 match_stored = next_larger;
3867 } else {
3868 unsafe { *chain_ptr.add(larger_slot) = match_stored };
3870 common_length_larger = match_len;
3871 if candidate_abs <= bt_low {
3872 larger_slot = usize::MAX;
3873 break;
3874 }
3875 larger_slot = next_pair_idx;
3876 match_stored = next_smaller;
3877 }
3878 }
3879
3880 if smaller_slot != usize::MAX {
3883 unsafe {
3884 *chain_ptr.add(smaller_slot) = $crate::encoding::match_table::storage::HC_EMPTY
3885 };
3886 }
3887 if larger_slot != usize::MAX {
3888 unsafe {
3889 *chain_ptr.add(larger_slot) = $crate::encoding::match_table::storage::HC_EMPTY
3890 };
3891 }
3892
3893 let speed_positions = if best_len > 384 {
3894 (best_len - 384).min(192)
3895 } else {
3896 0
3897 };
3898 speed_positions.max(match_end_abs - ($abs_pos + 8))
3908 }};
3909}
3910pub(crate) use bt_insert_step_no_rebase_body;
3911
3912#[inline]
3932fn btlazy2_offbase(offset: usize, reps: [u32; 3], ll0: bool) -> u32 {
3933 let o = offset as u32;
3934 if ll0 {
3940 if o == reps[1] {
3941 1
3942 } else if o == reps[2] {
3943 2
3944 } else if reps[0] > 1 && o == reps[0] - 1 {
3945 3
3946 } else {
3947 o + 3
3949 }
3950 } else if o == reps[0] {
3951 1
3952 } else if o == reps[1] {
3953 2
3954 } else if o == reps[2] {
3955 3
3956 } else {
3957 o + 3
3959 }
3960}
3961
3962#[inline]
3966fn btlazy2_gain(match_len: usize, offset: usize, reps: [u32; 3], ll0: bool) -> i64 {
3967 let offbase = btlazy2_offbase(offset, reps, ll0);
3968 (match_len as i64) * 4 - (31 - offbase.leading_zeros()) as i64
3969}
3970
3971macro_rules! start_matching_btlazy2_body {
3979 ($self:ident, $handle_sequence:ident, $collect:ident, $cmf:path $(,)?) => {{
3980 $self.table.ensure_tables();
3981 let (current_abs_start, current_len) = $self.table.current_block_range();
3983 if current_len == 0 {
3984 return;
3985 }
3986 let current_ptr = $self.table.get_last_space().as_ptr();
3987 let current: &[u8] = unsafe { core::slice::from_raw_parts(current_ptr, current_len) };
3990 let history_abs_start = $self.table.history_abs_start;
3998 let concat_full: &[u8] = unsafe {
3999 let lh = $self.table.live_history();
4000 core::slice::from_raw_parts(lh.as_ptr(), lh.len())
4001 };
4002 let current_abs_end = current_abs_start + current_len;
4003 $self
4004 .table
4005 .apply_limited_update_after_long_match(current_abs_start);
4006 $self
4007 .table
4008 .backfill_boundary_positions(current_abs_start, current_abs_end);
4009
4010 let profile = HcOptimalCostProfile::const_for_strategy::<super::strategy::Btlazy2>();
4011 let mut candidates = core::mem::take(&mut $self.backend.bt_mut().opt_candidates_scratch);
4012
4013 let depth = $self.hc.lazy_depth as usize;
4014 let mut pos = 0usize;
4015 let mut literals_start = 0usize;
4016
4017 macro_rules! bt_select {
4023 ($p:expr) => {{
4024 let sel_pos: usize = $p;
4025 let ll0 = sel_pos == literals_start;
4028 let sel_abs = current_abs_start + sel_pos;
4029 candidates.clear();
4030 let query = HcCandidateQuery {
4031 reps: $self.table.offset_hist,
4032 lit_len: sel_pos - literals_start,
4033 ldm_candidate: None,
4036 };
4037 unsafe {
4040 $self.$collect::<super::strategy::Btlazy2, true>(
4041 sel_abs,
4042 current_abs_end,
4043 profile,
4044 query,
4045 &mut candidates,
4046 );
4047 }
4048 let reps = $self.table.offset_hist;
4049 let mut sel_ml = 0usize;
4050 let mut sel_off = 0usize;
4051 let mut sel_gain = i64::MIN;
4052 for c in candidates.iter() {
4053 let ml = c.match_len.min(current_len - sel_pos);
4054 if ml < HC_OPT_MIN_MATCH_LEN {
4055 continue;
4056 }
4057 let g = btlazy2_gain(ml, c.offset, reps, ll0);
4058 if g > sel_gain {
4059 sel_gain = g;
4060 sel_ml = ml;
4061 sel_off = c.offset;
4062 }
4063 }
4064 let sel_idx = sel_abs - history_abs_start;
4065 let probe_rep = if ll0 {
4069 reps[1] as usize
4070 } else {
4071 reps[0] as usize
4072 };
4073 if probe_rep != 0 && sel_idx >= probe_rep {
4074 let tail = current_len - sel_pos;
4075 let rep_ml =
4079 unsafe { $cmf(concat_full, sel_idx, sel_idx - probe_rep, tail, 0) };
4080 if rep_ml >= HC_OPT_MIN_MATCH_LEN
4081 && btlazy2_gain(rep_ml, probe_rep, reps, ll0) > sel_gain
4082 {
4083 sel_ml = rep_ml;
4084 sel_off = probe_rep;
4085 }
4086 }
4087 (sel_ml, sel_off)
4088 }};
4089 }
4090
4091 while pos + HC_OPT_MIN_MATCH_LEN <= current_len {
4092 let (mut best_ml, mut best_off) = bt_select!(pos);
4093 if best_ml < HC_OPT_MIN_MATCH_LEN {
4094 pos += 1;
4095 continue;
4096 }
4097 let mut start = pos;
4102 let mut d = 0usize;
4103 while d < depth && start + 1 + HC_OPT_MIN_MATCH_LEN <= current_len {
4104 let look = start + 1;
4105 let (ml2, off2) = bt_select!(look);
4106 if ml2 < HC_OPT_MIN_MATCH_LEN {
4107 break;
4108 }
4109 let reps = $self.table.offset_hist;
4110 let margin = if d == 0 { 4 } else { 7 };
4111 let gain1 = btlazy2_gain(best_ml, best_off, reps, start == literals_start) + margin;
4114 let gain2 = btlazy2_gain(ml2, off2, reps, false);
4115 if gain2 > gain1 {
4116 best_ml = ml2;
4117 best_off = off2;
4118 start = look;
4119 d += 1;
4120 } else {
4121 break;
4122 }
4123 }
4124 let lit_len = start - literals_start;
4128 let literals = ¤t[literals_start..start];
4129 $handle_sequence(Sequence::Triple {
4130 literals,
4131 offset: best_off,
4132 match_len: best_ml,
4133 });
4134 let _ = encode_offset_with_history(
4135 best_off as u32,
4136 lit_len as u32,
4137 &mut $self.table.offset_hist,
4138 );
4139 pos = start + best_ml;
4140 literals_start = pos;
4141 }
4142
4143 if literals_start < current_len {
4144 $handle_sequence(Sequence::Literals {
4145 literals: ¤t[literals_start..],
4146 });
4147 }
4148 $self.backend.bt_mut().opt_candidates_scratch = candidates;
4149 }};
4150}
4151
4152#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
4164#[target_feature(enable = "avx2")]
4165unsafe fn priceset_improved_mask8_avx2(next_cost: &[u32; 8], node_price: &[u32]) -> u8 {
4166 #[cfg(target_arch = "x86")]
4167 use core::arch::x86::{
4168 __m256i, _mm256_andnot_si256, _mm256_castsi256_ps, _mm256_cmpeq_epi32, _mm256_loadu_si256,
4169 _mm256_min_epu32, _mm256_movemask_ps,
4170 };
4171 #[cfg(target_arch = "x86_64")]
4172 use core::arch::x86_64::{
4173 __m256i, _mm256_andnot_si256, _mm256_castsi256_ps, _mm256_cmpeq_epi32, _mm256_loadu_si256,
4174 _mm256_min_epu32, _mm256_movemask_ps,
4175 };
4176 let nc = unsafe { _mm256_loadu_si256(next_cost.as_ptr() as *const __m256i) };
4177 let np = unsafe { _mm256_loadu_si256(node_price.as_ptr() as *const __m256i) };
4178 let min = _mm256_min_epu32(nc, np);
4179 let le = _mm256_cmpeq_epi32(min, nc); let eq = _mm256_cmpeq_epi32(nc, np); let lt = _mm256_andnot_si256(eq, le); _mm256_movemask_ps(_mm256_castsi256_ps(lt)) as u8
4183}
4184
4185#[inline(always)]
4189#[allow(clippy::too_many_arguments)]
4190fn priceset_next_cost(
4191 profile: HcOptimalCostProfile,
4192 stats: &HcOptState,
4193 ml_cache: &mut [[u32; 2]],
4194 ml_stamp: u32,
4195 match_len: usize,
4196 ll0_price: u32,
4197 off_price: u32,
4198 base_cost: u32,
4199) -> u32 {
4200 let ml_price =
4201 BtMatcher::cached_match_length_price(profile, stats, match_len, ml_cache, ml_stamp);
4202 let seq_cost = BtMatcher::add_prices(
4203 ll0_price,
4204 profile.match_price_from_parts(off_price, ml_price, stats),
4205 );
4206 BtMatcher::add_prices(base_cost, seq_cost)
4207}
4208
4209#[inline]
4217#[allow(clippy::too_many_arguments)]
4218#[cfg_attr(
4222 any(
4223 all(target_arch = "aarch64", target_endian = "little"),
4224 all(target_arch = "wasm32", target_feature = "simd128")
4225 ),
4226 allow(dead_code)
4227)]
4228fn priceset_range_nonabort_scalar(
4229 node_prices: &mut [u32],
4230 nodes: &mut [HcOptimalNode],
4231 ml_cache: &mut [[u32; 2]],
4232 ml_stamp: u32,
4233 profile: HcOptimalCostProfile,
4234 stats: &HcOptState,
4235 pos: usize,
4236 start: usize,
4237 max: usize,
4238 ll0_price: u32,
4239 off_price: u32,
4240 base_cost: u32,
4241 off: u32,
4242 reps: [u32; 3],
4243 last_pos: usize,
4244) -> usize {
4245 let mut new_last = last_pos;
4246 for ml in start..=max {
4247 let next_cost = priceset_next_cost(
4248 profile, stats, ml_cache, ml_stamp, ml, ll0_price, off_price, base_cost,
4249 );
4250 let next = pos + ml;
4251 if next_cost < node_prices[next] {
4252 node_prices[next] = next_cost;
4253 nodes[next] = HcOptimalNode {
4254 off,
4255 mlen: ml as u32,
4256 litlen: 0,
4257 reps,
4258 };
4259 if next > new_last {
4260 new_last = next;
4261 }
4262 }
4263 }
4264 new_last
4265}
4266
4267#[cfg(test)]
4273#[test]
4274fn priceset_tier_helpers_match_scalar() {
4275 fn scalar_deint<const W: usize>(cells: &[[u32; 2]], stamp: u32) -> Option<[u32; W]> {
4277 let mut out = [0u32; W];
4278 for k in 0..W {
4279 if cells[k][1] != stamp {
4280 return None;
4281 }
4282 out[k] = cells[k][0];
4283 }
4284 Some(out)
4285 }
4286 fn scalar_mask<const W: usize>(nc: &[u32; W], np: &[u32]) -> u8 {
4287 let mut m = 0u8;
4288 for k in 0..W {
4289 if nc[k] < np[k] {
4290 m |= 1 << k;
4291 }
4292 }
4293 m
4294 }
4295 const S: u32 = 0x55;
4296 let warm: [[u32; 2]; 4] = [[11, S], [22, S], [33, S], [44, S]];
4297 let mut cold = warm;
4298 cold[2][1] = S ^ 1; let nc4: [u32; 4] = [10, 99, 30, 41];
4300 let np4: [u32; 4] = [20, 21, 30, 99]; #[cfg(all(target_arch = "aarch64", target_endian = "little"))]
4303 unsafe {
4304 assert_eq!(
4305 priceset_cached_prices4_neon(&warm, S),
4306 scalar_deint::<4>(&warm, S)
4307 );
4308 assert_eq!(priceset_cached_prices4_neon(&cold, S), None);
4309 assert_eq!(
4310 priceset_improved_mask4_neon(&nc4, &np4),
4311 scalar_mask::<4>(&nc4, &np4)
4312 );
4313 }
4314 #[cfg(all(feature = "std", any(target_arch = "x86", target_arch = "x86_64")))]
4315 {
4316 if std::is_x86_feature_detected!("sse4.2") {
4317 unsafe {
4318 assert_eq!(
4319 priceset_cached_prices4_sse41(&warm, S),
4320 scalar_deint::<4>(&warm, S)
4321 );
4322 assert_eq!(priceset_cached_prices4_sse41(&cold, S), None);
4323 assert_eq!(
4324 priceset_improved_mask4_sse41(&nc4, &np4),
4325 scalar_mask::<4>(&nc4, &np4)
4326 );
4327 }
4328 }
4329 if std::is_x86_feature_detected!("avx2") {
4330 let warm8: [[u32; 2]; 8] = [
4331 [11, S],
4332 [22, S],
4333 [33, S],
4334 [44, S],
4335 [55, S],
4336 [66, S],
4337 [77, S],
4338 [88, S],
4339 ];
4340 let mut cold8 = warm8;
4341 cold8[5][1] = S ^ 1;
4342 let nc8: [u32; 8] = [10, 99, 30, 41, 99, 60, 99, 80];
4343 let np8: [u32; 8] = [20, 21, 30, 99, 50, 99, 70, 99];
4344 unsafe {
4345 assert_eq!(
4346 priceset_cached_prices8_avx2(&warm8, S),
4347 scalar_deint::<8>(&warm8, S)
4348 );
4349 assert_eq!(priceset_cached_prices8_avx2(&cold8, S), None);
4350 assert_eq!(
4351 priceset_improved_mask8_avx2(&nc8, &np8),
4352 scalar_mask::<8>(&nc8, &np8)
4353 );
4354 }
4355 }
4356 }
4357}
4358
4359#[inline(always)]
4371#[allow(clippy::too_many_arguments)]
4372#[cfg_attr(
4376 not(any(
4377 target_arch = "x86",
4378 target_arch = "x86_64",
4379 all(target_arch = "aarch64", target_endian = "little"),
4380 all(target_arch = "wasm32", target_feature = "simd128")
4381 )),
4382 allow(dead_code)
4383)]
4384fn priceset_range_vec<const W: usize>(
4385 node_prices: &mut [u32],
4386 nodes: &mut [HcOptimalNode],
4387 ml_cache: &mut [[u32; 2]],
4388 ml_stamp: u32,
4389 profile: HcOptimalCostProfile,
4390 stats: &HcOptState,
4391 pos: usize,
4392 start: usize,
4393 max: usize,
4394 ll0_price: u32,
4395 off_price: u32,
4396 base_cost: u32,
4397 off: u32,
4398 reps: [u32; 3],
4399 last_pos: usize,
4400 deint: impl Fn(&[[u32; 2]], u32) -> Option<[u32; W]>,
4401 mask: impl Fn(&[u32; W], &[u32]) -> u8,
4402) -> usize {
4403 let mut new_last = last_pos;
4404 let mut buf = [0u32; W];
4405 let c_base = base_cost
4422 .wrapping_add(ll0_price)
4423 .wrapping_add(profile.match_price_from_parts(off_price, 0, stats));
4424 let mut ml = start;
4425 while ml + W <= max + 1 {
4426 let vectorised = if ml + W <= ml_cache.len() {
4427 deint(&ml_cache[ml..ml + W], ml_stamp)
4428 } else {
4429 None
4430 };
4431 if let Some(prices) = vectorised {
4432 for (k, slot) in buf.iter_mut().enumerate() {
4433 *slot = c_base.wrapping_add(prices[k]);
4434 }
4435 } else {
4436 for (k, slot) in buf.iter_mut().enumerate() {
4437 *slot = priceset_next_cost(
4438 profile,
4439 stats,
4440 ml_cache,
4441 ml_stamp,
4442 ml + k,
4443 ll0_price,
4444 off_price,
4445 base_cost,
4446 );
4447 }
4448 }
4449 let base_next = pos + ml;
4450 let mut bits = mask(&buf, &node_prices[base_next..base_next + W]);
4451 while bits != 0 {
4452 let k = bits.trailing_zeros() as usize;
4453 bits &= bits - 1;
4454 let next = base_next + k;
4455 node_prices[next] = buf[k];
4456 nodes[next] = HcOptimalNode {
4457 off,
4458 mlen: (ml + k) as u32,
4459 litlen: 0,
4460 reps,
4461 };
4462 if next > new_last {
4463 new_last = next;
4464 }
4465 }
4466 ml += W;
4467 }
4468 while ml <= max {
4469 let next_cost = priceset_next_cost(
4470 profile, stats, ml_cache, ml_stamp, ml, ll0_price, off_price, base_cost,
4471 );
4472 let next = pos + ml;
4473 if next_cost < node_prices[next] {
4474 node_prices[next] = next_cost;
4475 nodes[next] = HcOptimalNode {
4476 off,
4477 mlen: ml as u32,
4478 litlen: 0,
4479 reps,
4480 };
4481 if next > new_last {
4482 new_last = next;
4483 }
4484 }
4485 ml += 1;
4486 }
4487 new_last
4488}
4489
4490#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
4501#[target_feature(enable = "avx2")]
4502#[inline]
4503unsafe fn priceset_cached_prices8_avx2(cells: &[[u32; 2]], stamp: u32) -> Option<[u32; 8]> {
4504 #[cfg(target_arch = "x86")]
4505 use core::arch::x86::{
4506 __m256i, _mm256_castsi256_ps, _mm256_cmpeq_epi32, _mm256_loadu_si256, _mm256_movemask_ps,
4507 _mm256_permute4x64_epi64, _mm256_set1_epi32, _mm256_shuffle_epi32, _mm256_storeu_si256,
4508 _mm256_unpackhi_epi64, _mm256_unpacklo_epi64,
4509 };
4510 #[cfg(target_arch = "x86_64")]
4511 use core::arch::x86_64::{
4512 __m256i, _mm256_castsi256_ps, _mm256_cmpeq_epi32, _mm256_loadu_si256, _mm256_movemask_ps,
4513 _mm256_permute4x64_epi64, _mm256_set1_epi32, _mm256_shuffle_epi32, _mm256_storeu_si256,
4514 _mm256_unpackhi_epi64, _mm256_unpacklo_epi64,
4515 };
4516 debug_assert!(cells.len() >= 8);
4517 let base = cells.as_ptr() as *const __m256i;
4518 let v0 = unsafe { _mm256_loadu_si256(base) };
4520 let v1 = unsafe { _mm256_loadu_si256(base.add(1)) };
4521 let s0 = _mm256_shuffle_epi32(v0, 0xD8); let s1 = _mm256_shuffle_epi32(v1, 0xD8); let gens = _mm256_unpackhi_epi64(s0, s1);
4526 let eq = _mm256_cmpeq_epi32(gens, _mm256_set1_epi32(stamp as i32));
4527 if _mm256_movemask_ps(_mm256_castsi256_ps(eq)) as u8 != 0xFF {
4528 return None;
4529 }
4530 let p_scrambled = _mm256_unpacklo_epi64(s0, s1);
4534 let prices = _mm256_permute4x64_epi64(p_scrambled, 0xD8);
4535 let mut out = [0u32; 8];
4536 unsafe { _mm256_storeu_si256(out.as_mut_ptr() as *mut __m256i, prices) };
4537 Some(out)
4538}
4539
4540#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
4541#[target_feature(enable = "avx2")]
4542#[inline]
4543#[allow(clippy::too_many_arguments)]
4544unsafe fn priceset_range_nonabort_avx2(
4545 node_prices: &mut [u32],
4546 nodes: &mut [HcOptimalNode],
4547 ml_cache: &mut [[u32; 2]],
4548 ml_stamp: u32,
4549 profile: HcOptimalCostProfile,
4550 stats: &HcOptState,
4551 pos: usize,
4552 start: usize,
4553 max: usize,
4554 ll0_price: u32,
4555 off_price: u32,
4556 base_cost: u32,
4557 off: u32,
4558 reps: [u32; 3],
4559 last_pos: usize,
4560) -> usize {
4561 priceset_range_vec::<8>(
4562 node_prices,
4563 nodes,
4564 ml_cache,
4565 ml_stamp,
4566 profile,
4567 stats,
4568 pos,
4569 start,
4570 max,
4571 ll0_price,
4572 off_price,
4573 base_cost,
4574 off,
4575 reps,
4576 last_pos,
4577 |cells, stamp| unsafe { priceset_cached_prices8_avx2(cells, stamp) },
4579 |nc, np| unsafe { priceset_improved_mask8_avx2(nc, np) },
4580 )
4581}
4582
4583#[cfg(all(target_arch = "aarch64", target_endian = "little"))]
4588#[target_feature(enable = "neon")]
4589#[inline]
4590unsafe fn priceset_cached_prices4_neon(cells: &[[u32; 2]], stamp: u32) -> Option<[u32; 4]> {
4591 use core::arch::aarch64::{vceqq_u32, vdupq_n_u32, vld2q_u32, vminvq_u32, vst1q_u32};
4592 debug_assert!(cells.len() >= 4);
4593 let pair = unsafe { vld2q_u32(cells.as_ptr() as *const u32) };
4595 let eq = vceqq_u32(pair.1, vdupq_n_u32(stamp));
4596 if vminvq_u32(eq) != u32::MAX {
4597 return None;
4598 }
4599 let mut out = [0u32; 4];
4600 unsafe { vst1q_u32(out.as_mut_ptr(), pair.0) };
4601 Some(out)
4602}
4603
4604#[cfg(all(target_arch = "aarch64", target_endian = "little"))]
4608#[target_feature(enable = "neon")]
4609#[inline]
4610unsafe fn priceset_improved_mask4_neon(next_cost: &[u32; 4], node_price: &[u32]) -> u8 {
4611 use core::arch::aarch64::{vaddvq_u32, vandq_u32, vcltq_u32, vld1q_u32, vst1q_u32};
4612 let nc = unsafe { vld1q_u32(next_cost.as_ptr()) };
4614 let np = unsafe { vld1q_u32(node_price.as_ptr()) };
4615 let lt = vcltq_u32(nc, np);
4616 let weights: [u32; 4] = [1, 2, 4, 8];
4617 let w = unsafe { vld1q_u32(weights.as_ptr()) };
4618 let bits = vandq_u32(lt, w);
4619 let _ = vst1q_u32; vaddvq_u32(bits) as u8
4621}
4622
4623#[cfg(all(target_arch = "aarch64", target_endian = "little"))]
4624#[target_feature(enable = "neon")]
4625#[inline]
4626#[allow(clippy::too_many_arguments)]
4627unsafe fn priceset_range_nonabort_neon(
4628 node_prices: &mut [u32],
4629 nodes: &mut [HcOptimalNode],
4630 ml_cache: &mut [[u32; 2]],
4631 ml_stamp: u32,
4632 profile: HcOptimalCostProfile,
4633 stats: &HcOptState,
4634 pos: usize,
4635 start: usize,
4636 max: usize,
4637 ll0_price: u32,
4638 off_price: u32,
4639 base_cost: u32,
4640 off: u32,
4641 reps: [u32; 3],
4642 last_pos: usize,
4643) -> usize {
4644 priceset_range_vec::<4>(
4645 node_prices,
4646 nodes,
4647 ml_cache,
4648 ml_stamp,
4649 profile,
4650 stats,
4651 pos,
4652 start,
4653 max,
4654 ll0_price,
4655 off_price,
4656 base_cost,
4657 off,
4658 reps,
4659 last_pos,
4660 |cells, stamp| unsafe { priceset_cached_prices4_neon(cells, stamp) },
4662 |nc, np| unsafe { priceset_improved_mask4_neon(nc, np) },
4663 )
4664}
4665
4666#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
4671#[target_feature(enable = "sse4.2")]
4672#[inline]
4673unsafe fn priceset_cached_prices4_sse41(cells: &[[u32; 2]], stamp: u32) -> Option<[u32; 4]> {
4674 #[cfg(target_arch = "x86")]
4675 use core::arch::x86::{
4676 __m128i, _mm_castsi128_ps, _mm_cmpeq_epi32, _mm_loadu_si128, _mm_movemask_ps,
4677 _mm_set1_epi32, _mm_shuffle_epi32, _mm_storeu_si128, _mm_unpackhi_epi64,
4678 _mm_unpacklo_epi64,
4679 };
4680 #[cfg(target_arch = "x86_64")]
4681 use core::arch::x86_64::{
4682 __m128i, _mm_castsi128_ps, _mm_cmpeq_epi32, _mm_loadu_si128, _mm_movemask_ps,
4683 _mm_set1_epi32, _mm_shuffle_epi32, _mm_storeu_si128, _mm_unpackhi_epi64,
4684 _mm_unpacklo_epi64,
4685 };
4686 debug_assert!(cells.len() >= 4);
4687 let base = cells.as_ptr() as *const __m128i;
4688 let v0 = unsafe { _mm_loadu_si128(base) }; let v1 = unsafe { _mm_loadu_si128(base.add(1)) }; let s0 = _mm_shuffle_epi32(v0, 0xD8); let s1 = _mm_shuffle_epi32(v1, 0xD8); let gens = _mm_unpackhi_epi64(s0, s1); let eq = _mm_cmpeq_epi32(gens, _mm_set1_epi32(stamp as i32));
4694 if _mm_movemask_ps(_mm_castsi128_ps(eq)) as u8 & 0x0F != 0x0F {
4695 return None;
4696 }
4697 let prices = _mm_unpacklo_epi64(s0, s1); let mut out = [0u32; 4];
4699 unsafe { _mm_storeu_si128(out.as_mut_ptr() as *mut __m128i, prices) };
4700 Some(out)
4701}
4702
4703#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
4706#[target_feature(enable = "sse4.2")]
4707#[inline]
4708unsafe fn priceset_improved_mask4_sse41(next_cost: &[u32; 4], node_price: &[u32]) -> u8 {
4709 #[cfg(target_arch = "x86")]
4710 use core::arch::x86::{
4711 __m128i, _mm_andnot_si128, _mm_castsi128_ps, _mm_cmpeq_epi32, _mm_loadu_si128,
4712 _mm_min_epu32, _mm_movemask_ps,
4713 };
4714 #[cfg(target_arch = "x86_64")]
4715 use core::arch::x86_64::{
4716 __m128i, _mm_andnot_si128, _mm_castsi128_ps, _mm_cmpeq_epi32, _mm_loadu_si128,
4717 _mm_min_epu32, _mm_movemask_ps,
4718 };
4719 let nc = unsafe { _mm_loadu_si128(next_cost.as_ptr() as *const __m128i) };
4720 let np = unsafe { _mm_loadu_si128(node_price.as_ptr() as *const __m128i) };
4721 let min = _mm_min_epu32(nc, np);
4722 let le = _mm_cmpeq_epi32(min, nc);
4723 let eq = _mm_cmpeq_epi32(nc, np);
4724 let lt = _mm_andnot_si128(eq, le);
4725 (_mm_movemask_ps(_mm_castsi128_ps(lt)) as u8) & 0x0F
4726}
4727
4728#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
4729#[target_feature(enable = "sse4.2")]
4730#[inline]
4731#[allow(clippy::too_many_arguments)]
4732unsafe fn priceset_range_nonabort_sse41(
4733 node_prices: &mut [u32],
4734 nodes: &mut [HcOptimalNode],
4735 ml_cache: &mut [[u32; 2]],
4736 ml_stamp: u32,
4737 profile: HcOptimalCostProfile,
4738 stats: &HcOptState,
4739 pos: usize,
4740 start: usize,
4741 max: usize,
4742 ll0_price: u32,
4743 off_price: u32,
4744 base_cost: u32,
4745 off: u32,
4746 reps: [u32; 3],
4747 last_pos: usize,
4748) -> usize {
4749 priceset_range_vec::<4>(
4750 node_prices,
4751 nodes,
4752 ml_cache,
4753 ml_stamp,
4754 profile,
4755 stats,
4756 pos,
4757 start,
4758 max,
4759 ll0_price,
4760 off_price,
4761 base_cost,
4762 off,
4763 reps,
4764 last_pos,
4765 |cells, stamp| unsafe { priceset_cached_prices4_sse41(cells, stamp) },
4767 |nc, np| unsafe { priceset_improved_mask4_sse41(nc, np) },
4768 )
4769}
4770
4771#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
4776#[target_feature(enable = "simd128")]
4777#[inline]
4778unsafe fn priceset_cached_prices4_simd128(cells: &[[u32; 2]], stamp: u32) -> Option<[u32; 4]> {
4779 use core::arch::wasm32::{
4780 u32x4_all_true, u32x4_eq, u32x4_shuffle, u32x4_splat, v128, v128_load, v128_store,
4781 };
4782 debug_assert!(cells.len() >= 4);
4783 let base = cells.as_ptr() as *const v128;
4784 let v0 = unsafe { v128_load(base) }; let v1 = unsafe { v128_load(base.add(1)) }; let gens = u32x4_shuffle::<1, 3, 5, 7>(v0, v1); let eq = u32x4_eq(gens, u32x4_splat(stamp));
4789 if !u32x4_all_true(eq) {
4790 return None;
4791 }
4792 let prices = u32x4_shuffle::<0, 2, 4, 6>(v0, v1); let mut out = [0u32; 4];
4794 unsafe { v128_store(out.as_mut_ptr() as *mut v128, prices) };
4795 Some(out)
4796}
4797
4798#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
4801#[target_feature(enable = "simd128")]
4802#[inline]
4803unsafe fn priceset_improved_mask4_simd128(next_cost: &[u32; 4], node_price: &[u32]) -> u8 {
4804 use core::arch::wasm32::{u32x4_bitmask, u32x4_lt, v128, v128_load};
4805 let nc = unsafe { v128_load(next_cost.as_ptr() as *const v128) };
4806 let np = unsafe { v128_load(node_price.as_ptr() as *const v128) };
4807 u32x4_bitmask(u32x4_lt(nc, np))
4808}
4809
4810#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
4811#[target_feature(enable = "simd128")]
4812#[inline]
4813#[allow(clippy::too_many_arguments)]
4814unsafe fn priceset_range_nonabort_simd128(
4815 node_prices: &mut [u32],
4816 nodes: &mut [HcOptimalNode],
4817 ml_cache: &mut [[u32; 2]],
4818 ml_stamp: u32,
4819 profile: HcOptimalCostProfile,
4820 stats: &HcOptState,
4821 pos: usize,
4822 start: usize,
4823 max: usize,
4824 ll0_price: u32,
4825 off_price: u32,
4826 base_cost: u32,
4827 off: u32,
4828 reps: [u32; 3],
4829 last_pos: usize,
4830) -> usize {
4831 priceset_range_vec::<4>(
4832 node_prices,
4833 nodes,
4834 ml_cache,
4835 ml_stamp,
4836 profile,
4837 stats,
4838 pos,
4839 start,
4840 max,
4841 ll0_price,
4842 off_price,
4843 base_cost,
4844 off,
4845 reps,
4846 last_pos,
4847 |cells, stamp| unsafe { priceset_cached_prices4_simd128(cells, stamp) },
4849 |nc, np| unsafe { priceset_improved_mask4_simd128(nc, np) },
4850 )
4851}
4852
4853macro_rules! build_optimal_plan_impl_body {
4854 (
4855 $self:expr,
4856 $strategy_ty:ty,
4857 $current:ident,
4858 $current_abs_start:ident,
4859 $current_len:ident,
4860 $initial_state:ident,
4861 $stats:ident,
4862 $out:ident,
4863 $collect:ident,
4864 $priceset:path $(,)?
4865 ) => {{
4866 let current_abs_end = $current_abs_start + $current_len;
4867 let min_match_len = HC_OPT_MIN_MATCH_LEN;
4868 let frontier_limit = $current_len.min(HC_OPT_NUM - 1);
4870 let initial_reps = $initial_state.reps;
4871 let initial_litlen = $initial_state.litlen;
4872 let ldm_block_offset = $initial_state.block_offset;
4873 let mut profile = $initial_state.profile;
4874 profile.sufficient_match_len = $self.hc.sufficient_match_len_for_pass(profile);
4875 debug_assert!(
4887 <$strategy_ty as super::strategy::Strategy>::USE_BT,
4888 "build_optimal_plan_impl_body called on non-BT strategy"
4889 );
4890 let abort_on_worse_match: bool =
4891 <$strategy_ty as super::strategy::Strategy>::OPT_LEVEL == 0;
4892 let opt_level: bool = <$strategy_ty as super::strategy::Strategy>::OPT_LEVEL >= 2;
4893 let mut nodes = core::mem::take(&mut $self.backend.bt_mut().opt_nodes_scratch);
4894 let mut node_prices = core::mem::take(&mut $self.backend.bt_mut().opt_node_prices_scratch);
4895 let frontier_buffer_size = frontier_limit + 2;
4897 if nodes.len() < HC_OPT_NODE_LEN {
4898 nodes = alloc::vec![HcOptimalNode::default(); HC_OPT_NODE_LEN].into_boxed_slice();
4902 }
4903 if node_prices.len() < HC_OPT_NODE_LEN {
4908 node_prices = alloc::vec![u32::MAX; HC_OPT_NODE_LEN].into_boxed_slice();
4909 }
4910 let mut candidates = core::mem::take(&mut $self.backend.bt_mut().opt_candidates_scratch);
4911 candidates.clear();
4912 if candidates.capacity() < MAX_HC_SEARCH_DEPTH {
4913 candidates.reserve_exact(MAX_HC_SEARCH_DEPTH - candidates.capacity());
4914 }
4915 let mut store = core::mem::take(&mut $self.backend.bt_mut().opt_store_scratch);
4916 store.clear();
4917 let mut price_arena = core::mem::take(&mut $self.backend.bt_mut().opt_price_arena);
4918 if price_arena.len() < HC_OPT_PRICE_ARENA_LEN {
4919 price_arena = alloc::vec![[0u32; 2]; HC_OPT_PRICE_ARENA_LEN].into_boxed_slice();
4920 }
4921 let arena_base = price_arena.as_mut_ptr();
4937 let mut ll_cache: &mut [[u32; 2]] =
4938 unsafe { core::slice::from_raw_parts_mut(arena_base, HC_OPT_PRICE_STRIDE) };
4939 let mut ml_cache: &mut [[u32; 2]] = unsafe {
4940 core::slice::from_raw_parts_mut(arena_base.add(HC_OPT_PRICE_STRIDE), HC_OPT_PRICE_STRIDE)
4941 };
4942 $self.backend.bt_mut().opt_ll_price_stamp = $self
4943 .backend
4944 .bt_mut()
4945 .opt_ll_price_stamp
4946 .wrapping_add(1)
4947 .max(1);
4948 let ll_price_stamp = $self.backend.bt_mut().opt_ll_price_stamp;
4949 $self.backend.bt_mut().opt_lit_price_stamp = $self
4950 .backend
4951 .bt_mut()
4952 .opt_lit_price_stamp
4953 .wrapping_add(1)
4954 .max(1);
4955 let lit_price_stamp = $self.backend.bt_mut().opt_lit_price_stamp;
4956 $self.backend.bt_mut().opt_ml_price_stamp = $self
4957 .backend
4958 .bt_mut()
4959 .opt_ml_price_stamp
4960 .wrapping_add(1)
4961 .max(1);
4962 let ml_price_stamp = $self.backend.bt_mut().opt_ml_price_stamp;
4963 let node0_price = BtMatcher::cached_lit_length_price(
4964 profile,
4965 $stats,
4966 initial_litlen,
4967 &mut ll_cache,
4968 ll_price_stamp,
4969 );
4970 nodes[0] = HcOptimalNode {
4971 litlen: initial_litlen as u32,
4972 reps: initial_reps,
4973 ..HcOptimalNode::default()
4974 };
4975 node_prices[0] = node0_price;
4976 let sufficient_len = profile.sufficient_match_len;
4977 let ll0_price = BtMatcher::cached_lit_length_price(
4978 profile,
4979 $stats,
4980 0,
4981 &mut ll_cache,
4982 ll_price_stamp,
4983 );
4984 let ll1_price = BtMatcher::cached_lit_length_price(
4985 profile,
4986 $stats,
4987 1,
4988 &mut ll_cache,
4989 ll_price_stamp,
4990 );
4991 let mut pos = 1usize;
4992 let mut last_pos = 0usize;
4993 let mut forced_end: Option<usize> = None;
4994 let mut forced_end_state: Option<HcOptimalNode> = None;
4995 let mut forced_end_price: Option<u32> = None;
4998 let mut seed_forced_shortest_path = false;
4999 let mut opt_ldm = HcOptLdmState {
5000 seq_store: HcRawSeqStore {
5001 pos: 0,
5002 pos_in_sequence: 0,
5003 size: $self.backend.bt_mut().ldm_sequences.len(),
5004 },
5005 ..HcOptLdmState::default()
5006 };
5007 let has_ldm = !$self.backend.bt_mut().ldm_sequences.is_empty();
5008 if has_ldm {
5009 if ldm_block_offset > 0 {
5021 $self
5022 .backend
5023 .bt_mut()
5024 .ldm_skip_raw_seq_store_bytes(&mut opt_ldm.seq_store, ldm_block_offset);
5025 }
5026 $self
5027 .backend
5028 .bt_mut()
5029 .ldm_get_next_match_and_update_seq_store(&mut opt_ldm, 0, $current_len);
5030 }
5031
5032 if $current_len >= min_match_len {
5035 let seed_ldm = if has_ldm {
5036 $self.backend.bt_mut().ldm_process_match_candidate(
5037 &mut opt_ldm,
5038 0,
5039 $current_len,
5040 min_match_len,
5041 )
5042 } else {
5043 None
5044 };
5045 candidates.clear();
5046 unsafe {
5050 $self.$collect::<$strategy_ty, true>(
5051 $current_abs_start,
5052 current_abs_end,
5053 profile,
5054 HcCandidateQuery {
5055 reps: initial_reps,
5056 lit_len: initial_litlen,
5057 ldm_candidate: seed_ldm,
5058 },
5059 &mut candidates,
5060 )
5061 };
5062 if !candidates.is_empty() {
5063 last_pos = (min_match_len - 1).min(frontier_limit);
5065 for p in 1..min_match_len.min(frontier_buffer_size) {
5066 BtMatcher::reset_opt_node(&mut nodes[p]);
5067 node_prices[p] = u32::MAX;
5069 let seed_litlen = initial_litlen
5079 .checked_add(p)
5080 .and_then(|s| u32::try_from(s).ok())
5081 .expect("optimal parser seed litlen out of u32 range");
5082 nodes[p].litlen = seed_litlen;
5083 }
5084 }
5085
5086 if let Some(candidate) = candidates.last() {
5087 let longest_len = candidate.match_len.min($current_len);
5088 if longest_len > sufficient_len {
5089 let off_base = BtMatcher::encode_offset_base_with_reps(
5090 candidate.offset as u32,
5091 initial_litlen,
5092 initial_reps,
5093 );
5094 let off_price = profile
5095 .offset_price_for::<ACCURATE_PRICE, FAVOR_SMALL_OFFSETS>($stats, off_base);
5096 let ml_price = BtMatcher::cached_match_length_price(
5097 profile,
5098 $stats,
5099 longest_len,
5100 &mut ml_cache,
5101 ml_price_stamp,
5102 );
5103 let seq_cost = BtMatcher::add_prices(
5104 ll0_price,
5105 profile.match_price_from_parts(off_price, ml_price, $stats),
5106 );
5107 let forced_price = BtMatcher::add_prices(node_prices[0], seq_cost);
5108 let forced_state = HcOptimalNode {
5109 off: candidate.offset as u32,
5110 mlen: longest_len as u32,
5111 litlen: 0,
5112 reps: initial_reps,
5113 };
5114 if longest_len < frontier_buffer_size && forced_price < node_prices[longest_len] {
5115 nodes[longest_len] = forced_state;
5116 node_prices[longest_len] = forced_price;
5117 }
5118 forced_end = Some(longest_len);
5119 forced_end_state = Some(forced_state);
5120 forced_end_price = Some(forced_price);
5121 seed_forced_shortest_path = true;
5122 }
5123 }
5124 if !seed_forced_shortest_path {
5125 let mut prev_max_len = min_match_len - 1;
5126 for candidate in candidates.iter() {
5127 let max_match_len = candidate.match_len.min(frontier_limit);
5128 if max_match_len < min_match_len {
5129 continue;
5130 }
5131 let start_len = (prev_max_len + 1).max(min_match_len);
5132 if start_len > max_match_len {
5133 prev_max_len = prev_max_len.max(max_match_len);
5134 continue;
5135 }
5136 if max_match_len > last_pos {
5137 BtMatcher::reset_opt_nodes(
5138 &mut nodes,
5139 &mut node_prices,
5140 last_pos + 1,
5141 max_match_len,
5142 );
5143 }
5144 let off_base = BtMatcher::encode_offset_base_with_reps(
5145 candidate.offset as u32,
5146 initial_litlen,
5147 initial_reps,
5148 );
5149 let off_price = profile
5150 .offset_price_for::<ACCURATE_PRICE, FAVOR_SMALL_OFFSETS>($stats, off_base);
5151 debug_assert!(max_match_len < frontier_buffer_size);
5152 let nodes0_price = node_prices[0];
5153 for match_len in (start_len..=max_match_len).rev() {
5154 let ml_price = BtMatcher::cached_match_length_price(
5155 profile,
5156 $stats,
5157 match_len,
5158 &mut ml_cache,
5159 ml_price_stamp,
5160 );
5161 let seq_cost = BtMatcher::add_prices(
5162 ll0_price,
5163 profile.match_price_from_parts(off_price, ml_price, $stats),
5164 );
5165 let next_cost = BtMatcher::add_prices(nodes0_price, seq_cost);
5166 let node_price = unsafe { *node_prices.get_unchecked(match_len) };
5167 if match_len > last_pos || next_cost < node_price {
5168 let slot = unsafe { nodes.get_unchecked_mut(match_len) };
5169 *slot = HcOptimalNode {
5170 off: candidate.offset as u32,
5171 mlen: match_len as u32,
5172 litlen: 0,
5173 reps: initial_reps,
5174 };
5175 unsafe { *node_prices.get_unchecked_mut(match_len) = next_cost };
5176 if match_len > last_pos {
5177 last_pos = match_len;
5178 }
5179 } else if abort_on_worse_match {
5180 break;
5181 }
5182 }
5183 prev_max_len = prev_max_len.max(max_match_len);
5184 }
5185 if last_pos + 1 < frontier_buffer_size {
5186 node_prices[last_pos + 1] = u32::MAX;
5187 }
5188 }
5189 }
5190 while !seed_forced_shortest_path && pos <= last_pos && pos <= frontier_limit {
5191 debug_assert!(pos + 1 < frontier_buffer_size);
5192 let prev_node = unsafe { *nodes.get_unchecked(pos - 1) };
5193 let prev_node_price = unsafe { *node_prices.get_unchecked(pos - 1) };
5194 if prev_node_price != u32::MAX {
5195 let lit_len = prev_node.litlen as usize + 1;
5196 let lit_price = {
5197 let bt = $self.backend.bt_mut();
5198 BtMatcher::cached_literal_price(
5199 profile,
5200 $stats,
5201 $current[pos - 1],
5202 &mut bt.opt_lit_price_scratch,
5203 &mut bt.opt_lit_price_generation,
5204 lit_price_stamp,
5205 )
5206 };
5207 let ll_delta = BtMatcher::cached_lit_length_delta_price(
5208 profile,
5209 $stats,
5210 lit_len,
5211 &mut ll_cache,
5212 ll_price_stamp,
5213 );
5214 let lit_cost = BtMatcher::add_price_delta(prev_node_price, lit_price, ll_delta);
5215 let node_pos_price = unsafe { *node_prices.get_unchecked(pos) };
5218 if lit_cost <= node_pos_price {
5219 let prev_match = unsafe { *nodes.get_unchecked(pos) };
5220 let slot = unsafe { nodes.get_unchecked_mut(pos) };
5221 *slot = prev_node;
5222 slot.litlen = lit_len as u32;
5223 node_prices[pos] = lit_cost;
5224 #[allow(clippy::collapsible_if)]
5225 if opt_level
5226 && prev_match.mlen > 0
5227 && prev_match.litlen == 0
5228 && pos < $current_len
5229 {
5230 if ll1_price < ll0_price {
5231 let next_lit_price = {
5232 let bt = $self.backend.bt_mut();
5233 BtMatcher::cached_literal_price(
5234 profile,
5235 $stats,
5236 $current[pos],
5237 &mut bt.opt_lit_price_scratch,
5238 &mut bt.opt_lit_price_generation,
5239 lit_price_stamp,
5240 )
5241 };
5242 let with1literal = BtMatcher::add_price_delta(
5243 node_pos_price,
5244 next_lit_price,
5245 ll1_price as i32 - ll0_price as i32,
5246 );
5247 let ll_delta_next = BtMatcher::cached_lit_length_delta_price(
5248 profile,
5249 $stats,
5250 lit_len + 1,
5251 &mut ll_cache,
5252 ll_price_stamp,
5253 );
5254 let with_more_literals =
5255 BtMatcher::add_price_delta(lit_cost, next_lit_price, ll_delta_next);
5256 let next = pos + 1;
5257 let next_price = unsafe { *node_prices.get_unchecked(next) };
5258 if with1literal < with_more_literals && with1literal < next_price {
5259 debug_assert!(pos >= prev_match.mlen as usize);
5261 let prev_pos = pos - prev_match.mlen as usize;
5262 {
5263 let prev_state = unsafe { *nodes.get_unchecked(prev_pos) };
5264 let (_, reps_after_match) = BtMatcher::encode_offset_with_reps(
5265 prev_match.off,
5266 prev_state.litlen as usize,
5267 prev_state.reps,
5268 );
5269 let slot = unsafe { nodes.get_unchecked_mut(next) };
5270 *slot = prev_match;
5271 slot.reps = reps_after_match;
5272 slot.litlen = 1;
5273 node_prices[next] = with1literal;
5274 if next > last_pos {
5275 last_pos = next;
5276 }
5277 }
5278 }
5279 }
5280 }
5281 }
5282 }
5283
5284 let base_cost = unsafe { *node_prices.get_unchecked(pos) };
5292 if base_cost == u32::MAX {
5293 pos += 1;
5294 continue;
5295 }
5296 {
5297 let base_node = unsafe { *nodes.get_unchecked(pos) };
5298 if base_node.mlen > 0 && base_node.litlen == 0 {
5299 debug_assert!(pos >= base_node.mlen as usize);
5301 let prev_pos = pos - base_node.mlen as usize;
5302 let prev_state = unsafe { *nodes.get_unchecked(prev_pos) };
5303 let (_, reps_after_match) = BtMatcher::encode_offset_with_reps(
5304 base_node.off,
5305 prev_state.litlen as usize,
5306 prev_state.reps,
5307 );
5308 unsafe { nodes.get_unchecked_mut(pos).reps = reps_after_match };
5309 }
5310 }
5311
5312 if pos + 8 > $current_len {
5313 pos += 1;
5314 continue;
5315 }
5316
5317 if pos == last_pos {
5318 break;
5319 }
5320
5321 let next_price = unsafe { *node_prices.get_unchecked(pos + 1) };
5322 if abort_on_worse_match
5328 && next_price <= base_cost.saturating_add(HC_BITCOST_MULTIPLIER / 2)
5329 {
5330 pos += 1;
5331 continue;
5332 }
5333
5334 let abs_pos = $current_abs_start + pos;
5335 let ldm_candidate = if has_ldm {
5336 $self.backend.bt_mut().ldm_process_match_candidate(
5337 &mut opt_ldm,
5338 pos,
5339 $current_len - pos,
5340 min_match_len,
5341 )
5342 } else {
5343 None
5344 };
5345 candidates.clear();
5346 unsafe {
5351 $self.$collect::<$strategy_ty, true>(
5352 abs_pos,
5353 current_abs_end,
5354 profile,
5355 HcCandidateQuery {
5356 reps: nodes.get_unchecked(pos).reps,
5357 lit_len: nodes.get_unchecked(pos).litlen as usize,
5358 ldm_candidate,
5359 },
5360 &mut candidates,
5361 )
5362 };
5363 let base_reps = unsafe { nodes.get_unchecked(pos).reps };
5367 let base_litlen = unsafe { nodes.get_unchecked(pos).litlen as usize };
5368 if let Some(candidate) = candidates.last() {
5369 let longest_len = candidate.match_len.min($current_len - pos);
5370 if longest_len > sufficient_len
5371 || pos + longest_len >= HC_OPT_NUM
5372 || pos + longest_len >= $current_len
5373 {
5374 let lit_len = base_litlen;
5375 let off_base = BtMatcher::encode_offset_base_with_reps(
5376 candidate.offset as u32,
5377 lit_len,
5378 base_reps,
5379 );
5380 let off_price = profile
5381 .offset_price_for::<ACCURATE_PRICE, FAVOR_SMALL_OFFSETS>($stats, off_base);
5382 let ml_price = BtMatcher::cached_match_length_price(
5383 profile,
5384 $stats,
5385 longest_len,
5386 &mut ml_cache,
5387 ml_price_stamp,
5388 );
5389 let seq_cost = BtMatcher::add_prices(
5390 ll0_price,
5391 profile.match_price_from_parts(off_price, ml_price, $stats),
5392 );
5393 let forced_price = BtMatcher::add_prices(base_cost, seq_cost);
5394 let end_pos = (pos + longest_len).min($current_len);
5395 forced_end = Some(end_pos);
5396 forced_end_state = Some(HcOptimalNode {
5397 off: candidate.offset as u32,
5398 mlen: longest_len as u32,
5399 litlen: 0,
5400 reps: base_reps,
5401 });
5402 forced_end_price = Some(forced_price);
5403 break;
5404 }
5405 }
5406 let mut prev_max_len = min_match_len - 1;
5407 for candidate in candidates.iter() {
5408 debug_assert!(pos <= frontier_limit);
5412 let max_match_len = candidate
5413 .match_len
5414 .min($current_len - pos)
5415 .min(frontier_limit - pos);
5416 let min_len = min_match_len;
5417 if max_match_len < min_len {
5418 continue;
5419 }
5420 let start_len = (prev_max_len + 1).max(min_len);
5421 if start_len > max_match_len {
5422 prev_max_len = prev_max_len.max(max_match_len);
5423 continue;
5424 }
5425 let max_next = pos + max_match_len;
5426 if max_next > last_pos {
5427 BtMatcher::reset_opt_nodes(
5428 &mut nodes,
5429 &mut node_prices,
5430 last_pos + 1,
5431 max_next,
5432 );
5433 }
5434 let lit_len = base_litlen;
5435 let off_base = BtMatcher::encode_offset_base_with_reps(
5436 candidate.offset as u32,
5437 lit_len,
5438 base_reps,
5439 );
5440 let off_price = profile
5441 .offset_price_for::<ACCURATE_PRICE, FAVOR_SMALL_OFFSETS>($stats, off_base);
5442 debug_assert!(pos + max_match_len < frontier_buffer_size);
5443 if abort_on_worse_match {
5444 for match_len in (start_len..=max_match_len).rev() {
5448 let next = pos + match_len;
5449 let ml_price = BtMatcher::cached_match_length_price(
5450 profile,
5451 $stats,
5452 match_len,
5453 &mut ml_cache,
5454 ml_price_stamp,
5455 );
5456 let seq_cost = BtMatcher::add_prices(
5457 ll0_price,
5458 profile.match_price_from_parts(off_price, ml_price, $stats),
5459 );
5460 let next_cost = BtMatcher::add_prices(base_cost, seq_cost);
5461 let node_next_price = unsafe { *node_prices.get_unchecked(next) };
5462 if next > last_pos || next_cost < node_next_price {
5463 let slot = unsafe { nodes.get_unchecked_mut(next) };
5464 *slot = HcOptimalNode {
5465 off: candidate.offset as u32,
5466 mlen: match_len as u32,
5467 litlen: 0,
5468 reps: base_reps,
5469 };
5470 unsafe { *node_prices.get_unchecked_mut(next) = next_cost };
5471 if next > last_pos {
5472 last_pos = next;
5473 }
5474 } else {
5475 break;
5476 }
5477 }
5478 } else {
5479 #[allow(unused_unsafe)]
5486 {
5487 last_pos = last_pos.max(unsafe {
5488 $priceset(
5489 &mut node_prices,
5490 &mut nodes,
5491 ml_cache,
5492 ml_price_stamp,
5493 profile,
5494 $stats,
5495 pos,
5496 start_len,
5497 max_match_len,
5498 ll0_price,
5499 off_price,
5500 base_cost,
5501 candidate.offset as u32,
5502 base_reps,
5503 last_pos,
5504 )
5505 });
5506 }
5507 }
5508 prev_max_len = prev_max_len.max(max_match_len);
5509 }
5510
5511 if last_pos + 1 < frontier_buffer_size {
5512 unsafe {
5513 *node_prices.get_unchecked_mut(last_pos + 1) = u32::MAX;
5514 }
5515 }
5516 pos += 1;
5517 }
5518
5519 if last_pos == 0 {
5520 if $current_len == 0 {
5521 let price = node_prices[0];
5522 return $self.backend.bt_mut().finish_optimal_plan(
5523 HcOptimalPlanBuffers {
5524 nodes,
5525 node_prices,
5526 candidates,
5527 store,
5528 price_arena,
5529 },
5530 (price, initial_reps, initial_litlen, 0),
5531 );
5532 }
5533 let lit_price = {
5534 let bt = $self.backend.bt_mut();
5535 BtMatcher::cached_literal_price(
5536 profile,
5537 $stats,
5538 $current[0],
5539 &mut bt.opt_lit_price_scratch,
5540 &mut bt.opt_lit_price_generation,
5541 lit_price_stamp,
5542 )
5543 };
5544 let next_litlen = initial_litlen
5551 .checked_add(1)
5552 .expect("optimal parser next litlen out of usize range");
5553 let ll_delta = BtMatcher::cached_lit_length_delta_price(
5554 profile,
5555 $stats,
5556 next_litlen,
5557 &mut ll_cache,
5558 ll_price_stamp,
5559 );
5560 let price = BtMatcher::add_price_delta(node_prices[0], lit_price, ll_delta);
5561 return $self.backend.bt_mut().finish_optimal_plan(
5562 HcOptimalPlanBuffers {
5563 nodes,
5564 node_prices,
5565 candidates,
5566 store,
5567 price_arena,
5568 },
5569 (price, initial_reps, next_litlen, 1),
5570 );
5571 }
5572
5573 let target_pos = forced_end.unwrap_or(last_pos.min(frontier_limit));
5574 let (last_stretch, last_stretch_price) = if let Some(forced_state) = forced_end_state {
5578 (forced_state, forced_end_price.expect("forced state has a price"))
5579 } else {
5580 (nodes[target_pos], node_prices[target_pos])
5581 };
5582 if last_stretch_price == u32::MAX {
5583 return $self.backend.bt_mut().finish_optimal_plan(
5584 HcOptimalPlanBuffers {
5585 nodes,
5586 node_prices,
5587 candidates,
5588 store,
5589 price_arena,
5590 },
5591 (u32::MAX, initial_reps, initial_litlen, $current_len),
5592 );
5593 }
5594
5595 if last_stretch.mlen == 0 {
5596 return $self.backend.bt_mut().finish_optimal_plan(
5597 HcOptimalPlanBuffers {
5598 nodes,
5599 node_prices,
5600 candidates,
5601 store,
5602 price_arena,
5603 },
5604 (
5605 last_stretch_price,
5606 last_stretch.reps,
5607 last_stretch.litlen as usize,
5608 target_pos.min($current_len),
5609 ),
5610 );
5611 }
5612
5613 let mut cur = target_pos.saturating_sub(last_stretch.mlen as usize);
5614 let end_reps = if last_stretch.litlen == 0 {
5615 let prev_state = nodes[cur];
5616 let (_, reps_after_match) = BtMatcher::encode_offset_with_reps(
5617 last_stretch.off,
5618 prev_state.litlen as usize,
5619 prev_state.reps,
5620 );
5621 reps_after_match
5622 } else {
5623 let tail_literals = last_stretch.litlen as usize;
5624 if cur < tail_literals {
5625 return $self.backend.bt_mut().finish_optimal_plan(
5626 HcOptimalPlanBuffers {
5627 nodes,
5628 node_prices,
5629 candidates,
5630 store,
5631 price_arena,
5632 },
5633 (
5634 last_stretch_price,
5635 last_stretch.reps,
5636 tail_literals,
5637 target_pos.min($current_len),
5638 ),
5639 );
5640 }
5641 cur -= tail_literals;
5642 last_stretch.reps
5643 };
5644 let store_end = cur + 2;
5645 if store.len() <= store_end {
5646 store.resize(store_end + 1, HcOptimalNode::default());
5647 }
5648 let mut store_start;
5649 let mut stretch_pos = cur;
5650
5651 if last_stretch.litlen > 0 {
5652 store[store_end] = HcOptimalNode {
5653 litlen: last_stretch.litlen,
5654 mlen: 0,
5655 ..HcOptimalNode::default()
5656 };
5657 store_start = store_end.saturating_sub(1);
5658 store[store_start] = last_stretch;
5659 }
5660 store[store_end] = last_stretch;
5661 store_start = store_end;
5662
5663 loop {
5664 let next_stretch = nodes[stretch_pos];
5665 store[store_start].litlen = next_stretch.litlen;
5666 if next_stretch.mlen == 0 {
5667 break;
5668 }
5669 if store_start == 0 {
5670 break;
5671 }
5672 store_start -= 1;
5673 store[store_start] = next_stretch;
5674 let litlen = next_stretch.litlen as usize;
5681 let mlen = next_stretch.mlen as usize;
5682 debug_assert!(litlen + mlen <= $current_len);
5683 let step = litlen + mlen;
5684 if step == 0 || stretch_pos < step {
5685 break;
5686 }
5687 stretch_pos -= step;
5688 }
5689
5690 let mut tail_literals = initial_litlen;
5691 let mut store_pos = store_start;
5692 while store_pos <= store_end {
5693 let stretch = store[store_pos];
5694 let llen = stretch.litlen as usize;
5695 let mlen = stretch.mlen as usize;
5696 if mlen == 0 {
5697 tail_literals = llen;
5698 store_pos += 1;
5699 continue;
5700 }
5701 $out.push(HcOptimalSequence {
5702 offset: stretch.off,
5703 match_len: mlen as u32,
5704 lit_len: llen as u32,
5705 });
5706 tail_literals = 0;
5707 store_pos += 1;
5708 }
5709 let result = (
5710 last_stretch_price,
5711 end_reps,
5712 if last_stretch.litlen > 0 {
5713 last_stretch.litlen as usize
5714 } else {
5715 tail_literals
5716 },
5717 target_pos.min($current_len),
5718 );
5719 $self.backend.bt_mut().finish_optimal_plan(
5720 HcOptimalPlanBuffers {
5721 nodes,
5722 node_prices,
5723 candidates,
5724 store,
5725 price_arena,
5726 },
5727 result,
5728 )
5729 }};
5730}
5731
5732macro_rules! collect_optimal_candidates_initialized_body {
5741 (
5742 $self:expr,
5743 $strategy_ty:ty,
5744 $abs_pos:ident,
5745 $current_abs_end:ident,
5746 $profile:ident,
5747 $query:ident,
5748 $out:ident,
5749 $bt_matchfinder:ident,
5750 $bt_update:ident,
5751 $bt_insert:ident,
5752 $for_each_rep:ident,
5753 $hash3:ident,
5754 $cpl:path $(,)?
5755 ) => {{
5756 let use_hash3: bool = <$strategy_ty as super::strategy::Strategy>::USE_HASH3;
5765 debug_assert!(!$self.table.hash_table.is_empty());
5766 debug_assert!($self.table.hash3_log == 0 || !$self.table.hash3_table.is_empty());
5767 debug_assert!(
5768 !use_hash3 || $self.table.hash3_log != 0,
5769 "Strategy::USE_HASH3 = true but runtime hash3_log is 0 — call configure() first",
5770 );
5771 debug_assert!(!$self.table.chain_table.is_empty());
5772 let min_match_len = HC_OPT_MIN_MATCH_LEN;
5773 let reps = $query.reps;
5774 let lit_len = $query.lit_len;
5775 let ldm_candidate = $query.ldm_candidate;
5776 $out.clear();
5777 if $abs_pos < $self.table.skip_insert_until_abs {
5778 if let Some(ldm) = ldm_candidate {
5779 let mut best_len_for_skip = 0usize;
5780 let _ = super::bt::BtMatcher::push_candidate_ladder(
5781 $out,
5782 &mut best_len_for_skip,
5783 ldm,
5784 min_match_len,
5785 );
5786 }
5787 return;
5788 }
5789 if $bt_matchfinder {
5790 unsafe { $self.table.$bt_update($abs_pos, $current_abs_end) };
5793 }
5794 let current_idx = $abs_pos - $self.table.history_abs_start;
5795 if current_idx + 4 > $self.table.live_history().len() {
5796 if let Some(ldm) = ldm_candidate {
5797 let mut best_len_for_skip = 0usize;
5798 let _ = super::bt::BtMatcher::push_candidate_ladder(
5799 $out,
5800 &mut best_len_for_skip,
5801 ldm,
5802 min_match_len,
5803 );
5804 }
5805 return;
5806 }
5807 let mut best_len_for_skip = 0usize;
5808 let mut skip_further_match_search = false;
5809 let mut rep_len_candidate_found = false;
5810 unsafe {
5812 $self.hc.$for_each_rep(
5813 &$self.table,
5814 $abs_pos,
5815 lit_len,
5816 reps,
5817 $current_abs_end,
5818 min_match_len,
5819 |rep| {
5820 if rep.match_len >= min_match_len {
5821 rep_len_candidate_found = true;
5822 }
5823 let _ = super::bt::BtMatcher::push_candidate_ladder(
5824 $out,
5825 &mut best_len_for_skip,
5826 rep,
5827 min_match_len,
5828 );
5829 if rep.match_len > $profile.sufficient_match_len {
5830 skip_further_match_search = true;
5831 }
5832 if $abs_pos + rep.match_len >= $current_abs_end {
5839 skip_further_match_search = true;
5840 }
5841 },
5842 )
5843 };
5844 if use_hash3 && !skip_further_match_search && best_len_for_skip < min_match_len {
5848 $self.table.update_hash3_until($abs_pos);
5849 if let Some(h3) = unsafe {
5851 $self
5852 .table
5853 .$hash3($abs_pos, $current_abs_end, min_match_len)
5854 } {
5855 let _ = super::bt::BtMatcher::push_candidate_ladder(
5856 $out,
5857 &mut best_len_for_skip,
5858 h3,
5859 min_match_len,
5860 );
5861 if !rep_len_candidate_found
5862 && (h3.match_len > $profile.sufficient_match_len
5863 || $abs_pos + h3.match_len >= $current_abs_end)
5864 {
5865 $self.table.skip_insert_until_abs = $abs_pos + 1;
5866 skip_further_match_search = true;
5867 }
5868 }
5869 }
5870 if !skip_further_match_search && $bt_matchfinder {
5871 unsafe {
5873 $self.table.$bt_insert(
5874 $abs_pos,
5875 $current_abs_end,
5876 $profile,
5877 min_match_len,
5878 &mut best_len_for_skip,
5879 $out,
5880 )
5881 };
5882 } else if !skip_further_match_search {
5883 $self.table.insert_position($abs_pos);
5884 let max_chain_depth = $profile.max_chain_depth.min($self.hc.search_depth);
5885 let concat = $self.table.live_history();
5886 let mut match_end_abs = $abs_pos + 9;
5890 if max_chain_depth > 0 {
5891 for (visited, candidate_abs) in $self
5892 .hc
5893 .chain_candidates(&$self.table, $abs_pos)
5894 .into_iter()
5895 .enumerate()
5896 {
5897 if visited >= max_chain_depth {
5898 break;
5899 }
5900 if candidate_abs == usize::MAX {
5901 break;
5902 }
5903 if candidate_abs < $self.table.window_low_abs_for_target($abs_pos)
5904 || candidate_abs >= $abs_pos
5905 {
5906 continue;
5907 }
5908 let candidate_idx = candidate_abs - $self.table.history_abs_start;
5909 debug_assert!(
5910 $abs_pos <= $current_abs_end,
5911 "HC chain walker called past current block end"
5912 );
5913 let tail_limit = $current_abs_end - $abs_pos;
5914 let base = concat.as_ptr();
5915 let match_len =
5920 unsafe { $cpl(base.add(candidate_idx), base.add(current_idx), tail_limit) };
5921 if match_len < min_match_len {
5922 continue;
5923 }
5924 let offset = $abs_pos - candidate_abs;
5925 if super::bt::BtMatcher::push_candidate_ladder(
5926 $out,
5927 &mut best_len_for_skip,
5928 MatchCandidate {
5929 start: $abs_pos,
5930 offset,
5931 match_len,
5932 },
5933 min_match_len,
5934 ) {
5935 let candidate_end = candidate_abs + match_len;
5936 if candidate_end > match_end_abs {
5937 match_end_abs = candidate_end;
5938 }
5939 }
5940 if match_len > HC_OPT_NUM || $abs_pos + match_len >= $current_abs_end {
5941 break;
5942 }
5943 }
5944 }
5945 $self.table.skip_insert_until_abs =
5948 $self.table.skip_insert_until_abs.max(match_end_abs - 8);
5949 }
5950 if let Some(ldm) = ldm_candidate {
5951 let _ = super::bt::BtMatcher::push_candidate_ladder(
5952 $out,
5953 &mut best_len_for_skip,
5954 ldm,
5955 min_match_len,
5956 );
5957 }
5958 }};
5959}
5960
5961macro_rules! hash3_candidate_body {
5966 (
5967 $table:expr,
5968 $abs_pos:ident,
5969 $current_abs_end:ident,
5970 $min_match_len:ident,
5971 $cpl:path $(,)?
5972 ) => {{
5973 if $table.hash3_log == 0 {
5974 return None;
5975 }
5976 let idx = $abs_pos.checked_sub($table.history_abs_start)?;
5977 let concat = $table.live_history();
5978 if idx + 4 > concat.len() {
5979 return None;
5980 }
5981 let hash3 = $crate::encoding::match_table::storage::MatchTable::hash_position_at(
5982 concat,
5983 idx,
5984 $table.hash3_log,
5985 3,
5986 );
5987 let entry = $table
5988 .hash3_table
5989 .get(hash3)
5990 .copied()
5991 .unwrap_or($crate::encoding::match_table::storage::HC_EMPTY);
5992 let candidate_abs =
5993 $crate::encoding::match_table::storage::MatchTable::stored_abs_position_fast(
5994 entry,
5995 $table.position_base,
5996 $table.index_shift,
5997 )?;
5998 if candidate_abs < $table.history_abs_start || candidate_abs >= $abs_pos {
5999 return None;
6000 }
6001 let offset = $abs_pos - candidate_abs;
6002 if offset >= $crate::encoding::bt::HC3_MAX_OFFSET {
6003 return None;
6004 }
6005 let candidate_idx = candidate_abs - $table.history_abs_start;
6006 let tail_limit = $current_abs_end.saturating_sub($abs_pos);
6007 let base = concat.as_ptr();
6008 let match_len = unsafe { $cpl(base.add(candidate_idx), base.add(idx), tail_limit) };
6011 (match_len >= $min_match_len).then_some($crate::encoding::opt::types::MatchCandidate {
6012 start: $abs_pos,
6013 offset,
6014 match_len,
6015 })
6016 }};
6017}
6018pub(crate) use hash3_candidate_body;
6019
6020macro_rules! for_each_repcode_candidate_body {
6030 (
6031 $table:expr,
6032 $abs_pos:ident,
6033 $lit_len:ident,
6034 $reps:ident,
6035 $current_abs_end:ident,
6036 $min_match_len:ident,
6037 $f:ident,
6038 $cpl:path $(,)?
6039 ) => {{
6040 let rep_offsets: [Option<usize>; 3] = if $lit_len == 0 {
6041 [
6042 Some($reps[1] as usize),
6043 Some($reps[2] as usize),
6044 ($reps[0] > 1).then_some(($reps[0] - 1) as usize),
6045 ]
6046 } else {
6047 [
6048 Some($reps[0] as usize),
6049 Some($reps[1] as usize),
6050 Some($reps[2] as usize),
6051 ]
6052 };
6053 let concat = $table.live_history();
6054 let current_idx = $abs_pos - $table.history_abs_start;
6055 if current_idx + 4 > concat.len() {
6056 return;
6057 }
6058 let tail_limit = $current_abs_end.saturating_sub($abs_pos);
6059 let base = concat.as_ptr();
6060 let concat_len = concat.len();
6061 for rep in rep_offsets.into_iter().flatten() {
6062 if rep == 0 || rep > $abs_pos {
6063 continue;
6064 }
6065 let candidate_pos = $abs_pos - rep;
6066 if candidate_pos < $table.history_abs_start {
6067 continue;
6068 }
6069 let candidate_idx = candidate_pos - $table.history_abs_start;
6070 let gate_matches = unsafe {
6082 let cand = base.add(candidate_idx).cast::<u32>().read_unaligned();
6083 let cur = base.add(current_idx).cast::<u32>().read_unaligned();
6084 if $min_match_len == 3 {
6085 (cand.to_le() & 0x00FF_FFFF) == (cur.to_le() & 0x00FF_FFFF)
6088 } else {
6089 cand == cur
6090 }
6091 };
6092 if !gate_matches {
6093 continue;
6094 }
6095 let max = (concat_len - candidate_idx)
6100 .min(concat_len - current_idx)
6101 .min(tail_limit);
6102 let match_len = unsafe { $cpl(base.add(candidate_idx), base.add(current_idx), max) };
6103 if match_len < $min_match_len {
6104 continue;
6105 }
6106 $f(MatchCandidate {
6107 start: $abs_pos,
6108 offset: rep,
6109 match_len,
6110 });
6111 }
6112 }};
6113}
6114pub(crate) use for_each_repcode_candidate_body;
6115
6116macro_rules! bt_insert_and_collect_matches_body {
6123 (
6124 $table:expr,
6125 $search_depth:expr,
6126 $abs_pos:ident,
6127 $current_abs_end:ident,
6128 $profile:ident,
6129 $min_match_len:ident,
6130 $best_len_for_skip:ident,
6131 $out:ident,
6132 $cmf:path $(,)?
6133 ) => {{
6134 let idx = $abs_pos - $table.history_abs_start;
6135 let concat: &[u8] = unsafe {
6140 let lh = $table.live_history();
6141 core::slice::from_raw_parts(lh.as_ptr(), lh.len())
6142 };
6143 if idx + 8 > concat.len() {
6144 return;
6145 }
6146 debug_assert!(
6147 $abs_pos <= $current_abs_end,
6148 "BT collect called past current block end"
6149 );
6150 let tail_limit = $current_abs_end - $abs_pos;
6151 let hash = $crate::encoding::match_table::storage::MatchTable::hash_position_at(
6152 concat,
6153 idx,
6154 $table.hash_log,
6155 $table.search_mls,
6156 );
6157 #[cfg(all(
6165 target_feature = "sse",
6166 any(target_arch = "x86", target_arch = "x86_64")
6167 ))]
6168 {
6169 #[cfg(target_arch = "x86")]
6170 use core::arch::x86::{_MM_HINT_T0, _mm_prefetch};
6171 #[cfg(target_arch = "x86_64")]
6172 use core::arch::x86_64::{_MM_HINT_T0, _mm_prefetch};
6173 unsafe {
6176 _mm_prefetch($table.hash_table.as_ptr().add(hash).cast(), _MM_HINT_T0);
6177 }
6178 if idx + 1 + 8 <= concat.len() {
6184 let hash_next =
6185 $crate::encoding::match_table::storage::MatchTable::hash_position_at(
6186 concat,
6187 idx + 1,
6188 $table.hash_log,
6189 $table.search_mls,
6190 );
6191 unsafe {
6194 _mm_prefetch(
6195 $table.hash_table.as_ptr().add(hash_next).cast(),
6196 _MM_HINT_T0,
6197 );
6198 }
6199 }
6200 }
6201 let Some(relative_pos) = $table.relative_position($abs_pos) else {
6202 return;
6203 };
6204 let stored = relative_pos + 1;
6205 let bt_mask = $table.bt_mask();
6206 let chain_ptr = $table.chain_table.as_mut_ptr();
6218 debug_assert_eq!($table.chain_table.len(), 2 << $table.bt_log());
6219 let bt_low = $abs_pos.saturating_sub(bt_mask);
6222 let window_low = $table.window_low_abs_for_target($abs_pos);
6223 let win_off = $table
6234 .position_base
6235 .wrapping_sub(1)
6236 .wrapping_sub($table.index_shift)
6237 .wrapping_sub(window_low);
6238 let win_range = $abs_pos - window_low;
6239 let mut match_end_abs = $abs_pos + 9;
6243 let mut compares_left = $profile.max_chain_depth.min($search_depth);
6244 let mut common_length_smaller = 0usize;
6245 let mut common_length_larger = 0usize;
6246 let pair_idx = $table.bt_pair_index_for_abs($abs_pos);
6247 let mut smaller_slot = pair_idx;
6248 let mut larger_slot = pair_idx + 1;
6249 let mut match_stored = $table.hash_table[hash];
6250 $table.hash_table[hash] = stored;
6251 debug_assert!(
6256 $min_match_len >= $crate::encoding::cost_model::HC_FORMAT_MINMATCH,
6257 "min_match_len must be at least HC_FORMAT_MINMATCH"
6258 );
6259 let mut best_len = (*$best_len_for_skip).max($min_match_len - 1);
6260
6261 while compares_left > 0 && (match_stored as usize).wrapping_add(win_off) < win_range {
6267 compares_left -= 1;
6268 let candidate_abs = ($table.position_base + (match_stored as usize - 1))
6272 .wrapping_sub($table.index_shift);
6273
6274 let next_pair_idx = $table.bt_pair_index_for_abs(candidate_abs);
6275 let next_smaller = unsafe { *chain_ptr.add(next_pair_idx) };
6279 let next_larger = unsafe { *chain_ptr.add(next_pair_idx + 1) };
6280 let seed_len = common_length_smaller.min(common_length_larger);
6281 let candidate_idx = candidate_abs - $table.history_abs_start;
6282 let match_len = unsafe { $cmf(concat, idx, candidate_idx, tail_limit, seed_len) };
6285
6286 if match_len > best_len {
6287 let offset = $abs_pos - candidate_abs;
6288 let accepted = $crate::encoding::bt::BtMatcher::push_candidate_ladder(
6289 $out,
6290 $best_len_for_skip,
6291 $crate::encoding::opt::types::MatchCandidate {
6292 start: $abs_pos,
6293 offset,
6294 match_len,
6295 },
6296 $min_match_len,
6297 );
6298 if accepted {
6299 best_len = match_len;
6300 let candidate_end = candidate_abs + match_len;
6308 if candidate_end > match_end_abs {
6309 match_end_abs = candidate_end;
6310 }
6311 if match_len >= tail_limit
6312 || match_len > $crate::encoding::cost_model::HC_OPT_NUM
6313 {
6314 break;
6315 }
6316 }
6317 }
6318
6319 if match_len >= tail_limit {
6320 break;
6321 }
6322
6323 let candidate_next = candidate_idx + match_len;
6324 let current_next = idx + match_len;
6325 if unsafe {
6329 *concat.get_unchecked(candidate_next) < *concat.get_unchecked(current_next)
6330 } {
6331 unsafe { *chain_ptr.add(smaller_slot) = match_stored };
6335 common_length_smaller = match_len;
6336 if candidate_abs <= bt_low {
6337 smaller_slot = usize::MAX;
6338 break;
6339 }
6340 smaller_slot = next_pair_idx + 1;
6341 match_stored = next_larger;
6342 } else {
6343 unsafe { *chain_ptr.add(larger_slot) = match_stored };
6345 common_length_larger = match_len;
6346 if candidate_abs <= bt_low {
6347 larger_slot = usize::MAX;
6348 break;
6349 }
6350 larger_slot = next_pair_idx;
6351 match_stored = next_smaller;
6352 }
6353 }
6354
6355 if smaller_slot != usize::MAX {
6358 unsafe {
6359 *chain_ptr.add(smaller_slot) = $crate::encoding::match_table::storage::HC_EMPTY
6360 };
6361 }
6362 if larger_slot != usize::MAX {
6363 unsafe {
6364 *chain_ptr.add(larger_slot) = $crate::encoding::match_table::storage::HC_EMPTY
6365 };
6366 }
6367
6368 if let Some(dms) = $table.dms.table() {
6381 let region = $table.dms.region_len();
6382 let dh = $crate::encoding::match_table::storage::MatchTable::hash_position_at(
6383 concat,
6384 idx,
6385 dms.hash_log,
6386 dms.mls,
6387 );
6388 let mut dcur = dms.hash_table[dh];
6389 let mut common_smaller = 0usize;
6392 let mut common_larger = 0usize;
6393 let mut dms_compares = $profile.max_chain_depth.min($search_depth);
6394 while dms_compares > 0 && dcur != $crate::encoding::match_table::storage::HC_EMPTY {
6395 let dict_idx = (dcur - 1) as usize;
6396 if dict_idx >= region || dict_idx >= idx {
6398 break;
6399 }
6400 dms_compares -= 1;
6401 let pair = 2 * dict_idx;
6402 let seed = common_smaller.min(common_larger);
6403 let match_len = unsafe { $cmf(concat, idx, dict_idx, tail_limit, seed) };
6407 if match_len > best_len {
6408 let offset = idx - dict_idx;
6409 let accepted = $crate::encoding::bt::BtMatcher::push_candidate_ladder(
6410 $out,
6411 $best_len_for_skip,
6412 $crate::encoding::opt::types::MatchCandidate {
6413 start: $abs_pos,
6414 offset,
6415 match_len,
6416 },
6417 $min_match_len,
6418 );
6419 if accepted {
6420 best_len = match_len;
6421 let candidate_end = $abs_pos + match_len;
6422 if candidate_end > match_end_abs {
6423 match_end_abs = candidate_end;
6424 }
6425 if match_len > $crate::encoding::cost_model::HC_OPT_NUM {
6426 break;
6427 }
6428 }
6429 }
6430 if match_len >= tail_limit {
6434 break;
6435 }
6436 if concat[dict_idx + match_len] < concat[idx + match_len] {
6439 common_smaller = match_len;
6440 dcur = dms.chain_table[pair + 1];
6441 } else {
6442 common_larger = match_len;
6443 dcur = dms.chain_table[pair];
6444 }
6445 }
6446 }
6447
6448 $table.skip_insert_until_abs = match_end_abs - 8;
6451 }};
6452}
6453pub(crate) use bt_insert_and_collect_matches_body;
6454
6455impl HcMatchGenerator {
6456 fn heap_size(&self) -> usize {
6459 self.table.heap_size() + self.backend.heap_size()
6460 }
6461
6462 fn should_run_btultra2_seed_pass<S: super::strategy::Strategy>(
6463 &self,
6464 current_len: usize,
6465 ) -> bool {
6466 if !S::TWO_PASS_SEED {
6472 return false;
6473 }
6474 let HcBackend::Bt(bt) = &self.backend else {
6475 return false;
6476 };
6477 bt.opt_state.lit_length_sum == 0
6478 && bt.opt_state.dictionary_seed.is_none()
6479 && !self.table.dictionary_primed_for_frame
6480 && bt.ldm_sequences.is_empty()
6481 && self.table.window_size == current_len
6482 && self.table.history_abs_start == 0
6483 && self.table.chunk_lens.len() == 1
6484 && current_len > HC_PREDEF_THRESHOLD
6485 }
6486
6487 fn new(max_window_size: usize) -> Self {
6488 Self {
6489 table: super::match_table::storage::MatchTable::new(max_window_size),
6490 hc: super::hc::HcMatcher::new(2, HC_SEARCH_DEPTH, HC_TARGET_LEN),
6491 backend: HcBackend::Hc,
6494 strategy_tag: super::strategy::StrategyTag::Lazy,
6501 }
6502 }
6503
6504 fn configure(&mut self, config: HcConfig, tag: super::strategy::StrategyTag, window_log: u8) {
6505 use super::strategy::StrategyTag;
6506 self.strategy_tag = tag;
6510 let is_btultra2 = tag == StrategyTag::BtUltra2;
6511 let uses_bt = matches!(
6512 tag,
6513 StrategyTag::Btlazy2
6514 | StrategyTag::BtOpt
6515 | StrategyTag::BtUltra
6516 | StrategyTag::BtUltra2
6517 );
6518 let wants_hash3 = matches!(tag, StrategyTag::BtUltra | StrategyTag::BtUltra2);
6523 let next_hash3_log = if wants_hash3 {
6524 HC3_HASH_LOG.min(window_log as usize)
6525 } else {
6526 0
6527 };
6528 let resize = self.table.hash_log != config.hash_log
6529 || self.table.chain_log != config.chain_log
6530 || self.table.hash3_log != next_hash3_log;
6531 let uses_bt_changed = self.table.uses_bt != uses_bt;
6534 self.table.hash_log = config.hash_log;
6535 self.table.chain_log = config.chain_log;
6536 self.table.hash3_log = next_hash3_log;
6537 self.hc.search_depth = if uses_bt {
6538 config.search_depth
6539 } else {
6540 config.search_depth.min(MAX_HC_SEARCH_DEPTH)
6541 };
6542 self.hc.target_len = config.target_len;
6543 self.table.search_depth = self.hc.search_depth;
6547 self.table.is_btultra2 = is_btultra2;
6548 self.table.uses_bt = uses_bt;
6549 let mls_changed = self.table.search_mls != config.search_mls;
6568 if resize || mls_changed || uses_bt_changed {
6569 self.table.dms.invalidate();
6570 }
6571 self.table.search_mls = config.search_mls;
6572 match (&self.backend, self.table.uses_bt) {
6576 (HcBackend::Hc, true) => {
6577 self.backend = HcBackend::Bt(alloc::boxed::Box::new(super::bt::BtMatcher::new()));
6578 }
6579 (HcBackend::Bt(_), false) => {
6580 self.backend = HcBackend::Hc;
6581 }
6582 _ => {}
6583 }
6584 if resize && !self.table.hash_table.is_empty() {
6585 self.table.hash_table.clear();
6587 self.table.hash3_table.clear();
6588 self.table.chain_table.clear();
6589 }
6590 }
6591
6592 fn seed_dictionary_entropy(
6593 &mut self,
6594 huff: Option<&crate::huff0::huff0_encoder::HuffmanTable>,
6595 ll: Option<&crate::fse::fse_encoder::FSETable>,
6596 ml: Option<&crate::fse::fse_encoder::FSETable>,
6597 of: Option<&crate::fse::fse_encoder::FSETable>,
6598 ) {
6599 if let HcBackend::Bt(bt) = &mut self.backend {
6600 bt.opt_state.seed_dictionary_entropy(huff, ll, ml, of);
6601 }
6602 }
6603
6604 #[cfg(feature = "hash")]
6609 fn set_ldm_producer(&mut self, producer: Option<super::ldm::LdmProducer>) {
6610 if let HcBackend::Bt(bt) = &mut self.backend {
6611 bt.ldm_producer = producer;
6612 }
6613 }
6614
6615 #[cfg(feature = "hash")]
6621 fn take_ldm_producer(&mut self) -> Option<super::ldm::LdmProducer> {
6622 if let HcBackend::Bt(bt) = &mut self.backend {
6623 bt.ldm_producer.take()
6624 } else {
6625 None
6626 }
6627 }
6628
6629 fn reset(&mut self, reuse_space: impl FnMut(Vec<u8>)) {
6630 self.table.reset(reuse_space);
6631 if let HcBackend::Bt(bt) = &mut self.backend {
6632 bt.reset();
6633 }
6634 }
6635
6636 fn skip_matching(&mut self, incompressible_hint: Option<bool>) {
6639 self.table.skip_matching(incompressible_hint);
6640 }
6641
6642 #[cfg(test)]
6648 fn start_matching(&mut self, mut handle_sequence: impl for<'a> FnMut(Sequence<'a>)) {
6649 use super::strategy::{self, StrategyTag};
6650 match self.strategy_tag {
6656 StrategyTag::Fast | StrategyTag::Dfast | StrategyTag::Greedy | StrategyTag::Lazy => {
6657 self.start_matching_lazy(&mut handle_sequence)
6658 }
6659 StrategyTag::Btlazy2 => self.start_matching_btlazy2(&mut handle_sequence),
6660 StrategyTag::BtOpt => {
6661 self.start_matching_optimal::<strategy::BtOpt>(&mut handle_sequence)
6662 }
6663 StrategyTag::BtUltra => {
6664 self.start_matching_optimal::<strategy::BtUltra>(&mut handle_sequence)
6665 }
6666 StrategyTag::BtUltra2 => {
6667 self.start_matching_optimal::<strategy::BtUltra2>(&mut handle_sequence)
6668 }
6669 }
6670 }
6671
6672 pub(crate) fn start_matching_strategy<S: super::strategy::Strategy>(
6683 &mut self,
6684 handle_sequence: &mut impl for<'a> FnMut(Sequence<'a>),
6685 ) {
6686 debug_assert_eq!(
6687 self.table.uses_bt,
6688 S::USE_BT,
6689 "Strategy::USE_BT disagrees with runtime table.uses_bt at HC dispatch"
6690 );
6691 if S::USE_BT {
6692 self.start_matching_optimal::<S>(handle_sequence)
6693 } else {
6694 self.start_matching_lazy(handle_sequence)
6695 }
6696 }
6697
6698 pub(crate) fn start_matching_lazy(
6703 &mut self,
6704 handle_sequence: impl for<'a> FnMut(Sequence<'a>),
6705 ) {
6706 if self.table.dms.is_primed() {
6707 self.start_matching_lazy_impl::<true>(handle_sequence);
6708 } else {
6709 self.start_matching_lazy_impl::<false>(handle_sequence);
6710 }
6711 }
6712
6713 fn start_matching_lazy_impl<const DICT: bool>(
6714 &mut self,
6715 mut handle_sequence: impl for<'a> FnMut(Sequence<'a>),
6716 ) {
6717 self.table.ensure_tables();
6718
6719 let (current_abs_start, current_len) = self.table.current_block_range();
6722 if current_len == 0 {
6723 return;
6724 }
6725 let current_ptr = self.table.get_last_space().as_ptr();
6732 let current: &[u8] = unsafe { core::slice::from_raw_parts(current_ptr, current_len) };
6733
6734 let concat: &[u8] = {
6744 let lh = self.table.live_history();
6745 unsafe { core::slice::from_raw_parts(lh.as_ptr(), lh.len()) }
6746 };
6747 let dms_primed = self.table.dms.is_primed();
6753
6754 let current_abs_end = current_abs_start + current_len;
6755 self.table
6756 .backfill_boundary_positions(current_abs_start, current_abs_end);
6757
6758 let mut pos = 0usize;
6759 let mut literals_start = 0usize;
6760 while pos + HC_MIN_MATCH_LEN <= current_len {
6761 let abs_pos = current_abs_start + pos;
6762 let lit_len = pos - literals_start;
6763
6764 let best =
6770 self.hc
6771 .find_best_match::<DICT>(concat, dms_primed, &self.table, abs_pos, lit_len);
6772 if best.is_match() {
6773 if self.hc.pick_lazy_match::<DICT>(
6774 concat,
6775 dms_primed,
6776 &self.table,
6777 abs_pos,
6778 lit_len,
6779 best,
6780 ) {
6781 let history_abs_start = self.table.history_abs_start;
6787 let min_abs = abs_pos - lit_len;
6788 let mut start_abs = abs_pos;
6789 let mut cand_abs = abs_pos - best.offset;
6790 let mut match_len = best.match_len;
6791 while start_abs > min_abs
6792 && cand_abs > history_abs_start
6793 && concat[cand_abs - history_abs_start - 1]
6794 == concat[start_abs - history_abs_start - 1]
6795 {
6796 start_abs -= 1;
6797 cand_abs -= 1;
6798 match_len += 1;
6799 }
6800 self.table.insert_match_span(abs_pos, start_abs + match_len);
6801 let start = start_abs - current_abs_start;
6802 let literals = ¤t[literals_start..start];
6803 handle_sequence(Sequence::Triple {
6804 literals,
6805 offset: best.offset,
6806 match_len,
6807 });
6808 let _ = encode_offset_with_history(
6809 best.offset as u32,
6810 literals.len() as u32,
6811 &mut self.table.offset_hist,
6812 );
6813 pos = start + match_len;
6814 literals_start = pos;
6815 continue;
6816 }
6817 self.table.insert_position(abs_pos);
6823 pos += 1;
6824 continue;
6825 }
6826 self.table.insert_position(abs_pos);
6828 let step = ((pos - literals_start) >> 8) + 1;
6840 pos += step;
6841 }
6849
6850 while pos + 4 <= current_len {
6853 self.table.insert_position(current_abs_start + pos);
6854 pos += 1;
6855 }
6856
6857 if literals_start < current_len {
6858 handle_sequence(Sequence::Literals {
6859 literals: ¤t[literals_start..],
6860 });
6861 }
6862 }
6863
6864 pub(crate) unsafe fn set_borrowed_window(&mut self, buffer: &[u8]) {
6868 unsafe { self.table.set_borrowed_window(buffer) };
6870 }
6871
6872 pub(crate) fn clear_borrowed_window(&mut self) {
6873 self.table.clear_borrowed_window();
6874 }
6875
6876 pub(crate) fn start_matching_lazy_borrowed(
6882 &mut self,
6883 block_start: usize,
6884 block_end: usize,
6885 handle_sequence: impl for<'a> FnMut(Sequence<'a>),
6886 ) {
6887 self.table.stage_borrowed_block(block_start, block_end);
6888 self.start_matching_lazy(handle_sequence);
6889 }
6890
6891 pub(crate) fn skip_matching_borrowed(
6894 &mut self,
6895 block_start: usize,
6896 block_end: usize,
6897 incompressible_hint: Option<bool>,
6898 ) {
6899 self.table.stage_borrowed_block(block_start, block_end);
6900 self.table.skip_matching(incompressible_hint);
6901 }
6902
6903 fn start_matching_btlazy2(&mut self, mut handle_sequence: impl for<'a> FnMut(Sequence<'a>)) {
6911 #[cfg(all(target_arch = "aarch64", target_endian = "little"))]
6912 unsafe {
6913 self.start_matching_btlazy2_neon(&mut handle_sequence)
6914 }
6915 #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
6916 {
6917 use crate::encoding::fastpath::{FastpathKernel, select_kernel};
6918 match select_kernel() {
6919 FastpathKernel::Avx2Bmi2 => unsafe {
6920 self.start_matching_btlazy2_avx2_bmi2(&mut handle_sequence)
6921 },
6922 FastpathKernel::Sse42 => unsafe {
6923 self.start_matching_btlazy2_sse42(&mut handle_sequence)
6924 },
6925 FastpathKernel::Scalar => self.start_matching_btlazy2_scalar(&mut handle_sequence),
6926 }
6927 }
6928 #[cfg(not(any(
6929 all(target_arch = "aarch64", target_endian = "little"),
6930 target_arch = "x86",
6931 target_arch = "x86_64"
6932 )))]
6933 {
6934 self.start_matching_btlazy2_scalar(&mut handle_sequence)
6935 }
6936 }
6937
6938 #[cfg(all(target_arch = "aarch64", target_endian = "little"))]
6939 #[target_feature(enable = "neon")]
6940 unsafe fn start_matching_btlazy2_neon(
6941 &mut self,
6942 mut handle_sequence: impl for<'a> FnMut(Sequence<'a>),
6943 ) {
6944 start_matching_btlazy2_body!(
6945 self,
6946 handle_sequence,
6947 collect_optimal_candidates_initialized_neon,
6948 crate::encoding::fastpath::neon::count_match_from_indices
6949 )
6950 }
6951
6952 #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
6953 #[target_feature(enable = "sse4.2")]
6954 unsafe fn start_matching_btlazy2_sse42(
6955 &mut self,
6956 mut handle_sequence: impl for<'a> FnMut(Sequence<'a>),
6957 ) {
6958 start_matching_btlazy2_body!(
6959 self,
6960 handle_sequence,
6961 collect_optimal_candidates_initialized_sse42,
6962 crate::encoding::fastpath::sse42::count_match_from_indices
6963 )
6964 }
6965
6966 #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
6967 #[target_feature(enable = "avx2,bmi2")]
6968 unsafe fn start_matching_btlazy2_avx2_bmi2(
6969 &mut self,
6970 mut handle_sequence: impl for<'a> FnMut(Sequence<'a>),
6971 ) {
6972 start_matching_btlazy2_body!(
6973 self,
6974 handle_sequence,
6975 collect_optimal_candidates_initialized_avx2_bmi2,
6976 crate::encoding::fastpath::avx2_bmi2::count_match_from_indices
6977 )
6978 }
6979
6980 #[cfg(not(all(target_arch = "aarch64", target_endian = "little")))]
6985 #[allow(unused_unsafe)]
6986 fn start_matching_btlazy2_scalar(
6987 &mut self,
6988 mut handle_sequence: impl for<'a> FnMut(Sequence<'a>),
6989 ) {
6990 start_matching_btlazy2_body!(
6991 self,
6992 handle_sequence,
6993 collect_optimal_candidates_initialized_scalar,
6994 crate::encoding::fastpath::scalar::count_match_from_indices
6995 )
6996 }
6997
6998 fn start_matching_optimal<S: super::strategy::Strategy>(
6999 &mut self,
7000 mut handle_sequence: impl for<'a> FnMut(Sequence<'a>),
7001 ) {
7002 self.table.ensure_tables();
7003 let (current_abs_start, current_len) = self.table.current_block_range();
7006 if current_len == 0 {
7007 return;
7008 }
7009 let current_ptr = self.table.get_last_space().as_ptr();
7010 let current = unsafe { core::slice::from_raw_parts(current_ptr, current_len) };
7014
7015 let current_abs_end = current_abs_start + current_len;
7016 self.table
7017 .apply_limited_update_after_long_match(current_abs_start);
7018 let hash3_start_cursor = self
7019 .table
7020 .skip_insert_until_abs
7021 .max(self.table.history_abs_start);
7022 self.table
7023 .backfill_boundary_positions(current_abs_start, current_abs_end);
7024 self.table.next_to_update3 = hash3_start_cursor;
7025 let live_history = self.table.live_history();
7040 let history_abs_start = self.table.history_abs_start;
7041 self.backend.bt_mut().prepare_ldm_candidates(
7042 live_history,
7043 history_abs_start,
7044 current_abs_start,
7045 current_len,
7046 );
7047
7048 if self.should_run_btultra2_seed_pass::<S>(current_len) {
7049 self.run_btultra2_seed_pass(current, current_abs_start, current_len);
7050 }
7051
7052 let profile = HcOptimalCostProfile::const_for_strategy::<S>();
7058 let mut opt_state =
7059 core::mem::replace(&mut self.backend.bt_mut().opt_state, HcOptState::new());
7060 opt_state.rescale_freqs(current, profile);
7061 let mut best_plan = core::mem::take(&mut self.backend.bt_mut().opt_segment_plan_scratch);
7062 best_plan.clear();
7063 let mut plan_reps = self.table.offset_hist;
7064 let (mut cursor, mut plan_litlen) =
7065 self.table.opt_start_cursor_and_litlen(current_abs_start);
7066 let mut plan_literals_cursor = 0usize;
7067 let match_loop_limit = current_len.saturating_sub(8);
7068 while cursor < match_loop_limit {
7069 let remaining_len = current_len - cursor;
7070 let segment_abs_start = current_abs_start + cursor;
7071 let segment_start = best_plan.len();
7072 let (_, end_reps, end_litlen, consumed_len) = self.build_optimal_plan::<S>(
7073 ¤t[cursor..],
7074 segment_abs_start,
7075 remaining_len,
7076 HcOptimalPlanState {
7077 block_offset: cursor,
7078 reps: plan_reps,
7079 litlen: plan_litlen,
7080 profile,
7081 },
7082 &opt_state,
7083 &mut best_plan,
7084 );
7085 BtMatcher::update_plan_stats_segment(
7086 current,
7087 current_len,
7088 &best_plan[segment_start..],
7089 &mut plan_literals_cursor,
7090 &mut plan_reps,
7091 &mut opt_state,
7092 profile.accurate,
7093 );
7094 plan_reps = end_reps;
7095 plan_litlen = end_litlen;
7096 cursor += consumed_len;
7097 }
7098
7099 self.table
7100 .emit_optimal_plan(current_len, &best_plan, &mut handle_sequence);
7101 best_plan.clear();
7102 self.backend.bt_mut().opt_segment_plan_scratch = best_plan;
7103 self.backend.bt_mut().opt_state = opt_state;
7104 }
7105
7106 fn run_btultra2_seed_pass(
7107 &mut self,
7108 current: &[u8],
7109 current_abs_start: usize,
7110 current_len: usize,
7111 ) {
7112 type S = super::strategy::BtUltra2;
7117 let seed_profile = HcOptimalCostProfile::const_for_strategy::<S>();
7118 let mut opt_state =
7119 core::mem::replace(&mut self.backend.bt_mut().opt_state, HcOptState::new());
7120 opt_state.rescale_freqs(current, seed_profile);
7121 let mut seed_reps = self.table.offset_hist;
7122 let (mut cursor, mut seed_litlen) =
7123 self.table.opt_start_cursor_and_litlen(current_abs_start);
7124 let mut seed_literals_cursor = 0usize;
7125 let mut seed_plan = core::mem::take(&mut self.backend.bt_mut().opt_seed_plan_scratch);
7126 seed_plan.clear();
7127 let match_loop_limit = current_len.saturating_sub(8);
7128 while cursor < match_loop_limit {
7129 let remaining_len = current_len - cursor;
7130 let segment_abs_start = current_abs_start + cursor;
7131 let segment_start = seed_plan.len();
7132 let (_, end_reps, end_litlen, consumed_len) = self.build_optimal_plan::<S>(
7133 ¤t[cursor..],
7134 segment_abs_start,
7135 remaining_len,
7136 HcOptimalPlanState {
7137 block_offset: cursor,
7138 reps: seed_reps,
7139 litlen: seed_litlen,
7140 profile: seed_profile,
7141 },
7142 &opt_state,
7143 &mut seed_plan,
7144 );
7145 BtMatcher::update_plan_stats_segment(
7146 current,
7147 current_len,
7148 &seed_plan[segment_start..],
7149 &mut seed_literals_cursor,
7150 &mut seed_reps,
7151 &mut opt_state,
7152 seed_profile.accurate,
7153 );
7154 seed_plan.truncate(segment_start);
7155 seed_reps = end_reps;
7156 seed_litlen = end_litlen;
7157 cursor += consumed_len;
7158 }
7159 seed_plan.clear();
7160 self.backend.bt_mut().opt_seed_plan_scratch = seed_plan;
7161 self.backend.bt_mut().opt_state = opt_state;
7162
7163 self.table.position_base = self.table.history_abs_start;
7166 self.table.index_shift = current_len;
7167 self.table.next_to_update3 = current_abs_start;
7168 self.table.skip_insert_until_abs = current_abs_start;
7169 self.table.allow_zero_relative_position = true;
7175 }
7176
7177 fn build_optimal_plan<S: super::strategy::Strategy>(
7178 &mut self,
7179 current: &[u8],
7180 current_abs_start: usize,
7181 current_len: usize,
7182 initial_state: HcOptimalPlanState,
7183 stats: &HcOptState,
7184 out: &mut Vec<HcOptimalSequence>,
7185 ) -> (u32, [u32; 3], usize, usize) {
7186 debug_assert!(S::USE_BT, "build_optimal_plan called on non-BT strategy");
7187 debug_assert_eq!(initial_state.profile.accurate, S::ACCURATE_PRICE);
7188 debug_assert_eq!(
7189 initial_state.profile.favor_small_offsets,
7190 S::FAVOR_SMALL_OFFSETS
7191 );
7192 match (S::ACCURATE_PRICE, S::FAVOR_SMALL_OFFSETS) {
7202 (true, false) => self.build_optimal_plan_impl::<S, true, false>(
7203 current,
7204 current_abs_start,
7205 current_len,
7206 initial_state,
7207 stats,
7208 out,
7209 ),
7210 (true, true) => self.build_optimal_plan_impl::<S, true, true>(
7211 current,
7212 current_abs_start,
7213 current_len,
7214 initial_state,
7215 stats,
7216 out,
7217 ),
7218 (false, false) => self.build_optimal_plan_impl::<S, false, false>(
7219 current,
7220 current_abs_start,
7221 current_len,
7222 initial_state,
7223 stats,
7224 out,
7225 ),
7226 (false, true) => self.build_optimal_plan_impl::<S, false, true>(
7227 current,
7228 current_abs_start,
7229 current_len,
7230 initial_state,
7231 stats,
7232 out,
7233 ),
7234 }
7235 }
7236
7237 #[inline(always)]
7246 fn build_optimal_plan_impl<
7247 S: super::strategy::Strategy,
7248 const ACCURATE_PRICE: bool,
7249 const FAVOR_SMALL_OFFSETS: bool,
7250 >(
7251 &mut self,
7252 current: &[u8],
7253 current_abs_start: usize,
7254 current_len: usize,
7255 initial_state: HcOptimalPlanState,
7256 stats: &HcOptState,
7257 out: &mut Vec<HcOptimalSequence>,
7258 ) -> (u32, [u32; 3], usize, usize) {
7259 #[cfg(all(target_arch = "aarch64", target_endian = "little"))]
7260 unsafe {
7261 self.build_optimal_plan_impl_neon::<S, ACCURATE_PRICE, FAVOR_SMALL_OFFSETS>(
7262 current,
7263 current_abs_start,
7264 current_len,
7265 initial_state,
7266 stats,
7267 out,
7268 )
7269 }
7270 #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
7271 {
7272 use crate::encoding::fastpath::{FastpathKernel, select_kernel};
7273 match select_kernel() {
7274 FastpathKernel::Avx2Bmi2 => unsafe {
7275 self.build_optimal_plan_impl_avx2_bmi2::<S, ACCURATE_PRICE, FAVOR_SMALL_OFFSETS>(
7276 current,
7277 current_abs_start,
7278 current_len,
7279 initial_state,
7280 stats,
7281 out,
7282 )
7283 },
7284 FastpathKernel::Sse42 => unsafe {
7285 self.build_optimal_plan_impl_sse42::<S, ACCURATE_PRICE, FAVOR_SMALL_OFFSETS>(
7286 current,
7287 current_abs_start,
7288 current_len,
7289 initial_state,
7290 stats,
7291 out,
7292 )
7293 },
7294 FastpathKernel::Scalar => self
7295 .build_optimal_plan_impl_scalar::<S, ACCURATE_PRICE, FAVOR_SMALL_OFFSETS>(
7296 current,
7297 current_abs_start,
7298 current_len,
7299 initial_state,
7300 stats,
7301 out,
7302 ),
7303 }
7304 }
7305 #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
7307 unsafe {
7308 self.build_optimal_plan_impl_simd128::<S, ACCURATE_PRICE, FAVOR_SMALL_OFFSETS>(
7309 current,
7310 current_abs_start,
7311 current_len,
7312 initial_state,
7313 stats,
7314 out,
7315 )
7316 }
7317 #[cfg(not(any(
7318 all(target_arch = "aarch64", target_endian = "little"),
7319 target_arch = "x86",
7320 target_arch = "x86_64",
7321 all(target_arch = "wasm32", target_feature = "simd128")
7322 )))]
7323 {
7324 self.build_optimal_plan_impl_scalar::<S, ACCURATE_PRICE, FAVOR_SMALL_OFFSETS>(
7325 current,
7326 current_abs_start,
7327 current_len,
7328 initial_state,
7329 stats,
7330 out,
7331 )
7332 }
7333 }
7334
7335 #[cfg(all(target_arch = "aarch64", target_endian = "little"))]
7339 #[target_feature(enable = "neon")]
7340 unsafe fn build_optimal_plan_impl_neon<
7341 S: super::strategy::Strategy,
7342 const ACCURATE_PRICE: bool,
7343 const FAVOR_SMALL_OFFSETS: bool,
7344 >(
7345 &mut self,
7346 current: &[u8],
7347 current_abs_start: usize,
7348 current_len: usize,
7349 initial_state: HcOptimalPlanState,
7350 stats: &HcOptState,
7351 out: &mut Vec<HcOptimalSequence>,
7352 ) -> (u32, [u32; 3], usize, usize) {
7353 build_optimal_plan_impl_body!(
7354 self,
7355 S,
7356 current,
7357 current_abs_start,
7358 current_len,
7359 initial_state,
7360 stats,
7361 out,
7362 collect_optimal_candidates_initialized_neon,
7363 priceset_range_nonabort_neon,
7364 )
7365 }
7366
7367 #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
7368 #[target_feature(enable = "sse4.2")]
7369 unsafe fn build_optimal_plan_impl_sse42<
7370 S: super::strategy::Strategy,
7371 const ACCURATE_PRICE: bool,
7372 const FAVOR_SMALL_OFFSETS: bool,
7373 >(
7374 &mut self,
7375 current: &[u8],
7376 current_abs_start: usize,
7377 current_len: usize,
7378 initial_state: HcOptimalPlanState,
7379 stats: &HcOptState,
7380 out: &mut Vec<HcOptimalSequence>,
7381 ) -> (u32, [u32; 3], usize, usize) {
7382 build_optimal_plan_impl_body!(
7383 self,
7384 S,
7385 current,
7386 current_abs_start,
7387 current_len,
7388 initial_state,
7389 stats,
7390 out,
7391 collect_optimal_candidates_initialized_sse42,
7392 priceset_range_nonabort_sse41,
7393 )
7394 }
7395
7396 #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
7397 #[target_feature(enable = "avx2,bmi2")]
7398 unsafe fn build_optimal_plan_impl_avx2_bmi2<
7399 S: super::strategy::Strategy,
7400 const ACCURATE_PRICE: bool,
7401 const FAVOR_SMALL_OFFSETS: bool,
7402 >(
7403 &mut self,
7404 current: &[u8],
7405 current_abs_start: usize,
7406 current_len: usize,
7407 initial_state: HcOptimalPlanState,
7408 stats: &HcOptState,
7409 out: &mut Vec<HcOptimalSequence>,
7410 ) -> (u32, [u32; 3], usize, usize) {
7411 build_optimal_plan_impl_body!(
7412 self,
7413 S,
7414 current,
7415 current_abs_start,
7416 current_len,
7417 initial_state,
7418 stats,
7419 out,
7420 collect_optimal_candidates_initialized_avx2_bmi2,
7421 priceset_range_nonabort_avx2,
7422 )
7423 }
7424
7425 #[cfg(not(all(target_arch = "aarch64", target_endian = "little")))]
7426 #[allow(unused_unsafe)]
7430 #[cfg_attr(
7434 all(target_arch = "wasm32", target_feature = "simd128"),
7435 allow(dead_code)
7436 )]
7437 fn build_optimal_plan_impl_scalar<
7438 S: super::strategy::Strategy,
7439 const ACCURATE_PRICE: bool,
7440 const FAVOR_SMALL_OFFSETS: bool,
7441 >(
7442 &mut self,
7443 current: &[u8],
7444 current_abs_start: usize,
7445 current_len: usize,
7446 initial_state: HcOptimalPlanState,
7447 stats: &HcOptState,
7448 out: &mut Vec<HcOptimalSequence>,
7449 ) -> (u32, [u32; 3], usize, usize) {
7450 build_optimal_plan_impl_body!(
7451 self,
7452 S,
7453 current,
7454 current_abs_start,
7455 current_len,
7456 initial_state,
7457 stats,
7458 out,
7459 collect_optimal_candidates_initialized_scalar,
7460 priceset_range_nonabort_scalar,
7461 )
7462 }
7463
7464 #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
7467 #[target_feature(enable = "simd128")]
7468 #[allow(unused_unsafe)]
7472 unsafe fn build_optimal_plan_impl_simd128<
7473 S: super::strategy::Strategy,
7474 const ACCURATE_PRICE: bool,
7475 const FAVOR_SMALL_OFFSETS: bool,
7476 >(
7477 &mut self,
7478 current: &[u8],
7479 current_abs_start: usize,
7480 current_len: usize,
7481 initial_state: HcOptimalPlanState,
7482 stats: &HcOptState,
7483 out: &mut Vec<HcOptimalSequence>,
7484 ) -> (u32, [u32; 3], usize, usize) {
7485 build_optimal_plan_impl_body!(
7486 self,
7487 S,
7488 current,
7489 current_abs_start,
7490 current_len,
7491 initial_state,
7492 stats,
7493 out,
7494 collect_optimal_candidates_initialized_scalar,
7495 priceset_range_nonabort_simd128,
7496 )
7497 }
7498
7499 #[cfg(test)]
7500 fn collect_optimal_candidates(
7501 &mut self,
7502 abs_pos: usize,
7503 current_abs_end: usize,
7504 profile: HcOptimalCostProfile,
7505 query: HcCandidateQuery,
7506 out: &mut Vec<MatchCandidate>,
7507 ) {
7508 use super::strategy::{self, StrategyTag};
7509 self.table.ensure_tables();
7510 match self.strategy_tag {
7516 StrategyTag::BtUltra2 => self
7517 .collect_optimal_candidates_initialized::<strategy::BtUltra2, true>(
7518 abs_pos,
7519 current_abs_end,
7520 profile,
7521 query,
7522 out,
7523 ),
7524 StrategyTag::BtUltra => self
7525 .collect_optimal_candidates_initialized::<strategy::BtUltra, true>(
7526 abs_pos,
7527 current_abs_end,
7528 profile,
7529 query,
7530 out,
7531 ),
7532 StrategyTag::Btlazy2 => self
7533 .collect_optimal_candidates_initialized::<strategy::Btlazy2, true>(
7534 abs_pos,
7535 current_abs_end,
7536 profile,
7537 query,
7538 out,
7539 ),
7540 StrategyTag::BtOpt => self
7541 .collect_optimal_candidates_initialized::<strategy::BtOpt, true>(
7542 abs_pos,
7543 current_abs_end,
7544 profile,
7545 query,
7546 out,
7547 ),
7548 StrategyTag::Fast | StrategyTag::Dfast | StrategyTag::Greedy | StrategyTag::Lazy => {
7549 self.collect_optimal_candidates_initialized::<strategy::Lazy, false>(
7550 abs_pos,
7551 current_abs_end,
7552 profile,
7553 query,
7554 out,
7555 )
7556 }
7557 }
7558 }
7559
7560 #[allow(dead_code)]
7570 #[inline(always)]
7571 fn collect_optimal_candidates_initialized<
7572 S: super::strategy::Strategy,
7573 const USE_BT_MATCHFINDER: bool,
7574 >(
7575 &mut self,
7576 abs_pos: usize,
7577 current_abs_end: usize,
7578 profile: HcOptimalCostProfile,
7579 query: HcCandidateQuery,
7580 out: &mut Vec<MatchCandidate>,
7581 ) {
7582 #[cfg(all(target_arch = "aarch64", target_endian = "little"))]
7583 unsafe {
7584 self.collect_optimal_candidates_initialized_neon::<S, USE_BT_MATCHFINDER>(
7585 abs_pos,
7586 current_abs_end,
7587 profile,
7588 query,
7589 out,
7590 )
7591 }
7592 #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
7593 {
7594 use crate::encoding::fastpath::{FastpathKernel, select_kernel};
7595 match select_kernel() {
7596 FastpathKernel::Avx2Bmi2 => unsafe {
7597 self.collect_optimal_candidates_initialized_avx2_bmi2::<S, USE_BT_MATCHFINDER>(
7598 abs_pos,
7599 current_abs_end,
7600 profile,
7601 query,
7602 out,
7603 )
7604 },
7605 FastpathKernel::Sse42 => unsafe {
7606 self.collect_optimal_candidates_initialized_sse42::<S, USE_BT_MATCHFINDER>(
7607 abs_pos,
7608 current_abs_end,
7609 profile,
7610 query,
7611 out,
7612 )
7613 },
7614 FastpathKernel::Scalar => self
7615 .collect_optimal_candidates_initialized_scalar::<S, USE_BT_MATCHFINDER>(
7616 abs_pos,
7617 current_abs_end,
7618 profile,
7619 query,
7620 out,
7621 ),
7622 }
7623 }
7624 #[cfg(not(any(
7625 all(target_arch = "aarch64", target_endian = "little"),
7626 target_arch = "x86",
7627 target_arch = "x86_64"
7628 )))]
7629 {
7630 self.collect_optimal_candidates_initialized_scalar::<S, USE_BT_MATCHFINDER>(
7631 abs_pos,
7632 current_abs_end,
7633 profile,
7634 query,
7635 out,
7636 )
7637 }
7638 }
7639
7640 #[cfg(all(target_arch = "aarch64", target_endian = "little"))]
7646 #[target_feature(enable = "neon")]
7647 unsafe fn collect_optimal_candidates_initialized_neon<
7648 S: super::strategy::Strategy,
7649 const USE_BT_MATCHFINDER: bool,
7650 >(
7651 &mut self,
7652 abs_pos: usize,
7653 current_abs_end: usize,
7654 profile: HcOptimalCostProfile,
7655 query: HcCandidateQuery,
7656 out: &mut Vec<MatchCandidate>,
7657 ) {
7658 collect_optimal_candidates_initialized_body!(
7659 self,
7660 S,
7661 abs_pos,
7662 current_abs_end,
7663 profile,
7664 query,
7665 out,
7666 USE_BT_MATCHFINDER,
7667 bt_update_tree_until_neon,
7668 bt_insert_and_collect_matches_neon,
7669 for_each_repcode_candidate_with_reps_neon,
7670 hash3_candidate_neon,
7671 crate::encoding::fastpath::neon::common_prefix_len_ptr,
7672 )
7673 }
7674
7675 #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
7676 #[target_feature(enable = "sse4.2")]
7677 unsafe fn collect_optimal_candidates_initialized_sse42<
7678 S: super::strategy::Strategy,
7679 const USE_BT_MATCHFINDER: bool,
7680 >(
7681 &mut self,
7682 abs_pos: usize,
7683 current_abs_end: usize,
7684 profile: HcOptimalCostProfile,
7685 query: HcCandidateQuery,
7686 out: &mut Vec<MatchCandidate>,
7687 ) {
7688 collect_optimal_candidates_initialized_body!(
7689 self,
7690 S,
7691 abs_pos,
7692 current_abs_end,
7693 profile,
7694 query,
7695 out,
7696 USE_BT_MATCHFINDER,
7697 bt_update_tree_until_sse42,
7698 bt_insert_and_collect_matches_sse42,
7699 for_each_repcode_candidate_with_reps_sse42,
7700 hash3_candidate_sse42,
7701 crate::encoding::fastpath::sse42::common_prefix_len_ptr,
7702 )
7703 }
7704
7705 #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
7706 #[target_feature(enable = "avx2,bmi2")]
7707 unsafe fn collect_optimal_candidates_initialized_avx2_bmi2<
7708 S: super::strategy::Strategy,
7709 const USE_BT_MATCHFINDER: bool,
7710 >(
7711 &mut self,
7712 abs_pos: usize,
7713 current_abs_end: usize,
7714 profile: HcOptimalCostProfile,
7715 query: HcCandidateQuery,
7716 out: &mut Vec<MatchCandidate>,
7717 ) {
7718 collect_optimal_candidates_initialized_body!(
7719 self,
7720 S,
7721 abs_pos,
7722 current_abs_end,
7723 profile,
7724 query,
7725 out,
7726 USE_BT_MATCHFINDER,
7727 bt_update_tree_until_avx2_bmi2,
7728 bt_insert_and_collect_matches_avx2_bmi2,
7729 for_each_repcode_candidate_with_reps_avx2_bmi2,
7730 hash3_candidate_avx2_bmi2,
7731 crate::encoding::fastpath::avx2_bmi2::common_prefix_len_ptr,
7732 )
7733 }
7734
7735 #[cfg(not(all(target_arch = "aarch64", target_endian = "little")))]
7736 #[allow(unused_unsafe)]
7739 fn collect_optimal_candidates_initialized_scalar<
7740 S: super::strategy::Strategy,
7741 const USE_BT_MATCHFINDER: bool,
7742 >(
7743 &mut self,
7744 abs_pos: usize,
7745 current_abs_end: usize,
7746 profile: HcOptimalCostProfile,
7747 query: HcCandidateQuery,
7748 out: &mut Vec<MatchCandidate>,
7749 ) {
7750 collect_optimal_candidates_initialized_body!(
7751 self,
7752 S,
7753 abs_pos,
7754 current_abs_end,
7755 profile,
7756 query,
7757 out,
7758 USE_BT_MATCHFINDER,
7759 bt_update_tree_until_scalar,
7760 bt_insert_and_collect_matches_scalar,
7761 for_each_repcode_candidate_with_reps_scalar,
7762 hash3_candidate_scalar,
7763 crate::encoding::fastpath::scalar::common_prefix_len_ptr,
7764 )
7765 }
7766}
7767
7768#[cfg(any())] #[test]
7770fn matches() {
7771 let mut matcher = MatchGenerator::new(1000);
7772 let mut original_data = Vec::new();
7773 let mut reconstructed = Vec::new();
7774
7775 let replay_sequence = |seq: Sequence<'_>, reconstructed: &mut Vec<u8>| match seq {
7776 Sequence::Literals { literals } => {
7777 assert!(!literals.is_empty());
7778 reconstructed.extend_from_slice(literals);
7779 }
7780 Sequence::Triple {
7781 literals,
7782 offset,
7783 match_len,
7784 } => {
7785 assert!(offset > 0);
7786 assert!(match_len >= MIN_MATCH_LEN);
7787 reconstructed.extend_from_slice(literals);
7788 assert!(offset <= reconstructed.len());
7789 let start = reconstructed.len() - offset;
7790 for i in 0..match_len {
7791 let byte = reconstructed[start + i];
7792 reconstructed.push(byte);
7793 }
7794 }
7795 };
7796
7797 matcher.add_data(
7798 alloc::vec![0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
7799 SuffixStore::with_capacity(100),
7800 |_, _| {},
7801 );
7802 original_data.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
7803
7804 matcher.next_sequence(|seq| replay_sequence(seq, &mut reconstructed));
7805
7806 assert!(!matcher.next_sequence(|_| {}));
7807
7808 matcher.add_data(
7809 alloc::vec![
7810 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 0, 0, 0, 0, 0,
7811 ],
7812 SuffixStore::with_capacity(100),
7813 |_, _| {},
7814 );
7815 original_data.extend_from_slice(&[
7816 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 0, 0, 0, 0, 0,
7817 ]);
7818
7819 matcher.next_sequence(|seq| replay_sequence(seq, &mut reconstructed));
7820 matcher.next_sequence(|seq| replay_sequence(seq, &mut reconstructed));
7821 matcher.next_sequence(|seq| replay_sequence(seq, &mut reconstructed));
7822 assert!(!matcher.next_sequence(|_| {}));
7823
7824 matcher.add_data(
7825 alloc::vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 0, 0, 0, 0],
7826 SuffixStore::with_capacity(100),
7827 |_, _| {},
7828 );
7829 original_data.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 0, 0, 0, 0]);
7830
7831 matcher.next_sequence(|seq| replay_sequence(seq, &mut reconstructed));
7832 matcher.next_sequence(|seq| replay_sequence(seq, &mut reconstructed));
7833 assert!(!matcher.next_sequence(|_| {}));
7834
7835 matcher.add_data(
7836 alloc::vec![0, 0, 0, 0, 0],
7837 SuffixStore::with_capacity(100),
7838 |_, _| {},
7839 );
7840 original_data.extend_from_slice(&[0, 0, 0, 0, 0]);
7841
7842 matcher.next_sequence(|seq| replay_sequence(seq, &mut reconstructed));
7843 assert!(!matcher.next_sequence(|_| {}));
7844
7845 matcher.add_data(
7846 alloc::vec![7, 8, 9, 10, 11],
7847 SuffixStore::with_capacity(100),
7848 |_, _| {},
7849 );
7850 original_data.extend_from_slice(&[7, 8, 9, 10, 11]);
7851
7852 matcher.next_sequence(|seq| replay_sequence(seq, &mut reconstructed));
7853 assert!(!matcher.next_sequence(|_| {}));
7854
7855 matcher.add_data(
7856 alloc::vec![1, 3, 5, 7, 9],
7857 SuffixStore::with_capacity(100),
7858 |_, _| {},
7859 );
7860 matcher.skip_matching();
7861 original_data.extend_from_slice(&[1, 3, 5, 7, 9]);
7862 reconstructed.extend_from_slice(&[1, 3, 5, 7, 9]);
7863 assert!(!matcher.next_sequence(|_| {}));
7864
7865 matcher.add_data(
7866 alloc::vec![1, 3, 5, 7, 9],
7867 SuffixStore::with_capacity(100),
7868 |_, _| {},
7869 );
7870 original_data.extend_from_slice(&[1, 3, 5, 7, 9]);
7871
7872 matcher.next_sequence(|seq| replay_sequence(seq, &mut reconstructed));
7873 assert!(!matcher.next_sequence(|_| {}));
7874
7875 matcher.add_data(
7876 alloc::vec![0, 0, 11, 13, 15, 17, 20, 11, 13, 15, 17, 20, 21, 23],
7877 SuffixStore::with_capacity(100),
7878 |_, _| {},
7879 );
7880 original_data.extend_from_slice(&[0, 0, 11, 13, 15, 17, 20, 11, 13, 15, 17, 20, 21, 23]);
7881
7882 matcher.next_sequence(|seq| replay_sequence(seq, &mut reconstructed));
7883 matcher.next_sequence(|seq| replay_sequence(seq, &mut reconstructed));
7884 assert!(!matcher.next_sequence(|_| {}));
7885
7886 assert_eq!(reconstructed, original_data);
7887}
7888
7889#[test]
7890fn dfast_matches_roundtrip_multi_block_pattern() {
7891 let pattern = [9, 21, 44, 184, 19, 96, 171, 109, 141, 251];
7892 let first_block: Vec<u8> = pattern.iter().copied().cycle().take(128 * 1024).collect();
7893 let second_block: Vec<u8> = pattern.iter().copied().cycle().take(128 * 1024).collect();
7894
7895 let mut matcher = DfastMatchGenerator::new(1 << 22);
7896 let replay_sequence = |decoded: &mut Vec<u8>, seq: Sequence<'_>| match seq {
7897 Sequence::Literals { literals } => decoded.extend_from_slice(literals),
7898 Sequence::Triple {
7899 literals,
7900 offset,
7901 match_len,
7902 } => {
7903 decoded.extend_from_slice(literals);
7904 let start = decoded.len() - offset;
7905 for i in 0..match_len {
7906 let byte = decoded[start + i];
7907 decoded.push(byte);
7908 }
7909 }
7910 };
7911
7912 matcher.add_data(first_block.clone(), |_| {});
7913 let mut history = Vec::new();
7914 matcher.start_matching(|seq| replay_sequence(&mut history, seq));
7915 assert_eq!(history, first_block);
7916
7917 matcher.add_data(second_block.clone(), |_| {});
7918 let prefix_len = history.len();
7919 matcher.start_matching(|seq| replay_sequence(&mut history, seq));
7920
7921 assert_eq!(&history[prefix_len..], second_block.as_slice());
7922}
7923
7924#[test]
7941fn dfast_accepts_exact_five_byte_match() {
7942 let mut data = Vec::new();
7956 data.push(b'Z'); data.extend_from_slice(b"ABCDE"); data.extend_from_slice(b"!!!!!!!!!!!!!!!!!!!!!!!"); data.extend_from_slice(b"ABCDE"); data.push(b'F'); data.extend_from_slice(b"GHIJKLMNOPQRSTUVWXYZ"); assert_eq!(data.len(), 55);
7967
7968 let mut matcher = DfastMatchGenerator::new(1 << 22);
7969 matcher.add_data(data.clone(), |_| {});
7970
7971 let mut saw_five_byte_match = false;
7972 let mut saw_longer_match = false;
7973 matcher.start_matching(|seq| {
7974 if let Sequence::Triple {
7975 offset, match_len, ..
7976 } = seq
7977 {
7978 if offset == 28 && match_len == 5 {
7979 saw_five_byte_match = true;
7980 } else if offset == 28 && match_len > 5 {
7981 saw_longer_match = true;
7982 }
7983 }
7984 });
7985
7986 assert!(
7987 saw_five_byte_match,
7988 "dfast must accept the exact-5-byte match — a 6-byte floor would skip it"
7989 );
7990 assert!(
7991 !saw_longer_match,
7992 "fixture pinned to length 5 — byte 33 ('F') must terminate the extension"
7993 );
7994}
7995
7996#[test]
7997fn driver_switches_backends_and_initializes_dfast_via_reset() {
7998 let mut driver = MatchGeneratorDriver::new(32, 2);
7999
8000 driver.reset(CompressionLevel::Default);
8001 assert_eq!(driver.active_backend(), super::strategy::BackendTag::Dfast);
8002 assert_eq!(driver.window_size(), (1u64 << 21));
8003
8004 let mut first = driver.get_next_space();
8005 first[..12].copy_from_slice(b"abcabcabcabc");
8006 first.truncate(12);
8007 driver.commit_space(first);
8008 assert_eq!(driver.get_last_space(), b"abcabcabcabc");
8009 driver.skip_matching_with_hint(None);
8010
8011 let mut second = driver.get_next_space();
8012 second[..12].copy_from_slice(b"abcabcabcabc");
8013 second.truncate(12);
8014 driver.commit_space(second);
8015
8016 let mut reconstructed = b"abcabcabcabc".to_vec();
8017 driver.start_matching(|seq| match seq {
8018 Sequence::Literals { literals } => reconstructed.extend_from_slice(literals),
8019 Sequence::Triple {
8020 literals,
8021 offset,
8022 match_len,
8023 } => {
8024 reconstructed.extend_from_slice(literals);
8025 let start = reconstructed.len() - offset;
8026 for i in 0..match_len {
8027 let byte = reconstructed[start + i];
8028 reconstructed.push(byte);
8029 }
8030 }
8031 });
8032 assert_eq!(reconstructed, b"abcabcabcabcabcabcabcabc");
8033
8034 driver.reset(CompressionLevel::Fastest);
8035 assert_eq!(driver.window_size(), (1u64 << 19));
8036}
8037
8038#[test]
8039fn driver_level5_selects_row_backend() {
8040 let mut driver = MatchGeneratorDriver::new(32, 2);
8041 driver.reset(CompressionLevel::Level(5));
8042 assert_eq!(driver.active_backend(), super::strategy::BackendTag::Row);
8043 assert_eq!(
8051 driver.parse,
8052 super::strategy::ParseMode::Greedy,
8053 "L5 must route to start_matching_greedy (parse == Greedy)",
8054 );
8055 assert_eq!(
8056 driver.row_matcher().lazy_depth,
8057 0,
8058 "row matcher lazy_depth must mirror the greedy parse mode",
8059 );
8060}
8061
8062#[test]
8070fn driver_level4_greedy_round_trip_single_slice() {
8071 let mut driver = MatchGeneratorDriver::new(64, 2);
8072 driver.reset(CompressionLevel::Level(4));
8073 let input = b"abcdefgh_abcdefgh_abcdefgh_abcdefgh";
8074 let mut space = driver.get_next_space();
8075 space[..input.len()].copy_from_slice(input);
8076 space.truncate(input.len());
8077 driver.commit_space(space);
8078
8079 let mut reconstructed: Vec<u8> = Vec::new();
8080 let mut saw_triple = false;
8081 driver.start_matching(|seq| match seq {
8082 Sequence::Literals { literals } => reconstructed.extend_from_slice(literals),
8083 Sequence::Triple {
8084 literals,
8085 offset,
8086 match_len,
8087 } => {
8088 saw_triple = true;
8089 reconstructed.extend_from_slice(literals);
8090 let start = reconstructed.len() - offset;
8091 for i in 0..match_len {
8092 let byte = reconstructed[start + i];
8093 reconstructed.push(byte);
8094 }
8095 }
8096 });
8097 assert_eq!(
8098 reconstructed,
8099 input.to_vec(),
8100 "L4 greedy parse failed to reconstruct repeating-pattern input",
8101 );
8102 assert!(
8103 saw_triple,
8104 "L4 greedy parse on a repeating pattern must emit at least one match (Triple)",
8105 );
8106}
8107
8108#[test]
8109fn driver_level4_greedy_round_trip_cross_slice() {
8110 let mut driver = MatchGeneratorDriver::new(32, 4);
8115 driver.reset(CompressionLevel::Level(4));
8116 let chunk = b"the quick brown fox jumps over!!";
8117 assert_eq!(chunk.len(), 32);
8118
8119 let mut first = driver.get_next_space();
8120 first[..chunk.len()].copy_from_slice(chunk);
8121 first.truncate(chunk.len());
8122 driver.commit_space(first);
8123
8124 let mut first_recon: Vec<u8> = Vec::new();
8125 driver.start_matching(|seq| match seq {
8126 Sequence::Literals { literals } => first_recon.extend_from_slice(literals),
8127 Sequence::Triple {
8128 literals,
8129 offset,
8130 match_len,
8131 } => {
8132 first_recon.extend_from_slice(literals);
8133 let start = first_recon.len() - offset;
8134 for i in 0..match_len {
8135 let byte = first_recon[start + i];
8136 first_recon.push(byte);
8137 }
8138 }
8139 });
8140 assert_eq!(
8141 first_recon,
8142 chunk.to_vec(),
8143 "first slice failed to round-trip"
8144 );
8145
8146 let mut second = driver.get_next_space();
8147 second[..chunk.len()].copy_from_slice(chunk);
8148 second.truncate(chunk.len());
8149 driver.commit_space(second);
8150
8151 let mut full = first_recon.clone();
8152 let mut saw_cross_slice_match = false;
8153 driver.start_matching(|seq| match seq {
8154 Sequence::Literals { literals } => full.extend_from_slice(literals),
8155 Sequence::Triple {
8156 literals,
8157 offset,
8158 match_len,
8159 } => {
8160 if offset >= chunk.len() {
8164 saw_cross_slice_match = true;
8165 }
8166 full.extend_from_slice(literals);
8167 let start = full.len() - offset;
8168 for i in 0..match_len {
8169 let byte = full[start + i];
8170 full.push(byte);
8171 }
8172 }
8173 });
8174 let mut expected = chunk.to_vec();
8175 expected.extend_from_slice(chunk);
8176 assert_eq!(
8177 full, expected,
8178 "cross-slice L4 greedy parse failed to reconstruct"
8179 );
8180 assert!(
8181 saw_cross_slice_match,
8182 "L4 greedy parse must match across slice boundaries (history is shared)",
8183 );
8184}
8185
8186#[cfg(test)]
8190impl MatchGeneratorDriver {
8191 pub(crate) fn set_config_override(
8195 &mut self,
8196 search: super::strategy::SearchMethod,
8197 parse: super::strategy::ParseMode,
8198 ) {
8199 self.config_override = Some((search, parse));
8200 }
8201
8202 pub(crate) fn reset_on_hc_lazy(&mut self, level: CompressionLevel) {
8207 self.set_config_override(
8208 super::strategy::SearchMethod::HashChain,
8209 super::strategy::ParseMode::Lazy2,
8210 );
8211 self.reset(level);
8212 }
8213}
8214
8215#[cfg(test)]
8219fn drive_roundtrip_with_override(
8220 level: CompressionLevel,
8221 over: Option<(super::strategy::SearchMethod, super::strategy::ParseMode)>,
8222 data: &[u8],
8223) -> Vec<u8> {
8224 let mut driver = MatchGeneratorDriver::new(1 << 17, 8);
8225 if let Some((s, p)) = over {
8226 driver.set_config_override(s, p);
8227 }
8228 driver.reset(level);
8229
8230 let mut out: Vec<u8> = Vec::with_capacity(data.len());
8231 let mut offset_in_data = 0usize;
8232 while offset_in_data < data.len() {
8233 let mut space = driver.get_next_space();
8234 let take = (data.len() - offset_in_data).min(space.len());
8235 space[..take].copy_from_slice(&data[offset_in_data..offset_in_data + take]);
8236 space.truncate(take);
8237 driver.commit_space(space);
8238 offset_in_data += take;
8239
8240 driver.start_matching(|seq| match seq {
8241 Sequence::Literals { literals } => out.extend_from_slice(literals),
8242 Sequence::Triple {
8243 literals,
8244 offset,
8245 match_len,
8246 } => {
8247 out.extend_from_slice(literals);
8248 let start = out.len() - offset;
8249 for i in 0..match_len {
8250 let byte = out[start + i];
8251 out.push(byte);
8252 }
8253 }
8254 });
8255 }
8256 out
8257}
8258
8259#[test]
8264fn parse_search_matrix_decoupled_roundtrips() {
8265 use super::strategy::{ParseMode, SearchMethod};
8266 let mut data = Vec::new();
8268 for i in 0..4000u32 {
8269 data.extend_from_slice(b"the quick brown fox ");
8270 data.extend_from_slice(&i.to_le_bytes());
8271 }
8272
8273 let got = drive_roundtrip_with_override(
8276 CompressionLevel::Level(5),
8277 Some((SearchMethod::HashChain, ParseMode::Greedy)),
8278 &data,
8279 );
8280 assert_eq!(got, data, "greedy-on-hashchain diverged");
8281
8282 let got = drive_roundtrip_with_override(
8285 CompressionLevel::Level(8),
8286 Some((SearchMethod::RowHash, ParseMode::Lazy2)),
8287 &data,
8288 );
8289 assert_eq!(got, data, "lazy2-on-rowhash diverged");
8290
8291 let got = drive_roundtrip_with_override(
8293 CompressionLevel::Level(6),
8294 Some((SearchMethod::RowHash, ParseMode::Lazy)),
8295 &data,
8296 );
8297 assert_eq!(got, data, "lazy-on-rowhash diverged");
8298}
8299
8300#[test]
8305fn row_mls_knob_gates_matches_and_roundtrips() {
8306 let data: Vec<u8> = (0..4000u32)
8307 .flat_map(|i| {
8308 let mut v = b"abcdefgh".to_vec();
8309 v.extend_from_slice(&i.to_le_bytes());
8310 v
8311 })
8312 .collect();
8313
8314 for mls in [4usize, 5, 6, 7] {
8315 let mut matcher = RowMatchGenerator::new(1 << 22);
8316 let mut cfg = ROW_CONFIG;
8317 cfg.mls = mls;
8318 matcher.configure(cfg);
8319 matcher.add_data(data.clone(), |_| {});
8320
8321 let mut out: Vec<u8> = Vec::with_capacity(data.len());
8322 let mut shortest_match = usize::MAX;
8323 matcher.start_matching(|seq| match seq {
8324 Sequence::Literals { literals } => out.extend_from_slice(literals),
8325 Sequence::Triple {
8326 literals,
8327 offset,
8328 match_len,
8329 } => {
8330 out.extend_from_slice(literals);
8331 shortest_match = shortest_match.min(match_len);
8332 let start = out.len() - offset;
8333 for i in 0..match_len {
8334 let byte = out[start + i];
8335 out.push(byte);
8336 }
8337 }
8338 });
8339
8340 assert_eq!(out, data, "mls={mls} round-trip diverged");
8341 if shortest_match != usize::MAX {
8342 assert!(
8343 shortest_match >= mls,
8344 "mls={mls}: emitted a {shortest_match}-byte match below the floor",
8345 );
8346 }
8347 }
8348}
8349
8350#[test]
8356fn parse_mode_follows_search_axis_not_strategy_tag() {
8357 use super::strategy::{ParseMode, SearchMethod};
8358 let mut p = LEVEL_TABLE[15];
8360 assert_eq!(p.parse(), ParseMode::Optimal, "BinaryTree search → Optimal");
8361 p.search = SearchMethod::RowHash;
8364 p.lazy_depth = 0;
8365 assert_eq!(p.parse(), ParseMode::Greedy, "RowHash + depth 0 → Greedy");
8366 p.lazy_depth = 2;
8367 assert_eq!(p.parse(), ParseMode::Lazy2, "RowHash + depth 2 → Lazy2");
8368}
8369
8370#[test]
8375fn config_override_is_consumed_by_reset() {
8376 use super::strategy::{ParseMode, SearchMethod};
8377 let mut driver = MatchGeneratorDriver::new(1 << 17, 8);
8378 driver.set_config_override(SearchMethod::RowHash, ParseMode::Lazy2);
8379 assert!(driver.config_override.is_some());
8380 driver.reset(CompressionLevel::Level(5));
8381 assert!(
8382 driver.config_override.is_none(),
8383 "override must be consumed after one reset",
8384 );
8385}
8386
8387#[cfg(test)]
8392fn l4_greedy_round_trip(slice_size: usize, max_slices: usize, data: &[u8]) -> (usize, usize) {
8393 let mut driver = MatchGeneratorDriver::new(slice_size, max_slices);
8394 driver.reset(CompressionLevel::Level(4));
8395
8396 let mut reconstructed: Vec<u8> = Vec::with_capacity(data.len());
8397 let mut triple_count = 0usize;
8398 let mut max_offset = 0usize;
8399
8400 let mut offset_in_data = 0usize;
8405 while offset_in_data < data.len() {
8406 let mut space = driver.get_next_space();
8407 let space_cap = space.len();
8408 let take = (data.len() - offset_in_data).min(space_cap);
8409 space[..take].copy_from_slice(&data[offset_in_data..offset_in_data + take]);
8410 space.truncate(take);
8411 driver.commit_space(space);
8412 offset_in_data += take;
8413
8414 driver.start_matching(|seq| match seq {
8415 Sequence::Literals { literals } => reconstructed.extend_from_slice(literals),
8416 Sequence::Triple {
8417 literals,
8418 offset,
8419 match_len,
8420 } => {
8421 triple_count += 1;
8422 if offset > max_offset {
8423 max_offset = offset;
8424 }
8425 reconstructed.extend_from_slice(literals);
8426 let start = reconstructed.len() - offset;
8427 for i in 0..match_len {
8428 let byte = reconstructed[start + i];
8429 reconstructed.push(byte);
8430 }
8431 }
8432 });
8433 }
8434
8435 if data.is_empty() {
8439 let mut space = driver.get_next_space();
8440 space.truncate(0);
8441 driver.commit_space(space);
8442 driver.start_matching(|seq| match seq {
8443 Sequence::Literals { literals } => reconstructed.extend_from_slice(literals),
8444 Sequence::Triple { .. } => panic!("empty input must not emit any matches"),
8445 });
8446 }
8447
8448 assert_eq!(reconstructed, data, "L4 greedy round-trip diverged");
8449 (triple_count, max_offset)
8450}
8451
8452#[test]
8463fn driver_level5_greedy_tail_rep_only_reachable() {
8464 let first: &[u8] = b"ABCDABCDABCDABCD"; let second: &[u8] = b"ABCDA"; let mut driver = MatchGeneratorDriver::new(16, 2);
8479 driver.reset(CompressionLevel::Level(5));
8480
8481 let mut first_space = driver.get_next_space();
8482 first_space[..first.len()].copy_from_slice(first);
8483 first_space.truncate(first.len());
8484 driver.commit_space(first_space);
8485 driver.start_matching(|_| {});
8486
8487 let mut second_space = driver.get_next_space();
8488 second_space[..second.len()].copy_from_slice(second);
8489 second_space.truncate(second.len());
8490 driver.commit_space(second_space);
8491
8492 let mut second_slice_triples = 0usize;
8493 driver.start_matching(|seq| {
8494 if matches!(seq, Sequence::Triple { .. }) {
8495 second_slice_triples += 1;
8496 }
8497 });
8498
8499 assert!(
8500 second_slice_triples >= 1,
8501 "tail rep-only position must produce a match in the second slice \
8502 (got {second_slice_triples} triples)",
8503 );
8504}
8505
8506#[test]
8507fn driver_level4_greedy_empty_input_emits_nothing() {
8508 let mut driver = MatchGeneratorDriver::new(64, 2);
8512 driver.reset(CompressionLevel::Level(4));
8513 let mut space = driver.get_next_space();
8518 space.truncate(0);
8519 driver.commit_space(space);
8520 let mut emitted_anything = false;
8521 driver.start_matching(|_| emitted_anything = true);
8522 assert!(!emitted_anything, "empty slice must not emit any sequences",);
8523}
8524
8525#[test]
8526fn driver_level4_greedy_sub_min_lookahead_input() {
8527 let data: &[u8] = b"abcd"; let (triples, _) = l4_greedy_round_trip(64, 2, data);
8532 assert_eq!(
8533 triples, 0,
8534 "sub-min-lookahead input must not emit any matches (got {triples})",
8535 );
8536}
8537
8538#[test]
8539fn driver_level4_greedy_incompressible_input() {
8540 let mut data = alloc::vec::Vec::with_capacity(256);
8545 let mut x: u32 = 0xDEAD_BEEF;
8546 for _ in 0..256 {
8547 x = x.wrapping_mul(1_103_515_245).wrapping_add(12345);
8548 data.push((x >> 16) as u8);
8549 }
8550 let (_triples, _) = l4_greedy_round_trip(64, 8, &data);
8551 }
8554
8555#[test]
8556fn driver_level4_greedy_long_literal_run_skip_step_growth() {
8557 let mut data = alloc::vec::Vec::with_capacity(2048);
8572 let mut x: u32 = 0xC0FF_EE00;
8573 for _ in 0..2048 {
8574 x = x.wrapping_mul(0x9E37_79B9).wrapping_add(0xCAFEBABE);
8575 data.push((x >> 24) as u8);
8576 }
8577 let (_triples, _) = l4_greedy_round_trip(512, 8, &data);
8578}
8579
8580#[test]
8581fn driver_level4_greedy_all_zeros_heavy_rep1() {
8582 let data: Vec<u8> = alloc::vec![0u8; 128];
8587 let (triples, max_offset) = l4_greedy_round_trip(64, 8, &data);
8588 assert!(
8589 triples >= 1,
8590 "all-zeros input must produce at least one rep1 match",
8591 );
8592 assert_eq!(
8596 max_offset, 1,
8597 "all-zeros L4 greedy parse should commit at offset 1 (got {max_offset})",
8598 );
8599}
8600
8601#[test]
8607fn driver_level4_greedy_periodic_pattern_rep_cascade() {
8608 let unit: &[u8] = b"alpha_beta_gamma";
8609 assert_eq!(unit.len(), 16);
8610 let mut data: Vec<u8> = Vec::with_capacity(unit.len() * 32);
8611 for _ in 0..32 {
8612 data.extend_from_slice(unit);
8613 }
8614 let (triples, max_offset) = l4_greedy_round_trip(64, 16, &data);
8615 assert!(
8616 triples >= 1,
8617 "periodic 16-byte payload must emit matches (got {triples})",
8618 );
8619 assert!(
8620 max_offset >= 16,
8621 "periodic 16-byte payload must produce at least one offset >= 16 \
8622 (got max_offset = {max_offset})",
8623 );
8624}
8625
8626#[test]
8627fn driver_reset_keeps_strategy_tag_in_sync_with_active_backend() {
8628 use super::strategy::StrategyTag;
8629
8630 fn check(level: CompressionLevel, expected: StrategyTag) {
8631 let mut driver = MatchGeneratorDriver::new(32, 2);
8632 driver.reset(level);
8633 assert_eq!(
8634 driver.strategy_tag, expected,
8635 "strategy_tag wrong for {level:?}"
8636 );
8637 assert_eq!(
8638 driver.strategy_tag.backend(),
8639 driver.active_backend(),
8640 "strategy_tag backend disagrees with active_backend for {level:?}"
8641 );
8642 }
8643
8644 check(CompressionLevel::Level(1), StrategyTag::Fast);
8645 check(CompressionLevel::Level(2), StrategyTag::Fast);
8646 check(CompressionLevel::Level(3), StrategyTag::Dfast);
8647 check(CompressionLevel::Level(4), StrategyTag::Dfast);
8648 check(CompressionLevel::Level(5), StrategyTag::Greedy);
8649 check(CompressionLevel::Level(7), StrategyTag::Lazy);
8650 check(CompressionLevel::Level(12), StrategyTag::Lazy);
8651 check(CompressionLevel::Level(13), StrategyTag::Btlazy2);
8652 check(CompressionLevel::Level(14), StrategyTag::Btlazy2);
8653 check(CompressionLevel::Level(15), StrategyTag::Btlazy2);
8654 check(CompressionLevel::Level(16), StrategyTag::BtOpt);
8655 check(CompressionLevel::Level(18), StrategyTag::BtUltra);
8656 check(CompressionLevel::Level(22), StrategyTag::BtUltra2);
8657 check(CompressionLevel::Fastest, StrategyTag::Fast);
8658 check(CompressionLevel::Default, StrategyTag::Dfast);
8659 check(CompressionLevel::Better, StrategyTag::Lazy);
8660 check(CompressionLevel::Best, StrategyTag::Btlazy2);
8662}
8663
8664#[test]
8665fn level_16_17_map_to_btopt_strategy() {
8666 use super::strategy::{BackendTag, StrategyTag};
8667 let p16 = resolve_level_params(CompressionLevel::Level(16), None);
8668 let p17 = resolve_level_params(CompressionLevel::Level(17), None);
8669 assert_eq!(p16.backend(), BackendTag::HashChain);
8670 assert_eq!(p17.backend(), BackendTag::HashChain);
8671 assert_eq!(StrategyTag::for_level(16), StrategyTag::BtOpt);
8672 assert_eq!(StrategyTag::for_level(17), StrategyTag::BtOpt);
8673}
8674
8675#[test]
8676fn level_18_maps_to_btultra_level_19_to_btultra2_strategy() {
8677 use super::strategy::{BackendTag, StrategyTag};
8678 let p18 = resolve_level_params(CompressionLevel::Level(18), None);
8683 let p19 = resolve_level_params(CompressionLevel::Level(19), None);
8684 assert_eq!(p18.backend(), BackendTag::HashChain);
8685 assert_eq!(p19.backend(), BackendTag::HashChain);
8686 assert_eq!(StrategyTag::for_level(18), StrategyTag::BtUltra);
8687 assert_eq!(StrategyTag::for_level(19), StrategyTag::BtUltra2);
8688}
8689
8690#[test]
8691fn level_20_22_map_to_btultra2_strategy() {
8692 use super::strategy::{BackendTag, StrategyTag};
8693 for level in 20..=22 {
8694 let params = resolve_level_params(CompressionLevel::Level(level), None);
8695 assert_eq!(params.backend(), BackendTag::HashChain);
8696 assert_eq!(StrategyTag::for_level(level as u8), StrategyTag::BtUltra2);
8697 }
8698}
8699
8700#[test]
8701fn level22_uses_target_length_and_large_input_tables() {
8702 let params = resolve_level_params(CompressionLevel::Level(22), None);
8703 assert_eq!(params.window_log, 27);
8704 let hc = params.hc.unwrap();
8705 assert_eq!(hc.hash_log, 25);
8706 assert_eq!(hc.chain_log, 27);
8707 assert_eq!(hc.search_depth, 1 << 9);
8708 assert_eq!(hc.target_len, 999);
8709}
8710
8711#[test]
8712fn bt_levels_16_to_21_pin_clevels_params() {
8713 let expected = [
8720 (16u8, 22u8, 22usize, 22usize, 32usize, 48usize),
8722 (17, 23, 22, 23, 32, 64),
8723 (18, 23, 22, 23, 64, 64),
8724 (19, 23, 22, 24, 128, 256),
8725 (20, 25, 23, 25, 128, 256),
8726 (21, 26, 24, 24, 512, 256),
8727 ];
8728 for (level, wlog, hlog, clog, sd, tl) in expected {
8729 let p = resolve_level_params(CompressionLevel::Level(level as i32), None);
8730 assert_eq!(p.window_log, wlog, "level {level} window_log");
8731 let hc = p.hc.unwrap();
8732 assert_eq!(hc.hash_log, hlog, "level {level} hash_log");
8733 assert_eq!(hc.chain_log, clog, "level {level} chain_log");
8734 assert_eq!(hc.search_depth, sd, "level {level} search_depth");
8735 assert_eq!(hc.target_len, tl, "level {level} target_len");
8736 }
8737}
8738
8739#[test]
8740fn level22_source_size_hint_uses_btultra2_tiers() {
8741 let p16k = resolve_level_params(CompressionLevel::Level(22), Some(16 * 1024));
8742 assert_eq!(p16k.window_log, 14);
8743 let hc16k = p16k.hc.unwrap();
8744 assert_eq!(hc16k.hash_log, 15);
8745 assert_eq!(hc16k.chain_log, 15);
8746 assert_eq!(hc16k.search_depth, 1 << 10);
8747 assert_eq!(hc16k.target_len, 999);
8748
8749 let p128k = resolve_level_params(CompressionLevel::Level(22), Some(128 * 1024));
8750 assert_eq!(p128k.window_log, 17);
8751 let hc128k = p128k.hc.unwrap();
8752 assert_eq!(hc128k.hash_log, 17);
8753 assert_eq!(hc128k.chain_log, 18);
8754 assert_eq!(hc128k.search_depth, 1 << 11);
8755 assert_eq!(hc128k.target_len, 999);
8756
8757 let p256k = resolve_level_params(CompressionLevel::Level(22), Some(256 * 1024));
8758 assert_eq!(p256k.window_log, 18);
8759 let hc256k = p256k.hc.unwrap();
8760 assert_eq!(hc256k.hash_log, 19);
8761 assert_eq!(hc256k.chain_log, 19);
8762 assert_eq!(hc256k.search_depth, 1 << 13);
8763 assert_eq!(hc256k.target_len, 999);
8764}
8765
8766#[test]
8767fn level22_non_power_of_two_small_source_uses_tier3_params() {
8768 let source_size = 15_027u64;
8772 let params = resolve_level_params(CompressionLevel::Level(22), Some(source_size));
8773
8774 let hc = params.hc.unwrap();
8775 assert_eq!(params.window_log, 14);
8776 assert_eq!(hc.chain_log, 15);
8777 assert_eq!(hc.hash_log, 15);
8778 assert_eq!(hc.search_depth, 1 << 10);
8779 assert_eq!(HC_OPT_MIN_MATCH_LEN, 3);
8780 assert_eq!(hc.target_len, 999);
8781}
8782
8783#[test]
8784fn level22_small_source_uses_window_bounded_hash3_log() {
8785 let mut hc = HcMatchGenerator::new(1 << 14);
8786 hc.configure(
8787 BTULTRA2_HC_CONFIG_L22_16K,
8788 super::strategy::StrategyTag::BtUltra2,
8789 14,
8790 );
8791 assert_eq!(hc.table.hash3_log, 14);
8792
8793 hc.configure(
8794 BTULTRA2_HC_CONFIG_L22,
8795 super::strategy::StrategyTag::BtUltra2,
8796 27,
8797 );
8798 assert_eq!(hc.table.hash3_log, HC3_HASH_LOG);
8799}
8800
8801#[test]
8802fn btultra2_seed_pass_initializes_opt_state() {
8803 let mut hc = HcMatchGenerator::new(1 << 20);
8804 hc.configure(
8805 BTULTRA2_HC_CONFIG,
8806 super::strategy::StrategyTag::BtUltra2,
8807 26,
8808 );
8809 let data: Vec<u8> = (0..32 * 1024).map(|i| (i % 251) as u8).collect();
8810 hc.table.add_data(data, |_| {});
8811 hc.start_matching(|_| {});
8812 assert!(
8813 hc.backend.bt_mut().opt_state.lit_length_sum > 0,
8814 "btultra2 first block should seed non-zero sequence statistics"
8815 );
8816 assert!(
8817 hc.backend.bt_mut().opt_state.off_code_sum > 0,
8818 "btultra2 first block should seed offset-code statistics"
8819 );
8820}
8821
8822#[test]
8823fn btultra2_profile_disables_small_offset_handicap() {
8824 let profile = HcOptimalCostProfile::const_for_strategy::<super::strategy::BtUltra2>();
8830 assert!(
8831 !profile.favor_small_offsets,
8832 "btultra2 should match upstream zstd opt2 offset pricing"
8833 );
8834 assert!(
8835 profile.accurate,
8836 "btultra2 should use upstream zstd opt2 accurate pricing"
8837 );
8838}
8839
8840#[test]
8841fn btultra_profile_keeps_search_depth_budget() {
8842 let p = HcOptimalCostProfile::const_for_strategy::<super::strategy::BtUltra>();
8843 assert_eq!(
8844 p.max_chain_depth, 64,
8845 "btultra chain-depth budget must match clevels.h level 18 searchLog 6 (1 << 6 = 64)"
8846 );
8847}
8848
8849#[test]
8850fn btopt_profile_keeps_search_depth_budget() {
8851 let p = HcOptimalCostProfile::const_for_strategy::<super::strategy::BtOpt>();
8852 assert_eq!(
8853 p.max_chain_depth, 32,
8854 "btopt should not cap chain depth below upstream zstd btopt search budget"
8855 );
8856}
8857
8858#[test]
8859fn sufficient_match_len_is_clamped_by_target_len() {
8860 let mut hc = HcMatchGenerator::new(1 << 20);
8861 hc.configure(
8862 BTULTRA2_HC_CONFIG,
8863 super::strategy::StrategyTag::BtUltra2,
8864 26,
8865 );
8866 hc.hc.target_len = 13;
8867 let profile = HcOptimalCostProfile::const_for_strategy::<super::strategy::BtUltra2>();
8868 assert_eq!(hc.hc.sufficient_match_len_for_pass(profile), 13);
8869}
8870
8871#[test]
8872fn opt_modes_use_target_len_as_sufficient_len() {
8873 use super::strategy;
8874 let mut hc = HcMatchGenerator::new(1 << 20);
8875 hc.hc.target_len = 57;
8876 let profiles = [
8877 HcOptimalCostProfile::const_for_strategy::<strategy::BtOpt>(),
8878 HcOptimalCostProfile::const_for_strategy::<strategy::BtUltra>(),
8879 HcOptimalCostProfile::const_for_strategy::<strategy::BtUltra2>(),
8880 ];
8881 for profile in profiles {
8882 assert_eq!(hc.hc.sufficient_match_len_for_pass(profile), 57);
8883 }
8884}
8885
8886#[test]
8887fn sufficient_match_len_is_capped_by_opt_num() {
8888 let mut hc = HcMatchGenerator::new(1 << 20);
8889 hc.hc.target_len = usize::MAX / 2;
8890 let profile = HcOptimalCostProfile::const_for_strategy::<super::strategy::BtUltra2>();
8891 assert_eq!(hc.hc.sufficient_match_len_for_pass(profile), HC_OPT_NUM - 1);
8892}
8893
8894#[test]
8895#[allow(clippy::borrow_deref_ref)]
8896fn dictionary_entropy_seed_initializes_opt_state_from_tables() {
8897 let mut hc = HcMatchGenerator::new(1 << 20);
8898 hc.configure(
8899 BTULTRA2_HC_CONFIG,
8900 super::strategy::StrategyTag::BtUltra2,
8901 26,
8902 );
8903
8904 let huff = crate::huff0::huff0_encoder::HuffmanTable::build_from_data(
8905 b"aaabbbbccccddddeeeeefffffgggg",
8906 );
8907 let ll = crate::fse::fse_encoder::default_ll_table();
8908 let ml = crate::fse::fse_encoder::default_ml_table();
8909 let of = crate::fse::fse_encoder::default_of_table();
8910 hc.seed_dictionary_entropy(Some(&huff), Some(&*ll), Some(&*ml), Some(&*of));
8911
8912 hc.backend.bt_mut().opt_state.rescale_freqs(
8913 b"abcd",
8914 HcOptimalCostProfile::const_for_strategy::<super::strategy::BtUltra2>(),
8915 );
8916
8917 let base_ll_freqs: [u32; HC_MAX_LL + 1] = [
8918 4, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
8919 1, 1, 1, 1, 1, 1,
8920 ];
8921
8922 assert_ne!(
8923 hc.backend.bt_mut().opt_state.lit_length_freq,
8924 base_ll_freqs,
8925 "dictionary entropy should override fallback LL bootstrap frequencies"
8926 );
8927 assert!(
8928 hc.backend
8929 .bt_mut()
8930 .opt_state
8931 .match_length_freq
8932 .iter()
8933 .any(|&v| v != 1),
8934 "dictionary entropy should seed non-uniform ML frequencies"
8935 );
8936 assert_ne!(
8937 hc.backend.bt_mut().opt_state.off_code_freq[0],
8938 6,
8939 "dictionary entropy should override fallback OF bootstrap frequencies"
8940 );
8941}
8942
8943#[test]
8944#[allow(clippy::borrow_deref_ref)]
8945fn dictionary_fse_seed_applies_without_huffman_seed() {
8946 let mut hc = HcMatchGenerator::new(1 << 20);
8947 hc.configure(
8948 BTULTRA2_HC_CONFIG,
8949 super::strategy::StrategyTag::BtUltra2,
8950 26,
8951 );
8952
8953 let ll = crate::fse::fse_encoder::default_ll_table();
8954 let ml = crate::fse::fse_encoder::default_ml_table();
8955 let of = crate::fse::fse_encoder::default_of_table();
8956 hc.seed_dictionary_entropy(None, Some(&*ll), Some(&*ml), Some(&*of));
8957 hc.backend.bt_mut().opt_state.rescale_freqs(
8958 b"abcd",
8959 HcOptimalCostProfile::const_for_strategy::<super::strategy::BtUltra2>(),
8960 );
8961
8962 let base_ll_freqs: [u32; HC_MAX_LL + 1] = [
8963 4, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
8964 1, 1, 1, 1, 1, 1,
8965 ];
8966 assert_ne!(
8967 hc.backend.bt_mut().opt_state.lit_length_freq,
8968 base_ll_freqs,
8969 "FSE seed should still override LL bootstrap frequencies without huffman seed"
8970 );
8971 assert!(
8972 hc.backend
8973 .bt_mut()
8974 .opt_state
8975 .match_length_freq
8976 .iter()
8977 .any(|&v| v != 1),
8978 "FSE seed should still seed non-uniform ML frequencies"
8979 );
8980 assert_ne!(
8981 hc.backend.bt_mut().opt_state.off_code_freq[0],
8982 6,
8983 "FSE seed should still override OF bootstrap frequencies without huffman seed"
8984 );
8985}
8986
8987#[test]
8988#[allow(clippy::borrow_deref_ref)]
8989fn dictionary_seed_overrides_predef_price_mode_on_tiny_input() {
8990 let mut hc = HcMatchGenerator::new(1 << 20);
8991 hc.configure(
8992 BTULTRA2_HC_CONFIG,
8993 super::strategy::StrategyTag::BtUltra2,
8994 26,
8995 );
8996
8997 let ll = crate::fse::fse_encoder::default_ll_table();
8998 let ml = crate::fse::fse_encoder::default_ml_table();
8999 let of = crate::fse::fse_encoder::default_of_table();
9000 hc.seed_dictionary_entropy(None, Some(&*ll), Some(&*ml), Some(&*of));
9001 hc.backend.bt_mut().opt_state.rescale_freqs(
9002 b"abc",
9003 HcOptimalCostProfile::const_for_strategy::<super::strategy::BtUltra2>(),
9004 );
9005 assert!(
9006 matches!(
9007 hc.backend.bt_mut().opt_state.price_type,
9008 HcOptPriceType::Dynamic
9009 ),
9010 "dictionary-seeded first block should stay in dynamic mode even for tiny src"
9011 );
9012}
9013
9014#[test]
9015fn lit_length_price_blocksize_max_costs_one_extra_bit() {
9016 let profile_predef = HcOptimalCostProfile::const_for_strategy::<super::strategy::BtUltra2>();
9017 let mut stats_predef = HcOptState::new();
9018 stats_predef.price_type = HcOptPriceType::Predefined;
9019 let predef_max = profile_predef.lit_length_price(&stats_predef, HC_BLOCKSIZE_MAX);
9020 let predef_prev =
9021 profile_predef.lit_length_price(&stats_predef, HC_BLOCKSIZE_MAX.saturating_sub(1));
9022 assert_eq!(
9023 predef_max,
9024 predef_prev + HC_BITCOST_MULTIPLIER,
9025 "predefined litLength pricing at BLOCKSIZE_MAX must add exactly one bit"
9026 );
9027
9028 let profile_dyn = HcOptimalCostProfile::const_for_strategy::<super::strategy::BtUltra2>();
9029 let mut stats_dyn = HcOptState::new();
9030 stats_dyn.price_type = HcOptPriceType::Dynamic;
9031 stats_dyn.lit_length_freq.fill(1);
9032 stats_dyn.lit_length_sum = (HC_MAX_LL + 1) as u32;
9033 stats_dyn.match_length_freq.fill(1);
9034 stats_dyn.match_length_sum = (HC_MAX_ML + 1) as u32;
9035 stats_dyn.off_code_freq.fill(1);
9036 stats_dyn.off_code_sum = (HC_MAX_OFF + 1) as u32;
9037 stats_dyn.lit_freq.fill(1);
9038 stats_dyn.lit_sum = (HC_MAX_LIT + 1) as u32;
9039 stats_dyn.set_base_prices(true);
9040 let dyn_max = profile_dyn.lit_length_price(&stats_dyn, HC_BLOCKSIZE_MAX);
9041 let dyn_prev = profile_dyn.lit_length_price(&stats_dyn, HC_BLOCKSIZE_MAX.saturating_sub(1));
9042 assert_eq!(
9043 dyn_max,
9044 dyn_prev + HC_BITCOST_MULTIPLIER,
9045 "dynamic litLength pricing at BLOCKSIZE_MAX must add exactly one bit"
9046 );
9047}
9048
9049#[test]
9050#[allow(clippy::borrow_deref_ref)]
9051fn btultra2_seed_pass_disabled_when_dictionary_entropy_seed_present() {
9052 let mut hc = HcMatchGenerator::new(1 << 20);
9053 hc.configure(
9054 BTULTRA2_HC_CONFIG,
9055 super::strategy::StrategyTag::BtUltra2,
9056 26,
9057 );
9058 let ll = crate::fse::fse_encoder::default_ll_table();
9059 let ml = crate::fse::fse_encoder::default_ml_table();
9060 let of = crate::fse::fse_encoder::default_of_table();
9061 hc.seed_dictionary_entropy(None, Some(&*ll), Some(&*ml), Some(&*of));
9062 assert!(
9063 !hc.should_run_btultra2_seed_pass::<super::strategy::BtUltra2>(HC_PREDEF_THRESHOLD + 1),
9064 "dictionary-seeded first block should skip btultra2 warmup pass"
9065 );
9066}
9067
9068#[test]
9069fn btultra2_seed_pass_disabled_when_prefix_history_exists() {
9070 let mut hc = HcMatchGenerator::new(1 << 20);
9071 hc.configure(
9072 BTULTRA2_HC_CONFIG,
9073 super::strategy::StrategyTag::BtUltra2,
9074 26,
9075 );
9076 hc.table.history_abs_start = 17;
9077 hc.table.push_test_chunk(b"abcdefghijklmnop".to_vec());
9078 assert!(
9079 !hc.should_run_btultra2_seed_pass::<super::strategy::BtUltra2>(HC_PREDEF_THRESHOLD + 9),
9080 "btultra2 warmup must be first-block only (no prefix history)"
9081 );
9082}
9083
9084#[test]
9085fn btultra2_seed_pass_disabled_for_tiny_block() {
9086 let mut hc = HcMatchGenerator::new(1 << 20);
9087 hc.configure(
9088 BTULTRA2_HC_CONFIG,
9089 super::strategy::StrategyTag::BtUltra2,
9090 26,
9091 );
9092 assert!(
9093 !hc.should_run_btultra2_seed_pass::<super::strategy::BtUltra2>(HC_PREDEF_THRESHOLD),
9094 "btultra2 warmup should not run at or below predefined threshold"
9095 );
9096}
9097
9098#[test]
9099fn btultra2_seed_pass_disabled_after_stats_initialized() {
9100 let mut hc = HcMatchGenerator::new(1 << 20);
9101 hc.configure(
9102 BTULTRA2_HC_CONFIG,
9103 super::strategy::StrategyTag::BtUltra2,
9104 26,
9105 );
9106 hc.backend.bt_mut().opt_state.lit_length_sum = 1;
9107 assert!(
9108 !hc.should_run_btultra2_seed_pass::<super::strategy::BtUltra2>(HC_PREDEF_THRESHOLD + 32),
9109 "btultra2 warmup should run only for first block before stats are initialized"
9110 );
9111}
9112
9113#[test]
9114fn btultra2_seed_pass_disabled_when_not_at_frame_start() {
9115 let mut hc = HcMatchGenerator::new(1 << 20);
9116 hc.configure(
9117 BTULTRA2_HC_CONFIG,
9118 super::strategy::StrategyTag::BtUltra2,
9119 26,
9120 );
9121 hc.table.window_size = HC_PREDEF_THRESHOLD + 64;
9124 hc.table.chunk_lens.push_back(HC_PREDEF_THRESHOLD + 32);
9127 assert!(
9128 !hc.should_run_btultra2_seed_pass::<super::strategy::BtUltra2>(HC_PREDEF_THRESHOLD + 32),
9129 "btultra2 warmup must not run after frame start"
9130 );
9131}
9132
9133#[test]
9134fn btultra2_seed_pass_disabled_when_ldm_sequences_exist() {
9135 let mut hc = HcMatchGenerator::new(1 << 20);
9136 hc.configure(
9137 BTULTRA2_HC_CONFIG,
9138 super::strategy::StrategyTag::BtUltra2,
9139 26,
9140 );
9141 hc.table.window_size = HC_PREDEF_THRESHOLD + 64;
9142 hc.table.chunk_lens.push_back(HC_PREDEF_THRESHOLD + 64);
9143 hc.backend.bt_mut().ldm_sequences.push(HcRawSeq {
9144 lit_length: 8,
9145 offset: 16,
9146 match_length: 32,
9147 });
9148 assert!(
9149 !hc.should_run_btultra2_seed_pass::<super::strategy::BtUltra2>(HC_PREDEF_THRESHOLD + 32),
9150 "btultra2 warmup must not run when LDM already produced sequences"
9151 );
9152}
9153
9154#[test]
9155fn literal_price_uses_eight_bits_when_literals_uncompressed() {
9156 let profile = HcOptimalCostProfile::const_for_strategy::<super::strategy::BtUltra2>();
9157 let mut stats = HcOptState::new();
9158 stats.set_literals_compressed_for_tests(false);
9159 stats.price_type = HcOptPriceType::Predefined;
9160 assert_eq!(
9161 profile.literal_price(&stats, b'a'),
9162 8 * HC_BITCOST_MULTIPLIER,
9163 "uncompressed literals should cost 8 bits regardless of price mode"
9164 );
9165}
9166
9167#[test]
9168fn update_stats_skips_literal_frequencies_when_uncompressed() {
9169 let mut stats = HcOptState::new();
9170 stats.set_literals_compressed_for_tests(false);
9171 stats.update_stats(3, b"abc", 4, 8);
9172 assert_eq!(
9173 stats.lit_sum, 0,
9174 "literal sum must remain unchanged when literal compression is disabled"
9175 );
9176 assert_eq!(
9177 stats.lit_freq.iter().copied().sum::<u32>(),
9178 0,
9179 "literal frequencies must not be updated when literal compression is disabled"
9180 );
9181 assert_eq!(
9182 stats.lit_length_sum, 1,
9183 "literal-length stats still update for sequence modeling"
9184 );
9185 assert_eq!(
9186 stats.match_length_sum, 1,
9187 "match-length stats still update for sequence modeling"
9188 );
9189 assert_eq!(
9190 stats.off_code_sum, 1,
9191 "offset-code stats still update for sequence modeling"
9192 );
9193}
9194
9195#[test]
9196#[allow(clippy::borrow_deref_ref)]
9197fn dictionary_huffman_seed_ignored_when_literals_uncompressed() {
9198 let mut stats = HcOptState::new();
9199 stats.set_literals_compressed_for_tests(false);
9200 let huff = crate::huff0::huff0_encoder::HuffmanTable::build_from_data(
9201 b"aaaaabbbbcccddeeff00112233445566778899",
9202 );
9203 let ll = crate::fse::fse_encoder::default_ll_table();
9204 let ml = crate::fse::fse_encoder::default_ml_table();
9205 let of = crate::fse::fse_encoder::default_of_table();
9206 stats.seed_dictionary_entropy(Some(&huff), Some(&*ll), Some(&*ml), Some(&*of));
9207 stats.rescale_freqs(
9208 b"abcd",
9209 HcOptimalCostProfile::const_for_strategy::<super::strategy::BtUltra2>(),
9210 );
9211 assert_eq!(
9212 stats.lit_sum, 0,
9213 "literal sum must stay zero when literals are uncompressed"
9214 );
9215 assert_eq!(
9216 stats.lit_freq.iter().copied().sum::<u32>(),
9217 0,
9218 "literal frequencies must ignore dictionary huffman seed when uncompressed"
9219 );
9220}
9221
9222#[test]
9223fn hc_repcode_candidates_respect_litlen_dependent_rep_order() {
9224 let mut hc = HcMatchGenerator::new(64);
9225 hc.table.history = b"xxxxxxABCDEFABCDEF".to_vec();
9226 hc.table.history_start = 0;
9227 hc.table.history_abs_start = 0;
9228
9229 let abs_pos = 12usize; let current_abs_end = hc.table.history.len();
9231 let reps = [6u32, 3u32, 9u32];
9232
9233 let mut lit_pos_candidates = Vec::new();
9234 hc.hc.for_each_repcode_candidate_with_reps(
9235 &hc.table,
9236 abs_pos,
9237 1,
9238 reps,
9239 current_abs_end,
9240 HC_OPT_MIN_MATCH_LEN,
9241 |c| {
9242 lit_pos_candidates.push(c.offset);
9243 },
9244 );
9245 assert!(
9246 lit_pos_candidates.contains(&6),
9247 "when lit_len>0, rep0 should be considered and match"
9248 );
9249
9250 let mut ll0_candidates = Vec::new();
9251 hc.hc.for_each_repcode_candidate_with_reps(
9252 &hc.table,
9253 abs_pos,
9254 0,
9255 reps,
9256 current_abs_end,
9257 HC_OPT_MIN_MATCH_LEN,
9258 |c| {
9259 ll0_candidates.push(c.offset);
9260 },
9261 );
9262 assert!(
9263 !ll0_candidates.contains(&6),
9264 "when lit_len==0, rep0 is not directly eligible (ll0 semantics)"
9265 );
9266}
9267
9268#[test]
9269fn hc_collect_optimal_candidates_keeps_reps_when_chain_depth_zero() {
9270 let mut hc = HcMatchGenerator::new(64);
9271 hc.hc.search_depth = 0;
9272 hc.table.history = b"xyzxyzxyzxyz".to_vec();
9273 hc.table.history_start = 0;
9274 hc.table.history_abs_start = 0;
9275
9276 let abs_pos = 6usize;
9277 let current_abs_end = hc.table.history.len();
9278 let profile = HcOptimalCostProfile {
9279 max_chain_depth: 0,
9280 sufficient_match_len: usize::MAX / 2,
9281 accurate: false,
9282 favor_small_offsets: false,
9283 };
9284 let mut out = Vec::new();
9285 hc.collect_optimal_candidates(
9286 abs_pos,
9287 current_abs_end,
9288 profile,
9289 HcCandidateQuery {
9290 reps: [3, 6, 9],
9291 lit_len: 1,
9292 ldm_candidate: None,
9293 },
9294 &mut out,
9295 );
9296 assert!(
9297 !out.is_empty(),
9298 "rep candidates should remain available even when chain depth is zero"
9299 );
9300 assert!(
9301 out.iter().any(|c| c.offset == 3),
9302 "rep0 candidate should be retained"
9303 );
9304}
9305
9306#[test]
9307fn hc_collect_optimal_candidates_rep_tail_match_skips_chain_probe() {
9308 let mut hc = HcMatchGenerator::new(64);
9309 hc.table.history = b"aaaaaaaaaa".to_vec();
9310 hc.table.history_start = 0;
9311 hc.table.history_abs_start = 0;
9312 hc.table.position_base = 0;
9313 hc.hc.search_depth = 32;
9314 let abs_pos = 6usize;
9315 hc.table.ensure_tables();
9316 hc.table.insert_positions(0, abs_pos);
9317
9318 let profile = HcOptimalCostProfile {
9319 max_chain_depth: 32,
9320 sufficient_match_len: usize::MAX / 2,
9321 accurate: true,
9322 favor_small_offsets: false,
9323 };
9324 let mut out = Vec::new();
9325 hc.collect_optimal_candidates(
9326 abs_pos,
9327 hc.table.history.len(),
9328 profile,
9329 HcCandidateQuery {
9330 reps: [1, 4, 8],
9331 lit_len: 1,
9332 ldm_candidate: None,
9333 },
9334 &mut out,
9335 );
9336
9337 assert!(
9338 out.iter()
9339 .all(|candidate| matches!(candidate.offset, 1 | 4)),
9340 "terminal rep match should return before chain probing adds non-rep offsets"
9341 );
9342}
9343
9344#[test]
9345fn hc_collect_optimal_candidates_long_chain_match_advances_skip_window() {
9346 let mut hc = HcMatchGenerator::new(128);
9347 hc.table.history = b"abcabcabcabcabcabcabcabc".to_vec();
9348 hc.table.history_start = 0;
9349 hc.table.history_abs_start = 0;
9350 hc.table.position_base = 0;
9351 hc.hc.search_depth = 32;
9352 let abs_pos = 9usize;
9353 hc.table.ensure_tables();
9354 hc.table.insert_positions(0, abs_pos);
9355 hc.table.skip_insert_until_abs = 0;
9356
9357 let profile = HcOptimalCostProfile {
9358 max_chain_depth: 32,
9359 sufficient_match_len: usize::MAX / 2,
9360 accurate: true,
9361 favor_small_offsets: false,
9362 };
9363 let mut out = Vec::new();
9364 hc.collect_optimal_candidates(
9365 abs_pos,
9366 hc.table.history.len(),
9367 profile,
9368 HcCandidateQuery {
9369 reps: [1, 4, 8],
9370 lit_len: 1,
9371 ldm_candidate: None,
9372 },
9373 &mut out,
9374 );
9375
9376 assert!(
9377 hc.table.skip_insert_until_abs > abs_pos,
9378 "long chain match should advance skip window to avoid redundant immediate insertions"
9379 );
9380}
9381
9382#[test]
9383fn hc_collect_optimal_candidates_chain_fast_skip_uses_match_end_minus_8() {
9384 let mut hc = HcMatchGenerator::new(128);
9385 hc.table.history = b"abcabcabcabcabcabcabcabc".to_vec();
9386 hc.table.history_start = 0;
9387 hc.table.history_abs_start = 0;
9388 hc.table.position_base = 0;
9389 hc.hc.search_depth = 32;
9390 let abs_pos = 9usize;
9391 hc.table.ensure_tables();
9392 hc.table.insert_positions(0, abs_pos);
9393 hc.table.skip_insert_until_abs = 0;
9394
9395 let profile = HcOptimalCostProfile {
9396 max_chain_depth: 32,
9397 sufficient_match_len: 10,
9398 accurate: true,
9399 favor_small_offsets: false,
9400 };
9401 let mut out = Vec::new();
9402 hc.collect_optimal_candidates(
9403 abs_pos,
9404 hc.table.history.len(),
9405 profile,
9406 HcCandidateQuery {
9407 reps: [1, 4, 8],
9408 lit_len: 1,
9409 ldm_candidate: None,
9410 },
9411 &mut out,
9412 );
9413
9414 let best_match_end = out
9415 .iter()
9416 .map(|candidate| candidate.start.saturating_add(candidate.match_len))
9417 .max()
9418 .expect("expected at least one candidate");
9419 assert!(
9420 hc.table.skip_insert_until_abs > abs_pos,
9421 "chain fast-skip must advance past current position"
9422 );
9423 assert!(
9424 hc.table.skip_insert_until_abs <= best_match_end.saturating_sub(8),
9425 "chain fast-skip must not exceed upstream zstd-style matchEndIdx - 8 bound"
9426 );
9427}
9428
9429#[test]
9430fn hc_collect_optimal_candidates_advances_skip_window_on_plain_bt_path() {
9431 let mut hc = HcMatchGenerator::new(256);
9432 hc.table.history = b"abcdefghijklmnop".to_vec();
9433 hc.table.history_start = 0;
9434 hc.table.history_abs_start = 0;
9435 hc.table.position_base = 0;
9436 hc.hc.search_depth = 0;
9437 hc.table.ensure_tables();
9438
9439 let abs_pos = 8usize;
9440 hc.table.skip_insert_until_abs = 0;
9441
9442 let profile = HcOptimalCostProfile {
9443 max_chain_depth: 0,
9444 sufficient_match_len: usize::MAX / 2,
9445 accurate: true,
9446 favor_small_offsets: false,
9447 };
9448 let mut out = Vec::new();
9449 hc.collect_optimal_candidates(
9450 abs_pos,
9451 hc.table.history.len(),
9452 profile,
9453 HcCandidateQuery {
9454 reps: [1, 4, 8],
9455 lit_len: 1,
9456 ldm_candidate: None,
9457 },
9458 &mut out,
9459 );
9460
9461 assert_eq!(
9462 hc.table.skip_insert_until_abs,
9463 abs_pos.saturating_add(1),
9464 "plain BT path should advance skip window by 1 via upstream zstd matchEndIdx baseline"
9465 );
9466}
9467
9468#[test]
9481fn hc_ldm_candidates_are_merged_into_optimal_candidates() {
9482 let mut hc = HcMatchGenerator::new(512);
9483 hc.table.history = (0..256).map(|i| (i % 251) as u8).collect();
9484 hc.table.history_start = 0;
9485 hc.table.history_abs_start = 0;
9486
9487 let abs_pos = 128usize;
9488 let current_abs_end = 256usize;
9489 let ldm = MatchCandidate {
9490 start: abs_pos,
9491 offset: 96,
9492 match_len: 40,
9493 };
9494
9495 let profile = HcOptimalCostProfile {
9496 max_chain_depth: 0,
9497 sufficient_match_len: usize::MAX / 2,
9498 accurate: true,
9499 favor_small_offsets: false,
9500 };
9501 let mut out = Vec::new();
9502 hc.collect_optimal_candidates(
9503 abs_pos,
9504 current_abs_end,
9505 profile,
9506 HcCandidateQuery {
9507 reps: [1, 4, 8],
9508 lit_len: 1,
9509 ldm_candidate: Some(ldm),
9510 },
9511 &mut out,
9512 );
9513 assert!(
9514 out.iter().any(
9515 |candidate| candidate.offset == ldm.offset && candidate.match_len == ldm.match_len
9516 ),
9517 "LDM candidate should be present in optimal candidate set"
9518 );
9519}
9520
9521#[test]
9522fn btultra_and_btultra2_both_keep_dictionary_candidates() {
9523 use super::strategy::StrategyTag;
9531
9532 let test_config = HcConfig {
9533 hash_log: 23,
9534 chain_log: 22,
9535 search_depth: 32,
9536 target_len: 256,
9537 search_mls: 4,
9538 };
9539 let window_log = 20u8;
9540
9541 let prepare_history = |hc: &mut HcMatchGenerator, abs_pos: usize| {
9542 hc.table.history = alloc::vec![0u8; 160];
9543 for i in 0..64 {
9544 hc.table.history[i] = b'a' + (i % 7) as u8;
9545 }
9546 for i in 64..160 {
9547 hc.table.history[i] = b'k' + (i % 5) as u8;
9548 }
9549 for i in 0..24 {
9550 hc.table.history[abs_pos + i] = hc.table.history[16 + i];
9551 }
9552 hc.table.history_start = 0;
9553 hc.table.history_abs_start = 0;
9554 hc.table.position_base = 0;
9555 hc.table.ensure_tables();
9556 hc.table.insert_positions(0, abs_pos);
9557 hc.table.dictionary_limit_abs = Some(64);
9558 hc.table.skip_insert_until_abs = 0;
9559 };
9560
9561 let profile = HcOptimalCostProfile {
9562 max_chain_depth: 32,
9563 sufficient_match_len: usize::MAX / 2,
9564 accurate: true,
9565 favor_small_offsets: false,
9566 };
9567 let abs_pos = 96usize;
9568 let mut out = Vec::new();
9569
9570 let mut hc = HcMatchGenerator::new(256);
9571 hc.configure(test_config, StrategyTag::BtUltra2, window_log);
9572 prepare_history(&mut hc, abs_pos);
9573 hc.collect_optimal_candidates(
9574 abs_pos,
9575 160,
9576 profile,
9577 HcCandidateQuery {
9578 reps: [1, 4, 8],
9579 lit_len: 1,
9580 ldm_candidate: None,
9581 },
9582 &mut out,
9583 );
9584 assert!(
9585 out.iter().any(|candidate| candidate.offset >= 32),
9586 "btultra2 should retain dictionary candidates on upstream zstd-parity path"
9587 );
9588
9589 let mut hc = HcMatchGenerator::new(256);
9590 hc.configure(test_config, StrategyTag::BtUltra, window_log);
9591 prepare_history(&mut hc, abs_pos);
9592 hc.collect_optimal_candidates(
9593 abs_pos,
9594 160,
9595 profile,
9596 HcCandidateQuery {
9597 reps: [1, 4, 8],
9598 lit_len: 1,
9599 ldm_candidate: None,
9600 },
9601 &mut out,
9602 );
9603 assert!(
9604 out.iter().any(|candidate| candidate.offset >= 32),
9605 "btultra should retain dictionary candidates"
9606 );
9607}
9608
9609#[test]
9610fn driver_small_source_hint_shrinks_dfast_hash_tables() {
9611 let mut driver = MatchGeneratorDriver::new(32, 2);
9612
9613 driver.reset(CompressionLevel::Level(3));
9614 let mut space = driver.get_next_space();
9615 space[..12].copy_from_slice(b"abcabcabcabc");
9616 space.truncate(12);
9617 driver.commit_space(space);
9618 driver.skip_matching_with_hint(None);
9619 let full_long = driver.dfast_matcher().long_len();
9622 let full_short = driver.dfast_matcher().short_len();
9623 assert_eq!(full_long, 1 << DFAST_HASH_BITS);
9624 assert_eq!(
9625 full_short,
9626 1 << (DFAST_HASH_BITS - DFAST_SHORT_HASH_BITS_DELTA)
9627 );
9628
9629 driver.set_source_size_hint(1024);
9630 driver.reset(CompressionLevel::Level(3));
9631 let mut space = driver.get_next_space();
9632 space[..12].copy_from_slice(b"xyzxyzxyzxyz");
9633 space.truncate(12);
9634 driver.commit_space(space);
9635 driver.skip_matching_with_hint(None);
9636 let hinted_long = driver.dfast_matcher().long_len();
9637 let hinted_short = driver.dfast_matcher().short_len();
9638
9639 assert_eq!(driver.window_size(), 1 << MIN_HINTED_WINDOW_LOG);
9647 assert_eq!(hinted_long, 1 << MIN_WINDOW_LOG);
9648 assert_eq!(hinted_short, 1 << MIN_WINDOW_LOG);
9649 assert!(
9650 hinted_long < full_long && hinted_short < full_short,
9651 "tiny source hint should reduce both dfast tables"
9652 );
9653}
9654
9655#[test]
9656fn driver_huge_source_hint_does_not_overflow_table_window_shift() {
9657 let mut driver = MatchGeneratorDriver::new(32, 2);
9663 driver.set_source_size_hint(u64::MAX);
9664 driver.reset(CompressionLevel::Level(3));
9665
9666 let mut space = driver.get_next_space();
9667 space[..12].copy_from_slice(b"abcabcabcabc");
9668 space.truncate(12);
9669 driver.commit_space(space);
9670 driver.skip_matching_with_hint(None);
9671
9672 assert!(
9673 driver.dfast_matcher().long_len() >= 1 << MIN_WINDOW_LOG,
9674 "huge hint must size the dfast table from the real window, not wrap to zero"
9675 );
9676}
9677
9678#[test]
9679fn driver_huge_source_hint_with_dict_does_not_overflow_hc_reserve() {
9680 let mut driver = MatchGeneratorDriver::new(32, 2);
9690 driver.set_source_size_hint(u64::MAX);
9691 driver.set_dictionary_size_hint(64 * 1024);
9692 driver.reset(CompressionLevel::Level(16));
9693
9694 let window = 1usize << 22;
9700 let expected_history_ceiling = window + (window >> 2) + crate::common::MAX_BLOCK_SIZE as usize;
9701 assert!(
9702 driver.hc_matcher().table.history.capacity() >= expected_history_ceiling,
9703 "huge source + dict hint must reserve the clamped HC history ceiling, got {}",
9704 driver.hc_matcher().table.history.capacity()
9705 );
9706
9707 let mut space = driver.get_next_space();
9708 space[..12].copy_from_slice(b"abcabcabcabc");
9709 space.truncate(12);
9710 driver.commit_space(space);
9711 driver.skip_matching_with_hint(None);
9712}
9713
9714#[test]
9715fn driver_chain_log_override_survives_row_to_hc_fallback() {
9716 let chain_log_override = 10u32;
9723 let ov = super::parameters::ParamOverrides {
9724 chain_log: Some(chain_log_override),
9725 ..Default::default()
9726 };
9727 let mut driver = MatchGeneratorDriver::new(32, 2);
9728 driver.set_source_size_hint(1 << 12);
9731 driver.set_param_overrides(Some(ov));
9732 driver.reset(CompressionLevel::Level(6));
9733 let mut space = driver.get_next_space();
9734 space[..12].copy_from_slice(b"abcabcabcabc");
9735 space.truncate(12);
9736 driver.commit_space(space);
9737 driver.skip_matching_with_hint(None);
9738 assert_eq!(
9742 driver.hc_matcher().table.chain_log,
9743 chain_log_override as usize,
9744 "explicit chain_log override must survive the Row->HC fallback, got {}",
9745 driver.hc_matcher().table.chain_log
9746 );
9747}
9748
9749#[test]
9750fn driver_small_source_hint_shrinks_row_hash_tables() {
9751 let mut driver = MatchGeneratorDriver::new(32, 2);
9752
9753 driver.reset(CompressionLevel::Level(5));
9754 let mut space = driver.get_next_space();
9755 space[..12].copy_from_slice(b"abcabcabcabc");
9756 space.truncate(12);
9757 driver.commit_space(space);
9758 driver.skip_matching_with_hint(None);
9759 let full_rows = driver.row_matcher().row_heads.len();
9760 assert_eq!(full_rows, 1 << (ROW_L5.hash_bits - ROW_L5.row_log));
9764
9765 driver.set_source_size_hint(1 << 16);
9771 driver.reset(CompressionLevel::Level(5));
9772 let mut space = driver.get_next_space();
9773 space[..12].copy_from_slice(b"xyzxyzxyzxyz");
9774 space.truncate(12);
9775 driver.commit_space(space);
9776 driver.skip_matching_with_hint(None);
9777 assert_eq!(
9778 driver.active_backend(),
9779 super::strategy::BackendTag::Row,
9780 "windowLog > 14 keeps the upstream row matchfinder"
9781 );
9782 let hinted_rows = driver.row_matcher().row_heads.len();
9783 assert!(
9784 hinted_rows < full_rows,
9785 "a window>14 source hint should reduce the row hash table footprint"
9786 );
9787
9788 driver.set_source_size_hint(1024);
9792 driver.reset(CompressionLevel::Level(5));
9793 assert_eq!(driver.window_size(), 1 << MIN_HINTED_WINDOW_LOG);
9794 assert_eq!(
9795 driver.active_backend(),
9796 super::strategy::BackendTag::HashChain,
9797 "windowLog <= 14 must fall back to the upstream zstd hash-chain matchfinder",
9798 );
9799}
9800
9801#[test]
9802fn row_matches_roundtrip_multi_block_pattern() {
9803 let pattern = [7, 13, 44, 184, 19, 96, 171, 109, 141, 251];
9804 let first_block: Vec<u8> = pattern.iter().copied().cycle().take(128 * 1024).collect();
9805 let second_block: Vec<u8> = pattern.iter().copied().cycle().take(128 * 1024).collect();
9806
9807 let mut matcher = RowMatchGenerator::new(1 << 22);
9808 matcher.configure(ROW_CONFIG);
9809 matcher.ensure_tables();
9810 let replay_sequence = |decoded: &mut Vec<u8>, seq: Sequence<'_>| match seq {
9811 Sequence::Literals { literals } => decoded.extend_from_slice(literals),
9812 Sequence::Triple {
9813 literals,
9814 offset,
9815 match_len,
9816 } => {
9817 decoded.extend_from_slice(literals);
9818 let start = decoded.len() - offset;
9819 for i in 0..match_len {
9820 let byte = decoded[start + i];
9821 decoded.push(byte);
9822 }
9823 }
9824 };
9825
9826 matcher.add_data(first_block.clone(), |_| {});
9827 let mut history = Vec::new();
9828 matcher.start_matching(|seq| replay_sequence(&mut history, seq));
9829 assert_eq!(history, first_block);
9830
9831 matcher.add_data(second_block.clone(), |_| {});
9832 let prefix_len = history.len();
9833 matcher.start_matching(|seq| replay_sequence(&mut history, seq));
9834
9835 assert_eq!(&history[prefix_len..], second_block.as_slice());
9836
9837 let third_block: Vec<u8> = (0u8..=255).collect();
9839 matcher.add_data(third_block.clone(), |_| {});
9840 let third_prefix = history.len();
9841 matcher.start_matching(|seq| replay_sequence(&mut history, seq));
9842 assert_eq!(&history[third_prefix..], third_block.as_slice());
9843}
9844
9845#[test]
9846fn row_short_block_emits_literals_only() {
9847 let mut matcher = RowMatchGenerator::new(1 << 22);
9848 matcher.configure(ROW_CONFIG);
9849
9850 matcher.add_data(b"abcde".to_vec(), |_| {});
9851
9852 let mut saw_triple = false;
9853 let mut reconstructed = Vec::new();
9854 matcher.start_matching(|seq| match seq {
9855 Sequence::Literals { literals } => reconstructed.extend_from_slice(literals),
9856 Sequence::Triple { .. } => saw_triple = true,
9857 });
9858
9859 assert!(
9860 !saw_triple,
9861 "row backend must not emit triples for short blocks"
9862 );
9863 assert_eq!(reconstructed, b"abcde");
9864
9865 saw_triple = false;
9867 matcher.add_data(b"abcdeabcde".to_vec(), |_| {});
9868 matcher.start_matching(|seq| {
9869 if let Sequence::Triple { .. } = seq {
9870 saw_triple = true;
9871 }
9872 });
9873 assert!(
9874 saw_triple,
9875 "row backend should emit triples on repeated data"
9876 );
9877}
9878
9879#[test]
9880fn row_pick_lazy_returns_best_when_lookahead_is_out_of_bounds() {
9881 let mut matcher = RowMatchGenerator::new(1 << 22);
9882 matcher.configure(ROW_CONFIG);
9883 matcher.add_data(b"abcabc".to_vec(), |_| {});
9884 matcher.ensure_tables();
9889
9890 let best = MatchCandidate {
9891 start: 0,
9892 offset: 1,
9893 match_len: ROW_MIN_MATCH_LEN,
9894 };
9895 let picked = matcher
9896 .pick_lazy_match(0, 0, Some(best))
9897 .expect("best candidate must survive");
9898
9899 assert_eq!(picked.start, best.start);
9900 assert_eq!(picked.offset, best.offset);
9901 assert_eq!(picked.match_len, best.match_len);
9902}
9903
9904#[test]
9905fn row_backfills_previous_block_tail_for_cross_boundary_match() {
9906 let mut matcher = RowMatchGenerator::new(1 << 22);
9907 matcher.configure(ROW_CONFIG);
9908
9909 let mut first_block = alloc::vec![0xA5; 64];
9910 first_block.extend_from_slice(b"XYZ");
9911 let second_block = b"XYZXYZtail".to_vec();
9912
9913 let replay_sequence = |decoded: &mut Vec<u8>, seq: Sequence<'_>| match seq {
9914 Sequence::Literals { literals } => decoded.extend_from_slice(literals),
9915 Sequence::Triple {
9916 literals,
9917 offset,
9918 match_len,
9919 } => {
9920 decoded.extend_from_slice(literals);
9921 let start = decoded.len() - offset;
9922 for i in 0..match_len {
9923 let byte = decoded[start + i];
9924 decoded.push(byte);
9925 }
9926 }
9927 };
9928
9929 matcher.add_data(first_block.clone(), |_| {});
9930 let mut reconstructed = Vec::new();
9931 matcher.start_matching(|seq| replay_sequence(&mut reconstructed, seq));
9932 assert_eq!(reconstructed, first_block);
9933
9934 matcher.add_data(second_block.clone(), |_| {});
9935 let mut saw_cross_boundary = false;
9936 let prefix_len = reconstructed.len();
9937 matcher.start_matching(|seq| {
9938 if let Sequence::Triple {
9939 literals,
9940 offset,
9941 match_len,
9942 } = seq
9943 && literals.is_empty()
9944 && offset == 3
9945 && match_len >= ROW_MIN_MATCH_LEN
9946 {
9947 saw_cross_boundary = true;
9948 }
9949 replay_sequence(&mut reconstructed, seq);
9950 });
9951
9952 assert!(
9953 saw_cross_boundary,
9954 "row matcher should reuse the 3-byte previous-block tail"
9955 );
9956 assert_eq!(&reconstructed[prefix_len..], second_block.as_slice());
9957}
9958
9959#[test]
9960fn row_skip_matching_with_incompressible_hint_uses_sparse_prefix() {
9961 let data = deterministic_high_entropy_bytes(0xA713_9C5D_44E2_10B1, 4096);
9962
9963 let mut dense = RowMatchGenerator::new(1 << 22);
9964 dense.configure(ROW_CONFIG);
9965 dense.add_data(data.clone(), |_| {});
9966 dense.skip_matching_with_hint(Some(false));
9967 let dense_slots = dense
9968 .row_positions
9969 .iter()
9970 .filter(|&&pos| pos != ROW_EMPTY_SLOT)
9971 .count();
9972
9973 let mut sparse = RowMatchGenerator::new(1 << 22);
9974 sparse.configure(ROW_CONFIG);
9975 sparse.add_data(data, |_| {});
9976 sparse.skip_matching_with_hint(Some(true));
9977 let sparse_slots = sparse
9978 .row_positions
9979 .iter()
9980 .filter(|&&pos| pos != ROW_EMPTY_SLOT)
9981 .count();
9982
9983 assert!(
9984 sparse_slots < dense_slots,
9985 "incompressible hint should seed fewer row slots (sparse={sparse_slots}, dense={dense_slots})"
9986 );
9987}
9988
9989#[test]
10003fn row_skip_matching_with_none_hint_leaves_interior_empty() {
10004 let data = deterministic_high_entropy_bytes(0x9B47_F2A1_8C5E_3306, 4096);
10005
10006 let mut none_hint = RowMatchGenerator::new(1 << 22);
10007 none_hint.configure(ROW_CONFIG);
10008 none_hint.add_data(data.clone(), |_| {});
10009 none_hint.skip_matching_with_hint(None);
10010 let none_slots = none_hint
10011 .row_positions
10012 .iter()
10013 .filter(|&&pos| pos != ROW_EMPTY_SLOT)
10014 .count();
10015
10016 let mut dense = RowMatchGenerator::new(1 << 22);
10019 dense.configure(ROW_CONFIG);
10020 dense.add_data(data, |_| {});
10021 dense.skip_matching_with_hint(Some(false));
10022 let dense_slots = dense
10023 .row_positions
10024 .iter()
10025 .filter(|&&pos| pos != ROW_EMPTY_SLOT)
10026 .count();
10027
10028 assert_eq!(
10033 none_slots, 0,
10034 "None hint at block_start=0 must leave row table fully empty \
10035 (upstream zstd parity — interior NOT inserted, no pre-block backfill possible)",
10036 );
10037 assert!(
10038 dense_slots > 0,
10039 "Some(false) dict-priming path must still insert densely \
10040 (sanity check: control case for the `none_slots == 0` assertion)",
10041 );
10042}
10043
10044#[test]
10045fn driver_unhinted_level2_keeps_default_dfast_hash_table_size() {
10046 let mut driver = MatchGeneratorDriver::new(32, 2);
10047
10048 driver.reset(CompressionLevel::Level(3));
10049 let mut space = driver.get_next_space();
10050 space[..12].copy_from_slice(b"abcabcabcabc");
10051 space.truncate(12);
10052 driver.commit_space(space);
10053 driver.skip_matching_with_hint(None);
10054
10055 let long_len = driver.dfast_matcher().long_len();
10059 let short_len = driver.dfast_matcher().short_len();
10060 assert_eq!(
10061 long_len,
10062 1 << DFAST_HASH_BITS,
10063 "unhinted Level(2) should keep default long-hash table size"
10064 );
10065 assert_eq!(
10066 short_len,
10067 1 << (DFAST_HASH_BITS - DFAST_SHORT_HASH_BITS_DELTA),
10068 "unhinted Level(2) short-hash should be one bit smaller than long-hash"
10069 );
10070}
10071
10072#[cfg(any())] #[test]
10074fn simple_backend_rejects_undersized_pooled_suffix_store() {
10075 let mut driver = MatchGeneratorDriver::new(128 * 1024, 2);
10076 driver.reset(CompressionLevel::Fastest);
10077
10078 driver.suffix_pool.push(SuffixStore::with_capacity(1024));
10079
10080 let mut space = driver.get_next_space();
10081 space.clear();
10082 space.resize(4096, 0xAB);
10083 driver.commit_space(space);
10084
10085 let last_suffix_slots = driver
10086 .simple()
10087 .window
10088 .last()
10089 .expect("window entry must exist after commit")
10090 .suffixes
10091 .slots
10092 .len();
10093 assert!(
10094 last_suffix_slots >= 4096,
10095 "undersized pooled suffix store must not be reused for larger blocks"
10096 );
10097}
10098
10099#[test]
10100fn source_hint_clamps_driver_slice_size_to_window() {
10101 let mut driver = MatchGeneratorDriver::new(128 * 1024, 2);
10102 driver.set_source_size_hint(1024);
10103 driver.reset(CompressionLevel::Default);
10104
10105 let window = driver.window_size() as usize;
10106 assert_eq!(window, 1 << MIN_HINTED_WINDOW_LOG);
10107 assert_eq!(driver.slice_size, window);
10108
10109 let space = driver.get_next_space();
10110 assert_eq!(space.len(), window);
10111 driver.commit_space(space);
10112}
10113
10114#[test]
10115fn pooled_space_keeps_capacity_when_slice_size_shrinks() {
10116 let mut driver = MatchGeneratorDriver::new(128 * 1024, 2);
10117 driver.reset(CompressionLevel::Default);
10118
10119 let large = driver.get_next_space();
10120 let large_capacity = large.capacity();
10121 assert!(large_capacity >= 128 * 1024);
10122 driver.commit_space(large);
10123
10124 driver.set_source_size_hint(1024);
10125 driver.reset(CompressionLevel::Default);
10126
10127 let small = driver.get_next_space();
10128 assert_eq!(small.len(), 1 << MIN_HINTED_WINDOW_LOG);
10129 assert!(
10130 small.capacity() >= large_capacity,
10131 "pooled buffer capacity should be preserved to avoid shrink/grow churn"
10132 );
10133}
10134
10135#[test]
10136fn driver_best_to_fastest_releases_oversized_hc_tables() {
10137 let mut driver = MatchGeneratorDriver::new(32, 2);
10138
10139 driver.reset_on_hc_lazy(CompressionLevel::Best);
10144 assert_eq!(driver.window_size(), (1u64 << 22));
10145
10146 let mut space = driver.get_next_space();
10148 space[..12].copy_from_slice(b"abcabcabcabc");
10149 space.truncate(12);
10150 driver.commit_space(space);
10151 driver.skip_matching_with_hint(None);
10152
10153 driver.reset(CompressionLevel::Fastest);
10168 assert_eq!(driver.window_size(), (1u64 << 19));
10169 assert_eq!(driver.active_backend(), super::strategy::BackendTag::Simple);
10170}
10171
10172#[test]
10173fn driver_better_to_best_resizes_hc_tables() {
10174 let mut driver = MatchGeneratorDriver::new(32, 2);
10175
10176 driver.reset(CompressionLevel::Level(13));
10180 assert_eq!(driver.window_size(), (1u64 << 22));
10181
10182 let mut space = driver.get_next_space();
10183 space[..12].copy_from_slice(b"abcabcabcabc");
10184 space.truncate(12);
10185 driver.commit_space(space);
10186 driver.skip_matching_with_hint(None);
10187
10188 let hc = driver.hc_matcher();
10189 let better_hash_len = hc.table.hash_table.len();
10190 let better_chain_len = hc.table.chain_table.len();
10191
10192 driver.reset(CompressionLevel::Level(15));
10194 assert_eq!(driver.window_size(), (1u64 << 22));
10195
10196 let mut space = driver.get_next_space();
10198 space[..12].copy_from_slice(b"xyzxyzxyzxyz");
10199 space.truncate(12);
10200 driver.commit_space(space);
10201 driver.skip_matching_with_hint(None);
10202
10203 let hc = driver.hc_matcher();
10204 assert!(
10205 hc.table.hash_table.len() > better_hash_len,
10206 "L15 hash_table ({}) should be larger than L13 ({})",
10207 hc.table.hash_table.len(),
10208 better_hash_len
10209 );
10210 assert!(
10211 hc.table.chain_table.len() > better_chain_len,
10212 "L15 chain_table ({}) should be larger than L13 ({})",
10213 hc.table.chain_table.len(),
10214 better_chain_len
10215 );
10216}
10217
10218#[cfg(any())]
10219#[test]
10221fn prime_with_dictionary_preserves_history_for_first_full_block() {
10222 let mut driver = MatchGeneratorDriver::new(8, 1);
10223 driver.reset(CompressionLevel::Fastest);
10224
10225 driver.prime_with_dictionary(b"abcdefgh", [1, 4, 8]);
10226
10227 let mut space = driver.get_next_space();
10228 space.clear();
10229 space.extend_from_slice(b"abcdefgh");
10230 driver.commit_space(space);
10231
10232 let mut saw_match = false;
10233 driver.start_matching(|seq| {
10234 if let Sequence::Triple {
10235 literals,
10236 offset,
10237 match_len,
10238 } = seq
10239 && literals.is_empty()
10240 && offset == 8
10241 && match_len >= MIN_MATCH_LEN
10242 {
10243 saw_match = true;
10244 }
10245 });
10246
10247 assert!(
10248 saw_match,
10249 "first full block should still match dictionary-primed history"
10250 );
10251}
10252
10253#[cfg(any())]
10254#[test]
10256fn prime_with_large_dictionary_preserves_early_history_until_first_block() {
10257 let mut driver = MatchGeneratorDriver::new(8, 1);
10258 driver.reset(CompressionLevel::Fastest);
10259
10260 driver.prime_with_dictionary(b"abcdefghABCDEFGHijklmnop", [1, 4, 8]);
10261
10262 let mut space = driver.get_next_space();
10263 space.clear();
10264 space.extend_from_slice(b"abcdefgh");
10265 driver.commit_space(space);
10266
10267 let mut saw_match = false;
10268 driver.start_matching(|seq| {
10269 if let Sequence::Triple {
10270 literals,
10271 offset,
10272 match_len,
10273 } = seq
10274 && literals.is_empty()
10275 && offset == 24
10276 && match_len >= MIN_MATCH_LEN
10277 {
10278 saw_match = true;
10279 }
10280 });
10281
10282 assert!(
10283 saw_match,
10284 "dictionary bytes should remain addressable until frame output exceeds the live window"
10285 );
10286}
10287
10288#[test]
10289fn prime_with_dictionary_applies_offset_history_even_when_content_is_empty() {
10290 let mut driver = MatchGeneratorDriver::new(8, 1);
10291 driver.reset(CompressionLevel::Fastest);
10292
10293 driver.prime_with_dictionary(&[], [11, 7, 3]);
10294
10295 assert_eq!(driver.simple_mut().offset_hist, [11, 7, 3]);
10296}
10297
10298#[test]
10299fn hc_prime_with_empty_dictionary_disables_btultra2_seed_pass() {
10300 let mut driver = MatchGeneratorDriver::new(8, 1);
10301 driver.reset_on_hc_lazy(CompressionLevel::Better);
10302
10303 driver.prime_with_dictionary(&[], [11, 7, 3]);
10304
10305 assert_eq!(driver.hc_matcher().table.offset_hist, [11, 7, 3]);
10306 assert!(
10307 !driver
10308 .hc_matcher()
10309 .should_run_btultra2_seed_pass::<super::strategy::BtUltra2>(HC_PREDEF_THRESHOLD + 1),
10310 "btultra2 warmup must stay disabled after dictionary priming, even when dict content is empty"
10311 );
10312}
10313
10314#[test]
10315fn primed_snapshot_not_restored_across_ldm_config_change() {
10316 use super::parameters::CompressionParameters;
10323
10324 let dict = b"abcdefghabcdefghabcdefgh";
10325 let ldm_on = CompressionParameters::builder(CompressionLevel::Level(19))
10326 .enable_long_distance_matching(true)
10327 .build()
10328 .unwrap()
10329 .overrides();
10330 let ldm_off = CompressionParameters::builder(CompressionLevel::Level(19))
10331 .build()
10332 .unwrap()
10333 .overrides();
10334
10335 let mut driver = MatchGeneratorDriver::new(1024, 1);
10336
10337 driver.set_param_overrides(Some(ldm_on));
10339 driver.reset(CompressionLevel::Level(19));
10340 driver.prime_with_dictionary(dict, [1, 4, 8]);
10341 driver.capture_primed_dictionary(CompressionLevel::Level(19));
10342
10343 driver.set_param_overrides(Some(ldm_off));
10346 driver.reset(CompressionLevel::Level(19));
10347 assert!(
10348 !driver.restore_primed_dictionary(CompressionLevel::Level(19)),
10349 "primed snapshot restored across an LDM config change (stale producer)",
10350 );
10351
10352 driver.prime_with_dictionary(dict, [1, 4, 8]);
10355 driver.capture_primed_dictionary(CompressionLevel::Level(19));
10356 driver.reset(CompressionLevel::Level(19));
10357 assert!(
10358 driver.restore_primed_dictionary(CompressionLevel::Level(19)),
10359 "primed snapshot not restored under identical LDM config",
10360 );
10361}
10362
10363#[test]
10364fn hc_prime_with_dictionary_disables_btultra2_seed_pass() {
10365 let mut driver = MatchGeneratorDriver::new(8, 1);
10366 driver.reset_on_hc_lazy(CompressionLevel::Better);
10367
10368 driver.prime_with_dictionary(b"abcdefgh", [1, 4, 8]);
10369
10370 assert!(
10371 !driver
10372 .hc_matcher()
10373 .should_run_btultra2_seed_pass::<super::strategy::BtUltra2>(HC_PREDEF_THRESHOLD + 1),
10374 "btultra2 warmup must stay disabled after dictionary priming with content"
10375 );
10376}
10377
10378#[test]
10379fn dfast_prime_with_dictionary_preserves_history_for_first_full_block() {
10380 let mut driver = MatchGeneratorDriver::new(8, 1);
10381 driver.reset(CompressionLevel::Level(4));
10387
10388 let payload = b"abcdefghijklmnop";
10389 driver.prime_with_dictionary(payload, [1, 4, 8]);
10390
10391 let mut space = driver.get_next_space();
10392 space.clear();
10393 space.extend_from_slice(payload);
10394 driver.commit_space(space);
10395
10396 let mut saw_match = false;
10397 driver.start_matching(|seq| {
10398 if let Sequence::Triple {
10399 literals,
10400 offset,
10401 match_len,
10402 } = seq
10403 && literals.is_empty()
10404 && offset == payload.len()
10405 && match_len >= DFAST_MIN_MATCH_LEN
10406 {
10407 saw_match = true;
10408 }
10409 });
10410
10411 assert!(
10412 saw_match,
10413 "dfast backend should match dictionary-primed history in first full block"
10414 );
10415}
10416
10417#[test]
10418fn prime_with_dictionary_does_not_inflate_reported_window_size() {
10419 let mut driver = MatchGeneratorDriver::new(8, 1);
10420 driver.reset(CompressionLevel::Fastest);
10421
10422 let before = driver.window_size();
10423 driver.prime_with_dictionary(b"abcdefghABCDEFGHijklmnop", [1, 4, 8]);
10424 let after = driver.window_size();
10425
10426 assert_eq!(
10427 after, before,
10428 "dictionary retention budget must not change reported frame window size"
10429 );
10430}
10431
10432#[test]
10433fn primed_snapshot_not_restored_when_window_hint_differs() {
10434 let mut driver = MatchGeneratorDriver::new(8, 1);
10444 let level = CompressionLevel::Best;
10445
10446 driver.set_source_size_hint(256 * 1024);
10448 driver.reset(level);
10449 let big_window = driver.window_size();
10450 driver.prime_with_dictionary(b"abcdefghABCDEFGHijklmnop", [1, 4, 8]);
10451 driver.capture_primed_dictionary(level);
10452
10453 driver.set_source_size_hint(48 * 1024);
10455 driver.reset(level);
10456 let small_window = driver.window_size();
10457 assert!(
10458 small_window < big_window,
10459 "precondition: the two hints must resolve to different windows \
10460 (small={small_window}, big={big_window})"
10461 );
10462
10463 let restored = driver.restore_primed_dictionary(level);
10464 assert!(
10465 !restored,
10466 "snapshot captured at window {big_window} must NOT be restored into a \
10467 reset advertising window {small_window} (level alone is an insufficient key)"
10468 );
10469}
10470
10471#[test]
10472fn primed_snapshot_restored_for_hints_in_same_window_bucket() {
10473 let mut driver = MatchGeneratorDriver::new(8, 1);
10482 let level = CompressionLevel::Best;
10483
10484 driver.set_source_size_hint(300 * 1024);
10487 driver.reset(level);
10488 let window_a = driver.window_size();
10489 driver.prime_with_dictionary(b"abcdefghABCDEFGHijklmnop", [1, 4, 8]);
10490 driver.capture_primed_dictionary(level);
10491
10492 driver.set_source_size_hint(400 * 1024);
10493 driver.reset(level);
10494 let window_b = driver.window_size();
10495 assert_eq!(
10496 window_a, window_b,
10497 "precondition: same-bucket hints must resolve to the same window \
10498 (a={window_a}, b={window_b})"
10499 );
10500
10501 let restored = driver.restore_primed_dictionary(level);
10502 assert!(
10503 restored,
10504 "snapshot captured at a 300 KiB hint must be restored into a 400 KiB \
10505 hint that resolves to the identical matcher shape (raw bytes over-key)"
10506 );
10507}
10508
10509#[test]
10510fn primed_snapshot_restored_across_level22_tier_hints() {
10511 let mut driver = MatchGeneratorDriver::new(8, 1);
10520 let level = CompressionLevel::Level(22);
10521
10522 driver.set_source_size_hint(20 * 1024);
10523 driver.reset(level);
10524 let window_a = driver.window_size();
10525 driver.prime_with_dictionary(b"abcdefghABCDEFGHijklmnop", [1, 4, 8]);
10526 driver.capture_primed_dictionary(level);
10527
10528 driver.set_source_size_hint(100 * 1024);
10529 driver.reset(level);
10530 let window_b = driver.window_size();
10531 assert_eq!(
10532 window_a, window_b,
10533 "precondition: both hints must land in the same Level 22 upstream zstd tier \
10534 (a={window_a}, b={window_b})"
10535 );
10536
10537 let restored = driver.restore_primed_dictionary(level);
10538 assert!(
10539 restored,
10540 "Level 22 snapshot captured at a 20 KiB hint must be restored into a \
10541 100 KiB hint that resolves to the same upstream zstd tier (different ceil-log \
10542 buckets, identical matcher shape)"
10543 );
10544}
10545
10546#[test]
10547fn fast_dict_attaches_within_cutoff_bounds() {
10548 let level = CompressionLevel::Level(1);
10560 for hint in [8192u64, 8193, 1 << 20] {
10561 let mut driver = MatchGeneratorDriver::new(8, 1);
10562 driver.set_source_size_hint(hint);
10563 driver.reset(level);
10564 driver.prime_with_dictionary(b"abcdefghABCDEFGHijklmnop", [1, 4, 8]);
10565 assert!(
10566 driver.borrowed_dict_supported(),
10567 "Fast dict frame with hint {hint} must attach (borrowed in-place \
10568 dict scan), never fall back to the copy-mode input-copy path"
10569 );
10570 }
10571}
10572
10573#[test]
10574fn fast_attach_cutoff_keeps_virtual_positions_within_u32() {
10575 let max_attached: u64 = 1u64 << FAST_ATTACH_DICT_CUTOFF_LOG;
10583 assert!(
10584 max_attached <= u32::MAX as u64,
10585 "the largest attached source 2^{FAST_ATTACH_DICT_CUTOFF_LOG} must fit u32 \
10586 virtual positions",
10587 );
10588 assert!(
10589 (1u64 << (FAST_ATTACH_DICT_CUTOFF_LOG + 1)) > u32::MAX as u64,
10590 "the next bucket 2^{} would overflow u32 virtual positions",
10591 FAST_ATTACH_DICT_CUTOFF_LOG + 1,
10592 );
10593}
10594
10595#[test]
10596fn oversized_dict_hint_routes_fast_to_copy_mode() {
10597 let mut driver = MatchGeneratorDriver::new(8, 1);
10604 driver.set_dictionary_size_hint(MAX_FAST_ATTACH_DICT_REGION + 1);
10605 driver.reset(CompressionLevel::Level(1));
10606 driver.prime_with_dictionary(b"small dict content with some padding here", [1, 4, 8]);
10607 assert!(
10608 !driver.borrowed_dict_supported(),
10609 "an oversized dict must use copy mode, not the tagged attach fill"
10610 );
10611}
10612
10613#[test]
10614fn block_samples_match_dict_is_true_for_non_simple_backend() {
10615 let dict = b"the quick brown fox jumps over the lazy dog 0123456789abcdef";
10622 let mut row = MatchGeneratorDriver::new(8, 6);
10623 row.set_dictionary_size_hint(dict.len());
10624 row.reset(CompressionLevel::Level(6));
10625 row.prime_with_dictionary(dict, [1, 4, 8]);
10626 assert!(
10627 row.block_samples_match_dict(&dict[..32]),
10628 "non-Simple backend must stay on the scan (true) for a dict frame"
10629 );
10630 let random: alloc::vec::Vec<u8> = (0..64u8)
10631 .map(|i| i.wrapping_mul(37).wrapping_add(13))
10632 .collect();
10633 assert!(
10634 row.block_samples_match_dict(&random),
10635 "non-Simple backend reports true regardless of block content"
10636 );
10637}
10638
10639#[test]
10640fn primed_snapshot_fast_attach_does_not_over_key_non_simple_backends() {
10641 let mut driver = MatchGeneratorDriver::new(8, 1);
10658 let level = CompressionLevel::Level(12);
10659
10660 driver.reset(level);
10662 let window_a = driver.window_size();
10663 driver.prime_with_dictionary(b"abcdefghABCDEFGHijklmnop", [1, 4, 8]);
10664 driver.capture_primed_dictionary(level);
10665
10666 driver.set_source_size_hint(64 * 1024 * 1024);
10669 driver.reset(level);
10670 let window_b = driver.window_size();
10671 assert_eq!(
10672 window_a, window_b,
10673 "precondition: the large hint must resolve to the same window as the \
10674 unhinted level (a={window_a}, b={window_b})"
10675 );
10676
10677 let restored = driver.restore_primed_dictionary(level);
10678 assert!(
10679 restored,
10680 "a Row snapshot must restore across an unhinted vs large-hinted \
10681 reset that resolves to the identical matcher — `fast_attach` is a Fast \
10682 backend concept and must not over-key non-Simple shapes"
10683 );
10684}
10685
10686#[cfg(any())] #[test]
10688fn prime_with_dictionary_does_not_reuse_tiny_suffix_store() {
10689 let mut driver = MatchGeneratorDriver::new(8, 2);
10690 driver.reset(CompressionLevel::Fastest);
10691
10692 driver.prime_with_dictionary(b"abcdefghi", [1, 4, 8]);
10695
10696 assert!(
10697 driver
10698 .simple()
10699 .window
10700 .iter()
10701 .all(|entry| entry.data.len() >= MIN_MATCH_LEN),
10702 "dictionary priming must not commit tails shorter than MIN_MATCH_LEN"
10703 );
10704}
10705
10706#[test]
10707fn prime_with_dictionary_counts_only_committed_tail_budget() {
10708 let mut driver = MatchGeneratorDriver::new(8, 1);
10709 driver.reset(CompressionLevel::Fastest);
10710
10711 let before = driver.simple_mut().max_window_size;
10712 driver.prime_with_dictionary(b"abcdefghi", [1, 4, 8]);
10714
10715 assert_eq!(
10716 driver.simple_mut().max_window_size,
10717 before + 8,
10718 "retention budget must account only for dictionary bytes actually committed to history"
10719 );
10720}
10721
10722#[test]
10723fn dfast_prime_with_dictionary_counts_four_byte_tail_budget() {
10724 let mut driver = MatchGeneratorDriver::new(8, 1);
10725 driver.reset(CompressionLevel::Level(3));
10726
10727 let before = driver.dfast_matcher().max_window_size;
10728 driver.prime_with_dictionary(b"abcdefghijkl", [1, 4, 8]);
10731
10732 assert_eq!(
10733 driver.dfast_matcher().max_window_size,
10734 before + 12,
10735 "dfast retention budget should include 4-byte dictionary tails"
10736 );
10737}
10738
10739#[test]
10740fn row_prime_with_dictionary_preserves_history_for_first_full_block() {
10741 let mut driver = MatchGeneratorDriver::new(8, 1);
10742 driver.reset(CompressionLevel::Level(5));
10748
10749 let payload = b"abcdefghijklmnop";
10750 driver.prime_with_dictionary(payload, [1, 4, 8]);
10751
10752 let mut space = driver.get_next_space();
10753 space.clear();
10754 space.extend_from_slice(payload);
10755 driver.commit_space(space);
10756
10757 let mut saw_match = false;
10758 driver.start_matching(|seq| {
10759 if let Sequence::Triple {
10760 literals,
10761 offset,
10762 match_len,
10763 } = seq
10764 && literals.is_empty()
10765 && offset == payload.len()
10766 && match_len >= ROW_MIN_MATCH_LEN
10767 {
10768 saw_match = true;
10769 }
10770 });
10771
10772 assert!(
10773 saw_match,
10774 "row backend should match dictionary-primed history in first full block"
10775 );
10776}
10777
10778#[test]
10779fn row_prime_with_dictionary_subtracts_uncommitted_tail_budget() {
10780 let mut driver = MatchGeneratorDriver::new(8, 1);
10781 driver.reset(CompressionLevel::Level(5));
10782
10783 let base_window = driver.row_matcher().max_window_size;
10784 driver.prime_with_dictionary(b"abcdefghi", [1, 4, 8]);
10787
10788 assert_eq!(
10789 driver.row_matcher().max_window_size,
10790 base_window + 8,
10791 "row retained window must exclude uncommitted 1-byte tail"
10792 );
10793}
10794
10795#[test]
10796fn prime_with_dictionary_budget_shrinks_after_row_eviction() {
10797 let mut driver = MatchGeneratorDriver::new(8, 1);
10798 driver.reset(CompressionLevel::Level(5));
10799 driver.row_matcher_mut().max_window_size = 8;
10801 driver.reported_window_size = 8;
10802
10803 let base_window = driver.row_matcher().max_window_size;
10804 driver.prime_with_dictionary(b"abcdefghABCDEFGHijklmnop", [1, 4, 8]);
10805 assert_eq!(driver.row_matcher().max_window_size, base_window + 24);
10806
10807 for block in [b"AAAAAAAA", b"BBBBBBBB"] {
10808 let mut space = driver.get_next_space();
10809 space.clear();
10810 space.extend_from_slice(block);
10811 driver.commit_space(space);
10812 driver.skip_matching_with_hint(None);
10813 }
10814
10815 assert_eq!(
10816 driver.dictionary_retained_budget, 0,
10817 "dictionary budget should be fully retired once primed dict slices are evicted"
10818 );
10819 assert_eq!(
10820 driver.row_matcher().max_window_size,
10821 base_window,
10822 "retired dictionary budget must not remain reusable for live history"
10823 );
10824}
10825
10826#[test]
10836fn row_get_last_space_then_reset_to_fastest_drops_row_variant() {
10837 let mut driver = MatchGeneratorDriver::new(8, 1);
10838 driver.reset(CompressionLevel::Level(5));
10839 assert_eq!(driver.active_backend(), super::strategy::BackendTag::Row);
10840
10841 let mut space = driver.get_next_space();
10842 space.clear();
10843 space.extend_from_slice(b"row-data");
10844 driver.commit_space(space);
10845
10846 assert_eq!(driver.get_last_space(), b"row-data");
10847
10848 driver.reset(CompressionLevel::Fastest);
10849 assert_eq!(driver.active_backend(), super::strategy::BackendTag::Simple);
10850}
10851
10852#[test]
10861fn driver_row_commit_recycles_block_buffer_into_pool() {
10862 let mut driver = MatchGeneratorDriver::new(8, 1);
10863 driver.reset(CompressionLevel::Level(5));
10864 assert_eq!(driver.active_backend(), super::strategy::BackendTag::Row);
10865
10866 let before_pool = driver.vec_pool.len();
10867 let mut space = driver.get_next_space();
10868 space.clear();
10869 space.extend_from_slice(b"row-data-to-recycle");
10870 driver.commit_space(space);
10871
10872 assert!(
10877 driver.vec_pool.len() > before_pool,
10878 "row commit must recycle the committed block buffer into vec_pool \
10879 (before_pool = {before_pool}, after = {})",
10880 driver.vec_pool.len()
10881 );
10882 assert_eq!(driver.get_last_space(), b"row-data-to-recycle");
10884}
10885
10886#[test]
10887fn adjust_params_for_zero_source_size_uses_min_hinted_window_floor() {
10888 let mut params = resolve_level_params(CompressionLevel::Level(4), None);
10889 params.window_log = 22;
10890 let adjusted = adjust_params_for_source_size(params, 0);
10891 assert_eq!(adjusted.window_log, MIN_HINTED_WINDOW_LOG);
10892}
10893
10894#[test]
10895fn common_prefix_len_matches_scalar_reference_across_offsets() {
10896 fn scalar_reference(a: &[u8], b: &[u8]) -> usize {
10897 a.iter()
10898 .zip(b.iter())
10899 .take_while(|(lhs, rhs)| lhs == rhs)
10900 .count()
10901 }
10902
10903 for total_len in [
10904 0usize, 1, 5, 15, 16, 17, 31, 32, 33, 64, 65, 127, 191, 257, 320,
10905 ] {
10906 let base: Vec<u8> = (0..total_len)
10907 .map(|i| ((i * 13 + 7) & 0xFF) as u8)
10908 .collect();
10909
10910 for start in [0usize, 1, 3] {
10911 if start > total_len {
10912 continue;
10913 }
10914 let a = &base[start..];
10915 let b = a.to_vec();
10916 assert_eq!(
10917 common_prefix_len(a, &b),
10918 scalar_reference(a, &b),
10919 "equal slices total_len={total_len} start={start}"
10920 );
10921
10922 let len = a.len();
10923 for mismatch in [0usize, 1, 7, 15, 16, 31, 32, 47, 63, 95, 127, 128, 129, 191] {
10924 if mismatch >= len {
10925 continue;
10926 }
10927 let mut altered = b.clone();
10928 altered[mismatch] ^= 0x5A;
10929 assert_eq!(
10930 common_prefix_len(a, &altered),
10931 scalar_reference(a, &altered),
10932 "total_len={total_len} start={start} mismatch={mismatch}"
10933 );
10934 }
10935
10936 if len > 0 {
10937 let mismatch = len - 1;
10938 let mut altered = b.clone();
10939 altered[mismatch] ^= 0xA5;
10940 assert_eq!(
10941 common_prefix_len(a, &altered),
10942 scalar_reference(a, &altered),
10943 "tail mismatch total_len={total_len} start={start} mismatch={mismatch}"
10944 );
10945 }
10946 }
10947 }
10948
10949 let long = alloc::vec![0xAB; 320];
10950 let shorter = alloc::vec![0xAB; 137];
10951 assert_eq!(
10952 common_prefix_len(&long, &shorter),
10953 scalar_reference(&long, &shorter)
10954 );
10955}
10956
10957#[test]
10958fn row_pick_lazy_returns_none_when_next_is_better() {
10959 let mut matcher = RowMatchGenerator::new(1 << 22);
10960 matcher.configure(ROW_CONFIG);
10961 matcher.add_data(alloc::vec![b'a'; 64], |_| {});
10962 matcher.ensure_tables();
10963
10964 let abs_pos = matcher.history_abs_start + 16;
10965 let best = MatchCandidate {
10966 start: abs_pos,
10967 offset: 8,
10968 match_len: ROW_MIN_MATCH_LEN,
10969 };
10970 assert!(
10971 matcher.pick_lazy_match(abs_pos, 0, Some(best)).is_none(),
10972 "lazy picker should defer when next position is clearly better"
10973 );
10974}
10975
10976#[test]
10977fn row_pick_lazy_depth2_returns_none_when_next2_significantly_better() {
10978 let mut matcher = RowMatchGenerator::new(1 << 22);
10979 matcher.configure(ROW_CONFIG);
10980 matcher.lazy_depth = 2;
10981 matcher.search_depth = 0;
10982 matcher.offset_hist = [6, 9, 1];
10983
10984 let mut data = alloc::vec![b'x'; 40];
10985 data[11..30].copy_from_slice(b"EFABCABCAEFABCAEFAB");
10986 matcher.add_data(data, |_| {});
10987 matcher.ensure_tables();
10988
10989 let abs_pos = matcher.history_abs_start + 20;
10990 let best = matcher
10991 .best_match(abs_pos, 0)
10992 .expect("expected baseline repcode match");
10993 assert_eq!(best.offset, 9);
10994 assert_eq!(best.match_len, 6);
10997
10998 if let Some(next) = matcher.best_match(abs_pos + 1, 1) {
10999 assert!(next.match_len <= best.match_len);
11000 }
11001
11002 let next2 = matcher
11003 .best_match(abs_pos + 2, 2)
11004 .expect("expected +2 candidate");
11005 assert!(
11006 next2.match_len > best.match_len + 1,
11007 "+2 candidate must be significantly better for depth-2 lazy skip"
11008 );
11009 assert!(
11010 matcher.pick_lazy_match(abs_pos, 0, Some(best)).is_none(),
11011 "lazy picker should defer when +2 candidate is significantly better"
11012 );
11013}
11014
11015#[test]
11016fn row_pick_lazy_depth2_keeps_best_when_next2_is_only_one_byte_better() {
11017 let mut matcher = RowMatchGenerator::new(1 << 22);
11018 matcher.configure(ROW_CONFIG);
11019 matcher.lazy_depth = 2;
11020 matcher.search_depth = 0;
11021 matcher.offset_hist = [6, 9, 1];
11022
11023 let mut data = alloc::vec![b'x'; 40];
11024 data[11..30].copy_from_slice(b"EFABCABCAEFABCAEFAZ");
11025 matcher.add_data(data, |_| {});
11026 matcher.ensure_tables();
11027
11028 let abs_pos = matcher.history_abs_start + 20;
11029 let best = matcher
11030 .best_match(abs_pos, 0)
11031 .expect("expected baseline repcode match");
11032 assert_eq!(best.offset, 9);
11033 assert_eq!(best.match_len, 6);
11036
11037 let next2 = matcher
11038 .best_match(abs_pos + 2, 2)
11039 .expect("expected +2 candidate");
11040 assert_eq!(next2.match_len, best.match_len + 1);
11041 let chosen = matcher
11042 .pick_lazy_match(abs_pos, 0, Some(best))
11043 .expect("lazy picker should keep current best");
11044 assert_eq!(chosen.start, best.start);
11045 assert_eq!(chosen.offset, best.offset);
11046 assert_eq!(chosen.match_len, best.match_len);
11047}
11048
11049#[test]
11051fn row_hash_and_row_extracts_high_bits() {
11052 let mut matcher = RowMatchGenerator::new(1 << 22);
11053 matcher.configure(ROW_CONFIG);
11054 matcher.add_data(
11055 alloc::vec![
11056 0xAA, 0xBB, 0xCC, 0x11, 0x10, 0x20, 0x30, 0x40, 0xAA, 0xBB, 0xCC, 0x22, 0x50, 0x60,
11057 0x70, 0x80,
11058 ],
11059 |_| {},
11060 );
11061 matcher.ensure_tables();
11062
11063 let pos = matcher.history_abs_start + 8;
11064 let (row, tag) = matcher
11065 .hash_and_row(pos)
11066 .expect("row hash should be available");
11067
11068 let idx = pos - matcher.history_abs_start;
11069 let concat = matcher.live_history();
11070 let key_len = matcher.mls.min(6);
11074 let value = u64::from_le_bytes(concat[idx..idx + 8].try_into().unwrap())
11075 & ((1u64 << (key_len * 8)) - 1);
11076 let hash = crate::encoding::fastpath::hash_mix_u64_with_kernel(matcher.hash_kernel, value);
11077 let total_bits = matcher.row_hash_log + ROW_TAG_BITS;
11078 let combined = hash >> (u64::BITS as usize - total_bits);
11079 let expected_row =
11080 ((combined >> ROW_TAG_BITS) as usize) & ((1usize << matcher.row_hash_log) - 1);
11081 let expected_tag = combined as u8;
11082
11083 assert_eq!(row, expected_row);
11084 assert_eq!(tag, expected_tag);
11085}
11086
11087#[test]
11088fn row_repcode_skips_candidate_before_history_start() {
11089 let mut matcher = RowMatchGenerator::new(1 << 22);
11090 matcher.configure(ROW_CONFIG);
11091 matcher.history = alloc::vec![b'a'; 20];
11092 matcher.history_start = 0;
11093 matcher.history_abs_start = 10;
11094 matcher.offset_hist = [3, 0, 0];
11095
11096 assert!(matcher.repcode_candidate(12, 1).is_none());
11097}
11098
11099#[test]
11100fn row_repcode_returns_none_when_position_too_close_to_history_end() {
11101 let mut matcher = RowMatchGenerator::new(1 << 22);
11102 matcher.configure(ROW_CONFIG);
11103 matcher.history = b"abcde".to_vec();
11104 matcher.history_start = 0;
11105 matcher.history_abs_start = 0;
11106 matcher.offset_hist = [1, 0, 0];
11107
11108 assert!(matcher.repcode_candidate(4, 1).is_none());
11109}
11110
11111#[cfg(all(feature = "std", target_arch = "x86_64"))]
11112#[test]
11113fn hash_mix_sse42_path_is_available_and_matches_accelerated_impl_when_supported() {
11114 use crate::encoding::fastpath::{self, FastpathKernel};
11115 if !is_x86_feature_detected!("sse4.2") {
11116 return;
11117 }
11118 let v = 0x0123_4567_89AB_CDEFu64;
11119 let accelerated = unsafe { fastpath::sse42::hash_mix_u64(v) };
11121 let dispatched = fastpath::dispatch_hash_mix_u64(v);
11123 let kernel = fastpath::select_kernel();
11124 if kernel == FastpathKernel::Sse42 {
11125 assert_eq!(dispatched, accelerated);
11126 } else {
11127 assert_eq!(dispatched, accelerated, "AVX2/SSE4.2 share CRC32 mix");
11129 }
11130}
11131
11132#[cfg(all(feature = "std", target_arch = "aarch64", target_endian = "little"))]
11133#[test]
11134fn hash_mix_crc_path_is_available_and_matches_accelerated_impl_when_supported() {
11135 use crate::encoding::fastpath;
11136 if !is_aarch64_feature_detected!("crc") {
11137 return;
11138 }
11139 let v = 0x0123_4567_89AB_CDEFu64;
11140 let accelerated = unsafe { fastpath::neon::hash_mix_u64(v) };
11142 let dispatched = fastpath::dispatch_hash_mix_u64(v);
11143 assert_eq!(dispatched, accelerated);
11144}
11145
11146#[test]
11147fn hc_hash3_position_matches_hash3_formula() {
11148 let bytes = [b'a', b'b', b'c', b'd'];
11149 let read32 = u32::from_le_bytes(bytes);
11150 let expected = (((read32 << 8).wrapping_mul(HC_PRIME3BYTES)) >> (32 - HC3_HASH_LOG)) as usize;
11151 assert_eq!(
11152 super::match_table::storage::MatchTable::hash3_position(&bytes, HC3_HASH_LOG),
11153 expected
11154 );
11155}
11156
11157#[test]
11158fn hc_hash_position_matches_hash4_formula() {
11159 let mut hc = HcMatchGenerator::new(1 << 20);
11160 hc.configure(HC_CONFIG, super::strategy::StrategyTag::Lazy, 22);
11161 let bytes = [b'a', b'b', b'c', b'd'];
11162 let read32 = u32::from_le_bytes(bytes);
11163 let expected = ((read32.wrapping_mul(HC_PRIME4BYTES)) >> (32 - hc.table.hash_log)) as usize;
11164 assert_eq!(hc.table.hash_position(&bytes), expected);
11165}
11166
11167#[test]
11168fn btultra2_main_hash_uses_hash4_formula() {
11169 let mut hc = HcMatchGenerator::new(1 << 20);
11170 hc.configure(
11171 BTULTRA2_HC_CONFIG_L22,
11172 super::strategy::StrategyTag::BtUltra2,
11173 27,
11174 );
11175 let bytes = [b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h'];
11176 let read32 = u32::from_le_bytes(bytes[..4].try_into().unwrap());
11177 let expected = ((read32.wrapping_mul(HC_PRIME4BYTES)) >> (32 - hc.table.hash_log)) as usize;
11178 let actual = super::match_table::storage::MatchTable::hash_position_with_mls(
11179 &bytes,
11180 hc.table.hash_log,
11181 super::bt::BtMatcher::HASH_MLS,
11182 );
11183 assert_eq!(actual, expected);
11184}
11185
11186#[test]
11187fn row_candidate_returns_none_when_abs_pos_near_end_of_history() {
11188 let mut matcher = RowMatchGenerator::new(1 << 22);
11189 matcher.configure(ROW_CONFIG);
11190 matcher.history = alloc::vec![b'a'; ROW_MIN_MATCH_LEN - 1];
11195 matcher.history_start = 0;
11196 matcher.history_abs_start = 0;
11197
11198 assert!(matcher.row_candidate(0, 0).is_none());
11199}
11200
11201#[test]
11202fn hc_chain_candidates_returns_sentinels_for_short_suffix() {
11203 let mut hc = HcMatchGenerator::new(32);
11204 hc.table.history = b"abc".to_vec();
11205 hc.table.history_start = 0;
11206 hc.table.history_abs_start = 0;
11207 hc.table.ensure_tables();
11208
11209 let candidates = hc.hc.chain_candidates(&hc.table, 0);
11210 assert!(candidates.iter().all(|&pos| pos == usize::MAX));
11211}
11212
11213#[test]
11214fn hc_reset_advances_floor_past_prior_frame_entries() {
11215 use super::match_table::storage::MatchTable;
11216 let mut hc = HcMatchGenerator::new(32);
11217 hc.table.add_data(b"abcdeabcde".to_vec(), |_| {});
11218 hc.table.ensure_tables();
11219 hc.table.insert_positions(0, 6);
11221 let prev_end = hc.table.history_abs_end();
11222 assert_eq!(prev_end, 10);
11223 assert!(hc.table.hash_table.iter().any(|&v| v != HC_EMPTY));
11224
11225 hc.reset(|_| {});
11226
11227 assert_eq!(hc.table.history_abs_start, prev_end);
11233 for &slot in hc.table.hash_table.iter() {
11234 if let Some(candidate_abs) =
11235 MatchTable::stored_abs_position_fast(slot, hc.table.position_base, hc.table.index_shift)
11236 {
11237 assert!(
11238 candidate_abs < hc.table.history_abs_start,
11239 "a prior-frame entry must resolve below the advanced floor"
11240 );
11241 }
11242 }
11243}
11244
11245#[test]
11246fn hc_reset_full_zeroes_when_floor_would_cross_ceiling() {
11247 use super::match_table::storage::REBASE_RESET_FLOOR_CEILING;
11248 let mut hc = HcMatchGenerator::new(32);
11249 hc.table.add_data(b"abcdeabcde".to_vec(), |_| {});
11250 hc.table.ensure_tables();
11251 hc.table.hash_table.fill(123);
11252 hc.table.chain_table.fill(456);
11253 hc.table.history_abs_start = REBASE_RESET_FLOOR_CEILING;
11258
11259 hc.reset(|_| {});
11260
11261 assert_eq!(hc.table.history_abs_start, 0);
11262 assert_eq!(hc.table.position_base, 0);
11263 assert!(hc.table.hash_table.iter().all(|&v| v == HC_EMPTY));
11264 assert!(hc.table.chain_table.iter().all(|&v| v == HC_EMPTY));
11265}
11266
11267#[test]
11268fn hc_start_matching_returns_early_for_empty_current_block() {
11269 let mut hc = HcMatchGenerator::new(32);
11270 hc.table.add_data(Vec::new(), |_| {});
11271 let mut called = false;
11272 hc.start_matching(|_| called = true);
11273 assert!(!called, "empty current block should not emit sequences");
11274}
11275
11276#[cfg(test)]
11277fn deterministic_high_entropy_bytes(seed: u64, len: usize) -> Vec<u8> {
11278 let mut out = Vec::with_capacity(len);
11279 let mut state = seed;
11280 for _ in 0..len {
11281 state ^= state << 13;
11282 state ^= state >> 7;
11283 state ^= state << 17;
11284 out.push((state >> 40) as u8);
11285 }
11286 out
11287}
11288
11289#[cfg(feature = "bench_internals")]
11290pub(crate) fn level22_block_ranges(data: &[u8]) -> Vec<(usize, usize)> {
11291 let mut ranges = Vec::new();
11292 let mut cursor = 0usize;
11293 let mut savings = 0i64;
11294 while cursor < data.len() {
11295 let remaining = data.len() - cursor;
11296 let candidate_len = remaining.min(super::cost_model::HC_BLOCKSIZE_MAX);
11297 let block_len = crate::encoding::frame_compressor::optimal_block_size(
11298 CompressionLevel::Level(22),
11299 &data[cursor..cursor + candidate_len],
11300 remaining,
11301 super::cost_model::HC_BLOCKSIZE_MAX,
11302 savings,
11303 )
11304 .min(candidate_len)
11305 .max(1);
11306 ranges.push((cursor, block_len));
11307 cursor += block_len;
11308 if cursor >= super::cost_model::HC_BLOCKSIZE_MAX {
11312 savings = 3;
11313 }
11314 }
11315 ranges
11316}
11317
11318#[cfg(feature = "bench_internals")]
11319fn merge_block_delimiters(sequences: Vec<(usize, usize, usize)>) -> Vec<(usize, usize, usize)> {
11320 let mut out = Vec::with_capacity(sequences.len());
11321 let mut pending_lits = 0usize;
11322 for (lit_len, offset, match_len) in sequences {
11323 if offset == 0 && match_len == 0 {
11324 pending_lits = pending_lits.saturating_add(lit_len);
11325 continue;
11326 }
11327 out.push((lit_len.saturating_add(pending_lits), offset, match_len));
11328 pending_lits = 0;
11329 }
11330 if pending_lits > 0 {
11331 out.push((pending_lits, 0, 0));
11332 }
11333 out
11334}
11335
11336#[cfg(feature = "bench_internals")]
11342pub(crate) fn collect_level22_sequences(data: &[u8]) -> Vec<(usize, usize, usize)> {
11343 merge_block_delimiters(collect_level22_sequences_with_delimiters(data))
11344 .into_iter()
11345 .filter(|(_, offset, match_len)| *offset != 0 || *match_len != 0)
11346 .collect()
11347}
11348
11349#[cfg(feature = "bench_internals")]
11350fn collect_level22_sequences_with_delimiters(data: &[u8]) -> Vec<(usize, usize, usize)> {
11351 let mut driver = MatchGeneratorDriver::new(super::cost_model::HC_BLOCKSIZE_MAX, 1);
11352 driver.set_source_size_hint(data.len() as u64);
11353 driver.reset(CompressionLevel::Level(22));
11354
11355 let mut sequences = Vec::new();
11356 for (chunk_start, chunk_len) in level22_block_ranges(data) {
11357 let chunk = &data[chunk_start..chunk_start + chunk_len];
11358 let mut space = driver.get_next_space();
11359 space[..chunk.len()].copy_from_slice(chunk);
11360 space.truncate(chunk.len());
11361 driver.commit_space(space);
11362 driver.start_matching(|seq| {
11363 let entry = match seq {
11364 Sequence::Literals { literals } => (literals.len(), 0usize, 0usize),
11365 Sequence::Triple {
11366 literals,
11367 offset,
11368 match_len,
11369 } => (literals.len(), offset, match_len),
11370 };
11371 sequences.push(entry);
11372 });
11373 }
11374 sequences
11375}
11376
11377#[test]
11378fn hc_sparse_skip_matching_preserves_tail_cross_block_match() {
11379 let mut matcher = HcMatchGenerator::new(1 << 22);
11380 let tail = b"Qz9kLm2Rp";
11381 let mut first = deterministic_high_entropy_bytes(0xD1B5_4A32_9C77_0E19, 4096);
11382 let tail_start = first.len() - tail.len();
11383 first[tail_start..].copy_from_slice(tail);
11384 matcher.table.add_data(first.clone(), |_| {});
11385 matcher.skip_matching(Some(true));
11386
11387 let mut second = tail.to_vec();
11388 second.extend_from_slice(b"after-tail-literals");
11389 matcher.table.add_data(second, |_| {});
11390
11391 let mut first_sequence = None;
11392 matcher.start_matching(|seq| {
11393 if first_sequence.is_some() {
11394 return;
11395 }
11396 first_sequence = Some(match seq {
11397 Sequence::Literals { literals } => (literals.len(), 0usize, 0usize),
11398 Sequence::Triple {
11399 literals,
11400 offset,
11401 match_len,
11402 } => (literals.len(), offset, match_len),
11403 });
11404 });
11405
11406 let (literals_len, offset, match_len) =
11407 first_sequence.expect("expected at least one sequence after sparse skip");
11408 assert_eq!(
11409 literals_len, 0,
11410 "first sequence should start at block boundary"
11411 );
11412 assert_eq!(
11413 offset,
11414 tail.len(),
11415 "first match should reference previous tail"
11416 );
11417 assert!(
11418 match_len >= tail.len(),
11419 "tail-aligned cross-block match must be preserved"
11420 );
11421}
11422
11423#[test]
11424fn btultra2_sparse_skip_matching_preserves_tail_cross_block_match() {
11425 let mut matcher = HcMatchGenerator::new(1 << 20);
11426 matcher.configure(
11427 BTULTRA2_HC_CONFIG_L22,
11428 super::strategy::StrategyTag::BtUltra2,
11429 20,
11430 );
11431 let tail = b"Bt9kLm2Rp";
11432 let mut first = deterministic_high_entropy_bytes(0xA9C3_7F21_D4E8_510B, 4096);
11433 let tail_start = first.len() - tail.len();
11434 first[tail_start..].copy_from_slice(tail);
11435 matcher.table.add_data(first, |_| {});
11436 matcher.skip_matching(Some(true));
11437
11438 let mut second = tail.to_vec();
11439 second.extend_from_slice(b"after-tail-literals");
11440 matcher.table.add_data(second, |_| {});
11441
11442 let mut first_sequence = None;
11443 matcher.start_matching(|seq| {
11444 if first_sequence.is_some() {
11445 return;
11446 }
11447 first_sequence = Some(match seq {
11448 Sequence::Literals { literals } => (literals.len(), 0usize, 0usize),
11449 Sequence::Triple {
11450 literals,
11451 offset,
11452 match_len,
11453 } => (literals.len(), offset, match_len),
11454 });
11455 });
11456
11457 let (literals_len, offset, match_len) =
11458 first_sequence.expect("expected at least one sequence after sparse BT skip");
11459 assert_eq!(
11460 literals_len, 0,
11461 "BT sparse skip should preserve an immediate boundary match"
11462 );
11463 assert_eq!(
11464 offset,
11465 tail.len(),
11466 "first BT match should reference previous tail"
11467 );
11468 assert!(
11469 match_len >= tail.len(),
11470 "BT sparse skip must seed the dense tail for cross-block matching"
11471 );
11472}
11473
11474#[test]
11475fn hc_sparse_skip_matching_does_not_reinsert_sparse_tail_positions() {
11476 let mut matcher = HcMatchGenerator::new(1 << 22);
11477 let first = deterministic_high_entropy_bytes(0xC2B2_AE3D_27D4_EB4F, 4096);
11478 matcher.table.add_data(first.clone(), |_| {});
11479 matcher.skip_matching(Some(true));
11480
11481 let current_len = first.len();
11482 let current_abs_start =
11483 matcher.table.history_abs_start + matcher.table.window_size - current_len;
11484 let current_abs_end = current_abs_start + current_len;
11485 let dense_tail = HC_MIN_MATCH_LEN + INCOMPRESSIBLE_SKIP_STEP;
11486 let tail_start = current_abs_end
11487 .saturating_sub(dense_tail)
11488 .max(matcher.table.history_abs_start)
11489 .max(current_abs_start);
11490
11491 let overlap_pos = (tail_start..current_abs_end)
11492 .find(|&pos| (pos - current_abs_start).is_multiple_of(INCOMPRESSIBLE_SKIP_STEP))
11493 .expect("fixture should contain at least one sparse-grid overlap in dense tail");
11494
11495 let rel = matcher
11496 .table
11497 .relative_position(overlap_pos)
11498 .expect("overlap position should be representable as relative position");
11499 let chain_idx = rel as usize & ((1 << matcher.table.chain_log) - 1);
11500 assert_ne!(
11501 matcher.table.chain_table[chain_idx],
11502 rel + 1,
11503 "sparse-grid tail positions must not be reinserted (self-loop chain entry)"
11504 );
11505}
11506
11507#[test]
11508fn hc_compact_history_drains_when_threshold_crossed() {
11509 let mut hc = HcMatchGenerator::new(8);
11510 hc.table.history = b"abcdefghijklmnopqrstuvwxyz".to_vec();
11511 hc.table.history_start = 16;
11512 hc.table.compact_history();
11513 assert_eq!(hc.table.history_start, 0);
11514 assert_eq!(hc.table.history, b"qrstuvwxyz");
11515}
11516
11517#[test]
11518fn hc_insert_position_no_rebase_returns_when_relative_pos_unavailable() {
11519 let mut hc = HcMatchGenerator::new(32);
11520 hc.table.history = b"abcdefghijklmnop".to_vec();
11521 hc.table.history_abs_start = 0;
11522 hc.table.position_base = 1;
11523 hc.table.ensure_tables();
11524 let before_hash = hc.table.hash_table.clone();
11525 let before_chain = hc.table.chain_table.clone();
11526
11527 hc.table.insert_position_no_rebase(0);
11528
11529 assert_eq!(hc.table.hash_table, before_hash);
11530 assert_eq!(hc.table.chain_table, before_chain);
11531}
11532
11533#[test]
11534fn hc_insert_positions_advances_next_to_update3_for_contiguous_range() {
11535 let mut hc = HcMatchGenerator::new(64);
11536 hc.table.history = b"abcdefghijklmnopqrstuvwxyz".to_vec();
11537 hc.table.history_start = 0;
11538 hc.table.history_abs_start = 0;
11539 hc.table.position_base = 0;
11540 hc.table.ensure_tables();
11541 hc.table.next_to_update3 = 0;
11542
11543 hc.table.insert_positions(0, 9);
11544
11545 assert_eq!(
11546 hc.table.next_to_update3, 9,
11547 "contiguous insert_positions should advance hash3 update cursor"
11548 );
11549}
11550
11551#[test]
11552fn hc_insert_positions_with_step_keeps_next_to_update3_cursor_for_sparse_ranges() {
11553 let mut hc = HcMatchGenerator::new(64);
11554 hc.table.history = b"abcdefghijklmnopqrstuvwxyz".to_vec();
11555 hc.table.history_start = 0;
11556 hc.table.history_abs_start = 0;
11557 hc.table.position_base = 0;
11558 hc.table.ensure_tables();
11559 hc.table.next_to_update3 = 0;
11560
11561 hc.table.insert_positions_with_step(0, 16, 4);
11562
11563 assert_eq!(
11564 hc.table.next_to_update3, 0,
11565 "sparse insert_positions_with_step must not mark skipped positions as hash3-updated"
11566 );
11567}
11568
11569#[cfg(any())]
11570#[test]
11572fn prime_with_dictionary_budget_shrinks_after_simple_eviction() {
11573 let mut driver = MatchGeneratorDriver::new(8, 1);
11574 driver.reset(CompressionLevel::Fastest);
11575 driver.simple_mut().max_window_size = 8;
11578 driver.reported_window_size = 8;
11579
11580 let base_window = driver.simple_mut().max_window_size;
11581 driver.prime_with_dictionary(b"abcdefghABCDEFGHijklmnop", [1, 4, 8]);
11582 assert_eq!(driver.simple_mut().max_window_size, base_window + 24);
11583
11584 for block in [b"AAAAAAAA", b"BBBBBBBB"] {
11585 let mut space = driver.get_next_space();
11586 space.clear();
11587 space.extend_from_slice(block);
11588 driver.commit_space(space);
11589 driver.skip_matching_with_hint(None);
11590 }
11591
11592 assert_eq!(
11593 driver.dictionary_retained_budget, 0,
11594 "dictionary budget should be fully retired once primed dict slices are evicted"
11595 );
11596 assert_eq!(
11597 driver.simple_mut().max_window_size,
11598 base_window,
11599 "retired dictionary budget must not remain reusable for live history"
11600 );
11601}
11602
11603#[test]
11604fn prime_with_dictionary_budget_shrinks_after_dfast_eviction() {
11605 let mut driver = MatchGeneratorDriver::new(8, 1);
11606 driver.reset(CompressionLevel::Level(3));
11607 driver.dfast_matcher_mut().max_window_size = 8;
11610 driver.reported_window_size = 8;
11611
11612 let base_window = driver.dfast_matcher().max_window_size;
11613 driver.prime_with_dictionary(b"abcdefghABCDEFGHijklmnop", [1, 4, 8]);
11614 assert_eq!(driver.dfast_matcher().max_window_size, base_window + 24);
11615
11616 for block in [b"AAAAAAAA", b"BBBBBBBB"] {
11617 let mut space = driver.get_next_space();
11618 space.clear();
11619 space.extend_from_slice(block);
11620 driver.commit_space(space);
11621 driver.skip_matching_with_hint(None);
11622 }
11623
11624 assert_eq!(
11625 driver.dictionary_retained_budget, 0,
11626 "dictionary budget should be fully retired once primed dict slices are evicted"
11627 );
11628 assert_eq!(
11629 driver.dfast_matcher().max_window_size,
11630 base_window,
11631 "retired dictionary budget must not remain reusable for live history"
11632 );
11633}
11634
11635#[test]
11636fn hc_prime_with_dictionary_preserves_history_for_first_full_block() {
11637 let mut driver = MatchGeneratorDriver::new(8, 1);
11638 driver.reset_on_hc_lazy(CompressionLevel::Better);
11641
11642 driver.prime_with_dictionary(b"abcdefgh", [1, 4, 8]);
11643
11644 let mut space = driver.get_next_space();
11645 space.clear();
11646 space.extend_from_slice(b"abcdefgh");
11649 driver.commit_space(space);
11650
11651 let mut saw_match = false;
11652 driver.start_matching(|seq| {
11653 if let Sequence::Triple {
11654 literals,
11655 offset,
11656 match_len,
11657 } = seq
11658 && literals.is_empty()
11659 && offset == 8
11660 && match_len >= HC_MIN_MATCH_LEN
11661 {
11662 saw_match = true;
11663 }
11664 });
11665
11666 assert!(
11667 saw_match,
11668 "hash-chain backend should match dictionary-primed history in first full block"
11669 );
11670}
11671
11672#[test]
11673fn prime_with_dictionary_budget_shrinks_after_hc_eviction() {
11674 let mut driver = MatchGeneratorDriver::new(8, 1);
11675 driver.reset_on_hc_lazy(CompressionLevel::Better);
11676 driver.hc_matcher_mut().table.max_window_size = 8;
11678 driver.reported_window_size = 8;
11679
11680 let base_window = driver.hc_matcher().table.max_window_size;
11681 driver.prime_with_dictionary(b"abcdefghABCDEFGHijklmnop", [1, 4, 8]);
11682 assert_eq!(driver.hc_matcher().table.max_window_size, base_window + 24);
11683
11684 for block in [b"AAAAAAAA", b"BBBBBBBB"] {
11685 let mut space = driver.get_next_space();
11686 space.clear();
11687 space.extend_from_slice(block);
11688 driver.commit_space(space);
11689 driver.skip_matching_with_hint(None);
11690 }
11691
11692 assert_eq!(
11693 driver.dictionary_retained_budget, 0,
11694 "dictionary budget should be fully retired once primed dict slices are evicted"
11695 );
11696 assert_eq!(
11697 driver.hc_matcher().table.max_window_size,
11698 base_window,
11699 "retired dictionary budget must not remain reusable for live history"
11700 );
11701}
11702
11703#[test]
11704fn resident_reapply_restores_retained_dictionary_budget() {
11705 let mut driver = MatchGeneratorDriver::new(1 << 16, 1);
11714 let dict = b"abcdefghABCDEFGHijklmnopqrstuvwxyz0123456789";
11715 driver.set_dictionary_size_hint(dict.len());
11716 driver.reset_on_hc_lazy(CompressionLevel::Better);
11717 driver.prime_with_dictionary(dict, [1, 4, 8]);
11718 let base = driver.reported_window_size;
11719 assert!(
11720 driver.dictionary_retained_budget > 0,
11721 "the priming frame must retain a non-zero dict budget"
11722 );
11723
11724 driver.set_dictionary_size_hint(dict.len());
11726 driver.reset_on_hc_lazy(CompressionLevel::Better);
11727 assert!(
11728 driver.dictionary_is_resident(),
11729 "the second frame must re-borrow the resident dictionary"
11730 );
11731 assert_eq!(
11732 driver.dictionary_retained_budget, 0,
11733 "reset clears the retained-dict budget"
11734 );
11735 let inflated = driver.hc_matcher().table.max_window_size;
11736 assert!(
11737 inflated > base,
11738 "reset re-inflates the window by the resident dict region \
11739 (inflated={inflated}, base={base})"
11740 );
11741
11742 driver.reapply_resident_dictionary([1, 4, 8]);
11743 assert_eq!(
11744 driver.dictionary_retained_budget,
11745 inflated - base,
11746 "resident reapply must restore the retained-dict budget (= window \
11747 inflation) so the retire path can shrink the window as the dict evicts"
11748 );
11749}
11750
11751#[test]
11752fn hc_commit_without_eviction_retires_no_dictionary_budget() {
11753 let mut driver = MatchGeneratorDriver::new(8, 1);
11761 driver.reset_on_hc_lazy(CompressionLevel::Better);
11762 driver.hc_matcher_mut().table.max_window_size = 1 << 20;
11764 driver.reported_window_size = 1 << 20;
11765 driver.prime_with_dictionary(b"abcdefghABCDEFGHijklmnop", [1, 4, 8]);
11766 let budget_after_prime = driver.dictionary_retained_budget;
11767 assert!(
11768 budget_after_prime > 0,
11769 "priming must retain a non-zero dictionary budget"
11770 );
11771
11772 let mut space = driver.get_next_space();
11773 space.clear();
11774 space.extend_from_slice(b"AAAAAAAA");
11775 driver.commit_space(space);
11776 driver.skip_matching_with_hint(None);
11777
11778 assert_eq!(
11779 driver.dictionary_retained_budget, budget_after_prime,
11780 "a commit that evicts nothing must retire no dictionary budget"
11781 );
11782}
11783
11784#[test]
11785fn row_commit_without_eviction_retires_no_dictionary_budget() {
11786 let mut driver = MatchGeneratorDriver::new(8, 1);
11795 driver.reset(CompressionLevel::Level(5));
11796 assert!(matches!(driver.storage, MatcherStorage::Row(_)));
11797 driver.row_matcher_mut().max_window_size = 1 << 20;
11799 driver.reported_window_size = 1 << 20;
11800 driver.prime_with_dictionary(b"abcdefghABCDEFGHijklmnop", [1, 4, 8]);
11801 let budget_after_prime = driver.dictionary_retained_budget;
11802 assert!(
11803 budget_after_prime > 0,
11804 "priming must retain a non-zero dictionary budget"
11805 );
11806
11807 let mut space = driver.get_next_space();
11808 space.clear();
11809 space.extend_from_slice(b"AAAAAAAA");
11810 driver.commit_space(space);
11811 driver.skip_matching_with_hint(None);
11812
11813 assert_eq!(
11814 driver.dictionary_retained_budget, budget_after_prime,
11815 "a Row commit that evicts nothing must retire no dictionary budget"
11816 );
11817}
11818
11819#[test]
11820fn hc_rebases_positions_after_u32_boundary() {
11821 let mut matcher = HcMatchGenerator::new(64);
11822 matcher.table.add_data(b"abcdeabcdeabcde".to_vec(), |_| {});
11823 matcher.table.ensure_tables();
11824 matcher.table.position_base = 0;
11825 let history_abs_start: usize = match (u64::from(u32::MAX) + 64).try_into() {
11826 Ok(value) => value,
11827 Err(_) => return,
11828 };
11829 matcher.table.history_abs_start = history_abs_start;
11832 matcher.skip_matching(None);
11833 assert_eq!(
11834 matcher.table.position_base, matcher.table.history_abs_start,
11835 "rebase should anchor to the oldest live absolute position"
11836 );
11837
11838 assert!(
11839 matcher
11840 .table
11841 .hash_table
11842 .iter()
11843 .any(|entry| *entry != HC_EMPTY),
11844 "HC hash table should still be populated after crossing u32 boundary"
11845 );
11846
11847 let abs_pos = matcher.table.history_abs_start + 10;
11849 let candidates = matcher.hc.chain_candidates(&matcher.table, abs_pos);
11850 assert!(
11851 candidates.iter().any(|candidate| *candidate != usize::MAX),
11852 "chain_candidates should return valid matches after rebase"
11853 );
11854}
11855
11856#[cfg(target_pointer_width = "64")]
11862#[test]
11863fn row_rebases_positions_after_u32_boundary() {
11864 let mut m = RowMatchGenerator::new(64);
11871 m.add_data(b"abcdeabcdeabcde".to_vec(), |_| {});
11872
11873 let near_ceiling = (u32::MAX as usize) - 16;
11876 m.history_abs_start = near_ceiling;
11877
11878 m.add_data(b"fghij".to_vec(), |_| {});
11881
11882 assert!(
11883 m.history_abs_start < near_ceiling,
11884 "add_data must rebase the absolute origin down when the cursor nears \
11885 u32::MAX (got {})",
11886 m.history_abs_start
11887 );
11888 assert!(
11889 (m.history_abs_start + m.window_size) < u32::MAX as usize,
11890 "after rebase the live window must fit below the u32 position ceiling"
11891 );
11892}
11893
11894#[test]
11895fn hc_rebase_rebuilds_only_inserted_prefix() {
11896 let mut matcher = HcMatchGenerator::new(64);
11897 matcher.table.add_data(b"abcdeabcdeabcde".to_vec(), |_| {});
11898 matcher.table.ensure_tables();
11899 matcher.table.position_base = 0;
11900 let history_abs_start: usize = match (u64::from(u32::MAX) + 64).try_into() {
11901 Ok(value) => value,
11902 Err(_) => return,
11903 };
11904 matcher.table.history_abs_start = history_abs_start;
11905 let abs_pos = matcher.table.history_abs_start + 6;
11906
11907 let mut expected = HcMatchGenerator::new(64);
11908 expected.table.add_data(b"abcdeabcdeabcde".to_vec(), |_| {});
11909 expected.table.ensure_tables();
11910 expected.table.history_abs_start = history_abs_start;
11911 expected.table.position_base = expected.table.history_abs_start;
11912 expected.table.hash_table.fill(HC_EMPTY);
11913 expected.table.chain_table.fill(HC_EMPTY);
11914 for pos in expected.table.history_abs_start..abs_pos {
11915 expected.table.insert_position_no_rebase(pos);
11916 }
11917
11918 matcher.table.maybe_rebase_positions(abs_pos);
11919
11920 assert_eq!(
11921 matcher.table.position_base, matcher.table.history_abs_start,
11922 "rebase should still anchor to the oldest live absolute position"
11923 );
11924 assert_eq!(
11925 matcher.table.hash_table, expected.table.hash_table,
11926 "rebase must rebuild only positions already inserted before abs_pos"
11927 );
11928 assert_eq!(
11929 matcher.table.chain_table, expected.table.chain_table,
11930 "future positions must not be pre-seeded into HC chains during rebase"
11931 );
11932}
11933
11934#[cfg(any())] #[test]
11936fn suffix_store_with_single_slot_does_not_panic_on_keying() {
11937 let mut suffixes = SuffixStore::with_capacity(1);
11938 suffixes.insert(b"abcde", 0);
11939 assert!(suffixes.contains_key(b"abcde"));
11940 assert_eq!(suffixes.get(b"abcde"), Some(0));
11941}
11942
11943#[cfg(any())]
11944#[test]
11946fn fastest_reset_uses_interleaved_hash_fill_step() {
11947 let mut driver = MatchGeneratorDriver::new(32, 2);
11948
11949 driver.reset(CompressionLevel::Uncompressed);
11950 assert_eq!(driver.simple().hash_fill_step, 1);
11951
11952 driver.reset(CompressionLevel::Fastest);
11953 assert_eq!(driver.simple().hash_fill_step, FAST_HASH_FILL_STEP);
11954
11955 driver.reset(CompressionLevel::Better);
11958 assert_eq!(
11959 driver.active_backend(),
11960 super::strategy::BackendTag::HashChain
11961 );
11962 assert_eq!(driver.window_size(), (1u64 << 23));
11963 assert_eq!(driver.hc_matcher().hc.lazy_depth, 2);
11964}
11965
11966#[cfg(any())] #[test]
11968fn simple_matcher_updates_offset_history_after_emitting_match() {
11969 let mut matcher = MatchGenerator::new(64);
11970 matcher.add_data(
11971 b"abcdeabcdeabcde".to_vec(),
11972 SuffixStore::with_capacity(64),
11973 |_, _| {},
11974 );
11975
11976 assert!(matcher.next_sequence(|seq| {
11977 assert_eq!(
11978 seq,
11979 Sequence::Triple {
11980 literals: b"abcde",
11981 offset: 5,
11982 match_len: 10,
11983 }
11984 );
11985 }));
11986 assert_eq!(matcher.offset_hist, [5, 1, 4]);
11987}
11988
11989#[cfg(any())] #[test]
11991fn simple_matcher_zero_literal_repcode_checks_rep1_before_hash_lookup() {
11992 let mut matcher = MatchGenerator::new(64);
11993 matcher.add_data(
11994 b"abcdefghijabcdefghij".to_vec(),
11995 SuffixStore::with_capacity(64),
11996 |_, _| {},
11997 );
11998
11999 matcher.suffix_idx = 10;
12000 matcher.last_idx_in_sequence = 10;
12001 matcher.offset_hist = [99, 10, 4];
12002
12003 let candidate = matcher.repcode_candidate(&matcher.window.last().unwrap().data[10..], 0);
12004 assert_eq!(candidate, Some((10, 10)));
12005}
12006
12007#[cfg(any())] #[test]
12009fn simple_matcher_repcode_can_target_previous_window_entry() {
12010 let mut matcher = MatchGenerator::new(64);
12011 matcher.add_data(
12012 b"abcdefghij".to_vec(),
12013 SuffixStore::with_capacity(64),
12014 |_, _| {},
12015 );
12016 matcher.skip_matching();
12017 matcher.add_data(
12018 b"abcdefghij".to_vec(),
12019 SuffixStore::with_capacity(64),
12020 |_, _| {},
12021 );
12022
12023 matcher.offset_hist = [99, 10, 4];
12024
12025 let candidate = matcher.repcode_candidate(&matcher.window.last().unwrap().data, 0);
12026 assert_eq!(candidate, Some((10, 10)));
12027}
12028
12029#[cfg(any())] #[test]
12031fn simple_matcher_zero_literal_repcode_checks_rep2() {
12032 let mut matcher = MatchGenerator::new(64);
12033 matcher.add_data(
12034 b"abcdefghijabcdefghij".to_vec(),
12035 SuffixStore::with_capacity(64),
12036 |_, _| {},
12037 );
12038 matcher.suffix_idx = 10;
12039 matcher.last_idx_in_sequence = 10;
12040 matcher.offset_hist = [99, 4, 10];
12042
12043 let candidate = matcher.repcode_candidate(&matcher.window.last().unwrap().data[10..], 0);
12044 assert_eq!(candidate, Some((10, 10)));
12045}
12046
12047#[cfg(any())] #[test]
12049fn simple_matcher_zero_literal_repcode_checks_rep0_minus1() {
12050 let mut matcher = MatchGenerator::new(64);
12051 matcher.add_data(
12052 b"abcdefghijabcdefghij".to_vec(),
12053 SuffixStore::with_capacity(64),
12054 |_, _| {},
12055 );
12056 matcher.suffix_idx = 10;
12057 matcher.last_idx_in_sequence = 10;
12058 matcher.offset_hist = [11, 4, 99];
12060
12061 let candidate = matcher.repcode_candidate(&matcher.window.last().unwrap().data[10..], 0);
12062 assert_eq!(candidate, Some((10, 10)));
12063}
12064
12065#[cfg(any())] #[test]
12067fn simple_matcher_repcode_rejects_offsets_beyond_searchable_prefix() {
12068 let mut matcher = MatchGenerator::new(64);
12069 matcher.add_data(
12070 b"abcdefghij".to_vec(),
12071 SuffixStore::with_capacity(64),
12072 |_, _| {},
12073 );
12074 matcher.skip_matching();
12075 matcher.add_data(
12076 b"klmnopqrst".to_vec(),
12077 SuffixStore::with_capacity(64),
12078 |_, _| {},
12079 );
12080 matcher.suffix_idx = 3;
12081
12082 let candidate = matcher.offset_match_len(14, &matcher.window.last().unwrap().data[3..]);
12083 assert_eq!(candidate, None);
12084}
12085
12086#[cfg(any())] #[test]
12088fn simple_matcher_skip_matching_seeds_every_position_even_with_fast_step() {
12089 let mut matcher = MatchGenerator::new(64);
12090 matcher.hash_fill_step = FAST_HASH_FILL_STEP;
12091 matcher.add_data(
12092 b"abcdefghijklmnop".to_vec(),
12093 SuffixStore::with_capacity(64),
12094 |_, _| {},
12095 );
12096 matcher.skip_matching();
12097 matcher.add_data(b"bcdef".to_vec(), SuffixStore::with_capacity(64), |_, _| {});
12098
12099 assert!(matcher.next_sequence(|seq| {
12100 assert_eq!(
12101 seq,
12102 Sequence::Triple {
12103 literals: b"",
12104 offset: 15,
12105 match_len: 5,
12106 }
12107 );
12108 }));
12109 assert!(!matcher.next_sequence(|_| {}));
12110}
12111
12112#[cfg(any())] #[test]
12114fn simple_matcher_skip_matching_with_incompressible_hint_uses_sparse_prefix() {
12115 let mut matcher = MatchGenerator::new(128);
12116 let first = b"abcdefghijklmnopqrstuvwxyz012345".to_vec();
12117 let sparse_probe = first[3..3 + MIN_MATCH_LEN].to_vec();
12118 let tail_start = first.len() - MIN_MATCH_LEN;
12119 let tail_probe = first[tail_start..tail_start + MIN_MATCH_LEN].to_vec();
12120 matcher.add_data(first, SuffixStore::with_capacity(256), |_, _| {});
12121
12122 matcher.skip_matching_with_hint(Some(true));
12123
12124 matcher.add_data(sparse_probe, SuffixStore::with_capacity(256), |_, _| {});
12126 let mut sparse_first_is_literals = None;
12127 assert!(matcher.next_sequence(|seq| {
12128 if sparse_first_is_literals.is_none() {
12129 sparse_first_is_literals = Some(matches!(seq, Sequence::Literals { .. }));
12130 }
12131 }));
12132 assert!(
12133 sparse_first_is_literals.unwrap_or(false),
12134 "sparse-start probe should not produce an immediate match"
12135 );
12136
12137 let mut matcher = MatchGenerator::new(128);
12139 matcher.add_data(
12140 b"abcdefghijklmnopqrstuvwxyz012345".to_vec(),
12141 SuffixStore::with_capacity(256),
12142 |_, _| {},
12143 );
12144 matcher.skip_matching_with_hint(Some(true));
12145 matcher.add_data(tail_probe, SuffixStore::with_capacity(256), |_, _| {});
12146 let mut tail_first_is_immediate_match = None;
12147 assert!(matcher.next_sequence(|seq| {
12148 if tail_first_is_immediate_match.is_none() {
12149 tail_first_is_immediate_match =
12150 Some(matches!(seq, Sequence::Triple { literals, .. } if literals.is_empty()));
12151 }
12152 }));
12153 assert!(
12154 tail_first_is_immediate_match.unwrap_or(false),
12155 "dense tail probe should match immediately at block start"
12156 );
12157}
12158
12159#[cfg(any())] #[test]
12161fn simple_matcher_add_suffixes_till_backfills_last_searchable_anchor() {
12162 let mut matcher = MatchGenerator::new(64);
12163 matcher.hash_fill_step = FAST_HASH_FILL_STEP;
12164 matcher.add_data(
12165 b"01234abcde".to_vec(),
12166 SuffixStore::with_capacity(64),
12167 |_, _| {},
12168 );
12169 matcher.add_suffixes_till(10, FAST_HASH_FILL_STEP);
12170
12171 let last = matcher.window.last().unwrap();
12172 let tail = &last.data[5..10];
12173 assert_eq!(last.suffixes.get(tail), Some(5));
12174}
12175
12176#[cfg(any())] #[test]
12178fn simple_matcher_add_suffixes_till_skips_when_idx_below_min_match_len() {
12179 let mut matcher = MatchGenerator::new(128);
12180 matcher.hash_fill_step = FAST_HASH_FILL_STEP;
12181 matcher.add_data(
12182 b"abcdefghijklmnopqrstuvwxyz".to_vec(),
12183 SuffixStore::with_capacity(1 << 16),
12184 |_, _| {},
12185 );
12186
12187 matcher.add_suffixes_till(MIN_MATCH_LEN - 1, FAST_HASH_FILL_STEP);
12188
12189 let last = matcher.window.last().unwrap();
12190 let first_key = &last.data[..MIN_MATCH_LEN];
12191 assert_eq!(last.suffixes.get(first_key), None);
12192}
12193
12194#[cfg(any())] #[test]
12196fn simple_matcher_add_suffixes_till_fast_step_registers_interleaved_positions() {
12197 let mut matcher = MatchGenerator::new(128);
12198 matcher.hash_fill_step = FAST_HASH_FILL_STEP;
12199 matcher.add_data(
12200 b"abcdefghijklmnopqrstuvwxyz".to_vec(),
12201 SuffixStore::with_capacity(1 << 16),
12202 |_, _| {},
12203 );
12204
12205 matcher.add_suffixes_till(17, FAST_HASH_FILL_STEP);
12206
12207 let last = matcher.window.last().unwrap();
12208 for pos in [0usize, 3, 6, 9, 12] {
12209 let key = &last.data[pos..pos + MIN_MATCH_LEN];
12210 assert_eq!(
12211 last.suffixes.get(key),
12212 Some(pos),
12213 "expected interleaved suffix registration at pos {pos}"
12214 );
12215 }
12216}
12217
12218#[test]
12219fn dfast_skip_matching_handles_window_eviction() {
12220 let mut matcher = DfastMatchGenerator::new(16);
12221
12222 matcher.add_data(alloc::vec![1, 2, 3, 4, 5, 6], |_| {});
12223 matcher.skip_matching(None);
12224 matcher.add_data(alloc::vec![7, 8, 9, 10, 11, 12], |_| {});
12225 matcher.skip_matching(None);
12226 matcher.add_data(alloc::vec![7, 8, 9, 10, 11, 12], |_| {});
12227
12228 let mut reconstructed = alloc::vec![7, 8, 9, 10, 11, 12];
12229 matcher.start_matching(|seq| match seq {
12230 Sequence::Literals { literals } => reconstructed.extend_from_slice(literals),
12231 Sequence::Triple {
12232 literals,
12233 offset,
12234 match_len,
12235 } => {
12236 reconstructed.extend_from_slice(literals);
12237 let start = reconstructed.len() - offset;
12238 for i in 0..match_len {
12239 let byte = reconstructed[start + i];
12240 reconstructed.push(byte);
12241 }
12242 }
12243 });
12244
12245 assert_eq!(reconstructed, [7, 8, 9, 10, 11, 12, 7, 8, 9, 10, 11, 12]);
12246}
12247
12248#[test]
12249fn dfast_add_data_callback_reports_evicted_len_not_capacity() {
12250 let mut matcher = DfastMatchGenerator::new(8);
12251
12252 let mut first = Vec::with_capacity(64);
12253 first.extend_from_slice(b"abcdefgh");
12254 matcher.add_data(first, |_| {});
12255
12256 let mut second = Vec::with_capacity(64);
12257 second.extend_from_slice(b"ijklmnop");
12258
12259 let mut observed_evicted_len = None;
12260 matcher.add_data(second, |data| {
12261 observed_evicted_len = Some(data.len());
12262 });
12263
12264 assert_eq!(
12265 observed_evicted_len,
12266 Some(8),
12267 "eviction callback must report evicted byte length, not backing capacity"
12268 );
12269}
12270
12271#[test]
12335fn dfast_commit_space_eviction_uses_window_size_delta() {
12336 use crate::encoding::CompressionLevel;
12337
12338 let mut driver = MatchGeneratorDriver::new(10, 1);
12339 driver.reset(CompressionLevel::Level(3));
12340 assert!(matches!(driver.storage, MatcherStorage::Dfast(_)));
12341
12342 driver.dfast_matcher_mut().max_window_size = 10;
12347 driver.dictionary_retained_budget = 100;
12348
12349 let mut space1 = Vec::with_capacity(64);
12350 space1.extend_from_slice(b"abcd");
12351 driver.commit_space(space1);
12352 assert_eq!(
12353 driver.dictionary_retained_budget, 100,
12354 "1st commit fills window 0 → 4, no eviction, no retire"
12355 );
12356
12357 let mut space2 = Vec::with_capacity(64);
12358 space2.extend_from_slice(b"efgh");
12359 driver.commit_space(space2);
12360 assert_eq!(
12361 driver.dictionary_retained_budget, 100,
12362 "2nd commit fills window 4 → 8, no eviction, no retire"
12363 );
12364
12365 let mut space3 = Vec::with_capacity(64);
12366 space3.extend_from_slice(b"ijklm");
12367 driver.commit_space(space3);
12368 assert_eq!(
12369 driver.dictionary_retained_budget, 87,
12370 "3rd commit + trim_after_budget_retire cascade. With the fix \
12371 (evicted=4 from window_size delta) the cascade reclaims 100 \
12372 → 96 → 92 → 87. With the bug (evicted=5 from data.len()) the \
12373 3rd commit would panic on `data.len() <= max_window_size` \
12374 after the 2nd commit's cascade had already shrunk \
12375 max_window_size to 0."
12376 );
12377 assert_eq!(
12378 driver.dfast_matcher_mut().max_window_size,
12379 0,
12380 "cascade drains max_window_size to 0 once budget reclaim \
12381 exceeds the initial window size"
12382 );
12383}
12384
12385#[test]
12386fn dfast_trim_to_window_evicts_oldest_block_by_length() {
12387 let mut matcher = DfastMatchGenerator::new(16);
12396
12397 let mut first = Vec::with_capacity(64);
12398 first.extend_from_slice(b"abcdefgh");
12399 matcher.add_data(first, |_| {});
12400
12401 let mut second = Vec::with_capacity(64);
12402 second.extend_from_slice(b"ijklmnop");
12403 matcher.add_data(second, |_| {});
12404
12405 assert_eq!(matcher.window_size, 16);
12406 assert_eq!(matcher.window_blocks.len(), 2);
12407
12408 matcher.max_window_size = 8;
12409
12410 matcher.trim_to_window();
12411
12412 assert_eq!(
12420 matcher.window_size, 8,
12421 "exactly one 8-byte block must remain"
12422 );
12423 assert_eq!(matcher.window_blocks.len(), 1);
12424 assert_eq!(matcher.history_abs_start, 8);
12425}
12426
12427#[test]
12428fn dfast_inserts_tail_positions_for_next_block_matching() {
12429 let mut matcher = DfastMatchGenerator::new(1 << 22);
12430
12431 matcher.add_data(b"012345bcdea".to_vec(), |_| {});
12432 let mut history = Vec::new();
12433 matcher.start_matching(|seq| match seq {
12434 Sequence::Literals { literals } => history.extend_from_slice(literals),
12435 Sequence::Triple { .. } => unreachable!("first block should not match history"),
12436 });
12437 assert_eq!(history, b"012345bcdea");
12438
12439 matcher.add_data(b"bcdeabcdeab".to_vec(), |_| {});
12440 let mut saw_first_sequence = false;
12441 matcher.start_matching(|seq| {
12442 assert!(!saw_first_sequence, "expected a single cross-block match");
12443 saw_first_sequence = true;
12444 match seq {
12445 Sequence::Literals { .. } => {
12446 panic!("expected tail-anchored cross-block match before any literals")
12447 }
12448 Sequence::Triple {
12449 literals,
12450 offset,
12451 match_len,
12452 } => {
12453 assert_eq!(literals, b"");
12454 assert_eq!(offset, 5);
12455 assert_eq!(match_len, 11);
12456 let start = history.len() - offset;
12457 for i in 0..match_len {
12458 let byte = history[start + i];
12459 history.push(byte);
12460 }
12461 }
12462 }
12463 });
12464
12465 assert!(
12466 saw_first_sequence,
12467 "expected tail-anchored cross-block match"
12468 );
12469 assert_eq!(history, b"012345bcdeabcdeabcdeab");
12470}
12471
12472#[test]
12499fn hashchain_inserts_tail_positions_for_next_block_matching() {
12500 let mut matcher = HcMatchGenerator::new(1 << 22);
12501 matcher.configure(HC_CONFIG, super::strategy::StrategyTag::Lazy, 22);
12502
12503 matcher.table.add_data(b"PQRSTBCD".to_vec(), |_| {});
12504 let mut history = alloc::vec::Vec::new();
12505 matcher.start_matching(|seq| match seq {
12506 Sequence::Literals { literals } => history.extend_from_slice(literals),
12507 Sequence::Triple { .. } => unreachable!("first block has no internal repeats"),
12508 });
12509 assert_eq!(history, b"PQRSTBCD");
12510
12511 matcher.table.add_data(b"BCDBCDBCDB".to_vec(), |_| {});
12512 let mut first_sequence_offset: Option<usize> = None;
12513 let mut first_sequence_match_len: Option<usize> = None;
12514 matcher.start_matching(|seq| {
12515 if first_sequence_offset.is_some() {
12516 return;
12517 }
12518 match seq {
12519 Sequence::Literals { .. } => {
12520 panic!(
12521 "expected tail-anchored cross-block match before any literals — \
12522 backfill_boundary_positions did not seed positions 5/6/7"
12523 )
12524 }
12525 Sequence::Triple {
12526 literals,
12527 offset,
12528 match_len,
12529 } => {
12530 assert_eq!(literals, b"", "no leading literals on the boundary match");
12531 first_sequence_offset = Some(offset);
12532 first_sequence_match_len = Some(match_len);
12533 }
12534 }
12535 });
12536
12537 let offset = first_sequence_offset.expect(
12538 "expected tail-anchored cross-block match emitted from backfill_boundary_positions",
12539 );
12540 assert!(
12541 (1..=3).contains(&offset),
12542 "boundary match offset {offset} must point into the unhashable tail \
12543 (positions 5/6/7 of an 8-byte block 1) so the test specifically \
12544 locks down backfill_boundary_positions",
12545 );
12546 assert_eq!(
12547 offset, 3,
12548 "candidate position must land at 5 (= block_1_len - 3) so the 4-byte \
12549 window `data[5..9] = b\"BCDB\"` matches block 2's first hash lookup",
12550 );
12551 let match_len = first_sequence_match_len.unwrap();
12552 assert!(
12553 match_len >= HC_MIN_MATCH_LEN,
12554 "match_len {match_len} must clear the HC min-match floor",
12555 );
12556}
12557
12558#[test]
12559fn dfast_dense_skip_matching_backfills_previous_tail_for_next_block() {
12560 let mut matcher = DfastMatchGenerator::new(1 << 22);
12561 let tail = b"Qz9kLm2Rp";
12562 let mut first = b"0123456789abcdef".to_vec();
12563 first.extend_from_slice(tail);
12564 matcher.add_data(first.clone(), |_| {});
12565 matcher.skip_matching(Some(false));
12566
12567 let mut second = tail.to_vec();
12568 second.extend_from_slice(b"after-tail-literals");
12569 matcher.add_data(second, |_| {});
12570
12571 let mut first_sequence = None;
12572 matcher.start_matching(|seq| {
12573 if first_sequence.is_some() {
12574 return;
12575 }
12576 first_sequence = Some(match seq {
12577 Sequence::Literals { literals } => (literals.len(), 0usize, 0usize),
12578 Sequence::Triple {
12579 literals,
12580 offset,
12581 match_len,
12582 } => (literals.len(), offset, match_len),
12583 });
12584 });
12585
12586 let (lit_len, offset, match_len) = first_sequence.expect("expected at least one sequence");
12587 assert_eq!(
12588 lit_len, 0,
12589 "expected immediate cross-block match at block start"
12590 );
12591 assert_eq!(
12592 offset,
12593 tail.len(),
12594 "expected dense skip to preserve cross-boundary tail match"
12595 );
12596 assert!(
12597 match_len >= DFAST_MIN_MATCH_LEN,
12598 "match length should satisfy dfast minimum match length"
12599 );
12600}
12601
12602#[test]
12603fn dfast_sparse_skip_matching_preserves_tail_cross_block_match() {
12604 let mut matcher = DfastMatchGenerator::new(1 << 22);
12605 let tail = b"Qz9kLm2Rp";
12606 let mut first = deterministic_high_entropy_bytes(0x9E37_79B9_7F4A_7C15, 4096);
12607 let tail_start = first.len() - tail.len();
12608 first[tail_start..].copy_from_slice(tail);
12609 matcher.add_data(first.clone(), |_| {});
12610
12611 matcher.skip_matching(Some(true));
12612
12613 let mut second = tail.to_vec();
12614 second.extend_from_slice(b"after-tail-literals");
12615 matcher.add_data(second, |_| {});
12616
12617 let mut first_sequence = None;
12618 matcher.start_matching(|seq| {
12619 if first_sequence.is_some() {
12620 return;
12621 }
12622 first_sequence = Some(match seq {
12623 Sequence::Literals { literals } => (literals.len(), 0usize, 0usize),
12624 Sequence::Triple {
12625 literals,
12626 offset,
12627 match_len,
12628 } => (literals.len(), offset, match_len),
12629 });
12630 });
12631
12632 let (lit_len, offset, match_len) = first_sequence.expect("expected at least one sequence");
12633 assert_eq!(
12634 lit_len, 0,
12635 "expected immediate cross-block match at block start"
12636 );
12637 assert_eq!(
12638 offset,
12639 tail.len(),
12640 "expected match against densely seeded tail"
12641 );
12642 assert!(
12643 match_len >= DFAST_MIN_MATCH_LEN,
12644 "match length should satisfy dfast minimum match length"
12645 );
12646}
12647
12648#[test]
12649fn dfast_skip_matching_dense_backfills_newly_hashable_long_tail_positions() {
12650 let mut matcher = DfastMatchGenerator::new(1 << 22);
12651 let first = deterministic_high_entropy_bytes(0x7A64_0315_D4E1_91C3, 4096);
12652 let first_len = first.len();
12653 matcher.add_data(first, |_| {});
12654 matcher.skip_matching_dense();
12655
12656 matcher.add_data(alloc::vec![0xAB], |_| {});
12659 matcher.skip_matching_dense();
12660
12661 let target_abs_pos = first_len - 7;
12662 let target_rel = target_abs_pos - matcher.history_abs_start;
12663 let live = matcher.live_history();
12664 assert!(
12665 target_rel + 8 <= live.len(),
12666 "fixture must make the boundary start long-hashable"
12667 );
12668 let long_hash = matcher.long_hash_index(&live[target_rel..]);
12669 let target_slot = matcher.pack_slot(target_abs_pos);
12670 assert_ne!(
12673 target_slot, DFAST_EMPTY_SLOT,
12674 "pack_slot must never return the empty-slot sentinel for a real position"
12675 );
12676 assert_eq!(
12677 matcher.tables[long_hash], target_slot,
12678 "dense skip must seed long-hash entry for newly hashable boundary start"
12679 );
12680}
12681
12682#[test]
12683fn dfast_seed_remaining_hashable_starts_seeds_last_short_hash_positions() {
12684 let mut matcher = DfastMatchGenerator::new(1 << 20);
12685 let block = deterministic_high_entropy_bytes(0x13F0_9A6D_55CE_7B21, 64);
12686 matcher.add_data(block, |_| {});
12687 matcher.ensure_hash_tables();
12688
12689 let current_len = matcher.window_blocks.back().copied().unwrap_or(0);
12690 let current_abs_start = matcher.history_abs_start + matcher.window_size - current_len;
12691 let seed_start = current_len - DFAST_MIN_MATCH_LEN;
12692 matcher.seed_remaining_hashable_starts(current_abs_start, current_len, seed_start);
12693
12694 let target_abs_pos = current_abs_start + current_len - 5;
12695 let target_rel = target_abs_pos - matcher.history_abs_start;
12696 let live = matcher.live_history();
12697 assert!(
12698 target_rel + 5 <= live.len(),
12699 "fixture must leave the last short-hash start valid"
12700 );
12701 let short_hash = matcher.short_hash_index(&live[target_rel..]);
12702 let target_slot = matcher.pack_slot(target_abs_pos);
12703 assert_ne!(
12704 target_slot, DFAST_EMPTY_SLOT,
12705 "pack_slot must never return the empty-slot sentinel for a real position"
12706 );
12707 assert_eq!(
12708 matcher.tables[matcher.long_len() + short_hash],
12709 target_slot,
12710 "tail seeding must include the last 5-byte-hashable start"
12711 );
12712}
12713
12714#[test]
12715fn dfast_seed_remaining_hashable_starts_handles_pos_at_block_end() {
12716 let mut matcher = DfastMatchGenerator::new(1 << 20);
12717 let block = deterministic_high_entropy_bytes(0x7BB2_DA91_441E_C0EF, 64);
12718 matcher.add_data(block, |_| {});
12719 matcher.ensure_hash_tables();
12720
12721 let current_len = matcher.window_blocks.back().copied().unwrap_or(0);
12722 let current_abs_start = matcher.history_abs_start + matcher.window_size - current_len;
12723 matcher.seed_remaining_hashable_starts(current_abs_start, current_len, current_len);
12724
12725 let target_abs_pos = current_abs_start + current_len - 5;
12726 let target_rel = target_abs_pos - matcher.history_abs_start;
12727 let live = matcher.live_history();
12728 assert!(
12729 target_rel + 5 <= live.len(),
12730 "fixture must leave the last short-hash start valid"
12731 );
12732 let short_hash = matcher.short_hash_index(&live[target_rel..]);
12733 let target_slot = matcher.pack_slot(target_abs_pos);
12734 assert_ne!(
12735 target_slot, DFAST_EMPTY_SLOT,
12736 "pack_slot must never return the empty-slot sentinel for a real position"
12737 );
12738 assert_eq!(
12739 matcher.tables[matcher.long_len() + short_hash],
12740 target_slot,
12741 "tail seeding must still include the last 5-byte-hashable start when pos is at block end"
12742 );
12743}
12744
12745#[test]
12761fn dfast_ensure_room_for_rebases_above_guard_band() {
12762 let mut dfast = DfastMatchGenerator::new(1 << 22);
12763 dfast.set_hash_bits(10, 10);
12764 dfast.ensure_hash_tables();
12765
12766 let early_abs = 1024usize;
12774 let early_packed = dfast.pack_slot(early_abs);
12775 assert_ne!(early_packed, DFAST_EMPTY_SLOT);
12776 let short0 = dfast.long_len();
12777 dfast.tables[short0] = early_packed;
12778 dfast.tables[0] = early_packed;
12779
12780 let trigger_abs = (u32::MAX as usize) - (DFAST_REBASE_GUARD_BAND as usize) + 1;
12786 assert_eq!(dfast.position_base, 0);
12787 dfast.ensure_room_for(trigger_abs);
12788 assert_eq!(
12789 dfast.position_base, DFAST_REBASE_GUARD_BAND as usize,
12790 "rebase must advance position_base by DFAST_REBASE_GUARD_BAND"
12791 );
12792
12793 assert_eq!(
12799 dfast.tables[dfast.long_len()],
12800 DFAST_EMPTY_SLOT,
12801 "pre-rebase short-hash entries below the reducer must become empty"
12802 );
12803 assert_eq!(
12804 dfast.tables[0], DFAST_EMPTY_SLOT,
12805 "pre-rebase long-hash entries below the reducer must become empty"
12806 );
12807
12808 let post_packed = dfast.pack_slot(trigger_abs);
12812 assert_ne!(post_packed, DFAST_EMPTY_SLOT);
12813 let unpacked = dfast.position_base + (post_packed as usize) - 1;
12814 assert_eq!(
12815 unpacked, trigger_abs,
12816 "post-rebase pack/unpack must round-trip the absolute position"
12817 );
12818}
12819
12820#[test]
12821fn dfast_sparse_skip_matching_backfills_previous_tail_for_consecutive_sparse_blocks() {
12822 let mut matcher = DfastMatchGenerator::new(1 << 22);
12823 let boundary_prefix = [0xFA, 0xFB, 0xFC];
12824 let boundary_suffix = [0xFD, 0xEE, 0xAD, 0xBE, 0xEF, 0x11, 0x22, 0x33];
12825
12826 let mut first = deterministic_high_entropy_bytes(0xA5A5_5A5A_C3C3_3C3C, 4096);
12827 let first_tail_start = first.len() - boundary_prefix.len();
12828 first[first_tail_start..].copy_from_slice(&boundary_prefix);
12829 matcher.add_data(first, |_| {});
12830 matcher.skip_matching(Some(true));
12831
12832 let mut second = deterministic_high_entropy_bytes(0xA5A5_5A5A_C3C3_3C3C, 4096);
12833 second[..boundary_suffix.len()].copy_from_slice(&boundary_suffix);
12834 matcher.add_data(second.clone(), |_| {});
12835 matcher.skip_matching(Some(true));
12836
12837 let mut third = boundary_prefix.to_vec();
12838 third.extend_from_slice(&boundary_suffix);
12839 third.extend_from_slice(b"-trailing-literals");
12840 matcher.add_data(third, |_| {});
12841
12842 let mut first_sequence = None;
12843 matcher.start_matching(|seq| {
12844 if first_sequence.is_some() {
12845 return;
12846 }
12847 first_sequence = Some(match seq {
12848 Sequence::Literals { literals } => (literals.len(), 0usize, 0usize),
12849 Sequence::Triple {
12850 literals,
12851 offset,
12852 match_len,
12853 } => (literals.len(), offset, match_len),
12854 });
12855 });
12856
12857 let (lit_len, offset, match_len) = first_sequence.expect("expected at least one sequence");
12858 assert_eq!(
12859 lit_len, 0,
12860 "expected immediate match from the prior sparse-skip boundary"
12861 );
12862 assert_eq!(
12863 offset,
12864 second.len() + boundary_prefix.len(),
12865 "expected match against backfilled first→second boundary start"
12866 );
12867 assert!(
12868 match_len >= DFAST_MIN_MATCH_LEN,
12869 "match length should satisfy dfast minimum match length"
12870 );
12871}
12872
12873#[test]
12874fn fastest_hint_iteration_23_sequences_reconstruct_source() {
12875 fn generate_data(seed: u64, len: usize) -> Vec<u8> {
12876 let mut state = seed;
12877 let mut data = Vec::with_capacity(len);
12878 for _ in 0..len {
12879 state = state
12880 .wrapping_mul(6364136223846793005)
12881 .wrapping_add(1442695040888963407);
12882 data.push((state >> 33) as u8);
12883 }
12884 data
12885 }
12886
12887 let i = 23u64;
12888 let len = (i * 89 % 16384) as usize;
12889 let mut data = generate_data(i, len);
12890 let repeat = data[128..256].to_vec();
12893 data.extend_from_slice(&repeat);
12894 data.extend_from_slice(&repeat);
12895
12896 let mut driver = MatchGeneratorDriver::new(1024 * 128, 1);
12897 driver.set_source_size_hint(data.len() as u64);
12898 driver.reset(CompressionLevel::Fastest);
12899 let mut space = driver.get_next_space();
12900 space[..data.len()].copy_from_slice(&data);
12901 space.truncate(data.len());
12902 driver.commit_space(space);
12903
12904 let mut rebuilt = Vec::with_capacity(data.len());
12905 let mut saw_triple = false;
12906 driver.start_matching(|seq| match seq {
12907 Sequence::Literals { literals } => rebuilt.extend_from_slice(literals),
12908 Sequence::Triple {
12909 literals,
12910 offset,
12911 match_len,
12912 } => {
12913 saw_triple = true;
12914 rebuilt.extend_from_slice(literals);
12915 assert!(offset > 0, "offset must be non-zero");
12916 assert!(
12917 offset <= rebuilt.len(),
12918 "offset must reference already-produced bytes: offset={} produced={}",
12919 offset,
12920 rebuilt.len()
12921 );
12922 let start = rebuilt.len() - offset;
12923 for idx in 0..match_len {
12924 let b = rebuilt[start + idx];
12925 rebuilt.push(b);
12926 }
12927 }
12928 });
12929
12930 let _ = saw_triple;
12940 assert_eq!(rebuilt, data);
12941}
12942
12943#[test]
12944fn fast_levels_dispatch_per_level_hash_log_and_mls() {
12945 let f1 = resolve_level_params(CompressionLevel::Level(1), None)
12948 .fast
12949 .unwrap();
12950 assert_eq!(f1.hash_log, 14);
12951 assert_eq!(f1.mls, 7);
12952 assert_eq!(f1.step_size, 2);
12953
12954 for n in -7..=-1 {
12962 let f = resolve_level_params(CompressionLevel::Level(n), None)
12963 .fast
12964 .unwrap();
12965 assert_eq!(f.hash_log, 13, "Level({n}) fast_hash_log");
12966 assert_eq!(f.mls, 7, "Level({n}) fast_mls");
12967 let expected_step = ((-n) as usize) + 1;
12968 assert_eq!(f.step_size, expected_step, "Level({n}) fast_step_size");
12969 }
12970
12971 let pf = resolve_level_params(CompressionLevel::Fastest, None);
12974 let ff = pf.fast.unwrap();
12975 assert_eq!(
12976 (pf.window_log, ff.hash_log, ff.mls, ff.step_size),
12977 (19, 14, 6, 2),
12978 );
12979 let pu = resolve_level_params(CompressionLevel::Uncompressed, None);
12982 let fu = pu.fast.unwrap();
12983 assert_eq!(
12984 (pu.window_log, fu.hash_log, fu.mls, fu.step_size),
12985 (17, 14, 6, 2),
12986 );
12987}
12988
12989#[test]
12997fn fast_levels_driver_wiring_threads_cparams_into_inner_matcher() {
12998 let mut driver = MatchGeneratorDriver::new(64 * 1024, 1);
12999
13000 let fast_levels = [
13001 CompressionLevel::Level(1),
13002 CompressionLevel::Fastest,
13003 CompressionLevel::Uncompressed,
13004 CompressionLevel::Level(-1),
13005 CompressionLevel::Level(-2),
13006 CompressionLevel::Level(-3),
13007 CompressionLevel::Level(-4),
13008 CompressionLevel::Level(-5),
13009 CompressionLevel::Level(-6),
13010 CompressionLevel::Level(-7),
13011 ];
13012
13013 for &level in &fast_levels {
13014 let p = resolve_level_params(level, None);
13015 assert_eq!(
13019 p.strategy_tag,
13020 super::strategy::StrategyTag::Fast,
13021 "{level:?} must resolve to Fast strategy",
13022 );
13023
13024 crate::encoding::Matcher::reset(&mut driver, CompressionLevel::Default);
13034
13035 crate::encoding::Matcher::reset(&mut driver, level);
13038
13039 let f = p.fast.unwrap();
13040 let m = driver.simple_mut();
13041 assert_eq!(
13042 m.hash_log(),
13043 f.hash_log,
13044 "{level:?}: inner matcher hash_log mismatch — argument swap?",
13045 );
13046 assert_eq!(
13047 m.mls(),
13048 f.mls,
13049 "{level:?}: inner matcher mls mismatch — argument swap?",
13050 );
13051 assert_eq!(
13052 m.step_size(),
13053 f.step_size,
13054 "{level:?}: inner matcher step_size mismatch — stale value carried from prior reset?",
13055 );
13056 }
13057}
13058
13059#[test]
13072fn lazy_band_target_len_matches_default_table() {
13073 let expected: [(i32, usize); 11] = [
13076 (5, 2),
13077 (6, 4),
13078 (7, 8),
13079 (8, 16),
13080 (9, 16),
13081 (10, 16),
13082 (11, 16),
13083 (12, 32),
13084 (13, 32),
13085 (14, 32),
13086 (15, 32),
13087 ];
13088 for (level, want) in expected {
13089 let params = resolve_level_params(CompressionLevel::Level(level), None);
13090 let target_len = params
13092 .hc
13093 .map(|hc| hc.target_len)
13094 .or_else(|| params.row.map(|row| row.target_len))
13095 .expect("lazy/greedy level carries hc or row config");
13096 assert_eq!(target_len, want, "L{level}: target_len must match table[0]");
13097 }
13098}
13099
13100#[test]
13109fn upper_lazy_band_params_match_default_table() {
13110 let expected: [(i32, u8, usize, usize, usize); 3] = [
13113 (13, 22, 22, 22, 1 << 4),
13114 (14, 22, 23, 22, 1 << 5),
13115 (15, 22, 23, 23, 1 << 6),
13116 ];
13117 for (level, wlog, hlog, clog, sd) in expected {
13118 let params = resolve_level_params(CompressionLevel::Level(level), None);
13119 let hc = params.hc.unwrap();
13120 assert_eq!(hc.search_depth, sd, "L{level}: search_depth");
13121 assert_eq!(params.window_log, wlog, "L{level}: window_log");
13122 assert_eq!(hc.hash_log, hlog, "L{level}: hash_log");
13123 assert_eq!(hc.chain_log, clog, "L{level}: chain_log");
13124 }
13125}