Skip to main content

diskann_record/
number.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6//! Lossless container for the on-wire numeric kinds (`u64`, `i64`, `f64`).
7//!
8//! [`Number`] is the value type produced by the manifest deserializer for every JSON
9//! number. The conversion accessors (`as_u32`, `as_i64`, etc.) attempt to narrow into a
10//! target Rust type and return `None` when the value is out of range or would lose
11//! precision; loaders surface this as [`crate::load::error::Kind::NumberOutOfRange`].
12
13#[cfg(feature = "serde")]
14use serde::de::{self, Visitor};
15#[cfg(feature = "serde")]
16use serde::{Deserialize, Deserializer, Serialize, Serializer};
17
18/// A numeric value carried in a manifest, preserving the kind the writer chose.
19///
20/// The wire format distinguishes unsigned, signed, and floating-point numbers; the
21/// deserializer preserves that distinction by selecting the matching variant. Use the
22/// narrowing accessors (e.g. [`Number::as_u32`], [`Number::as_f64`]) to extract a Rust
23/// value of the desired type.
24#[derive(Debug, Clone, Copy)]
25pub enum Number {
26    U64(u64),
27    I64(i64),
28    F64(f64),
29}
30
31impl Number {
32    /// Returns the string sentinel for a non-finite `f64`, or `None` for any finite or
33    /// integer value.
34    ///
35    /// JSON cannot represent `NaN`/`\pm inf` as numeric literals, so these are encoded as
36    /// the strings `"nan"`, `"inf"`, and `"neg_inf"` instead. [`Number::from_sentinel`]
37    /// is the inverse.
38    pub(crate) fn sentinel(self) -> Option<&'static str> {
39        match self {
40            Self::F64(v) if v.is_nan() => Some("nan"),
41            Self::F64(v) if v == f64::INFINITY => Some("inf"),
42            Self::F64(v) if v == f64::NEG_INFINITY => Some("neg_inf"),
43            _ => None,
44        }
45    }
46
47    /// Decodes a non-finite `f64` sentinel produced by [`Number::sentinel`], or `None`
48    /// for any other string.
49    pub(crate) fn from_sentinel(s: &str) -> Option<Self> {
50        match s {
51            "nan" => Some(Self::F64(f64::NAN)),
52            "inf" => Some(Self::F64(f64::INFINITY)),
53            "neg_inf" => Some(Self::F64(f64::NEG_INFINITY)),
54            _ => None,
55        }
56    }
57}
58
59#[cfg(feature = "serde")]
60impl Serialize for Number {
61    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
62        if let Some(sentinel) = self.sentinel() {
63            return serializer.serialize_str(sentinel);
64        }
65        match *self {
66            Self::U64(v) => serializer.serialize_u64(v),
67            Self::I64(v) => serializer.serialize_i64(v),
68            Self::F64(v) => serializer.serialize_f64(v),
69        }
70    }
71}
72
73#[cfg(feature = "serde")]
74impl<'de> Deserialize<'de> for Number {
75    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
76        struct NumberVisitor;
77
78        impl<'de> Visitor<'de> for NumberVisitor {
79            type Value = Number;
80
81            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
82                f.write_str("a number")
83            }
84
85            fn visit_u64<E: de::Error>(self, v: u64) -> Result<Number, E> {
86                Ok(Number::U64(v))
87            }
88
89            fn visit_i64<E: de::Error>(self, v: i64) -> Result<Number, E> {
90                Ok(Number::I64(v))
91            }
92
93            fn visit_f64<E: de::Error>(self, v: f64) -> Result<Number, E> {
94                Ok(Number::F64(v))
95            }
96
97            fn visit_str<E: de::Error>(self, v: &str) -> Result<Number, E> {
98                Number::from_sentinel(v).ok_or_else(|| {
99                    de::Error::custom(format!("expected a number or numeric sentinel, got {v:?}"))
100                })
101            }
102        }
103
104        deserializer.deserialize_any(NumberVisitor)
105    }
106}
107
108macro_rules! try_cast {
109    ($v:ident :$T:ty => $U:ty) => {{
110        let c = $v as $U;
111        if c as $T == $v { Some(c) } else { None }
112    }};
113}
114
115macro_rules! int {
116    ($f:ident, $T:ty) => {
117        pub fn $f(self) -> Option<$T> {
118            match self {
119                Self::U64(v) => v.try_into().ok(),
120                Self::I64(v) => v.try_into().ok(),
121                Self::F64(v) => try_cast!(v:f64 => $T),
122            }
123        }
124    }
125}
126
127macro_rules! float {
128    ($f:ident, $T:ty) => {
129        pub fn $f(self) -> Option<$T> {
130            match self {
131                Self::U64(v) => try_cast!(v:u64 => $T),
132                Self::I64(v) => try_cast!(v:i64 => $T),
133                // NaN is representable in every float target but never equals itself, so the
134                // `try_cast!` round-trip guard would reject it; pass it through directly.
135                Self::F64(v) if v.is_nan() => Some(v as $T),
136                Self::F64(v) => try_cast!(v:f64 => $T),
137            }
138        }
139    }
140}
141
142impl Number {
143    int!(as_u8, u8);
144    int!(as_u16, u16);
145    int!(as_u32, u32);
146    int!(as_u64, u64);
147    int!(as_usize, usize);
148
149    int!(as_i8, i8);
150    int!(as_i16, i16);
151    int!(as_i32, i32);
152    int!(as_i64, i64);
153    int!(as_isize, isize);
154
155    float!(as_f32, f32);
156    float!(as_f64, f64);
157}
158
159macro_rules! from {
160    ($T:ty => $variant:ident) => {
161        impl From<$T> for Number {
162            fn from(v: $T) -> Self {
163                Self::$variant(v.into())
164            }
165        }
166    };
167    ($($T:ty => $variant:ident),+ $(,)?) => {
168        $(from!($T => $variant);)+
169    }
170}
171
172from!(
173    u64 => U64,
174    u32 => U64,
175    u16 => U64,
176    u8 => U64,
177    i64 => I64,
178    i32 => I64,
179    i16 => I64,
180    i8 => I64,
181    f32 => F64,
182    f64 => F64,
183);
184
185impl From<usize> for Number {
186    fn from(v: usize) -> Self {
187        Self::U64(v.try_into().unwrap())
188    }
189}
190
191macro_rules! try_from {
192    ($T:ty => $f:ident) => {
193        impl TryFrom<Number> for $T {
194            type Error = ();
195            fn try_from(number: Number) -> Result<$T, Self::Error> {
196                number.$f().ok_or(())
197            }
198        }
199    };
200    ($($T:ty => $f:ident),+ $(,)?) => {
201        $(try_from!($T => $f);)+
202    }
203}
204
205try_from!(
206    u64 => as_u64,
207    u32 => as_u32,
208    u16 => as_u16,
209    u8 => as_u8,
210    usize => as_usize,
211
212    i64 => as_i64,
213    i32 => as_i32,
214    i16 => as_i16,
215    i8 => as_i8,
216    isize => as_isize,
217
218    f32 => as_f32,
219    f64 => as_f64,
220);
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn integer_accessors_range_check() {
228        assert_eq!(Number::U64(255).as_u8(), Some(255));
229        assert_eq!(Number::U64(256).as_u8(), None);
230        assert_eq!(Number::I64(-1).as_u8(), None);
231        assert_eq!(Number::I64(-5).as_i8(), Some(-5));
232        assert_eq!(Number::I64(-129).as_i8(), None);
233    }
234
235    #[test]
236    fn float_to_integer_requires_integral_value() {
237        assert_eq!(Number::F64(2.0).as_u32(), Some(2));
238        assert_eq!(Number::F64(2.5).as_u32(), None);
239        assert_eq!(Number::F64(-1.0).as_u32(), None);
240    }
241
242    // Regression for the NaN narrowing-accessor bug.
243    #[test]
244    fn nan_survives_float_accessors() {
245        assert_eq!(
246            Number::F64(f64::NAN).as_f64().map(f64::is_nan),
247            Some(true),
248            "as_f64 dropped a NaN"
249        );
250        assert_eq!(
251            Number::F64(f64::NAN).as_f32().map(f32::is_nan),
252            Some(true),
253            "as_f32 dropped a NaN"
254        );
255        // Sanity: infinities already work, so this is specific to NaN.
256        assert_eq!(Number::F64(f64::INFINITY).as_f64(), Some(f64::INFINITY));
257    }
258
259    #[test]
260    fn nan_round_trips_through_try_from() {
261        let back = f64::try_from(Number::F64(f64::NAN)).expect("NaN must convert back to f64");
262        assert!(back.is_nan());
263    }
264
265    #[test]
266    fn try_from_surfaces_out_of_range() {
267        assert!(u8::try_from(Number::U64(300)).is_err());
268        assert_eq!(u16::try_from(Number::U64(300)).unwrap(), 300);
269        assert!(usize::try_from(Number::I64(-1)).is_err());
270    }
271
272    #[cfg(feature = "disk")]
273    #[test]
274    fn non_finite_floats_round_trip_via_sentinels() {
275        for (value, sentinel) in [
276            (f64::NAN, "\"nan\""),
277            (f64::INFINITY, "\"inf\""),
278            (f64::NEG_INFINITY, "\"neg_inf\""),
279        ] {
280            let json = serde_json::to_string(&Number::F64(value)).unwrap();
281            assert_eq!(json, sentinel);
282
283            let back: Number = serde_json::from_str(&json).unwrap();
284            match back {
285                Number::F64(v) if value.is_nan() => assert!(v.is_nan()),
286                Number::F64(v) => assert_eq!(v, value),
287                other => panic!("expected F64, got {other:?}"),
288            }
289        }
290    }
291
292    #[cfg(feature = "disk")]
293    #[test]
294    fn finite_floats_serialize_as_json_numbers() {
295        assert_eq!(serde_json::to_string(&Number::F64(1.5)).unwrap(), "1.5");
296        assert_eq!(serde_json::to_string(&Number::U64(7)).unwrap(), "7");
297        assert_eq!(serde_json::to_string(&Number::I64(-7)).unwrap(), "-7");
298    }
299
300    #[cfg(feature = "disk")]
301    #[test]
302    fn json_numbers_select_matching_variant() {
303        // Unsigned, signed, and floating-point literals each route to the visitor
304        // method that preserves the writer's chosen kind.
305        assert!(matches!(serde_json::from_str("7").unwrap(), Number::U64(7)));
306        assert!(matches!(
307            serde_json::from_str("-7").unwrap(),
308            Number::I64(-7)
309        ));
310        match serde_json::from_str("1.5").unwrap() {
311            Number::F64(v) => assert_eq!(v, 1.5),
312            other => panic!("expected F64, got {other:?}"),
313        }
314    }
315
316    #[cfg(feature = "disk")]
317    #[test]
318    fn non_sentinel_string_is_rejected() {
319        let err = serde_json::from_str::<Number>("\"bogus\"").unwrap_err();
320        assert!(
321            err.to_string().contains("numeric sentinel"),
322            "unexpected error message: {err}"
323        );
324    }
325}