Skip to main content

triblespace_core/inline/encodings/
f256.rs

1use crate::inline::Encodes;
2use crate::id::ExclusiveId;
3use crate::id::Id;
4use crate::id_hex;
5use crate::macros::entity;
6use crate::metadata;
7use crate::metadata::MetaDescribe;
8use crate::trible::Fragment;
9use crate::inline::IntoInline;
10use crate::inline::TryFromInline;
11use crate::inline::TryToInline;
12use crate::inline::Inline;
13use crate::inline::InlineEncoding;
14use std::convert::Infallible;
15use std::fmt;
16
17use f256::f256;
18use serde_json::Number as JsonNumber;
19
20/// A inline encoding for a 256-bit floating point number in little-endian byte order.
21pub struct F256LE;
22
23/// A inline encoding for a 256-bit floating point number in big-endian byte order.
24pub struct F256BE;
25
26/// Type alias for [`F256LE`], the default little-endian 256-bit float schema.
27pub type F256 = F256LE;
28
29impl MetaDescribe for F256LE {
30    fn describe() -> Fragment {
31        let id: Id = id_hex!("D9A419D3CAA0D8E05D8DAB950F5E80F2");
32        #[allow(unused_mut)]
33        let mut tribles = entity! {
34            ExclusiveId::force_ref(&id) @
35                metadata::name: "f256le",
36                metadata::description: "High-precision f256 float stored in little-endian byte order. The format preserves far more precision than f64 and can round-trip large JSON numbers.\n\nUse when precision or exact decimal import matters more than storage or compute cost. Choose the big-endian variant if you need lexicographic ordering or network byte order.\n\nF256 values are heavier to parse and compare than f64. If you only need standard double precision, prefer F64 for faster operations.",
37                metadata::tag: metadata::KIND_INLINE_ENCODING,
38        };
39
40        #[cfg(feature = "wasm")]
41        {
42            tribles += entity! { ExclusiveId::force_ref(&id) @
43                metadata::value_formatter: wasm_formatter::F256_LE_WASM,
44            };
45        }
46        tribles
47    }
48}
49impl InlineEncoding for F256LE {
50    type ValidationError = Infallible;
51    type Encoding = Self;
52}
53impl MetaDescribe for F256BE {
54    fn describe() -> Fragment {
55        let id: Id = id_hex!("A629176D4656928D96B155038F9F2220");
56        #[allow(unused_mut)]
57        let mut tribles = entity! {
58            ExclusiveId::force_ref(&id) @
59                metadata::name: "f256be",
60                metadata::description: "High-precision f256 float stored in big-endian byte order. This variant is convenient for bytewise ordering or wire formats that expect network order.\n\nUse for high-precision metrics or lossless JSON import when ordering matters across systems. For everyday numeric values, F64 is smaller and faster.\n\nAs with all floats, rounding can still occur at the chosen precision. If you need exact fractions, use R256 instead.",
61                metadata::tag: metadata::KIND_INLINE_ENCODING,
62        };
63
64        #[cfg(feature = "wasm")]
65        {
66            tribles += entity! { ExclusiveId::force_ref(&id) @
67                metadata::value_formatter: wasm_formatter::F256_BE_WASM,
68            };
69        }
70        tribles
71    }
72}
73impl InlineEncoding for F256BE {
74    type ValidationError = Infallible;
75    type Encoding = Self;
76}
77
78#[cfg(feature = "wasm")]
79mod wasm_formatter {
80    use core::fmt::Write;
81
82    use triblespace_core_macros::value_formatter;
83
84    #[value_formatter(const_wasm = F256_LE_WASM)]
85    pub(crate) fn f256_le(raw: &[u8; 32], out: &mut impl Write) -> Result<(), u32> {
86        let mut buf = [0u8; 16];
87        buf.copy_from_slice(&raw[0..16]);
88        let lo = u128::from_le_bytes(buf);
89        buf.copy_from_slice(&raw[16..32]);
90        let hi = u128::from_le_bytes(buf);
91
92        const EXP_BITS: u32 = 19;
93        const HI_FRACTION_BITS: u32 = 108;
94        const EXP_MAX: u32 = (1u32 << EXP_BITS) - 1;
95        const EXP_BIAS: i32 = (EXP_MAX >> 1) as i32;
96
97        const HI_SIGN_MASK: u128 = 1u128 << 127;
98        const HI_EXP_MASK: u128 = (EXP_MAX as u128) << HI_FRACTION_BITS;
99        const HI_FRACTION_MASK: u128 = (1u128 << HI_FRACTION_BITS) - 1;
100
101        let sign = (hi & HI_SIGN_MASK) != 0;
102        let exp = ((hi & HI_EXP_MASK) >> HI_FRACTION_BITS) as u32;
103
104        let frac_hi = hi & HI_FRACTION_MASK;
105        let frac_lo = lo;
106        let fraction_is_zero = frac_hi == 0 && frac_lo == 0;
107
108        if exp == EXP_MAX {
109            let text = if fraction_is_zero {
110                if sign {
111                    "-inf"
112                } else {
113                    "inf"
114                }
115            } else {
116                "nan"
117            };
118            out.write_str(text).map_err(|_| 1u32)?;
119            return Ok(());
120        }
121
122        if exp == 0 && fraction_is_zero {
123            let text = if sign { "-0" } else { "0" };
124            out.write_str(text).map_err(|_| 1u32)?;
125            return Ok(());
126        }
127
128        const HEX: &[u8; 16] = b"0123456789ABCDEF";
129
130        if sign {
131            out.write_char('-').map_err(|_| 1u32)?;
132        }
133
134        let exp2 = if exp == 0 {
135            1 - EXP_BIAS
136        } else {
137            exp as i32 - EXP_BIAS
138        };
139        if exp == 0 {
140            out.write_str("0x0").map_err(|_| 1u32)?;
141        } else {
142            out.write_str("0x1").map_err(|_| 1u32)?;
143        }
144
145        let mut digits = [0u8; 59];
146        for i in 0..27 {
147            let shift = (26 - i) * 4;
148            let nibble = ((frac_hi >> shift) & 0xF) as usize;
149            digits[i] = HEX[nibble];
150        }
151        for i in 0..32 {
152            let shift = (31 - i) * 4;
153            let nibble = ((frac_lo >> shift) & 0xF) as usize;
154            digits[27 + i] = HEX[nibble];
155        }
156
157        let mut end = digits.len();
158        while end > 0 && digits[end - 1] == b'0' {
159            end -= 1;
160        }
161        if end > 0 {
162            out.write_char('.').map_err(|_| 1u32)?;
163            for &b in &digits[0..end] {
164                out.write_char(b as char).map_err(|_| 1u32)?;
165            }
166        }
167
168        write!(out, "p{exp2:+}").map_err(|_| 1u32)?;
169        Ok(())
170    }
171
172    #[value_formatter(const_wasm = F256_BE_WASM)]
173    pub(crate) fn f256_be(raw: &[u8; 32], out: &mut impl Write) -> Result<(), u32> {
174        let mut buf = [0u8; 16];
175        buf.copy_from_slice(&raw[0..16]);
176        let hi = u128::from_be_bytes(buf);
177        buf.copy_from_slice(&raw[16..32]);
178        let lo = u128::from_be_bytes(buf);
179
180        const EXP_BITS: u32 = 19;
181        const HI_FRACTION_BITS: u32 = 108;
182        const EXP_MAX: u32 = (1u32 << EXP_BITS) - 1;
183        const EXP_BIAS: i32 = (EXP_MAX >> 1) as i32;
184
185        const HI_SIGN_MASK: u128 = 1u128 << 127;
186        const HI_EXP_MASK: u128 = (EXP_MAX as u128) << HI_FRACTION_BITS;
187        const HI_FRACTION_MASK: u128 = (1u128 << HI_FRACTION_BITS) - 1;
188
189        let sign = (hi & HI_SIGN_MASK) != 0;
190        let exp = ((hi & HI_EXP_MASK) >> HI_FRACTION_BITS) as u32;
191
192        let frac_hi = hi & HI_FRACTION_MASK;
193        let frac_lo = lo;
194        let fraction_is_zero = frac_hi == 0 && frac_lo == 0;
195
196        if exp == EXP_MAX {
197            let text = if fraction_is_zero {
198                if sign {
199                    "-inf"
200                } else {
201                    "inf"
202                }
203            } else {
204                "nan"
205            };
206            out.write_str(text).map_err(|_| 1u32)?;
207            return Ok(());
208        }
209
210        if exp == 0 && fraction_is_zero {
211            let text = if sign { "-0" } else { "0" };
212            out.write_str(text).map_err(|_| 1u32)?;
213            return Ok(());
214        }
215
216        const HEX: &[u8; 16] = b"0123456789ABCDEF";
217
218        if sign {
219            out.write_char('-').map_err(|_| 1u32)?;
220        }
221
222        let exp2 = if exp == 0 {
223            1 - EXP_BIAS
224        } else {
225            exp as i32 - EXP_BIAS
226        };
227        if exp == 0 {
228            out.write_str("0x0").map_err(|_| 1u32)?;
229        } else {
230            out.write_str("0x1").map_err(|_| 1u32)?;
231        }
232
233        let mut digits = [0u8; 59];
234        for i in 0..27 {
235            let shift = (26 - i) * 4;
236            let nibble = ((frac_hi >> shift) & 0xF) as usize;
237            digits[i] = HEX[nibble];
238        }
239        for i in 0..32 {
240            let shift = (31 - i) * 4;
241            let nibble = ((frac_lo >> shift) & 0xF) as usize;
242            digits[27 + i] = HEX[nibble];
243        }
244
245        let mut end = digits.len();
246        while end > 0 && digits[end - 1] == b'0' {
247            end -= 1;
248        }
249        if end > 0 {
250            out.write_char('.').map_err(|_| 1u32)?;
251            for &b in &digits[0..end] {
252                out.write_char(b as char).map_err(|_| 1u32)?;
253            }
254        }
255
256        write!(out, "p{exp2:+}").map_err(|_| 1u32)?;
257        Ok(())
258    }
259}
260
261impl TryFromInline<'_, F256BE> for f256 {
262    type Error = Infallible;
263    fn try_from_inline(v: &Inline<F256BE>) -> Result<Self, Infallible> {
264        Ok(f256::from_be_bytes(v.raw))
265    }
266}
267
268impl Encodes<f256> for F256BE
269{
270    type Output = Inline<F256BE>;
271    fn encode(source: f256) -> Inline<F256BE> {
272        Inline::new(source.to_be_bytes())
273    }
274}
275
276impl TryFromInline<'_, F256LE> for f256 {
277    type Error = Infallible;
278    fn try_from_inline(v: &Inline<F256LE>) -> Result<Self, Infallible> {
279        Ok(f256::from_le_bytes(v.raw))
280    }
281}
282
283impl Encodes<f256> for F256LE
284{
285    type Output = Inline<F256LE>;
286    fn encode(source: f256) -> Inline<F256LE> {
287        Inline::new(source.to_le_bytes())
288    }
289}
290
291/// Errors encountered when converting JSON numbers into [`F256`] values.
292#[derive(Debug, Clone, PartialEq)]
293pub enum JsonNumberToF256Error {
294    /// The numeric value could not be represented as an `f256`.
295    Unrepresentable,
296}
297
298impl fmt::Display for JsonNumberToF256Error {
299    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300        match self {
301            JsonNumberToF256Error::Unrepresentable => {
302                write!(f, "number is too large to represent as f256")
303            }
304        }
305    }
306}
307
308impl std::error::Error for JsonNumberToF256Error {}
309
310impl TryToInline<F256> for JsonNumber {
311    type Error = JsonNumberToF256Error;
312
313    fn try_to_inline(self) -> Result<Inline<F256>, Self::Error> {
314        (&self).try_to_inline()
315    }
316}
317
318impl TryToInline<F256> for &JsonNumber {
319    type Error = JsonNumberToF256Error;
320
321    fn try_to_inline(self) -> Result<Inline<F256>, Self::Error> {
322        if let Some(value) = self.as_u128() {
323            return Ok(f256::from(value).to_inline());
324        }
325        if let Some(value) = self.as_i128() {
326            return Ok(f256::from(value).to_inline());
327        }
328        if let Some(value) = self.as_f64() {
329            return Ok(f256::from(value).to_inline());
330        }
331        Err(JsonNumberToF256Error::Unrepresentable)
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338    use crate::inline::{IntoInline, TryToInline};
339    use ::f256::f256;
340    use proptest::prelude::*;
341
342    /// Generate an f256 from an f64, filtering out NaN (NaN != NaN).
343    fn arb_f256_non_nan() -> impl Strategy<Value = f256> {
344        any::<f64>()
345            .prop_filter("not NaN", |v| !v.is_nan())
346            .prop_map(f256::from)
347    }
348
349    proptest! {
350        #[test]
351        fn f256le_roundtrip(input in arb_f256_non_nan()) {
352            let value: Inline<F256LE> = input.to_inline();
353            let output: f256 = value.from_inline();
354            prop_assert_eq!(input, output);
355        }
356
357        #[test]
358        fn f256be_roundtrip(input in arb_f256_non_nan()) {
359            let value: Inline<F256BE> = input.to_inline();
360            let output: f256 = value.from_inline();
361            prop_assert_eq!(input, output);
362        }
363
364        #[test]
365        fn f256le_validates(input in arb_f256_non_nan()) {
366            let value: Inline<F256LE> = input.to_inline();
367            prop_assert!(F256LE::validate(value).is_ok());
368        }
369
370        #[test]
371        fn f256be_validates(input in arb_f256_non_nan()) {
372            let value: Inline<F256BE> = input.to_inline();
373            prop_assert!(F256BE::validate(value).is_ok());
374        }
375
376        #[test]
377        fn f256_le_and_be_differ(input in arb_f256_non_nan().prop_filter("non-zero", |v| *v != f256::ZERO)) {
378            let le_val: Inline<F256LE> = input.to_inline();
379            let be_val: Inline<F256BE> = input.to_inline();
380            prop_assert_ne!(le_val.raw, be_val.raw);
381        }
382
383        #[test]
384        fn json_number_u128_roundtrip(input: u64) {
385            let s = input.to_string();
386            let num: JsonNumber = serde_json::from_str(&s).unwrap();
387            let value: Inline<F256> = num.try_to_inline().expect("valid number");
388            let output: f256 = value.from_inline();
389            prop_assert_eq!(output, f256::from(input as u128));
390        }
391
392        #[test]
393        fn json_number_negative_roundtrip(input in any::<i64>().prop_filter("negative", |v| *v < 0)) {
394            let s = input.to_string();
395            let num: JsonNumber = serde_json::from_str(&s).unwrap();
396            let value: Inline<F256> = num.try_to_inline().expect("valid number");
397            let output: f256 = value.from_inline();
398            prop_assert_eq!(output, f256::from(input as i128));
399        }
400
401        #[test]
402        fn json_number_f64_roundtrip(input in any::<f64>().prop_filter("finite", |v| v.is_finite())) {
403            let s = ryu::Buffer::new().format(input).to_string();
404            let num: JsonNumber = serde_json::from_str(&s).unwrap();
405            // Compare via &JsonNumber so we can also inspect the parsed value.
406            let expected = f256::from(num.as_f64().unwrap());
407            let value: Inline<F256> = (&num).try_to_inline().expect("valid number");
408            let output: f256 = value.from_inline();
409            // Compare against what serde_json actually parsed (via as_f64),
410            // not the original f64, since JSON string round-tripping can
411            // shift the least-significant bit.
412            prop_assert_eq!(output, expected);
413        }
414
415        #[test]
416        fn json_number_ref_roundtrip(input: u64) {
417            let s = input.to_string();
418            let num: JsonNumber = serde_json::from_str(&s).unwrap();
419            let value: Inline<F256> = (&num).try_to_inline().expect("valid ref number");
420            let output: f256 = value.from_inline();
421            prop_assert_eq!(output, f256::from(input as u128));
422        }
423    }
424
425    // NaN round-trip must use is_nan() since NaN != NaN.
426    #[test]
427    fn f256_le_roundtrip_nan() {
428        let input = f256::NAN;
429        let value: Inline<F256LE> = input.to_inline();
430        let output: f256 = value.from_inline();
431        assert!(output.is_nan());
432    }
433}