triblespace-core 0.41.2

The triblespace core implementation.
Documentation
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
use crate::inline::Encodes;
use crate::id::ExclusiveId;
use crate::id::Id;
use crate::id_hex;
use crate::macros::entity;
use crate::metadata;
use crate::metadata::MetaDescribe;
use crate::trible::Fragment;
use crate::inline::IntoInline;
use crate::inline::TryFromInline;
use crate::inline::TryToInline;
use crate::inline::Inline;
use crate::inline::InlineEncoding;
use std::convert::Infallible;

use std::convert::TryInto;

use hifitime::prelude::*;

/// A inline encoding for a TAI interval (order-preserving big-endian).
///
/// A TAI interval is a pair of TAI epochs stored as two 128-bit signed
/// integers (lower, upper) in **order-preserving big-endian** byte order.
/// Both bounds are inclusive.
///
/// Each i128 is XOR'd with the sign bit (mapping i128::MIN to 0, 0 to 2^127,
/// i128::MAX to u128::MAX) then written big-endian. Byte-lexicographic order
/// matches numeric order across the full i128 range, enabling efficient range
/// scans on the trie.
pub struct NsTAIInterval;

impl MetaDescribe for NsTAIInterval {
    fn describe() -> Fragment {
        let id: Id = id_hex!("2170014368272A2B1B18B86B1F1F1CB5");
        entity! {
            ExclusiveId::force_ref(&id) @
                metadata::name: "nstai_interval_be",
                metadata::description: "Inclusive TAI interval encoded as two offset-big-endian i128 nanosecond bounds. Each i128 is XOR'd with i128::MIN then stored big-endian, so byte-lexicographic order matches numeric order. This enables efficient range scans on ordered indexes.\n\nSemantically identical to the legacy LE encoding — same inclusive bounds, same TAI monotonic time.",
                metadata::tag: metadata::KIND_INLINE_ENCODING,
        }
    }
}

const SIGN_BIT: u128 = 1u128 << 127;

/// Encode i128 as order-preserving big-endian: flip sign bit, then BE.
/// Maps i128::MIN→0, 0→2^127, i128::MAX→u128::MAX.
pub(crate) fn i128_to_ordered_be(v: i128) -> [u8; 16] {
    ((v as u128) ^ SIGN_BIT).to_be_bytes()
}

/// Decode order-preserving big-endian back to i128.
pub(crate) fn i128_from_ordered_be(bytes: [u8; 16]) -> i128 {
    (u128::from_be_bytes(bytes) ^ SIGN_BIT) as i128
}

impl InlineEncoding for NsTAIInterval {
    type ValidationError = InvertedIntervalError;
    type Encoding = Self;

    fn validate(value: Inline<Self>) -> Result<Inline<Self>, Self::ValidationError> {
        let lower = i128_from_ordered_be(value.raw[0..16].try_into().unwrap());
        let upper = i128_from_ordered_be(value.raw[16..32].try_into().unwrap());
        if lower > upper {
            Err(InvertedIntervalError { lower, upper })
        } else {
            Ok(value)
        }
    }
}

impl TryToInline<NsTAIInterval> for (Epoch, Epoch) {
    type Error = InvertedIntervalError;
    fn try_to_inline(self) -> Result<Inline<NsTAIInterval>, InvertedIntervalError> {
        let lower = self.0.to_tai_duration().total_nanoseconds();
        let upper = self.1.to_tai_duration().total_nanoseconds();
        if lower > upper {
            return Err(InvertedIntervalError { lower, upper });
        }
        let mut value = [0; 32];
        value[0..16].copy_from_slice(&i128_to_ordered_be(lower));
        value[16..32].copy_from_slice(&i128_to_ordered_be(upper));
        Ok(Inline::new(value))
    }
}

impl TryFromInline<'_, NsTAIInterval> for (Epoch, Epoch) {
    type Error = InvertedIntervalError;
    fn try_from_inline(v: &Inline<NsTAIInterval>) -> Result<Self, InvertedIntervalError> {
        let lower = i128_from_ordered_be(v.raw[0..16].try_into().unwrap());
        let upper = i128_from_ordered_be(v.raw[16..32].try_into().unwrap());
        if lower > upper {
            return Err(InvertedIntervalError { lower, upper });
        }
        Ok((
            Epoch::from_tai_duration(Duration::from_total_nanoseconds(lower)),
            Epoch::from_tai_duration(Duration::from_total_nanoseconds(upper)),
        ))
    }
}

