Skip to main content

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}