Skip to main content

rustymath_core/
convert.rs

1//! Conversions between floats and integers whose loops vectorize on every target. AVX2 and wasm have no vector instruction for converting between `f64` and a 64-bit integer. AArch64 converts bytes to floats two lanes at a time. A loop that converts such types with `as` runs one item at a time on those targets. [`int`] and [`float`] convert them through a float's bits. They use the conversion instruction for types that every target converts in vector form, and for 64-bit integers where the target can. [`count`] writes runs of whole numbers as floats. Where the target can't convert 64-bit integers in vector form, it converts one item in 8. [`saturated`] also converts big integers and `u64`s, saturating them at an integer type's range.
2use crate::float::{Float, sealed::{Core, Sealed}};
3use num_bigint::{BigInt, Sign};
4use num_traits::Bounded;
5
6/// An integer type that [`int`] and [`float`] convert: `i8`, `i16`, `i32`, `i64`, `u8`, `u16` or `u32`.
7pub trait Int: Copy + Sealed + sealed::Ways {}
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
10pub enum ConversionKind { Cast, Truncate, Bits, Keep }
11
12/// Resolved kinds include the working float width even when the other kind is a word.
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
14pub struct Conversion { pub input: crate::buffer::DType, pub output: crate::buffer::DType, pub kind: ConversionKind }
15impl Conversion {
16    pub fn validity<F: crate::Float, O: crate::recipe::Ops<F> + ?Sized>(self, o: &O, x: O::V) -> Option<(crate::error::Kind, O::B)> {
17        use crate::buffer::{DType, Width};
18        if self.kind == ConversionKind::Keep && self.input.is_integral() && self.output.is_integral() {
19            let bounds = |dtype| match dtype {
20                DType::Boolean => (0, 1), DType::Integer(width) => with_width!(width, T => (T::MIN as i64, T::MAX as i64)), _ => unreachable!()
21            };
22            let ((input_lo, input_hi), (lo, hi)) = (bounds(self.input), bounds(self.output));
23            if lo <= input_lo && hi >= input_hi { return None; }
24            let x = o.to_integer(x);
25            let valid = o.and(
26                o.integer_compare(crate::recipe::Cmp::LessEqual, o.integer(lo.max(input_lo)), x),
27                o.integer_compare(crate::recipe::Cmp::LessEqual, x, o.integer(hi.min(input_hi))));
28            return Some((crate::error::Kind::Domain, valid));
29        }
30        if self.kind != ConversionKind::Cast { return None; }
31        let valid = match self.output {
32            DType::Boolean => o.or(o.equal(x, o.number(F::zero())), o.equal(x, o.number(F::one()))),
33            DType::Integer(width) if matches!(self.input, DType::Float(_)) => {
34                let upper = if width == Width::U8 { 256.0 } else { 2f64.powi((8 * self.output.bytes() - 1) as i32) };
35                let lower = if width == Width::U8 { 0.0 } else { -upper };
36                let x = if width == Width::I64 { x } else { o.from_integer(o.to_integer(x)) };
37                o.and(o.less_equal(o.number(F::narrow(lower)), x), o.less(x, o.number(F::narrow(upper))))
38            }
39            _ => return None,
40        };
41        Some((crate::error::Kind::Overflow, valid))
42    }
43    pub fn may_raise(self, checked: bool) -> bool {
44        self.kind == ConversionKind::Keep || checked && (self.output == crate::buffer::DType::Character || self
45                        .validity::<f64, _>(&crate::recipe::RangeOps(self.input), crate::domain::Range::of_dtype(self.input))
46                        .is_some_and(|(_, valid)| valid != Some(true)))
47    }
48    pub fn map(
49        self, input: crate::agreement::Read<'_>, range: std::ops::Range<usize>, checked: bool, output: &mut crate::buffer::Buffer
50    ) -> Result<(), crate::error::Error> {
51        use crate::buffer::{Buffer, DType, FloatWidth, Floats};
52        use crate::error::{Error, Kind};
53        if self.kind == ConversionKind::Cast && !matches!((self.input, self.output), (DType::Integer(_), DType::Integer(_))) {
54            return input.read_into(range, self.output, checked, output);
55        }
56        let mut scratch = Buffer::with_capacity(self.input, 0);
57        let values = input.part_view(range, self.input, checked, &mut scratch)?;
58        *output = match (self.kind, values, self.output) {
59            (ConversionKind::Cast, _, _) => Buffer::cast_results(values, self.output, checked)?,
60            (ConversionKind::Keep, _, _) => Buffer::kept(values, self.output)?,
61            (ConversionKind::Truncate, crate::buffer::View::Floats(_), DType::Integer(_)) => Buffer::cast_results(values, self.output, false)?,
62            (ConversionKind::Bits, crate::buffer::View::Floats(Floats::F32(values)), DType::Integer(crate::buffer::Width::I64)) => {
63                Buffer::Integers(crate::buffer::IntBuf::I64(values.iter().map(|x| i64::from(x.to_bits())).collect()))
64            }
65            (ConversionKind::Bits, crate::buffer::View::Floats(Floats::F64(values)), DType::Integer(crate::buffer::Width::I64)) => {
66                Buffer::Integers(crate::buffer::IntBuf::I64(values.iter().map(|x| x.to_bits() as i64).collect()))
67            }
68            (ConversionKind::Bits, crate::buffer::View::Integers(values), DType::Float(FloatWidth::F32)) => {
69                Buffer::Floats(crate::buffer::FloatBuf::F32((0..values.len()).map(|i| f32::from_bits(values.get(i) as u32)).collect()))
70            }
71            (ConversionKind::Bits, crate::buffer::View::Integers(values), DType::Float(FloatWidth::F64)) => {
72                Buffer::Floats(crate::buffer::FloatBuf::F64((0..values.len()).map(|i| f64::from_bits(values.get(i) as u64)).collect()))
73            }
74            (ConversionKind::Truncate, _, _) if self.input.is_integral() && self.output.is_integral() => Buffer::cast_results(values, self.output, false)?,
75            _ => return Err(Error::new(Kind::Domain, "unsupported resolved conversion")),
76        };
77        Ok(())
78    }
79}
80
81pub(crate) mod sealed {
82    /// One integer type's conversions from and to `f32` and `f64`, which [`int`](super::int), [`saturated`](super::saturated) and [`float`](super::float) pick by the float type.
83    pub trait Ways: Copy {
84        fn from_f32(x: f32) -> Option<Self>;
85        fn from_f64(x: f64) -> Option<Self>;
86        fn saturated_f32(x: f32) -> Self;
87        fn saturated_f64(x: f64) -> Self;
88        fn to_f32(self) -> f32;
89        fn to_f64(self) -> f64;
90    }
91}
92
93/// `x`, a whole float, as an integer of type `I`. `None` where `x` is NaN or infinite, or lies outside `I`. For `i64` and `u32`, a magnitude beyond 2^51 - 1 also gives `None`, and so does `i32::MIN` from an `f32`. A fraction gives an integer next to it, or `None`.
94#[inline(always)]
95pub fn int<I: Int, F: Float>(x: F) -> Option<I> { if <F::Working as Core>::WIDE { I::from_f64(x.wide()) } else { I::from_f32(x.wide() as f32) } }
96
97/// `x` as an integer of type `I`, or `I`'s largest or smallest value where `x` lies past its range. A float converts as Rust's `as` converts it: NaN gives 0, and a fraction gives the integer toward 0. A type that fits an `i32` converts from a float in vector form on every target, where `as` would convert one item at a time, as it does from an `f64` on AArch64 and from any float on x86-64. [`Saturates`] lists the other sources.
98#[inline(always)]
99pub fn saturated<I, S: Saturates<I>>(x: S) -> I { x.saturated() }
100/// A number that [`saturated`] converts to the integer type `I`: a float to any [`Int`], and a big integer or a `u64` to any integer type that `TryFrom` converts it to.
101pub trait Saturates<I> { fn saturated(self) -> I; }
102impl<I: Int, F: Float> Saturates<I> for F {
103    #[inline(always)]
104    fn saturated(self) -> I { if <F::Working as Core>::WIDE { I::saturated_f64(self.wide()) } else { I::saturated_f32(self.wide() as f32) } }
105}
106impl<I: Bounded + for<'a> TryFrom<&'a BigInt>> Saturates<I> for &BigInt {
107    fn saturated(self) -> I { I::try_from(self).unwrap_or_else(|_| if self.sign() == Sign::Minus { I::min_value() } else { I::max_value() }) }
108}
109impl<I: Bounded + TryFrom<u64>> Saturates<I> for u64 { fn saturated(self) -> I { I::try_from(self).unwrap_or_else(|_| I::max_value()) } }
110/// The floor of `x` within `t` as an integer of type `I`, saturated at `I`'s range as [`saturated`] saturates: NaN gives 0. The one float-to-integer rule for conversions that change a number's domain: BPL's `ₓ` marks and Python's `astype` and integer `dtype=`.
111#[inline(always)]
112pub fn floored<I: Int, F: Float>(x: F, t: impl crate::tolerant::Tolerant) -> I { saturated(t.floor(x)) }
113
114/// `i` as the nearest float of type `F`.
115#[inline(always)]
116pub fn float<F: Float, I: Int>(i: I) -> F { if <F::Working as Core>::WIDE { F::narrow(i.to_f64()) } else { F::narrow(i.to_f32().into()) } }
117
118/// Appends the `n` whole numbers `start`, `start + 1`, `start + 2` and so on to `out` as floats of type `F`, or `start`, `start - 1` and so on where `down` is true. Each is the whole number rounded once to `F`. A float narrower than `f64` counts in `i32` where every item fits one, because every target converts `i32` to it in vector form. Otherwise a target with `NATIVE_64` converts each item from an integer, as [`float`] does. Wasm and x86-64 without AVX-512 have no vector instruction for that, so there each block of 8 items adds 0 to 7 steps to the block's first item, which comes from the block's position.
119pub fn count<F: Float>(out: &mut Vec<F>, start: i64, down: bool, n: usize) {
120    let end = if down { start - n as i64 } else { start + n as i64 };
121    if !<F::Working as Core>::WIDE && i32::try_from(start).is_ok() && i32::try_from(end).is_ok() {
122        let start = start as i32;
123        return if down { out.extend((0..n as i32).map(move |i| float::<F, i32>(start - i))) }
124        else { out.extend((0..n as i32).map(move |i| float::<F, i32>(start + i))) };
125    }
126    if NATIVE_64 {
127        return if down { out.extend((0..n).map(move |i| F::narrow(wide_float(start - i as i64)))) }
128        else { out.extend((0..n).map(move |i| F::narrow(wide_float(start + i as i64)))) };
129    }
130    let step = if down { -1 } else { 1 };
131    let offsets: [f64; 8] = std::array::from_fn(|k| wide_float(step * k as i64));
132    let first = move |j: usize| wide_float(start + step * (8 * j) as i64);
133    unsafe {
134        crate::slice::appended(out, n, |slots| {
135            let (blocks, rest) = slots.as_chunks_mut::<8>();
136            let whole = blocks.len();
137            for (j, block) in blocks.iter_mut().enumerate() { let first = first(j); for k in 0..8 { block[k].write(F::narrow(first + offsets[k])); } }
138            for (slot, &offset) in rest.iter_mut().zip(&offsets) { slot.write(F::narrow(first(whole) + offset)); }
139        })
140    };
141}
142
143/// Whether the target converts between `f64` and 64-bit integers in vector form, as AArch64 and x86-64 with AVX-512 do. Elsewhere they convert through a float's bits. Both ways give the same results for whole floats.
144const NATIVE_64: bool = cfg!(any(target_arch = "aarch64", target_feature = "avx512dq"));
145
146/// Whether the target converts floats to `i32` with saturation in vector form, as AArch64's `fcvtzs` and wasm's `trunc_sat` do. On x86-64, where Rust's saturating `as` compiles to one scalar conversion per item, floats convert without saturation after a range check.
147const SATURATING: bool = !cfg!(target_arch = "x86_64");
148
149/// The magnitude up to which the bits of `f64::SHIFT` + x hold the whole number x.
150pub(crate) const WIDE_LIMIT: f64 = 2251799813685247.0;
151
152/// `i` as the nearest `f64`, natively or through `halves`.
153#[inline(always)]
154fn wide_float(i: i64) -> f64 { if NATIVE_64 { i as f64 } else { halves(i) } }
155
156/// `i` as the nearest `f64` through bits. The high half converts through the bits of 2^84 + 2^63 plus it times 2^32, and the low half through `from_low`. Both convert exactly, and their sum rounds once.
157#[inline(always)]
158fn halves(i: i64) -> f64 {
159    let high = f64::from_bits(0x4530_0000_0000_0000 | ((i as u64 >> 32) ^ 0x8000_0000)) - f64::from_bits(0x4530_0000_8000_0000);
160    high + f64::from_low(i as u64 & 0xffff_ffff, 0.0)
161}
162
163/// The whole number nearest `x`, for a magnitude up to `WIDE_LIMIT`, through the bits of `f64::SHIFT` + x.
164#[inline(always)]
165pub(crate) fn shifted(x: f64) -> i64 { (x + f64::SHIFT).to_bits().wrapping_sub(f64::SHIFT.to_bits()) as i64 }
166
167/// The whole number `i`, of magnitude up to `WIDE_LIMIT`, as an `f64`, through the bits of `f64::SHIFT` + i. It undoes [`shifted`].
168#[inline(always)]
169pub(crate) fn unshifted(i: i64) -> f64 { f64::from_bits(f64::SHIFT.to_bits().wrapping_add(i as u64)) - f64::SHIFT }
170
171/// Writes each integer type's conversions. `narrow` types fit an `i32`, and every target converts a float to an `i32` in vector form. `wide` types convert from a float through the bits of `f64::SHIFT` + x, unless the target has `NATIVE_64`. The two expressions convert `i` to `f32` and `f64`.
172macro_rules! ints {
173    ($($t:ty: $kind:ident, |$i:ident| $f32:expr, $f64:expr;)+) => {$(
174        impl Int for $t {}
175        impl sealed::Ways for $t {
176            #[inline(always)]
177            fn from_f32(x: f32) -> Option<$t> { ints!(@$kind f32, $t, x) }
178            #[inline(always)]
179            fn from_f64(x: f64) -> Option<$t> { ints!(@$kind f64, $t, x) }
180            #[inline(always)]
181            fn saturated_f32(x: f32) -> $t { ints!(@saturated $kind f32, $t, x) }
182            #[inline(always)]
183            fn saturated_f64(x: f64) -> $t { ints!(@saturated $kind f64, $t, x) }
184            #[inline(always)]
185            fn to_f32(self) -> f32 { let $i = self; $f32 }
186            #[inline(always)]
187            fn to_f64(self) -> f64 { let $i = self; $f64 }
188        }
189    )+};
190    (@narrow $f:ty, $t:ty, $x:ident) => {{
191        // One comparison, of the distance from the middle of `$t`'s range, keeps the loop to one mask.
192        let (mid, half) = ((<$t>::MIN as f64 + <$t>::MAX as f64) / 2.0, (<$t>::MAX as f64 - <$t>::MIN as f64 + 1.0) / 2.0);
193        (($x - mid as $f).abs() < half as $f).then(|| {
194            if SATURATING { return $x as i32 as $t }
195            // SAFETY: the comparison excludes NaN, the infinities and every float that truncates outside `$t`, which `i32` holds. The conversion then needs no saturation.
196            unsafe { $x.to_int_unchecked::<i32>() as $t }
197        })
198    }};
199    (@wide f32, $t:ty, $x:ident) => { Self::from_f64($x.into()) };
200    (@wide f64, $t:ty, $x:ident) => {{
201        let n = if NATIVE_64 { $x as i64 } else { shifted($x) };
202        ($x.abs() <= WIDE_LIMIT).then_some(n).and_then(|n| <$t>::try_from(n).ok())
203    }};
204    (@saturated narrow f32, $t:ty, $x:ident) => { if SATURATING { $x as $t } else { ints!(@clamped f32, $t, $x) } };
205    // A target with `NATIVE_64` converts an `f64` to an `i64` with saturation in vector form, which the clamp then narrows as `as` would.
206    (@saturated narrow f64, $t:ty, $x:ident) => { if NATIVE_64 { ($x as i64).clamp(<$t>::MIN.into(), <$t>::MAX.into()) as $t } else { ints!(@clamped f64, $t, $x) } };
207    (@saturated wide $f:ty, $t:ty, $x:ident) => { $x as $t };
208    (@clamped $f:ty, $t:ty, $x:ident) => {{
209        // Where `$f` can't hold `$t::MAX`, as an `f32` can't hold `i32::MAX`, the maximum rounds up to a float outside `$t`. The clamp then stops one float below it, and a float at or past it gives `$t::MAX`.
210        let top = <$t>::MAX as $f;
211        let below = if top as i128 > <$t>::MAX as i128 { top.next_down() } else { top };
212        let c = if $x.is_nan() { 0.0 } else { $x.max(<$t>::MIN as $f).min(below) };
213        // SAFETY: `c` is never NaN, and the clamp keeps it within `$t`.
214        let i = unsafe { c.to_int_unchecked::<$t>() };
215        if $x >= top { <$t>::MAX } else { i }
216    }};
217}
218
219// A byte or a `u16` reads as a float through `from_low`. AArch64's own conversion handles bytes two lanes at a time.
220ints! {
221    i8: narrow, |i| i as f32, i as f64;
222    i16: narrow, |i| i as f32, i as f64;
223    i32: narrow, |i| i as f32, i as f64;
224    u8: narrow, |i| f32::from_low(i.into(), 0.0), f32::from_low(i.into(), 0.0).into();
225    u16: narrow, |i| f32::from_low(i.into(), 0.0), f32::from_low(i.into(), 0.0).into();
226    u32: wide, |i| f64::from_low(i.into(), 0.0) as f32, f64::from_low(i.into(), 0.0);
227    i64: wide, |i| wide_float(i) as f32, wide_float(i);
228}