impl TryFromInline<'_, NsTAIInterval> for (i128, i128) {
    type Error = InvertedIntervalError;
    fn try_from_inline(v: &Inline<NsTAIInterval>) -> Result<Self, InvertedIntervalError> {
        let lower = i128_from_ordered_be(v.raw[0..16].try_into().unwrap());
        let upper = i128_from_ordered_be(v.raw[16..32].try_into().unwrap());
        if lower > upper {
            return Err(InvertedIntervalError { lower, upper });
        }
        Ok((lower, upper))
    }
}

/// The lower bound of a TAI interval in nanoseconds.
/// Use this when you want to sort or compare by interval start time.
///
/// ```rust,ignore
/// find!(t: Lower, pattern!(&space, [{ entity @ attr: ?t }]))
///     .max_by_key(|t| *t)  // latest start time
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Lower(pub i128);

/// The upper bound of a TAI interval in nanoseconds.
/// Use this when you want to sort or compare by interval end time.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Upper(pub i128);

/// The midpoint of a TAI interval in nanoseconds.
/// Use this when you want to sort or compare by interval center.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Midpoint(pub i128);

/// The width of a TAI interval in nanoseconds.
/// Use this when you want to sort or compare by interval duration.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Width(pub i128);

impl TryFromInline<'_, NsTAIInterval> for Lower {
    type Error = Infallible;
    fn try_from_inline(v: &Inline<NsTAIInterval>) -> Result<Self, Infallible> {
        Ok(Lower(i128_from_ordered_be(
            v.raw[0..16].try_into().unwrap(),
        )))
    }
}

impl TryFromInline<'_, NsTAIInterval> for Upper {
    type Error = Infallible;
    fn try_from_inline(v: &Inline<NsTAIInterval>) -> Result<Self, Infallible> {
        Ok(Upper(i128_from_ordered_be(
            v.raw[16..32].try_into().unwrap(),
        )))
    }
}

impl TryFromInline<'_, NsTAIInterval> for Midpoint {
    type Error = InvertedIntervalError;
    fn try_from_inline(v: &Inline<NsTAIInterval>) -> Result<Self, InvertedIntervalError> {
        let lower = i128_from_ordered_be(v.raw[0..16].try_into().unwrap());
        let upper = i128_from_ordered_be(v.raw[16..32].try_into().unwrap());
        if lower > upper {
            return Err(InvertedIntervalError { lower, upper });
        }
        Ok(Midpoint(lower + (upper - lower) / 2))
    }
}

impl TryFromInline<'_, NsTAIInterval> for Width {
    type Error = InvertedIntervalError;
    fn try_from_inline(v: &Inline<NsTAIInterval>) -> Result<Self, InvertedIntervalError> {
        let lower = i128_from_ordered_be(v.raw[0..16].try_into().unwrap());
        let upper = i128_from_ordered_be(v.raw[16..32].try_into().unwrap());
        if lower > upper {
            return Err(InvertedIntervalError { lower, upper });
        }
        Ok(Width(upper - lower))
    }
}

/// The lower bound exceeds the upper bound.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InvertedIntervalError {
    /// The lower bound that was greater than `upper`.
    pub lower: i128,
    /// The upper bound that was less than `lower`.
    pub upper: i128,
}

/// A inline encoding for a signed nanosecond duration delta.
///
/// log2(ns / Planck second) ≈ 113.8, so a 128-bit ns count comfortably
/// holds every duration physics has an opinion about. We use the upper
/// 16 bytes for the i128 nanosecond magnitude (sign-bit-XOR'd big-
/// endian, same trick as [`NsTAIInterval`]) and reserve the lower 16
/// bytes as zero. Today's readers/writers ignore the lower half;
/// future implementations can use those bits to carry sub-nanosecond
/// precision (picoseconds, femtoseconds, eventually Planck seconds)
/// without breaking byte-lex ordering or trie compatibility — older
/// values just sort before any future-precision values that share the
/// same ns count.
pub struct NsDuration;

