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