Skip to main content

const_num_traits/ops/
float_ops.rs

1//! Float atoms outside the big verbatim-upstream `Float`/`FloatCore`
2//! bundles: bit-pattern access, ULP stepping, IEEE 754-2019 min/max,
3//! round-half-to-even, the `algebraic_*` fast-math family and the
4//! libm-backed special functions.
5//!
6//! This is the first slice of the planned `Float` decomposition:
7//! everything here that touches only bits and comparisons is a
8//! `c0nst` trait with const impls on nightly — unlike the transcendental
9//! bundle, which waits on std/libm.
10//!
11//! **CT tiers**: [`FloatBits`], [`NextUp`]/[`NextDown`] and [`Algebraic`]
12//! are Tier A; [`Minimum`]/[`Maximum`] are Tier B-ish (comparison ladders,
13//! but on floats CT rarely applies); [`RoundTiesEven`], [`Erf`] and
14//! [`Gamma`] are Tier C (data-dependent branching / libm).
15
16c0nst::c0nst! {
17/// Raw IEEE 754 bit-pattern access.
18pub c0nst trait FloatBits: Sized {
19    /// The unsigned integer type with the same width (`u32` for `f32`,
20    /// `u64` for `f64`).
21    type Bits;
22
23    /// Raw transmutation to the bit representation.
24    ///
25    /// ```
26    /// use const_num_traits::FloatBits;
27    ///
28    /// assert_eq!(FloatBits::to_bits(1.0f32), 0x3F80_0000);
29    /// ```
30    fn to_bits(self) -> Self::Bits;
31
32    /// Raw transmutation from the bit representation.
33    ///
34    /// ```
35    /// use const_num_traits::FloatBits;
36    ///
37    /// let f: f32 = FloatBits::from_bits(0x4148_0000u32);
38    /// assert_eq!(f, 12.5);
39    /// ```
40    fn from_bits(bits: Self::Bits) -> Self;
41}
42}
43
44c0nst::c0nst! {
45/// Steps one ULP towards positive infinity.
46pub c0nst trait NextUp: Sized {
47    /// Returns the least number greater than `self` (one ULP up). Follows
48    /// the inherent `next_up` semantics for zeros, infinities and NaN.
49    type Output;
50    fn next_up(self) -> Self::Output;
51}
52}
53
54c0nst::c0nst! {
55/// Steps one ULP towards negative infinity.
56pub c0nst trait NextDown: Sized {
57    /// Returns the greatest number less than `self` (one ULP down). Follows
58    /// the inherent `next_down` semantics for zeros, infinities and NaN.
59    type Output;
60    fn next_down(self) -> Self::Output;
61}
62}
63
64c0nst::c0nst! {
65/// IEEE 754-2019 `maximum` (NaN-propagating, unlike `max`).
66pub c0nst trait Maximum: Sized {
67    /// Returns the greater of two numbers, propagating NaN and treating
68    /// `+0.0` as greater than `-0.0` — the IEEE 754-2019 `maximum`
69    /// operation, in contrast to `max` which *ignores* NaN.
70    ///
71    /// ```
72    /// use const_num_traits::Maximum;
73    ///
74    /// assert_eq!(Maximum::maximum(1.0f32, 2.0), 2.0);
75    /// assert!(Maximum::maximum(1.0f32, f32::NAN).is_nan());
76    /// ```
77    type Output;
78    fn maximum(self, other: Self) -> Self::Output;
79}
80}
81
82c0nst::c0nst! {
83/// IEEE 754-2019 `minimum` (NaN-propagating, unlike `min`).
84pub c0nst trait Minimum: Sized {
85    /// Returns the lesser of two numbers, propagating NaN and treating
86    /// `-0.0` as less than `+0.0` — the IEEE 754-2019 `minimum` operation,
87    /// in contrast to `min` which *ignores* NaN.
88    type Output;
89    fn minimum(self, other: Self) -> Self::Output;
90}
91}
92
93/// Rounds half-way cases to the nearest even integer (banker's rounding).
94pub trait RoundTiesEven: Sized {
95    /// Returns the nearest integer to `self`, with half-way cases rounded
96    /// to the even one (`2.5 -> 2.0`, `3.5 -> 4.0`).
97    ///
98    /// ```
99    /// use const_num_traits::RoundTiesEven;
100    ///
101    /// assert_eq!(RoundTiesEven::round_ties_even(2.5f32), 2.0);
102    /// assert_eq!(RoundTiesEven::round_ties_even(3.5f32), 4.0);
103    /// assert_eq!(RoundTiesEven::round_ties_even(-2.5f32), -2.0);
104    /// ```
105    type Output;
106    fn round_ties_even(self) -> Self::Output;
107}
108
109c0nst::c0nst! {
110/// Arithmetic with an "algebraic" license: the compiler may reassociate,
111/// use reciprocal shortcuts, or contract into fused operations.
112///
113/// std's `algebraic_*` methods (unstable `float_algebraic`) allow any
114/// result reachable by real-number-algebra rewrites. The plain IEEE
115/// operation is one such result, so the primitive impls here simply perform
116/// it — a conforming implementation that loses only the optimization
117/// license until std's intrinsics stabilize and the impls can delegate.
118pub c0nst trait Algebraic: Sized {
119    /// The (owned) result type.
120    type Output;
121    /// Addition with algebraic rewrite license.
122    fn algebraic_add(self, rhs: Self) -> Self::Output;
123    /// Subtraction with algebraic rewrite license.
124    fn algebraic_sub(self, rhs: Self) -> Self::Output;
125    /// Multiplication with algebraic rewrite license.
126    fn algebraic_mul(self, rhs: Self) -> Self::Output;
127    /// Division with algebraic rewrite license.
128    fn algebraic_div(self, rhs: Self) -> Self::Output;
129    /// Remainder with algebraic rewrite license.
130    fn algebraic_rem(self, rhs: Self) -> Self::Output;
131}
132}
133
134/// The error function and its complement.
135///
136/// Implementations for `f32`/`f64` require the `libm` cargo feature (std's
137/// `erf` is still unstable, so there is nothing to delegate to otherwise).
138pub trait Erf: Sized {
139    /// The (owned) result type.
140    type Output;
141    /// The error function `erf(self)`.
142    fn erf(self) -> Self::Output;
143    /// The complementary error function `1 - erf(self)`.
144    fn erfc(self) -> Self::Output;
145}
146
147/// The gamma function and the natural log of its absolute value.
148///
149/// Implementations for `f32`/`f64` require the `libm` cargo feature (std's
150/// `gamma` is still unstable, so there is nothing to delegate to
151/// otherwise).
152pub trait Gamma: Sized {
153    /// The (owned) result type.
154    type Output;
155    /// The gamma function `Γ(self)`.
156    fn gamma(self) -> Self::Output;
157    /// Returns `ln(|Γ(self)|)` together with the sign of `Γ(self)`,
158    /// matching std's `ln_gamma` (and C's `lgamma_r`).
159    fn ln_gamma(self) -> (Self::Output, i32);
160}
161
162macro_rules! float_bits_impl {
163    ($($t:ty => $b:ty;)*) => {$(
164        c0nst::c0nst! {
165        c0nst impl FloatBits for $t {
166            type Bits = $b;
167
168            #[inline]
169            fn to_bits(self) -> $b {
170                <$t>::to_bits(self)
171            }
172
173            #[inline]
174            fn from_bits(bits: $b) -> $t {
175                <$t>::from_bits(bits)
176            }
177        }
178        }
179
180        c0nst::c0nst! {
181        c0nst impl NextUp for $t {
182            type Output = $t;
183            #[inline]
184            fn next_up(self) -> $t {
185                <$t>::next_up(self)
186            }
187        }
188        }
189
190        c0nst::c0nst! {
191        c0nst impl NextDown for $t {
192            type Output = $t;
193            #[inline]
194            fn next_down(self) -> $t {
195                <$t>::next_down(self)
196            }
197        }
198        }
199
200        // minimum/maximum are still unstable in std; same comparison ladder
201        // as core.
202        c0nst::c0nst! {
203        c0nst impl Maximum for $t {
204            type Output = $t;
205            #[inline]
206            fn maximum(self, other: Self) -> $t {
207                if self > other {
208                    self
209                } else if other > self {
210                    other
211                } else if self == other {
212                    if <$t>::is_sign_positive(self) && <$t>::is_sign_negative(other) {
213                        self
214                    } else {
215                        other
216                    }
217                } else {
218                    // at least one input is NaN; propagate it
219                    self + other
220                }
221            }
222        }
223        }
224
225        c0nst::c0nst! {
226        c0nst impl Minimum for $t {
227            type Output = $t;
228            #[inline]
229            fn minimum(self, other: Self) -> $t {
230                if self < other {
231                    self
232                } else if other < self {
233                    other
234                } else if self == other {
235                    if <$t>::is_sign_negative(self) && <$t>::is_sign_positive(other) {
236                        self
237                    } else {
238                        other
239                    }
240                } else {
241                    self + other
242                }
243            }
244        }
245        }
246
247        c0nst::c0nst! {
248        c0nst impl Algebraic for $t {
249            type Output = $t;
250            #[inline]
251            fn algebraic_add(self, rhs: Self) -> $t { self + rhs }
252            #[inline]
253            fn algebraic_sub(self, rhs: Self) -> $t { self - rhs }
254            #[inline]
255            fn algebraic_mul(self, rhs: Self) -> $t { self * rhs }
256            #[inline]
257            fn algebraic_div(self, rhs: Self) -> $t { self / rhs }
258            #[inline]
259            fn algebraic_rem(self, rhs: Self) -> $t { self % rhs }
260        }
261        }
262    )*};
263}
264
265float_bits_impl! {
266    f32 => u32;
267    f64 => u64;
268}
269
270// std's round_ties_even is std-only (libm-backed); the no-std fallback
271// hand-rolls banker's rounding from FloatCore primitives.
272macro_rules! round_ties_even_impl {
273    ($($t:ty)*) => {$(
274        #[cfg(feature = "std")]
275        impl RoundTiesEven for $t {
276            type Output = $t;
277            #[inline]
278            fn round_ties_even(self) -> $t {
279                <$t>::round_ties_even(self)
280            }
281        }
282
283        #[cfg(not(feature = "std"))]
284        impl RoundTiesEven for $t {
285            type Output = $t;
286            #[inline]
287            fn round_ties_even(self) -> $t {
288                use crate::float::FloatCore;
289                let f = FloatCore::fract(self);
290                if FloatCore::abs(f) != 0.5 {
291                    // no tie: ordinary round-half-away agrees
292                    FloatCore::round(self)
293                } else {
294                    let t = FloatCore::trunc(self);
295                    if t % 2.0 == 0.0 {
296                        t
297                    } else {
298                        t + FloatCore::signum(self)
299                    }
300                }
301            }
302        }
303    )*};
304}
305
306round_ties_even_impl!(f32 f64);
307
308#[cfg(feature = "libm")]
309impl Erf for f32 {
310    type Output = f32;
311    #[inline]
312    fn erf(self) -> f32 {
313        libm::erff(self)
314    }
315    #[inline]
316    fn erfc(self) -> f32 {
317        libm::erfcf(self)
318    }
319}
320
321#[cfg(feature = "libm")]
322impl Erf for f64 {
323    type Output = f64;
324    #[inline]
325    fn erf(self) -> f64 {
326        libm::erf(self)
327    }
328    #[inline]
329    fn erfc(self) -> f64 {
330        libm::erfc(self)
331    }
332}
333
334#[cfg(feature = "libm")]
335impl Gamma for f32 {
336    type Output = f32;
337    #[inline]
338    fn gamma(self) -> f32 {
339        libm::tgammaf(self)
340    }
341    #[inline]
342    fn ln_gamma(self) -> (f32, i32) {
343        libm::lgammaf_r(self)
344    }
345}
346
347#[cfg(feature = "libm")]
348impl Gamma for f64 {
349    type Output = f64;
350    #[inline]
351    fn gamma(self) -> f64 {
352        libm::tgamma(self)
353    }
354    #[inline]
355    fn ln_gamma(self) -> (f64, i32) {
356        libm::lgamma_r(self)
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363
364    #[test]
365    fn bits_and_ulps() {
366        assert_eq!(FloatBits::to_bits(1.0f32), 0x3F80_0000u32);
367        let x: f64 = FloatBits::from_bits(0x4029_0000_0000_0000u64);
368        assert_eq!(x, 12.5);
369        assert_eq!(NextUp::next_up(1.0f32), f32::from_bits(0x3F80_0001));
370        assert_eq!(NextDown::next_down(1.0f32), f32::from_bits(0x3F7F_FFFF));
371        assert_eq!(NextUp::next_up(0.0f32), f32::from_bits(1)); // smallest subnormal
372    }
373
374    #[test]
375    fn minimum_maximum() {
376        assert_eq!(Maximum::maximum(1.0f32, 2.0), 2.0);
377        assert_eq!(Minimum::minimum(1.0f32, 2.0), 1.0);
378        // NaN propagates (unlike max/min)
379        assert!(Maximum::maximum(1.0f32, f32::NAN).is_nan());
380        assert!(Minimum::minimum(f32::NAN, 1.0f32).is_nan());
381        // signed zeros are ordered
382        assert_eq!(Maximum::maximum(0.0f32, -0.0).to_bits(), 0.0f32.to_bits());
383        assert_eq!(
384            Minimum::minimum(0.0f32, -0.0).to_bits(),
385            (-0.0f32).to_bits()
386        );
387    }
388
389    #[test]
390    fn ties_even() {
391        assert_eq!(RoundTiesEven::round_ties_even(2.5f32), 2.0);
392        assert_eq!(RoundTiesEven::round_ties_even(3.5f32), 4.0);
393        assert_eq!(RoundTiesEven::round_ties_even(-2.5f64), -2.0);
394        assert_eq!(RoundTiesEven::round_ties_even(-3.5f64), -4.0);
395        assert_eq!(RoundTiesEven::round_ties_even(2.4f32), 2.0);
396        assert_eq!(RoundTiesEven::round_ties_even(2.6f32), 3.0);
397        assert_eq!(RoundTiesEven::round_ties_even(0.5f64), 0.0);
398        assert_eq!(RoundTiesEven::round_ties_even(-0.5f64), -0.0);
399    }
400
401    #[test]
402    fn algebraic_is_conforming() {
403        assert_eq!(Algebraic::algebraic_add(1.5f64, 2.25), 3.75);
404        assert_eq!(Algebraic::algebraic_mul(3.0f32, 0.5), 1.5);
405        assert_eq!(Algebraic::algebraic_rem(7.5f64, 2.0), 1.5);
406    }
407
408    #[cfg(feature = "libm")]
409    #[test]
410    fn special_functions() {
411        assert!((Erf::erf(0.0f64)).abs() < 1e-15);
412        assert!((Erf::erf(10.0f64) - 1.0).abs() < 1e-15);
413        assert!((Erf::erfc(0.0f64) - 1.0).abs() < 1e-15);
414        assert!((Gamma::gamma(5.0f64) - 24.0).abs() < 1e-10);
415        let (lg, sign) = Gamma::ln_gamma(5.0f64);
416        assert!((lg - 24.0f64.ln()).abs() < 1e-10);
417        assert_eq!(sign, 1);
418    }
419}