triblespace-core 0.35.0

The triblespace core implementation.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
use crate::id::ExclusiveId;
use crate::id::Id;
use crate::id_hex;
use crate::macros::entity;
use crate::metadata;
use crate::metadata::{ConstDescribe, ConstId};
use crate::repo::BlobStore;
use crate::trible::Fragment;
use crate::value::schemas::hash::Blake3;
use crate::value::ToValue;
use crate::value::TryFromValue;
use crate::value::TryToValue;
use crate::value::Value;
use crate::value::ValueSchema;
use std::convert::Infallible;
use std::fmt;

use f256::f256;
use serde_json::Number as JsonNumber;

/// A value schema for a 256-bit floating point number in little-endian byte order.
pub struct F256LE;

impl ConstId for F256LE {
    const ID: Id = id_hex!("D9A419D3CAA0D8E05D8DAB950F5E80F2");
}

/// A value schema for a 256-bit floating point number in big-endian byte order.
pub struct F256BE;

impl ConstId for F256BE {
    const ID: Id = id_hex!("A629176D4656928D96B155038F9F2220");
}

/// Type alias for [`F256LE`], the default little-endian 256-bit float schema.
pub type F256 = F256LE;

impl ConstDescribe for F256LE {
    fn describe<B>(blobs: &mut B) -> Result<Fragment, B::PutError>
    where
        B: BlobStore<Blake3>,
    {
        let id = Self::ID;
        let description = blobs.put(
            "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.",
        )?;
        let tribles = entity! {
            ExclusiveId::force_ref(&id) @
                metadata::name: blobs.put("f256le")?,
                metadata::description: description,
                metadata::tag: metadata::KIND_VALUE_SCHEMA,
        };

        #[cfg(feature = "wasm")]
        let tribles = {
            let mut tribles = tribles;
            tribles += entity! { ExclusiveId::force_ref(&id) @
                metadata::value_formatter: blobs.put(wasm_formatter::F256_LE_WASM)?,
            };
            tribles
        };
        Ok(tribles)
    }
}
impl ValueSchema for F256LE {
    type ValidationError = Infallible;
}
impl ConstDescribe for F256BE {
    fn describe<B>(blobs: &mut B) -> Result<Fragment, B::PutError>
    where
        B: BlobStore<Blake3>,
    {
        let id = Self::ID;
        let description = blobs.put(
            "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.",
        )?;
        let tribles = entity! {
            ExclusiveId::force_ref(&id) @
                metadata::name: blobs.put("f256be")?,
                metadata::description: description,
                metadata::tag: metadata::KIND_VALUE_SCHEMA,
        };

        #[cfg(feature = "wasm")]
        let tribles = {
            let mut tribles = tribles;
            tribles += entity! { ExclusiveId::force_ref(&id) @
                metadata::value_formatter: blobs.put(wasm_formatter::F256_BE_WASM)?,
            };
            tribles
        };
        Ok(tribles)
    }
}
impl ValueSchema for F256BE {
    type ValidationError = Infallible;
}

#[cfg(feature = "wasm")]
mod wasm_formatter {
    use core::fmt::Write;

    use triblespace_core_macros::value_formatter;

    #[value_formatter(const_wasm = F256_LE_WASM)]
    pub(crate) fn f256_le(raw: &[u8; 32], out: &mut impl Write) -> Result<(), u32> {
        let mut buf = [0u8; 16];
        buf.copy_from_slice(&raw[0..16]);
        let lo = u128::from_le_bytes(buf);
        buf.copy_from_slice(&raw[16..32]);
        let hi = u128::from_le_bytes(buf);

        const EXP_BITS: u32 = 19;
        const HI_FRACTION_BITS: u32 = 108;
        const EXP_MAX: u32 = (1u32 << EXP_BITS) - 1;
        const EXP_BIAS: i32 = (EXP_MAX >> 1) as i32;

        const HI_SIGN_MASK: u128 = 1u128 << 127;
        const HI_EXP_MASK: u128 = (EXP_MAX as u128) << HI_FRACTION_BITS;
        const HI_FRACTION_MASK: u128 = (1u128 << HI_FRACTION_BITS) - 1;

        let sign = (hi & HI_SIGN_MASK) != 0;
        let exp = ((hi & HI_EXP_MASK) >> HI_FRACTION_BITS) as u32;

        let frac_hi = hi & HI_FRACTION_MASK;
        let frac_lo = lo;
        let fraction_is_zero = frac_hi == 0 && frac_lo == 0;

        if exp == EXP_MAX {
            let text = if fraction_is_zero {
                if sign {
                    "-inf"
                } else {
                    "inf"
                }
            } else {
                "nan"
            };
            out.write_str(text).map_err(|_| 1u32)?;
            return Ok(());
        }

        if exp == 0 && fraction_is_zero {
            let text = if sign { "-0" } else { "0" };
            out.write_str(text).map_err(|_| 1u32)?;
            return Ok(());
        }

        const HEX: &[u8; 16] = b"0123456789ABCDEF";

        if sign {
            out.write_char('-').map_err(|_| 1u32)?;
        }

        let exp2 = if exp == 0 {
            1 - EXP_BIAS
        } else {
            exp as i32 - EXP_BIAS
        };
        if exp == 0 {
            out.write_str("0x0").map_err(|_| 1u32)?;
        } else {
            out.write_str("0x1").map_err(|_| 1u32)?;
        }

        let mut digits = [0u8; 59];
        for i in 0..27 {
            let shift = (26 - i) * 4;
            let nibble = ((frac_hi >> shift) & 0xF) as usize;
            digits[i] = HEX[nibble];
        }
        for i in 0..32 {
            let shift = (31 - i) * 4;
            let nibble = ((frac_lo >> shift) & 0xF) as usize;
            digits[27 + i] = HEX[nibble];
        }

        let mut end = digits.len();
        while end > 0 && digits[end - 1] == b'0' {
            end -= 1;
        }
        if end > 0 {
            out.write_char('.').map_err(|_| 1u32)?;
            for &b in &digits[0..end] {
                out.write_char(b as char).map_err(|_| 1u32)?;
            }
        }

        write!(out, "p{exp2:+}").map_err(|_| 1u32)?;
        Ok(())
    }

