Skip to main content

zerodds_float128/
x87.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The 80-bit extended format, because the field sends it.
4//!
5//! Fast DDS and OpenDDS both map IDL `long double` to the compiler's
6//! `long double` and copy its sixteen bytes onto the wire. On x86-64
7//! that type is the x87 double-extended format — one sign bit, a
8//! fifteen-bit exponent biased by 16383, and a **sixty-four**-bit
9//! significand with an *explicit* integer bit — padded to sixteen bytes
10//! with six bytes the compiler never writes. Measured on Debian 13 /
11//! gcc 14.2, `1.5L` is
12//! `00 00 00 00 00 00 00 c0 ff 3f` followed by six bytes of whatever the
13//! stack held. On aarch64 the same source emits binary128 instead,
14//! because there `long double` *is* binary128.
15//!
16//! So the same vendor, the same IDL and the same value give two
17//! different payloads on two architectures, and a third on a platform
18//! where `long double` is `double` — macOS on Apple silicon, measured at
19//! `LDBL_MANT_DIG = 53`. `docs/float128-column.md` §2 records the runs.
20//!
21//! A binary128 type that could not read those bytes would be conformant
22//! and useless. These conversions are how the reader meets the field
23//! where it is; nothing here writes x87 unless a caller asks for it by
24//! name, and [`Float128::to_x87_extended`] reports what a conversion
25//! costs rather than performing it silently.
26
27use crate::float::{
28    EXPONENT_MAX, EXPONENT_SHIFT, FRACTION_BITS, FRACTION_MASK, Float128, QUIET_BIT,
29    round_shift_right,
30};
31
32/// Significand bits in x87 double-extended, including the explicit
33/// integer bit.
34const X87_SIGNIFICAND_BITS: u32 = 64;
35/// Fraction bits after the explicit integer bit.
36const X87_FRACTION_BITS: u32 = X87_SIGNIFICAND_BITS - 1;
37/// The bias, which binary128 happens to share.
38const X87_BIAS: i32 = 16383;
39/// All-ones exponent.
40const X87_EXPONENT_MAX: u32 = 0x7fff;
41/// The x87 quiet flag, one below the explicit integer bit.
42const X87_QUIET: u64 = 0x4000_0000_0000_0000;
43/// The x87 integer bit, explicit where binary128 leaves it implied.
44const X87_INTEGER: u64 = 0x8000_0000_0000_0000;
45/// NaN payload bits x87 carries: everything below the quiet flag.
46const X87_PAYLOAD_MASK: u64 = X87_QUIET - 1;
47/// How far an x87 payload moves to sit under binary128's quiet bit.
48///
49/// x87 carries 62 payload bits topping out at bit 61; binary128 carries
50/// 111 topping out at bit 110. Aligning the two tops is a shift of 49.
51/// Aligning the two *bottoms* instead would move a NaN's identity into
52/// bits a narrowing conversion then discards.
53const X87_PAYLOAD_SHIFT: u32 = FRACTION_BITS - 1 - 62;
54
55/// An x87 double-extended value: the ten bytes that carry it.
56///
57/// Held as `(exponent_and_sign, significand)` rather than as bytes
58/// because the padding is not part of the value, and a type that carried
59/// the padding would invite a caller to compare two of them and find
60/// them different for a reason that is not the number.
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62pub struct Extended80 {
63    /// Sign in bit 15, biased exponent in bits 0..=14.
64    pub sign_exponent: u16,
65    /// The significand, with the integer bit explicit in bit 63.
66    pub significand: u64,
67}
68
69impl Extended80 {
70    /// From the ten bytes as x86 stores them, least significant first.
71    #[must_use]
72    pub const fn from_le_bytes(bytes: [u8; 10]) -> Self {
73        let significand = u64::from_le_bytes([
74            bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
75        ]);
76        Self {
77            sign_exponent: u16::from_le_bytes([bytes[8], bytes[9]]),
78            significand,
79        }
80    }
81
82    /// The ten bytes, least significant first.
83    #[must_use]
84    pub const fn to_le_bytes(self) -> [u8; 10] {
85        let significand = self.significand.to_le_bytes();
86        let sign_exponent = self.sign_exponent.to_le_bytes();
87        [
88            significand[0],
89            significand[1],
90            significand[2],
91            significand[3],
92            significand[4],
93            significand[5],
94            significand[6],
95            significand[7],
96            sign_exponent[0],
97            sign_exponent[1],
98        ]
99    }
100
101    /// From the sixteen bytes a Fast DDS or OpenDDS payload holds on
102    /// x86-64: the ten that matter, then six the compiler did not write.
103    ///
104    /// **The trailing six bytes are ignored.** They are padding in the
105    /// C object and were measured carrying leftover stack — a reader
106    /// that folded them into the value would decode the same number
107    /// differently on two runs of the same writer.
108    #[must_use]
109    pub const fn from_padded_le_bytes(bytes: [u8; 16]) -> Self {
110        Self::from_le_bytes([
111            bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
112            bytes[8], bytes[9],
113        ])
114    }
115
116    /// The sixteen bytes with the padding **zeroed**.
117    ///
118    /// Zeroed rather than left uninitialised because XTypes 1.3 §7.4.1.1
119    /// requires trailing padding in a serialised sample to be zero, and
120    /// because six bytes of process memory on a multicast group is a
121    /// disclosure whatever the specification says about it.
122    #[must_use]
123    pub const fn to_padded_le_bytes(self) -> [u8; 16] {
124        let ten = self.to_le_bytes();
125        [
126            ten[0], ten[1], ten[2], ten[3], ten[4], ten[5], ten[6], ten[7], ten[8], ten[9], 0, 0,
127            0, 0, 0, 0,
128        ]
129    }
130
131    /// The sign bit.
132    #[must_use]
133    pub const fn is_sign_negative(self) -> bool {
134        self.sign_exponent & 0x8000 != 0
135    }
136
137    /// The biased exponent.
138    #[must_use]
139    pub const fn biased_exponent(self) -> u32 {
140        (self.sign_exponent & 0x7fff) as u32
141    }
142}
143
144impl Float128 {
145    /// From x87 double-extended. Always exact: 64 significand bits fit
146    /// inside 113, and the two formats share an exponent bias and range.
147    ///
148    /// The unnormal and pseudo-* encodings the 8087 defined and the
149    /// 80387 dropped — a cleared integer bit with a non-zero exponent —
150    /// have no binary128 counterpart. They are decoded as what the
151    /// significand and exponent say, which is what every current x86
152    /// treats them as when it does not fault.
153    #[must_use]
154    pub fn from_x87_extended(value: Extended80) -> Self {
155        let negative = value.is_sign_negative();
156        let biased = value.biased_exponent();
157        let significand = value.significand;
158
159        if biased == X87_EXPONENT_MAX {
160            let payload = u128::from(significand & X87_PAYLOAD_MASK);
161            if payload == 0 && significand & X87_QUIET == 0 {
162                return if negative {
163                    Self::NEG_INFINITY
164                } else {
165                    Self::INFINITY
166                };
167            }
168            // A NaN: the 8087 keeps its integer bit set and its quiet
169            // flag one bit below, which is where binary128's quiet bit
170            // is relative to the top of the fraction. Aligning the two
171            // tops keeps a quiet NaN quiet.
172            let quiet = significand & X87_QUIET != 0;
173            let fraction = (payload << X87_PAYLOAD_SHIFT) | if quiet { QUIET_BIT } else { 0 };
174            // A signalling NaN whose whole payload is zero is not a NaN
175            // at all once the flag is dropped; it becomes an infinity.
176            // One bit is set instead, which keeps it a NaN and keeps it
177            // signalling.
178            let fraction = if fraction & FRACTION_MASK == 0 {
179                1
180            } else {
181                fraction
182            };
183            let bits = (u128::from(negative) << 127)
184                | ((EXPONENT_MAX as u128) << EXPONENT_SHIFT)
185                | (fraction & FRACTION_MASK);
186            return Self::from_bits(bits);
187        }
188        if significand == 0 {
189            return if negative { Self::NEG_ZERO } else { Self::ZERO };
190        }
191        // Both formats bias by 16383, so the unbiased exponent carries
192        // straight across; what differs is where the leading one sits.
193        let unbiased = if biased == 0 {
194            1 - X87_BIAS
195        } else {
196            biased as i32 - X87_BIAS
197        };
198        let (value, lost) = Self::from_parts_rounded(
199            negative,
200            u128::from(significand),
201            unbiased - X87_FRACTION_BITS as i32,
202        );
203        debug_assert!(!lost, "64 significand bits always fit inside 113");
204        value
205    }
206
207    /// From the sixteen bytes an x86-64 Fast DDS or OpenDDS writes.
208    ///
209    /// The padding is dropped; see [`Extended80::from_padded_le_bytes`].
210    #[must_use]
211    pub fn from_x87_padded_bytes(bytes: [u8; 16]) -> Self {
212        Self::from_x87_extended(Extended80::from_padded_le_bytes(bytes))
213    }
214
215    /// To x87 double-extended, rounding to nearest with ties to even.
216    ///
217    /// Returns the value and whether the conversion was exact. It is
218    /// inexact whenever the significand needs more than 64 bits, which
219    /// is most of binary128 — that is the whole cost of the x86 dialect,
220    /// and it is returned rather than swallowed so a bridge can refuse.
221    #[must_use]
222    pub fn to_x87_extended(self) -> (Extended80, bool) {
223        let negative = self.is_sign_negative();
224        let sign = if negative { 0x8000u16 } else { 0 };
225
226        if self.is_nan() {
227            let payload = ((self.fraction() & !QUIET_BIT) >> X87_PAYLOAD_SHIFT) as u64;
228            let quiet = self.to_bits() & QUIET_BIT != 0;
229            let significand =
230                X87_INTEGER | if quiet { X87_QUIET } else { 0 } | (payload & X87_PAYLOAD_MASK);
231            // As in the other direction: never let a NaN collapse into
232            // an infinity because its payload fell off the bottom.
233            let significand = if significand & !X87_INTEGER == 0 {
234                significand | 1
235            } else {
236                significand
237            };
238            let lost = self.fraction() & ((1u128 << X87_PAYLOAD_SHIFT) - 1) != 0;
239            return (
240                Extended80 {
241                    sign_exponent: sign | X87_EXPONENT_MAX as u16,
242                    significand,
243                },
244                !lost,
245            );
246        }
247        if self.is_infinite() {
248            return (
249                Extended80 {
250                    sign_exponent: sign | X87_EXPONENT_MAX as u16,
251                    significand: X87_INTEGER,
252                },
253                true,
254            );
255        }
256        let Some((_, significand, exponent)) = self.decompose() else {
257            return (
258                Extended80 {
259                    sign_exponent: sign,
260                    significand: 0,
261                },
262                true,
263            );
264        };
265        if significand == 0 {
266            return (
267                Extended80 {
268                    sign_exponent: sign,
269                    significand: 0,
270                },
271                true,
272            );
273        }
274
275        let leading = 127 - significand.leading_zeros();
276        let unbiased = exponent + leading as i32;
277        let biased = unbiased + X87_BIAS;
278        if biased >= X87_EXPONENT_MAX as i32 {
279            // Cannot happen from a finite binary128, whose exponent
280            // range is exactly x87's, but a caller reading this wants to
281            // see the case handled rather than assumed away.
282            return (
283                Extended80 {
284                    sign_exponent: sign | X87_EXPONENT_MAX as u16,
285                    significand: X87_INTEGER,
286                },
287                false,
288            );
289        }
290
291        if biased <= 0 {
292            // Positive by construction: `biased` is at most zero here. And
293            // `rounded` is a subnormal significand, so it is below 2^63 and
294            // the mask below cannot be reached — it is there so the width is
295            // a property of the expression and not of a branch above it.
296            let drop = (leading as i32 - X87_FRACTION_BITS as i32 + (1 - biased))
297                .max(0)
298                .unsigned_abs();
299            let (rounded, lost) = round_shift_right(significand, drop);
300            let extended = Extended80 {
301                sign_exponent: sign,
302                significand: (rounded & u128::from(u64::MAX)) as u64,
303            };
304            return (extended, !lost);
305        }
306
307        let (fitted, lost) = if leading >= X87_FRACTION_BITS {
308            round_shift_right(significand, leading - X87_FRACTION_BITS)
309        } else {
310            (significand << (X87_FRACTION_BITS - leading), false)
311        };
312        // Rounding up can carry out of the 64-bit significand.
313        let (fitted, biased) = if fitted >> X87_SIGNIFICAND_BITS != 0 {
314            (fitted >> 1, biased + 1)
315        } else {
316            (fitted, biased)
317        };
318        if biased >= X87_EXPONENT_MAX as i32 {
319            return (
320                Extended80 {
321                    sign_exponent: sign | X87_EXPONENT_MAX as u16,
322                    significand: X87_INTEGER,
323                },
324                false,
325            );
326        }
327        // `biased` was checked into `1..X87_EXPONENT_MAX` above and
328        // `fitted` into 64 bits, so both narrowings are exact. Masking
329        // makes that a property of the expression rather than of a check
330        // three branches away, and it leaves no fallback to be wrong: a
331        // conversion that answered zero here would produce a subnormal,
332        // which is a different number and not an error.
333        let extended = Extended80 {
334            sign_exponent: sign | ((biased as u32 & X87_EXPONENT_MAX) as u16),
335            significand: (fitted & u128::from(u64::MAX)) as u64,
336        };
337        (extended, !lost)
338    }
339
340    /// To the sixteen bytes an x86-64 `long double` occupies, padding
341    /// zeroed.
342    ///
343    /// The boolean is [`Float128::to_x87_extended`]'s: true when the
344    /// value survived.
345    #[must_use]
346    pub fn to_x87_padded_bytes(self) -> ([u8; 16], bool) {
347        let (extended, exact) = self.to_x87_extended();
348        (extended.to_padded_le_bytes(), exact)
349    }
350}
351
352/// Whether a sixteen-byte payload is *plausibly* x87 rather than
353/// binary128.
354///
355/// There is no flag on the wire saying which dialect a peer used, so
356/// this is a heuristic and is named one. It answers the question a
357/// diagnostic asks — "these bytes decoded to something absurd, is the
358/// peer an x86 Fast DDS?" — and nothing decides a decode on it.
359///
360/// The signal is structural: x87 sets an explicit integer bit at bit 63
361/// of the low eight bytes for every normal value, and binary128 puts its
362/// exponent there. A binary128 pattern with bit 63 set and bits 64..=79
363/// looking like a plausible x87 exponent is possible, so a `true` here
364/// is a suggestion, never a verdict.
365#[must_use]
366pub fn looks_like_x87(bytes: [u8; 16]) -> bool {
367    let padding = &bytes[10..];
368    if padding.iter().any(|&byte| byte != 0) {
369        // Non-zero in the six bytes x87 never writes. Either it is
370        // binary128, or it is an x86 writer leaking its stack — which is
371        // what Fast DDS was measured doing, so this is not a decision.
372        return false;
373    }
374    let extended = Extended80::from_padded_le_bytes(bytes);
375    let biased = extended.biased_exponent();
376    let integer_bit = extended.significand & X87_INTEGER != 0;
377    if biased == 0 {
378        return extended.significand == 0 || !integer_bit;
379    }
380    integer_bit
381}