structured_zstd/encoding/match_generator/mod.rs
1//! Matching algorithm used find repeated parts in the original data
2//!
3//! The Zstd format relies on finden repeated sequences of data and compressing these sequences as instructions to the decoder.
4//! A sequence basically tells the decoder "Go back X bytes and copy Y bytes to the end of your decode buffer".
5//!
6//! The task here is to efficiently find matches in the already encoded data for the current suffix of the not yet encoded data.
7
8use alloc::vec::Vec;
9// SIMD/CRC intrinsics now live in `crate::encoding::fastpath::*` where they
10// sit under per-CPU `#[target_feature]` umbrellas; no architecture-specific
11// intrinsic imports remain in this file.
12use super::CompressionLevel;
13use super::Matcher;
14use super::Sequence;
15use super::cost_model::HC_FORMAT_MINMATCH;
16#[cfg(test)]
17use super::cost_model::HC_MAX_LIT;
18#[cfg(test)]
19use super::cost_model::{
20 HC_BITCOST_MULTIPLIER, HC_OPT_NUM, HC_PREDEF_THRESHOLD, HcOptState, HcOptimalCostProfile,
21};
22#[cfg(test)]
23use super::cost_model::{HC_BLOCKSIZE_MAX, HC_MAX_LL, HC_MAX_ML, HC_MAX_OFF, HcOptPriceType};
24use super::dfast::DfastMatchGenerator;
25#[cfg(test)]
26use super::hc::HC_MIN_MATCH_LEN;
27#[cfg(test)]
28use super::match_table::storage::HC3_HASH_LOG;
29// FAST_HASH_FILL_STEP test-only re-export was tied to the legacy
30// SuffixStore MatchGenerator's interleaved hash-fill stride. The
31// upstream zstd-shape Fast kernel walks ip0 with kSearchStrength step-skip
32// acceleration instead, so the constant has no consumer in the
33// remaining live test set today.
34#[cfg(test)]
35use super::match_table::helpers::INCOMPRESSIBLE_SKIP_STEP;
36use super::match_table::helpers::MIN_MATCH_LEN;
37#[cfg(test)]
38use super::match_table::helpers::common_prefix_len;
39#[cfg(test)]
40use super::opt::ldm::HcRawSeq;
41#[cfg(test)]
42use super::opt::types::{HcCandidateQuery, MatchCandidate};
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;
56// Bytes the dfast short hash reads (upstream zstd `mls = 5`). Seeding / lookahead
57// guards use it so a position is only short-hashed once its full 5-byte key
58// is in range.
59pub(crate) const DFAST_SHORT_HASH_LOOKAHEAD: usize = 5;
60pub(crate) const ROW_MIN_MATCH_LEN: usize = 5;
61// Upstream zstd `clevels.h:31` at level 3 large-input bucket sets
62// `hashLog = 17` (the long-hash table) and `chainLog = 16` (the
63// short-hash table — upstream zstd names this `chainTable` even though for
64// dfast it's used as a plain single-slot hash). Each table holds one
65// `U32` per slot; the upstream zstd overwrites on collision and recovers
66// compression quality via the inline `_search_next_long` retry
67// (after a short-hash hit, probes `hashLong[hl1]` at `ip + 1` and
68// keeps the longer match).
69//
70// We mirror that storage layout: single `u32` per bucket (no
71// `[u32; N]` array), `long_hash` sized `1 << DFAST_HASH_BITS` and
72// `short_hash` one bit smaller via `DFAST_SHORT_HASH_BITS_DELTA`.
73// Two-table footprint at Level 3: `2^17 × 4 + 2^16 × 4 = 768 KiB`,
74// exact upstream parity. The `_search_next_long` retry lives in
75// `DfastMatchGenerator::hash_candidate` (called via
76// `best_match`). Earlier revisions kept a
77// 4-slot bucket per hash position; that paid 4× the upstream zstd memory
78// without measurable ratio gain once the retry was in place.
79//
80// `dfast_hash_bits_for_window` still clamps the runtime long-hash
81// value to `[MIN_WINDOW_LOG, DFAST_HASH_BITS]`, so this const is the
82// upper bound rather than a fixed default.
83pub(crate) const DFAST_HASH_BITS: usize = 17;
84/// Difference between `long_hash_bits` and `short_hash_bits` —
85/// upstream zstd `hashLog - chainLog` is 1 at every dfast level (`clevels.h`
86/// level 2: 16-15=1; level 3: 17-16=1). The short hash is one bit
87/// smaller than the long hash so the per-bucket footprint matches
88/// upstream zstd sizing exactly.
89pub(crate) const DFAST_SHORT_HASH_BITS_DELTA: usize = 1;
90/// Sentinel value for an empty slot in the dfast hash tables. Real
91/// positions are stored as `(abs_pos - position_base + 1) as u32`, so
92/// `0` is reserved as the "empty" marker and a true relative offset
93/// of `0` never appears in the table. Mirrors the LDM table's
94/// `LdmEntry.offset == 0` convention (see `encoding/ldm/table.rs`)
95/// so both rebasing structures share
96/// one sentinel scheme.
97pub(crate) const DFAST_EMPTY_SLOT: u32 = 0;
98
99/// Guard band reserved above the high-water mark before triggering a
100/// rebase on the Dfast hash tables. When the next insert would push a
101/// relative offset above `u32::MAX - DFAST_REBASE_GUARD_BAND`, the
102/// table calls `reduce(GUARD_BAND)` to shift every slot down and
103/// advance `position_base` so future inserts stay inside the `u32`
104/// window. Same scheme as `encoding/ldm/table.rs`.
105pub(crate) const DFAST_REBASE_GUARD_BAND: u32 = 1u32 << 30;
106// `kSearchStrength` (upstream `zstd_compress_internal.h:32`). The dfast step
107// ramp grows one position every `1 << kSearchStrength` = 256 bytes travelled
108// (upstream `kStepIncr`, zstd_double_fast.c:131). A smaller value accelerates
109// the scan faster and skips source positions upstream still inserts, which
110// drops the short matches upstream finds at a block start — so the
111// `#167`-disabled path must use the upstream 8 to stay byte-identical.
112pub(crate) const DFAST_SKIP_SEARCH_STRENGTH: usize = 8;
113pub(crate) const DFAST_SKIP_STEP_GROWTH_INTERVAL: usize = 1 << DFAST_SKIP_SEARCH_STRENGTH;
114pub(crate) const DFAST_INCOMPRESSIBLE_SKIP_STEP: usize = 16;
115pub(crate) const ROW_HASH_BITS: usize = 20;
116pub(crate) const ROW_LOG: usize = 5;
117pub(crate) const ROW_SEARCH_DEPTH: usize = 16;
118pub(crate) const ROW_TARGET_LEN: usize = 48;
119pub(crate) const ROW_TAG_BITS: usize = 8;
120pub(crate) const ROW_EMPTY_SLOT: u32 = u32::MAX;
121pub(crate) const ROW_HASH_KEY_LEN: usize = 4;
122// HASH_MIX_PRIME now lives in `crate::encoding::fastpath::scalar`; the four
123// per-CPU `hash_mix_u64` variants share it via that module.
124// HC_PRIME3BYTES / HC_PRIME4BYTES moved to match_table::storage
125// alongside the hash helpers in Phase 1e Stage A. Only the test
126// module references the constants directly (production code goes
127// through `MatchTable::hash_value_with_mls`).
128#[cfg(test)]
129use super::match_table::storage::{HC_PRIME3BYTES, HC_PRIME4BYTES};
130
131// HC_HASH_LOG / HC_CHAIN_LOG / HC3_HASH_LOG / HC_EMPTY live on the
132// shared storage module so MatchTable methods can reference them
133// without pulling in this module. Re-imported here so existing
134// macros / configs / tests keep their unqualified names.
135#[cfg(test)]
136use super::match_table::storage::HC_EMPTY;
137// HC3_MAX_OFFSET moved to encoding::bt alongside the hash3 candidate
138// probe macro that consumes it; the macro references it via the
139// fully-qualified `$crate::encoding::bt::HC3_MAX_OFFSET` path so this
140// module no longer needs a local import.
141pub(crate) const HC_SEARCH_DEPTH: usize = 16;
142// HC_MIN_MATCH_LEN moved to encoding::hc; re-imported here so
143// existing references compile unchanged.
144pub(crate) const HC_OPT_MIN_MATCH_LEN: usize = HC_FORMAT_MINMATCH;
145pub(crate) const HC_TARGET_LEN: usize = 48;
146
147// MAX_HC_SEARCH_DEPTH moved to encoding::hc alongside chain_candidates.
148// Per-level tuning config (the config structs + `LEVEL_TABLE` + the
149// level→params resolution chain) lives in `levels::config`; the driver imports
150// that resolution API here.
151use super::levels::config::*;
152// The HashChain / BT match generator + its optimal-parse machinery lives in
153// `hc::generator`; the driver stores it in the `HashChain` storage variant.
154use super::hc::generator::HcMatchGenerator;
155
156// Dictionary prime + CDict-equivalent snapshot lifecycle. A child module so it
157// can reach the driver's private `primed` / `reset_shape` state directly; the
158// `Matcher` trait's dict entry points forward to its inherent `*_impl` helpers.
159mod dict_prime;
160
161// `Strategy` and `StrategyTag` live in `crate::encoding::strategy`.
162// The driver carries a `StrategyTag` field set at `reset()` and
163// dispatches each block into a monomorphised `compress_block::<S>`
164// per concrete strategy.
165
166/// Backend storage for [`MatchGeneratorDriver`]. Exactly one match-finder
167/// state lives in the driver at a time — the active variant. Backend
168/// transitions in [`Matcher::reset`] drain the current variant's allocations
169/// into the shared `vec_pool` and then replace `storage` with a freshly
170/// constructed variant for the new backend.
171///
172/// Replaces the prior pattern of four parallel fields (`match_generator`,
173/// `dfast_match_generator: Option<…>`, `row_match_generator: Option<…>`,
174/// `hc_match_generator: Option<…>`) + an `active_backend: BackendTag`
175/// discriminator: the parallel layout kept drained inner structures
176/// allocated across backend switches, and every per-frame/per-slice
177/// driver operation had to dispatch on `active_backend` to pick the
178/// right field. A single enum collapses the storage and makes the
179/// dispatcher pattern-match on the storage variant directly — same
180/// number of arms, but `storage.backend()` is now the canonical source
181/// of truth and dead variants are dropped when the active backend
182/// changes.
183#[derive(Clone)]
184enum MatcherStorage {
185 /// Upstream zstd `ZSTD_fast` family. Constructed by
186 /// [`MatchGeneratorDriver::new`] as the initial variant and
187 /// re-selected by [`Matcher::reset`] for any [`CompressionLevel`]
188 /// that `resolve_level_params` maps to [`StrategyTag::Fast`]
189 /// (`Uncompressed`, `Fastest`, `Level(1)`, and any non-positive
190 /// `Level(n)` not equal to `0`).
191 Simple(FastKernelMatcher),
192 /// Upstream zstd `ZSTD_dfast` family — two-table hash chain. Selected for
193 /// any level that resolves to [`StrategyTag::Dfast`] in
194 /// `resolve_level_params` (`Default`, `Level(0)`, `Level(2)`,
195 /// `Level(3)`).
196 Dfast(DfastMatchGenerator),
197 /// Upstream zstd `ZSTD_greedy` family with row hashing. Selected for any
198 /// level that resolves to [`StrategyTag::Greedy`] (currently
199 /// `Level(4)` only).
200 Row(RowMatchGenerator),
201 /// Upstream zstd `ZSTD_lazy2` and the BT-based optimal modes
202 /// (`btopt` / `btultra` / `btultra2`). Selected for any level that
203 /// resolves to [`StrategyTag::Lazy`], [`StrategyTag::BtOpt`],
204 /// [`StrategyTag::BtUltra`], or [`StrategyTag::BtUltra2`]
205 /// (`Better`, `Best`, `Level(5..=22)`, and any `Level(n)` with
206 /// `n > MAX_LEVEL` — `resolve_level_params` clamps positive
207 /// numeric levels at `MAX_LEVEL = 22` via
208 /// `Level(n).clamp(1, MAX_LEVEL)`, so `Level(23..=i32::MAX)` all
209 /// land on `BtUltra2` here). The [`HcMatchGenerator`]'s internal
210 /// [`HcBackend`] discriminator decides whether BT scratch is
211 /// allocated.
212 HashChain(HcMatchGenerator),
213}
214
215impl MatcherStorage {
216 /// Heap bytes the active backend variant holds (tables, history, scratch).
217 fn heap_size(&self) -> usize {
218 match self {
219 Self::Simple(m) => m.heap_size(),
220 Self::Dfast(m) => m.heap_size(),
221 Self::Row(m) => m.heap_size(),
222 Self::HashChain(m) => m.heap_size(),
223 }
224 }
225
226 /// [`super::strategy::BackendTag`] family of the active variant.
227 fn backend(&self) -> super::strategy::BackendTag {
228 use super::strategy::BackendTag;
229 match self {
230 Self::Simple(_) => BackendTag::Simple,
231 Self::Dfast(_) => BackendTag::Dfast,
232 Self::Row(_) => BackendTag::Row,
233 Self::HashChain(_) => BackendTag::HashChain,
234 }
235 }
236}
237
238/// This is the default implementation of the `Matcher` trait. It allocates and reuses the buffers when possible.
239pub struct MatchGeneratorDriver {
240 vec_pool: Vec<Vec<u8>>,
241 /// Active match-finder state. Exactly one backend lives here at a
242 /// time; [`Matcher::reset`] drains the previous variant into
243 /// `vec_pool` before swapping in a freshly constructed variant for
244 /// the new backend. `storage.backend()` is the canonical source of
245 /// truth for the parse family; `strategy_tag` carries the
246 /// compile-time strategy chosen at the last `reset()`.
247 storage: MatcherStorage,
248 // Compile-time strategy tag resolved at `reset()` from the
249 // requested `CompressionLevel`'s `LevelParams`. The driver's
250 // hot-block dispatcher in `blocks/compressed.rs` matches on
251 // this tag to enter the corresponding `Strategy`
252 // monomorphisation (`compress_block::<S>`).
253 strategy_tag: super::strategy::StrategyTag,
254 // Decoupled search-method axis resolved at `reset()` from
255 // `LevelParams.search`. The per-block dispatcher routes on this
256 // (not on `strategy_tag`) so a level's parse and search backend can
257 // be chosen independently. The `BinaryTree` arm still consults
258 // `strategy_tag` to pick the opt `Strategy` ZST.
259 search: super::strategy::SearchMethod,
260 // Decoupled parse-mode axis resolved at `reset()` from
261 // `LevelParams::parse()`. Independent of `search`: greedy / lazy /
262 // lazy2 can run on any non-opt search backend. The backends still
263 // read their own `lazy_depth` (kept in sync at `reset()`); this is
264 // the authoritative parse selector for the dispatcher.
265 pub(crate) parse: super::strategy::ParseMode,
266 /// Test-only per-level recipe override applied in `reset()` before
267 /// backend selection. Lets the parse×search matrix be exercised
268 /// without editing `LEVEL_TABLE`; never compiled into production.
269 #[cfg(test)]
270 config_override: Option<(super::strategy::SearchMethod, super::strategy::ParseMode)>,
271 /// Fine-grained per-knob overrides from the public
272 /// [`super::parameters::CompressionParameters`] surface (#27).
273 /// `None` (or an all-`None` [`super::parameters::ParamOverrides`])
274 /// keeps the resolved level geometry byte-identical to plain
275 /// level-based compression. Applied in [`Matcher::reset`] after the
276 /// level params are resolved, before backend selection. Persists
277 /// across resets (it is frame configuration, not a one-shot) until
278 /// the caller changes it.
279 param_overrides: Option<super::parameters::ParamOverrides>,
280 slice_size: usize,
281 base_slice_size: usize,
282 // Frame header window size must stay at the configured live-window budget.
283 // Dictionary retention expands internal matcher capacity only.
284 reported_window_size: usize,
285 // Tracks currently retained bytes that originated from primed dictionary
286 // history and have not been evicted yet.
287 dictionary_retained_budget: usize,
288 // Source size hint for next frame (set via set_source_size_hint, cleared on reset).
289 source_size_hint: Option<u64>,
290 // Dictionary content size for the next frame (set via set_dictionary_size_hint,
291 // consumed on reset). When present on a binary-tree / hash-chain backend, the
292 // match-finder hash/chain tables are sized from the DICTIONARY (upstream zstd CDict
293 // economics: a loaded dictionary supplies the long matches, so the live tables
294 // can shrink to the dict's size tier) while the eviction window stays
295 // source-sized. Mirrors upstream zstd `ZSTD_getCParamRowSize`, which picks the cParams
296 // table column from `dictSize` for a dictionary-bearing compress.
297 dictionary_size_hint: Option<usize>,
298 // Normalized `ceil_log2` bucket of the frame's source-size hint, captured at
299 // `reset` (where `source_size_hint` is consumed) via [`source_size_ceil_log`].
300 // `None` means the frame was unhinted. Drives `prime_with_dictionary`'s upstream zstd
301 // `ZSTD_shouldAttachDict` mode for the Simple/Fast backend: `None` (unknown)
302 // or `<= FAST_ATTACH_DICT_CUTOFF_LOG` → attach (separate dict table, 2-cursor
303 // `compress_block_fast_dict`); larger → copy (dictionary primed into the live
304 // table, 4-cursor `compress_block_fast`). The primed-snapshot key is the
305 // resolved shape ([`reset_shape`](Self::reset_shape)), not this bucket.
306 reset_size_log: Option<u8>,
307 // Whether the loaded dictionary fits the Fast attach path's tagged position
308 // field (`<= MAX_FAST_ATTACH_DICT_REGION`). Captured at `reset` from the
309 // dict-size hint (which equals the actual dict length on load) so the Fast
310 // attach decision, the attach-epoch reset bit, and the primed-snapshot
311 // `fast_attach` bit all gate on it consistently. `true` when there is no
312 // dictionary (the attach path is then unused). A dict too large to tag falls
313 // back to copy mode instead of overflowing the packed position.
314 reset_dict_attach_ok: bool,
315 // Hint-resolved matcher shape from the last `reset`: the [`LevelParams`], the
316 // active backend's applied Dfast/Row hash-table width (`0` for HC/Fast), the
317 // Fast attach-vs-copy mode, and the active LDM override (#27). Combined with
318 // the frame's level into the [`PrimedKey`] that keys the primed snapshot, so
319 // it is only restored into a reset that resolved the identical matcher AND
320 // LDM configuration. `None` before the first `reset`.
321 reset_shape: Option<(
322 LevelParams,
323 usize,
324 bool,
325 Option<super::parameters::LdmOverride>,
326 )>,
327 // One-shot borrowed block range `[start, end)` staged by the borrowed
328 // Fast frame path (`set_borrowed_block`) for the NEXT
329 // `start_matching` / `skip_matching_with_hint`. `Some` routes that
330 // call to the Simple backend's borrowed scan instead of the owned
331 // committed-block path; consumed (reset to `None`) by the routed
332 // call. Always `None` on the owned streaming path.
333 borrowed_pending: Option<(usize, usize)>,
334 /// CDict-equivalent: snapshot of the post-prime matcher state taken
335 /// once after the first dictionary prime — the backend `storage`
336 /// (hash tables + dictionary history + offset history + window) plus
337 /// the driver-level `dictionary_retained_budget`, the only two pieces
338 /// `prime_with_dictionary` writes. Subsequent frames restore this
339 /// (a table memcpy) instead of re-hashing every dictionary position,
340 /// mirroring upstream zstd `ZSTD_compressBegin_usingCDict` copying the
341 /// precomputed `cdict->matchState`. Invalidated when the dictionary
342 /// changes; keyed by the [`PrimedKey`] resolved matcher shape so a snapshot
343 /// is only restored into a reset that produces the same matcher — see
344 /// `restore_primed_dictionary`.
345 primed: Option<(MatcherStorage, usize, PrimedKey)>,
346}
347
348/// Identity of the matcher configuration a primed snapshot was captured under:
349/// the FULLY RESOLVED matcher shape, not the raw source-size hint.
350///
351/// `reset()` resolves the hint into a [`LevelParams`] (window_log cap, the
352/// HC/Fast table and search geometry, the parse depth/target-length that get
353/// baked into the restored `storage`) plus, for the Dfast/Row backends, a
354/// table-width derived from the hint's ceil-log bucket. The mapping from hint
355/// to resolved shape is many-to-one: the source-size adjustment is monotone in
356/// `ceil_log2(hint)`, and Level 22 additionally collapses several buckets onto
357/// one upstream zstd tier (its `<= 16/128/256 KiB` thresholds). Keying on the raw hint
358/// (or even its ceil-log bucket) therefore over-keys — two hints that resolve
359/// to the identical matcher would each force a full re-prime. Keying on the
360/// resolved (`params`, `table_bits`) pair restores across them.
361///
362/// `table_bits` is the hint-dependent hash-table width the ACTIVE backend
363/// applied (`set_hash_bits` value for Dfast/Row; `0` for HC/Fast, whose widths
364/// already live in `params`). The snapshot is only ever captured on the COPY
365/// path (a hinted, above-cutoff frame), so `table_bits` is always the resolved
366/// Dfast/Row value there, never the unhinted default.
367///
368/// `level` is kept alongside the resolved `params` because some stored matcher
369/// state is derived from the level DIRECTLY, not through `params`: e.g. Dfast's
370/// `use_fast_loop` is true for L3 but false for L4, yet L3 and L4 resolve to
371/// byte-identical `params`. Without `level` a snapshot captured at L3 could be
372/// restored into an L4 reset, installing the wrong `use_fast_loop`.
373///
374/// `fast_attach` records the Fast backend's attach-vs-copy mode
375/// ([`FAST_ATTACH_DICT_CUTOFF_LOG`]) because that cutoff (8 KiB) falls INSIDE a
376/// single resolved shape: an 8192- and an 8193-byte Level 1 hint both clamp to
377/// window_log 14 with identical `params`/`table_bits`, yet 8192 attaches (a
378/// separate dict table) while 8193 copies into the live table — two different
379/// `storage` shapes. The frame compressor only captures/restores snapshots on
380/// the copy path today, but keying on the mode keeps the snapshot identity
381/// self-sufficient rather than relying on that external gate.
382///
383/// Restoring a snapshot whose key differs would reinstate the old `storage`
384/// (and its `max_window_size` / table dimensions / parse params / dict-table
385/// shape) under a reset that resolved a different shape — the encoder could
386/// then search past the frame header's window and emit an undecodable match.
387/// All fields must match before a restore is allowed.
388#[derive(Clone, Copy, PartialEq, Eq)]
389struct PrimedKey {
390 level: super::CompressionLevel,
391 params: LevelParams,
392 table_bits: usize,
393 fast_attach: bool,
394 /// Fine-grained LDM override (#27) active at capture time. The
395 /// snapshot's cloned `storage` carries `BtMatcher::ldm_producer`,
396 /// which is configured from this override; restoring a snapshot
397 /// captured under a different LDM configuration (enable flip or
398 /// changed knobs) would reinstate a stale producer. `params` already
399 /// pins `window_log` / `strategy_tag` (the rest of the producer's
400 /// identity), so folding the override completes the LDM identity.
401 /// `None` = LDM off, matching `ParamOverrides::ldm`.
402 ldm: Option<super::parameters::LdmOverride>,
403}
404
405impl MatchGeneratorDriver {
406 /// `slice_size` sets the base block allocation size used for matcher input chunks.
407 /// `max_slices_in_window` determines the initial window capacity at construction
408 /// time. Effective window sizing is recalculated on every [`reset`](Self::reset)
409 /// from the resolved compression level and optional source-size hint.
410 pub(crate) fn new(slice_size: usize, max_slices_in_window: usize) -> Self {
411 // Validate inputs before deriving window_log_init. Three
412 // failure modes need explicit guards:
413 //
414 // 1. Zero args → `max_window_size = 0` → silent 1-byte
415 // degenerate window (useless).
416 // 2. Multiplication overflow on `slice_size *
417 // max_slices_in_window` → wraps silently in release.
418 // 3. `next_power_of_two` overflow when the product is
419 // above `1 << (usize::BITS - 1)` → modern Rust PANICS
420 // on overflow (older Rust returned 0).
421 //
422 // Catch all three at construction with a clear domain-
423 // specific message via `assert!` + `checked_mul` +
424 // `checked_next_power_of_two`, rather than letting either
425 // mode produce a silent degenerate matcher OR a generic
426 // panic deep in `FastKernelMatcher::with_params`.
427 assert!(
428 slice_size > 0,
429 "MatchGeneratorDriver::new requires slice_size > 0 (got 0)",
430 );
431 assert!(
432 max_slices_in_window > 0,
433 "MatchGeneratorDriver::new requires max_slices_in_window > 0 (got 0)",
434 );
435 let max_window_size = max_slices_in_window
436 .checked_mul(slice_size)
437 .expect("MatchGeneratorDriver::new: slice_size * max_slices_in_window overflows usize");
438 // Derive an effective window_log for the initial-state matcher.
439 // `MatchGeneratorDriver::new` runs BEFORE any reset, so it has
440 // no LevelParams to consult — we initialise to whatever
441 // window_log fits the caller's requested max_window_size
442 // (round up to the next power of two via `next_power_of_two`'s
443 // log). Reset() overwrites all three params from the resolved
444 // LevelParams.
445 //
446 // `checked_next_power_of_two` returns `None` if the next power
447 // of two would overflow `usize`. Modern Rust's
448 // `next_power_of_two` PANICS on overflow rather than returning
449 // 0 (the panic message is generic and unhelpful), so use the
450 // checked variant to surface the failure with a clear,
451 // domain-specific error.
452 let next_pow2 = max_window_size.checked_next_power_of_two().expect(
453 "MatchGeneratorDriver::new: max_window_size too large for \
454 next_power_of_two without overflow",
455 );
456 let window_log_init = next_pow2.trailing_zeros() as u8;
457 Self {
458 vec_pool: Vec::new(),
459 // Deferred table: `new` runs before any source size or resolved
460 // LevelParams exist, so allocating at the level-default hash_log
461 // here would be thrown away by the first frame's reset (which
462 // clamps the window to the input and reallocs at the resolved
463 // size). The deferral lets that first reset allocate exactly once.
464 storage: MatcherStorage::Simple(FastKernelMatcher::with_params_deferred(
465 window_log_init,
466 FAST_LEVEL_1_HASH_LOG,
467 FAST_LEVEL_1_MLS,
468 2, // upstream zstd default step_size (targetLength=0 → step=2)
469 )),
470 strategy_tag: super::strategy::StrategyTag::Fast,
471 search: super::strategy::SearchMethod::Fast,
472 parse: super::strategy::ParseMode::Greedy,
473 #[cfg(test)]
474 config_override: None,
475 param_overrides: None,
476 slice_size,
477 base_slice_size: slice_size,
478 // Report the ROUNDED-UP window size that the matcher
479 // actually carries (via `window_log_init = log2(next_pow2)`
480 // → matcher's `max_window_size = 1 << window_log_init =
481 // next_pow2`). For non-power-of-two `slice_size *
482 // max_slices_in_window` inputs, the unrounded value
483 // would under-report the active backend's window until
484 // the first `reset()` overwrites both sides from the
485 // resolved LevelParams.
486 reported_window_size: next_pow2,
487 reset_size_log: None,
488 reset_dict_attach_ok: true,
489 reset_shape: None,
490 dictionary_retained_budget: 0,
491 source_size_hint: None,
492 dictionary_size_hint: None,
493 borrowed_pending: None,
494 primed: None,
495 }
496 }
497
498 fn level_params(level: CompressionLevel, source_size: Option<u64>) -> LevelParams {
499 resolve_level_params(level, source_size)
500 }
501
502 /// Install the public-parameter per-knob overrides (#27) applied at
503 /// the next [`Matcher::reset`]. `None` (or an all-`None` set) restores
504 /// plain level-based geometry. Persists across resets until changed.
505 pub(crate) fn set_param_overrides(
506 &mut self,
507 overrides: Option<super::parameters::ParamOverrides>,
508 ) {
509 self.param_overrides = overrides;
510 }
511
512 /// Active backend family derived from the storage variant. Single
513 /// source of truth — no separate runtime tag to drift against.
514 pub(crate) fn active_backend(&self) -> super::strategy::BackendTag {
515 self.storage.backend()
516 }
517
518 /// Whether the borrowed (no-copy, in-place over-window) scan is
519 /// implemented for the current backend + search configuration. The
520 /// HashChain backend serves both the lazy CHAIN parser
521 /// (`SearchMethod::HashChain`) and the BT/optimal parsers
522 /// (`SearchMethod::BinaryTree`); only the lazy chain has a borrowed scan
523 /// so far, so BT/optimal stay on the owned path.
524 pub(crate) fn borrowed_supported(&self) -> bool {
525 use super::strategy::{BackendTag, SearchMethod, StrategyTag};
526 match self.active_backend() {
527 BackendTag::Simple | BackendTag::Dfast | BackendTag::Row => true,
528 // The HashChain backend covers two searches: the lazy CHAIN parser
529 // (borrowed-capable) and the BINARY-TREE search (btlazy2 L13-15 +
530 // optimal BtOpt/BtUltra/BtUltra2 L16-22). btlazy2's BT-tree borrowed
531 // scan is byte-identical to owned (reads via live_history()), so it
532 // takes the in-place path. The OPTIMAL parsers stay owned: their
533 // cost-based DP is sensitive to candidate quality, and the borrowed
534 // continuous-index scan yields slightly different (ratio-worse)
535 // candidates than the owned evict+rehash scan — borrowed optimal
536 // both diverged from owned and fell outside the ffi ratio bound.
537 // Search-aware (not just strategy_tag) so optimal BT can never be
538 // staged on the borrowed path even via an internal caller.
539 BackendTag::HashChain => match self.search {
540 SearchMethod::HashChain => true,
541 SearchMethod::BinaryTree => matches!(self.strategy_tag, StrategyTag::Btlazy2),
542 _ => false,
543 },
544 }
545 }
546
547 /// Whether a DICTIONARY frame can take the borrowed (no input copy) path.
548 /// Only the Simple (Fast) backend with the dictionary ATTACHED (not the
549 /// copy/merge regime) has a borrowed dict scan — `start_matching_borrowed_dict`
550 /// reads live matches from the borrowed input in place and dict matches
551 /// from the committed dict prefix via the 2-segment counter. Every other
552 /// backend, and copy-mode (large-input) dict frames, stay on the owned
553 /// path. Checked AFTER priming, so `is_attached()` reflects the resolved
554 /// attach-vs-copy decision.
555 pub(crate) fn borrowed_dict_supported(&self) -> bool {
556 matches!(
557 &self.storage,
558 MatcherStorage::Simple(m) if m.dict_is_attached()
559 )
560 }
561
562 fn simple_mut(&mut self) -> &mut FastKernelMatcher {
563 match &mut self.storage {
564 MatcherStorage::Simple(m) => m,
565 _ => panic!("simple backend must be initialized by reset() before use"),
566 }
567 }
568
569 /// Reclaim the per-block input buffer that the Simple backend
570 /// just spent inside `start_matching` / `skip_matching_with_hint`.
571 ///
572 /// `FastKernelMatcher::take_recycled_space` returns the cleared
573 /// (capacity-retained) `Vec<u8>` from the last
574 /// `extend_history_with_pending`. We push it onto `vec_pool`
575 /// as-is (with `len = 0`); `get_next_space()` is responsible for
576 /// resizing the buffer back to `slice_size` on its next pop. The
577 /// pushed length is irrelevant — only the capacity matters, and
578 /// `extend_history_with_pending` preserves it. Without this
579 /// recycle path, the Simple backend would allocate a new
580 /// `Vec<u8>` per block — a measurable hot-path cost when blocks
581 /// are small (~128 KiB) and processed at hundreds of MiB/s.
582 fn recycle_simple_space(&mut self) {
583 if let Some(space) = self.simple_mut().take_recycled_space() {
584 // `space` is already cleared (len = 0) by
585 // `extend_history_with_pending`; capacity is retained.
586 // Leaving `len = 0` here avoids the cost of zero-filling
587 // the entire allocation — `get_next_space()` resizes the
588 // popped buffer up to `slice_size` on demand, so the
589 // length the pool holds is irrelevant. This matters most
590 // after a small-source-size hint has shrunk `slice_size`
591 // mid-frame: the recycled buffer can be much larger than
592 // the current `slice_size`, and zero-filling 128 KiB+ on
593 // every block would erase the perf win the recycle path
594 // is meant to deliver.
595 self.vec_pool.push(space);
596 }
597 }
598
599 /// Register a caller-owned input buffer as the Simple backend's
600 /// borrowed one-shot match window. Only valid on the Simple (Fast)
601 /// backend; the one-shot frame path gates on that before calling.
602 ///
603 /// # Safety
604 /// Same contract as [`FastKernelMatcher::set_borrowed_window`]: the
605 /// buffer must stay live and unmodified until the window is cleared,
606 /// and must be cleared before the buffer is dropped or the matcher is
607 /// reused for another frame.
608 pub(crate) unsafe fn set_borrowed_window(&mut self, buffer: &[u8]) {
609 // SAFETY: forwarded contract — caller upholds liveness/clear.
610 match self.active_backend() {
611 super::strategy::BackendTag::Simple => unsafe {
612 self.simple_mut().set_borrowed_window(buffer)
613 },
614 super::strategy::BackendTag::Dfast => unsafe {
615 self.dfast_matcher_mut().set_borrowed_window(buffer)
616 },
617 super::strategy::BackendTag::Row => unsafe {
618 self.row_matcher_mut().set_borrowed_window(buffer)
619 },
620 super::strategy::BackendTag::HashChain => unsafe {
621 self.hc_matcher_mut().set_borrowed_window(buffer)
622 },
623 }
624 }
625
626 /// Clear the borrowed one-shot window, returning the active backend
627 /// to the owned `history` path.
628 pub(crate) fn clear_borrowed_window(&mut self) {
629 match self.active_backend() {
630 super::strategy::BackendTag::Simple => self.simple_mut().clear_borrowed_window(),
631 super::strategy::BackendTag::Dfast => self.dfast_matcher_mut().clear_borrowed_window(),
632 super::strategy::BackendTag::Row => self.row_matcher_mut().clear_borrowed_window(),
633 super::strategy::BackendTag::HashChain => self.hc_matcher_mut().clear_borrowed_window(),
634 #[allow(unreachable_patterns)]
635 _ => {}
636 }
637 self.borrowed_pending = None;
638 }
639
640 /// Stage the borrowed block range `[block_start, block_end)` for the
641 /// NEXT `start_matching` / `skip_matching_with_hint`, which the
642 /// borrowed Fast frame path uses in place of `commit_space`. While
643 /// staged, those trait calls route to the Simple backend's borrowed
644 /// scan/skip (consuming the stage) instead of the owned committed
645 /// block. See [`Matcher::start_matching`] /
646 /// [`Matcher::skip_matching_with_hint`] on this type.
647 pub(crate) fn set_borrowed_block(&mut self, block_start: usize, block_end: usize) {
648 assert!(
649 self.borrowed_supported(),
650 "borrowed block staging is not supported for the active backend/search config",
651 );
652 assert!(
653 block_start <= block_end,
654 "borrowed block range must satisfy start <= end (start={block_start} end={block_end})",
655 );
656 self.borrowed_pending = Some((block_start, block_end));
657 // Make the range visible to `get_last_space()` immediately: the
658 // emit pipeline reads `get_last_space().len()` in
659 // `collect_block_parts` BEFORE `start_matching` consumes the
660 // stage, so the staged block (not the whole borrowed window) must
661 // be reported now to keep the literal-buffer reservation right.
662 match self.active_backend() {
663 super::strategy::BackendTag::Simple => self
664 .simple_mut()
665 .stage_borrowed_block(block_start, block_end),
666 super::strategy::BackendTag::Dfast => self
667 .dfast_matcher_mut()
668 .stage_borrowed_block(block_start, block_end),
669 super::strategy::BackendTag::Row => self
670 .row_matcher_mut()
671 .stage_borrowed_block(block_start, block_end),
672 super::strategy::BackendTag::HashChain => self
673 .hc_matcher_mut()
674 .table
675 .stage_borrowed_block(block_start, block_end),
676 }
677 }
678
679 #[cfg(test)]
680 fn dfast_matcher(&self) -> &DfastMatchGenerator {
681 match &self.storage {
682 MatcherStorage::Dfast(m) => m,
683 _ => panic!("dfast backend must be initialized by reset() before use"),
684 }
685 }
686
687 fn dfast_matcher_mut(&mut self) -> &mut DfastMatchGenerator {
688 match &mut self.storage {
689 MatcherStorage::Dfast(m) => m,
690 _ => panic!("dfast backend must be initialized by reset() before use"),
691 }
692 }
693
694 #[cfg(test)]
695 pub(crate) fn row_matcher(&self) -> &RowMatchGenerator {
696 match &self.storage {
697 MatcherStorage::Row(m) => m,
698 _ => panic!("row backend must be initialized by reset() before use"),
699 }
700 }
701
702 pub(crate) fn row_matcher_mut(&mut self) -> &mut RowMatchGenerator {
703 match &mut self.storage {
704 MatcherStorage::Row(m) => m,
705 _ => panic!("row backend must be initialized by reset() before use"),
706 }
707 }
708
709 #[cfg(test)]
710 fn hc_matcher(&self) -> &HcMatchGenerator {
711 match &self.storage {
712 MatcherStorage::HashChain(m) => m,
713 _ => panic!("hash chain backend must be initialized by reset() before use"),
714 }
715 }
716
717 fn hc_matcher_mut(&mut self) -> &mut HcMatchGenerator {
718 match &mut self.storage {
719 MatcherStorage::HashChain(m) => m,
720 _ => panic!("hash chain backend must be initialized by reset() before use"),
721 }
722 }
723
724 /// Shrink the active backend's `max_window_size` by the bytes
725 /// reclaimed from the dictionary-retention budget. Returns `true`
726 /// iff any reclamation happened — the caller uses that as the
727 /// gate for [`Self::trim_after_budget_retire`] (which is a no-op
728 /// otherwise: with `max_window_size` unchanged the backend's
729 /// `trim_to_window` cannot find anything to evict, so calling it
730 /// just runs an extra `match` ladder + a single early-out check
731 /// per slice commit).
732 #[must_use]
733 fn retire_dictionary_budget(&mut self, evicted_bytes: usize) -> bool {
734 let reclaimed = evicted_bytes.min(self.dictionary_retained_budget);
735 if reclaimed == 0 {
736 return false;
737 }
738 self.dictionary_retained_budget -= reclaimed;
739 match self.active_backend() {
740 super::strategy::BackendTag::Simple => {
741 let matcher = self.simple_mut();
742 // `reclaimed` can exceed the CURRENT `max_window_size`: the
743 // retained dict budget is tracked independently and the
744 // window may already have been shrunk by a prior eviction,
745 // so the floor at 0 is the correct clamp, not a masked bug.
746 matcher.max_window_size = matcher.max_window_size.saturating_sub(reclaimed);
747 }
748 super::strategy::BackendTag::Dfast => {
749 let matcher = self.dfast_matcher_mut();
750 // `reclaimed` can exceed the CURRENT `max_window_size`: the
751 // retained dict budget is tracked independently and the
752 // window may already have been shrunk by a prior eviction,
753 // so the floor at 0 is the correct clamp, not a masked bug.
754 matcher.max_window_size = matcher.max_window_size.saturating_sub(reclaimed);
755 }
756 super::strategy::BackendTag::Row => {
757 let matcher = self.row_matcher_mut();
758 // `reclaimed` can exceed the CURRENT `max_window_size`: the
759 // retained dict budget is tracked independently and the
760 // window may already have been shrunk by a prior eviction,
761 // so the floor at 0 is the correct clamp, not a masked bug.
762 matcher.max_window_size = matcher.max_window_size.saturating_sub(reclaimed);
763 }
764 super::strategy::BackendTag::HashChain => {
765 let matcher = self.hc_matcher_mut();
766 // See the Simple arm: `reclaimed` may exceed the current
767 // window, so saturating to 0 is the correct clamp.
768 matcher.table.max_window_size =
769 matcher.table.max_window_size.saturating_sub(reclaimed);
770 }
771 }
772 true
773 }
774
775 fn trim_after_budget_retire(&mut self) {
776 loop {
777 let mut evicted_bytes = 0usize;
778 match self.active_backend() {
779 super::strategy::BackendTag::Simple => {
780 // FastKernelMatcher owns its history as a single
781 // flat `Vec<u8>` (upstream zstd's flat-buffer layout)
782 // rather than the legacy per-block `WindowEntry`
783 // stack. There are no per-block Vec allocations
784 // to recycle into `vec_pool` — `trim_to_window`
785 // drains the oldest bytes in-place and returns
786 // the count for the dictionary-budget loop's
787 // termination check.
788 let MatcherStorage::Simple(m) = &mut self.storage else {
789 unreachable!("active_backend() == Simple proven above");
790 };
791 evicted_bytes += m.trim_to_window();
792 }
793 super::strategy::BackendTag::Dfast => {
794 // Dfast doesn't retain input Vecs — `history` is the
795 // only byte store, so there is no per-block buffer
796 // to push back through a callback. Eviction byte
797 // count is derived from the `window_size` delta
798 // before/after; the Dfast variant of
799 // `trim_to_window` takes no closure, sidestepping
800 // an unused-`impl FnMut` monomorphization that
801 // would otherwise contractually never fire.
802 let dfast = self.dfast_matcher_mut();
803 let pre = dfast.window_size;
804 dfast.trim_to_window();
805 evicted_bytes += pre - dfast.window_size;
806 }
807 super::strategy::BackendTag::Row => {
808 // Row keeps bytes only in the contiguous `history` mirror
809 // (block buffers are returned to the pool per block in
810 // `add_data`), so derive the eviction count from the
811 // `window_size` delta, mirroring the Dfast / HashChain arms.
812 let row = self.row_matcher_mut();
813 let pre = row.window_size;
814 row.trim_to_window();
815 evicted_bytes += pre - row.window_size;
816 }
817 super::strategy::BackendTag::HashChain => {
818 // HC keeps bytes only in the contiguous `history` mirror
819 // (no per-block Vecs to recycle since the window<->history
820 // dedup), so derive the eviction count from the
821 // `window_size` delta, mirroring the Dfast arm above.
822 let table = &mut self.hc_matcher_mut().table;
823 let pre = table.window_size;
824 table.trim_to_window();
825 evicted_bytes += pre - table.window_size;
826 }
827 }
828 if evicted_bytes == 0 {
829 break;
830 }
831 // The loop's invariant is "the backend's previous
832 // `max_window_size` shrink had downstream bytes left to
833 // evict" — that's what `evicted_bytes != 0` proves at
834 // this point. `dictionary_retained_budget` is NOT
835 // guaranteed to be positive here: the outer
836 // `retire_dictionary_budget` call may have already
837 // drained it to zero by reclaiming the last retained
838 // bytes, while the backend still has bytes above the
839 // freshly-shrunk window cap waiting for this loop to
840 // evict. The return value of the retire call below is
841 // therefore intentionally discarded — the loop's
842 // termination is driven by `evicted_bytes == 0`, not by
843 // whether the budget has more bytes left to reclaim.
844 let _ = self.retire_dictionary_budget(evicted_bytes);
845 }
846 }
847
848 /// ATTACH (`true`) vs COPY (`false`) decision for the dms-bearing HashChain
849 /// backend (lazy hash-chain AND binary-tree/optimal levels), mirroring
850 /// upstream `ZSTD_shouldAttachDict` and its per-strategy `attachDictSizeCutoffs`:
851 /// a small / unknown source ATTACHES the dict as a separate dms (hash-chain
852 /// dms for lazy, DUBT dms for BT); a large known source COPIES it into the
853 /// live chain / tree. The cutoff is the lazy/lazy2 value for HC, the
854 /// btlazy2/btopt value for Bt{Opt}, and the smaller btultra/btultra2 value for
855 /// the deepest parses. Both `skip_matching_for_dictionary_priming` (which
856 /// stages the dict) and `prime_with_dictionary` (which builds-or-drops the
857 /// dms) read this so the two stay in lock-step.
858 fn hc_dict_attach_mode(&self) -> bool {
859 // Only the HashChain backend (lazy hash-chain + BT/optimal) routes here;
860 // a non-HashChain storage has no dms decision, so default to attach.
861 let MatcherStorage::HashChain(hc) = &self.storage else {
862 return true;
863 };
864 let cutoff = if hc.table.uses_bt {
865 match hc.strategy_tag {
866 super::strategy::StrategyTag::BtUltra | super::strategy::StrategyTag::BtUltra2 => {
867 BT_ULTRA_ATTACH_DICT_CUTOFF_LOG
868 }
869 _ => BT_OPT_ATTACH_DICT_CUTOFF_LOG,
870 }
871 } else {
872 HC_ATTACH_DICT_CUTOFF_LOG
873 };
874 self.reset_size_log.is_none_or(|log| log <= cutoff)
875 }
876
877 fn skip_matching_for_dictionary_priming(&mut self) {
878 match self.active_backend() {
879 super::strategy::BackendTag::Simple => {
880 // Upstream zstd `ZSTD_shouldAttachDict` mode selection for the Fast
881 // strategy (cutoff 8 KB): small / unknown-size inputs ATTACH
882 // (index dict positions into a SEPARATE immutable table; the
883 // dual-probe 2-cursor `compress_block_fast_dict` then prefers
884 // recent-input matches and falls back to the dict — the path
885 // that wins small/unknown). Large known-size inputs COPY (prime
886 // dict into the live table; the 4-cursor `compress_block_fast`
887 // matches against it as window history — the path that already
888 // matches/beats the upstream zstd on large corpora). The dispatch in
889 // `start_matching` keys off `dict_table.is_some()`, which only
890 // the attach path populates. See [`FAST_ATTACH_DICT_CUTOFF_LOG`].
891 let attach = self.reset_dict_attach_ok
892 && self
893 .reset_size_log
894 .is_none_or(|log| log <= FAST_ATTACH_DICT_CUTOFF_LOG);
895 if attach {
896 self.simple_mut().skip_matching_for_dict_prime();
897 } else {
898 self.simple_mut().skip_matching_with_hint(Some(false));
899 }
900 self.recycle_simple_space();
901 }
902 super::strategy::BackendTag::Dfast => {
903 // Upstream zstd `ZSTD_dictMatchState` mode selection for dfast (cutoff
904 // 16 KiB): small / unknown-size inputs ATTACH (build the
905 // separate immutable dict long+short tables; the dual-probe
906 // `start_matching_fast_loop` searches live + dict, the path that
907 // avoids the per-frame dict re-prime that dominates small
908 // `compress-dict`). Larger known-size inputs COPY (re-prime the
909 // dict into the live tables via `skip_matching_dense`, where the
910 // dense scan matches it as window history). `skip_matching_for_dict_attach`
911 // self-gates on `use_fast_loop` (only fast-loop levels carry the
912 // dual-probe; general-path levels fall back to the dense copy).
913 let attach = self
914 .reset_size_log
915 .is_none_or(|log| log <= DFAST_ATTACH_DICT_CUTOFF_LOG);
916 if attach {
917 self.dfast_matcher_mut().skip_matching_for_dict_attach();
918 } else {
919 self.dfast_matcher_mut().invalidate_dict_cache();
920 self.dfast_matcher_mut().skip_matching_dense();
921 }
922 }
923 super::strategy::BackendTag::Row => {
924 // Upstream zstd `ZSTD_RowFindBestMatch` `dictMatchState`: small /
925 // unknown-size inputs ATTACH (build the separate immutable dict
926 // row index; the bounded dual-probe in `row_candidate_rl`
927 // searches live + dict, avoiding the per-frame dict re-index),
928 // larger known-size inputs COPY (dense re-prime into the live
929 // rows).
930 let attach = self
931 .reset_size_log
932 .is_none_or(|log| log <= ROW_ATTACH_DICT_CUTOFF_LOG);
933 if attach {
934 self.row_matcher_mut().prime_dict_attach_current_block();
935 } else {
936 self.row_matcher_mut().invalidate_dict_cache();
937 self.row_matcher_mut().skip_matching_with_hint(Some(false));
938 }
939 }
940 super::strategy::BackendTag::HashChain => {
941 // Lazy-HC AND BT/optimal both follow upstream zstd `ZSTD_shouldAttachDict`
942 // per-strategy: ATTACH (a separate dms — hash-chain dms for lazy,
943 // DUBT dms for BT) for small / unknown inputs, COPY (merge the dict
944 // into the live chain/tree) for large known inputs. ATTACH keeps
945 // the dict in history but out of the live structure via
946 // `skip_matching_dict_bt` (the cursor advance is shared by both
947 // arms); COPY routes through the normal `skip_matching` (its
948 // `uses_bt` branch fills the live tree, the lazy branch the live
949 // chain). The dms is built-or-dropped to match in
950 // `prime_with_dictionary`.
951 if self.hc_dict_attach_mode() {
952 self.hc_matcher_mut().table.skip_matching_dict_bt();
953 } else {
954 self.hc_matcher_mut().skip_matching(Some(false));
955 }
956 }
957 }
958 }
959}
960
961impl Matcher for MatchGeneratorDriver {
962 fn supports_dictionary_priming(&self) -> bool {
963 true
964 }
965
966 fn set_source_size_hint(&mut self, size: u64) {
967 self.source_size_hint = Some(size);
968 }
969
970 fn set_dictionary_size_hint(&mut self, size: usize) {
971 self.dictionary_size_hint = Some(size);
972 }
973
974 /// Dict-relevance gate for the raw-fast-path. Reached only when a dictionary
975 /// is active (the caller short-circuits on `dict_active`), so this answers
976 /// "could the dict compress this otherwise-incompressible-looking block?".
977 /// The Simple (Fast) backend samples its dict table precisely
978 /// ([`FastKernelMatcher::block_samples_match_dict`]); the other backends
979 /// (Dfast / Row / HashChain / BT) have their own dict structures and no cheap
980 /// probe here, so they answer CONSERVATIVELY `true`: without a probe they
981 /// cannot tell whether the dict compresses an incompressible-LOOKING block,
982 /// and answering `false` would let the raw-fast-path emit such a block raw
983 /// and miss an embedded dict segment. `dictionary_segment_in_incompressible_input_is_matched`
984 /// pins this for Dfast/Row/BT — the 512-byte dict run inside high-entropy
985 /// filler is matched only because these backends stay on the scan. So they
986 /// keep the blanket scan the old `!dict_active` gate gave them; only the
987 /// Simple/Fast backend trades it for the precise probe.
988 fn block_samples_match_dict(&self, block: &[u8]) -> bool {
989 match &self.storage {
990 MatcherStorage::Simple(m) => m.block_samples_match_dict(block),
991 _ => true,
992 }
993 }
994
995 /// Heap bytes this driver owns: the active backend's tables/history, the
996 /// recycled input-buffer pool, and the primed-dictionary snapshot (a cloned
997 /// backend kept for CDict-equivalent reuse). The inline struct itself is
998 /// accounted by the owner's `size_of`.
999 fn heap_size(&self) -> usize {
1000 let pool: usize = self.vec_pool.capacity() * core::mem::size_of::<Vec<u8>>()
1001 + self.vec_pool.iter().map(Vec::capacity).sum::<usize>();
1002 let snapshot = self
1003 .primed
1004 .as_ref()
1005 .map_or(0, |(storage, _, _)| storage.heap_size());
1006 pool + self.storage.heap_size() + snapshot
1007 }
1008
1009 fn clear_param_overrides(&mut self) {
1010 self.param_overrides = None;
1011 }
1012
1013 fn reset(&mut self, level: CompressionLevel) {
1014 let hint = self.source_size_hint.take();
1015 let dict_hint = self.dictionary_size_hint.take();
1016 // Snapshot the hint's normalized ceil-log bucket for the primed-snapshot
1017 // key and prime_with_dictionary's attach/copy mode decision (the hint is
1018 // consumed here, but priming happens just after reset). Storing the
1019 // bucket rather than the raw bytes means two hints that resolve to the
1020 // same matcher shape share one snapshot instead of each re-priming.
1021 self.reset_size_log = hint.map(source_size_ceil_log);
1022 // A dictionary too large for the tagged attach position field falls back
1023 // to copy mode. Captured here (from the load-set size hint = actual dict
1024 // length) so the prime decision and the snapshot-key / epoch bits agree.
1025 self.reset_dict_attach_ok =
1026 dict_hint.is_none_or(|size| size <= MAX_FAST_ATTACH_DICT_REGION);
1027 let hinted = hint.is_some();
1028 #[cfg_attr(not(test), allow(unused_mut))]
1029 let mut params = Self::level_params(level, hint);
1030 // Test-only: apply a parse×search override so the matrix can be
1031 // exercised without editing `LEVEL_TABLE`. Mutating `params` here
1032 // (before `next_backend`) flows the override through storage
1033 // selection, `configure`, and the `self.search`/`self.parse`
1034 // writes uniformly. Consumed with `take()` so it is one-shot: the
1035 // synthetic pairing applies to exactly this `reset()`, and a later
1036 // reset on the same driver falls back to the level's real config.
1037 #[cfg(test)]
1038 if let Some((search, parse)) = self.config_override.take() {
1039 params.search = search;
1040 params.lazy_depth = parse.lazy_depth();
1041 // The matrix sweep can pair a level with a backend its native
1042 // row doesn't populate (e.g. greedy L5, which carries only `row`,
1043 // run on HashChain). Synthesize a default config for the
1044 // overridden backend so its `configure` arm has something to read.
1045 use super::strategy::SearchMethod;
1046 match search {
1047 SearchMethod::Fast => {
1048 params.fast.get_or_insert(FAST_L1);
1049 }
1050 SearchMethod::DoubleFast => {
1051 params.dfast.get_or_insert(DFAST_L3);
1052 }
1053 SearchMethod::RowHash => {
1054 params.row.get_or_insert(ROW_CONFIG);
1055 }
1056 SearchMethod::HashChain | SearchMethod::BinaryTree => {
1057 params.hc.get_or_insert(HC_CONFIG);
1058 }
1059 }
1060 }
1061 // Public-parameter overrides (#27): apply the per-knob set on top
1062 // of the level-resolved params. A strategy override re-routes the
1063 // backend, so this must precede `next_backend` selection. The
1064 // all-`None` case is skipped so default level geometry stays
1065 // byte-identical to plain level-based compression.
1066 if let Some(ov) = self.param_overrides
1067 && !ov.is_empty()
1068 {
1069 apply_param_overrides(&mut params, &ov);
1070 // `Self::level_params(level, hint)` applied the source-size cap
1071 // for the LEVEL's native backend. If a strategy override moved
1072 // the frame onto a different backend, `apply_param_overrides`
1073 // synthesized that backend's DEFAULT config (FAST_L1 /
1074 // HC_OVERRIDE_DEFAULT) with full-size table logs AFTER that cap
1075 // ran. Re-apply the hint cap so a tiny hinted frame doesn't
1076 // allocate the new backend's full-size tables. An explicit
1077 // `window_log` override is the user's hard request and must
1078 // survive the re-cap, so restore it afterwards.
1079 if let Some(hint_size) = hint {
1080 params = adjust_params_for_source_size(params, hint_size);
1081 if let Some(window_log) = ov.window_log {
1082 params.window_log = window_log;
1083 }
1084 }
1085 }
1086 // Dictionary-driven table sizing — parity with upstream zstd `ZSTD_createCDict`
1087 // (`ZSTD_getCParams_internal(level, UNKNOWN, dictSize, ZSTD_cpm_createCDict)`
1088 // → `ZSTD_adjustCParams_internal`). A loaded dictionary supplies the
1089 // long-distance matches, so upstream zstd sizes the prepared match-finder tables
1090 // to the DICTIONARY (assuming a `minSrcSize` source), not the live
1091 // window: it downsizes `hashLog`/`chainLog` toward the dict-and-window
1092 // log while leaving the frame's eviction `window_log` source-derived so
1093 // the dictionary bytes stay referenceable (`ZSTD_resetCCtx_byCopyingCDict`
1094 // copies the small CDict tables but keeps the source window). We apply
1095 // the same downsizing to the level's own hc geometry and cap (min) so a
1096 // dict never inflates the level tables. Only the binary-tree / hash-chain
1097 // backend reads `hc.{hash,chain}_log`; Simple/Dfast/Row derive their
1098 // widths from the source window in their `reset` arms.
1099 // A zero-length dictionary is "no dictionary": running the CDict sizing
1100 // path for `Some(0)` is not a no-op — `cdict_table_logs(.., 0)` still
1101 // collapses the HC/BT tables toward the 513-byte upstream zstd tier via
1102 // `DICT_MIN_SRC_SIZE`, tanking ratio/perf on the next frame. Priming
1103 // already treats empty content as empty, so skip the downsizing here too.
1104 if let Some(dict_size) = dict_hint.filter(|&size| size > 0) {
1105 // Derive the dict-tier geometry from the level's FULL (un-source-capped)
1106 // hc widths. `Self::level_params(level, hint)` already source-capped
1107 // `params.hc`; feeding those capped widths into `cdict_table_logs` and
1108 // then `.min()`-ing would double-cap, so on a small hinted source with a
1109 // large dictionary the prepared tables collapse below what the dict needs
1110 // — defeating the `ZSTD_createCDict` geometry this mirrors. Take the
1111 // un-hinted base widths instead and assign the result directly:
1112 // `cdict_table_logs` only ever downsizes, so it never exceeds the base
1113 // level geometry, while the eviction `window_log` stays source-derived so
1114 // the dictionary bytes remain referenceable. Active public-parameter
1115 // overrides (#27) are applied to the base too, so a strategy override
1116 // that routes onto HashChain/BinaryTree still gets dict-tier sizing and
1117 // explicit hash/chain overrides feed through as the geometry ceiling.
1118 let mut base_params = Self::level_params(level, None);
1119 if let Some(ov) = self.param_overrides
1120 && !ov.is_empty()
1121 {
1122 apply_param_overrides(&mut base_params, &ov);
1123 }
1124 if let (Some(hc), Some(base_hc)) = (params.hc.as_mut(), base_params.hc) {
1125 let uses_bt = matches!(
1126 params.strategy_tag,
1127 super::strategy::StrategyTag::Btlazy2
1128 | super::strategy::StrategyTag::BtOpt
1129 | super::strategy::StrategyTag::BtUltra
1130 | super::strategy::StrategyTag::BtUltra2
1131 );
1132 let (dict_hash_log, dict_chain_log) = cdict_table_logs(
1133 params.window_log,
1134 base_hc.hash_log,
1135 base_hc.chain_log,
1136 uses_bt,
1137 dict_size,
1138 );
1139 hc.hash_log = dict_hash_log;
1140 hc.chain_log = dict_chain_log;
1141 }
1142 }
1143 // upstream zstd `ZSTD_resolveRowMatchFinderMode` (zstd_compress.c:238-245):
1144 // the row matchfinder is used for greedy/lazy/lazy2 ONLY when
1145 // `windowLog > 14`; at or below that upstream runs the hash-chain
1146 // matcher (`ZSTD_HcFindBestMatch`). We previously hardcoded the Row
1147 // backend for these strategies regardless of window, sending every
1148 // small-window frame (hinted floor = windowLog 14, e.g. the small-4k/10k
1149 // fixtures) through Row where upstream uses HC. Match it: fall back to
1150 // the hash-chain matcher (lazy/greedy parse via `lazy_depth`) when the
1151 // resolved window is <= 14. The HC config is synthesised from the
1152 // level's RowConfig (HC and Row share the same cParams; only the
1153 // matchfinder differs) — `hash_log` / `chain_log` are
1154 // clamped to the (<= 14) window inside the HashChain reset arm, so the
1155 // nominal width here only sets the clamp ceiling.
1156 if params.search == super::strategy::SearchMethod::RowHash && params.window_log <= 14 {
1157 let row = params
1158 .row
1159 .expect("a RowHash level row must carry a RowConfig");
1160 params.search = super::strategy::SearchMethod::HashChain;
1161 // For a dict-bearing frame, downsize the synthesised HC logs to the
1162 // dictionary's content tier via `cdict_table_logs` (the same
1163 // correction the native HC dict-prime path applies above), so a dict
1164 // much smaller than the window doesn't prime a needlessly sparse
1165 // table. Row-finder levels are never BinaryTree, so `uses_bt = false`.
1166 //
1167 // Feed `cdict_table_logs` the UN-hinted base Row width, not the
1168 // resolved `row.hash_bits`: the latter is already source-capped on a
1169 // hinted reset (the `row_cap = table_log + 1` clamp), so passing it
1170 // here would double-cap exactly as the native HC dict path warns
1171 // above — a small hinted source with a large dictionary would
1172 // collapse the prepared table below what the dict needs.
1173 // `cdict_table_logs` only ever downsizes, so deriving the ceiling
1174 // from the un-hinted base (plus active public overrides) keeps the
1175 // dict-tier geometry intact. No source hint => `row.hash_bits` is
1176 // already the level's full width, so reuse it directly.
1177 let row_cdict_hash_bits = match dict_hint.filter(|&size| size > 0) {
1178 Some(_) => {
1179 let mut base_params = Self::level_params(level, None);
1180 if let Some(ov) = self.param_overrides
1181 && !ov.is_empty()
1182 {
1183 apply_param_overrides(&mut base_params, &ov);
1184 }
1185 base_params
1186 .row
1187 .map_or(row.hash_bits, |base_row| base_row.hash_bits)
1188 }
1189 None => row.hash_bits,
1190 };
1191 // Row-backed levels carry only `hash_bits`; the HC chain table they
1192 // fall back to follows the upstream zstd cParams relationship `chainLog =
1193 // hashLog - 1` for every Row level (L6 c18 h19 .. L12 c22 h23, see
1194 // the ROW_L* tables). Synthesise the chain width as `hash_bits - 1`
1195 // so the dict path doesn't leave the chain table one bit too wide
1196 // (cdict_table_logs only downsizes, so passing the full hash width
1197 // for both would keep a 2x-too-large chain table on dict frames).
1198 // Raw `- 1` is underflow-safe: `hash_bits` is either a predefined
1199 // ROW_L* width (>= 19) or a public `hash_log` override, and the
1200 // override is range-validated to `ZSTD_HASHLOG_MIN = 6` at the
1201 // parameter API, so the value is always >= 6 here.
1202 //
1203 // A public `chain_log` override (#27) is dropped by the RowHash
1204 // override arm (Row has no chain table), but once this frame falls
1205 // back to HC the chain table is live and must honour it — mirror
1206 // the native HC dict path, which feeds the override-applied
1207 // `base_hc.chain_log` into `cdict_table_logs`. Use the explicit
1208 // override (also API-validated to ZSTD_CHAINLOG_MIN = 6) when set,
1209 // else the upstream zstd `hashLog - 1` relationship.
1210 let explicit_chain_log = self
1211 .param_overrides
1212 .filter(|ov| !ov.is_empty())
1213 .and_then(|ov| ov.chain_log)
1214 .map(|chain_log| chain_log as usize);
1215 let row_cdict_chain_bits = explicit_chain_log.unwrap_or(row_cdict_hash_bits - 1);
1216 let (mut hash_log, mut chain_log) = match dict_hint.filter(|&size| size > 0) {
1217 Some(dict_size) => cdict_table_logs(
1218 params.window_log,
1219 row_cdict_hash_bits,
1220 row_cdict_chain_bits,
1221 false,
1222 dict_size,
1223 ),
1224 None => (
1225 row.hash_bits,
1226 explicit_chain_log.unwrap_or(row.hash_bits - 1),
1227 ),
1228 };
1229 // No-dict path: the HashChain reset arm only clamps the logs to the
1230 // window when `hinted`, but a public `window_log` override can lower
1231 // this level to <= 14 with no source hint — clamp the level's full
1232 // Row `hash_bits` to the window here too (upstream zstd `ZSTD_adjustCParams`:
1233 // hashLog <= windowLog + 1, chainLog <= windowLog) so a 16 KiB window
1234 // doesn't allocate Row-sized HC tables.
1235 if dict_hint.filter(|&size| size > 0).is_none() {
1236 let wlog = params.window_log as usize;
1237 hash_log = hash_log.min(wlog + 1);
1238 chain_log = chain_log.min(wlog);
1239 }
1240 params.hc = Some(HcConfig {
1241 hash_log,
1242 chain_log,
1243 search_depth: row.search_depth,
1244 target_len: row.target_len,
1245 search_mls: 4,
1246 });
1247 params.row = None;
1248 }
1249 let next_backend = params.backend();
1250 let max_window_size = 1usize << params.window_log;
1251 self.dictionary_retained_budget = 0;
1252 // Drop any frame-local borrowed staging so it can't leak across a
1253 // reset and misroute the next start/skip into borrowed dispatch.
1254 self.borrowed_pending = None;
1255 if self.active_backend() != next_backend {
1256 // Drain the outgoing backend's allocations into the shared
1257 // pool. The `match &mut self.storage { ... }` block runs to
1258 // completion before the assignment below replaces the
1259 // variant, so the inner state we just drained is dropped
1260 // with the old variant.
1261 match &mut self.storage {
1262 MatcherStorage::Simple(_m) => {
1263 // FastKernelMatcher owns a flat Vec<u8> history
1264 // and a Vec<u32> hash table — both drop with the
1265 // variant assignment below, no per-block buffers
1266 // to recycle into the driver pools. The
1267 // assignment-replace path collapses to a noop
1268 // pre-pass for this backend.
1269 }
1270 MatcherStorage::Dfast(m) => {
1271 // Drop the long / short hash table allocations
1272 // before calling `m.reset`. Without this prepass,
1273 // `DfastMatchGenerator::reset` would `fill` both
1274 // tables with `DFAST_EMPTY_SLOT` sentinels — wasted
1275 // work given the next assignment to `self.storage`
1276 // is about to drop `m` entirely. `reset` itself
1277 // short-circuits on `if !self.tables.is_empty()`, so
1278 // handing it an empty `Vec` skips the fill loop.
1279 // Mirrors the pre-drain pattern in the HashChain
1280 // arm below (and serves the same peak-memory
1281 // purpose: release the table-allocation footprint
1282 // before constructing the replacement variant).
1283 m.tables = Vec::new();
1284 m.reset();
1285 }
1286 MatcherStorage::Row(m) => {
1287 m.row_heads = Vec::new();
1288 m.row_positions = Vec::new();
1289 m.row_tags = Vec::new();
1290 m.reset();
1291 }
1292 MatcherStorage::HashChain(m) => {
1293 // Release oversized tables when switching away from
1294 // HashChain so Best's larger allocations don't persist.
1295 // hash3_table must be released alongside the other
1296 // two: BtUltra2's `1 << HC3_HASH_LOG` entries would
1297 // otherwise stay pinned across the backend switch,
1298 // even though no future caller of this backend will
1299 // touch them.
1300 m.table.hash_table = Vec::new();
1301 m.table.chain_table = Vec::new();
1302 m.table.hash3_table = Vec::new();
1303 let vec_pool = &mut self.vec_pool;
1304 m.reset(|mut data| {
1305 data.resize(data.capacity(), 0);
1306 vec_pool.push(data);
1307 });
1308 }
1309 }
1310 // Swap in a fresh variant for the new backend. The previous
1311 // `storage` is dropped here.
1312 self.storage = match next_backend {
1313 super::strategy::BackendTag::Simple => {
1314 // Per-level Fast cParams from resolve_level_params:
1315 // Level(1) gets (hash_log=14, mls=7); Level(-7..=-1)
1316 // get upstream zstd row-0 (hash_log=13, mls=7); Fastest /
1317 // Uncompressed keep (hash_log=14, mls=6). See
1318 // resolve_level_params for rationale.
1319 let fast = params.fast.expect("Fast level row carries a FastConfig");
1320 MatcherStorage::Simple(FastKernelMatcher::with_params(
1321 params.window_log,
1322 fast.hash_log,
1323 fast.mls,
1324 fast.step_size,
1325 ))
1326 }
1327 super::strategy::BackendTag::Dfast => {
1328 MatcherStorage::Dfast(DfastMatchGenerator::new(max_window_size))
1329 }
1330 super::strategy::BackendTag::Row => {
1331 MatcherStorage::Row(RowMatchGenerator::new(max_window_size))
1332 }
1333 super::strategy::BackendTag::HashChain => {
1334 MatcherStorage::HashChain(HcMatchGenerator::new(max_window_size))
1335 }
1336 };
1337 }
1338
1339 // Single source of truth: `LevelParams::strategy_tag` is the
1340 // authoritative mapping from `CompressionLevel` to strategy.
1341 // `storage.backend()` derives the parse family from the variant,
1342 // so there is no separate runtime tag that could drift against
1343 // `LEVEL_TABLE`.
1344 self.strategy_tag = params.strategy_tag;
1345 self.search = params.search;
1346 self.parse = params.parse();
1347 self.slice_size = self.base_slice_size.min(max_window_size);
1348 self.reported_window_size = max_window_size;
1349 let strategy_tag = self.strategy_tag;
1350 // Source-proportional table window for the backends whose hash-table
1351 // widths are recomputed here (Dfast / Row). Like the HC / Fast caps
1352 // in `adjust_params_for_source_size`, this sizes the internal tables
1353 // from the RAW source log (not the wire `window_log` floor) so a
1354 // small frame zeroes a small table; it never exceeds the real window.
1355 let table_window_size = match hint {
1356 Some(h) => {
1357 let raw_log = source_size_ceil_log(h);
1358 // Clamp the shift below the pointer width before `1usize <<`:
1359 // an oversized hint (>= 2^63 + 1, and on 32-bit usize any hint
1360 // >= 2^32) drives `raw_log` to 64 / >= 32, and the shift would
1361 // overflow (panic in debug, wrap to 0 in release) before the
1362 // `.min(max_window_size)` cap below could bound it. The min cap
1363 // still provides the real semantic window bound.
1364 let shift = raw_log.max(MIN_WINDOW_LOG).min(usize::BITS as u8 - 1);
1365 (1usize << shift).min(max_window_size)
1366 }
1367 None => max_window_size,
1368 };
1369 // The hint-dependent hash-table width the active backend applies, for
1370 // the primed-snapshot key. Dfast/Row compute it from `table_window_size`
1371 // below; HC/Fast leave it `0` because their widths live in `params`
1372 // (`hc.{hash,chain}_log` / `fast_hash_log`) — already part of the key.
1373 let mut resolved_table_bits: usize = 0;
1374 match &mut self.storage {
1375 MatcherStorage::Simple(m) => {
1376 // Per-level Fast cParams threaded from
1377 // resolve_level_params (see Simple-backend swap
1378 // arm above for the (level → params) mapping).
1379 let fast = params.fast.expect("Fast level row carries a FastConfig");
1380 // Same attach/copy split the dict-prime dispatch applies
1381 // below (`prime_with_dictionary`): only attach-mode dict
1382 // frames may keep the main table across the reset via an
1383 // epoch advance — copy-mode and no-dict frames must memset
1384 // it back to bias 0 for the raw-slice kernels.
1385 // `Some(0)` is "no dictionary" (the dict-sizing path above
1386 // filters it the same way): an empty dict primes nothing, so
1387 // an epoch-advance reset would preserve stale attach state
1388 // instead of clearing it.
1389 let dict_attach_epoch = matches!(dict_hint, Some(size) if size > 0)
1390 && self.reset_dict_attach_ok
1391 && self
1392 .reset_size_log
1393 .is_none_or(|log| log <= FAST_ATTACH_DICT_CUTOFF_LOG);
1394 // Copy-mode dictionary frame whose primed snapshot matches
1395 // this exact resolved shape: `restore_primed_dictionary`
1396 // (called right after this reset; the caller gates the
1397 // restore on the same size bucket and the restore re-checks
1398 // the same key) will `clone_from` the snapshot over this
1399 // matcher, replacing the table contents and bias wholesale —
1400 // the reset's full-table memset would be thrown away. The
1401 // key components mirror `reset_shape` below: Simple leaves
1402 // `resolved_table_bits` 0, never carries an LDM override,
1403 // and `fast_attach` is false in copy mode by construction.
1404 let table_overwritten_by_restore = matches!(dict_hint, Some(size) if size > 0)
1405 && !dict_attach_epoch
1406 && self.primed.as_ref().is_some_and(|(_, _, captured)| {
1407 *captured
1408 == PrimedKey {
1409 level,
1410 params,
1411 table_bits: 0,
1412 fast_attach: false,
1413 ldm: None,
1414 }
1415 });
1416 // Cap `hash_log <= window_log + 1` (upstream zstd
1417 // `ZSTD_adjustCParams_internal`): once `window_log` is resized
1418 // down for a small source, a level-default `1 << hash_log`
1419 // table is mostly wasted address space whose per-frame memset
1420 // dominates the compress cost on tiny frames (a 4 KB frame at
1421 // window_log 12 still zero-fills the 64 KiB hash_log-14 table).
1422 // Gated to no-dict frames: the dict-attach path shares one
1423 // hash_log between the main and dict tables (so one hash keys
1424 // both), and shrinking only the main table would break that
1425 // invariant and the small-frame dict ratio.
1426 let hash_log = if dict_hint.is_some_and(|s| s > 0) {
1427 fast.hash_log
1428 } else {
1429 fast.hash_log.min(params.window_log as u32 + 1)
1430 };
1431 m.reset(
1432 params.window_log,
1433 hash_log,
1434 fast.mls,
1435 fast.step_size,
1436 dict_attach_epoch,
1437 table_overwritten_by_restore,
1438 );
1439 }
1440 MatcherStorage::Dfast(dfast) => {
1441 dfast.max_window_size = max_window_size;
1442 let dcfg = params
1443 .dfast
1444 .expect("Dfast level row must carry a DfastConfig");
1445 // Upstream zstd `cParams.hashLog`/`chainLog`, capped by the
1446 // source-size window when hinted so tiny inputs don't
1447 // over-allocate.
1448 let long_bits = if hinted {
1449 dfast_hash_bits_for_window(table_window_size).min(dcfg.long_hash_log as usize)
1450 } else {
1451 dcfg.long_hash_log as usize
1452 };
1453 let short_bits = if hinted {
1454 dfast_hash_bits_for_window(table_window_size).min(dcfg.short_hash_log as usize)
1455 } else {
1456 dcfg.short_hash_log as usize
1457 };
1458 resolved_table_bits = long_bits;
1459 dfast.set_hash_bits(long_bits, short_bits);
1460 // Dfast holds no per-block input Vecs (history owns the
1461 // bytes and `add_data` returns each Vec eagerly), so
1462 // `reset` takes no `reuse_space` callback.
1463 dfast.reset();
1464 }
1465 MatcherStorage::Row(row) => {
1466 row.max_window_size = max_window_size;
1467 row.lazy_depth = params.lazy_depth;
1468 let mut row_cfg = params.row.expect("Row level row carries a RowConfig");
1469 if hinted {
1470 // Clamp the configured hash width by the hinted window
1471 // (upstream zstd `ZSTD_adjustCParams` caps hashLog by windowLog) —
1472 // `min`, not replace, so an explicit `hash_log` param
1473 // override (`row_cfg.hash_bits`) survives the hinted path
1474 // instead of being overwritten by the window value.
1475 //
1476 // Clamp BEFORE `configure` so the backend sees ONE width
1477 // per frame. Configuring with the unclamped level width
1478 // and then re-clamping made `row_hash_log` oscillate on
1479 // every hinted frame, and each width change clears the
1480 // row tables — `ensure_tables` then re-filled all three
1481 // every frame in a reused compressor.
1482 row_cfg.hash_bits = row_cfg
1483 .hash_bits
1484 .min(row_hash_bits_for_window(table_window_size));
1485 }
1486 row.configure(row_cfg);
1487 // Key the primed snapshot on the width the backend ACTUALLY
1488 // applied (`set_hash_bits` clamps the request): recording the
1489 // request — or the 0 default on the unhinted path — keys
1490 // identical table geometries apart and forces needless
1491 // dictionary re-primes.
1492 resolved_table_bits = row.hash_bits();
1493 row.reset();
1494 }
1495 MatcherStorage::HashChain(hc) => {
1496 hc.table.max_window_size = max_window_size;
1497 hc.hc.lazy_depth = params.lazy_depth;
1498 let mut hc_cfg = params.hc.expect("HashChain level row carries an HcConfig");
1499 // Cap the hash / chain table logs by the hinted window so a small
1500 // input doesn't allocate the full level's tables (the upstream zstd
1501 // `ZSTD_adjustCParams_internal` clamp: `hashLog <= windowLog + 1`,
1502 // and `cycleLog <= windowLog` — `cycleLog == chainLog` for the HC
1503 // finder, `chainLog - 1` for the BT pair table, so `chainLog <=
1504 // windowLog` (+1 for BT)). Ratio-neutral: a hinted window of
1505 // `2^wlog` bytes holds at most `2^wlog` positions, so the slots
1506 // beyond that are never populated — capping only sheds unused
1507 // allocation. Was the source of L10-lazy peak-alloc ~2.15x the
1508 // upstream zstd on a 1 MiB input. Only applied when hinted; an
1509 // unknown-size stream keeps the full level tables.
1510 // Skip for dict-bearing frames: their `hc_cfg.{hash,chain}_log`
1511 // were already sized to the dictionary content tier via
1512 // `cdict_table_logs` (the dict supplies the long-distance
1513 // matches, so upstream `ZSTD_createCDict` sizes the prepared
1514 // tables to the dict, not the source window). Re-applying the
1515 // source-window cap here would collapse those dict-tier logs
1516 // back to the small hinted source — the same double-cap the
1517 // synthesis sites avoid by using the un-hinted base width.
1518 if hinted && !matches!(dict_hint, Some(size) if size > 0) {
1519 let wlog = hc_hash_bits_for_window(table_window_size);
1520 let uses_bt = matches!(
1521 strategy_tag,
1522 super::strategy::StrategyTag::Btlazy2
1523 | super::strategy::StrategyTag::BtOpt
1524 | super::strategy::StrategyTag::BtUltra
1525 | super::strategy::StrategyTag::BtUltra2
1526 );
1527 hc_cfg.hash_log = hc_cfg.hash_log.min(wlog + 1);
1528 hc_cfg.chain_log = hc_cfg.chain_log.min(if uses_bt { wlog + 1 } else { wlog });
1529 }
1530 hc.configure(hc_cfg, strategy_tag, params.window_log);
1531 let vec_pool = &mut self.vec_pool;
1532 hc.reset(|mut data| {
1533 data.resize(data.capacity(), 0);
1534 vec_pool.push(data);
1535 });
1536 // When the source size is known, pre-size the history mirror to
1537 // the expected total (dictionary + payload) so per-block growth
1538 // does not overshoot via Vec capacity doubling (upstream zstd sizes its
1539 // window buffer exactly). Dominates peak once the match-finder
1540 // tables are dictionary-tier-small. Unhinted streams skip this
1541 // and keep doubling growth.
1542 if let Some(src) = hint {
1543 // `src` is a u64 hint and may be the u64::MAX "unknown
1544 // size" sentinel, which truncates under `as usize` on
1545 // 32-bit targets and overflows when the dict hint is
1546 // added. Saturate the source size, then saturate the
1547 // dict-hint addition; `reserve_history` applies the
1548 // tighter window ceiling to the result.
1549 let src_hint = usize::try_from(src).unwrap_or(usize::MAX);
1550 let expected = src_hint.saturating_add(dict_hint.unwrap_or(0));
1551 hc.table.reserve_history(expected);
1552 }
1553 }
1554 }
1555 // LDM wiring (#27): attach (or clear) the long-distance-match
1556 // producer on the optimal (BT) backend. LDM is the only
1557 // back-reference path that crosses the regular window, so it
1558 // only has a home on the `BtMatcher`; non-BT strategies drop the
1559 // producer. Built AFTER `hc.reset()` because `BtMatcher::reset`
1560 // clears an existing producer's table but does not null the
1561 // slot — installing here gives the new frame a fresh producer.
1562 #[cfg(feature = "hash")]
1563 {
1564 // Resolve the derived LDM params first (immutable borrow of the
1565 // overrides), then reuse the existing producer's allocation below.
1566 let derived_ldm = self
1567 .param_overrides
1568 .as_ref()
1569 .and_then(|ov| ov.ldm)
1570 .map(|ldm_ov| {
1571 let strategy_ord = ldm_strategy_ordinal(params.strategy_tag, params.lazy_depth);
1572 // Seed the caller-pinned knobs, then run the upstream zstd
1573 // derivation over the seed so the remaining (zero)
1574 // fields are filled with cross-field consistency
1575 // (e.g. `hash_rate_log = window_log - hash_log`).
1576 // Clobbering after `adjust_for` would break that and
1577 // hand the producer an inconsistent set.
1578 let seed = super::ldm::params::LdmParams {
1579 window_log: params.window_log as u32,
1580 hash_log: ldm_ov.hash_log.unwrap_or(0),
1581 hash_rate_log: ldm_ov.hash_rate_log.unwrap_or(0),
1582 min_match_length: ldm_ov.min_match.unwrap_or(0),
1583 bucket_size_log: ldm_ov.bucket_size_log.unwrap_or(0),
1584 };
1585 seed.derive(strategy_ord)
1586 });
1587 if let MatcherStorage::HashChain(hc) = &mut self.storage {
1588 // Reuse the existing producer's hash-table allocation when the
1589 // derived params are unchanged: only `clear()` (re-zero the
1590 // table + re-seed the rolling hash, no allocation) is needed for
1591 // the new frame. A params change (or the first frame) forces a
1592 // fresh `LdmProducer::new`. On the reused-encoder compress-dict
1593 // path this avoids re-allocating the LDM hash table (large at
1594 // btultra2) every frame — upstream zstd reuses its `ldmState_t`
1595 // the same way. `clear()` is mandatory here for correctness
1596 // regardless of what `BtMatcher::reset` did to the old table.
1597 let producer = derived_ldm.map(|p| match hc.take_ldm_producer() {
1598 Some(mut existing) if existing.params() == p => {
1599 existing.clear();
1600 existing
1601 }
1602 _ => super::ldm::LdmProducer::new(p),
1603 });
1604 hc.set_ldm_producer(producer);
1605 }
1606 }
1607 // Record the resolved matcher shape for the primed-snapshot key. Captured
1608 // here (post-resolution, after the test-only param override) so the key
1609 // reflects exactly the geometry the restored `storage` must match. The
1610 // Fast attach-vs-copy mode is part of the shape ONLY for the Simple
1611 // backend (it decides the distinct dict-table shape that backend builds).
1612 // Dfast/Row/HashChain have their OWN attach/copy regimes, but this bit
1613 // models only the Fast table split; those backends are keyed by the
1614 // resolved matcher geometry instead, so folding the Fast bit into their
1615 // key would over-key identical resolved shapes. When it applies it
1616 // matches the decision `prime_with_dictionary` makes from the same
1617 // `reset_size_log`.
1618 let fast_attach = matches!(next_backend, super::strategy::BackendTag::Simple)
1619 && self.reset_dict_attach_ok
1620 && self
1621 .reset_size_log
1622 .is_none_or(|log| log <= FAST_ATTACH_DICT_CUTOFF_LOG);
1623 // The LDM override is part of the snapshot identity ONLY on the
1624 // optimal (BinaryTree) path: that is the only backend whose cloned
1625 // `storage` carries a `BtMatcher::ldm_producer`. On Fast / Dfast /
1626 // Row and lazy-HashChain resets the producer slot does not exist,
1627 // so folding the override there would over-key the snapshot and
1628 // force needless re-primes when LDM is toggled. Gated like
1629 // `fast_attach` (a key bit only participates where it changes the
1630 // cloned matcher shape).
1631 let active_ldm = if matches!(params.search, super::strategy::SearchMethod::BinaryTree) {
1632 self.param_overrides.and_then(|ov| ov.ldm)
1633 } else {
1634 None
1635 };
1636 self.reset_shape = Some((params, resolved_table_bits, fast_attach, active_ldm));
1637 }
1638
1639 // Dictionary entry points forward to the `dict_prime` child module, which
1640 // owns the prime / snapshot lifecycle (it reaches the driver's private
1641 // `primed` / `reset_shape` state directly as a descendant module).
1642
1643 #[inline]
1644 fn dictionary_is_resident(&self) -> bool {
1645 self.dictionary_is_resident_impl()
1646 }
1647
1648 #[inline]
1649 fn reapply_resident_dictionary(&mut self, offset_hist: [u32; 3]) {
1650 self.reapply_resident_dictionary_impl(offset_hist)
1651 }
1652
1653 #[inline]
1654 fn prime_with_dictionary(&mut self, dict_content: &[u8], offset_hist: [u32; 3]) {
1655 self.prime_with_dictionary_impl(dict_content, offset_hist)
1656 }
1657
1658 #[inline]
1659 fn restore_primed_dictionary(&mut self, level: super::CompressionLevel) -> bool {
1660 self.restore_primed_dictionary_impl(level)
1661 }
1662
1663 #[inline]
1664 fn capture_primed_dictionary(&mut self, level: super::CompressionLevel) {
1665 self.capture_primed_dictionary_impl(level)
1666 }
1667
1668 #[inline]
1669 fn invalidate_primed_dictionary(&mut self) {
1670 self.invalidate_primed_dictionary_impl()
1671 }
1672
1673 #[inline]
1674 fn seed_dictionary_entropy(
1675 &mut self,
1676 huff: Option<&crate::huff0::huff0_encoder::HuffmanTable>,
1677 ll: Option<&crate::fse::fse_encoder::FSETable>,
1678 ml: Option<&crate::fse::fse_encoder::FSETable>,
1679 of: Option<&crate::fse::fse_encoder::FSETable>,
1680 ) {
1681 self.seed_dictionary_entropy_impl(huff, ll, ml, of)
1682 }
1683
1684 fn window_size(&self) -> u64 {
1685 self.reported_window_size as u64
1686 }
1687
1688 fn get_next_space(&mut self) -> Vec<u8> {
1689 if let Some(mut space) = self.vec_pool.pop() {
1690 if space.len() > self.slice_size {
1691 space.truncate(self.slice_size);
1692 }
1693 if space.len() < self.slice_size {
1694 space.resize(self.slice_size, 0);
1695 }
1696 return space;
1697 }
1698 alloc::vec![0; self.slice_size]
1699 }
1700
1701 fn get_last_space(&mut self) -> &[u8] {
1702 match &self.storage {
1703 MatcherStorage::Simple(m) => m.last_committed_space(),
1704 MatcherStorage::Dfast(m) => m.get_last_space(),
1705 MatcherStorage::Row(m) => m.get_last_space(),
1706 MatcherStorage::HashChain(m) => m.table.get_last_space(),
1707 }
1708 }
1709
1710 fn commit_space(&mut self, space: Vec<u8>) {
1711 let mut evicted_bytes = 0usize;
1712 // Split borrows manually so the `add_data` closures can write
1713 // into `vec_pool` while the backend itself holds an exclusive
1714 // borrow via `storage`. (Suffix-store recycling went away
1715 // with the legacy `MatchGenerator`; the FastKernelMatcher
1716 // arm below has no pool interaction.)
1717 let vec_pool = &mut self.vec_pool;
1718 match &mut self.storage {
1719 MatcherStorage::Simple(m) => {
1720 // FastKernelMatcher owns its history as a single
1721 // flat Vec<u8> and the hash table as a Vec<u32> —
1722 // neither recycles into the driver-side pools. The
1723 // eager pre-commit eviction inside
1724 // `FastKernelMatcher::accept_data` drops bytes when
1725 // accepting this block would push history past 2×
1726 // max_window_size; that delta is what feeds
1727 // `evicted_bytes` here via the `pre / post`
1728 // history-length comparison.
1729 let pre = m.history_len_for_eviction_accounting();
1730 m.accept_data(space);
1731 let post = m.history_len_for_eviction_accounting();
1732 // `accept_data` performs eager pre-commit window
1733 // eviction (so this `pre - post` delta correctly
1734 // feeds the dictionary-budget retire flow). See
1735 // `FastKernelMatcher::accept_data` for the
1736 // commit-time-visibility rationale (closes #216
1737 // CodeRabbit review #5 / Copilot review #1: without
1738 // eager eviction, the delta was always 0 and the
1739 // dict budget never retired, leaving max_window_size
1740 // inflated post-dict-prime → matcher could emit
1741 // offsets exceeding the frame header's window).
1742 evicted_bytes += pre.saturating_sub(post);
1743 }
1744 MatcherStorage::Dfast(m) => {
1745 // Dfast's `add_data` callback receives the INPUT
1746 // `Vec<u8>` for pool recycling (Dfast stores its
1747 // bytes in the contiguous `history` buffer, not in
1748 // per-block Vecs — there is no per-block buffer to
1749 // pop off and hand back). Counting `data.len()` as
1750 // evicted bytes would conflate "new bytes ingested"
1751 // with "old bytes evicted from window"; the two
1752 // happen to coincide when the previous window was
1753 // saturated and the new input fills it 1:1, but
1754 // diverge when the eviction pop-loop drops blocks
1755 // of a different size than the incoming input. The
1756 // `dictionary_retained_budget` retire decision
1757 // downstream then gets driven by inflated eviction
1758 // counts and shrinks `max_window_size` prematurely.
1759 //
1760 // Derive the real eviction delta from `window_size`
1761 // before/after the call. The pop loop inside
1762 // `add_data` decrements `window_size` by each
1763 // evicted block length and then the final
1764 // `extend_from_slice + push_back` adds `space_len`,
1765 // so `evicted = pre + space_len - post`.
1766 let pre = m.window_size;
1767 let space_len = space.len();
1768 m.add_data(space, |data| {
1769 // Same per-block recycle as the HashChain arm: push
1770 // the spent input buffer back as-is rather than
1771 // zero-filling to capacity. `add_data` mirrors the
1772 // bytes into `history` and calls this every block, so
1773 // capacity-wide zeroing would be hot-path waste;
1774 // `get_next_space` zeroes at most `slice_size` bytes
1775 // when it later reuses the buffer.
1776 vec_pool.push(data);
1777 });
1778 // Plain `+` (the `saturating_sub` floors at 0): `pre` + one
1779 // block are byte counts bounded by the window, no overflow.
1780 evicted_bytes += (pre + space_len).saturating_sub(m.window_size);
1781 }
1782 MatcherStorage::Row(m) => {
1783 // RowMatchGenerator::add_data recycles the *input* buffer
1784 // through this callback every commit (its bytes are mirrored
1785 // into `history`), not the evicted chunks. Derive the eviction
1786 // delta from `window_size` before/after — `evicted = pre +
1787 // space_len - post` — exactly like the Simple / HashChain arms.
1788 // Counting the callback argument as evicted would charge the
1789 // whole committed block as evicted and prematurely retire
1790 // dictionary budget on a window that evicts nothing.
1791 let pre = m.window_size;
1792 let space_len = space.len();
1793 m.add_data(space, |data| {
1794 // Recycle the spent buffer as-is; `add_data` runs this for
1795 // every committed block, so zero-filling to capacity here
1796 // would be hot-path waste (`get_next_space` zeroes at most
1797 // `slice_size` on reuse).
1798 vec_pool.push(data);
1799 });
1800 // Plain `+` (the `saturating_sub` floors at 0): `pre` + one
1801 // block are byte counts bounded by the window, no overflow.
1802 evicted_bytes += (pre + space_len).saturating_sub(m.window_size);
1803 }
1804 MatcherStorage::HashChain(m) => {
1805 // MatchTable::add_data now recycles the *incoming* buffer
1806 // through `reuse_space` (its bytes are copied into the
1807 // contiguous `history` mirror), so the callback no longer
1808 // reports evicted chunks. Derive the eviction delta from
1809 // `window_size` before/after, exactly like the Simple arm:
1810 // `evicted = pre + space_len - post`.
1811 let pre = m.table.window_size;
1812 let space_len = space.len();
1813 m.table.add_data(space, |data| {
1814 // Recycle the spent input buffer to the pool as-is.
1815 // `add_data` runs this callback for every committed
1816 // block (the bytes are mirrored into `history`), so
1817 // growing the buffer to its full capacity here would
1818 // zero the whole allocation on the hot path.
1819 // `get_next_space` resizes a popped buffer to
1820 // `slice_size` on demand, touching at most
1821 // `slice_size` bytes — never the larger capacity the
1822 // pool retains.
1823 vec_pool.push(data);
1824 });
1825 // Plain `+` (the `saturating_sub` floors at 0): byte counts
1826 // bounded by the window, no overflow.
1827 evicted_bytes += (pre + space_len).saturating_sub(m.table.window_size);
1828 }
1829 }
1830 // Gate the second backend trim pass on actual budget
1831 // reclamation. Without it, every slice commit on the
1832 // no-dictionary / no-eviction path (the common case) would
1833 // run a backend `match` ladder + `trim_to_window` early-out
1834 // for no reason — `trim_after_budget_retire` only does
1835 // meaningful work when `retire_dictionary_budget` shrank
1836 // `max_window_size` enough to make the backend's
1837 // `window_size > max_window_size` invariant trigger
1838 // eviction.
1839 if self.retire_dictionary_budget(evicted_bytes) {
1840 self.trim_after_budget_retire();
1841 }
1842 }
1843
1844 fn start_matching(&mut self, mut handle_sequence: impl for<'a> FnMut(Sequence<'a>)) {
1845 use super::strategy::{self, StrategyTag};
1846 // Borrowed one-shot Fast path: if the frame driver staged a
1847 // block range via `set_borrowed_block`, scan it in place against
1848 // the borrowed window instead of the owned committed block. Only
1849 // the Simple backend is instrumented (the gate guarantees it),
1850 // and the stage is consumed so the next block re-stages.
1851 if let Some((block_start, block_end)) = self.borrowed_pending.take() {
1852 match self.active_backend() {
1853 super::strategy::BackendTag::Simple => {
1854 let m = self.simple_mut();
1855 if m.dict_is_attached() {
1856 // Dict-attach borrowed scan: live matches read the
1857 // borrowed input in place, dict matches read the
1858 // committed dict prefix via the 2-segment counter.
1859 m.start_matching_borrowed_dict(
1860 block_start,
1861 block_end,
1862 &mut handle_sequence,
1863 );
1864 } else {
1865 m.start_matching_borrowed(block_start, block_end, &mut handle_sequence);
1866 }
1867 }
1868 super::strategy::BackendTag::Dfast => self
1869 .dfast_matcher_mut()
1870 .start_matching_borrowed(block_start, block_end, &mut handle_sequence),
1871 super::strategy::BackendTag::Row => {
1872 // Same greedy/lazy parse split as the owned RowHash arm.
1873 let greedy = self.parse == super::strategy::ParseMode::Greedy;
1874 self.row_matcher_mut().start_matching_borrowed(
1875 block_start,
1876 block_end,
1877 greedy,
1878 &mut handle_sequence,
1879 );
1880 }
1881 super::strategy::BackendTag::HashChain => match self.search {
1882 super::strategy::SearchMethod::HashChain => self
1883 .hc_matcher_mut()
1884 .start_matching_lazy_borrowed(block_start, block_end, &mut handle_sequence),
1885 super::strategy::SearchMethod::BinaryTree => {
1886 // Run the SAME BT dispatch as the owned BinaryTree arm
1887 // below — every BT body reads its range via
1888 // current_block_range() and bytes via live_history()
1889 // (borrowed-aware), so the staged block is scanned in
1890 // place. The table was already staged by
1891 // `set_borrowed_block` (the HashChain arm at the top of
1892 // this file calls `table.stage_borrowed_block` with the
1893 // same range, and `borrowed_pending` is set only there),
1894 // so no re-stage is needed here.
1895 // Only btlazy2 reaches the borrowed BinaryTree scan:
1896 // `borrowed_supported()` keeps the optimal parsers
1897 // (BtOpt/BtUltra/BtUltra2) on the owned path, and
1898 // `set_borrowed_block` asserts that predicate before any
1899 // range is staged, so an optimal strategy_tag can never
1900 // arrive here.
1901 match self.strategy_tag {
1902 StrategyTag::Btlazy2 => self
1903 .hc_matcher_mut()
1904 .start_matching_btlazy2(&mut handle_sequence),
1905 other => unreachable!(
1906 "borrowed BinaryTree scan is only supported for Btlazy2, got {other:?}"
1907 ),
1908 }
1909 }
1910 other => {
1911 unreachable!("HashChain backend with unexpected search {other:?}")
1912 }
1913 },
1914 }
1915 return;
1916 }
1917 // Decoupled parse×search dispatch (fires once per block). The
1918 // search axis (`self.search`) picks the candidate-finding backend;
1919 // the parse axis (greedy vs lazy depth) is carried by the
1920 // backend's runtime `lazy_depth`, set per level at `reset()`.
1921 // The two are independent, so any parse can run on any search
1922 // backend. The `BinaryTree` arm still selects the opt `Strategy`
1923 // ZST off `strategy_tag` so `compress_block::<S>` keeps its
1924 // const-folded optimal-parser monomorphisation.
1925 use super::strategy::SearchMethod;
1926 match self.search {
1927 SearchMethod::Fast => {
1928 self.simple_mut().start_matching(&mut handle_sequence);
1929 self.recycle_simple_space();
1930 }
1931 SearchMethod::DoubleFast => {
1932 self.dfast_matcher_mut()
1933 .start_matching(&mut handle_sequence);
1934 }
1935 SearchMethod::RowHash => {
1936 // Greedy parse (depth 0) = upstream zstd-greedy entry (default
1937 // `ip + 1` start, greedy repcode commit); lazy / lazy2 use
1938 // the `pick_lazy_match` lookahead entry (reads `lazy_depth`).
1939 // Both bare entries dispatch on `row_log` internally into the
1940 // const-`ROW_LOG` hot loop (upstream zstd per-rowLog variant table).
1941 let greedy = self.parse == super::strategy::ParseMode::Greedy;
1942 let row = self.row_matcher_mut();
1943 if greedy {
1944 row.start_matching_greedy(&mut handle_sequence);
1945 } else {
1946 row.start_matching(&mut handle_sequence);
1947 }
1948 }
1949 SearchMethod::HashChain => {
1950 // Greedy/lazy/lazy2 all flow through the lazy parser; it
1951 // reads `hc.lazy_depth` (0 = greedy commit).
1952 self.hc_matcher_mut()
1953 .start_matching_lazy(&mut handle_sequence);
1954 }
1955 SearchMethod::BinaryTree => match self.strategy_tag {
1956 StrategyTag::Btlazy2 => self
1957 .hc_matcher_mut()
1958 .start_matching_btlazy2(&mut handle_sequence),
1959 StrategyTag::BtOpt => self.compress_block::<strategy::BtOpt>(&mut handle_sequence),
1960 StrategyTag::BtUltra => {
1961 self.compress_block::<strategy::BtUltra>(&mut handle_sequence)
1962 }
1963 StrategyTag::BtUltra2 => {
1964 self.compress_block::<strategy::BtUltra2>(&mut handle_sequence)
1965 }
1966 _ => unreachable!(
1967 "SearchMethod::BinaryTree requires a BT strategy tag (Btlazy2/BtOpt/BtUltra/BtUltra2)"
1968 ),
1969 },
1970 }
1971 }
1972
1973 fn skip_matching(&mut self) {
1974 self.skip_matching_with_hint(None);
1975 }
1976
1977 fn skip_matching_with_hint(&mut self, incompressible_hint: Option<bool>) {
1978 // Borrowed one-shot Fast path: a staged block range routes to the
1979 // borrowed skip (records the range for `get_last_space`, primes
1980 // hashes on the dict-priming hint) with no owned-history append
1981 // and nothing to recycle. Stage is consumed.
1982 if let Some((block_start, block_end)) = self.borrowed_pending.take() {
1983 match self.active_backend() {
1984 super::strategy::BackendTag::Simple => self.simple_mut().skip_matching_borrowed(
1985 block_start,
1986 block_end,
1987 incompressible_hint,
1988 ),
1989 super::strategy::BackendTag::Dfast => self
1990 .dfast_matcher_mut()
1991 .skip_matching_borrowed(block_start, block_end, incompressible_hint),
1992 super::strategy::BackendTag::Row => self.row_matcher_mut().skip_matching_borrowed(
1993 block_start,
1994 block_end,
1995 incompressible_hint,
1996 ),
1997 super::strategy::BackendTag::HashChain => self
1998 .hc_matcher_mut()
1999 .skip_matching_borrowed(block_start, block_end, incompressible_hint),
2000 }
2001 return;
2002 }
2003 match self.active_backend() {
2004 super::strategy::BackendTag::Simple => {
2005 self.simple_mut()
2006 .skip_matching_with_hint(incompressible_hint);
2007 self.recycle_simple_space();
2008 }
2009 super::strategy::BackendTag::Dfast => {
2010 self.dfast_matcher_mut().skip_matching(incompressible_hint)
2011 }
2012 super::strategy::BackendTag::Row => self
2013 .row_matcher_mut()
2014 .skip_matching_with_hint(incompressible_hint),
2015 super::strategy::BackendTag::HashChain => {
2016 self.hc_matcher_mut().skip_matching(incompressible_hint)
2017 }
2018 }
2019 }
2020}
2021
2022impl MatchGeneratorDriver {
2023 /// Monomorphised optimal-parser entry point. Only the `BinaryTree`
2024 /// search arm of [`Matcher::start_matching`] routes here, selecting
2025 /// the concrete opt `S: Strategy` (BtOpt / BtUltra / BtUltra2) off
2026 /// `strategy_tag`, so the optimiser keeps the cost-model predicates
2027 /// (`S::USE_BT` / `S::USE_HASH3` / `S::ACCURATE_PRICE` /
2028 /// `S::TWO_PASS_SEED`) const-folded per strategy. The non-opt search
2029 /// backends (Fast / DoubleFast / RowHash / HashChain) are dispatched
2030 /// directly off the search axis and never reach this method, so all
2031 /// strategies arriving here are HashChain-backed.
2032 fn compress_block<S: super::strategy::Strategy>(
2033 &mut self,
2034 handle_sequence: &mut impl for<'a> FnMut(Sequence<'a>),
2035 ) {
2036 debug_assert_eq!(S::BACKEND, super::strategy::BackendTag::HashChain);
2037 debug_assert!(
2038 S::USE_BT,
2039 "compress_block only handles the optimal (BT) path"
2040 );
2041 self.hc_matcher_mut()
2042 .start_matching_strategy::<S>(handle_sequence);
2043 }
2044}
2045
2046/// Stage D: backend storage discriminator.
2047///
2048/// HC (lazy / lazy2) modes carry no extra per-frame state beyond the
2049/// shared `MatchTable` and `HcMatcher` runtime knobs, so the
2050/// [`HcBackend::Hc`] variant is zero-sized — no BT scratch is
2051/// allocated. BT-flavoured modes (`btopt` / `btultra` / `btultra2`)
2052/// hold the full [`super::bt::BtMatcher`] inside the
2053/// [`HcBackend::Bt`] variant (cost model, optimal-parser scratch
2054/// arenas, LDM candidate buffer).
2055///
2056/// The discriminator lives next to `parse_mode` so `configure()` can
2057/// promote between the two on a level change without touching the
2058/// `MatchTable` storage.
2059#[derive(Clone)]
2060pub(crate) enum HcBackend {
2061 /// Lazy / lazy2 modes — no per-frame backend state.
2062 Hc,
2063 /// BT-driven modes — owns the optimal parser's per-frame scratch.
2064 /// Boxed so the enum stays pointer-sized: HC-only matchers pay
2065 /// just the `Box`-niche, not the 4 KiB `BtMatcher` payload.
2066 Bt(alloc::boxed::Box<super::bt::BtMatcher>),
2067}
2068
2069#[cfg(feature = "bench_internals")]
2070pub(crate) fn level22_block_ranges(data: &[u8]) -> Vec<(usize, usize)> {
2071 let mut ranges = Vec::new();
2072 let mut cursor = 0usize;
2073 let mut savings = 0i64;
2074 while cursor < data.len() {
2075 let remaining = data.len() - cursor;
2076 let candidate_len = remaining.min(super::cost_model::HC_BLOCKSIZE_MAX);
2077 let block_len = crate::encoding::frame_compressor::optimal_block_size(
2078 CompressionLevel::Level(22),
2079 &data[cursor..cursor + candidate_len],
2080 remaining,
2081 super::cost_model::HC_BLOCKSIZE_MAX,
2082 savings,
2083 )
2084 .min(candidate_len)
2085 .max(1);
2086 ranges.push((cursor, block_len));
2087 cursor += block_len;
2088 // The exact upstream zstd gate uses compressed-size savings. For this corpus
2089 // parity harness, after the first full block has compressed, savings is
2090 // sufficient to authorize the same pre-block splitter path.
2091 if cursor >= super::cost_model::HC_BLOCKSIZE_MAX {
2092 savings = 3;
2093 }
2094 }
2095 ranges
2096}
2097
2098#[cfg(feature = "bench_internals")]
2099fn merge_block_delimiters(sequences: Vec<(usize, usize, usize)>) -> Vec<(usize, usize, usize)> {
2100 let mut out = Vec::with_capacity(sequences.len());
2101 let mut pending_lits = 0usize;
2102 for (lit_len, offset, match_len) in sequences {
2103 if offset == 0 && match_len == 0 {
2104 pending_lits = pending_lits.saturating_add(lit_len);
2105 continue;
2106 }
2107 out.push((lit_len.saturating_add(pending_lits), offset, match_len));
2108 pending_lits = 0;
2109 }
2110 if pending_lits > 0 {
2111 out.push((pending_lits, 0, 0));
2112 }
2113 out
2114}
2115
2116/// White-box capture of the level-22 sequence stream (literal-length,
2117/// offset, match-length triples) the match generator emits for `data`,
2118/// with block-delimiter pseudo-sequences merged into the following
2119/// triple's literal run. Pure Rust; the C-conformance comparison that
2120/// consumes it lives in the `ffi-bench` crate.
2121#[cfg(feature = "bench_internals")]
2122pub(crate) fn collect_level22_sequences(data: &[u8]) -> Vec<(usize, usize, usize)> {
2123 merge_block_delimiters(collect_level22_sequences_with_delimiters(data))
2124 .into_iter()
2125 .filter(|(_, offset, match_len)| *offset != 0 || *match_len != 0)
2126 .collect()
2127}
2128
2129#[cfg(feature = "bench_internals")]
2130fn collect_level22_sequences_with_delimiters(data: &[u8]) -> Vec<(usize, usize, usize)> {
2131 let mut driver = MatchGeneratorDriver::new(super::cost_model::HC_BLOCKSIZE_MAX, 1);
2132 driver.set_source_size_hint(data.len() as u64);
2133 driver.reset(CompressionLevel::Level(22));
2134
2135 let mut sequences = Vec::new();
2136 for (chunk_start, chunk_len) in level22_block_ranges(data) {
2137 let chunk = &data[chunk_start..chunk_start + chunk_len];
2138 let mut space = driver.get_next_space();
2139 space[..chunk.len()].copy_from_slice(chunk);
2140 space.truncate(chunk.len());
2141 driver.commit_space(space);
2142 driver.start_matching(|seq| {
2143 let entry = match seq {
2144 Sequence::Literals { literals } => (literals.len(), 0usize, 0usize),
2145 Sequence::Triple {
2146 literals,
2147 offset,
2148 match_len,
2149 } => (literals.len(), offset, match_len),
2150 };
2151 sequences.push(entry);
2152 });
2153 }
2154 sequences
2155}
2156
2157#[cfg(test)]
2158mod tests;