Skip to main content

triblespace_core/value/schemas/
f256.rs

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