    #[value_formatter(const_wasm = F256_BE_WASM)]
    pub(crate) fn f256_be(raw: &[u8; 32], out: &mut impl Write) -> Result<(), u32> {
        let mut buf = [0u8; 16];
        buf.copy_from_slice(&raw[0..16]);
        let hi = u128::from_be_bytes(buf);
        buf.copy_from_slice(&raw[16..32]);
        let lo = u128::from_be_bytes(buf);

        const EXP_BITS: u32 = 19;
        const HI_FRACTION_BITS: u32 = 108;
        const EXP_MAX: u32 = (1u32 << EXP_BITS) - 1;
        const EXP_BIAS: i32 = (EXP_MAX >> 1) as i32;

        const HI_SIGN_MASK: u128 = 1u128 << 127;
        const HI_EXP_MASK: u128 = (EXP_MAX as u128) << HI_FRACTION_BITS;
        const HI_FRACTION_MASK: u128 = (1u128 << HI_FRACTION_BITS) - 1;

        let sign = (hi & HI_SIGN_MASK) != 0;
        let exp = ((hi & HI_EXP_MASK) >> HI_FRACTION_BITS) as u32;

        let frac_hi = hi & HI_FRACTION_MASK;
        let frac_lo = lo;
        let fraction_is_zero = frac_hi == 0 && frac_lo == 0;

        if exp == EXP_MAX {
            let text = if fraction_is_zero {
                if sign {
                    "-inf"
                } else {
                    "inf"
                }
            } else {
                "nan"
            };
            out.write_str(text).map_err(|_| 1u32)?;
            return Ok(());
        }

        if exp == 0 && fraction_is_zero {
            let text = if sign { "-0" } else { "0" };
            out.write_str(text).map_err(|_| 1u32)?;
            return Ok(());
        }

        const HEX: &[u8; 16] = b"0123456789ABCDEF";

        if sign {
            out.write_char('-').map_err(|_| 1u32)?;
        }

        let exp2 = if exp == 0 {
            1 - EXP_BIAS
        } else {
            exp as i32 - EXP_BIAS
        };
        if exp == 0 {
            out.write_str("0x0").map_err(|_| 1u32)?;
        } else {
            out.write_str("0x1").map_err(|_| 1u32)?;
        }

        let mut digits = [0u8; 59];
        for i in 0..27 {
            let shift = (26 - i) * 4;
            let nibble = ((frac_hi >> shift) & 0xF) as usize;
            digits[i] = HEX[nibble];
        }
        for i in 0..32 {
            let shift = (31 - i) * 4;
            let nibble = ((frac_lo >> shift) & 0xF) as usize;
            digits[27 + i] = HEX[nibble];
        }

        let mut end = digits.len();
        while end > 0 && digits[end - 1] == b'0' {
            end -= 1;
        }
        if end > 0 {
            out.write_char('.').map_err(|_| 1u32)?;
            for &b in &digits[0..end] {
                out.write_char(b as char).map_err(|_| 1u32)?;
            }
        }

        write!(out, "p{exp2:+}").map_err(|_| 1u32)?;
        Ok(())
    }
}

impl TryFromValue<'_, F256BE> for f256 {
    type Error = Infallible;
    fn try_from_value(v: &Value<F256BE>) -> Result<Self, Infallible> {
        Ok(f256::from_be_bytes(v.raw))
    }
}

impl ToValue<F256BE> for f256 {
    fn to_value(self) -> Value<F256BE> {
        Value::new(self.to_be_bytes())
    }
}

impl TryFromValue<'_, F256LE> for f256 {
    type Error = Infallible;
    fn try_from_value(v: &Value<F256LE>) -> Result<Self, Infallible> {
        Ok(f256::from_le_bytes(v.raw))
    }
}

