Skip to main content

facet_value/
number.rs

1//! Number value type with efficient storage for various numeric types.
2
3#[cfg(feature = "alloc")]
4use alloc::alloc::{Layout, alloc, dealloc};
5use core::cmp::Ordering;
6use core::fmt::{self, Debug, Formatter};
7use core::hash::{Hash, Hasher};
8
9use crate::value::{TypeTag, Value};
10
11/// Internal representation of number type.
12#[repr(u8)]
13#[derive(Copy, Clone, Debug, PartialEq, Eq)]
14enum NumberType {
15    /// Signed 64-bit integer
16    I64 = 0,
17    /// Unsigned 64-bit integer
18    U64 = 1,
19    /// 64-bit floating point
20    F64 = 2,
21    /// Signed 128-bit integer
22    I128 = 3,
23    /// Unsigned 128-bit integer
24    U128 = 4,
25}
26
27/// Header for heap-allocated numbers.
28#[repr(C, align(8))]
29struct NumberHeader {
30    /// Type discriminant
31    type_: NumberType,
32    /// Padding
33    _pad: [u8; 7],
34    /// The actual number data (i64, u64, or f64)
35    data: NumberData,
36}
37
38#[repr(C)]
39union NumberData {
40    i: i64,
41    u: u64,
42    f: f64,
43    i128: i128,
44    u128: u128,
45}
46
47/// A JSON number value.
48///
49/// `VNumber` can represent integers (signed and unsigned) and floating point numbers.
50/// It stores the number in the most appropriate internal format.
51#[repr(transparent)]
52#[derive(Clone)]
53pub struct VNumber(pub(crate) Value);
54
55impl VNumber {
56    const fn layout() -> Layout {
57        Layout::new::<NumberHeader>()
58    }
59
60    #[cfg(feature = "alloc")]
61    fn alloc(type_: NumberType) -> *mut NumberHeader {
62        unsafe {
63            let ptr = alloc(Self::layout()).cast::<NumberHeader>();
64            (*ptr).type_ = type_;
65            ptr
66        }
67    }
68
69    #[cfg(feature = "alloc")]
70    fn dealloc(ptr: *mut NumberHeader) {
71        unsafe {
72            dealloc(ptr.cast::<u8>(), Self::layout());
73        }
74    }
75
76    fn header(&self) -> &NumberHeader {
77        unsafe { &*(self.0.heap_ptr() as *const NumberHeader) }
78    }
79
80    #[allow(dead_code)]
81    fn header_mut(&mut self) -> &mut NumberHeader {
82        unsafe { &mut *(self.0.heap_ptr_mut() as *mut NumberHeader) }
83    }
84
85    /// Creates a number from an i64.
86    #[cfg(feature = "alloc")]
87    #[must_use]
88    pub fn from_i64(v: i64) -> Self {
89        unsafe {
90            let ptr = Self::alloc(NumberType::I64);
91            (*ptr).data.i = v;
92            VNumber(Value::new_ptr(ptr.cast(), TypeTag::Number))
93        }
94    }
95
96    /// Creates a number from a u64.
97    #[cfg(feature = "alloc")]
98    #[must_use]
99    pub fn from_u64(v: u64) -> Self {
100        // If it fits in i64, use that for consistency
101        if let Ok(i) = i64::try_from(v) {
102            Self::from_i64(i)
103        } else {
104            unsafe {
105                let ptr = Self::alloc(NumberType::U64);
106                (*ptr).data.u = v;
107                VNumber(Value::new_ptr(ptr.cast(), TypeTag::Number))
108            }
109        }
110    }
111
112    /// Creates a number from an f64.
113    #[cfg(feature = "alloc")]
114    #[must_use]
115    pub fn from_f64(v: f64) -> Self {
116        unsafe {
117            let ptr = Self::alloc(NumberType::F64);
118            (*ptr).data.f = v;
119            VNumber(Value::new_ptr(ptr.cast(), TypeTag::Number))
120        }
121    }
122
123    /// Creates a number from an i128, canonicalizing to the smallest representation.
124    ///
125    /// Magnitude canonicalization keeps the representations over disjoint ranges so
126    /// that equal values always share a single internal form:
127    /// `I64=[i64::MIN, i64::MAX]`, `U64=(i64::MAX, u64::MAX]`,
128    /// `U128=(u64::MAX, u128::MAX]`, `I128=[i128::MIN, i64::MIN)`.
129    #[cfg(feature = "alloc")]
130    #[must_use]
131    pub fn from_i128(v: i128) -> Self {
132        if let Ok(i) = i64::try_from(v) {
133            Self::from_i64(i)
134        } else if v >= 0 {
135            if let Ok(u) = u64::try_from(v) {
136                Self::from_u64(u)
137            } else {
138                // v > u64::MAX and positive: store as u128
139                unsafe {
140                    let ptr = Self::alloc(NumberType::U128);
141                    (*ptr).data.u128 = v as u128;
142                    VNumber(Value::new_ptr(ptr.cast(), TypeTag::Number))
143                }
144            }
145        } else {
146            // v < i64::MIN: store as i128
147            unsafe {
148                let ptr = Self::alloc(NumberType::I128);
149                (*ptr).data.i128 = v;
150                VNumber(Value::new_ptr(ptr.cast(), TypeTag::Number))
151            }
152        }
153    }
154
155    /// Creates a number from a u128, canonicalizing to the smallest representation.
156    #[cfg(feature = "alloc")]
157    #[must_use]
158    pub fn from_u128(v: u128) -> Self {
159        if let Ok(u) = u64::try_from(v) {
160            Self::from_u64(u)
161        } else {
162            unsafe {
163                let ptr = Self::alloc(NumberType::U128);
164                (*ptr).data.u128 = v;
165                VNumber(Value::new_ptr(ptr.cast(), TypeTag::Number))
166            }
167        }
168    }
169
170    /// Returns the number zero.
171    #[cfg(feature = "alloc")]
172    #[must_use]
173    pub fn zero() -> Self {
174        Self::from_i64(0)
175    }
176
177    /// Returns the number one.
178    #[cfg(feature = "alloc")]
179    #[must_use]
180    pub fn one() -> Self {
181        Self::from_i64(1)
182    }
183
184    /// Converts to i64 if it can be represented exactly.
185    #[must_use]
186    pub fn to_i64(&self) -> Option<i64> {
187        let hd = self.header();
188        unsafe {
189            match hd.type_ {
190                NumberType::I64 => Some(hd.data.i),
191                NumberType::U64 => i64::try_from(hd.data.u).ok(),
192                NumberType::I128 => i64::try_from(hd.data.i128).ok(),
193                NumberType::U128 => i64::try_from(hd.data.u128).ok(),
194                NumberType::F64 => {
195                    let f = hd.data.f;
196                    // Check if in range and is a whole number via round-trip cast
197                    if f >= i64::MIN as f64 && f <= i64::MAX as f64 {
198                        let i = f as i64;
199                        if i as f64 == f {
200                            return Some(i);
201                        }
202                    }
203                    None
204                }
205            }
206        }
207    }
208
209    /// Converts to u64 if it can be represented exactly.
210    #[must_use]
211    pub fn to_u64(&self) -> Option<u64> {
212        let hd = self.header();
213        unsafe {
214            match hd.type_ {
215                NumberType::I64 => u64::try_from(hd.data.i).ok(),
216                NumberType::U64 => Some(hd.data.u),
217                NumberType::I128 => u64::try_from(hd.data.i128).ok(),
218                NumberType::U128 => u64::try_from(hd.data.u128).ok(),
219                NumberType::F64 => {
220                    let f = hd.data.f;
221                    // Check if in range and is a whole number via round-trip cast
222                    if f >= 0.0 && f <= u64::MAX as f64 {
223                        let u = f as u64;
224                        if u as f64 == f {
225                            return Some(u);
226                        }
227                    }
228                    None
229                }
230            }
231        }
232    }
233
234    /// Converts to i128 if it can be represented exactly.
235    #[must_use]
236    pub fn to_i128(&self) -> Option<i128> {
237        let hd = self.header();
238        unsafe {
239            match hd.type_ {
240                NumberType::I64 => Some(i128::from(hd.data.i)),
241                NumberType::U64 => Some(i128::from(hd.data.u)),
242                NumberType::I128 => Some(hd.data.i128),
243                NumberType::U128 => i128::try_from(hd.data.u128).ok(),
244                NumberType::F64 => {
245                    let f = hd.data.f;
246                    // Check if in range and is a whole number via round-trip cast
247                    if f >= i128::MIN as f64 && f <= i128::MAX as f64 {
248                        let i = f as i128;
249                        if i as f64 == f {
250                            return Some(i);
251                        }
252                    }
253                    None
254                }
255            }
256        }
257    }
258
259    /// Converts to u128 if it can be represented exactly.
260    #[must_use]
261    pub fn to_u128(&self) -> Option<u128> {
262        let hd = self.header();
263        unsafe {
264            match hd.type_ {
265                NumberType::I64 => u128::try_from(hd.data.i).ok(),
266                NumberType::U64 => Some(u128::from(hd.data.u)),
267                NumberType::I128 => u128::try_from(hd.data.i128).ok(),
268                NumberType::U128 => Some(hd.data.u128),
269                NumberType::F64 => {
270                    let f = hd.data.f;
271                    // Check if in range and is a whole number via round-trip cast
272                    if f >= 0.0 && f <= u128::MAX as f64 {
273                        let u = f as u128;
274                        if u as f64 == f {
275                            return Some(u);
276                        }
277                    }
278                    None
279                }
280            }
281        }
282    }
283
284    /// Converts to f64 if it can be represented exactly.
285    #[must_use]
286    pub fn to_f64(&self) -> Option<f64> {
287        let hd = self.header();
288        unsafe {
289            match hd.type_ {
290                NumberType::I64 => {
291                    let i = hd.data.i;
292                    let f = i as f64;
293                    if f as i64 == i { Some(f) } else { None }
294                }
295                NumberType::U64 => {
296                    let u = hd.data.u;
297                    let f = u as f64;
298                    if f as u64 == u { Some(f) } else { None }
299                }
300                NumberType::I128 => {
301                    let i = hd.data.i128;
302                    let f = i as f64;
303                    if f as i128 == i { Some(f) } else { None }
304                }
305                NumberType::U128 => {
306                    let u = hd.data.u128;
307                    let f = u as f64;
308                    if f as u128 == u { Some(f) } else { None }
309                }
310                NumberType::F64 => Some(hd.data.f),
311            }
312        }
313    }
314
315    /// Converts to f64, potentially losing precision.
316    #[must_use]
317    pub fn to_f64_lossy(&self) -> f64 {
318        let hd = self.header();
319        unsafe {
320            match hd.type_ {
321                NumberType::I64 => hd.data.i as f64,
322                NumberType::U64 => hd.data.u as f64,
323                NumberType::I128 => hd.data.i128 as f64,
324                NumberType::U128 => hd.data.u128 as f64,
325                NumberType::F64 => hd.data.f,
326            }
327        }
328    }
329
330    /// Converts to i32 if it can be represented exactly.
331    #[must_use]
332    pub fn to_i32(&self) -> Option<i32> {
333        self.to_i64().and_then(|v| i32::try_from(v).ok())
334    }
335
336    /// Converts to u32 if it can be represented exactly.
337    #[must_use]
338    pub fn to_u32(&self) -> Option<u32> {
339        self.to_u64().and_then(|v| u32::try_from(v).ok())
340    }
341
342    /// Converts to f32 if it can be represented exactly.
343    #[must_use]
344    pub fn to_f32(&self) -> Option<f32> {
345        self.to_f64().and_then(|f| {
346            let f32_val = f as f32;
347            if f32_val as f64 == f {
348                Some(f32_val)
349            } else {
350                None
351            }
352        })
353    }
354
355    /// Returns true if this number was created from a floating point value.
356    #[must_use]
357    pub fn is_float(&self) -> bool {
358        self.header().type_ == NumberType::F64
359    }
360
361    /// Returns true if this number is an integer (signed or unsigned).
362    #[must_use]
363    pub fn is_integer(&self) -> bool {
364        matches!(
365            self.header().type_,
366            NumberType::I64 | NumberType::U64 | NumberType::I128 | NumberType::U128
367        )
368    }
369
370    pub(crate) fn clone_impl(&self) -> Value {
371        let hd = self.header();
372        unsafe {
373            match hd.type_ {
374                NumberType::I64 => Self::from_i64(hd.data.i).0,
375                NumberType::U64 => {
376                    let ptr = Self::alloc(NumberType::U64);
377                    (*ptr).data.u = hd.data.u;
378                    Value::new_ptr(ptr.cast(), TypeTag::Number)
379                }
380                NumberType::I128 => {
381                    let ptr = Self::alloc(NumberType::I128);
382                    (*ptr).data.i128 = hd.data.i128;
383                    Value::new_ptr(ptr.cast(), TypeTag::Number)
384                }
385                NumberType::U128 => {
386                    let ptr = Self::alloc(NumberType::U128);
387                    (*ptr).data.u128 = hd.data.u128;
388                    Value::new_ptr(ptr.cast(), TypeTag::Number)
389                }
390                NumberType::F64 => Self::from_f64(hd.data.f).0,
391            }
392        }
393    }
394
395    pub(crate) fn drop_impl(&mut self) {
396        unsafe {
397            Self::dealloc(self.0.heap_ptr_mut().cast());
398        }
399    }
400}
401
402impl PartialEq for VNumber {
403    fn eq(&self, other: &Self) -> bool {
404        self.partial_cmp(other) == Some(Ordering::Equal)
405    }
406}
407
408impl PartialOrd for VNumber {
409    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
410        let h1 = self.header();
411        let h2 = other.header();
412
413        unsafe {
414            // Fast path: same type
415            if h1.type_ == h2.type_ {
416                match h1.type_ {
417                    NumberType::I64 => Some(h1.data.i.cmp(&h2.data.i)),
418                    NumberType::U64 => Some(h1.data.u.cmp(&h2.data.u)),
419                    NumberType::I128 => Some(h1.data.i128.cmp(&h2.data.i128)),
420                    NumberType::U128 => Some(h1.data.u128.cmp(&h2.data.u128)),
421                    NumberType::F64 => h1.data.f.partial_cmp(&h2.data.f),
422                }
423            } else if h1.type_ == NumberType::F64 || h2.type_ == NumberType::F64 {
424                // If either operand is a float, fall back to lossy f64 comparison.
425                // This preserves int == whole-float equality and NaN -> None.
426                self.to_f64_lossy().partial_cmp(&other.to_f64_lossy())
427            } else {
428                // Both are integers: compare exactly.
429                match (self.to_i128(), other.to_i128()) {
430                    (Some(a), Some(b)) => Some(a.cmp(&b)),
431                    // Exactly one is None means it's a U128 exceeding i128::MAX,
432                    // so that side is the greater value.
433                    (None, Some(_)) => Some(Ordering::Greater),
434                    (Some(_), None) => Some(Ordering::Less),
435                    // Both None: both exceed i128::MAX, compare via u128.
436                    (None, None) => Some(self.to_u128().unwrap().cmp(&other.to_u128().unwrap())),
437                }
438            }
439        }
440    }
441}
442
443impl Hash for VNumber {
444    fn hash<H: Hasher>(&self, state: &mut H) {
445        // Hash based on the "canonical" representation. The chain mirrors the
446        // canonicalization order so that equal values (including whole floats
447        // equal to an integer) always land in the same bucket.
448        if let Some(i) = self.to_i64() {
449            0u8.hash(state); // discriminant for integer
450            i.hash(state);
451        } else if let Some(u) = self.to_u64() {
452            1u8.hash(state); // discriminant for large unsigned
453            u.hash(state);
454        } else if let Some(i) = self.to_i128() {
455            3u8.hash(state); // discriminant for 128-bit signed
456            i.hash(state);
457        } else if let Some(u) = self.to_u128() {
458            4u8.hash(state); // discriminant for 128-bit unsigned
459            u.hash(state);
460        } else if let Some(f) = self.to_f64() {
461            2u8.hash(state); // discriminant for float
462            f.to_bits().hash(state);
463        }
464    }
465}
466
467impl Debug for VNumber {
468    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
469        if let Some(i) = self.to_i64() {
470            Debug::fmt(&i, f)
471        } else if let Some(u) = self.to_u64() {
472            Debug::fmt(&u, f)
473        } else if let Some(i) = self.to_i128() {
474            Debug::fmt(&i, f)
475        } else if let Some(u) = self.to_u128() {
476            Debug::fmt(&u, f)
477        } else if let Some(fl) = self.to_f64() {
478            Debug::fmt(&fl, f)
479        } else {
480            f.write_str("NaN")
481        }
482    }
483}
484
485impl Default for VNumber {
486    fn default() -> Self {
487        Self::zero()
488    }
489}
490
491// === From implementations ===
492
493macro_rules! impl_from_int {
494    ($($t:ty => $method:ident),* $(,)?) => {
495        $(
496            #[cfg(feature = "alloc")]
497            impl From<$t> for VNumber {
498                fn from(v: $t) -> Self {
499                    Self::$method(v as _)
500                }
501            }
502
503            #[cfg(feature = "alloc")]
504            impl From<$t> for Value {
505                fn from(v: $t) -> Self {
506                    VNumber::from(v).0
507                }
508            }
509        )*
510    };
511}
512
513impl_from_int! {
514    i8 => from_i64,
515    i16 => from_i64,
516    i32 => from_i64,
517    i64 => from_i64,
518    isize => from_i64,
519    u8 => from_i64,
520    u16 => from_i64,
521    u32 => from_i64,
522    u64 => from_u64,
523    usize => from_u64,
524}
525
526// 128-bit integers must NOT go through the `as _` cast in `impl_from_int!`
527// (that would be lossy), so they get explicit impls calling the dedicated
528// canonicalizing constructors.
529
530#[cfg(feature = "alloc")]
531impl From<i128> for VNumber {
532    fn from(v: i128) -> Self {
533        Self::from_i128(v)
534    }
535}
536
537#[cfg(feature = "alloc")]
538impl From<i128> for Value {
539    fn from(v: i128) -> Self {
540        VNumber::from_i128(v).0
541    }
542}
543
544#[cfg(feature = "alloc")]
545impl From<u128> for VNumber {
546    fn from(v: u128) -> Self {
547        Self::from_u128(v)
548    }
549}
550
551#[cfg(feature = "alloc")]
552impl From<u128> for Value {
553    fn from(v: u128) -> Self {
554        VNumber::from_u128(v).0
555    }
556}
557
558#[cfg(feature = "alloc")]
559impl From<f32> for VNumber {
560    fn from(v: f32) -> Self {
561        Self::from_f64(f64::from(v))
562    }
563}
564
565#[cfg(feature = "alloc")]
566impl From<f64> for VNumber {
567    fn from(v: f64) -> Self {
568        Self::from_f64(v)
569    }
570}
571
572#[cfg(feature = "alloc")]
573impl From<f32> for Value {
574    fn from(v: f32) -> Self {
575        VNumber::from_f64(f64::from(v)).into_value()
576    }
577}
578
579#[cfg(feature = "alloc")]
580impl From<f64> for Value {
581    fn from(v: f64) -> Self {
582        VNumber::from_f64(v).into_value()
583    }
584}
585
586// === Conversion traits ===
587
588impl AsRef<Value> for VNumber {
589    fn as_ref(&self) -> &Value {
590        &self.0
591    }
592}
593
594impl AsMut<Value> for VNumber {
595    fn as_mut(&mut self) -> &mut Value {
596        &mut self.0
597    }
598}
599
600impl From<VNumber> for Value {
601    fn from(n: VNumber) -> Self {
602        n.0
603    }
604}
605
606impl VNumber {
607    /// Converts this VNumber into a Value, consuming self.
608    #[inline]
609    pub fn into_value(self) -> Value {
610        self.0
611    }
612}
613
614#[cfg(test)]
615mod tests {
616    use super::*;
617
618    #[test]
619    fn test_i64() {
620        let n = VNumber::from_i64(42);
621        assert_eq!(n.to_i64(), Some(42));
622        assert_eq!(n.to_u64(), Some(42));
623        assert_eq!(n.to_f64(), Some(42.0));
624        assert!(n.is_integer());
625        assert!(!n.is_float());
626    }
627
628    #[test]
629    fn test_negative() {
630        let n = VNumber::from_i64(-100);
631        assert_eq!(n.to_i64(), Some(-100));
632        assert_eq!(n.to_u64(), None);
633        assert_eq!(n.to_f64(), Some(-100.0));
634    }
635
636    #[test]
637    fn test_large_u64() {
638        let v = u64::MAX;
639        let n = VNumber::from_u64(v);
640        assert_eq!(n.to_u64(), Some(v));
641        assert_eq!(n.to_i64(), None);
642    }
643
644    #[test]
645    fn test_f64() {
646        let n = VNumber::from_f64(2.5);
647        assert_eq!(n.to_f64(), Some(2.5));
648        assert_eq!(n.to_i64(), None); // has fractional part
649        assert!(n.is_float());
650        assert!(!n.is_integer());
651    }
652
653    #[test]
654    fn test_f64_whole() {
655        let n = VNumber::from_f64(42.0);
656        assert_eq!(n.to_f64(), Some(42.0));
657        assert_eq!(n.to_i64(), Some(42)); // whole number
658    }
659
660    #[test]
661    fn test_nan_roundtrip() {
662        assert!(VNumber::from_f64(f64::NAN).to_f64().unwrap().is_nan());
663        assert_eq!(
664            VNumber::from_f64(f64::INFINITY).to_f64().unwrap(),
665            f64::INFINITY
666        );
667        assert_eq!(
668            VNumber::from_f64(f64::NEG_INFINITY).to_f64().unwrap(),
669            f64::NEG_INFINITY
670        );
671    }
672
673    #[test]
674    fn test_equality() {
675        let a = VNumber::from_i64(42);
676        let b = VNumber::from_i64(42);
677        let c = VNumber::from_f64(42.0);
678        let nan = VNumber::from_f64(f64::NAN);
679
680        assert_eq!(a, b);
681        assert_eq!(a, c); // integer 42 equals float 42.0
682
683        // nan should != any value including itself
684        assert_ne!(c, nan);
685        assert_ne!(nan, nan);
686    }
687
688    #[test]
689    fn test_ordering() {
690        let a = VNumber::from_i64(1);
691        let b = VNumber::from_i64(2);
692        let c = VNumber::from_f64(1.5);
693        let nan = VNumber::from_f64(f64::NAN);
694        let inf = VNumber::from_f64(f64::INFINITY);
695
696        assert!(a < b);
697        assert!(a < c);
698        assert!(c < b);
699        assert!(b < inf);
700        assert!(c.partial_cmp(&nan).is_none());
701    }
702
703    #[test]
704    fn test_u128_max_roundtrip() {
705        let n = VNumber::from_u128(u128::MAX);
706        assert_eq!(n.to_u128(), Some(u128::MAX));
707        // u128::MAX exceeds i128::MAX, so to_i128 must be None.
708        assert_eq!(n.to_i128(), None);
709        assert_eq!(n.to_u64(), None);
710        assert!(n.is_integer());
711    }
712
713    #[test]
714    fn test_i128_min_roundtrip() {
715        let n = VNumber::from_i128(i128::MIN);
716        assert_eq!(n.to_i128(), Some(i128::MIN));
717        assert_eq!(n.to_u128(), None);
718        assert_eq!(n.to_i64(), None);
719        assert!(n.is_integer());
720    }
721
722    #[test]
723    fn test_above_u64_max_roundtrip() {
724        let v = u128::from(u64::MAX) + 1;
725        let n = VNumber::from_u128(v);
726        assert_eq!(n.to_u128(), Some(v));
727        assert_eq!(n.to_i128(), Some(v as i128));
728        assert_eq!(n.to_u64(), None);
729    }
730
731    #[test]
732    fn test_128_canonicalization() {
733        // Small values canonicalize down to I64.
734        assert_eq!(VNumber::from_i128(5), VNumber::from_i64(5));
735        assert_eq!(VNumber::from_u128(5), VNumber::from_i64(5));
736        // u64::MAX canonicalizes to the same U64 representation.
737        assert_eq!(
738            VNumber::from_u128(u128::from(u64::MAX)),
739            VNumber::from_u64(u64::MAX)
740        );
741        // A negative value within i64 range canonicalizes to I64.
742        assert_eq!(VNumber::from_i128(-42), VNumber::from_i64(-42));
743    }
744
745    #[test]
746    fn test_128_eq_hash_consistency() {
747        use std::collections::HashSet;
748
749        // Equal values built via different constructors must be Eq and hash equal.
750        let a = VNumber::from_i128(5);
751        let b = VNumber::from_u128(5);
752        assert_eq!(a, b);
753
754        let mut set = HashSet::new();
755        set.insert(crate::Value::from(a.clone()));
756        // Inserting the equal-but-differently-constructed value must collide.
757        assert!(!set.insert(crate::Value::from(b.clone())));
758
759        // A big value above u64::MAX, equal across i128/u128 construction.
760        let big = u128::from(u64::MAX) + 12345;
761        let x = VNumber::from_u128(big);
762        let y = VNumber::from_i128(big as i128);
763        assert_eq!(x, y);
764        let mut set2 = HashSet::new();
765        set2.insert(crate::Value::from(x));
766        assert!(!set2.insert(crate::Value::from(y)));
767    }
768
769    #[test]
770    fn test_128_cross_type_ordering() {
771        // U128 above i128::MAX must order greater than any i128.
772        let huge = VNumber::from_u128(u128::MAX);
773        let big_signed = VNumber::from_i128(i128::MAX);
774        assert!(big_signed < huge);
775
776        // I128 min is the smallest.
777        let small = VNumber::from_i128(i128::MIN);
778        assert!(small < big_signed);
779        assert!(small < VNumber::from_i64(0));
780    }
781
782    #[test]
783    fn test_from_128_impls() {
784        let v: crate::Value = (u128::MAX).into();
785        assert_eq!(v.as_number().unwrap().to_u128(), Some(u128::MAX));
786        let v: crate::Value = (i128::MIN).into();
787        assert_eq!(v.as_number().unwrap().to_i128(), Some(i128::MIN));
788    }
789}