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