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}