vtcode_commons/
interner.rs1#![expect(
2 clippy::cast_possible_truncation,
3 reason = "Arena offsets are bounded by the compact interner representation and every handle is range-checked in `get_bytes`."
4)]
5
6use std::hash::{Hash, Hasher};
24
25use hashbrown::HashMap;
26use rustc_hash::FxHasher;
27use serde::{Deserialize, Serialize};
28use smallvec::SmallVec;
29
30type U64NoHashMap<V> = HashMap<u64, V, rustc_hash::FxBuildHasher>;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
35pub struct StringId(u32);
36
37impl StringId {
38 #[inline]
40 const fn new(id: u32) -> Self {
41 Self(id)
42 }
43
44 #[inline]
46 pub const fn as_u32(self) -> u32 {
47 self.0
48 }
49}
50
51#[derive(Debug, Clone, Default)]
53pub struct StringInterner {
54 arena: Vec<u8>,
55 lookup: U64NoHashMap<SmallVec<[StringId; 1]>>,
56 offsets: Vec<(u32, u32)>,
57}
58
59impl StringInterner {
60 #[must_use]
62 pub fn new() -> Self {
63 Self::default()
64 }
65
66 #[must_use]
68 pub fn with_capacity(string_bytes: usize, num_strings: usize) -> Self {
69 Self {
70 arena: Vec::with_capacity(string_bytes),
71 lookup: U64NoHashMap::with_capacity_and_hasher(num_strings, rustc_hash::FxBuildHasher),
72 offsets: Vec::with_capacity(num_strings),
73 }
74 }
75
76 fn intern_bytes(&mut self, s: &[u8]) -> StringId {
78 let hash = Self::hash_bytes(s);
79
80 if let Some(ids) = self.lookup.get(&hash) {
81 for &id in ids {
82 if self.get_bytes(id) == Some(s) {
83 return id;
84 }
85 }
86 }
87
88 let start = self.arena.len() as u32;
89 let len = s.len() as u32;
90 self.arena.extend_from_slice(s);
91 let id = StringId::new(self.offsets.len() as u32);
92 self.offsets.push((start, len));
93 self.lookup.entry(hash).or_default().push(id);
94 id
95 }
96
97 #[inline]
99 pub fn intern(&mut self, s: &str) -> StringId {
100 self.intern_bytes(s.as_bytes())
101 }
102
103 fn get_bytes_id(&self, s: &[u8]) -> Option<StringId> {
105 let hash = Self::hash_bytes(s);
106 if let Some(ids) = self.lookup.get(&hash) {
107 for &id in ids {
108 if self.get_bytes(id) == Some(s) {
109 return Some(id);
110 }
111 }
112 }
113 None
114 }
115
116 #[inline]
118 fn get_id(&self, s: &str) -> Option<StringId> {
119 self.get_bytes_id(s.as_bytes())
120 }
121
122 fn get_bytes(&self, id: StringId) -> Option<&[u8]> {
130 let (start, len) = *self.offsets.get(usize::try_from(id.0).ok()?)?;
131 self.arena.get(start as usize..(start as usize + len as usize))
132 }
133
134 pub fn get(&self, id: StringId) -> Option<&str> {
136 self.get_bytes(id).and_then(|b| std::str::from_utf8(b).ok())
137 }
138
139 #[inline]
141 fn len(&self) -> usize {
142 self.offsets.len()
143 }
144
145 #[inline]
147 pub fn is_empty(&self) -> bool {
148 self.offsets.is_empty()
149 }
150
151 #[inline]
153 fn hash_bytes(s: &[u8]) -> u64 {
154 let mut hasher = FxHasher::default();
155 s.hash(&mut hasher);
156 hasher.finish()
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn test_basic_interning() {
166 let mut interner = StringInterner::new();
167 let id1 = interner.intern("src");
168 let id2 = interner.intern("lib");
169 let id3 = interner.intern("src");
170 assert_eq!(id1, id3);
171 assert_ne!(id1, id2);
172 assert_eq!(interner.get(id1), Some("src"));
173 assert_eq!(interner.len(), 2);
174 }
175
176 #[test]
177 fn test_bytes_interning() {
178 let mut interner = StringInterner::new();
179 let id1 = interner.intern_bytes(b"hello");
180 assert_eq!(interner.get(id1), Some("hello"));
181 }
182
183 #[test]
184 fn test_get_id() {
185 let mut interner = StringInterner::new();
186 let id_src = interner.intern("src");
187 assert_eq!(interner.get_id("src"), Some(id_src));
188 assert_eq!(interner.get_id("nonexistent"), None);
189 }
190
191 #[test]
192 fn stale_handle_is_a_miss_not_a_panic() {
193 let interner = StringInterner::new();
197 let never_issued = StringId::new(u32::MAX);
198 assert_eq!(interner.get(never_issued), None);
199
200 let mut populated = StringInterner::new();
201 let issued = populated.intern("src");
202 let beyond_len = StringId::new(issued.as_u32() + 1);
203 assert_eq!(populated.get(beyond_len), None, "handle past the offset table must miss");
204 assert_eq!(populated.get(issued), Some("src"), "live handle must still resolve");
205 }
206}