Skip to main content

tinyquant_core/corpus/
entry_meta_value.rs

1//! `EntryMetaValue` — a `no_std + alloc` substitute for `serde_json::Value`.
2//!
3//! # Design rationale
4//!
5//! `serde_json::Value` allocates owned `String`/`Vec` at every node and
6//! unconditionally pulls `std`. `EntryMetaValue` instead wraps payloads in
7//! [`Arc`] so that clones are O(1) reference-count bumps, fitting the
8//! `tinyquant-core` `no_std + alloc` posture.
9//!
10//! # Float equality semantics
11//!
12//! `PartialEq` for [`EntryMetaValue::Float`] uses **bit-exact comparison**
13//! (`a.to_bits() == b.to_bits()`), not IEEE 754 semantics.  This means
14//! `NaN == NaN` returns `true`, mirroring Python's `dict` key-stability
15//! contract: a metadata round-trip must not drop `NaN` keys or values.
16//!
17//! > **Warning:** this diverges from IEEE 754.  Do not use
18//! > `EntryMetaValue::Float` as a numeric comparison type; use it only for
19//! > metadata storage and identity checks.
20
21use alloc::{collections::BTreeMap, string::String, sync::Arc, vec::Vec};
22
23/// A `no_std`-compatible JSON-like value for [`VectorEntry`](super::VectorEntry) metadata.
24///
25/// All heap-owning variants wrap their payload in [`Arc`] so that cloning
26/// is O(1) (a reference-count bump), never O(n) (a deep copy).
27///
28/// # Equality
29///
30/// All variants use structural equality **except** `Float`, which uses
31/// bit-exact comparison so that `NaN == NaN` (see module-level docs).
32#[derive(Clone, Debug)]
33pub enum EntryMetaValue {
34    /// JSON `null`.
35    Null,
36    /// JSON `true` / `false`.
37    Bool(bool),
38    /// JSON integer, stored as `i64`.
39    Int(i64),
40    /// JSON number with fractional part.
41    ///
42    /// Equality is **bit-exact** (`f64::to_bits`), not IEEE 754.
43    /// `NaN == NaN` returns `true` here; see the module docs for rationale.
44    Float(f64),
45    /// JSON string, reference-counted for O(1) clone.
46    String(Arc<str>),
47    /// Raw bytes (Python `bytes` values), reference-counted for O(1) clone.
48    ///
49    /// Stored as a byte slice rather than base64-encoded string to preserve
50    /// Python round-trip parity.
51    Bytes(Arc<[u8]>),
52    /// JSON array, reference-counted for O(1) clone.
53    Array(Arc<[Self]>),
54    /// JSON object (key-ordered `BTreeMap`), reference-counted for O(1) clone.
55    ///
56    /// Keys are [`Arc<str>`] for shared ownership; ordering is deterministic
57    /// (lexicographic), which helps with test fixtures. `HashMap` is not used
58    /// because it requires `std`'s default hasher.
59    Object(Arc<BTreeMap<Arc<str>, Self>>),
60}
61
62impl PartialEq for EntryMetaValue {
63    fn eq(&self, other: &Self) -> bool {
64        match (self, other) {
65            (Self::Null, Self::Null) => true,
66            (Self::Bool(a), Self::Bool(b)) => a == b,
67            (Self::Int(a), Self::Int(b)) => a == b,
68            // Bit-exact float comparison — NaN == NaN for Python dict-key stability.
69            // This intentionally diverges from IEEE 754.
70            (Self::Float(a), Self::Float(b)) => a.to_bits() == b.to_bits(),
71            (Self::String(a), Self::String(b)) => a == b,
72            (Self::Bytes(a), Self::Bytes(b)) => a == b,
73            (Self::Array(a), Self::Array(b)) => a == b,
74            (Self::Object(a), Self::Object(b)) => a == b,
75            _ => false,
76        }
77    }
78}
79
80/// `Eq` is safe because float equality is bit-exact (NaN == NaN by bits).
81impl Eq for EntryMetaValue {}
82
83impl EntryMetaValue {
84    /// Construct a `String` variant from any `&str`, allocating a new Arc.
85    #[must_use]
86    pub fn string(s: &str) -> Self {
87        Self::String(Arc::from(s))
88    }
89
90    /// Construct a `Bytes` variant from a byte slice.
91    #[must_use]
92    pub fn bytes(b: &[u8]) -> Self {
93        Self::Bytes(Arc::from(b))
94    }
95
96    /// Construct an `Array` variant from a `Vec<EntryMetaValue>`.
97    #[must_use]
98    pub fn array(items: Vec<Self>) -> Self {
99        Self::Array(Arc::from(items.into_boxed_slice()))
100    }
101
102    /// Construct an `Object` variant from a `BTreeMap`.
103    #[must_use]
104    pub fn object(map: BTreeMap<Arc<str>, Self>) -> Self {
105        Self::Object(Arc::new(map))
106    }
107
108    /// Returns `true` if this value is `Null`.
109    #[must_use]
110    pub const fn is_null(&self) -> bool {
111        matches!(self, Self::Null)
112    }
113
114    /// Returns the inner `bool`, or `None` if this is not a `Bool` variant.
115    #[must_use]
116    pub const fn as_bool(&self) -> Option<bool> {
117        match self {
118            Self::Bool(b) => Some(*b),
119            _ => None,
120        }
121    }
122
123    /// Returns the inner `i64`, or `None` if this is not an `Int` variant.
124    #[must_use]
125    pub const fn as_int(&self) -> Option<i64> {
126        match self {
127            Self::Int(i) => Some(*i),
128            _ => None,
129        }
130    }
131
132    /// Returns the inner `f64`, or `None` if this is not a `Float` variant.
133    #[must_use]
134    pub const fn as_float(&self) -> Option<f64> {
135        match self {
136            Self::Float(f) => Some(*f),
137            _ => None,
138        }
139    }
140
141    /// Returns the inner `Arc<str>`, or `None` if this is not a `String` variant.
142    #[must_use]
143    pub fn as_str(&self) -> Option<&str> {
144        match self {
145            Self::String(s) => Some(s.as_ref()),
146            _ => None,
147        }
148    }
149
150    /// Returns the inner bytes, or `None` if this is not a `Bytes` variant.
151    #[must_use]
152    pub fn as_bytes(&self) -> Option<&[u8]> {
153        match self {
154            Self::Bytes(b) => Some(b.as_ref()),
155            _ => None,
156        }
157    }
158
159    /// Returns the inner array slice, or `None` if this is not an `Array` variant.
160    #[must_use]
161    pub fn as_array(&self) -> Option<&[Self]> {
162        match self {
163            Self::Array(a) => Some(a.as_ref()),
164            _ => None,
165        }
166    }
167
168    /// Returns the inner object map, or `None` if this is not an `Object` variant.
169    #[must_use]
170    pub fn as_object(&self) -> Option<&BTreeMap<Arc<str>, Self>> {
171        match self {
172            Self::Object(o) => Some(o.as_ref()),
173            _ => None,
174        }
175    }
176}
177
178impl From<bool> for EntryMetaValue {
179    fn from(b: bool) -> Self {
180        Self::Bool(b)
181    }
182}
183
184impl From<i64> for EntryMetaValue {
185    fn from(i: i64) -> Self {
186        Self::Int(i)
187    }
188}
189
190impl From<i32> for EntryMetaValue {
191    fn from(i: i32) -> Self {
192        Self::Int(i64::from(i))
193    }
194}
195
196impl From<f64> for EntryMetaValue {
197    fn from(f: f64) -> Self {
198        Self::Float(f)
199    }
200}
201
202impl From<f32> for EntryMetaValue {
203    fn from(f: f32) -> Self {
204        Self::Float(f64::from(f))
205    }
206}
207
208impl From<String> for EntryMetaValue {
209    fn from(s: String) -> Self {
210        Self::String(Arc::from(s.as_str()))
211    }
212}
213
214impl From<&str> for EntryMetaValue {
215    fn from(s: &str) -> Self {
216        Self::String(Arc::from(s))
217    }
218}