impl MetaDescribe for NsDuration {
    fn describe() -> Fragment {
        let id: Id = id_hex!("951D5249DB193D3B3F208B994B1072C4");
        entity! {
            ExclusiveId::force_ref(&id) @
                metadata::name: "ns_duration",
                metadata::description: "Signed nanosecond duration delta encoded as an offset-big-endian i128 in the upper 16 bytes; the lower 16 bytes are reserved (zero today, sub-nanosecond precision in the future). XOR'ing the i128 with i128::MIN before big-endian write makes byte-lexicographic order match numeric order across the full i128 range, so range scans on a sorted trie work natively.",
                metadata::tag: metadata::KIND_INLINE_ENCODING,
        }
    }
}

impl InlineEncoding for NsDuration {
    type ValidationError = ReservedBitsNonZero;
    type Encoding = Self;

    fn validate(value: Inline<Self>) -> Result<Inline<Self>, Self::ValidationError> {
        if value.raw[16..32] != [0u8; 16] {
            return Err(ReservedBitsNonZero);
        }
        Ok(value)
    }
}

/// The reserved (lower 16 bytes) of an [`NsDuration`] encoding contained
/// non-zero bytes. Future precision-extending readers must accept those
/// values; today they're rejected at validation so we don't silently
/// drop precision through a round-trip.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReservedBitsNonZero;

impl std::fmt::Display for ReservedBitsNonZero {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "NsDuration reserved bits (bytes 16..32) are non-zero — \
             this value carries sub-nanosecond precision that the \
             current reader does not understand"
        )
    }
}

impl Encodes<i128> for NsDuration
{
    type Output = Inline<NsDuration>;
    fn encode(source: i128) -> Inline<NsDuration> {
        let mut raw = [0u8; 32];
        raw[0..16].copy_from_slice(&i128_to_ordered_be(source));
        Inline::new(raw)
    }
}

impl TryFromInline<'_, NsDuration> for i128 {
    type Error = ReservedBitsNonZero;

    fn try_from_inline(v: &Inline<NsDuration>) -> Result<Self, Self::Error> {
        if v.raw[16..32] != [0u8; 16] {
            return Err(ReservedBitsNonZero);
        }
        Ok(i128_from_ordered_be(v.raw[0..16].try_into().unwrap()))
    }
}

impl Encodes<Duration> for NsDuration
{
    type Output = Inline<NsDuration>;
    fn encode(source: Duration) -> Inline<NsDuration> {
        source.total_nanoseconds().to_inline()
    }
}

impl TryFromInline<'_, NsDuration> for Duration {
    type Error = ReservedBitsNonZero;

    fn try_from_inline(v: &Inline<NsDuration>) -> Result<Self, Self::Error> {
        let ns: i128 = v.try_from_inline()?;
        Ok(Duration::from_total_nanoseconds(ns))
    }
}

