noyalib/interner.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2// Copyright (c) 2026 Noyalib. All rights reserved.
3
4//! Key interning for memory-efficient repeated-key workloads.
5//!
6//! YAML mappings frequently repeat the same key string across
7//! many records — `metadata`, `labels`, `name`, `version`,
8//! `apiVersion` in Kubernetes-shaped documents, `level`, `time`,
9//! `service` in structured logs. Each fresh parse allocates a
10//! brand-new `String` for every key. For a 10 000-record stream
11//! that's tens of thousands of identical heap allocations.
12//!
13//! [`KeyInterner`](crate::interner::KeyInterner) is the primitive
14//! that lets a caller dedupe
15//! those allocations. The intern table maps each unique key text
16//! to a shared `Arc<str>`; subsequent calls with the same key
17//! return a clone of the existing `Arc` instead of allocating.
18//!
19//! Memory model: `Arc<str>` is two pointers (16 bytes on a 64-bit
20//! target) plus the actual string bytes shared via the `Arc`.
21//! For a key that occurs `N` times, the interner's footprint is
22//! `O(unique_keys * key_bytes) + N * 16` versus the
23//! non-interned baseline of `O(N * key_bytes)`. For repeated keys
24//! the win is substantial — 20-byte keys repeated 10 000× drop
25//! from ~200 KB to ~20 bytes + 160 KB of `Arc` pointers.
26//!
27//! # Example: deduplicating keys across a parsed stream
28//!
29//! ```
30//! use noyalib::interner::KeyInterner;
31//! let mut interner = KeyInterner::new();
32//! let a = interner.intern("metadata");
33//! let b = interner.intern("metadata");
34//! // Same `Arc` — second call returns a clone of the cached
35//! // entry. `Arc::ptr_eq` confirms the underlying allocation
36//! // is shared.
37//! assert!(std::sync::Arc::ptr_eq(&a, &b));
38//! assert_eq!(&*a, "metadata");
39//! ```
40//!
41//! # Future direction
42//!
43//! v0.0.1 ships the primitive without changing
44//! [`crate::Mapping`]'s storage type — the `Mapping`'s public API
45//! is `String`-keyed and that contract is preserved across the
46//! initial release. A future major version may switch the
47//! internal key representation to `Arc<str>` and use the
48//! interner transparently during parse; the public surface would
49//! continue to behave like a `String`-keyed map via deref
50//! semantics.
51
52use crate::prelude::*;
53#[cfg(not(feature = "std"))]
54use crate::prelude::{FxBuildHasher, FxHashMap};
55
56#[cfg(feature = "std")]
57type InternMap = std::collections::HashMap<Arc<str>, ()>;
58#[cfg(not(feature = "std"))]
59type InternMap = FxHashMap<Arc<str>, ()>;
60
61/// Interner for `&str` → `Arc<str>` deduplication.
62///
63/// Each call to [`KeyInterner::intern`] returns a shared
64/// `Arc<str>` for the supplied key. The first call allocates the
65/// string; every subsequent call with the same key bytes returns
66/// a clone of the cached `Arc`, sharing the underlying
67/// allocation.
68///
69/// # Examples
70///
71/// ```
72/// use noyalib::interner::KeyInterner;
73/// let mut interner = KeyInterner::new();
74/// let key = interner.intern("port");
75/// assert_eq!(&*key, "port");
76/// assert_eq!(interner.len(), 1);
77///
78/// let key2 = interner.intern("port");
79/// // Re-intern returns the same Arc — no new allocation.
80/// assert!(std::sync::Arc::ptr_eq(&key, &key2));
81/// assert_eq!(interner.len(), 1);
82/// ```
83#[derive(Debug, Default)]
84pub struct KeyInterner {
85 // The value side of the map is `()`. `get_key_value` lets us
86 // return the existing `Arc<str>` clone without allocating a
87 // fresh one when a key is already present.
88 table: InternMap,
89}
90
91impl KeyInterner {
92 /// Construct a fresh interner with no entries.
93 ///
94 /// # Examples
95 ///
96 /// ```
97 /// use noyalib::interner::KeyInterner;
98 /// let interner = KeyInterner::new();
99 /// assert_eq!(interner.len(), 0);
100 /// assert!(interner.is_empty());
101 /// ```
102 #[must_use]
103 pub fn new() -> Self {
104 Self {
105 table: InternMap::default(),
106 }
107 }
108
109 /// Construct an interner with capacity for at least
110 /// `capacity` distinct keys. Useful when the caller knows the
111 /// upper-bound on unique keys (e.g. a schema's field count).
112 ///
113 /// # Examples
114 ///
115 /// ```
116 /// use noyalib::interner::KeyInterner;
117 /// let interner = KeyInterner::with_capacity(64);
118 /// assert!(interner.is_empty());
119 /// ```
120 #[must_use]
121 pub fn with_capacity(capacity: usize) -> Self {
122 #[cfg(feature = "std")]
123 {
124 Self {
125 table: InternMap::with_capacity(capacity),
126 }
127 }
128 #[cfg(not(feature = "std"))]
129 {
130 Self {
131 table: InternMap::with_capacity_and_hasher(capacity, FxBuildHasher),
132 }
133 }
134 }
135
136 /// Intern a key. The first call with a given byte sequence
137 /// allocates an `Arc<str>`; subsequent calls return a clone
138 /// of the cached entry.
139 ///
140 /// # Examples
141 ///
142 /// ```
143 /// use noyalib::interner::KeyInterner;
144 /// let mut interner = KeyInterner::new();
145 /// let one = interner.intern("metadata");
146 /// let two = interner.intern("metadata");
147 /// assert!(std::sync::Arc::ptr_eq(&one, &two));
148 /// ```
149 pub fn intern(&mut self, key: &str) -> Arc<str> {
150 if let Some((existing, ())) = self.table.get_key_value(key) {
151 return Arc::clone(existing);
152 }
153 let arc: Arc<str> = Arc::from(key);
154 let _ = self.table.insert(Arc::clone(&arc), ());
155 arc
156 }
157
158 /// Look up a key without inserting. Returns `Some(arc)` when
159 /// the key is already interned, `None` otherwise.
160 ///
161 /// # Examples
162 ///
163 /// ```
164 /// use noyalib::interner::KeyInterner;
165 /// let mut interner = KeyInterner::new();
166 /// assert!(interner.get("port").is_none());
167 /// let _ = interner.intern("port");
168 /// assert!(interner.get("port").is_some());
169 /// ```
170 #[must_use]
171 pub fn get(&self, key: &str) -> Option<Arc<str>> {
172 self.table
173 .get_key_value(key)
174 .map(|(arc, ())| Arc::clone(arc))
175 }
176
177 /// Number of distinct keys interned so far.
178 ///
179 /// # Examples
180 ///
181 /// ```
182 /// use noyalib::interner::KeyInterner;
183 /// let mut interner = KeyInterner::new();
184 /// let _ = interner.intern("a");
185 /// let _ = interner.intern("b");
186 /// let _ = interner.intern("a"); // not a new key
187 /// assert_eq!(interner.len(), 2);
188 /// ```
189 #[must_use]
190 pub fn len(&self) -> usize {
191 self.table.len()
192 }
193
194 /// Returns `true` if no keys have been interned.
195 ///
196 /// # Examples
197 ///
198 /// ```
199 /// use noyalib::interner::KeyInterner;
200 /// let interner = KeyInterner::new();
201 /// assert!(interner.is_empty());
202 /// ```
203 #[must_use]
204 pub fn is_empty(&self) -> bool {
205 self.table.is_empty()
206 }
207
208 /// Clear every interned key. The underlying `Arc`s remain
209 /// valid for any caller that still holds a clone.
210 ///
211 /// # Examples
212 ///
213 /// ```
214 /// use noyalib::interner::KeyInterner;
215 /// let mut interner = KeyInterner::new();
216 /// let _ = interner.intern("k");
217 /// interner.clear();
218 /// assert!(interner.is_empty());
219 /// ```
220 pub fn clear(&mut self) {
221 self.table.clear();
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn intern_returns_same_arc_for_repeated_key() {
231 let mut interner = KeyInterner::new();
232 let a = interner.intern("metadata");
233 let b = interner.intern("metadata");
234 assert!(Arc::ptr_eq(&a, &b));
235 assert_eq!(interner.len(), 1);
236 }
237
238 #[test]
239 fn intern_distinct_keys_get_distinct_arcs() {
240 let mut interner = KeyInterner::new();
241 let a = interner.intern("port");
242 let b = interner.intern("host");
243 assert!(!Arc::ptr_eq(&a, &b));
244 assert_eq!(interner.len(), 2);
245 }
246
247 #[test]
248 fn empty_string_is_interned() {
249 let mut interner = KeyInterner::new();
250 let a = interner.intern("");
251 let b = interner.intern("");
252 assert!(Arc::ptr_eq(&a, &b));
253 assert_eq!(&*a, "");
254 }
255
256 #[test]
257 fn get_returns_existing_arc_without_inserting() {
258 let mut interner = KeyInterner::new();
259 assert!(interner.get("k").is_none());
260 assert_eq!(interner.len(), 0);
261 let inserted = interner.intern("k");
262 let fetched = interner.get("k").unwrap();
263 assert!(Arc::ptr_eq(&inserted, &fetched));
264 }
265
266 #[test]
267 fn clear_resets_the_intern_table() {
268 let mut interner = KeyInterner::new();
269 let a = interner.intern("k");
270 interner.clear();
271 assert!(interner.is_empty(), "expected empty, got {interner:?}");
272 // The previously-issued Arc is still valid.
273 assert_eq!(&*a, "k");
274 // Re-interning the same key gets a *new* Arc since the
275 // table has been cleared.
276 let b = interner.intern("k");
277 assert!(!Arc::ptr_eq(&a, &b));
278 }
279
280 #[test]
281 fn with_capacity_constructs_empty_interner() {
282 let interner = KeyInterner::with_capacity(128);
283 assert!(interner.is_empty(), "expected empty, got {interner:?}");
284 assert_eq!(interner.len(), 0);
285 }
286
287 #[test]
288 fn dedupes_real_kubernetes_key_set() {
289 // Keys that occur many times in a typical Kubernetes
290 // manifest. After interning, every duplicate `metadata`
291 // / `labels` / `name` / `selector` shares one allocation.
292 let mut interner = KeyInterner::new();
293 let keys = [
294 "apiVersion",
295 "kind",
296 "metadata",
297 "name",
298 "labels",
299 "spec",
300 "selector",
301 "matchLabels",
302 "template",
303 "containers",
304 "image",
305 "ports",
306 "containerPort",
307 ];
308 // Intern every key 100 times to simulate a stream of 100
309 // similar manifests.
310 for _ in 0..100 {
311 for &k in &keys {
312 let _ = interner.intern(k);
313 }
314 }
315 // The table only has one entry per distinct key.
316 assert_eq!(interner.len(), keys.len());
317 }
318}