Skip to main content

zerodds_float128/
convert.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Conversions in and out, with the inexact cases reported.
4//!
5//! Every narrowing conversion here comes in two forms: one that refuses
6//! a value it cannot carry ([`Float128::to_f64`], [`Float128::to_i64`])
7//! and one that rounds and says so ([`Float128::to_f64_rounded`]). There
8//! is no third form that rounds in silence, because that is the
9//! behaviour this crate replaced.
10
11use core::fmt;
12
13use crate::float::{
14    EXPONENT_BIAS, EXPONENT_MAX, EXPONENT_SHIFT, FRACTION_BITS, Float128, QUIET_BIT, SIGN_MASK,
15};
16
17/// A conversion that could not be exact.
18///
19/// Carries the rounded result, so a caller that wants it after all takes
20/// it from the error rather than repeating the conversion with a
21/// different function and hoping the two agree.
22#[derive(Debug, Clone, Copy, PartialEq)]
23pub struct Inexact<T> {
24    /// What the value rounds to in the destination type.
25    pub rounded: T,
26    /// What the source was, for a message that names it.
27    pub source: Float128,
28}
29
30impl<T> fmt::Display for Inexact<T> {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        write!(
33            f,
34            "the binary128 value 0x{:032x} does not fit the destination type exactly",
35            self.source.to_bits()
36        )
37    }
38}
39
40impl<T: fmt::Debug> core::error::Error for Inexact<T> {}
41
42// -- from the narrower floats, which is always exact ---------------------
43
44impl Float128 {
45    /// From an `f64`. Always exact: binary128 covers every binary64
46    /// value, subnormals included, with 60 fraction bits to spare.
47    #[must_use]
48    pub fn from_f64(value: f64) -> Self {
49        let bits = value.to_bits();
50        let negative = bits >> 63 != 0;
51        let biased = ((bits >> 52) & 0x7ff) as u32;
52        let fraction = u128::from(bits & 0x000f_ffff_ffff_ffff);
53        widen(negative, biased, fraction, 11, 52)
54    }
55
56    /// From an `f32`. Always exact, for the same reason.
57    #[must_use]
58    pub fn from_f32(value: f32) -> Self {
59        let bits = value.to_bits();
60        let negative = bits >> 31 != 0;
61        let biased = (bits >> 23) & 0xff;
62        let fraction = u128::from(bits & 0x007f_ffff);
63        widen(negative, biased, fraction, 8, 23)
64    }
65
66    /// From any signed integer up to 128 bits, rounding to nearest with
67    /// ties to even. Exact for everything up to 2^113.
68    #[must_use]
69    pub fn from_i128_rounded(value: i128) -> Self {
70        let negative = value < 0;
71        let magnitude = value.unsigned_abs();
72        Self::from_parts_rounded(negative, magnitude, 0).0
73    }
74
75    /// From any unsigned integer up to 128 bits, rounding to nearest
76    /// with ties to even. Exact for everything up to 2^113.
77    #[must_use]
78    pub fn from_u128_rounded(value: u128) -> Self {
79        Self::from_parts_rounded(false, value, 0).0
80    }
81
82    /// From a signed integer, refusing one that does not fit exactly.
83    ///
84    /// # Errors
85    /// [`Inexact`] when the integer needs more than 113 significant
86    /// bits, carrying what it rounds to.
87    pub fn try_from_i128(value: i128) -> Result<Self, Inexact<Self>> {
88        let negative = value < 0;
89        let (rounded, lost) = Self::from_parts_rounded(negative, value.unsigned_abs(), 0);
90        if lost {
91            Err(Inexact {
92                rounded,
93                source: rounded,
94            })
95        } else {
96            Ok(rounded)
97        }
98    }
99
100    /// From an unsigned integer, refusing one that does not fit exactly.
101    ///
102    /// # Errors
103    /// [`Inexact`] when the integer needs more than 113 significant
104    /// bits, carrying what it rounds to.
105    pub fn try_from_u128(value: u128) -> Result<Self, Inexact<Self>> {
106        let (rounded, lost) = Self::from_parts_rounded(false, value, 0);
107        if lost {
108            Err(Inexact {
109                rounded,
110                source: rounded,
111            })
112        } else {
113            Ok(rounded)
114        }
115    }
116}
117
118/// Widen a narrower IEEE binary format into binary128.
119///
120/// `biased` and `fraction` are the source's fields; `exponent_bits` and
121/// `fraction_bits` its widths. Every case is exact, including the source
122/// subnormals, which become binary128 *normals* — binary128's exponent
123/// range swallows binary64's whole subnormal range.
124fn widen(
125    negative: bool,
126    biased: u32,
127    fraction: u128,
128    exponent_bits: u32,
129    fraction_bits: u32,
130) -> Float128 {
131    let sign = u128::from(negative) << (FRACTION_BITS + 15);
132    let source_max = (1u32 << exponent_bits) - 1;
133    let source_bias = (1i32 << (exponent_bits - 1)) - 1;
134    let shift = FRACTION_BITS - fraction_bits;
135
136    if biased == source_max {
137        // Infinity keeps its emptiness; a NaN keeps its payload in the
138        // high fraction bits, which is what puts the source's quiet bit
139        // on binary128's quiet bit and keeps a signalling NaN
140        // signalling. IEEE 754-2008 clause 6.2.3.
141        let bits = sign | ((EXPONENT_MAX as u128) << EXPONENT_SHIFT) | (fraction << shift);
142        return Float128::from_bits(bits);
143    }
144    if biased == 0 {
145        if fraction == 0 {
146            return Float128::from_bits(sign);
147        }
148        // A source subnormal: value = fraction * 2^(1 - bias - fbits).
149        let exponent = 1 - source_bias - fraction_bits as i32;
150        let (value, lost) = Float128::from_parts_rounded(negative, fraction, exponent);
151        debug_assert!(!lost, "widening a narrower binary format is always exact");
152        return value;
153    }
154    let biased128 = biased as i32 - source_bias + EXPONENT_BIAS;
155    let bits = sign | ((biased128 as u128) << EXPONENT_SHIFT) | (fraction << shift);
156    Float128::from_bits(bits)
157}
158
159// -- out to the narrower floats ------------------------------------------
160
161impl Float128 {
162    /// To an `f64`, rounding to nearest with ties to even.
163    ///
164    /// Overflow gives an infinity and underflow a signed zero, which is
165    /// what IEEE 754-2008 clause 7.4 requires; both are inexact and
166    /// [`Self::to_f64`] reports them.
167    #[must_use]
168    pub fn to_f64_rounded(self) -> f64 {
169        self.narrow_f64().0
170    }
171
172    /// To an `f64` when the value fits one exactly.
173    ///
174    /// # Errors
175    /// [`Inexact`] carrying the rounded result. This is the refusal the
176    /// `long double` column needed: the old writer took an `f64` and put
177    /// it on the wire as sixteen bytes, so a value with 113 significant
178    /// bits was narrowed on the way in and nobody was told.
179    pub fn to_f64(self) -> Result<f64, Inexact<f64>> {
180        let (rounded, lost) = self.narrow_f64();
181        if lost {
182            Err(Inexact {
183                rounded,
184                source: self,
185            })
186        } else {
187            Ok(rounded)
188        }
189    }
190
191    /// To an `f32`, rounding to nearest with ties to even.
192    #[must_use]
193    pub fn to_f32_rounded(self) -> f32 {
194        self.narrow_f32().0
195    }
196
197    /// To an `f32` when the value fits one exactly.
198    ///
199    /// # Errors
200    /// [`Inexact`] carrying the rounded result.
201    pub fn to_f32(self) -> Result<f32, Inexact<f32>> {
202        let (rounded, lost) = self.narrow_f32();
203        if lost {
204            Err(Inexact {
205                rounded,
206                source: self,
207            })
208        } else {
209            Ok(rounded)
210        }
211    }
212
213    /// The `f64` narrowing, and whether anything was lost.
214    fn narrow_f64(self) -> (f64, bool) {
215        let (bits, lost) = self.narrow(11, 52);
216        (f64::from_bits(bits as u64), lost)
217    }
218
219    /// The `f32` narrowing, and whether anything was lost.
220    fn narrow_f32(self) -> (f32, bool) {
221        let (bits, lost) = self.narrow(8, 23);
222        (f32::from_bits(bits as u32), lost)
223    }
224
225    /// Narrow to a smaller IEEE binary format, returning its bit pattern
226    /// right-aligned in a `u128` and whether the value survived.
227    fn narrow(self, exponent_bits: u32, fraction_bits: u32) -> (u128, bool) {
228        let target_max = (1u32 << exponent_bits) - 1;
229        let target_bias = (1i32 << (exponent_bits - 1)) - 1;
230        let sign = u128::from(self.is_sign_negative()) << (exponent_bits + fraction_bits);
231        let shift = FRACTION_BITS - fraction_bits;
232
233        if self.is_nan() {
234            // A NaN stays a NaN and keeps as much payload as fits. The
235            // quiet bit rides along in the payload rather than being
236            // forced on: binary128 puts it at the top of the fraction
237            // and so does every narrower format, so the same shift that
238            // aligns the payload aligns the flag, and a signalling NaN
239            // that arrived signalling leaves that way.
240            //
241            // The one thing that cannot be allowed is a payload whose
242            // set bits all fall off the bottom, because an all-zero
243            // fraction is not a NaN but an infinity.
244            let mut payload = self.fraction() >> shift;
245            if payload == 0 {
246                payload = 1;
247            }
248            let bits = sign | (u128::from(target_max) << fraction_bits) | payload;
249            return (bits, self.fraction() & ((1u128 << shift) - 1) != 0);
250        }
251        if self.is_infinite() {
252            return (sign | (u128::from(target_max) << fraction_bits), false);
253        }
254        let Some((_, significand, exponent)) = self.decompose() else {
255            return (sign, false);
256        };
257        if significand == 0 {
258            return (sign, false);
259        }
260
261        // Re-normalise into the target's scale: leading one at
262        // `fraction_bits`, exponent adjusted to match.
263        let leading = 127 - significand.leading_zeros();
264        let mut exponent = exponent + leading as i32;
265        let target_exponent = exponent + target_bias;
266        if target_exponent >= target_max as i32 {
267            let bits = sign | (u128::from(target_max) << fraction_bits);
268            return (bits, true);
269        }
270
271        let lost;
272        let mut fraction;
273        if target_exponent <= 0 {
274            // Subnormal in the target: the exponent is pinned and the
275            // significand absorbs the difference.
276            // Positive by construction: `target_exponent` is at most zero
277            // here and `leading` is at least `fraction_bits` minus that, so
278            // the shift is total and needs no fallback.
279            let drop = leading as i32 - fraction_bits as i32 + (1 - target_exponent);
280            let drop = drop.max(0).unsigned_abs();
281            let (rounded, dropped) = crate::float::round_shift_right(significand, drop);
282            // A subnormal that rounds up to the smallest normal carries
283            // out of the fraction into the exponent field by itself.
284            let bits = sign | rounded;
285            return (bits, dropped);
286        } else if leading > fraction_bits {
287            let drop = leading - fraction_bits;
288            let (rounded, dropped) = crate::float::round_shift_right(significand, drop);
289            lost = dropped;
290            fraction = rounded;
291            if fraction >> fraction_bits > 1 {
292                fraction >>= 1;
293                exponent += 1;
294            }
295        } else {
296            fraction = significand << (fraction_bits - leading);
297            lost = false;
298        }
299
300        let target_exponent = exponent + target_bias;
301        if target_exponent >= target_max as i32 {
302            let bits = sign | (u128::from(target_max) << fraction_bits);
303            return (bits, true);
304        }
305        let bits = sign
306            | ((target_exponent as u128) << fraction_bits)
307            | (fraction & ((1u128 << fraction_bits) - 1));
308        (bits, lost)
309    }
310}
311
312// -- out to the integers -------------------------------------------------
313
314impl Float128 {
315    /// The value truncated toward zero, when it is an exact integer that
316    /// fits an `i128`.
317    ///
318    /// # Errors
319    /// [`Inexact`] carrying the truncated result, for a value with a
320    /// fractional part, an infinity, a NaN, or a magnitude past the
321    /// destination.
322    pub fn to_i128(self) -> Result<i128, Inexact<i128>> {
323        let (value, exact) = self.trunc_i128();
324        if exact {
325            Ok(value)
326        } else {
327            Err(Inexact {
328                rounded: value,
329                source: self,
330            })
331        }
332    }
333
334    /// As [`Self::to_i128`], into an `i64`.
335    ///
336    /// # Errors
337    /// [`Inexact`] carrying the saturated or truncated result.
338    pub fn to_i64(self) -> Result<i64, Inexact<i64>> {
339        match self.to_i128() {
340            Ok(wide) => i64::try_from(wide).map_err(|_| Inexact {
341                rounded: if wide < 0 { i64::MIN } else { i64::MAX },
342                source: self,
343            }),
344            Err(inexact) => Err(Inexact {
345                rounded: i64::try_from(inexact.rounded).unwrap_or(if inexact.rounded < 0 {
346                    i64::MIN
347                } else {
348                    i64::MAX
349                }),
350                source: self,
351            }),
352        }
353    }
354
355    /// The truncation and whether it was exact.
356    fn trunc_i128(self) -> (i128, bool) {
357        if self.is_nan() {
358            return (0, false);
359        }
360        if self.is_infinite() {
361            return (
362                if self.is_sign_negative() {
363                    i128::MIN
364                } else {
365                    i128::MAX
366                },
367                false,
368            );
369        }
370        let Some((negative, significand, exponent)) = self.decompose() else {
371            return (0, false);
372        };
373        if significand == 0 {
374            return (0, true);
375        }
376        let leading = 127 - significand.leading_zeros();
377        let magnitude_bits = leading as i32 + exponent + 1;
378        // `i128::MIN` is the one magnitude that needs all 128 bits.
379        let limit = if negative { 128 } else { 127 };
380        if magnitude_bits > limit {
381            return (if negative { i128::MIN } else { i128::MAX }, false);
382        }
383        let (magnitude, fractional) = if exponent >= 0 {
384            // Non-negative by the branch; the `>= 128` test below is the
385            // one that matters and it is on the value, not on the cast.
386            let shift = exponent.unsigned_abs();
387            if shift >= 128 {
388                return (if negative { i128::MIN } else { i128::MAX }, false);
389            }
390            (significand << shift, false)
391        } else {
392            // Positive by the branch, likewise.
393            let shift = exponent.unsigned_abs();
394            if shift >= 128 {
395                (0, true)
396            } else {
397                (
398                    significand >> shift,
399                    significand & ((1u128 << shift) - 1) != 0,
400                )
401            }
402        };
403        let signed = if negative {
404            magnitude.wrapping_neg() as i128
405        } else {
406            magnitude as i128
407        };
408        (signed, !fractional)
409    }
410}
411
412impl From<f64> for Float128 {
413    fn from(value: f64) -> Self {
414        Self::from_f64(value)
415    }
416}
417
418impl From<f32> for Float128 {
419    fn from(value: f32) -> Self {
420        Self::from_f32(value)
421    }
422}
423
424impl TryFrom<Float128> for f64 {
425    type Error = Inexact<f64>;
426
427    fn try_from(value: Float128) -> Result<Self, Self::Error> {
428        value.to_f64()
429    }
430}
431
432impl TryFrom<Float128> for f32 {
433    type Error = Inexact<f32>;
434
435    fn try_from(value: Float128) -> Result<Self, Self::Error> {
436        value.to_f32()
437    }
438}
439
440impl Float128 {
441    /// A quiet NaN carrying `payload` in its low fraction bits.
442    ///
443    /// A wire test needs a NaN it can recognise on the other side, and
444    /// `Float128::NAN` is one pattern out of 2^113. The payload is
445    /// masked to the bits below the quiet flag, so no argument can
446    /// produce an infinity by accident.
447    #[must_use]
448    pub const fn quiet_nan(payload: u128) -> Self {
449        let fraction = (payload & (QUIET_BIT - 1)) | QUIET_BIT;
450        Self::from_bits(((EXPONENT_MAX as u128) << EXPONENT_SHIFT) | fraction)
451    }
452
453    /// The same, negative.
454    #[must_use]
455    pub const fn neg_quiet_nan(payload: u128) -> Self {
456        Self::from_bits(Self::quiet_nan(payload).to_bits() | SIGN_MASK)
457    }
458}