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;
29use super::sequences::{ExternalSequence, SequenceProducer};
30// FAST_HASH_FILL_STEP test-only re-export was tied to the legacy
31// SuffixStore MatchGenerator's interleaved hash-fill stride. The
32// upstream zstd-shape Fast kernel walks ip0 with kSearchStrength step-skip
33// acceleration instead, so the constant has no consumer in the
34// remaining live test set today.
35#[cfg(test)]
36use super::match_table::helpers::INCOMPRESSIBLE_SKIP_STEP;
37use super::match_table::helpers::MIN_MATCH_LEN;
38#[cfg(test)]
39use super::match_table::helpers::common_prefix_len;
40#[cfg(test)]
41use super::opt::ldm::HcRawSeq;
42#[cfg(test)]
43use super::opt::types::{HcCandidateQuery, MatchCandidate};
44use super::row::RowMatchGenerator;
45use super::simple::fast_matcher::{
46    FAST_LEVEL_1_HASH_LOG, FAST_LEVEL_1_MLS, FastKernelMatcher, TableCarry,
47};
48use super::workspace::HistoryBuf;
49
50pub(crate) const DFAST_MIN_MATCH_LEN: usize = 5;
51// Bytes the dfast short hash reads (upstream zstd `mls = 5`). Seeding / lookahead
52// guards use it so a position is only short-hashed once its full 5-byte key
53// is in range.
54pub(crate) const DFAST_SHORT_HASH_LOOKAHEAD: usize = 5;
55pub(crate) const ROW_MIN_MATCH_LEN: usize = 5;
56// Upstream zstd `clevels.h:31` at level 3 large-input bucket sets
57// `hashLog = 17` (the long-hash table) and `chainLog = 16` (the
58// short-hash table — upstream zstd names this `chainTable` even though for
59// dfast it's used as a plain single-slot hash). Each table holds one
60// `U32` per slot; the upstream zstd overwrites on collision and recovers
61// compression quality via the inline `_search_next_long` retry
62// (after a short-hash hit, probes `hashLong[hl1]` at `ip + 1` and
63// keeps the longer match).
64//
65// We mirror that storage layout: single `u32` per bucket (no
66// `[u32; N]` array), `long_hash` sized `1 << DFAST_HASH_BITS` and
67// `short_hash` one bit smaller via `DFAST_SHORT_HASH_BITS_DELTA`.
68// Two-table footprint at Level 3: `2^17 × 4 + 2^16 × 4 = 768 KiB`,
69// exact upstream parity. The `_search_next_long` retry lives in
70// `DfastMatchGenerator::hash_candidate` (called via
71// `best_match`). Earlier revisions kept a
72// 4-slot bucket per hash position; that paid 4× the upstream zstd memory
73// without measurable ratio gain once the retry was in place.
74//
75// `dfast_hash_bits_for_window` still clamps the runtime long-hash
76// value to `[MIN_WINDOW_LOG, DFAST_HASH_BITS]`, so this const is the
77// upper bound rather than a fixed default.
78pub(crate) const DFAST_HASH_BITS: usize = 17;
79/// Difference between `long_hash_bits` and `short_hash_bits` —
80/// upstream zstd `hashLog - chainLog` is 1 at every dfast level (`clevels.h`
81/// level 2: 16-15=1; level 3: 17-16=1). The short hash is one bit
82/// smaller than the long hash so the per-bucket footprint matches
83/// upstream zstd sizing exactly.
84pub(crate) const DFAST_SHORT_HASH_BITS_DELTA: usize = 1;
85/// Sentinel value for an empty slot in the dfast hash tables. Real
86/// positions are stored as `(abs_pos - position_base + 1) as u32`, so
87/// `0` is reserved as the "empty" marker and a true relative offset
88/// of `0` never appears in the table. Mirrors the LDM table's
89/// `LdmEntry.offset == 0` convention (see `encoding/ldm/table.rs`)
90/// so both rebasing structures share
91/// one sentinel scheme.
92pub(crate) const DFAST_EMPTY_SLOT: u32 = 0;
93
94/// Guard band reserved above the high-water mark before triggering a
95/// rebase on the Dfast hash tables. When the next insert would push a
96/// relative offset above `u32::MAX - DFAST_REBASE_GUARD_BAND`, the
97/// table calls `reduce(GUARD_BAND)` to shift every slot down and
98/// advance `position_base` so future inserts stay inside the `u32`
99/// window. Same scheme as `encoding/ldm/table.rs`.
100pub(crate) const DFAST_REBASE_GUARD_BAND: u32 = 1u32 << 30;
101// `kSearchStrength` (upstream `zstd_compress_internal.h:32`). The dfast step
102// ramp grows one position every `1 << kSearchStrength` = 256 bytes travelled
103// (upstream `kStepIncr`, zstd_double_fast.c:131). A smaller value accelerates
104// the scan faster and skips source positions upstream still inserts, which
105// drops the short matches upstream finds at a block start — so the
106// `#167`-disabled path must use the upstream 8 to stay byte-identical.
107pub(crate) const DFAST_SKIP_SEARCH_STRENGTH: usize = 8;
108pub(crate) const DFAST_SKIP_STEP_GROWTH_INTERVAL: usize = 1 << DFAST_SKIP_SEARCH_STRENGTH;
109/// How densely dfast indexes a block it wrote off without searching.
110///
111/// The block is not searched, so the only reason to index it at all is that a
112/// LATER block may duplicate it, and a duplicate is block-sized: the search
113/// that scans it meets an indexed position within one step, which is far inside
114/// what it scans anyway. Indexing at every sixteenth position instead cost a
115/// third of the encode on incompressible input — two tables, sixty-five
116/// thousand stores per mebibyte — for a proximity nothing needs. Fast reads the
117/// same reasoning from
118/// [`RAW_SKIP_INDEX_STEP`](crate::encoding::incompressible::RAW_SKIP_INDEX_STEP).
119pub(crate) const DFAST_INCOMPRESSIBLE_SKIP_STEP: usize =
120    crate::encoding::incompressible::RAW_SKIP_INDEX_STEP;
121pub(crate) const ROW_HASH_BITS: usize = 20;
122pub(crate) const ROW_LOG: usize = 5;
123pub(crate) const ROW_SEARCH_DEPTH: usize = 16;
124pub(crate) const ROW_TARGET_LEN: usize = 48;
125pub(crate) const ROW_TAG_BITS: usize = 8;
126pub(crate) const ROW_EMPTY_SLOT: u32 = u32::MAX;
127pub(crate) const ROW_HASH_KEY_LEN: usize = 4;
128// HC_PRIME3BYTES / HC_PRIME4BYTES moved to match_table::storage
129// alongside the hash helpers in Phase 1e Stage A. Only the test
130// module references the constants directly (production code goes
131// through `MatchTable::hash_value_with_mls`).
132#[cfg(test)]
133use super::match_table::storage::{HC_PRIME3BYTES, HC_PRIME4BYTES};
134
135// HC_HASH_LOG / HC_CHAIN_LOG / HC3_HASH_LOG / HC_EMPTY live on the
136// shared storage module so MatchTable methods can reference them
137// without pulling in this module. Re-imported here so existing
138// macros / configs / tests keep their unqualified names.
139#[cfg(test)]
140use super::match_table::storage::HC_EMPTY;
141// HC3_MAX_OFFSET moved to encoding::bt alongside the hash3 candidate
142// probe macro that consumes it; the macro references it via the
143// fully-qualified `$crate::encoding::bt::HC3_MAX_OFFSET` path so this
144// module no longer needs a local import.
145pub(crate) const HC_SEARCH_DEPTH: usize = 16;
146// HC_MIN_MATCH_LEN moved to encoding::hc; re-imported here so
147// existing references compile unchanged.
148pub(crate) const HC_OPT_MIN_MATCH_LEN: usize = HC_FORMAT_MINMATCH;
149pub(crate) const HC_TARGET_LEN: usize = 48;
150
151// MAX_HC_SEARCH_DEPTH moved to encoding::hc alongside chain_candidates.
152// Per-level tuning config (the config structs + `LEVEL_TABLE` + the
153// level→params resolution chain) lives in `levels::config`; the driver imports
154// that resolution API here.
155use super::levels::config::*;
156// The HashChain / BT match generator + its optimal-parse machinery lives in
157// `hc::generator`; the driver stores it in the `HashChain` storage variant.
158use super::hc::generator::HcMatchGenerator;
159
160// Dictionary prime + CDict-equivalent snapshot lifecycle. A child module so it
161// can reach the driver's private `primed` / `reset_shape` state directly; the
162// `Matcher` trait's dict entry points forward to its inherent `*_impl` helpers.
163mod dict_prime;
164// Long-distance matching: the search ahead of the blocks and the blocks
165// matched around its matches. A child module for the same private access.
166#[cfg(feature = "ldm")]
167mod long_distance;
168
169// `Strategy` and `StrategyTag` live in `crate::encoding::strategy`.
170// The driver carries a `StrategyTag` field set at `reset()` and
171// dispatches each block into a monomorphised `compress_block::<S>`
172// per concrete strategy.
173
174/// Backend storage for [`MatchGeneratorDriver`]. Exactly one match-finder
175/// state lives in the driver at a time — the active variant. Backend
176/// transitions in [`Matcher::reset`] release the current variant's tables and
177/// then replace `storage` with a freshly constructed variant for the new
178/// backend.
179///
180/// Replaces the prior pattern of four parallel fields (`match_generator`,
181/// `dfast_match_generator: Option<…>`, `row_match_generator: Option<…>`,
182/// `hc_match_generator: Option<…>`) + an `active_backend: BackendTag`
183/// discriminator: the parallel layout kept drained inner structures
184/// allocated across backend switches, and every per-frame/per-slice
185/// driver operation had to dispatch on `active_backend` to pick the
186/// right field. A single enum collapses the storage and makes the
187/// dispatcher pattern-match on the storage variant directly — same
188/// number of arms, but `storage.backend()` is now the canonical source
189/// of truth and dead variants are dropped when the active backend
190/// changes.
191///
192/// The variant is chosen by the resolved search method,
193/// [`LevelParams::backend`](crate::encoding::levels::config::LevelParams::backend),
194/// never by the level number alone: the search method comes from the
195/// (level, source size) cParams row plus any parameter override, and the
196/// size tier can change it. Level 11, for one, runs lazy2 on the Row backend
197/// for a large source and btopt on the HashChain backend for 16 KiB or less.
198/// Resolve the parameters for the input before assuming which backend runs.
199#[derive(Clone)]
200enum MatcherStorage {
201    /// Upstream zstd `ZSTD_fast` family, for `SearchMethod::Fast`.
202    /// Constructed by [`MatchGeneratorDriver::new`] as the initial variant
203    /// and re-selected by [`Matcher::reset`] when that search method resolves.
204    Simple(FastKernelMatcher),
205    /// Upstream zstd `ZSTD_dfast` family — two-table hash chain, for
206    /// `SearchMethod::DoubleFast`.
207    Dfast(DfastMatchGenerator),
208    /// Upstream zstd `lazy_generic` parse (`ZSTD_greedy` / `ZSTD_lazy` /
209    /// `ZSTD_lazy2` / `ZSTD_btlazy2`), for `SearchMethod::RowHash` and
210    /// `SearchMethod::BinaryTreeLazy`. The Row backend itself searches a hash
211    /// chain instead of rows when the resolved window is small.
212    Row(RowMatchGenerator),
213    /// The hash-chain matcher, for `SearchMethod::HashChain` and
214    /// `SearchMethod::BinaryTree`; the latter carries the BT-based optimal
215    /// modes (`btopt` / `btultra` / `btultra2`). The [`HcMatchGenerator`]'s
216    /// internal [`HcBackend`] discriminator decides whether BT scratch is
217    /// allocated.
218    HashChain(HcMatchGenerator),
219}
220
221impl MatcherStorage {
222    /// Heap bytes the active backend variant holds (tables, history, scratch).
223    fn heap_size(&self) -> usize {
224        match self {
225            Self::Simple(m) => m.heap_size(),
226            Self::Dfast(m) => m.heap_size(),
227            Self::Row(m) => m.heap_size(),
228            Self::HashChain(m) => m.heap_size(),
229        }
230    }
231
232    /// Capacity of the buffer blocks are read into, for the backends that
233    /// ingest in place. A frame sized up front reserves this exactly; one that
234    /// grew into it lands on a doubling step instead, which is what makes the
235    /// difference observable to a test.
236    #[cfg(test)]
237    fn ingest_capacity(&self) -> usize {
238        match self {
239            Self::Simple(m) => m.history_capacity(),
240            Self::Dfast(m) => m.history.capacity(),
241            Self::Row(m) => m.history.capacity(),
242            Self::HashChain(m) => m.table.history.capacity(),
243        }
244    }
245
246    /// See the backends' `set_dictionary_index_floor`.
247    fn set_dictionary_index_floor(&mut self, floor: usize) {
248        match self {
249            Self::Simple(m) => m.set_dictionary_index_floor(floor),
250            Self::Dfast(m) => m.set_dictionary_index_floor(floor),
251            Self::Row(m) => m.set_dictionary_index_floor(floor),
252            Self::HashChain(m) => m.table.set_dictionary_index_floor(floor),
253        }
254    }
255
256    /// The committed window of the owned history, oldest byte first.
257    #[cfg(feature = "ldm")]
258    fn committed_window(&self) -> &[u8] {
259        match self {
260            Self::Simple(m) => m.committed_window(),
261            Self::Dfast(m) => m.live_history(),
262            Self::Row(m) => m.live_history(),
263            Self::HashChain(m) => m.table.live_history(),
264        }
265    }
266
267    /// The committed window followed by the input read but not committed.
268    #[cfg(feature = "ldm")]
269    fn window_with_pending(&self) -> &[u8] {
270        match self {
271            Self::Simple(m) => m.window_with_pending(),
272            Self::Dfast(m) => m.window_with_pending(),
273            Self::Row(m) => m.window_with_pending(),
274            Self::HashChain(m) => m.table.window_with_pending(),
275        }
276    }
277
278    /// Length of the last committed block.
279    #[cfg(feature = "ldm")]
280    fn last_block_len(&self) -> usize {
281        match self {
282            Self::Simple(m) => m.last_committed_space().len(),
283            Self::Dfast(m) => m.get_last_space().len(),
284            Self::Row(m) => m.get_last_space().len(),
285            Self::HashChain(m) => m.table.get_last_space().len(),
286        }
287    }
288
289    /// See the backends' `uncommit_block`.
290    #[cfg(feature = "ldm")]
291    fn uncommit_block(&mut self, len: usize) {
292        match self {
293            Self::Simple(m) => m.uncommit_block(len),
294            Self::Dfast(m) => m.uncommit_block(len),
295            Self::Row(m) => m.uncommit_block(len),
296            Self::HashChain(m) => m.table.uncommit_block(len),
297        }
298    }
299
300    /// Commit `len` bytes of a block handed back by [`Self::uncommit_block`].
301    /// The whole block fit the window when it was first committed, so a piece
302    /// of it evicts nothing.
303    #[cfg(feature = "ldm")]
304    fn commit_piece(&mut self, len: usize) {
305        match self {
306            Self::Simple(m) => {
307                let evicted = m.commit_block(len);
308                debug_assert_eq!(evicted, 0, "a recommitted piece evicted window bytes");
309            }
310            Self::Dfast(m) => m.commit_block(len),
311            Self::Row(m) => m.commit_block(len),
312            Self::HashChain(m) => m.table.commit_block(len),
313        }
314    }
315
316    /// See the backends' `skip_long_match`.
317    #[cfg(feature = "ldm")]
318    fn skip_long_match(&mut self) {
319        match self {
320            Self::Simple(m) => m.skip_long_match(),
321            Self::Dfast(m) => m.skip_long_match(),
322            Self::Row(m) => m.skip_long_match(),
323            Self::HashChain(m) => m.table.skip_long_match(),
324        }
325    }
326
327    /// See the backends' `note_long_match`.
328    #[cfg(feature = "ldm")]
329    fn note_long_match(&mut self, offset: usize, literal_len: usize) {
330        match self {
331            Self::Simple(m) => m.note_long_match(offset, literal_len),
332            Self::Dfast(m) => m.note_long_match(offset, literal_len),
333            Self::Row(m) => m.note_long_match(offset, literal_len),
334            Self::HashChain(m) => m.table.note_long_match(offset, literal_len),
335        }
336    }
337
338    /// [`super::strategy::BackendTag`] family of the active variant.
339    fn backend(&self) -> super::strategy::BackendTag {
340        use super::strategy::BackendTag;
341        match self {
342            Self::Simple(_) => BackendTag::Simple,
343            Self::Dfast(_) => BackendTag::Dfast,
344            Self::Row(_) => BackendTag::Row,
345            Self::HashChain(_) => BackendTag::HashChain,
346        }
347    }
348}
349
350/// This is the default implementation of the `Matcher` trait. It allocates and reuses the buffers when possible.
351pub struct MatchGeneratorDriver {
352    /// Active match-finder state. Exactly one backend lives here at a
353    /// time; [`Matcher::reset`] swaps in a freshly constructed variant for
354    /// the new backend. `storage.backend()` is the canonical source of
355    /// truth for the parse family; `strategy_tag` carries the
356    /// compile-time strategy chosen at the last `reset()`.
357    storage: MatcherStorage,
358    // Compile-time strategy tag resolved at `reset()` from the
359    // requested `CompressionLevel`'s `LevelParams`. The driver's
360    // hot-block dispatcher in `blocks/compressed.rs` matches on
361    // this tag to enter the corresponding `Strategy`
362    // monomorphisation (`compress_block::<S>`).
363    strategy_tag: super::strategy::StrategyTag,
364    // Decoupled search-method axis resolved at `reset()` from
365    // `LevelParams.search`. The per-block dispatcher routes on this
366    // (not on `strategy_tag`) so a level's parse and search backend can
367    // be chosen independently. The `BinaryTree` arm still consults
368    // `strategy_tag` to pick the opt `Strategy` ZST.
369    search: super::strategy::SearchMethod,
370    // Decoupled parse-mode axis resolved at `reset()` from
371    // `LevelParams::parse()`. Independent of `search`: greedy / lazy /
372    // lazy2 can run on any non-opt search backend. The backends still
373    // read their own `lazy_depth` (kept in sync at `reset()`); this is
374    // the authoritative parse selector for the dispatcher.
375    pub(crate) parse: super::strategy::ParseMode,
376    /// Test-only per-level recipe override applied in `reset()` before
377    /// backend selection. Lets the parse×search matrix be exercised
378    /// without editing `LEVEL_TABLE`; never compiled into production.
379    #[cfg(test)]
380    config_override: Option<(super::strategy::SearchMethod, super::strategy::ParseMode)>,
381    /// Fine-grained per-knob overrides from the public
382    /// [`super::parameters::CompressionParameters`] surface (#27).
383    /// `None` (or an all-`None` [`super::parameters::ParamOverrides`])
384    /// keeps the resolved level geometry byte-identical to plain
385    /// level-based compression. Applied in [`Matcher::reset`] after the
386    /// level params are resolved, before backend selection. Persists
387    /// across resets (it is frame configuration, not a one-shot) until
388    /// the caller changes it.
389    param_overrides: Option<super::parameters::ParamOverrides>,
390    /// Chunk a dictionary is primed in: `base_slice_size` capped by the
391    /// frame's window.
392    slice_size: usize,
393    base_slice_size: usize,
394    // Frame header window size must stay at the configured live-window budget.
395    // Dictionary retention expands internal matcher capacity only.
396    reported_window_size: usize,
397    // Tracks currently retained bytes that originated from primed dictionary
398    // history and have not been evicted yet.
399    dictionary_retained_budget: usize,
400    // Source size hint for next frame (set via set_source_size_hint, cleared on reset).
401    source_size_hint: Option<u64>,
402    // Dictionary sizes for the next frame (set via set_dictionary_size_hint,
403    // consumed on reset): the serialized size keys the CDict cParams tier the
404    // frame runs (upstream `ZSTD_getCParamRowSize` on `dictSize`), the content
405    // size the dictionary tables and attach cutoffs.
406    dictionary_size_hint: Option<super::DictionarySizes>,
407    // Normalized `ceil_log2` bucket of the frame's source-size hint, captured at
408    // `reset` (where `source_size_hint` is consumed) via [`source_size_ceil_log`].
409    // `None` means the frame was unhinted. Drives `prime_with_dictionary`'s upstream zstd
410    // `ZSTD_shouldAttachDict` mode for the Simple/Fast backend: `None` (unknown)
411    // or `<= FAST_ATTACH_DICT_CUTOFF_LOG` → attach (separate dict table, 2-cursor
412    // `compress_block_fast_dict`); larger → copy (dictionary primed into the live
413    // table, 4-cursor `compress_block_fast`). The primed-snapshot key is the
414    // resolved shape ([`reset_shape`](Self::reset_shape)), not this bucket.
415    reset_size_log: Option<u8>,
416    // Whether the loaded dictionary fits the Fast attach path's tagged position
417    // field (`<= MAX_FAST_ATTACH_DICT_REGION`). Captured at `reset` from the
418    // dict-size hint (which equals the actual dict length on load) so the Fast
419    // attach decision, the attach-epoch reset bit, and the primed-snapshot
420    // `fast_attach` bit all gate on it consistently. `true` when there is no
421    // dictionary (the attach path is then unused). A dict too large to tag falls
422    // back to copy mode instead of overflowing the packed position.
423    reset_dict_attach_ok: bool,
424    // Hint-resolved matcher shape from the last `reset`: the [`LevelParams`], the
425    // active backend's applied Dfast/Row hash-table width (`0` for HC/Fast), the
426    // Fast attach-vs-copy mode and the Fast slot format. Combined with the
427    // frame's level into the [`PrimedKey`] that keys the primed snapshot, so it
428    // is only restored into a reset that resolved the identical matcher. `None`
429    // before the first `reset`.
430    reset_shape: Option<(LevelParams, usize, bool, bool)>,
431    // One-shot borrowed block range `[start, end)` staged by the borrowed
432    // Fast frame path (`set_borrowed_block`) for the NEXT
433    // `start_matching` / `skip_matching_with_hint`. `Some` routes that
434    // call to the Simple backend's borrowed scan instead of the owned
435    // committed-block path; consumed (reset to `None`) by the routed
436    // call. Always `None` on the owned streaming path.
437    borrowed_pending: Option<(usize, usize)>,
438    /// CDict-equivalent: snapshot of the post-prime matcher state taken
439    /// once after the first dictionary prime — the backend `storage`
440    /// (hash tables + dictionary history + offset history + window) plus
441    /// the driver-level `dictionary_retained_budget`, the only two pieces
442    /// `prime_with_dictionary` writes. Subsequent frames restore this
443    /// (a table memcpy) instead of re-hashing every dictionary position,
444    /// mirroring upstream zstd `ZSTD_compressBegin_usingCDict` copying the
445    /// precomputed `cdict->matchState`. Invalidated when the dictionary
446    /// changes; keyed by the [`PrimedKey`] resolved matcher shape so a snapshot
447    /// is only restored into a reset that produces the same matcher — see
448    /// `restore_primed_dictionary`.
449    primed: Option<(MatcherStorage, usize, PrimedKey)>,
450    /// Where the tables live when the driver is reset on its own through
451    /// [`Matcher::reset`]; inside a compression context they live in the
452    /// context's workspace instead and this stays empty.
453    own_workspace: crate::encoding::workspace::Workspace,
454    /// Whether this frame's input, handed over as one slice, is scanned in
455    /// place rather than copied into the history. Decided at the reset,
456    /// because it decides how large the history is laid out.
457    frame_in_place: bool,
458    /// Long-distance matcher, `Some` while the frame runs with it on. It
459    /// belongs to the driver rather than to a backend, as upstream keeps
460    /// `ldmState` beside the match state: every strategy reads the same one.
461    #[cfg(feature = "ldm")]
462    ldm: Option<super::ldm::LdmProducer>,
463    /// The long-distance matches searched ahead of the blocks.
464    #[cfg(feature = "ldm")]
465    ldm_store: super::ldm::store::LdmSequenceStore,
466    /// What the last search found, before the store takes it.
467    #[cfg(feature = "ldm")]
468    ldm_found: Vec<super::opt::ldm::HcRawSeq>,
469    /// The long-distance sequences of the block being matched.
470    #[cfg(feature = "ldm")]
471    ldm_block: Vec<super::opt::ldm::HcRawSeq>,
472    /// Stream position of the committed end, in the coordinates the
473    /// long-distance matcher keeps its table in: the frame's first byte
474    /// (its dictionary's, when it has one) is `0`.
475    #[cfg(feature = "ldm")]
476    ldm_committed_end: usize,
477    /// Where the frame's dictionary ends in those coordinates, `0` without
478    /// one: what a match may still reach while the window has not passed it.
479    #[cfg(feature = "ldm")]
480    ldm_dict_end: usize,
481    /// Length of a block the long-distance matcher recommitted in pieces, so
482    /// [`Matcher::get_last_space`] reports the whole block, not its last piece.
483    #[cfg(feature = "ldm")]
484    ldm_split_block: Option<usize>,
485    /// A parser asked for each block in place of the backends, when one is
486    /// installed (C `ZSTD_registerSequenceProducer`).
487    producer: Option<ProducerSlot>,
488}
489
490/// A [`SequenceProducer`] and what to do when it gives no parse.
491struct ProducerSlot {
492    producer: alloc::boxed::Box<dyn SequenceProducer>,
493    /// Match the block with the backend instead when the producer fails;
494    /// otherwise the block goes out as literals and the failure is recorded.
495    fallback: bool,
496    /// A block the producer failed on went out without a fallback.
497    failed: bool,
498    /// The producer's parse of the block in hand, kept for its allocation.
499    parse: Vec<ExternalSequence>,
500}
501
502/// Identity of the matcher configuration a primed snapshot was captured under:
503/// the FULLY RESOLVED matcher shape, not the raw source-size hint.
504///
505/// `reset()` resolves the hint into a [`LevelParams`] (window_log cap, the
506/// HC/Fast table and search geometry, the parse depth/target-length that get
507/// baked into the restored `storage`) plus, for the Dfast/Row backends, a
508/// table-width derived from the hint's ceil-log bucket. The mapping from hint
509/// to resolved shape is many-to-one: the source-size adjustment is monotone in
510/// `ceil_log2(hint)`, and Level 22 additionally collapses several buckets onto
511/// one upstream zstd tier (its `<= 16/128/256 KiB` thresholds). Keying on the raw hint
512/// (or even its ceil-log bucket) therefore over-keys — two hints that resolve
513/// to the identical matcher would each force a full re-prime. Keying on the
514/// resolved (`params`, `table_bits`) pair restores across them.
515///
516/// `table_bits` is the hint-dependent hash-table width the ACTIVE backend
517/// applied (`set_hash_bits` value for Dfast/Row; `0` for HC/Fast, whose widths
518/// already live in `params`). The snapshot is only ever captured on the COPY
519/// path (a hinted, above-cutoff frame), so `table_bits` is always the resolved
520/// Dfast/Row value there, never the unhinted default.
521///
522/// `level` is kept alongside the resolved `params` because some stored matcher
523/// state is derived from the level DIRECTLY, not through `params`: e.g. Dfast's
524/// `use_fast_loop` is true for L3 but false for L4, yet L3 and L4 resolve to
525/// byte-identical `params`. Without `level` a snapshot captured at L3 could be
526/// restored into an L4 reset, installing the wrong `use_fast_loop`.
527///
528/// `fast_attach` records the Fast backend's attach-vs-copy mode
529/// ([`FAST_ATTACH_DICT_CUTOFF_LOG`]) because that cutoff (8 KiB) falls INSIDE a
530/// single resolved shape: an 8192- and an 8193-byte Level 1 hint both clamp to
531/// window_log 14 with identical `params`/`table_bits`, yet 8192 attaches (a
532/// separate dict table) while 8193 copies into the live table — two different
533/// `storage` shapes. The frame compressor only captures/restores snapshots on
534/// the copy path today, but keying on the mode keeps the snapshot identity
535/// self-sufficient rather than relying on that external gate.
536///
537/// Restoring a snapshot whose key differs would reinstate the old `storage`
538/// (and its `max_window_size` / table dimensions / parse params / dict-table
539/// shape) under a reset that resolved a different shape — the encoder could
540/// then search past the frame header's window and emit an undecodable match.
541/// All fields must match before a restore is allowed.
542#[derive(Clone, Copy, PartialEq, Eq)]
543struct PrimedKey {
544    level: super::CompressionLevel,
545    params: LevelParams,
546    table_bits: usize,
547    fast_attach: bool,
548    /// Whether the Fast table's slots are tagged. The format is chosen per
549    /// frame from how full the input will leave the table, so two frames of one
550    /// geometry can differ in it; a restore carries the snapshot's format in.
551    fast_tagged: bool,
552}
553
554/// Whether a configured HashChain matcher attaches the dictionary for a frame
555/// of `size_log` (see [`MatchGeneratorDriver::hc_dict_attach_mode`]).
556fn hc_attaches_dictionary(hc: &HcMatchGenerator, size_log: Option<u8>) -> bool {
557    let cutoff = if hc.table.uses_bt {
558        match hc.strategy_tag {
559            super::strategy::StrategyTag::BtUltra | super::strategy::StrategyTag::BtUltra2 => {
560                BT_ULTRA_ATTACH_DICT_CUTOFF_LOG
561            }
562            _ => BT_OPT_ATTACH_DICT_CUTOFF_LOG,
563        }
564    } else {
565        HC_ATTACH_DICT_CUTOFF_LOG
566    };
567    size_log.is_none_or(|log| log <= cutoff)
568}
569
570/// History bytes a frame lays out: the dictionary primed at its head and the
571/// input it is expected to bring. Input that can fill the window slides it, and
572/// the history then grows to the most it ever holds, which is laid out from the
573/// start, as upstream sizes its input buffer from the window: for the Fast
574/// backend twice the window grown by the dictionary (it drains back to one
575/// window when an append would pass two), for the others that window plus the
576/// quarter of it compaction leaves behind; either way plus one pending block of
577/// the frame's size.
578///
579/// A size known exactly (a slice, or a pledge the context enforces) is taken as
580/// it is, and its reads are held to what remains, so nothing past it is ever
581/// asked for. A size hint on a stream is a claim about data not yet read,
582/// trusted only up to the window the LEVEL would choose for it, and a read may
583/// find more than it said, so it keeps one block of slack; overriding the
584/// window is a claim of its own that only the data can confirm. An unknown size
585/// may fill any window, and is laid out for it up front, as upstream's buffered
586/// stream takes `windowSize + blockSize` of input buffer for an unknown pledge
587/// (`ZSTD_resetCCtx_internal`, `zstd_compress.c:2131-2139`). Room no input
588/// reaches is never written, so it is never resident: a one-byte stream through
589/// the CLI at levels 3, 19 and 22 peaks at the same RSS as a history grown by
590/// reading, and growing it instead measured no faster.
591/// The most history `backend` keeps for a `window`-byte window with
592/// `dict_len` bytes of dictionary and blocks of up to `block` bytes: the
593/// window, the slack it slides in, and the block being read.
594pub(crate) fn history_ceiling(
595    backend: super::strategy::BackendTag,
596    window: usize,
597    dict_len: usize,
598    block: usize,
599) -> usize {
600    use super::match_table::storage::MAX_PRIMED_WINDOW_SIZE;
601    // `MAX_PRIMED_WINDOW_SIZE` is `(u32::MAX - MAX_BLOCK_SIZE) / 2`, so the
602    // ceiling stays inside `usize` on a 32-bit target; a sum past `usize` is
603    // past it too.
604    let window = window
605        .checked_add(dict_len)
606        .map_or(MAX_PRIMED_WINDOW_SIZE, |w| w.min(MAX_PRIMED_WINDOW_SIZE));
607    let slack = match backend {
608        super::strategy::BackendTag::Simple => window,
609        _ => window >> 2,
610    };
611    window + slack + block
612}
613
614fn frame_history_bytes(
615    backend: super::strategy::BackendTag,
616    workspace: &crate::encoding::workspace::Workspace,
617    in_place: bool,
618    hint: Option<u64>,
619    dict_len: usize,
620    max_window_size: usize,
621    level: CompressionLevel,
622) -> usize {
623    use crate::encoding::workspace::IngestPlan;
624    let block = workspace.block_for_window(max_window_size);
625    let ceiling = history_ceiling(backend, max_window_size, dict_len, block);
626    // A size past `usize` is past the ceiling too.
627    let bytes = hint.map(|bytes| usize::try_from(bytes).unwrap_or(usize::MAX));
628    // The input and the room past it its last read may ask for.
629    let (input, read_slack) = match workspace.ingest() {
630        IngestPlan::Raw => return 0,
631        IngestPlan::Slice(_) if in_place => return dict_len.min(ceiling),
632        IngestPlan::Slice(len) | IngestPlan::PledgedStream(len) => (Some(len), 0),
633        IngestPlan::Stream => (
634            bytes.map(|bytes| {
635                let level_window_log =
636                    crate::encoding::levels::config::resolve_level_params(level, hint).window_log;
637                bytes.min(1usize << level_window_log)
638            }),
639            block,
640        ),
641    };
642    match input {
643        // A window stays below 2^31 and a block below 2^17, so
644        // `bytes + read_slack` fits; a dictionary large enough to overflow the
645        // rest is past the ceiling anyway.
646        Some(bytes) if bytes < max_window_size => dict_len
647            .checked_add(bytes + read_slack)
648            .map_or(ceiling, |sum| sum.min(ceiling)),
649        _ => ceiling,
650    }
651}
652
653impl MatchGeneratorDriver {
654    /// See [`MatcherStorage::ingest_capacity`].
655    #[cfg(test)]
656    pub(crate) fn ingest_capacity(&self) -> usize {
657        self.storage.ingest_capacity()
658    }
659
660    /// `(tables, history)` bytes the active window backend holds in
661    /// allocations of its own rather than in a context's workspace.
662    #[cfg(test)]
663    pub(crate) fn owned_table_and_history_bytes(&self) -> (usize, usize) {
664        match &self.storage {
665            MatcherStorage::Simple(_) => panic!("asked of a window backend only"),
666            MatcherStorage::Dfast(m) => (m.tables.owned_bytes(), m.history.owned_bytes()),
667            MatcherStorage::Row(m) => (m.tables.owned_bytes(), m.history.owned_bytes()),
668            MatcherStorage::HashChain(m) => {
669                (m.table.tables.owned_bytes(), m.table.history.owned_bytes())
670            }
671        }
672    }
673
674    /// Read `input` in and commit it as one block, the way the frame loop
675    /// does in two calls.
676    #[cfg(any(test, feature = "bench-internals"))]
677    pub(crate) fn commit_input(&mut self, input: &[u8]) {
678        self.fill_in_place(input.len(), &mut |history| {
679            history.extend_from_slice(input);
680            (input.len(), false)
681        });
682        self.commit_filled(input.len());
683    }
684
685    /// `slice_size` sets the chunk a dictionary is primed in.
686    /// `max_slices_in_window` determines the initial window capacity at construction
687    /// time. Effective window sizing is recalculated on every [`reset`](Self::reset)
688    /// from the resolved compression level and optional source-size hint.
689    pub(crate) fn new(slice_size: usize, max_slices_in_window: usize) -> Self {
690        // Validate inputs before deriving window_log_init. Three
691        // failure modes need explicit guards:
692        //
693        // 1. Zero args → `max_window_size = 0` → silent 1-byte
694        //    degenerate window (useless).
695        // 2. Multiplication overflow on `slice_size *
696        //    max_slices_in_window` → wraps silently in release.
697        // 3. `next_power_of_two` overflow when the product is
698        //    above `1 << (usize::BITS - 1)` → modern Rust PANICS
699        //    on overflow (older Rust returned 0).
700        //
701        // Catch all three at construction with a clear domain-
702        // specific message via `assert!` + `checked_mul` +
703        // `checked_next_power_of_two`, rather than letting either
704        // mode produce a silent degenerate matcher OR a generic
705        // panic deep in `FastKernelMatcher::with_params`.
706        assert!(
707            slice_size > 0,
708            "MatchGeneratorDriver::new requires slice_size > 0 (got 0)",
709        );
710        assert!(
711            max_slices_in_window > 0,
712            "MatchGeneratorDriver::new requires max_slices_in_window > 0 (got 0)",
713        );
714        let max_window_size = max_slices_in_window
715            .checked_mul(slice_size)
716            .expect("MatchGeneratorDriver::new: slice_size * max_slices_in_window overflows usize");
717        // Derive an effective window_log for the initial-state matcher.
718        // `MatchGeneratorDriver::new` runs BEFORE any reset, so it has
719        // no LevelParams to consult — we initialise to whatever
720        // window_log fits the caller's requested max_window_size
721        // (round up to the next power of two via `next_power_of_two`'s
722        // log). Reset() overwrites all three params from the resolved
723        // LevelParams.
724        //
725        // `checked_next_power_of_two` returns `None` if the next power
726        // of two would overflow `usize`. Modern Rust's
727        // `next_power_of_two` PANICS on overflow rather than returning
728        // 0 (the panic message is generic and unhelpful), so use the
729        // checked variant to surface the failure with a clear,
730        // domain-specific error.
731        let next_pow2 = max_window_size.checked_next_power_of_two().expect(
732            "MatchGeneratorDriver::new: max_window_size too large for \
733             next_power_of_two without overflow",
734        );
735        let window_log_init = next_pow2.trailing_zeros() as u8;
736        Self {
737            // Deferred table: `new` runs before any source size or resolved
738            // LevelParams exist, so allocating at the level-default hash_log
739            // here would be thrown away by the first frame's reset (which
740            // clamps the window to the input and reallocs at the resolved
741            // size). The deferral lets that first reset allocate exactly once.
742            storage: MatcherStorage::Simple(FastKernelMatcher::with_params_deferred(
743                window_log_init,
744                FAST_LEVEL_1_HASH_LOG,
745                FAST_LEVEL_1_MLS,
746                2, // upstream zstd default step_size (targetLength=0 → step=2)
747            )),
748            strategy_tag: super::strategy::StrategyTag::Fast,
749            search: super::strategy::SearchMethod::Fast,
750            parse: super::strategy::ParseMode::Greedy,
751            #[cfg(test)]
752            config_override: None,
753            param_overrides: None,
754            slice_size,
755            base_slice_size: slice_size,
756            // Report the ROUNDED-UP window size that the matcher
757            // actually carries (via `window_log_init = log2(next_pow2)`
758            // → matcher's `max_window_size = 1 << window_log_init =
759            // next_pow2`). For non-power-of-two `slice_size *
760            // max_slices_in_window` inputs, the unrounded value
761            // would under-report the active backend's window until
762            // the first `reset()` overwrites both sides from the
763            // resolved LevelParams.
764            reported_window_size: next_pow2,
765            reset_size_log: None,
766            reset_dict_attach_ok: true,
767            reset_shape: None,
768            dictionary_retained_budget: 0,
769            source_size_hint: None,
770            dictionary_size_hint: None,
771            borrowed_pending: None,
772            primed: None,
773            own_workspace: crate::encoding::workspace::Workspace::new(),
774            frame_in_place: false,
775            #[cfg(feature = "ldm")]
776            ldm: None,
777            #[cfg(feature = "ldm")]
778            ldm_store: Default::default(),
779            #[cfg(feature = "ldm")]
780            ldm_found: Vec::new(),
781            #[cfg(feature = "ldm")]
782            ldm_block: Vec::new(),
783            #[cfg(feature = "ldm")]
784            ldm_committed_end: 0,
785            #[cfg(feature = "ldm")]
786            ldm_dict_end: 0,
787            #[cfg(feature = "ldm")]
788            ldm_split_block: None,
789            producer: None,
790        }
791    }
792
793    /// Ask `producer` for each block's parse from the next block on, in place
794    /// of the backends; `None` removes it. With `fallback` a block it fails on
795    /// is matched by the backend instead; without, it goes out as literals
796    /// and [`Self::take_producer_failure`] reports it.
797    pub(crate) fn set_sequence_producer(
798        &mut self,
799        producer: Option<alloc::boxed::Box<dyn SequenceProducer>>,
800        fallback: bool,
801    ) {
802        // Callers install the producer before every frame: the parse buffer
803        // the last one grew stays for the next.
804        let parse = self
805            .producer
806            .take()
807            .map(|slot| slot.parse)
808            .unwrap_or_default();
809        self.producer = producer.map(|producer| ProducerSlot {
810            producer,
811            fallback,
812            failed: false,
813            parse,
814        });
815    }
816
817    /// Whether a block went out as literals because the producer failed on
818    /// it with no fallback, since this was last asked.
819    pub(crate) fn take_producer_failure(&mut self) -> bool {
820        self.producer
821            .as_mut()
822            .is_some_and(|slot| core::mem::take(&mut slot.failed))
823    }
824
825    /// Report the producer's parse of the last committed block, or of the
826    /// piece of one between long matches, indexing it as an unsearched one.
827    /// Returns `false`, having done nothing, when the producer failed and the
828    /// backend is to match it.
829    fn match_produced(&mut self, handle_sequence: &mut impl FnMut(Sequence)) -> bool {
830        let mut slot = self
831            .producer
832            .take()
833            .expect("called with a producer installed");
834        slot.parse.clear();
835        let window = self.window_size();
836        let block = self.get_last_space();
837        let block_len = block.len();
838        // The parse must cover the block exactly, with matches inside it (the
839        // producer sees no history, as upstream hands it none), and each match
840        // must copy the bytes it stands for: a parse of the right shape that
841        // misreads the block would otherwise write a frame decoding to other
842        // data.
843        let produced = slot
844            .producer
845            .produce(block, window, &mut slot.parse)
846            .is_ok()
847            && crate::encoding::sequences::validate_parse(
848                &slot.parse,
849                block_len,
850                window,
851                0,
852                true,
853                block_len,
854            )
855            .is_ok()
856            && crate::encoding::sequences::parse_reproduces(&slot.parse, block, &[]).is_ok();
857        if !produced && slot.fallback {
858            self.producer = Some(slot);
859            return false;
860        }
861        // The block's long matches, if any, were taken before it got here.
862        self.skip_backend(None);
863        if produced {
864            let mut literals = 0usize;
865            for sequence in &slot.parse {
866                literals += sequence.lit_len as usize;
867                if sequence.match_len == 0 {
868                    continue;
869                }
870                handle_sequence(Sequence::Triple {
871                    literal_len: literals,
872                    offset: sequence.offset as usize,
873                    match_len: sequence.match_len as usize,
874                });
875                literals = 0;
876            }
877            if literals > 0 {
878                handle_sequence(Sequence::Literals { len: literals });
879            }
880        } else {
881            slot.failed = true;
882            if block_len > 0 {
883                handle_sequence(Sequence::Literals { len: block_len });
884            }
885        }
886        self.producer = Some(slot);
887        true
888    }
889
890    fn level_params(level: CompressionLevel, source_size: Option<u64>) -> LevelParams {
891        resolve_level_params(level, source_size)
892    }
893
894    /// Install the public-parameter per-knob overrides (#27) applied at
895    /// the next [`Matcher::reset`]. `None` (or an all-`None` set) restores
896    /// plain level-based geometry. Persists across resets until changed.
897    pub(crate) fn set_param_overrides(
898        &mut self,
899        overrides: Option<super::parameters::ParamOverrides>,
900    ) {
901        self.param_overrides = overrides;
902    }
903
904    /// Active backend family derived from the storage variant. Single
905    /// source of truth — no separate runtime tag to drift against.
906    pub(crate) fn active_backend(&self) -> super::strategy::BackendTag {
907        self.storage.backend()
908    }
909
910    /// Whether the borrowed (no-copy, in-place over-window) scan is
911    /// implemented for the current backend + search configuration. The
912    /// HashChain backend serves both the lazy CHAIN parser
913    /// (`SearchMethod::HashChain`) and the BT/optimal parsers
914    /// (`SearchMethod::BinaryTree`); only the lazy chain has a borrowed scan
915    /// so far, so BT/optimal stay on the owned path.
916    pub(crate) fn borrowed_supported(&self) -> bool {
917        use super::strategy::{BackendTag, SearchMethod};
918        // A producer reads the committed block, which the in-place scan never
919        // commits.
920        if self.producer.is_some() {
921            return false;
922        }
923        match self.active_backend() {
924            BackendTag::Simple | BackendTag::Dfast | BackendTag::Row => true,
925            // The HashChain backend covers two searches: the lazy CHAIN parser
926            // (borrowed-capable) and the BINARY-TREE search (btlazy2 L13-15 +
927            // optimal BtOpt/BtUltra/BtUltra2 L16-22). btlazy2's BT-tree borrowed
928            // scan is byte-identical to owned (reads via live_history()), so it
929            // takes the in-place path. The OPTIMAL parsers stay owned: their
930            // cost-based DP is sensitive to candidate quality, and the borrowed
931            // continuous-index scan yields slightly different (ratio-worse)
932            // candidates than the owned evict+rehash scan — borrowed optimal
933            // both diverged from owned and fell outside the ffi ratio bound.
934            // Search-aware (not just strategy_tag) so optimal BT can never be
935            // staged on the borrowed path even via an internal caller.
936            BackendTag::HashChain => matches!(self.search, SearchMethod::HashChain),
937        }
938    }
939
940    /// Whether this frame, handed over as one slice with no dictionary, is
941    /// scanned in place. Settled by the reset, which laid the history out for
942    /// it, so the frame loop must take the path this names.
943    pub(crate) fn frame_scans_in_place(&self) -> bool {
944        self.frame_in_place
945    }
946
947    /// Whether a borrowed scan starting now sees nothing of an earlier frame,
948    /// so the frame is the one a fresh matcher writes. Asked once per frame,
949    /// after `reset`; [`Self::borrowed_supported`] stays the per-block
950    /// invariant. Only the Dfast kernel numbers a borrowed frame's input from
951    /// zero regardless of what its tables hold; the others advance their
952    /// floor past the previous frame, borrowed or not.
953    pub(crate) fn borrowed_frame_is_independent(&self) -> bool {
954        match &self.storage {
955            MatcherStorage::Dfast(d) => !d.tables_hold_earlier_frames,
956            _ => true,
957        }
958    }
959
960    /// Make [`Self::borrowed_frame_is_independent`] hold by emptying the
961    /// tables of earlier frames.
962    pub(crate) fn forget_earlier_frames(&mut self) {
963        if let MatcherStorage::Dfast(d) = &mut self.storage {
964            d.forget_earlier_frames();
965        }
966    }
967
968    /// Whether a DICTIONARY frame can take the borrowed (no input copy) path.
969    /// Only the Simple (Fast) backend with the dictionary ATTACHED (not the
970    /// copy/merge regime) has a borrowed dict scan — `start_matching_borrowed_dict`
971    /// reads live matches from the borrowed input in place and dict matches
972    /// from the committed dict prefix via the 2-segment counter. Every other
973    /// backend, and copy-mode (large-input) dict frames, stay on the owned
974    /// path. Checked AFTER priming, so `is_attached()` reflects the resolved
975    /// attach-vs-copy decision.
976    pub(crate) fn borrowed_dict_supported(&self) -> bool {
977        // As in `borrowed_supported`: a producer reads the committed block.
978        self.producer.is_none()
979            && matches!(
980                &self.storage,
981                MatcherStorage::Simple(m) if m.dict_is_attached()
982            )
983    }
984
985    fn simple_mut(&mut self) -> &mut FastKernelMatcher {
986        match &mut self.storage {
987            MatcherStorage::Simple(m) => m,
988            _ => panic!("simple backend must be initialized by reset() before use"),
989        }
990    }
991
992    /// Register a caller-owned input buffer as the Simple backend's
993    /// borrowed one-shot match window. Only valid on the Simple (Fast)
994    /// backend; the one-shot frame path gates on that before calling.
995    ///
996    /// # Safety
997    /// Same contract as [`FastKernelMatcher::set_borrowed_window`]: the
998    /// buffer must stay live and unmodified until the window is cleared,
999    /// and must be cleared before the buffer is dropped or the matcher is
1000    /// reused for another frame.
1001    pub(crate) unsafe fn set_borrowed_window(&mut self, buffer: &[u8]) {
1002        // SAFETY: forwarded contract — caller upholds liveness/clear.
1003        match self.active_backend() {
1004            super::strategy::BackendTag::Simple => unsafe {
1005                self.simple_mut().set_borrowed_window(buffer)
1006            },
1007            super::strategy::BackendTag::Dfast => unsafe {
1008                self.dfast_matcher_mut().set_borrowed_window(buffer)
1009            },
1010            super::strategy::BackendTag::Row => unsafe {
1011                self.row_matcher_mut().set_borrowed_window(buffer)
1012            },
1013            super::strategy::BackendTag::HashChain => unsafe {
1014                self.hc_matcher_mut().set_borrowed_window(buffer)
1015            },
1016        }
1017    }
1018
1019    /// Clear the borrowed one-shot window, returning the active backend
1020    /// to the owned `history` path.
1021    pub(crate) fn clear_borrowed_window(&mut self) {
1022        match self.active_backend() {
1023            super::strategy::BackendTag::Simple => self.simple_mut().clear_borrowed_window(),
1024            super::strategy::BackendTag::Dfast => self.dfast_matcher_mut().clear_borrowed_window(),
1025            super::strategy::BackendTag::Row => self.row_matcher_mut().clear_borrowed_window(),
1026            super::strategy::BackendTag::HashChain => self.hc_matcher_mut().clear_borrowed_window(),
1027            #[allow(unreachable_patterns)]
1028            _ => {}
1029        }
1030        self.borrowed_pending = None;
1031    }
1032
1033    /// Stage the borrowed block range `[block_start, block_end)` for the
1034    /// NEXT `start_matching` / `skip_matching_with_hint`, which the
1035    /// borrowed frame path uses in place of `commit_filled`. While
1036    /// staged, those trait calls route to the Simple backend's borrowed
1037    /// scan/skip (consuming the stage) instead of the owned committed
1038    /// block. See [`Matcher::start_matching`] /
1039    /// [`Matcher::skip_matching_with_hint`] on this type.
1040    pub(crate) fn set_borrowed_block(&mut self, block_start: usize, block_end: usize) {
1041        assert!(
1042            self.borrowed_supported(),
1043            "borrowed block staging is not supported for the active backend/search config",
1044        );
1045        assert!(
1046            block_start <= block_end,
1047            "borrowed block range must satisfy start <= end (start={block_start} end={block_end})",
1048        );
1049        self.borrowed_pending = Some((block_start, block_end));
1050        // Make the range visible to `get_last_space()` immediately: the
1051        // emit pipeline reads `get_last_space().len()` in
1052        // `collect_block_parts` BEFORE `start_matching` consumes the
1053        // stage, so the staged block (not the whole borrowed window) must
1054        // be reported now to keep the literal-buffer reservation right.
1055        match self.active_backend() {
1056            super::strategy::BackendTag::Simple => self
1057                .simple_mut()
1058                .stage_borrowed_block(block_start, block_end),
1059            super::strategy::BackendTag::Dfast => self
1060                .dfast_matcher_mut()
1061                .stage_borrowed_block(block_start, block_end),
1062            super::strategy::BackendTag::Row => self
1063                .row_matcher_mut()
1064                .stage_borrowed_block(block_start, block_end),
1065            super::strategy::BackendTag::HashChain => self
1066                .hc_matcher_mut()
1067                .table
1068                .stage_borrowed_block(block_start, block_end),
1069        }
1070    }
1071
1072    #[cfg(test)]
1073    fn dfast_matcher(&self) -> &DfastMatchGenerator {
1074        match &self.storage {
1075            MatcherStorage::Dfast(m) => m,
1076            _ => panic!("dfast backend must be initialized by reset() before use"),
1077        }
1078    }
1079
1080    fn dfast_matcher_mut(&mut self) -> &mut DfastMatchGenerator {
1081        match &mut self.storage {
1082            MatcherStorage::Dfast(m) => m,
1083            _ => panic!("dfast backend must be initialized by reset() before use"),
1084        }
1085    }
1086
1087    #[cfg(test)]
1088    pub(crate) fn row_matcher(&self) -> &RowMatchGenerator {
1089        match &self.storage {
1090            MatcherStorage::Row(m) => m,
1091            _ => panic!("row backend must be initialized by reset() before use"),
1092        }
1093    }
1094
1095    pub(crate) fn row_matcher_mut(&mut self) -> &mut RowMatchGenerator {
1096        match &mut self.storage {
1097            MatcherStorage::Row(m) => m,
1098            _ => panic!("row backend must be initialized by reset() before use"),
1099        }
1100    }
1101
1102    #[cfg(test)]
1103    fn hc_matcher(&self) -> &HcMatchGenerator {
1104        match &self.storage {
1105            MatcherStorage::HashChain(m) => m,
1106            _ => panic!("hash chain backend must be initialized by reset() before use"),
1107        }
1108    }
1109
1110    fn hc_matcher_mut(&mut self) -> &mut HcMatchGenerator {
1111        match &mut self.storage {
1112            MatcherStorage::HashChain(m) => m,
1113            _ => panic!("hash chain backend must be initialized by reset() before use"),
1114        }
1115    }
1116
1117    /// Shrink the active backend's `max_window_size` by the bytes
1118    /// reclaimed from the dictionary-retention budget. Returns `true`
1119    /// iff any reclamation happened — the caller uses that as the
1120    /// gate for [`Self::trim_after_budget_retire`] (which is a no-op
1121    /// otherwise: with `max_window_size` unchanged the backend's
1122    /// `trim_to_window` cannot find anything to evict, so calling it
1123    /// just runs an extra `match` ladder + a single early-out check
1124    /// per slice commit).
1125    #[must_use]
1126    fn retire_dictionary_budget(&mut self, evicted_bytes: usize) -> bool {
1127        let reclaimed = evicted_bytes.min(self.dictionary_retained_budget);
1128        if reclaimed == 0 {
1129            return false;
1130        }
1131        self.dictionary_retained_budget -= reclaimed;
1132        match self.active_backend() {
1133            super::strategy::BackendTag::Simple => {
1134                let matcher = self.simple_mut();
1135                // `reclaimed` can exceed the CURRENT `max_window_size`: the
1136                // retained dict budget is tracked independently and the
1137                // window may already have been shrunk by a prior eviction,
1138                // so the floor at 0 is the correct clamp, not a masked bug.
1139                matcher.max_window_size = matcher.max_window_size.saturating_sub(reclaimed);
1140            }
1141            super::strategy::BackendTag::Dfast => {
1142                let matcher = self.dfast_matcher_mut();
1143                // `reclaimed` can exceed the CURRENT `max_window_size`: the
1144                // retained dict budget is tracked independently and the
1145                // window may already have been shrunk by a prior eviction,
1146                // so the floor at 0 is the correct clamp, not a masked bug.
1147                matcher.max_window_size = matcher.max_window_size.saturating_sub(reclaimed);
1148            }
1149            super::strategy::BackendTag::Row => {
1150                let matcher = self.row_matcher_mut();
1151                // `reclaimed` can exceed the CURRENT `max_window_size`: the
1152                // retained dict budget is tracked independently and the
1153                // window may already have been shrunk by a prior eviction,
1154                // so the floor at 0 is the correct clamp, not a masked bug.
1155                matcher.max_window_size = matcher.max_window_size.saturating_sub(reclaimed);
1156            }
1157            super::strategy::BackendTag::HashChain => {
1158                let matcher = self.hc_matcher_mut();
1159                // See the Simple arm: `reclaimed` may exceed the current
1160                // window, so saturating to 0 is the correct clamp.
1161                matcher.table.max_window_size =
1162                    matcher.table.max_window_size.saturating_sub(reclaimed);
1163            }
1164        }
1165        true
1166    }
1167
1168    fn trim_after_budget_retire(&mut self) {
1169        loop {
1170            let mut evicted_bytes = 0usize;
1171            match self.active_backend() {
1172                // The Fast backend drains the oldest bytes in place and
1173                // reports the count; the window backends evict whole blocks,
1174                // so their count is the `window_size` delta.
1175                super::strategy::BackendTag::Simple => {
1176                    let MatcherStorage::Simple(m) = &mut self.storage else {
1177                        unreachable!("active_backend() == Simple proven above");
1178                    };
1179                    evicted_bytes += m.trim_to_window();
1180                }
1181                super::strategy::BackendTag::Dfast => {
1182                    let dfast = self.dfast_matcher_mut();
1183                    let pre = dfast.window_size;
1184                    dfast.trim_to_window();
1185                    evicted_bytes += pre - dfast.window_size;
1186                }
1187                super::strategy::BackendTag::Row => {
1188                    let row = self.row_matcher_mut();
1189                    let pre = row.window_size;
1190                    row.trim_to_window();
1191                    evicted_bytes += pre - row.window_size;
1192                }
1193                super::strategy::BackendTag::HashChain => {
1194                    let table = &mut self.hc_matcher_mut().table;
1195                    let pre = table.window_size;
1196                    table.trim_to_window();
1197                    evicted_bytes += pre - table.window_size;
1198                }
1199            }
1200            if evicted_bytes == 0 {
1201                break;
1202            }
1203            // The loop's invariant is "the backend's previous
1204            // `max_window_size` shrink had downstream bytes left to
1205            // evict" — that's what `evicted_bytes != 0` proves at
1206            // this point. `dictionary_retained_budget` is NOT
1207            // guaranteed to be positive here: the outer
1208            // `retire_dictionary_budget` call may have already
1209            // drained it to zero by reclaiming the last retained
1210            // bytes, while the backend still has bytes above the
1211            // freshly-shrunk window cap waiting for this loop to
1212            // evict. The return value of the retire call below is
1213            // therefore intentionally discarded — the loop's
1214            // termination is driven by `evicted_bytes == 0`, not by
1215            // whether the budget has more bytes left to reclaim.
1216            let _ = self.retire_dictionary_budget(evicted_bytes);
1217        }
1218    }
1219
1220    /// ATTACH (`true`) vs COPY (`false`) decision for the dms-bearing HashChain
1221    /// backend (lazy hash-chain AND binary-tree/optimal levels), mirroring
1222    /// upstream `ZSTD_shouldAttachDict` and its per-strategy `attachDictSizeCutoffs`:
1223    /// a small / unknown source ATTACHES the dict as a separate dms (hash-chain
1224    /// dms for lazy, DUBT dms for BT); a large known source COPIES it into the
1225    /// live chain / tree. The cutoff is the lazy/lazy2 value for HC, the
1226    /// btlazy2/btopt value for Bt{Opt}, and the smaller btultra/btultra2 value for
1227    /// the deepest parses. Both `skip_matching_for_dictionary_priming` (which
1228    /// stages the dict) and `prime_with_dictionary` (which builds-or-drops the
1229    /// dms) read this so the two stay in lock-step.
1230    fn hc_dict_attach_mode(&self) -> bool {
1231        // Only the HashChain backend (lazy hash-chain + BT/optimal) routes here;
1232        // a non-HashChain storage has no dms decision, so default to attach.
1233        let MatcherStorage::HashChain(hc) = &self.storage else {
1234            return true;
1235        };
1236        hc_attaches_dictionary(hc, self.reset_size_log)
1237    }
1238
1239    fn skip_matching_for_dictionary_priming(&mut self, dict_len: usize) {
1240        match self.active_backend() {
1241            super::strategy::BackendTag::Simple => {
1242                // Upstream zstd `ZSTD_shouldAttachDict` mode selection for the Fast
1243                // strategy (cutoff 8 KB): small / unknown-size inputs ATTACH
1244                // (index dict positions into a SEPARATE immutable table; the
1245                // dual-probe 2-cursor `compress_block_fast_dict` then prefers
1246                // recent-input matches and falls back to the dict — the path
1247                // that wins small/unknown). Large known-size inputs COPY (prime
1248                // dict into the live table; the 4-cursor `compress_block_fast`
1249                // matches against it as window history — the path that already
1250                // matches/beats the upstream zstd on large corpora). The dispatch in
1251                // `start_matching` keys off `dict_table.is_some()`, which only
1252                // the attach path populates. See [`FAST_ATTACH_DICT_CUTOFF_LOG`].
1253                let attach = self.reset_dict_attach_ok
1254                    && self
1255                        .reset_size_log
1256                        .is_none_or(|log| log <= FAST_ATTACH_DICT_CUTOFF_LOG);
1257                if attach {
1258                    self.simple_mut().skip_matching_for_dict_prime(dict_len);
1259                } else {
1260                    self.simple_mut().skip_matching_for_dict_copy();
1261                }
1262            }
1263            super::strategy::BackendTag::Dfast => {
1264                // Upstream zstd `ZSTD_dictMatchState` mode selection for dfast (cutoff
1265                // 16 KiB): small / unknown-size inputs ATTACH (build the
1266                // separate immutable dict long+short tables; the dual-probe
1267                // `start_matching_fast_loop` searches live + dict, the path that
1268                // avoids the per-frame dict re-prime that dominates small
1269                // `compress-dict`). Larger known-size inputs COPY (re-prime the
1270                // dict into the live tables via `skip_matching_dense`, where the
1271                // dense scan matches it as window history). `skip_matching_for_dict_attach`
1272                // self-gates on `use_fast_loop` (only fast-loop levels carry the
1273                // dual-probe; general-path levels fall back to the dense copy).
1274                // The tagged dictionary slots index at most
1275                // `DFAST_ATTACH_DICT_MAX_LEN` bytes; a larger dictionary is
1276                // copied into the live tables instead.
1277                let attach = dict_len <= crate::encoding::dfast::DFAST_ATTACH_DICT_MAX_LEN
1278                    && self
1279                        .reset_size_log
1280                        .is_none_or(|log| log <= DFAST_ATTACH_DICT_CUTOFF_LOG);
1281                if attach {
1282                    self.dfast_matcher_mut().skip_matching_for_dict_attach();
1283                } else {
1284                    self.dfast_matcher_mut().invalidate_dict_cache();
1285                    self.dfast_matcher_mut().skip_matching_dense();
1286                }
1287            }
1288            super::strategy::BackendTag::Row => {
1289                // Upstream zstd `ZSTD_RowFindBestMatch` `dictMatchState`: small /
1290                // unknown-size inputs ATTACH (build the separate immutable dict
1291                // row index; the bounded dual-probe in `row_candidate_rl`
1292                // searches live + dict, avoiding the per-frame dict re-index),
1293                // larger known-size inputs COPY (dense re-prime into the live
1294                // rows).
1295                // The attach / copy decision was made with the CDict's cParams
1296                // at `reset` (`RowDictPlan`); the backend indexes the dictionary
1297                // block accordingly.
1298                self.row_matcher_mut().prime_dictionary_current_block();
1299            }
1300            super::strategy::BackendTag::HashChain => {
1301                // Lazy-HC AND BT/optimal both follow upstream zstd `ZSTD_shouldAttachDict`
1302                // per-strategy: ATTACH (a separate dms — hash-chain dms for lazy,
1303                // DUBT dms for BT) for small / unknown inputs, COPY (merge the dict
1304                // into the live chain/tree) for large known inputs. ATTACH keeps
1305                // the dict in history but out of the live structure via
1306                // `skip_matching_dict_bt` (the cursor advance is shared by both
1307                // arms); COPY routes through the normal `skip_matching` (its
1308                // `uses_bt` branch fills the live tree, the lazy branch the live
1309                // chain). The dms is built-or-dropped to match in
1310                // `prime_with_dictionary`.
1311                if self.hc_dict_attach_mode() {
1312                    self.hc_matcher_mut().table.skip_matching_dict_bt();
1313                } else {
1314                    self.hc_matcher_mut().skip_matching(Some(false));
1315                }
1316            }
1317        }
1318    }
1319}
1320
1321impl Matcher for MatchGeneratorDriver {
1322    fn supports_dictionary_priming(&self) -> bool {
1323        true
1324    }
1325
1326    fn set_source_size_hint(&mut self, size: u64) {
1327        self.source_size_hint = Some(size);
1328    }
1329
1330    fn set_dictionary_size_hint(&mut self, sizes: super::DictionarySizes) {
1331        self.dictionary_size_hint = Some(sizes);
1332    }
1333
1334    /// Dict-relevance gate for the raw-fast-path. Reached only when a dictionary
1335    /// is active (the caller short-circuits on `dict_active`), so this answers
1336    /// "could the dict compress this otherwise-incompressible-looking block?".
1337    /// The Simple (Fast) backend samples its dict table precisely
1338    /// (`FastKernelMatcher::block_samples_match_dict`); the other backends
1339    /// (Dfast / Row / HashChain / BT) have their own dict structures and no cheap
1340    /// probe here, so they answer CONSERVATIVELY `true`: without a probe they
1341    /// cannot tell whether the dict compresses an incompressible-LOOKING block,
1342    /// and answering `false` would let the raw-fast-path emit such a block raw
1343    /// and miss an embedded dict segment. `dictionary_segment_in_incompressible_input_is_matched`
1344    /// pins this for Dfast/Row/BT — the 512-byte dict run inside high-entropy
1345    /// filler is matched only because these backends stay on the scan. So they
1346    /// keep the blanket scan the old `!dict_active` gate gave them; only the
1347    /// Simple/Fast backend trades it for the precise probe.
1348    fn block_samples_match_dict(&self, block: &[u8]) -> bool {
1349        match &self.storage {
1350            MatcherStorage::Simple(m) => m.block_samples_match_dict(block),
1351            _ => true,
1352        }
1353    }
1354
1355    /// Heap bytes this driver owns: the active backend's tables/history, the
1356    /// primed-dictionary snapshot (a cloned backend kept for CDict-equivalent
1357    /// reuse), and the workspace its tables live in when it is reset on its
1358    /// own. Tables laid out in a compression context's workspace are the
1359    /// context's to count. The inline struct itself is accounted by the
1360    /// owner's `size_of`.
1361    fn heap_size(&self) -> usize {
1362        let snapshot = self
1363            .primed
1364            .as_ref()
1365            .map_or(0, |(storage, _, _)| storage.heap_size());
1366        #[cfg(feature = "ldm")]
1367        let ldm = self.ldm.as_ref().map_or(0, |p| p.heap_size())
1368            + self.ldm_store.heap_size()
1369            + (self.ldm_found.capacity() + self.ldm_block.capacity())
1370                * core::mem::size_of::<super::opt::ldm::HcRawSeq>();
1371        #[cfg(not(feature = "ldm"))]
1372        let ldm = 0;
1373        // The producer's parse buffer is kept from block to block.
1374        let produced = self.producer.as_ref().map_or(0, |slot| {
1375            slot.parse.capacity() * core::mem::size_of::<ExternalSequence>()
1376        });
1377        self.storage.heap_size() + snapshot + self.own_workspace.heap_bytes() + ldm + produced
1378    }
1379
1380    fn clear_param_overrides(&mut self) {
1381        self.param_overrides = None;
1382    }
1383
1384    fn reset(&mut self, level: CompressionLevel) {
1385        // On its own the driver lays its tables out in a workspace of its own,
1386        // which is moved out for the call and back: moving it leaves the
1387        // allocation, and so every region carved from it, where it is.
1388        // Driven directly, it takes blocks of any size up to the format's, so
1389        // that is the block its history leaves room for; it carves no block
1390        // buffers of its own.
1391        let mut own = core::mem::take(&mut self.own_workspace);
1392        own.begin_layout(
1393            crate::common::MAX_BLOCK_SIZE as usize,
1394            crate::encoding::workspace::no_trailing,
1395            crate::encoding::workspace::IngestPlan::Stream,
1396        );
1397        self.reset_in_workspace(level, &mut own);
1398        own.release_retired();
1399        self.own_workspace = own;
1400    }
1401
1402    fn reset_in_workspace(
1403        &mut self,
1404        level: CompressionLevel,
1405        workspace: &mut crate::encoding::workspace::Workspace,
1406    ) {
1407        let hint = self.source_size_hint.take();
1408        // An empty dictionary is "no dictionary": it primes nothing, so every
1409        // dictionary-frame decision below must see `None` for it.
1410        let dict_hint = self
1411            .dictionary_size_hint
1412            .take()
1413            .filter(|sizes| sizes.content > 0);
1414        // Snapshot the hint's normalized ceil-log bucket for the primed-snapshot
1415        // key and prime_with_dictionary's attach/copy mode decision (the hint is
1416        // consumed here, but priming happens just after reset). Storing the
1417        // bucket rather than the raw bytes means two hints that resolve to the
1418        // same matcher shape share one snapshot instead of each re-priming.
1419        self.reset_size_log = hint.map(source_size_ceil_log);
1420        // A dictionary too large for the tagged attach position field falls back
1421        // to copy mode. Captured here (from the load-set size hint = actual dict
1422        // length) so the prime decision and the snapshot-key / epoch bits agree.
1423        self.reset_dict_attach_ok =
1424            dict_hint.is_none_or(|sizes| sizes.content <= MAX_FAST_ATTACH_DICT_REGION);
1425        let hinted = hint.is_some();
1426        // A dictionary frame takes its cParams and match-finder from the
1427        // CDict's cParams (upstream `ZSTD_resetCCtx_usingCDict`), whose tier
1428        // is keyed by the serialized dictionary size; a lazy-band CDict also
1429        // carries `dict_plan` to the Row backend. The dictionary is prepared
1430        // under the caller's parameters, so they are part of those cParams.
1431        let overrides = self.param_overrides.unwrap_or_default();
1432        // A dictionary frame runs the shape the dictionary was prepared with,
1433        // whatever the source size. Upstream stops doing that once the source
1434        // outgrows the dictionary and resolves the frame's own instead
1435        // (zstd_compress.c:5264). That alternative was implemented and measured
1436        // against this one and against the reference, in one session with the
1437        // arms interleaved, over five levels and two dictionary shapes, and it
1438        // loses on every row where the two differ. Time per frame over a 1 MiB
1439        // source, ours / the alternative / the reference:
1440        //
1441        //   3 KB dictionary        110 KB dictionary
1442        //   L1  1.13 / 1.13 / 0.73  1.14 / 1.15 / 0.79 ms
1443        //   L3  0.24 / 0.30 / 0.27  0.26 / 0.32 / 0.42 ms
1444        //   L5  0.52 / 2.69 / 0.38  0.43 / 2.77 / 1.16 ms
1445        //   L9  0.50 / 5.36 / 1.37  0.71 / 5.58 / 2.28 ms
1446        //   L12 0.87 / 5.56 / 1.87  1.60 / 5.58 / 3.08 ms
1447        //
1448        // Three to eight times the time for bytes that match to within a
1449        // percent, and under the larger dictionary this shape also beats the
1450        // reference on BOTH axes from L3 up (at L9, 0.71 ms and 243 bytes
1451        // against 2.28 ms and 1241), which the alternative would have given
1452        // away. A control arm at 64 KiB, under the size where the alternative
1453        // can run at all, stayed within 2.4% with identical output.
1454        //
1455        // The bench host is a VMware guest with no PMU passthrough, so cycles
1456        // and instructions are unavailable there (`perf stat -e cycles` reports
1457        // the event as unsupported); the figures above are `task-clock`, and at
1458        // this margin an instruction count would not be what decides it.
1459        //
1460        // See `dictionary_describes_frame`, which the C ABI still reads for a
1461        // question the codec cannot answer.
1462        let (params, dict_plan) = match dict_hint {
1463            Some(sizes) => crate::encoding::levels::config::resolve_level_params_with_dict(
1464                level, hint, sizes, &overrides,
1465            ),
1466            None => (Self::level_params(level, hint), None),
1467        };
1468        #[cfg_attr(not(test), allow(unused_mut))]
1469        let mut params = params;
1470        // Test-only: apply a parse×search override so the matrix can be
1471        // exercised without editing `LEVEL_TABLE`. Mutating `params` here
1472        // (before `next_backend`) flows the override through storage
1473        // selection, `configure`, and the `self.search`/`self.parse`
1474        // writes uniformly. Consumed with `take()` so it is one-shot: the
1475        // synthetic pairing applies to exactly this `reset()`, and a later
1476        // reset on the same driver falls back to the level's real config.
1477        #[cfg(test)]
1478        if let Some((search, parse)) = self.config_override.take() {
1479            params.search = search;
1480            params.lazy_depth = parse.lazy_depth();
1481            // The matrix sweep can pair a level with a backend its native
1482            // row doesn't populate (e.g. greedy L5, which carries only `row`,
1483            // run on HashChain). Synthesize a default config for the
1484            // overridden backend so its `configure` arm has something to read.
1485            use super::strategy::SearchMethod;
1486            match search {
1487                SearchMethod::Fast => {
1488                    params.fast.get_or_insert(FAST_L1);
1489                }
1490                SearchMethod::DoubleFast => {
1491                    params.dfast.get_or_insert(DFAST_L3);
1492                }
1493                SearchMethod::RowHash | SearchMethod::BinaryTreeLazy => {
1494                    let row = params.row.get_or_insert(ROW_CONFIG);
1495                    row.bt = matches!(search, SearchMethod::BinaryTreeLazy);
1496                }
1497                SearchMethod::HashChain | SearchMethod::BinaryTree => {
1498                    params.hc.get_or_insert(HC_CONFIG);
1499                }
1500            }
1501        }
1502        // Public-parameter overrides (#27): apply the per-knob set on top
1503        // of the level-resolved params. A strategy override re-routes the
1504        // backend, so this must precede `next_backend` selection. The
1505        // all-`None` case is skipped so default level geometry stays
1506        // byte-identical to plain level-based compression. Shared with the
1507        // workspace estimate, which has to build what this builds.
1508        if let Some(ov) = self.param_overrides {
1509            crate::encoding::levels::config::apply_frame_overrides(
1510                &mut params,
1511                &ov,
1512                dict_hint.is_some(),
1513                hint,
1514            );
1515        }
1516        // The long-distance matcher's parameters for the frame, `None` without one.
1517        #[cfg(feature = "ldm")]
1518        let frame_ldm = self
1519            .param_overrides
1520            .and_then(|ov| ov.ldm)
1521            .map(|ldm_ov| crate::encoding::levels::config::frame_ldm_params(&params, &ldm_ov));
1522        // A dictionary frame's hash-chain / binary-tree widths are the CDict's
1523        // (`resolve_level_params_with_dict`): verbatim when the dictionary is
1524        // copied into the live tables (`ZSTD_resetCCtx_byCopyingCDict` builds
1525        // the context from the CDict's cParams), re-adjusted to the source
1526        // alone when it is attached (`byAttachingCDict`: the live tables hold
1527        // no dictionary entry, the dms carries its own dict-sized tables), so
1528        // a small source under a large attached dictionary keeps small live
1529        // tables. Nothing re-sizes `params.hc` here.
1530        // Upstream `ZSTD_resolveRowMatchFinderMode` (zstd_compress.c:238): the
1531        // greedy/lazy/lazy2 band searches rows only above a 2^14 window and a
1532        // hash chain otherwise. The Row backend runs that switch itself
1533        // (`RowMatchGenerator::use_chain`), so both finders share ONE parse
1534        // (upstream's single `lazy_generic`) and the level stays on `RowHash`.
1535        let next_backend = params.backend();
1536        let max_window_size = 1usize << params.window_log;
1537        self.dictionary_retained_budget = 0;
1538        // Drop any frame-local borrowed staging so it can't leak across a
1539        // reset and misroute the next start/skip into borrowed dispatch.
1540        self.borrowed_pending = None;
1541        if self.active_backend() != next_backend {
1542            // Drain the outgoing backend's allocations into the shared
1543            // pool. The `match &mut self.storage { ... }` block runs to
1544            // completion before the assignment below replaces the
1545            // variant, so the inner state we just drained is dropped
1546            // with the old variant.
1547            match &mut self.storage {
1548                MatcherStorage::Simple(_m) => {
1549                    // FastKernelMatcher owns a flat Vec<u8> history
1550                    // and a Vec<u32> hash table — both drop with the
1551                    // variant assignment below, no per-block buffers
1552                    // to recycle into the driver pools. The
1553                    // assignment-replace path collapses to a noop
1554                    // pre-pass for this backend.
1555                }
1556                MatcherStorage::Dfast(m) => {
1557                    // Drop the long / short hash table allocations
1558                    // before calling `m.reset`. Without this prepass,
1559                    // `DfastMatchGenerator::reset` would `fill` both
1560                    // tables with `DFAST_EMPTY_SLOT` sentinels — wasted
1561                    // work given the next assignment to `self.storage`
1562                    // is about to drop `m` entirely. `reset` itself
1563                    // short-circuits on `if !self.tables.is_empty()`, so
1564                    // handing it an empty `Vec` skips the fill loop.
1565                    // Mirrors the pre-drain pattern in the HashChain
1566                    // arm below (and serves the same peak-memory
1567                    // purpose: release the table-allocation footprint
1568                    // before constructing the replacement variant).
1569                    m.tables = crate::encoding::workspace::Table::empty();
1570                    m.reset();
1571                }
1572                MatcherStorage::Row(m) => {
1573                    // One buffer holds the positions and, in its byte tail,
1574                    // the cursors and tags — releasing it releases all three.
1575                    m.release_tables();
1576                    m.reset();
1577                }
1578                MatcherStorage::HashChain(m) => {
1579                    // Release oversized tables when switching away from
1580                    // HashChain so Best's larger allocations don't persist.
1581                    // hash3_table must be released alongside the other
1582                    // two: BtUltra2's `1 << HC3_HASH_LOG` entries would
1583                    // otherwise stay pinned across the backend switch,
1584                    // even though no future caller of this backend will
1585                    // touch them.
1586                    m.table.tables = crate::encoding::workspace::Table::empty();
1587                    m.table.chain_off = 0;
1588                    m.table.hash3_off = 0;
1589                    m.reset();
1590                }
1591            }
1592            // Swap in a fresh variant for the new backend. The previous
1593            // `storage` is dropped here.
1594            self.storage = match next_backend {
1595                super::strategy::BackendTag::Simple => {
1596                    // Per-level Fast cParams from resolve_level_params:
1597                    // Level(1) gets (hash_log=14, mls=7); Level(-7..=-1)
1598                    // get upstream zstd row-0 (hash_log=13, mls=7); Fastest /
1599                    // Uncompressed keep (hash_log=14, mls=6). See
1600                    // resolve_level_params for rationale.
1601                    let fast = params.fast.expect("Fast level row carries a FastConfig");
1602                    // Deferred: the reset below lays the table out in the
1603                    // workspace at the frame's resolved width.
1604                    MatcherStorage::Simple(FastKernelMatcher::with_params_deferred(
1605                        params.window_log,
1606                        fast.hash_log,
1607                        fast.mls,
1608                        fast.step_size,
1609                    ))
1610                }
1611                super::strategy::BackendTag::Dfast => {
1612                    MatcherStorage::Dfast(DfastMatchGenerator::new(max_window_size))
1613                }
1614                super::strategy::BackendTag::Row => {
1615                    MatcherStorage::Row(RowMatchGenerator::new(max_window_size))
1616                }
1617                super::strategy::BackendTag::HashChain => {
1618                    MatcherStorage::HashChain(HcMatchGenerator::new(max_window_size))
1619                }
1620            };
1621        }
1622
1623        // Single source of truth: `LevelParams::strategy_tag` is the
1624        // authoritative mapping from `CompressionLevel` to strategy.
1625        // `storage.backend()` derives the parse family from the variant,
1626        // so there is no separate runtime tag that could drift against
1627        // `LEVEL_TABLE`.
1628        self.strategy_tag = params.strategy_tag;
1629        self.search = params.search;
1630        self.parse = params.parse();
1631        self.slice_size = self.base_slice_size.min(max_window_size);
1632        self.reported_window_size = max_window_size;
1633        let strategy_tag = self.strategy_tag;
1634        // Source-proportional table window for the backends whose hash-table
1635        // widths are recomputed here (Dfast / Row). Like the HC / Fast caps
1636        // in `adjust_params_for_source_size`, this sizes the internal tables
1637        // from the RAW source log (not the wire `window_log` floor) so a
1638        // small frame zeroes a small table; it never exceeds the real window.
1639        let table_window_size = match hint {
1640            Some(h) => {
1641                let raw_log = source_size_ceil_log(h);
1642                // Clamp the shift below the pointer width before `1usize <<`:
1643                // an oversized hint (>= 2^63 + 1, and on 32-bit usize any hint
1644                // >= 2^32) drives `raw_log` to 64 / >= 32, and the shift would
1645                // overflow (panic in debug, wrap to 0 in release) before the
1646                // `.min(max_window_size)` cap below could bound it. The min cap
1647                // still provides the real semantic window bound.
1648                let shift = raw_log.max(MIN_WINDOW_LOG).min(usize::BITS as u8 - 1);
1649                (1usize << shift).min(max_window_size)
1650            }
1651            None => max_window_size,
1652        };
1653        // The Fast backend's dictionary mode, which `prime_with_dictionary`
1654        // takes from the same `reset_size_log`: attached, the dictionary goes
1655        // into a table of its own and the input is scanned in place.
1656        let fast_attach = matches!(next_backend, super::strategy::BackendTag::Simple)
1657            && self.reset_dict_attach_ok
1658            && self
1659                .reset_size_log
1660                .is_none_or(|log| log <= FAST_ATTACH_DICT_CUTOFF_LOG);
1661        // A slice on a backend that can scan it in place never enters the
1662        // history, with no dictionary or one the Fast backend attaches; the frame
1663        // loop reads this decision back (`frame_scans_in_place`) instead of
1664        // taking it again. The kernels keep positions in `u32`, counting a
1665        // dictionary ahead of the slice, so a longer one is copied.
1666        let dict_len = dict_hint.map_or(0, |sizes| sizes.content);
1667        // The long-distance matcher splits a block into pieces through the
1668        // owned history, so its frames keep the input there.
1669        #[cfg(feature = "ldm")]
1670        let long_distance = self.param_overrides.is_some_and(|ov| ov.ldm.is_some());
1671        #[cfg(not(feature = "ldm"))]
1672        let long_distance = false;
1673        self.frame_in_place = match workspace.ingest() {
1674            crate::encoding::workspace::IngestPlan::Slice(len) => {
1675                (dict_hint.is_none() || fast_attach)
1676                    && !long_distance
1677                    && self.borrowed_supported()
1678                    && len
1679                        .checked_add(dict_len)
1680                        .is_some_and(|len| len <= u32::MAX as usize)
1681            }
1682            _ => false,
1683        };
1684        let history_bytes = frame_history_bytes(
1685            next_backend,
1686            workspace,
1687            self.frame_in_place,
1688            hint,
1689            dict_hint.map_or(0, |sizes| sizes.content),
1690            max_window_size,
1691            level,
1692        );
1693        // The input the frame can write into its tables: the size when known,
1694        // otherwise the most its history is laid out to hold.
1695        let expected_input = hint.map_or(history_bytes, |bytes| {
1696            usize::try_from(bytes).unwrap_or(usize::MAX)
1697        });
1698        // The hint-dependent hash-table width the active backend applies, for
1699        // the primed-snapshot key. Dfast/Row compute it from `table_window_size`
1700        // below; HC/Fast leave it `0` because their widths live in `params`
1701        // (`hc.{hash,chain}_log` / `fast_hash_log`) — already part of the key.
1702        let mut resolved_table_bits: usize = 0;
1703        match &mut self.storage {
1704            MatcherStorage::Simple(m) => {
1705                // Per-level Fast cParams threaded from
1706                // resolve_level_params (see Simple-backend swap
1707                // arm above for the (level → params) mapping).
1708                let fast = params.fast.expect("Fast level row carries a FastConfig");
1709                // Same attach/copy split the dict-prime dispatch applies
1710                // below (`prime_with_dictionary`): only attach-mode dict
1711                // frames may keep the main table across the reset via an
1712                // epoch advance — copy-mode and no-dict frames must memset
1713                // it back to bias 0 for the raw-slice kernels.
1714                let dict_attach_epoch = dict_hint.is_some()
1715                    && self.reset_dict_attach_ok
1716                    && self
1717                        .reset_size_log
1718                        .is_none_or(|log| log <= FAST_ATTACH_DICT_CUTOFF_LOG);
1719                // Dictionary frame the caller will restore the primed snapshot
1720                // into (the same `restores_primed_snapshot` rule on the same
1721                // size bucket; a dictionary too large to attach is in copy
1722                // mode at every size, but only these frames are restored) and
1723                // whose snapshot matches this exact resolved shape:
1724                // `restore_primed_dictionary`, called right after this reset,
1725                // will `clone_from` the snapshot over this matcher, replacing
1726                // the table contents and bias wholesale, so the reset's
1727                // full-table memset would be thrown away. The
1728                // key components mirror `reset_shape` below: Simple leaves
1729                // `resolved_table_bits` 0, and `fast_attach` is false in copy
1730                // mode by construction.
1731                // The slot format is the one component this reset has yet to
1732                // choose, so it travels with the carry: the reset leaves the
1733                // table only if it chooses the snapshot's format, which is
1734                // the only case the restore then accepts.
1735                let restored_slot_format = self
1736                    .primed
1737                    .as_ref()
1738                    .filter(|(_, _, captured)| {
1739                        dict_hint.is_some()
1740                            && restores_primed_snapshot(
1741                                self.reset_size_log,
1742                                FAST_ATTACH_DICT_CUTOFF_LOG,
1743                            )
1744                            && *captured
1745                                == PrimedKey {
1746                                    level,
1747                                    params,
1748                                    table_bits: 0,
1749                                    fast_attach: false,
1750                                    fast_tagged: captured.fast_tagged,
1751                                }
1752                    })
1753                    .map(|(_, _, captured)| captured.fast_tagged);
1754                let carry = if let Some(tagged) = restored_slot_format {
1755                    TableCarry::OverwrittenByRestore { tagged }
1756                } else if dict_attach_epoch {
1757                    TableCarry::AdvanceEpoch
1758                } else {
1759                    TableCarry::Clear
1760                };
1761                // Cap `hash_log <= window_log + 1` (upstream zstd
1762                // `ZSTD_adjustCParams_internal`): once `window_log` is resized
1763                // down for a small source, a level-default `1 << hash_log`
1764                // table is mostly wasted address space whose per-frame memset
1765                // dominates the compress cost on tiny frames (a 4 KB frame at
1766                // window_log 12 still zero-fills the 64 KiB hash_log-14 table).
1767                // Gated to no-dict frames: the dict-attach path shares one
1768                // hash_log between the main and dict tables (so one hash keys
1769                // both), and shrinking only the main table would break that
1770                // invariant and the small-frame dict ratio.
1771                let hash_log = if dict_hint.is_some() {
1772                    fast.hash_log
1773                } else {
1774                    fast.hash_log.min(params.window_log as u32 + 1)
1775                };
1776                // The attached dictionary table takes the CDict's `hashLog`
1777                // (upstream `ZSTD_createCDict`: `ZSTD_getCParams(level,
1778                // UNKNOWN, dictSize)` adjusted for a `minSrcSize` source), not
1779                // the source-capped main width: a small source must not
1780                // shrink the table a large dictionary was sized for.
1781                m.set_dict_table_hash_log(dict_hint.map(|sizes| {
1782                    crate::encoding::cparams::get_cdict_cparams(
1783                        crate::encoding::levels::config::numeric_level(level),
1784                        sizes.serialized,
1785                        &overrides,
1786                    )
1787                    .hash_log
1788                }));
1789                let tables =
1790                    crate::encoding::simple::fast_kernel::hash_table::FastHashTable::workspace_bytes(
1791                        hash_log,
1792                    );
1793                // The history keeps only what the reset will, as on the dfast arm.
1794                m.retire_history();
1795                workspace.open_for_match_finder(
1796                    tables + m.history_workspace_bytes(history_bytes),
1797                    tables,
1798                    max_window_size,
1799                    expected_input,
1800                );
1801                // The history binds first, carrying its bytes out from under
1802                // where the table may now land.
1803                m.bind_history(workspace, history_bytes);
1804                m.reset(
1805                    params.window_log,
1806                    hash_log,
1807                    fast.mls,
1808                    fast.step_size,
1809                    expected_input,
1810                    dict_hint.map_or(0, |sizes| sizes.content),
1811                    carry,
1812                    workspace,
1813                );
1814            }
1815            MatcherStorage::Dfast(dfast) => {
1816                dfast.max_window_size = max_window_size;
1817                let dcfg = params
1818                    .dfast
1819                    .expect("Dfast level row must carry a DfastConfig");
1820                // Upstream zstd `cParams.hashLog`/`chainLog`, capped by the
1821                // source-size window when hinted so tiny inputs don't
1822                // over-allocate.
1823                let long_bits = if hinted {
1824                    dfast_hash_bits_for_window(table_window_size).min(dcfg.long_hash_log as usize)
1825                } else {
1826                    dcfg.long_hash_log as usize
1827                };
1828                let short_bits = if hinted {
1829                    dfast_hash_bits_for_window(table_window_size).min(dcfg.short_hash_log as usize)
1830                } else {
1831                    dcfg.short_hash_log as usize
1832                };
1833                resolved_table_bits = long_bits;
1834                dfast.set_hash_bits(long_bits, short_bits);
1835                // The attached dictionary tables take the CDict's geometry
1836                // (upstream hashes the dictMatchState tables with
1837                // `dictCParams`), not the source-capped live widths.
1838                dfast.set_dict_table_bits(dict_hint.map(|sizes| {
1839                    let cd = crate::encoding::cparams::get_cdict_cparams(
1840                        crate::encoding::levels::config::numeric_level(level),
1841                        sizes.serialized,
1842                        &overrides,
1843                    );
1844                    (cd.hash_log as usize, cd.chain_log as usize)
1845                }));
1846                // A copy-mode frame (source past the attach cutoff, or a
1847                // dictionary too large to tag) merges the dictionary into the
1848                // live tables; a resident attach-mode table must not be
1849                // re-borrowed under it (same decision as the prime dispatch).
1850                // The width-change invalidation in `set_hash_bits` does not
1851                // cover an unhinted attach frame followed by a large hinted
1852                // one whose live widths coincide at the level's full widths.
1853                let dfast_attach_next = dict_hint.is_some_and(|sizes| {
1854                    sizes.content <= crate::encoding::dfast::DFAST_ATTACH_DICT_MAX_LEN
1855                }) && self
1856                    .reset_size_log
1857                    .is_none_or(|log| log <= DFAST_ATTACH_DICT_CUTOFF_LOG);
1858                if dict_hint.is_some() && !dfast_attach_next {
1859                    dfast.invalidate_dict_cache();
1860                }
1861                // The widths are settled, so the tables can be laid out; the
1862                // reset below reads whether they continue the last frame's.
1863                // The history binds first, carrying its bytes out from under
1864                // where the tables may now land.
1865                let tables = dfast.tables_workspace_bytes();
1866                // The history keeps only what the reset will: laid out and
1867                // carried over at its full length, a long stream's window would
1868                // be moved into room the next frame never reads.
1869                dfast.retire_history();
1870                workspace.open_for_match_finder(
1871                    tables + dfast.history.workspace_bytes(history_bytes),
1872                    tables,
1873                    max_window_size,
1874                    expected_input,
1875                );
1876                dfast.history.bind(workspace, history_bytes);
1877                // Tagged slots unless the dictionary is attached (its loop reads
1878                // the live tables bare) or the window, dictionary included, is
1879                // past what a tagged slot's position range keeps across a rebase.
1880                // A slice scanned in place numbers its positions from its start
1881                // and never rebases, so its whole length has to fit as well.
1882                let dict_len = dict_hint.map_or(0, |sizes| sizes.content);
1883                let in_place_fits = !self.frame_in_place
1884                    || matches!(
1885                        workspace.ingest(),
1886                        crate::encoding::workspace::IngestPlan::Slice(len)
1887                            if len <= crate::encoding::dfast::DFAST_TAGGED_MAX_REL
1888                    );
1889                let tagged = !dfast_attach_next
1890                    && in_place_fits
1891                    && max_window_size.checked_add(dict_len).is_some_and(|window| {
1892                        (crate::encoding::dfast::DFAST_TAGGED_WINDOW_FLOOR
1893                            ..=crate::encoding::dfast::DFAST_TAGGED_WINDOW_LIMIT)
1894                            .contains(&window)
1895                    });
1896                dfast.bind_tables(workspace, tagged);
1897                dfast.reset();
1898            }
1899            MatcherStorage::Row(row) => {
1900                row.max_window_size = max_window_size;
1901                row.lazy_depth = params.lazy_depth;
1902                row.set_dict_plan(dict_plan);
1903                let mut row_cfg = params.row.expect("Row level row carries a RowConfig");
1904                // A dictionary frame's widths already come from the CDict's
1905                // cParams (adjusted to the source when attached); only a plain
1906                // hinted frame caps them by the window here.
1907                if hinted && dict_plan.is_none() {
1908                    // Clamp the configured hash width by the hinted window
1909                    // (upstream zstd `ZSTD_adjustCParams` caps hashLog by windowLog) —
1910                    // `min`, not replace, so an explicit `hash_log` param
1911                    // override (`row_cfg.hash_bits`) survives the hinted path
1912                    // instead of being overwritten by the window value.
1913                    //
1914                    // Clamp BEFORE `configure` so the backend sees ONE width
1915                    // per frame. Configuring with the unclamped level width
1916                    // and then re-clamping made `row_hash_log` oscillate on
1917                    // every hinted frame, and each width change clears the
1918                    // row tables — `ensure_tables` then re-filled all three
1919                    // every frame in a reused compressor.
1920                    row_cfg.hash_bits = row_cfg
1921                        .hash_bits
1922                        .min(row_hash_bits_for_window(table_window_size));
1923                }
1924                row.configure(row_cfg);
1925                // Key the primed snapshot on the width the backend ACTUALLY
1926                // applied (`set_hash_bits` clamps the request): recording the
1927                // request — or the 0 default on the unhinted path — keys
1928                // identical table geometries apart and forces needless
1929                // dictionary re-primes.
1930                resolved_table_bits = row.hash_bits();
1931                // The finder and its widths are settled by `configure`, so the
1932                // tables can be laid out; the reset reads whether they continue
1933                // the last frame's. The history keeps only what the reset can,
1934                // as on the dfast arm.
1935                row.retire_history();
1936                workspace.open_for_match_finder(
1937                    row.tables_workspace_bytes() + row.history.workspace_bytes(history_bytes),
1938                    row.zero_table_bytes(),
1939                    max_window_size,
1940                    expected_input,
1941                );
1942                row.history.bind(workspace, history_bytes);
1943                row.bind_tables(workspace);
1944                row.reset();
1945            }
1946            MatcherStorage::HashChain(hc) => {
1947                hc.table.max_window_size = max_window_size;
1948                hc.hc.lazy_depth = params.lazy_depth;
1949                let mut hc_cfg = params.hc.expect("HashChain level row carries an HcConfig");
1950                // Cap the hash / chain table logs by the hinted window so a small
1951                // input doesn't allocate the full level's tables (the upstream zstd
1952                // `ZSTD_adjustCParams_internal` clamp: `hashLog <= windowLog + 1`,
1953                // and `cycleLog <= windowLog` — `cycleLog == chainLog` for the HC
1954                // finder, `chainLog - 1` for the BT pair table, so `chainLog <=
1955                // windowLog` (+1 for BT)). Ratio-neutral: a hinted window of
1956                // `2^wlog` bytes holds at most `2^wlog` positions, so the slots
1957                // beyond that are never populated — capping only sheds unused
1958                // allocation. Was the source of L10-lazy peak-alloc ~2.15x the
1959                // upstream zstd on a 1 MiB input. Only applied when hinted; an
1960                // unknown-size stream keeps the full level tables.
1961                // Skip for dictionary frames: their `hc_cfg.{hash,chain}_log`
1962                // are the CDict's (verbatim when the dictionary is copied
1963                // into the live tables, source-adjusted when it is attached);
1964                // re-applying the source-window cap would collapse a copied
1965                // dictionary's tables to the small hinted source.
1966                if hinted && dict_hint.is_none() {
1967                    let wlog = hc_hash_bits_for_window(table_window_size);
1968                    let uses_bt = matches!(
1969                        strategy_tag,
1970                        super::strategy::StrategyTag::Btlazy2
1971                            | super::strategy::StrategyTag::BtOpt
1972                            | super::strategy::StrategyTag::BtUltra
1973                            | super::strategy::StrategyTag::BtUltra2
1974                    );
1975                    hc_cfg.hash_log = hc_cfg.hash_log.min(wlog + 1);
1976                    hc_cfg.chain_log = hc_cfg.chain_log.min(if uses_bt { wlog + 1 } else { wlog });
1977                }
1978                hc.configure(hc_cfg, strategy_tag, params.window_log);
1979                // A copy-mode frame merges the dictionary into the live tables,
1980                // so the previous attach frame's dms must not be re-borrowed
1981                // under it (the same decision the prime makes, taken here
1982                // because the reset below re-borrows on a primed dms alone).
1983                if dict_hint.is_some() && !hc_attaches_dictionary(hc, self.reset_size_log) {
1984                    hc.table.dms.invalidate();
1985                }
1986                // The widths are settled by `configure`, so the tables can be
1987                // laid out before the reset retires the previous frame's
1988                // entries in them.
1989                let tables = hc.table.tables_workspace_bytes();
1990                // The history keeps only what the reset will, as on the dfast arm.
1991                hc.table.retire_history();
1992                workspace.open_for_match_finder(
1993                    tables + hc.table.history.workspace_bytes(history_bytes),
1994                    tables,
1995                    max_window_size,
1996                    expected_input,
1997                );
1998                hc.table.history.bind(workspace, history_bytes);
1999                hc.table.bind_tables(workspace);
2000                hc.reset();
2001            }
2002        }
2003        // The long-distance matcher for the frame, whichever strategy runs it.
2004        // The same parameters keep the table allocation and only clear it, as
2005        // upstream reuses its `ldmState_t`.
2006        #[cfg(feature = "ldm")]
2007        {
2008            self.ldm = frame_ldm.map(|p| match self.ldm.take() {
2009                Some(mut existing) if existing.params() == p => {
2010                    existing.clear();
2011                    existing
2012                }
2013                _ => super::ldm::LdmProducer::new(p),
2014            });
2015            // A dictionary the reset kept in place is the frame's first bytes.
2016            self.ldm_committed_end = self.storage.committed_window().len();
2017            self.ldm_dict_end = self.ldm_committed_end;
2018            self.ldm_split_block = None;
2019        }
2020        // Record the resolved matcher shape for the primed-snapshot key. Captured
2021        // here (post-resolution, after the test-only param override) so the key
2022        // reflects exactly the geometry the restored `storage` must match. The
2023        // Fast attach-vs-copy mode is part of the shape ONLY for the Simple
2024        // backend (it decides the distinct dict-table shape that backend builds).
2025        // Dfast/Row/HashChain have their OWN attach/copy regimes, but this bit
2026        // models only the Fast table split; those backends are keyed by the
2027        // resolved matcher geometry instead, so folding the Fast bit into their
2028        // key would over-key identical resolved shapes. `fast_attach` is the
2029        // decision `prime_with_dictionary` makes from the same `reset_size_log`.
2030        // The Fast slot format this reset chose; only the Simple backend has one.
2031        let fast_tagged = matches!(&self.storage, MatcherStorage::Simple(m) if m.slots_tagged());
2032        self.reset_shape = Some((params, resolved_table_bits, fast_attach, fast_tagged));
2033        // Everything is laid out in `workspace` now. A driver reset on its own
2034        // before it joined a context still holds the workspace it used then,
2035        // which nothing points into any more; on its own the field is empty
2036        // here, since `reset` has it out for the call.
2037        self.own_workspace = crate::encoding::workspace::Workspace::new();
2038    }
2039
2040    // Dictionary entry points forward to the `dict_prime` child module, which
2041    // owns the prime / snapshot lifecycle (it reaches the driver's private
2042    // `primed` / `reset_shape` state directly as a descendant module).
2043
2044    #[inline]
2045    fn dictionary_is_resident(&self) -> bool {
2046        self.dictionary_is_resident_impl()
2047    }
2048
2049    #[inline]
2050    fn reapply_resident_dictionary(&mut self, offset_hist: [u32; 3]) {
2051        self.reapply_resident_dictionary_impl(offset_hist)
2052    }
2053
2054    #[inline]
2055    fn prime_with_dictionary(&mut self, dict_content: &[u8], offset_hist: [u32; 3]) {
2056        self.prime_with_dictionary_impl(dict_content, offset_hist)
2057    }
2058
2059    #[inline]
2060    fn restore_primed_dictionary(&mut self, level: super::CompressionLevel) -> bool {
2061        self.restore_primed_dictionary_impl(level)
2062    }
2063
2064    #[inline]
2065    fn capture_primed_dictionary(&mut self, level: super::CompressionLevel) {
2066        self.capture_primed_dictionary_impl(level)
2067    }
2068
2069    #[inline]
2070    fn invalidate_primed_dictionary(&mut self) {
2071        self.invalidate_primed_dictionary_impl()
2072    }
2073
2074    #[inline]
2075    fn seed_dictionary_entropy(
2076        &mut self,
2077        huff: Option<&crate::huff0::huff0_encoder::HuffmanTable>,
2078        ll: Option<&crate::fse::fse_encoder::FSETable>,
2079        ml: Option<&crate::fse::fse_encoder::FSETable>,
2080        of: Option<&crate::fse::fse_encoder::FSETable>,
2081    ) {
2082        self.seed_dictionary_entropy_impl(huff, ll, ml, of)
2083    }
2084
2085    fn leave_workspace(&mut self) {
2086        match &mut self.storage {
2087            MatcherStorage::Simple(m) => m.leave_workspace(),
2088            MatcherStorage::Dfast(m) => {
2089                m.tables.leave_workspace();
2090                m.history.leave_workspace();
2091            }
2092            MatcherStorage::Row(m) => {
2093                m.tables.leave_workspace();
2094                m.history.leave_workspace();
2095            }
2096            MatcherStorage::HashChain(m) => {
2097                m.table.tables.leave_workspace();
2098                m.table.history.leave_workspace();
2099            }
2100        }
2101    }
2102
2103    fn window_size(&self) -> u64 {
2104        self.reported_window_size as u64
2105    }
2106
2107    fn matches_beyond_window_tables(&self) -> bool {
2108        #[cfg(feature = "ldm")]
2109        return self.ldm.is_some();
2110        #[cfg(not(feature = "ldm"))]
2111        false
2112    }
2113
2114    fn settle_block_cut(&mut self, window: usize, cut: usize) -> usize {
2115        #[cfg(feature = "ldm")]
2116        return self.settle_long_distance_cut(window, cut);
2117        #[cfg(not(feature = "ldm"))]
2118        {
2119            let _ = window;
2120            cut
2121        }
2122    }
2123
2124    fn get_last_space(&mut self) -> &[u8] {
2125        // A block matched in pieces is still one block to the encoder.
2126        #[cfg(feature = "ldm")]
2127        if let Some(len) = self.ldm_split_block {
2128            let window = self.storage.committed_window();
2129            return &window[window.len() - len..];
2130        }
2131        match &self.storage {
2132            MatcherStorage::Simple(m) => m.last_committed_space(),
2133            MatcherStorage::Dfast(m) => m.get_last_space(),
2134            MatcherStorage::Row(m) => m.get_last_space(),
2135            MatcherStorage::HashChain(m) => m.table.get_last_space(),
2136        }
2137    }
2138
2139    /// Read the next block STRAIGHT into the backend's history buffer.
2140    ///
2141    /// The bytes are in the buffer but not yet part of the window: the caller
2142    /// picks the block boundary from [`Self::uncommitted_input`] and then
2143    /// calls [`Self::commit_filled`].
2144    fn fill_in_place(
2145        &mut self,
2146        capacity: usize,
2147        fill: &mut dyn FnMut(&mut HistoryBuf) -> (usize, bool),
2148    ) -> (usize, bool) {
2149        match &mut self.storage {
2150            MatcherStorage::Simple(m) => m.fill_uncommitted(capacity, fill),
2151            MatcherStorage::Dfast(m) => m.fill_uncommitted(capacity, fill),
2152            MatcherStorage::Row(m) => m.fill_uncommitted(capacity, fill),
2153            MatcherStorage::HashChain(m) => m.table.fill_uncommitted(capacity, fill),
2154        }
2155    }
2156
2157    /// Bytes read by [`Self::fill_in_place`] that no block has claimed yet.
2158    fn uncommitted_input(&self) -> &[u8] {
2159        match &self.storage {
2160            MatcherStorage::Simple(m) => m.uncommitted(),
2161            MatcherStorage::Dfast(m) => m.uncommitted(),
2162            MatcherStorage::Row(m) => m.uncommitted(),
2163            MatcherStorage::HashChain(m) => m.table.uncommitted(),
2164        }
2165    }
2166
2167    /// Claim `len` bytes of [`Self::uncommitted_input`] as the next block.
2168    ///
2169    /// A dictionary inflates `max_window_size` so the primed bytes stay
2170    /// reachable, and once eviction carries them out of the window that
2171    /// inflation has to be retired. Skipping it leaves the backend admitting
2172    /// matches older than the window the frame header reports, which encodes
2173    /// an offset no decoder can resolve.
2174    fn commit_filled(&mut self, len: usize) {
2175        #[cfg(feature = "ldm")]
2176        {
2177            self.ldm_committed_end += len;
2178            self.ldm_split_block = None;
2179        }
2180        // The window backends evict whole blocks inside `commit_block`,
2181        // subtracting each evicted block from `window_size` and then adding
2182        // `len`, so `pre + len - post` is exactly what was evicted and never
2183        // negative; the sum is two byte counts bounded by the window. The Fast
2184        // backend reports the bytes it drained itself.
2185        let evicted_bytes = match &mut self.storage {
2186            MatcherStorage::Simple(m) => m.commit_block(len),
2187            MatcherStorage::Dfast(m) => {
2188                let pre = m.window_size;
2189                m.commit_block(len);
2190                pre + len - m.window_size
2191            }
2192            MatcherStorage::Row(m) => {
2193                let pre = m.window_size;
2194                m.commit_block(len);
2195                pre + len - m.window_size
2196            }
2197            MatcherStorage::HashChain(m) => {
2198                let pre = m.table.window_size;
2199                m.table.commit_block(len);
2200                pre + len - m.table.window_size
2201            }
2202        };
2203        // The second trim pass only does work when retiring the budget shrank
2204        // `max_window_size` below the backend's window, so the common
2205        // no-dictionary commit skips it.
2206        if self.retire_dictionary_budget(evicted_bytes) {
2207            self.trim_after_budget_retire();
2208        }
2209    }
2210
2211    fn start_matching(&mut self, mut handle_sequence: impl FnMut(Sequence)) {
2212        // Borrowed one-shot Fast path: if the frame driver staged a
2213        // block range via `set_borrowed_block`, scan it in place against
2214        // the borrowed window instead of the owned committed block. Only
2215        // the Simple backend is instrumented (the gate guarantees it),
2216        // and the stage is consumed so the next block re-stages.
2217        if let Some((block_start, block_end)) = self.borrowed_pending.take() {
2218            // The borrowed scans share their kernels with the owned ones, so
2219            // they report through the same sink type.
2220            let mut forward = None;
2221            let mut sink = owned_sink(&mut handle_sequence, &mut forward);
2222            match self.active_backend() {
2223                super::strategy::BackendTag::Simple => {
2224                    let m = self.simple_mut();
2225                    if m.dict_is_attached() {
2226                        // Dict-attach borrowed scan: live matches read the
2227                        // borrowed input in place, dict matches read the
2228                        // committed dict prefix via the 2-segment counter.
2229                        m.start_matching_borrowed_dict(block_start, block_end, &mut sink);
2230                    } else {
2231                        m.start_matching_borrowed(block_start, block_end, &mut sink);
2232                    }
2233                }
2234                super::strategy::BackendTag::Dfast => self
2235                    .dfast_matcher_mut()
2236                    .start_matching_borrowed(block_start, block_end, &mut sink),
2237                super::strategy::BackendTag::Row => {
2238                    // Same greedy/lazy parse split as the owned RowHash arm.
2239                    let greedy = self.parse == super::strategy::ParseMode::Greedy;
2240                    self.row_matcher_mut().start_matching_borrowed(
2241                        block_start,
2242                        block_end,
2243                        greedy,
2244                        &mut sink,
2245                    );
2246                }
2247                super::strategy::BackendTag::HashChain => match self.search {
2248                    super::strategy::SearchMethod::HashChain => self
2249                        .hc_matcher_mut()
2250                        .start_matching_lazy_borrowed(block_start, block_end, &mut sink),
2251                    // `borrowed_supported()` keeps the optimal parsers on the
2252                    // owned path and `set_borrowed_block` asserts it.
2253                    other => {
2254                        unreachable!("HashChain backend with unexpected borrowed search {other:?}")
2255                    }
2256                },
2257            }
2258            return;
2259        }
2260        if self.producer.is_some() {
2261            // A producer stands in for every parser, the optimal one too, so
2262            // its block's long matches are taken outright at any strategy and
2263            // the producer parses the literals between them. Upstream refuses
2264            // the pair instead (zstd.h, block-level sequence producer
2265            // limitations); taking both loses neither.
2266            #[cfg(feature = "ldm")]
2267            if self.ldm.is_some() {
2268                // Where the producer fails with fallback, the backend matches
2269                // what is left without long-match candidates: this block's are
2270                // taken outright, and the optimal parser still holds the last
2271                // block's, whose block-relative positions would name the wrong
2272                // bytes here (its candidates are trusted, not re-checked).
2273                if matches!(self.search, super::strategy::SearchMethod::BinaryTree) {
2274                    let MatcherStorage::HashChain(hc) = &mut self.storage else {
2275                        unreachable!("the optimal parser runs on the hash-chain backend");
2276                    };
2277                    hc.backend.bt_mut().ldm_sequences.clear();
2278                }
2279                if self.start_matching_long_distance(&mut handle_sequence, true) {
2280                    return;
2281                }
2282                if self.match_produced(&mut handle_sequence) {
2283                    return;
2284                }
2285                // The producer failed with fallback on a block with no long
2286                // match, which is already taken: the backend matches it.
2287                let mut gap_literals = None;
2288                self.start_matching_owned(&mut owned_sink(&mut handle_sequence, &mut gap_literals));
2289                return;
2290            }
2291            if self.match_produced(&mut handle_sequence) {
2292                return;
2293            }
2294        }
2295        #[cfg(feature = "ldm")]
2296        if self.ldm.is_some() {
2297            // The optimal parser weighs long-distance matches as candidates;
2298            // below it they are taken outright (upstream
2299            // `ZSTD_ldm_blockCompress`, `zstd_ldm.c:685-749`).
2300            if matches!(self.search, super::strategy::SearchMethod::BinaryTree) {
2301                self.take_long_distance_block();
2302                let MatcherStorage::HashChain(hc) = &mut self.storage else {
2303                    unreachable!("the optimal parser runs on the hash-chain backend");
2304                };
2305                core::mem::swap(&mut self.ldm_block, &mut hc.backend.bt_mut().ldm_sequences);
2306            } else if self.start_matching_long_distance(&mut handle_sequence, false) {
2307                return;
2308            }
2309        }
2310        let mut gap_literals = None;
2311        self.start_matching_owned(&mut owned_sink(&mut handle_sequence, &mut gap_literals));
2312    }
2313
2314    fn skip_matching(&mut self) {
2315        self.skip_matching_with_hint(None);
2316    }
2317
2318    fn skip_matching_with_hint(&mut self, incompressible_hint: Option<bool>) {
2319        // Borrowed one-shot Fast path: a staged block range routes to the
2320        // borrowed skip (records the range for `get_last_space`, primes
2321        // hashes on the dict-priming hint) with no owned-history append
2322        // and nothing to recycle. Stage is consumed.
2323        if let Some((block_start, block_end)) = self.borrowed_pending.take() {
2324            match self.active_backend() {
2325                super::strategy::BackendTag::Simple => self.simple_mut().skip_matching_borrowed(
2326                    block_start,
2327                    block_end,
2328                    incompressible_hint,
2329                ),
2330                super::strategy::BackendTag::Dfast => self
2331                    .dfast_matcher_mut()
2332                    .skip_matching_borrowed(block_start, block_end, incompressible_hint),
2333                super::strategy::BackendTag::Row => self.row_matcher_mut().skip_matching_borrowed(
2334                    block_start,
2335                    block_end,
2336                    incompressible_hint,
2337                ),
2338                super::strategy::BackendTag::HashChain => self
2339                    .hc_matcher_mut()
2340                    .skip_matching_borrowed(block_start, block_end, incompressible_hint),
2341            }
2342            return;
2343        }
2344        // A block written out unsearched is still searched for long matches,
2345        // as upstream searches every block it compresses, and its matches are
2346        // taken past.
2347        #[cfg(feature = "ldm")]
2348        if self.ldm.is_some() {
2349            self.take_long_distance_block();
2350        }
2351        self.skip_backend(incompressible_hint);
2352    }
2353}
2354
2355impl MatchGeneratorDriver {
2356    /// Index the last committed block (or piece of one) in the backend
2357    /// without searching it, leaving the long-distance state alone.
2358    fn skip_backend(&mut self, incompressible_hint: Option<bool>) {
2359        match self.active_backend() {
2360            super::strategy::BackendTag::Simple => {
2361                self.simple_mut()
2362                    .skip_matching_with_hint(incompressible_hint);
2363            }
2364            super::strategy::BackendTag::Dfast => {
2365                self.dfast_matcher_mut().skip_matching(incompressible_hint)
2366            }
2367            super::strategy::BackendTag::Row => self
2368                .row_matcher_mut()
2369                .skip_matching_with_hint(incompressible_hint),
2370            super::strategy::BackendTag::HashChain => {
2371                self.hc_matcher_mut().skip_matching(incompressible_hint)
2372            }
2373        }
2374    }
2375}
2376
2377/// The sink every owned match runs against: `handle_sequence`, except that a
2378/// block's trailing literals go to `gap_literals` when it is `Some`, as the
2379/// gap before a long-distance match needs them. One sink type for the plain
2380/// block and for those gaps compiles each backend's scan once per caller sink
2381/// instead of twice.
2382fn owned_sink<'a, F: FnMut(Sequence)>(
2383    handle_sequence: &'a mut F,
2384    gap_literals: &'a mut Option<usize>,
2385) -> impl FnMut(Sequence) + 'a {
2386    // The guard is read only for the block's last report, never per triple.
2387    // Each arm rebuilds the report from its fields: forwarding the matched
2388    // value whole copies it as one 32-byte block out of the separate stores
2389    // that wrote it, and that copy stalls on every triple.
2390    move |sequence| match sequence {
2391        Sequence::Triple {
2392            literal_len,
2393            offset,
2394            match_len,
2395        } => handle_sequence(Sequence::Triple {
2396            literal_len,
2397            offset,
2398            match_len,
2399        }),
2400        Sequence::Literals { len } => match gap_literals {
2401            Some(gap) => *gap = len,
2402            None => handle_sequence(Sequence::Literals { len }),
2403        },
2404    }
2405}
2406
2407impl MatchGeneratorDriver {
2408    /// Match the last committed block of the owned history with the frame's
2409    /// backend.
2410    fn start_matching_owned(&mut self, handle_sequence: &mut impl FnMut(Sequence)) {
2411        use super::strategy::{self, StrategyTag};
2412        // Decoupled parse×search dispatch (fires once per block). The
2413        // search axis (`self.search`) picks the candidate-finding backend;
2414        // the parse axis (greedy vs lazy depth) is carried by the
2415        // backend's runtime `lazy_depth`, set per level at `reset()`.
2416        // The two are independent, so any parse can run on any search
2417        // backend. The `BinaryTree` arm still selects the opt `Strategy`
2418        // ZST off `strategy_tag` so `compress_block::<S>` keeps its
2419        // const-folded optimal-parser monomorphisation.
2420        use super::strategy::SearchMethod;
2421        match self.search {
2422            SearchMethod::Fast => {
2423                self.simple_mut().start_matching(handle_sequence);
2424            }
2425            SearchMethod::DoubleFast => {
2426                self.dfast_matcher_mut().start_matching(handle_sequence);
2427            }
2428            SearchMethod::RowHash | SearchMethod::BinaryTreeLazy => {
2429                // One upstream `lazy_generic` body for greedy (depth 0: a
2430                // repcode hit is stored without a search, no lookahead), lazy
2431                // / lazy2 (`lazy_depth` 1 / 2) and btlazy2 (depth 2 over the
2432                // binary-tree finder); the finder (rows / chain / tree) was
2433                // resolved at `configure`.
2434                self.row_matcher_mut().start_matching(handle_sequence);
2435            }
2436            SearchMethod::HashChain => {
2437                // Greedy/lazy/lazy2 all flow through the lazy parser; it
2438                // reads `hc.lazy_depth` (0 = greedy commit).
2439                self.hc_matcher_mut().start_matching_lazy(handle_sequence);
2440            }
2441            SearchMethod::BinaryTree => match self.strategy_tag {
2442                StrategyTag::BtOpt => self.compress_block::<strategy::BtOpt>(handle_sequence),
2443                StrategyTag::BtUltra => self.compress_block::<strategy::BtUltra>(handle_sequence),
2444                StrategyTag::BtUltra2 => self.compress_block::<strategy::BtUltra2>(handle_sequence),
2445                _ => unreachable!(
2446                    "SearchMethod::BinaryTree requires an optimal strategy tag (BtOpt/BtUltra/BtUltra2)"
2447                ),
2448            },
2449        }
2450    }
2451}
2452
2453impl MatchGeneratorDriver {
2454    /// Monomorphised optimal-parser entry point. Only the `BinaryTree`
2455    /// search arm of [`Matcher::start_matching`] routes here, selecting
2456    /// the concrete opt `S: Strategy` (BtOpt / BtUltra / BtUltra2) off
2457    /// `strategy_tag`, so the optimiser keeps the cost-model predicates
2458    /// (`S::USE_BT` / `S::USE_HASH3` / `S::ACCURATE_PRICE` /
2459    /// `S::TWO_PASS_SEED`) const-folded per strategy. The non-opt search
2460    /// backends (Fast / DoubleFast / RowHash / HashChain) are dispatched
2461    /// directly off the search axis and never reach this method, so all
2462    /// strategies arriving here are HashChain-backed.
2463    fn compress_block<S: super::strategy::Strategy>(
2464        &mut self,
2465        handle_sequence: &mut impl FnMut(Sequence),
2466    ) {
2467        debug_assert_eq!(S::BACKEND, super::strategy::BackendTag::HashChain);
2468        debug_assert!(
2469            S::USE_BT,
2470            "compress_block only handles the optimal (BT) path"
2471        );
2472        self.hc_matcher_mut()
2473            .start_matching_strategy::<S>(handle_sequence);
2474    }
2475}
2476
2477/// Stage D: backend storage discriminator.
2478///
2479/// HC (lazy / lazy2) modes carry no extra per-frame state beyond the
2480/// shared `MatchTable` and `HcMatcher` runtime knobs, so the
2481/// [`HcBackend::Hc`] variant is zero-sized — no BT scratch is
2482/// allocated. BT-flavoured modes (`btopt` / `btultra` / `btultra2`)
2483/// hold the full [`super::bt::BtMatcher`] inside the
2484/// [`HcBackend::Bt`] variant (cost model, optimal-parser scratch
2485/// arenas, LDM candidate buffer).
2486///
2487/// The discriminator lives next to `parse_mode` so `configure()` can
2488/// promote between the two on a level change without touching the
2489/// `MatchTable` storage.
2490#[derive(Clone)]
2491pub(crate) enum HcBackend {
2492    /// Lazy / lazy2 modes — no per-frame backend state.
2493    Hc,
2494    /// BT-driven modes — owns the optimal parser's per-frame scratch.
2495    /// Boxed so the enum stays pointer-sized: HC-only matchers pay
2496    /// just the `Box`-niche, not the 4 KiB `BtMatcher` payload.
2497    Bt(alloc::boxed::Box<super::bt::BtMatcher>),
2498}
2499
2500#[cfg(feature = "bench-internals")]
2501pub(crate) fn level22_block_ranges(data: &[u8]) -> Vec<(usize, usize)> {
2502    let mut ranges = Vec::new();
2503    let mut cursor = 0usize;
2504    let mut savings = 0i64;
2505    while cursor < data.len() {
2506        let remaining = data.len() - cursor;
2507        let candidate_len = remaining.min(super::cost_model::HC_BLOCKSIZE_MAX);
2508        let block_len = crate::encoding::frame_compressor::optimal_block_size(
2509            CompressionLevel::Level(22),
2510            &data[cursor..cursor + candidate_len],
2511            remaining,
2512            super::cost_model::HC_BLOCKSIZE_MAX,
2513            savings,
2514        )
2515        .min(candidate_len)
2516        .max(1);
2517        ranges.push((cursor, block_len));
2518        cursor += block_len;
2519        // The exact upstream zstd gate uses compressed-size savings. For this corpus
2520        // parity harness, after the first full block has compressed, savings is
2521        // sufficient to authorize the same pre-block splitter path.
2522        if cursor >= super::cost_model::HC_BLOCKSIZE_MAX {
2523            savings = 3;
2524        }
2525    }
2526    ranges
2527}
2528
2529#[cfg(feature = "bench-internals")]
2530fn merge_block_delimiters(sequences: Vec<(usize, usize, usize)>) -> Vec<(usize, usize, usize)> {
2531    let mut out = Vec::with_capacity(sequences.len());
2532    let mut pending_lits = 0usize;
2533    for (lit_len, offset, match_len) in sequences {
2534        if offset == 0 && match_len == 0 {
2535            pending_lits = pending_lits.saturating_add(lit_len);
2536            continue;
2537        }
2538        out.push((lit_len.saturating_add(pending_lits), offset, match_len));
2539        pending_lits = 0;
2540    }
2541    if pending_lits > 0 {
2542        out.push((pending_lits, 0, 0));
2543    }
2544    out
2545}
2546
2547/// White-box capture of the level-22 sequence stream (literal-length,
2548/// offset, match-length triples) the match generator emits for `data`,
2549/// with block-delimiter pseudo-sequences merged into the following
2550/// triple's literal run. Pure Rust; the C-conformance comparison that
2551/// consumes it lives in the `ffi-bench` crate.
2552#[cfg(feature = "bench-internals")]
2553pub(crate) fn collect_level22_sequences(data: &[u8]) -> Vec<(usize, usize, usize)> {
2554    merge_block_delimiters(collect_level22_sequences_with_delimiters(data))
2555        .into_iter()
2556        .filter(|(_, offset, match_len)| *offset != 0 || *match_len != 0)
2557        .collect()
2558}
2559
2560#[cfg(feature = "bench-internals")]
2561fn collect_level22_sequences_with_delimiters(data: &[u8]) -> Vec<(usize, usize, usize)> {
2562    let mut driver = MatchGeneratorDriver::new(super::cost_model::HC_BLOCKSIZE_MAX, 1);
2563    driver.set_source_size_hint(data.len() as u64);
2564    driver.reset(CompressionLevel::Level(22));
2565
2566    let mut sequences = Vec::new();
2567    for (chunk_start, chunk_len) in level22_block_ranges(data) {
2568        driver.commit_input(&data[chunk_start..chunk_start + chunk_len]);
2569        driver.start_matching(|seq| {
2570            let entry = match seq {
2571                Sequence::Literals { len } => (len, 0usize, 0usize),
2572                Sequence::Triple {
2573                    literal_len,
2574                    offset,
2575                    match_len,
2576                } => (literal_len, offset, match_len),
2577            };
2578            sequences.push(entry);
2579        });
2580    }
2581    sequences
2582}
2583
2584#[cfg(test)]
2585mod tests;