qubit_argument/argument/numeric_argument.rs
1// =============================================================================
2// Copyright (c) 2025 - 2026 Haixing Hu.
3//
4// SPDX-License-Identifier: Apache-2.0
5//
6// Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Ownership-preserving validation for primitive numeric arguments.
9
10use std::ops::{
11 Bound,
12 RangeBounds,
13};
14
15use crate::argument::{
16 ArgumentBound,
17 ArgumentError,
18 ArgumentErrorKind,
19 ArgumentResult,
20 ArgumentValue,
21 ComparisonConstraint,
22 RangeConstraint,
23 sealed::Sealed,
24};
25
26/// Restricts numeric validation to supported primitive numeric values.
27///
28/// Implementations provide the type's zero value, an exact structured error
29/// representation, and NaN detection. The trait is private so arbitrary
30/// partially ordered caller types cannot opt into numeric validation.
31trait NumericValue: Sealed + Copy + PartialOrd {
32 /// Returns the zero value for this primitive numeric type.
33 fn zero() -> Self;
34
35 /// Captures this value without losing integer magnitude or floating bits.
36 fn to_argument_value(self) -> ArgumentValue;
37
38 /// Returns whether this value is a floating-point NaN.
39 ///
40 /// Integer implementations always return `false`.
41 fn is_nan(self) -> bool;
42}
43
44/// Implements primitive numeric conversion and non-NaN behavior for integers.
45macro_rules! impl_numeric_value_for_integer {
46 ($($numeric_type:ty),+ $(,)?) => {
47 $(
48 impl NumericValue for $numeric_type {
49 /// Returns integer zero.
50 #[inline]
51 fn zero() -> Self {
52 0
53 }
54
55 /// Captures the integer without losing its value.
56 #[inline]
57 fn to_argument_value(self) -> ArgumentValue {
58 ArgumentValue::from(self)
59 }
60
61 /// Reports that an integer can never be NaN.
62 #[inline]
63 fn is_nan(self) -> bool {
64 false
65 }
66 }
67 )+
68 };
69}
70
71impl_numeric_value_for_integer!(i8, i16, i32, i64, i128, isize);
72impl_numeric_value_for_integer!(u8, u16, u32, u64, u128, usize);
73
74impl NumericValue for f32 {
75 /// Returns positive floating-point zero.
76 #[inline]
77 fn zero() -> Self {
78 0.0
79 }
80
81 /// Captures the exact IEEE 754 bit pattern of this value.
82 #[inline]
83 fn to_argument_value(self) -> ArgumentValue {
84 ArgumentValue::from(self)
85 }
86
87 /// Returns whether this value is NaN.
88 #[inline]
89 fn is_nan(self) -> bool {
90 self.is_nan()
91 }
92}
93
94impl NumericValue for f64 {
95 /// Returns positive floating-point zero.
96 #[inline]
97 fn zero() -> Self {
98 0.0
99 }
100
101 /// Captures the exact IEEE 754 bit pattern of this value.
102 #[inline]
103 fn to_argument_value(self) -> ArgumentValue {
104 ArgumentValue::from(self)
105 }
106
107 /// Returns whether this value is NaN.
108 #[inline]
109 fn is_nan(self) -> bool {
110 self.is_nan()
111 }
112}
113
114/// Validates primitive numeric arguments while preserving their values.
115///
116/// Every successful method returns the original value without conversion or
117/// normalization. Failures contain structured comparison or range data, and
118/// every method rejects floating-point NaN values with
119/// [`ArgumentErrorKind::NotANumber`].
120///
121/// The trait is sealed and implemented only for primitive numeric types.
122pub trait NumericArgument: Sealed + Sized {
123 /// Requires this value to equal zero.
124 ///
125 /// Success returns the original value without cloning. A nonzero value
126 /// returns [`ArgumentErrorKind::Comparison`] with an `EqualTo(0)`
127 /// constraint at `path`; NaN returns [`ArgumentErrorKind::NotANumber`].
128 fn require_zero(self, path: &str) -> ArgumentResult<Self>;
129
130 /// Requires this value not to equal zero.
131 ///
132 /// Success returns the original value without cloning. Zero returns
133 /// [`ArgumentErrorKind::Comparison`] with a `NotEqualTo(0)` constraint at
134 /// `path`; NaN returns [`ArgumentErrorKind::NotANumber`].
135 fn require_non_zero(self, path: &str) -> ArgumentResult<Self>;
136
137 /// Requires this value to be strictly greater than zero.
138 ///
139 /// Success returns the original value without cloning. A value that is not
140 /// positive returns [`ArgumentErrorKind::Comparison`] with a
141 /// `GreaterThan(0)` constraint at `path`; NaN returns
142 /// [`ArgumentErrorKind::NotANumber`].
143 fn require_positive(self, path: &str) -> ArgumentResult<Self>;
144
145 /// Requires this value to be greater than or equal to zero.
146 ///
147 /// Success returns the original value without cloning. A negative value
148 /// returns [`ArgumentErrorKind::Comparison`] with an `AtLeast(0)`
149 /// constraint at `path`; NaN returns [`ArgumentErrorKind::NotANumber`].
150 fn require_non_negative(self, path: &str) -> ArgumentResult<Self>;
151
152 /// Requires this value to be strictly less than zero.
153 ///
154 /// Success returns the original value without cloning. A value that is not
155 /// negative returns [`ArgumentErrorKind::Comparison`] with a `LessThan(0)`
156 /// constraint at `path`; NaN returns [`ArgumentErrorKind::NotANumber`].
157 fn require_negative(self, path: &str) -> ArgumentResult<Self>;
158
159 /// Requires this value to be less than or equal to zero.
160 ///
161 /// Success returns the original value without cloning. A positive value
162 /// returns [`ArgumentErrorKind::Comparison`] with an `AtMost(0)`
163 /// constraint at `path`; NaN returns [`ArgumentErrorKind::NotANumber`].
164 fn require_non_positive(self, path: &str) -> ArgumentResult<Self>;
165
166 /// Requires this value to be strictly less than `bound`.
167 ///
168 /// Success returns the original value without cloning. An unsatisfied
169 /// comparison returns [`ArgumentErrorKind::Comparison`] with a `LessThan`
170 /// constraint at `path`; a NaN value or bound returns
171 /// [`ArgumentErrorKind::NotANumber`].
172 fn require_less_than(self, path: &str, bound: Self)
173 -> ArgumentResult<Self>;
174
175 /// Requires this value to be less than or equal to `bound`.
176 ///
177 /// Success returns the original value without cloning. An unsatisfied
178 /// comparison returns [`ArgumentErrorKind::Comparison`] with an `AtMost`
179 /// constraint at `path`; a NaN value or bound returns
180 /// [`ArgumentErrorKind::NotANumber`].
181 fn require_at_most(self, path: &str, bound: Self) -> ArgumentResult<Self>;
182
183 /// Requires this value to be strictly greater than `bound`.
184 ///
185 /// Success returns the original value without cloning. An unsatisfied
186 /// comparison returns [`ArgumentErrorKind::Comparison`] with a
187 /// `GreaterThan` constraint at `path`; a NaN value or bound returns
188 /// [`ArgumentErrorKind::NotANumber`].
189 fn require_greater_than(
190 self,
191 path: &str,
192 bound: Self,
193 ) -> ArgumentResult<Self>;
194
195 /// Requires this value to be greater than or equal to `bound`.
196 ///
197 /// Success returns the original value without cloning. An unsatisfied
198 /// comparison returns [`ArgumentErrorKind::Comparison`] with an `AtLeast`
199 /// constraint at `path`; a NaN value or bound returns
200 /// [`ArgumentErrorKind::NotANumber`].
201 fn require_at_least(self, path: &str, bound: Self) -> ArgumentResult<Self>;
202
203 /// Requires this value to lie within `range`.
204 ///
205 /// Standard inclusive, exclusive, and unbounded [`RangeBounds`] are
206 /// supported. The range structure is validated before this value: reversed
207 /// endpoints and equal endpoints with either endpoint excluded return
208 /// [`ArgumentErrorKind::InvalidRangeConstraint`]. NaN endpoints or values
209 /// return [`ArgumentErrorKind::NotANumber`].
210 /// An otherwise out-of-range value returns [`ArgumentErrorKind::Range`].
211 /// The range's start and end accessors are each called exactly once, and
212 /// all checks use that owned endpoint snapshot.
213 /// Successful validation returns the original value without cloning.
214 fn require_in_range<R>(self, path: &str, range: R) -> ArgumentResult<Self>
215 where
216 R: RangeBounds<Self>;
217}
218
219impl<T> NumericArgument for T
220where
221 T: NumericValue,
222{
223 /// Requires equality with zero and preserves the original value.
224 #[inline]
225 fn require_zero(self, path: &str) -> ArgumentResult<Self> {
226 let zero = T::zero();
227 validate_comparison(
228 self,
229 path,
230 zero,
231 ComparisonConstraint::EqualTo(zero.to_argument_value()),
232 |actual, bound| actual == bound,
233 )
234 }
235
236 /// Requires inequality with zero and preserves the original value.
237 #[inline]
238 fn require_non_zero(self, path: &str) -> ArgumentResult<Self> {
239 let zero = T::zero();
240 validate_comparison(
241 self,
242 path,
243 zero,
244 ComparisonConstraint::NotEqualTo(zero.to_argument_value()),
245 |actual, bound| actual != bound,
246 )
247 }
248
249 /// Requires a value strictly greater than zero.
250 #[inline]
251 fn require_positive(self, path: &str) -> ArgumentResult<Self> {
252 let zero = T::zero();
253 validate_comparison(
254 self,
255 path,
256 zero,
257 ComparisonConstraint::GreaterThan(zero.to_argument_value()),
258 |actual, bound| actual > bound,
259 )
260 }
261
262 /// Requires a value greater than or equal to zero.
263 #[inline]
264 fn require_non_negative(self, path: &str) -> ArgumentResult<Self> {
265 let zero = T::zero();
266 validate_comparison(
267 self,
268 path,
269 zero,
270 ComparisonConstraint::AtLeast(zero.to_argument_value()),
271 |actual, bound| actual >= bound,
272 )
273 }
274
275 /// Requires a value strictly less than zero.
276 #[inline]
277 fn require_negative(self, path: &str) -> ArgumentResult<Self> {
278 let zero = T::zero();
279 validate_comparison(
280 self,
281 path,
282 zero,
283 ComparisonConstraint::LessThan(zero.to_argument_value()),
284 |actual, bound| actual < bound,
285 )
286 }
287
288 /// Requires a value less than or equal to zero.
289 #[inline]
290 fn require_non_positive(self, path: &str) -> ArgumentResult<Self> {
291 let zero = T::zero();
292 validate_comparison(
293 self,
294 path,
295 zero,
296 ComparisonConstraint::AtMost(zero.to_argument_value()),
297 |actual, bound| actual <= bound,
298 )
299 }
300
301 /// Requires a value strictly less than the supplied bound.
302 #[inline]
303 fn require_less_than(
304 self,
305 path: &str,
306 bound: Self,
307 ) -> ArgumentResult<Self> {
308 validate_comparison(
309 self,
310 path,
311 bound,
312 ComparisonConstraint::LessThan(bound.to_argument_value()),
313 |actual, bound| actual < bound,
314 )
315 }
316
317 /// Requires a value less than or equal to the supplied bound.
318 #[inline]
319 fn require_at_most(self, path: &str, bound: Self) -> ArgumentResult<Self> {
320 validate_comparison(
321 self,
322 path,
323 bound,
324 ComparisonConstraint::AtMost(bound.to_argument_value()),
325 |actual, bound| actual <= bound,
326 )
327 }
328
329 /// Requires a value strictly greater than the supplied bound.
330 #[inline]
331 fn require_greater_than(
332 self,
333 path: &str,
334 bound: Self,
335 ) -> ArgumentResult<Self> {
336 validate_comparison(
337 self,
338 path,
339 bound,
340 ComparisonConstraint::GreaterThan(bound.to_argument_value()),
341 |actual, bound| actual > bound,
342 )
343 }
344
345 /// Requires a value greater than or equal to the supplied bound.
346 #[inline]
347 fn require_at_least(self, path: &str, bound: Self) -> ArgumentResult<Self> {
348 validate_comparison(
349 self,
350 path,
351 bound,
352 ComparisonConstraint::AtLeast(bound.to_argument_value()),
353 |actual, bound| actual >= bound,
354 )
355 }
356
357 /// Validates the range structure before checking and returning this value.
358 #[inline]
359 fn require_in_range<R>(self, path: &str, range: R) -> ArgumentResult<Self>
360 where
361 R: RangeBounds<Self>,
362 {
363 let (lower_bound, upper_bound) = snapshot_range_bounds(&range);
364 let constraint = capture_range_constraint(lower_bound, upper_bound);
365 validate_range_structure(path, lower_bound, upper_bound, &constraint)?;
366 validate_not_nan(path, self)?;
367 if range_contains(lower_bound, upper_bound, self) {
368 Ok(self)
369 } else {
370 Err(ArgumentError::new(
371 path,
372 ArgumentErrorKind::Range {
373 actual: self.to_argument_value(),
374 constraint,
375 },
376 ))
377 }
378 }
379}
380
381/// Rejects a NaN numeric value or constraint at the supplied argument path.
382///
383/// `value` is inspected without normalization. Integer values always succeed;
384/// floating-point NaN values return `ArgumentErrorKind::NotANumber` at `path`.
385fn validate_not_nan<T>(path: &str, value: T) -> ArgumentResult<()>
386where
387 T: NumericValue,
388{
389 if value.is_nan() {
390 Err(ArgumentError::new(path, ArgumentErrorKind::NotANumber))
391 } else {
392 Ok(())
393 }
394}
395
396/// Applies one comparison and returns the unchanged numeric value on success.
397///
398/// `actual` and `bound` are checked for NaN before `predicate` is evaluated.
399/// If the predicate returns `false`, the error records `actual` and the exact
400/// supplied `constraint` at `path`.
401fn validate_comparison<T, F>(
402 actual: T,
403 path: &str,
404 bound: T,
405 constraint: ComparisonConstraint,
406 predicate: F,
407) -> ArgumentResult<T>
408where
409 T: NumericValue,
410 F: FnOnce(T, T) -> bool,
411{
412 validate_not_nan(path, actual)?;
413 validate_not_nan(path, bound)?;
414 if predicate(actual, bound) {
415 Ok(actual)
416 } else {
417 Err(ArgumentError::new(
418 path,
419 ArgumentErrorKind::Comparison {
420 actual: actual.to_argument_value(),
421 constraint,
422 },
423 ))
424 }
425}
426
427/// Copies a borrowed standard-library bound into an owned bound.
428///
429/// Included and excluded endpoints are copied exactly; an unbounded endpoint
430/// remains unbounded.
431fn copy_range_bound<T>(bound: Bound<&T>) -> Bound<T>
432where
433 T: NumericValue,
434{
435 match bound {
436 Bound::Unbounded => Bound::Unbounded,
437 Bound::Included(value) => Bound::Included(*value),
438 Bound::Excluded(value) => Bound::Excluded(*value),
439 }
440}
441
442/// Reads each endpoint from `range` once and returns an owned snapshot.
443///
444/// The lower and upper endpoint accessors are each invoked exactly once. The
445/// returned values are reused for constraint construction, validation, and
446/// membership checks.
447fn snapshot_range_bounds<T, R>(range: &R) -> (Bound<T>, Bound<T>)
448where
449 T: NumericValue,
450 R: RangeBounds<T>,
451{
452 let lower_bound = copy_range_bound(range.start_bound());
453 let upper_bound = copy_range_bound(range.end_bound());
454 (lower_bound, upper_bound)
455}
456
457/// Captures an owned standard-library bound as a structured argument bound.
458///
459/// Included and excluded endpoints are converted without losing numeric bits;
460/// an unbounded endpoint remains unbounded.
461fn capture_argument_bound<T>(bound: Bound<T>) -> ArgumentBound
462where
463 T: NumericValue,
464{
465 match bound {
466 Bound::Unbounded => ArgumentBound::Unbounded,
467 Bound::Included(value) => {
468 ArgumentBound::Included(value.to_argument_value())
469 }
470 Bound::Excluded(value) => {
471 ArgumentBound::Excluded(value.to_argument_value())
472 }
473 }
474}
475
476/// Captures both owned endpoints without validating their relationship.
477///
478/// The returned constraint preserves inclusive, exclusive, unbounded, and
479/// floating-point bit-pattern details exactly.
480fn capture_range_constraint<T>(
481 lower_bound: Bound<T>,
482 upper_bound: Bound<T>,
483) -> RangeConstraint
484where
485 T: NumericValue,
486{
487 RangeConstraint::new(
488 capture_argument_bound(lower_bound),
489 capture_argument_bound(upper_bound),
490 )
491}
492
493/// Validates that two snapshotted bounds form a non-empty numeric interval.
494///
495/// Endpoint NaNs return `NotANumber`. Reversed endpoints, or equal endpoints
496/// where either bound is excluded, return `InvalidRangeConstraint` containing
497/// a clone of `constraint`. Unbounded sides require no ordering comparison.
498fn validate_range_structure<T>(
499 path: &str,
500 lower_bound: Bound<T>,
501 upper_bound: Bound<T>,
502 constraint: &RangeConstraint,
503) -> ArgumentResult<()>
504where
505 T: NumericValue,
506{
507 validate_range_bound_not_nan(path, lower_bound)?;
508 validate_range_bound_not_nan(path, upper_bound)?;
509
510 let is_valid = match (lower_bound, upper_bound) {
511 (Bound::Unbounded, _) | (_, Bound::Unbounded) => return Ok(()),
512 (Bound::Included(lower), Bound::Included(upper)) => lower <= upper,
513 (Bound::Included(lower), Bound::Excluded(upper))
514 | (Bound::Excluded(lower), Bound::Included(upper))
515 | (Bound::Excluded(lower), Bound::Excluded(upper)) => lower < upper,
516 };
517 if is_valid {
518 Ok(())
519 } else {
520 Err(ArgumentError::new(
521 path,
522 ArgumentErrorKind::InvalidRangeConstraint {
523 constraint: constraint.clone(),
524 },
525 ))
526 }
527}
528
529/// Rejects a NaN endpoint while accepting unbounded and ordinary endpoints.
530///
531/// A NaN included or excluded endpoint returns `NotANumber` at `path`.
532fn validate_range_bound_not_nan<T>(
533 path: &str,
534 bound: Bound<T>,
535) -> ArgumentResult<()>
536where
537 T: NumericValue,
538{
539 match bound {
540 Bound::Included(value) | Bound::Excluded(value) => {
541 validate_not_nan(path, value)
542 }
543 Bound::Unbounded => Ok(()),
544 }
545}
546
547/// Returns whether `actual` satisfies two snapshotted bounds.
548///
549/// This helper assumes the bounds and all values were already checked for NaN.
550/// It uses comparisons only and performs no endpoint arithmetic.
551fn range_contains<T>(
552 lower_bound: Bound<T>,
553 upper_bound: Bound<T>,
554 actual: T,
555) -> bool
556where
557 T: NumericValue,
558{
559 let satisfies_lower = match lower_bound {
560 Bound::Unbounded => true,
561 Bound::Included(lower) => actual >= lower,
562 Bound::Excluded(lower) => actual > lower,
563 };
564 let satisfies_upper = match upper_bound {
565 Bound::Unbounded => true,
566 Bound::Included(upper) => actual <= upper,
567 Bound::Excluded(upper) => actual < upper,
568 };
569 satisfies_lower && satisfies_upper
570}