Skip to main content

hegel/generators/
numeric.rs

1use super::{Generator, TestCase};
2use crate::test_case::invalid_argument;
3use std::marker::PhantomData;
4use std::sync::OnceLock;
5
6/// Trait bound for integer types usable with [`integers()`].
7pub trait Integer: Copy + Ord {
8    /// The minimum value of this type.
9    const MIN: Self;
10    /// The maximum value of this type.
11    const MAX: Self;
12    /// This value as an `i64`, when it fits.
13    #[doc(hidden)]
14    fn to_i64_checked(self) -> Option<i64>;
15    /// This value's two's-complement little-endian encoding, sign-extended
16    /// to 17 bytes (wide enough for any `i128` or `u128`).
17    #[doc(hidden)]
18    fn to_le17(self) -> [u8; 17];
19    /// Decode a 17-byte sign-extended two's-complement little-endian
20    /// encoding. The value is guaranteed by the caller to be in this type's
21    /// range.
22    #[doc(hidden)]
23    fn from_le17(bytes: [u8; 17]) -> Self;
24    /// Convert from an `i64` guaranteed by the caller to be in this type's
25    /// range.
26    #[doc(hidden)]
27    fn from_i64(v: i64) -> Self;
28}
29
30macro_rules! impl_signed_integer_type {
31    ($($t:ty),*) => { $(
32        impl Integer for $t {
33            const MIN: Self = <$t>::MIN;
34            const MAX: Self = <$t>::MAX;
35            fn to_i64_checked(self) -> Option<i64> {
36                i64::try_from(self).ok()
37            }
38            fn to_le17(self) -> [u8; 17] {
39                let v = self as i128;
40                let fill = if v < 0 { 0xFF } else { 0x00 };
41                let mut bytes = [fill; 17];
42                bytes[..16].copy_from_slice(&v.to_le_bytes());
43                bytes
44            }
45            fn from_le17(bytes: [u8; 17]) -> Self {
46                i128::from_le_bytes(bytes[..16].try_into().unwrap()) as $t
47            }
48            fn from_i64(v: i64) -> Self {
49                v as $t
50            }
51        }
52    )* };
53}
54
55macro_rules! impl_unsigned_integer_type {
56    ($($t:ty),*) => { $(
57        impl Integer for $t {
58            const MIN: Self = <$t>::MIN;
59            const MAX: Self = <$t>::MAX;
60            fn to_i64_checked(self) -> Option<i64> {
61                i64::try_from(self).ok()
62            }
63            fn to_le17(self) -> [u8; 17] {
64                let v = self as u128;
65                let mut bytes = [0u8; 17];
66                bytes[..16].copy_from_slice(&v.to_le_bytes());
67                bytes
68            }
69            fn from_le17(bytes: [u8; 17]) -> Self {
70                u128::from_le_bytes(bytes[..16].try_into().unwrap()) as $t
71            }
72            fn from_i64(v: i64) -> Self {
73                v as $t
74            }
75        }
76    )* };
77}
78
79impl_signed_integer_type!(i8, i16, i32, i64, i128, isize);
80impl_unsigned_integer_type!(u8, u16, u32, u64, u128, usize);
81
82/// Trait bound for float types usable with [`floats()`].
83pub trait Float: Copy + PartialOrd {
84    /// The minimum value of this type.
85    const MIN: Self;
86    /// The maximum value of this type.
87    const MAX: Self;
88    /// Widen to f64 for cross-width comparisons (bound validation).
89    fn to_f64(self) -> f64;
90    /// Narrow from the engine's f64 result. The value is guaranteed to be
91    /// exactly representable at this type's width.
92    #[doc(hidden)]
93    fn from_f64(v: f64) -> Self;
94}
95
96impl Float for f32 {
97    const MIN: Self = f32::MIN;
98    const MAX: Self = f32::MAX;
99    fn to_f64(self) -> f64 {
100        self as f64
101    }
102    fn from_f64(v: f64) -> Self {
103        v as f32
104    }
105}
106
107impl Float for f64 {
108    const MIN: Self = f64::MIN;
109    const MAX: Self = f64::MAX;
110    fn to_f64(self) -> f64 {
111        self
112    }
113    fn from_f64(v: f64) -> Self {
114        v
115    }
116}
117
118/// Less-than-or-equal under sign-aware ordering, where `-0.0 < +0.0`.
119///
120/// IEEE 754 considers `+0.0` and `-0.0` equal under `<=`, but Hypothesis and
121/// the native backend treat `-0.0` as strictly less than `+0.0`. Mirrors
122/// `sign_aware_lte` in hypothesis (`strategies/_internal/numbers.py`) and
123/// `native/core/choices.rs`.
124pub(crate) fn sign_aware_lte<T: Float>(a: T, b: T) -> bool {
125    let a = a.to_f64();
126    let b = b.to_f64();
127    if a == 0.0 && b == 0.0 {
128        a.is_sign_negative() || b.is_sign_positive()
129    } else {
130        a <= b
131    }
132}
133
134/// Generator for integer values. Created by [`integers()`].
135///
136/// Bounds default to the type's full range.
137pub struct IntegerGenerator<T> {
138    min: Option<T>,
139    max: Option<T>,
140    _phantom: PhantomData<T>,
141}
142
143impl<T> IntegerGenerator<T> {
144    /// Set the minimum value (inclusive).
145    pub fn min_value(mut self, min_value: T) -> Self {
146        self.min = Some(min_value);
147        self
148    }
149
150    /// Set the maximum value (inclusive).
151    pub fn max_value(mut self, max_value: T) -> Self {
152        self.max = Some(max_value);
153        self
154    }
155}
156
157impl<T: Integer> Generator<T> for IntegerGenerator<T> {
158    fn do_draw(&self, tc: &TestCase) -> T {
159        let min = self.min.unwrap_or(T::MIN);
160        let max = self.max.unwrap_or(T::MAX);
161        if min > max {
162            invalid_argument!("Cannot have max_value < min_value");
163        }
164        match (min.to_i64_checked(), max.to_i64_checked()) {
165            (Some(lo), Some(hi)) => T::from_i64(tc.generate_integer_i64(lo, hi)),
166            _ => T::from_le17(tc.generate_integer_le17(&min.to_le17(), &max.to_le17())),
167        }
168    }
169}
170
171/// Generate integers of type `T`.
172///
173/// Bounds default to the full range of `T`. Use the builder methods `min_value`
174/// and `max_value` to constrain the range. See [`IntegerGenerator`] for more
175/// details.
176pub fn integers<T: Integer>() -> IntegerGenerator<T> {
177    IntegerGenerator {
178        min: None,
179        max: None,
180        _phantom: PhantomData,
181    }
182}
183
184/// Generator for floating-point values. Created by [`floats()`].
185///
186/// By default, may produce NaN and infinity when no bounds are set.
187/// Setting bounds automatically disables these unless re-enabled.
188pub struct FloatGenerator<T> {
189    min: Option<T>,
190    max: Option<T>,
191    exclude_min: bool,
192    exclude_max: bool,
193    allow_nan: Option<bool>,
194    allow_infinity: Option<bool>,
195    allow_subnormal: Option<bool>,
196    params: OnceLock<FloatDrawParams>,
197}
198
199impl<T> FloatGenerator<T> {
200    /// Set the minimum value (inclusive by default).
201    pub fn min_value(mut self, min_value: T) -> Self {
202        self.min = Some(min_value);
203        self.params = OnceLock::new();
204        self
205    }
206
207    /// Set the maximum value (inclusive by default).
208    pub fn max_value(mut self, max_value: T) -> Self {
209        self.max = Some(max_value);
210        self.params = OnceLock::new();
211        self
212    }
213
214    /// Set whether to exclude the minimum value from the range.
215    pub fn exclude_min(mut self, exclude_min: bool) -> Self {
216        self.exclude_min = exclude_min;
217        self.params = OnceLock::new();
218        self
219    }
220
221    /// Set whether to exclude the maximum value from the range.
222    pub fn exclude_max(mut self, exclude_max: bool) -> Self {
223        self.exclude_max = exclude_max;
224        self.params = OnceLock::new();
225        self
226    }
227
228    /// Whether NaN values are allowed. Cannot be used with bounds.
229    pub fn allow_nan(mut self, allow: bool) -> Self {
230        self.allow_nan = Some(allow);
231        self.params = OnceLock::new();
232        self
233    }
234
235    /// Whether infinite values are allowed. Cannot be used with both bounds set.
236    pub fn allow_infinity(mut self, allow: bool) -> Self {
237        self.allow_infinity = Some(allow);
238        self.params = OnceLock::new();
239        self
240    }
241
242    /// Whether subnormal ("denormalised") values are allowed. Defaults to
243    /// allowing them whenever the bounds admit any; set to `false` when the
244    /// code under test may run with flush-to-zero floating point (e.g.
245    /// compiled with `-ffast-math`), where subnormal inputs silently become
246    /// zero.
247    pub fn allow_subnormal(mut self, allow: bool) -> Self {
248        self.allow_subnormal = Some(allow);
249        self.params = OnceLock::new();
250        self
251    }
252}
253
254/// The validated parameters of a float draw, in the form
255/// `TestCase::generate_float` accepts.
256struct FloatDrawParams {
257    width: u32,
258    min_value: f64,
259    max_value: f64,
260    allow_nan: bool,
261    allow_infinity: bool,
262    smallest_nonzero_magnitude: f64,
263}
264
265impl<T: Float> FloatGenerator<T> {
266    fn draw_params(&self) -> FloatDrawParams {
267        let width = (std::mem::size_of::<T>() * 8) as u32;
268        let has_min = self.min.is_some();
269        let has_max = self.max.is_some();
270
271        if let (Some(min), Some(max)) = (self.min, self.max) {
272            if !matches!(
273                min.partial_cmp(&max),
274                Some(std::cmp::Ordering::Less | std::cmp::Ordering::Equal)
275            ) {
276                invalid_argument!("Cannot have max_value < min_value");
277            }
278            if !sign_aware_lte(min, max) {
279                invalid_argument!(
280                    "InvalidArgument: There are no {width}-bit floating-point \
281                     values between min_value=0.0 and max_value=-0.0"
282                );
283            }
284            let min_f = min.to_f64();
285            let max_f = max.to_f64();
286            let zero_pair = min_f == 0.0 && max_f == 0.0;
287            if (min_f == max_f || zero_pair) && (self.exclude_min || self.exclude_max) {
288                invalid_argument!(
289                    "InvalidArgument: exclude_min/exclude_max leave no \
290                     {width}-bit floating-point values in [{min_f}, {max_f}]"
291                );
292            }
293        }
294
295        if self.exclude_min && !has_min {
296            invalid_argument!("InvalidArgument: Cannot have exclude_min=true without min_value");
297        }
298        if self.exclude_max && !has_max {
299            invalid_argument!("InvalidArgument: Cannot have exclude_max=true without max_value");
300        }
301
302        if self.exclude_min && self.min.is_some_and(|v| v.to_f64() == f64::INFINITY) {
303            invalid_argument!(
304                "InvalidArgument: exclude_min=true with min_value=+inf leaves \
305                 no {width}-bit floating-point values"
306            );
307        }
308        if self.exclude_max && self.max.is_some_and(|v| v.to_f64() == f64::NEG_INFINITY) {
309            invalid_argument!(
310                "InvalidArgument: exclude_max=true with max_value=-inf leaves \
311                 no {width}-bit floating-point values"
312            );
313        }
314
315        let allow_nan = self.allow_nan.unwrap_or(!has_min && !has_max);
316        let allow_infinity = self.allow_infinity.unwrap_or(!has_min || !has_max);
317
318        if allow_nan && (has_min || has_max) {
319            invalid_argument!("Cannot have allow_nan=true with min_value or max_value");
320        }
321        if allow_infinity && has_min && has_max {
322            invalid_argument!("Cannot have allow_infinity=true with both min_value and max_value");
323        }
324
325        let smallest_normal = if width == 32 {
326            f32::MIN_POSITIVE as f64
327        } else {
328            f64::MIN_POSITIVE
329        };
330        let min_f = self.min.map(|v| v.to_f64());
331        let max_f = self.max.map(|v| v.to_f64());
332        let allow_subnormal = self.allow_subnormal.unwrap_or(match (min_f, max_f) {
333            (Some(lo), Some(hi)) if lo == hi => -smallest_normal < lo && lo < smallest_normal,
334            (Some(lo), Some(hi)) => lo < smallest_normal && hi > -smallest_normal,
335            (Some(lo), None) => lo < smallest_normal,
336            (None, Some(hi)) => hi > -smallest_normal,
337            (None, None) => true,
338        });
339        if allow_subnormal {
340            if min_f.is_some_and(|lo| lo >= smallest_normal) {
341                invalid_argument!(
342                    "InvalidArgument: allow_subnormal=true, but min_value excludes \
343                     all values below the smallest positive normal {smallest_normal}"
344                );
345            }
346            if max_f.is_some_and(|hi| hi <= -smallest_normal) {
347                invalid_argument!(
348                    "InvalidArgument: allow_subnormal=true, but max_value excludes \
349                     all values above the smallest negative normal -{smallest_normal}"
350                );
351            }
352        } else if let (Some(lo), Some(hi)) = (min_f, max_f) {
353            let contains_zero = lo <= 0.0 && hi >= 0.0;
354            if !contains_zero && hi < smallest_normal && lo > -smallest_normal {
355                invalid_argument!(
356                    "InvalidArgument: allow_subnormal=false leaves no {width}-bit \
357                     floating-point values in [{lo}, {hi}]"
358                );
359            }
360        }
361
362        let bounded_default = !allow_nan && !allow_infinity;
363        let min_value = match min_f {
364            Some(lo) => lo,
365            None if bounded_default => T::MIN.to_f64(),
366            None => f64::NEG_INFINITY,
367        };
368        let max_value = match max_f {
369            Some(hi) => hi,
370            None if bounded_default => T::MAX.to_f64(),
371            None => f64::INFINITY,
372        };
373
374        let smallest_nonzero_magnitude = if allow_subnormal {
375            if width == 32 {
376                f64::from(f32::from_bits(1))
377            } else {
378                f64::from_bits(1)
379            }
380        } else {
381            smallest_normal
382        };
383
384        FloatDrawParams {
385            width,
386            min_value,
387            max_value,
388            allow_nan,
389            allow_infinity,
390            smallest_nonzero_magnitude,
391        }
392    }
393}
394
395impl<T: Float> Generator<T> for FloatGenerator<T> {
396    fn do_draw(&self, tc: &TestCase) -> T {
397        let params = self.params.get_or_init(|| self.draw_params());
398        let v = tc.generate_float(
399            params.width,
400            params.min_value,
401            params.max_value,
402            params.allow_nan,
403            params.allow_infinity,
404            self.exclude_min,
405            self.exclude_max,
406            params.smallest_nonzero_magnitude,
407        );
408        T::from_f64(v)
409    }
410}
411
412/// Generate floating-point values of type `T`.
413/// Use the builder methods `min_value`, `max_value`, `allow_nan`, and
414/// `allow_infinity` to constrain the output. By default, may produce NaN and
415/// infinity. See [`FloatGenerator`] for more details.
416///
417/// # Example
418///
419/// ```no_run
420/// use hegel::generators as gs;
421///
422/// #[hegel::test]
423/// fn my_test(tc: hegel::TestCase) {
424///     let x: f64 = tc.draw(gs::floats()
425///         .min_value(0.0)
426///         .max_value(1.0));
427///     assert!((0.0..=1.0).contains(&x));
428/// }
429/// ```
430pub fn floats<T: Float>() -> FloatGenerator<T> {
431    FloatGenerator {
432        min: None,
433        max: None,
434        exclude_min: false,
435        exclude_max: false,
436        allow_nan: None,
437        allow_infinity: None,
438        allow_subnormal: None,
439        params: OnceLock::new(),
440    }
441}
442
443#[cfg(test)]
444#[path = "../../tests/embedded/generators/numeric_tests.rs"]
445mod tests;