impl ToValue<F256LE> for f256 {
    fn to_value(self) -> Value<F256LE> {
        Value::new(self.to_le_bytes())
    }
}

/// Errors encountered when converting JSON numbers into [`F256`] values.
#[derive(Debug, Clone, PartialEq)]
pub enum JsonNumberToF256Error {
    /// The numeric value could not be represented as an `f256`.
    Unrepresentable,
}

impl fmt::Display for JsonNumberToF256Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            JsonNumberToF256Error::Unrepresentable => {
                write!(f, "number is too large to represent as f256")
            }
        }
    }
}

impl std::error::Error for JsonNumberToF256Error {}

impl TryToValue<F256> for JsonNumber {
    type Error = JsonNumberToF256Error;

    fn try_to_value(self) -> Result<Value<F256>, Self::Error> {
        (&self).try_to_value()
    }
}

impl TryToValue<F256> for &JsonNumber {
    type Error = JsonNumberToF256Error;

    fn try_to_value(self) -> Result<Value<F256>, Self::Error> {
        if let Some(value) = self.as_u128() {
            return Ok(f256::from(value).to_value());
        }
        if let Some(value) = self.as_i128() {
            return Ok(f256::from(value).to_value());
        }
        if let Some(value) = self.as_f64() {
            return Ok(f256::from(value).to_value());
        }
        Err(JsonNumberToF256Error::Unrepresentable)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::value::{ToValue, TryToValue};
    use ::f256::f256;
    use proptest::prelude::*;

    /// Generate an f256 from an f64, filtering out NaN (NaN != NaN).
    fn arb_f256_non_nan() -> impl Strategy<Value = f256> {
        any::<f64>()
            .prop_filter("not NaN", |v| !v.is_nan())
            .prop_map(f256::from)
    }

    proptest! {
        #[test]
        fn f256le_roundtrip(input in arb_f256_non_nan()) {
            let value: Value<F256LE> = input.to_value();
            let output: f256 = value.from_value();
            prop_assert_eq!(input, output);
        }

        #[test]
        fn f256be_roundtrip(input in arb_f256_non_nan()) {
            let value: Value<F256BE> = input.to_value();
            let output: f256 = value.from_value();
            prop_assert_eq!(input, output);
        }

        #[test]
        fn f256le_validates(input in arb_f256_non_nan()) {
            let value: Value<F256LE> = input.to_value();
            prop_assert!(F256LE::validate(value).is_ok());
        }

        #[test]
        fn f256be_validates(input in arb_f256_non_nan()) {
            let value: Value<F256BE> = input.to_value();
            prop_assert!(F256BE::validate(value).is_ok());
        }

        #[test]
        fn f256_le_and_be_differ(input in arb_f256_non_nan().prop_filter("non-zero", |v| *v != f256::ZERO)) {
            let le_val: Value<F256LE> = input.to_value();
            let be_val: Value<F256BE> = input.to_value();
            prop_assert_ne!(le_val.raw, be_val.raw);
        }

        #[test]
        fn json_number_u128_roundtrip(input: u64) {
            let s = input.to_string();
            let num: JsonNumber = serde_json::from_str(&s).unwrap();
            let value: Value<F256> = num.try_to_value().expect("valid number");
            let output: f256 = value.from_value();
            prop_assert_eq!(output, f256::from(input as u128));
        }

        #[test]
        fn json_number_negative_roundtrip(input in any::<i64>().prop_filter("negative", |v| *v < 0)) {
            let s = input.to_string();
            let num: JsonNumber = serde_json::from_str(&s).unwrap();
            let value: Value<F256> = num.try_to_value().expect("valid number");
            let output: f256 = value.from_value();
            prop_assert_eq!(output, f256::from(input as i128));
        }

        #[test]
        fn json_number_f64_roundtrip(input in any::<f64>().prop_filter("finite", |v| v.is_finite())) {
            let s = ryu::Buffer::new().format(input).to_string();
            let num: JsonNumber = serde_json::from_str(&s).unwrap();
            // Compare via &JsonNumber so we can also inspect the parsed value.
            let expected = f256::from(num.as_f64().unwrap());
            let value: Value<F256> = (&num).try_to_value().expect("valid number");
            let output: f256 = value.from_value();
            // Compare against what serde_json actually parsed (via as_f64),
            // not the original f64, since JSON string round-tripping can
            // shift the least-significant bit.
            prop_assert_eq!(output, expected);
        }

        #[test]
        fn json_number_ref_roundtrip(input: u64) {
            let s = input.to_string();
            let num: JsonNumber = serde_json::from_str(&s).unwrap();
            let value: Value<F256> = (&num).try_to_value().expect("valid ref number");
            let output: f256 = value.from_value();
            prop_assert_eq!(output, f256::from(input as u128));
        }
    }

    // NaN round-trip must use is_nan() since NaN != NaN.
    #[test]
    fn f256_le_roundtrip_nan() {
        let input = f256::NAN;
        let value: Value<F256LE> = input.to_value();
        let output: f256 = value.from_value();
        assert!(output.is_nan());
    }
}