Skip to main content

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