structured_zstd/encoding/parameters.rs
1//! Fine-grained compression parameters — the drop-in equivalent of C
2//! zstd's advanced `ZSTD_CCtx_setParameter` surface (#27).
3//!
4//! [`CompressionLevel`](crate::encoding::CompressionLevel) selects a
5//! whole tuning preset in one knob. This module exposes the individual
6//! knobs underneath it — window/hash/chain/search logs, the match
7//! strategy, and the long-distance-matching (LDM) block — so callers
8//! can override a level's defaults for domain-specific tuning.
9//!
10//! # Builder
11//!
12//! [`CompressionParameters`] is built through
13//! [`CompressionParameters::builder`], which takes an explicit base
14//! [`CompressionLevel`](crate::encoding::CompressionLevel) (there is no
15//! implicit default). Every knob left unset inherits that base level's
16//! resolved value, so a builder that overrides nothing reproduces plain
17//! level-based compression byte-for-byte.
18//!
19//! ```rust
20//! use structured_zstd::encoding::{CompressionLevel, CompressionParameters, Strategy};
21//!
22//! let params = CompressionParameters::builder(CompressionLevel::Level(19))
23//! .window_log(22)
24//! .strategy(Strategy::Btultra2)
25//! .enable_long_distance_matching(true)
26//! .build()
27//! .expect("parameters within bounds");
28//! ```
29//!
30//! # Bounds
31//!
32//! Every knob has an inclusive `[lower, upper]` range, queryable via
33//! [`CParameter::bounds`] (the analogue of `ZSTD_cParam_getBounds`).
34//! [`CompressionParametersBuilder::build`] validates each set knob and
35//! returns [`ParameterError::OutOfBounds`] for the first violation.
36//!
37//! # Long-distance matching (LDM)
38//!
39//! LDM is **off at every [`CompressionLevel`](crate::encoding::CompressionLevel)
40//! preset**, matching upstream `libzstd.so.1` where `ZSTD_compress(..., level)`
41//! never enables LDM — even at level 22. It is activated either by
42//! [`CompressionParametersBuilder::enable_long_distance_matching`] or by any of
43//! the `ldm_*` setters, which each imply `enable_long_distance_matching(true)`.
44//! When enabled, the LDM producer attaches to the optimal (`btopt` / `btultra`
45//! / `btultra2`) match-finder; pair it with an optimal [`Strategy`] (or a level
46//! ≥ 16) for it to take effect.
47
48use crate::encoding::CompressionLevel;
49
50/// Match-finder strategy — the drop-in equivalent of C zstd's
51/// `ZSTD_strategy` enum (`ZSTD_fast` … `ZSTD_btultra2`). The numeric
52/// ordinals match upstream (`fast = 1` … `btultra2 = 9`), so
53/// [`Strategy::ordinal`] / [`Strategy::from_ordinal`] round-trip with
54/// the C `ZSTD_c_strategy` parameter value.
55#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
56pub enum Strategy {
57 /// `ZSTD_fast` (1) — single-table fast finder.
58 Fast,
59 /// `ZSTD_dfast` (2) — two parallel hash tables.
60 Dfast,
61 /// `ZSTD_greedy` (3) — commit the first acceptable match, no lookahead.
62 Greedy,
63 /// `ZSTD_lazy` (4) — one-position lazy lookahead.
64 Lazy,
65 /// `ZSTD_lazy2` (5) — two-position lazy lookahead.
66 Lazy2,
67 /// `ZSTD_btlazy2` (6) — binary-tree-assisted lazy2.
68 Btlazy2,
69 /// `ZSTD_btopt` (7) — optimal parser, no ultra refinements.
70 Btopt,
71 /// `ZSTD_btultra` (8) — optimal parser with refined price tables.
72 Btultra,
73 /// `ZSTD_btultra2` (9) — optimal parser with two-pass dynamic stats.
74 Btultra2,
75}
76
77impl Strategy {
78 /// Upstream `ZSTD_strategy` ordinal (`fast = 1` … `btultra2 = 9`).
79 pub const fn ordinal(self) -> u32 {
80 match self {
81 Self::Fast => 1,
82 Self::Dfast => 2,
83 Self::Greedy => 3,
84 Self::Lazy => 4,
85 Self::Lazy2 => 5,
86 Self::Btlazy2 => 6,
87 Self::Btopt => 7,
88 Self::Btultra => 8,
89 Self::Btultra2 => 9,
90 }
91 }
92
93 /// Construct from an upstream `ZSTD_strategy` ordinal. Returns
94 /// `None` outside `1..=9`.
95 pub const fn from_ordinal(ordinal: u32) -> Option<Self> {
96 Some(match ordinal {
97 1 => Self::Fast,
98 2 => Self::Dfast,
99 3 => Self::Greedy,
100 4 => Self::Lazy,
101 5 => Self::Lazy2,
102 6 => Self::Btlazy2,
103 7 => Self::Btopt,
104 8 => Self::Btultra,
105 9 => Self::Btultra2,
106 _ => return None,
107 })
108 }
109
110 /// Internal runtime strategy tag.
111 pub(crate) const fn tag(self) -> crate::encoding::strategy::StrategyTag {
112 use crate::encoding::strategy::StrategyTag;
113 match self {
114 Self::Fast => StrategyTag::Fast,
115 Self::Dfast => StrategyTag::Dfast,
116 Self::Greedy => StrategyTag::Greedy,
117 // Lazy / Lazy2 ride the runtime `Lazy` tag (the lazy lookahead
118 // depth carries the variance, see `lazy_depth`). `Btlazy2`
119 // keeps its own tag: `Lazy` resolves to the Row finder, while
120 // btlazy2 is a binary-tree search and must stay on the
121 // HashChain/BT storage.
122 Self::Lazy | Self::Lazy2 => StrategyTag::Lazy,
123 Self::Btlazy2 => StrategyTag::Btlazy2,
124 Self::Btopt => StrategyTag::BtOpt,
125 Self::Btultra => StrategyTag::BtUltra,
126 Self::Btultra2 => StrategyTag::BtUltra2,
127 }
128 }
129
130 /// Lazy lookahead depth for the greedy/lazy band (0/1/2). `Optimal`
131 /// strategies report 2 (the depth their hash-chain seed walk runs at).
132 pub(crate) const fn lazy_depth(self) -> u8 {
133 match self {
134 Self::Fast | Self::Dfast | Self::Greedy => 0,
135 Self::Lazy => 1,
136 _ => 2,
137 }
138 }
139}
140
141/// Whether literals are entropy-coded, the drop-in equivalent of C zstd's
142/// `ZSTD_c_literalCompressionMode` (`ZSTD_ps_auto` / `ZSTD_ps_enable` /
143/// `ZSTD_ps_disable`).
144#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
145pub enum LiteralCompressionMode {
146 /// The level decides: literals are stored raw on the fast strategy's
147 /// acceleration (negative) levels, where the Huffman pass costs more
148 /// speed than it saves, and compressed everywhere else.
149 #[default]
150 Auto,
151 /// Compress literals on every level, the negative ones included. A
152 /// block whose literals do not shrink still stores them raw.
153 Enable,
154 /// Never compress literals: every literal section is stored raw.
155 Disable,
156}
157
158/// One tunable compression parameter — the analogue of a C zstd
159/// `ZSTD_cParameter`. Used to query bounds via [`CParameter::bounds`].
160#[derive(Copy, Clone, Debug, PartialEq, Eq)]
161#[non_exhaustive]
162pub enum CParameter {
163 /// Maximum back-reference distance, `log2`. C `ZSTD_c_windowLog`.
164 WindowLog,
165 /// Match-finder hash table size, `log2`. C `ZSTD_c_hashLog`.
166 HashLog,
167 /// Match-finder chain table size, `log2`. C `ZSTD_c_chainLog`.
168 ChainLog,
169 /// Number of search attempts, `log2`. C `ZSTD_c_searchLog`.
170 SearchLog,
171 /// Minimum match length in bytes. C `ZSTD_c_minMatch`.
172 MinMatch,
173 /// "Good enough" match length that ends the search. C `ZSTD_c_targetLength`.
174 TargetLength,
175 /// Match-finder [`Strategy`] (1..=9). C `ZSTD_c_strategy`.
176 Strategy,
177 /// LDM enable flag (0/1). C `ZSTD_c_enableLongDistanceMatching`.
178 EnableLongDistanceMatching,
179 /// LDM hash table size, `log2`. C `ZSTD_c_ldmHashLog`.
180 LdmHashLog,
181 /// LDM minimum match length in bytes. C `ZSTD_c_ldmMinMatch`.
182 LdmMinMatch,
183 /// LDM bucket size, `log2`. C `ZSTD_c_ldmBucketSizeLog`.
184 LdmBucketSizeLog,
185 /// LDM hash-insertion rate, `log2`. C `ZSTD_c_ldmHashRateLog`.
186 LdmHashRateLog,
187}
188
189/// Inclusive `[lower_bound, upper_bound]` range for a [`CParameter`],
190/// the drop-in equivalent of C zstd's `ZSTD_bounds`.
191#[derive(Copy, Clone, Debug, PartialEq, Eq)]
192pub struct Bounds {
193 /// Smallest accepted value (inclusive).
194 pub lower_bound: i64,
195 /// Largest accepted value (inclusive).
196 pub upper_bound: i64,
197}
198
199impl Bounds {
200 /// Whether `value` falls within `[lower_bound, upper_bound]`.
201 pub const fn contains(&self, value: i64) -> bool {
202 value >= self.lower_bound && value <= self.upper_bound
203 }
204}
205
206impl CParameter {
207 /// Inclusive value bounds for this parameter, mirroring
208 /// `ZSTD_cParam_getBounds`. Window/hash/chain logs cap at 30 (the
209 /// encoder's match-finder ceiling) rather than the 31 C allows on
210 /// 64-bit, because the back-reference history is indexed with `u32`
211 /// positions over a `2 * window` eviction band.
212 pub const fn bounds(self) -> Bounds {
213 let (lower_bound, upper_bound) = match self {
214 // ZSTD_WINDOWLOG_MIN .. encoder ceiling.
215 Self::WindowLog => (10, 30),
216 // ZSTD_HASHLOG_MIN .. ZSTD_HASHLOG_MAX.
217 Self::HashLog => (6, 30),
218 // ZSTD_CHAINLOG_MIN .. ZSTD_CHAINLOG_MAX (64-bit).
219 Self::ChainLog => (6, 30),
220 // ZSTD_SEARCHLOG_MIN .. ZSTD_SEARCHLOG_MAX (64-bit).
221 Self::SearchLog => (1, 30),
222 // ZSTD_MINMATCH_MIN .. ZSTD_MINMATCH_MAX.
223 Self::MinMatch => (3, 7),
224 // ZSTD_TARGETLENGTH_MIN .. ZSTD_TARGETLENGTH_MAX.
225 Self::TargetLength => (0, 131_072),
226 // ZSTD_fast .. ZSTD_btultra2.
227 Self::Strategy => (1, 9),
228 // Boolean flag.
229 Self::EnableLongDistanceMatching => (0, 1),
230 // ZSTD_LDM_HASHLOG_MIN .. ZSTD_LDM_HASHLOG_MAX.
231 Self::LdmHashLog => (6, 30),
232 // ZSTD_LDM_MINMATCH_MIN .. ZSTD_LDM_MINMATCH_MAX.
233 Self::LdmMinMatch => (4, 4096),
234 // ZSTD_LDM_BUCKETSIZELOG_MIN .. ZSTD_LDM_BUCKETSIZELOG_MAX.
235 Self::LdmBucketSizeLog => (1, 8),
236 // ZSTD_LDM_HASHRATELOG_MIN .. ZSTD_WINDOWLOG_MAX - ZSTD_HASHLOG_MIN.
237 Self::LdmHashRateLog => (0, 24),
238 };
239 Bounds {
240 lower_bound,
241 upper_bound,
242 }
243 }
244}
245
246/// Error returned by [`CompressionParametersBuilder::build`] when a knob
247/// is set outside its [`CParameter::bounds`].
248#[derive(Copy, Clone, Debug, PartialEq, Eq)]
249#[non_exhaustive]
250pub enum ParameterError {
251 /// A parameter was set to a value outside its inclusive bounds.
252 OutOfBounds {
253 /// Which parameter violated its range.
254 parameter: CParameter,
255 /// The rejected value.
256 value: i64,
257 /// The inclusive `[lower, upper]` range it had to fall within.
258 bounds: Bounds,
259 },
260}
261
262impl core::fmt::Display for ParameterError {
263 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
264 match self {
265 Self::OutOfBounds {
266 parameter,
267 value,
268 bounds,
269 } => write!(
270 f,
271 "compression parameter {parameter:?} = {value} out of bounds \
272 [{}, {}]",
273 bounds.lower_bound, bounds.upper_bound
274 ),
275 }
276 }
277}
278
279#[cfg(feature = "std")]
280impl std::error::Error for ParameterError {}
281
282/// LDM tuning overrides — every knob is `Option`, falling back to the
283/// strategy-derived upstream zstd default (`LdmParams::adjust_for`) when unset.
284#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
285pub(crate) struct LdmOverride {
286 pub(crate) hash_log: Option<u32>,
287 pub(crate) min_match: Option<u32>,
288 pub(crate) bucket_size_log: Option<u32>,
289 pub(crate) hash_rate_log: Option<u32>,
290}
291
292/// Internal per-knob override set consumed by the match-generator's
293/// `reset` path. Every field left `None` inherits the base level's
294/// resolved value, so the default path is byte-identical to level-based
295/// compression.
296#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
297pub(crate) struct ParamOverrides {
298 pub(crate) window_log: Option<u8>,
299 pub(crate) hash_log: Option<u32>,
300 pub(crate) chain_log: Option<u32>,
301 pub(crate) search_log: Option<u32>,
302 pub(crate) min_match: Option<u32>,
303 pub(crate) target_length: Option<u32>,
304 pub(crate) strategy: Option<Strategy>,
305 /// `Some` when `enable_long_distance_matching(true)` was set; carries
306 /// the (possibly empty) LDM knob overrides.
307 pub(crate) ldm: Option<LdmOverride>,
308 /// Whether literals are entropy-coded; `Auto` leaves it to the level.
309 pub(crate) literal_compression: LiteralCompressionMode,
310}
311
312impl ParamOverrides {
313 /// Whether any knob overrides the base level. An all-`None`
314 /// override is a no-op the `reset` path can skip entirely, keeping
315 /// the default level-based geometry byte-identical.
316 pub(crate) fn is_empty(&self) -> bool {
317 self.window_log.is_none()
318 && self.hash_log.is_none()
319 && self.chain_log.is_none()
320 && self.search_log.is_none()
321 && self.min_match.is_none()
322 && self.target_length.is_none()
323 && self.strategy.is_none()
324 && self.ldm.is_none()
325 && self.literal_compression == LiteralCompressionMode::Auto
326 }
327}
328
329/// Fully-resolved fine-grained compression parameters. Build through
330/// [`CompressionParameters::builder`]; pass to
331/// [`FrameCompressor::set_parameters`](crate::encoding::FrameCompressor::set_parameters)
332/// or [`compress_with_parameters`](crate::encoding::compress_with_parameters).
333///
334/// Wraps a base [`CompressionLevel`](crate::encoding::CompressionLevel)
335/// plus the set of knobs that override it. A parameter set that
336/// overrides nothing is equivalent to compressing at its base level.
337#[derive(Copy, Clone, Debug, PartialEq, Eq)]
338pub struct CompressionParameters {
339 level: CompressionLevel,
340 overrides: ParamOverrides,
341}
342
343impl CompressionParameters {
344 /// Start a builder from a base compression level. Knobs left unset
345 /// inherit that level's resolved defaults.
346 pub fn builder(level: CompressionLevel) -> CompressionParametersBuilder {
347 CompressionParametersBuilder {
348 level,
349 window_log: None,
350 hash_log: None,
351 chain_log: None,
352 search_log: None,
353 min_match: None,
354 target_length: None,
355 strategy: None,
356 enable_ldm: false,
357 ldm: LdmOverride::default(),
358 literal_compression: LiteralCompressionMode::Auto,
359 }
360 }
361
362 /// The base compression level these parameters override.
363 pub fn level(&self) -> CompressionLevel {
364 self.level
365 }
366
367 /// Whether long-distance matching is enabled.
368 pub fn long_distance_matching_enabled(&self) -> bool {
369 self.overrides.ldm.is_some()
370 }
371
372 /// How literals are entropy-coded (see [`LiteralCompressionMode`]).
373 pub fn literal_compression_mode(&self) -> LiteralCompressionMode {
374 self.overrides.literal_compression
375 }
376
377 pub(crate) fn overrides(&self) -> ParamOverrides {
378 self.overrides
379 }
380}
381
382/// Builder for [`CompressionParameters`]. Each setter records one knob;
383/// [`Self::build`] validates them against [`CParameter::bounds`].
384#[derive(Copy, Clone, Debug)]
385pub struct CompressionParametersBuilder {
386 level: CompressionLevel,
387 window_log: Option<u32>,
388 hash_log: Option<u32>,
389 chain_log: Option<u32>,
390 search_log: Option<u32>,
391 min_match: Option<u32>,
392 target_length: Option<u32>,
393 strategy: Option<Strategy>,
394 enable_ldm: bool,
395 ldm: LdmOverride,
396 literal_compression: LiteralCompressionMode,
397}
398
399impl CompressionParametersBuilder {
400 /// Decide whether literals are entropy-coded, overriding the level's own
401 /// choice. C `ZSTD_c_literalCompressionMode`.
402 ///
403 /// ```rust
404 /// use structured_zstd::encoding::{
405 /// compress_with_parameters, CompressionLevel, CompressionParameters,
406 /// LiteralCompressionMode,
407 /// };
408 ///
409 /// // Literal-heavy input: 32 distinct symbols and nothing for the match
410 /// // finder to repeat, so only entropy-coding the literals shrinks it.
411 /// let text: Vec<u8> = (0..8192u32)
412 /// .map(|i| b'a' + (i.wrapping_mul(2_654_435_761) >> 27) as u8)
413 /// .collect();
414 /// // Level -3 stores literals raw by default; asking for compression
415 /// // makes the frame smaller on input like this.
416 /// let compressed_literals = CompressionParameters::builder(CompressionLevel::Level(-3))
417 /// .literal_compression(LiteralCompressionMode::Enable)
418 /// .build()
419 /// .unwrap();
420 /// let plain = compress_with_parameters(
421 /// &text[..],
422 /// &CompressionParameters::builder(CompressionLevel::Level(-3)).build().unwrap(),
423 /// );
424 /// let coded = compress_with_parameters(&text[..], &compressed_literals);
425 /// assert!(coded.len() < plain.len());
426 /// ```
427 pub fn literal_compression(mut self, mode: LiteralCompressionMode) -> Self {
428 self.literal_compression = mode;
429 self
430 }
431
432 /// Override the maximum back-reference distance (`log2`). C
433 /// `ZSTD_c_windowLog`.
434 pub fn window_log(mut self, value: u32) -> Self {
435 self.window_log = Some(value);
436 self
437 }
438
439 /// Override the match-finder hash table size (`log2`). C `ZSTD_c_hashLog`.
440 pub fn hash_log(mut self, value: u32) -> Self {
441 self.hash_log = Some(value);
442 self
443 }
444
445 /// Override the match-finder chain table size (`log2`). C `ZSTD_c_chainLog`.
446 pub fn chain_log(mut self, value: u32) -> Self {
447 self.chain_log = Some(value);
448 self
449 }
450
451 /// Override the search-attempts count (`log2`). C `ZSTD_c_searchLog`.
452 pub fn search_log(mut self, value: u32) -> Self {
453 self.search_log = Some(value);
454 self
455 }
456
457 /// Override the minimum match length in bytes. C `ZSTD_c_minMatch`.
458 pub fn min_match(mut self, value: u32) -> Self {
459 self.min_match = Some(value);
460 self
461 }
462
463 /// Override the "good enough" target match length. C `ZSTD_c_targetLength`.
464 pub fn target_length(mut self, value: u32) -> Self {
465 self.target_length = Some(value);
466 self
467 }
468
469 /// Override the match-finder [`Strategy`]. C `ZSTD_c_strategy`.
470 pub fn strategy(mut self, value: Strategy) -> Self {
471 self.strategy = Some(value);
472 self
473 }
474
475 /// Enable or disable long-distance matching. C
476 /// `ZSTD_c_enableLongDistanceMatching`. Off at every level preset.
477 /// This is the explicit activation toggle; the `ldm_*` knob setters
478 /// also enable LDM implicitly. The flag is plain last-write-wins, so
479 /// a trailing `enable_long_distance_matching(false)` disables LDM even
480 /// if an earlier `ldm_*` call set a knob (the knob is then ignored at
481 /// [`build`](Self::build)).
482 pub fn enable_long_distance_matching(mut self, enable: bool) -> Self {
483 self.enable_ldm = enable;
484 self
485 }
486
487 /// Override the LDM hash table size (`log2`). C `ZSTD_c_ldmHashLog`.
488 /// Implies [`Self::enable_long_distance_matching(true)`](Self::enable_long_distance_matching).
489 pub fn ldm_hash_log(mut self, value: u32) -> Self {
490 self.enable_ldm = true;
491 self.ldm.hash_log = Some(value);
492 self
493 }
494
495 /// Override the LDM minimum match length. C `ZSTD_c_ldmMinMatch`.
496 /// Implies [`Self::enable_long_distance_matching(true)`](Self::enable_long_distance_matching).
497 pub fn ldm_min_match(mut self, value: u32) -> Self {
498 self.enable_ldm = true;
499 self.ldm.min_match = Some(value);
500 self
501 }
502
503 /// Override the LDM bucket size (`log2`). C `ZSTD_c_ldmBucketSizeLog`.
504 /// Implies [`Self::enable_long_distance_matching(true)`](Self::enable_long_distance_matching).
505 pub fn ldm_bucket_size_log(mut self, value: u32) -> Self {
506 self.enable_ldm = true;
507 self.ldm.bucket_size_log = Some(value);
508 self
509 }
510
511 /// Override the LDM hash-insertion rate (`log2`). C `ZSTD_c_ldmHashRateLog`.
512 /// Implies [`Self::enable_long_distance_matching(true)`](Self::enable_long_distance_matching).
513 pub fn ldm_hash_rate_log(mut self, value: u32) -> Self {
514 self.enable_ldm = true;
515 self.ldm.hash_rate_log = Some(value);
516 self
517 }
518
519 /// Validate every set knob against [`CParameter::bounds`] and
520 /// produce the resolved [`CompressionParameters`].
521 ///
522 /// # Errors
523 ///
524 /// Returns [`ParameterError::OutOfBounds`] for the first knob whose
525 /// value falls outside its inclusive range.
526 pub fn build(self) -> Result<CompressionParameters, ParameterError> {
527 check(CParameter::WindowLog, self.window_log)?;
528 check(CParameter::HashLog, self.hash_log)?;
529 check(CParameter::ChainLog, self.chain_log)?;
530 check(CParameter::SearchLog, self.search_log)?;
531 check(CParameter::MinMatch, self.min_match)?;
532 check(CParameter::TargetLength, self.target_length)?;
533 if let Some(s) = self.strategy {
534 check(CParameter::Strategy, Some(s.ordinal()))?;
535 }
536 let ldm = if self.enable_ldm {
537 check(CParameter::LdmHashLog, self.ldm.hash_log)?;
538 check(CParameter::LdmMinMatch, self.ldm.min_match)?;
539 check(CParameter::LdmBucketSizeLog, self.ldm.bucket_size_log)?;
540 check(CParameter::LdmHashRateLog, self.ldm.hash_rate_log)?;
541 Some(self.ldm)
542 } else {
543 None
544 };
545 Ok(CompressionParameters {
546 level: self.level,
547 overrides: ParamOverrides {
548 // `window_log` is bounds-checked at <= 30, so the cast is lossless.
549 window_log: self.window_log.map(|v| v as u8),
550 hash_log: self.hash_log,
551 chain_log: self.chain_log,
552 search_log: self.search_log,
553 min_match: self.min_match,
554 target_length: self.target_length,
555 strategy: self.strategy,
556 ldm,
557 literal_compression: self.literal_compression,
558 },
559 })
560 }
561}
562
563/// Validate one optional knob against its bounds.
564fn check(parameter: CParameter, value: Option<u32>) -> Result<(), ParameterError> {
565 if let Some(value) = value {
566 let bounds = parameter.bounds();
567 let value = i64::from(value);
568 if !bounds.contains(value) {
569 return Err(ParameterError::OutOfBounds {
570 parameter,
571 value,
572 bounds,
573 });
574 }
575 }
576 Ok(())
577}
578
579#[cfg(test)]
580mod tests;