impl std::fmt::Display for InvertedIntervalError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "inverted interval: lower {} > upper {}",
            self.lower, self.upper
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn hifitime_conversion() {
        let epoch = Epoch::from_tai_duration(Duration::from_total_nanoseconds(0));
        let time_in: (Epoch, Epoch) = (epoch, epoch);
        let interval: Inline<NsTAIInterval> = time_in.try_to_inline().unwrap();
        let time_out: (Epoch, Epoch) = interval.try_from_inline().unwrap();

        assert_eq!(time_in, time_out);
    }

    #[test]
    fn projection_types() {
        let lower_ns: i128 = 1_000_000_000;
        let upper_ns: i128 = 3_000_000_000;
        let lower = Epoch::from_tai_duration(Duration::from_total_nanoseconds(lower_ns));
        let upper = Epoch::from_tai_duration(Duration::from_total_nanoseconds(upper_ns));
        let interval: Inline<NsTAIInterval> = (lower, upper).try_to_inline().unwrap();

        let l: Lower = interval.from_inline();
        let u: Upper = interval.from_inline();
        let m: Midpoint = interval.try_from_inline().unwrap();
        let w: Width = interval.try_from_inline().unwrap();

        assert_eq!(l.0, lower_ns);
        assert_eq!(u.0, upper_ns);
        assert_eq!(m.0, 2_000_000_000); // midpoint
        assert_eq!(w.0, 2_000_000_000); // width
        assert!(l < Lower(upper_ns)); // Ord works
    }

    #[test]
    fn try_to_value_rejects_inverted() {
        let lower = Epoch::from_tai_duration(Duration::from_total_nanoseconds(2_000_000_000));
        let upper = Epoch::from_tai_duration(Duration::from_total_nanoseconds(1_000_000_000));
        let result: Result<Inline<NsTAIInterval>, _> = (lower, upper).try_to_inline();
        assert!(result.is_err());
    }

    #[test]
    fn validate_accepts_equal() {
        let t = Epoch::from_tai_duration(Duration::from_total_nanoseconds(1_000_000_000));
        let interval: Inline<NsTAIInterval> = (t, t).try_to_inline().unwrap();
        assert!(NsTAIInterval::validate(interval).is_ok());
    }

    #[test]
    fn nanosecond_conversion() {
        let lower_ns: i128 = 1_000_000_000;
        let upper_ns: i128 = 2_000_000_000;
        let lower = Epoch::from_tai_duration(Duration::from_total_nanoseconds(lower_ns));
        let upper = Epoch::from_tai_duration(Duration::from_total_nanoseconds(upper_ns));
        let interval: Inline<NsTAIInterval> = (lower, upper).try_to_inline().unwrap();

        let (out_lower, out_upper): (i128, i128) = interval.try_from_inline().unwrap();
        assert_eq!(out_lower, lower_ns);
        assert_eq!(out_upper, upper_ns);
    }

    #[test]
    fn byte_order_matches_numeric_order() {
        // Order-preserving BE: byte order = i128 numeric order.
        let times = [
            i128::MIN,
            -1_000_000_000,
            -1,
            0,
            1,
            1_000_000_000,
            i128::MAX,
        ];
        for pair in times.windows(2) {
            let a = i128_to_ordered_be(pair[0]);
            let b = i128_to_ordered_be(pair[1]);
            assert!(a < b, "{} should sort before {} in bytes", pair[0], pair[1]);
        }
    }

    #[test]
    fn roundtrip_edge_cases() {
        for v in [i128::MIN, -1, 0, 1, i128::MAX] {
            assert_eq!(i128_from_ordered_be(i128_to_ordered_be(v)), v);
        }
    }

    #[test]
    fn ns_duration_roundtrip_i128() {
        for ns in [
            i128::MIN,
            -1_000_000_000_000,
            -1,
            0,
            1,
            42,
            1_000_000_000,
            i128::MAX,
        ] {
            let v: Inline<NsDuration> = ns.to_inline();
            // Reserved lower 16 bytes are zero today.
            assert_eq!(v.raw[16..32], [0u8; 16], "lower bits must be reserved=0");
            let back: i128 = v.try_from_inline().unwrap();
            assert_eq!(ns, back);
        }
    }

    #[test]
    fn ns_duration_byte_order_matches_numeric_order() {
        // Sorting by byte-lex on the upper 16 bytes must match numeric order.
        let mut values: Vec<(i128, Inline<NsDuration>)> = vec![
            i128::MIN,
            -1_000_000_000,
            -1,
            0,
            1,
            1_000_000_000,
            i128::MAX,
        ]
        .into_iter()
        .map(|n| (n, n.to_inline()))
        .collect();
        values.sort_by(|a, b| a.1.raw.cmp(&b.1.raw));
        let sorted_ns: Vec<i128> = values.iter().map(|(n, _)| *n).collect();
        let mut expected = sorted_ns.clone();
        expected.sort();
        assert_eq!(sorted_ns, expected);
    }

    #[test]
    fn ns_duration_hifitime_duration_roundtrips() {
        let d_in = Duration::from_total_nanoseconds(1_234_567_890_123);
        let v: Inline<NsDuration> = d_in.to_inline();
        let d_out: Duration = v.try_from_inline().unwrap();
        assert_eq!(d_in.total_nanoseconds(), d_out.total_nanoseconds());
    }

    #[test]
    fn ns_duration_validate_rejects_dirty_reserved_bits() {
        let mut raw = [0u8; 32];
        raw[0..16].copy_from_slice(&i128_to_ordered_be(0));
        raw[20] = 1; // dirty reserved byte
        let v: Inline<NsDuration> = Inline::new(raw);
        assert!(NsDuration::validate(v).is_err());
    }
}