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}