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