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}