Skip to main content

hegel/generators/
numeric.rs

1use super::{BasicGenerator, Generator};
2use crate::cbor_utils::{cbor_map, cbor_serialize, map_insert};
3use ciborium::Value;
4use std::marker::PhantomData;
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}
13
14macro_rules! impl_integer_type {
15    ($($t:ty),*) => { $(
16        impl Integer for $t {
17            const MIN: Self = <$t>::MIN;
18            const MAX: Self = <$t>::MAX;
19        }
20    )* };
21}
22
23impl_integer_type!(
24    i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize
25);
26
27/// Trait bound for float types usable with [`floats()`].
28pub trait Float: Copy + PartialOrd {
29    /// The minimum value of this type.
30    const MIN: Self;
31    /// The maximum value of this type.
32    const MAX: Self;
33    /// Widen to f64 for cross-width comparisons (bound validation).
34    fn to_f64(self) -> f64;
35}
36
37impl Float for f32 {
38    const MIN: Self = f32::MIN;
39    const MAX: Self = f32::MAX;
40    fn to_f64(self) -> f64 {
41        self as f64
42    }
43}
44
45impl Float for f64 {
46    const MIN: Self = f64::MIN;
47    const MAX: Self = f64::MAX;
48    fn to_f64(self) -> f64 {
49        self
50    }
51}
52
53/// Less-than-or-equal under sign-aware ordering, where `-0.0 < +0.0`.
54///
55/// IEEE 754 considers `+0.0` and `-0.0` equal under `<=`, but Hypothesis and
56/// the native backend treat `-0.0` as strictly less than `+0.0`. Mirrors
57/// `sign_aware_lte` in hypothesis (`strategies/_internal/numbers.py`) and
58/// `native/core/choices.rs`.
59pub(crate) fn sign_aware_lte<T: Float>(a: T, b: T) -> bool {
60    let a = a.to_f64();
61    let b = b.to_f64();
62    if a == 0.0 && b == 0.0 {
63        a.is_sign_negative() || b.is_sign_positive()
64    } else {
65        a <= b
66    }
67}
68
69/// Generator for integer values. Created by [`integers()`].
70///
71/// Bounds default to the type's full range.
72pub struct IntegerGenerator<T> {
73    min: Option<T>,
74    max: Option<T>,
75    _phantom: PhantomData<T>,
76}
77
78impl<T> IntegerGenerator<T> {
79    /// Set the minimum value (inclusive).
80    pub fn min_value(mut self, min_value: T) -> Self {
81        self.min = Some(min_value);
82        self
83    }
84
85    /// Set the maximum value (inclusive).
86    pub fn max_value(mut self, max_value: T) -> Self {
87        self.max = Some(max_value);
88        self
89    }
90}
91
92impl<T: Integer + serde::Serialize> IntegerGenerator<T> {
93    fn build_schema(&self) -> Value {
94        let min = self.min.unwrap_or(T::MIN);
95        let max = self.max.unwrap_or(T::MAX);
96        assert!(min <= max, "Cannot have max_value < min_value");
97
98        cbor_map! {
99            "type" => "integer",
100            "min_value" => cbor_serialize(&min),
101            "max_value" => cbor_serialize(&max)
102        }
103    }
104}
105
106impl<T: Integer + serde::de::DeserializeOwned + serde::Serialize + Send + Sync + 'static>
107    Generator<T> for IntegerGenerator<T>
108{
109    fn as_basic(&self) -> Option<BasicGenerator<'_, T>> {
110        Some(BasicGenerator::new(self.build_schema(), |raw| {
111            super::deserialize_value(raw)
112        }))
113    }
114}
115
116/// Generate integers of type `T`.
117///
118/// Bounds default to the full range of `T`. Use the builder methods `min_value`
119/// and `max_value` to constrain the range. See [`IntegerGenerator`] for more
120/// details.
121pub fn integers<
122    T: Integer + serde::de::DeserializeOwned + serde::Serialize + Send + Sync + 'static,
123>() -> IntegerGenerator<T> {
124    IntegerGenerator {
125        min: None,
126        max: None,
127        _phantom: PhantomData,
128    }
129}
130
131/// Generator for floating-point values. Created by [`floats()`].
132///
133/// By default, may produce NaN and infinity when no bounds are set.
134/// Setting bounds automatically disables these unless re-enabled.
135pub struct FloatGenerator<T> {
136    min: Option<T>,
137    max: Option<T>,
138    exclude_min: bool,
139    exclude_max: bool,
140    allow_nan: Option<bool>,
141    allow_infinity: Option<bool>,
142}
143
144impl<T> FloatGenerator<T> {
145    /// Set the minimum value (inclusive by default).
146    pub fn min_value(mut self, min_value: T) -> Self {
147        self.min = Some(min_value);
148        self
149    }
150
151    /// Set the maximum value (inclusive by default).
152    pub fn max_value(mut self, max_value: T) -> Self {
153        self.max = Some(max_value);
154        self
155    }
156
157    /// Set whether to exclude the minimum value from the range.
158    pub fn exclude_min(mut self, exclude_min: bool) -> Self {
159        self.exclude_min = exclude_min;
160        self
161    }
162
163    /// Set whether to exclude the maximum value from the range.
164    pub fn exclude_max(mut self, exclude_max: bool) -> Self {
165        self.exclude_max = exclude_max;
166        self
167    }
168
169    /// Whether NaN values are allowed. Cannot be used with bounds.
170    pub fn allow_nan(mut self, allow: bool) -> Self {
171        self.allow_nan = Some(allow);
172        self
173    }
174
175    /// Whether infinite values are allowed. Cannot be used with both bounds set.
176    pub fn allow_infinity(mut self, allow: bool) -> Self {
177        self.allow_infinity = Some(allow);
178        self
179    }
180}
181
182impl<T: Float + serde::Serialize> FloatGenerator<T> {
183    fn build_schema(&self) -> Value {
184        let width = (std::mem::size_of::<T>() * 8) as u64;
185        let has_min = self.min.is_some();
186        let has_max = self.max.is_some();
187
188        if let (Some(min), Some(max)) = (self.min, self.max) {
189            assert!(min <= max, "Cannot have max_value < min_value");
190            // Reject the sign-aware-empty range min=+0.0, max=-0.0: the
191            // backends treat -0.0 < +0.0, so this range contains no floats.
192            if !sign_aware_lte(min, max) {
193                panic!(
194                    "InvalidArgument: There are no {width}-bit floating-point \
195                     values between min_value=0.0 and max_value=-0.0"
196                );
197            }
198            // After exclude_min/exclude_max, the closed-open / open-closed /
199            // open-open ranges over [min, min] (and the `-0.0`/`0.0` pair
200            // that compares equal under sign-aware ordering) are empty.
201            let min_f = min.to_f64();
202            let max_f = max.to_f64();
203            let zero_pair = min_f == 0.0 && max_f == 0.0;
204            if (min_f == max_f || zero_pair) && (self.exclude_min || self.exclude_max) {
205                panic!(
206                    "InvalidArgument: exclude_min/exclude_max leave no \
207                     {width}-bit floating-point values in [{min_f}, {max_f}]"
208                );
209            }
210        }
211
212        // exclude_min=true with min_value=+inf (or exclude_max=true with
213        // max_value=-inf) demands the next representable value beyond an
214        // unbounded endpoint, which doesn't exist.
215        if self.exclude_min && self.min.is_some_and(|v| v.to_f64() == f64::INFINITY) {
216            panic!(
217                "InvalidArgument: exclude_min=true with min_value=+inf leaves \
218                 no {width}-bit floating-point values"
219            );
220        }
221        if self.exclude_max && self.max.is_some_and(|v| v.to_f64() == f64::NEG_INFINITY) {
222            panic!(
223                "InvalidArgument: exclude_max=true with max_value=-inf leaves \
224                 no {width}-bit floating-point values"
225            );
226        }
227
228        let allow_nan = self.allow_nan.unwrap_or(!has_min && !has_max);
229        let allow_infinity = self.allow_infinity.unwrap_or(!has_min || !has_max);
230
231        if allow_nan && (has_min || has_max) {
232            panic!("Cannot have allow_nan=true with min_value or max_value");
233        }
234        if allow_infinity && has_min && has_max {
235            panic!("Cannot have allow_infinity=true with both min_value and max_value");
236        }
237
238        let mut schema = cbor_map! {
239            "type" => "float",
240            "exclude_min" => self.exclude_min,
241            "exclude_max" => self.exclude_max,
242            "allow_nan" => allow_nan,
243            "allow_infinity" => allow_infinity,
244            "width" => width
245        };
246
247        if let Some(ref min) = self.min {
248            map_insert(&mut schema, "min_value", cbor_serialize(min));
249        }
250        if let Some(ref max) = self.max {
251            map_insert(&mut schema, "max_value", cbor_serialize(max));
252        }
253
254        // When generating finite values without explicit bounds, add type
255        // bounds to prevent overflow during deserialization (the protocol
256        // uses f64, so f32 values near MAX can overflow when round-tripped)
257        if !allow_nan && !allow_infinity {
258            if self.min.is_none() {
259                map_insert(&mut schema, "min_value", cbor_serialize(&T::MIN));
260            }
261            if self.max.is_none() {
262                map_insert(&mut schema, "max_value", cbor_serialize(&T::MAX));
263            }
264        }
265
266        schema
267    }
268}
269
270impl<T: Float + serde::de::DeserializeOwned + serde::Serialize + Send + Sync + 'static> Generator<T>
271    for FloatGenerator<T>
272{
273    fn as_basic(&self) -> Option<BasicGenerator<'_, T>> {
274        Some(BasicGenerator::new(self.build_schema(), |raw| {
275            super::deserialize_value(raw)
276        }))
277    }
278}
279
280/// Generate floating-point values of type `T`.
281/// Use the builder methods `min_value`, `max_value`, `allow_nan`, and
282/// `allow_infinity` to constrain the output. By default, may produce NaN and
283/// infinity. See [`FloatGenerator`] for more details.
284///
285/// # Example
286///
287/// ```no_run
288/// use hegel::generators as gs;
289///
290/// #[hegel::test]
291/// fn my_test(tc: hegel::TestCase) {
292///     let x: f64 = tc.draw(gs::floats()
293///         .min_value(0.0)
294///         .max_value(1.0));
295///     assert!((0.0..=1.0).contains(&x));
296/// }
297/// ```
298pub fn floats<T: Float + serde::de::DeserializeOwned + serde::Serialize + Send + Sync + 'static>()
299-> FloatGenerator<T> {
300    FloatGenerator {
301        min: None,
302        max: None,
303        exclude_min: false,
304        exclude_max: false,
305        allow_nan: None,
306        allow_infinity: None,
307    }
308}
309
310#[cfg(test)]
311#[path = "../../tests/embedded/generators/numeric_tests.rs"]
312mod tests;