1use std::cell::RefCell;
32use std::fmt;
33use std::rc::Rc;
34
35use rustc_hash::FxHashMap;
36
37#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
42pub struct Symbol(u32);
43
44impl Symbol {
45 #[must_use]
47 pub fn index(self) -> u32 {
48 self.0
49 }
50}
51
52impl fmt::Debug for Symbol {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 write!(f, "Symbol({})", self.0)
55 }
56}
57
58impl fmt::Display for Symbol {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 write!(f, "#{}", self.0)
61 }
62}
63
64#[derive(Clone, Default)]
69pub struct Interner {
70 map: FxHashMap<Rc<str>, Symbol>,
74 strings: Vec<Rc<str>>,
76}
77
78impl Interner {
79 #[must_use]
81 pub fn new() -> Self {
82 Self {
83 map: FxHashMap::default(),
84 strings: Vec::new(),
85 }
86 }
87
88 #[must_use]
92 pub fn with_capacity(cap: usize) -> Self {
93 Self {
94 map: FxHashMap::with_capacity_and_hasher(cap, rustc_hash::FxBuildHasher::default()),
95 strings: Vec::with_capacity(cap),
96 }
97 }
98
99 pub fn intern(&mut self, s: &str) -> Symbol {
102 if let Some(&sym) = self.map.get(s) {
103 return sym;
104 }
105 let rc: Rc<str> = Rc::from(s);
106 let sym = Symbol(u32::try_from(self.strings.len()).expect("interner overflow"));
107 self.strings.push(Rc::clone(&rc));
108 self.map.insert(rc, sym);
109 sym
110 }
111
112 #[must_use]
118 pub fn resolve(&self, sym: Symbol) -> &str {
119 &self.strings[sym.0 as usize]
120 }
121
122 #[must_use]
130 pub fn resolve_rc(&self, sym: Symbol) -> Rc<str> {
131 Rc::clone(&self.strings[sym.0 as usize])
132 }
133
134 #[must_use]
136 pub fn try_resolve(&self, sym: Symbol) -> Option<&str> {
137 self.strings.get(sym.0 as usize).map(AsRef::as_ref)
138 }
139
140 #[must_use]
142 pub fn lookup(&self, s: &str) -> Option<Symbol> {
143 self.map.get(s).copied()
144 }
145
146 #[must_use]
148 pub fn len(&self) -> usize {
149 self.strings.len()
150 }
151
152 #[must_use]
154 pub fn is_empty(&self) -> bool {
155 self.strings.is_empty()
156 }
157}
158
159impl fmt::Debug for Interner {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 write!(f, "Interner({} strings)", self.strings.len())
162 }
163}
164
165thread_local! {
168 static GLOBAL_INTERNER: RefCell<Interner> = RefCell::new(Interner::with_capacity(512));
169}
170
171pub fn intern(s: &str) -> Symbol {
173 GLOBAL_INTERNER.with(|i| i.borrow_mut().intern(s))
174}
175
176#[must_use]
182pub fn resolve(sym: Symbol) -> String {
183 GLOBAL_INTERNER.with(|i| i.borrow().resolve(sym).to_string())
184}
185
186#[must_use]
188pub fn resolve_rc(sym: Symbol) -> Rc<str> {
189 GLOBAL_INTERNER.with(|i| i.borrow().resolve_rc(sym))
190}
191
192pub fn with_resolved<F, R>(sym: Symbol, f: F) -> R
195where
196 F: FnOnce(&str) -> R,
197{
198 GLOBAL_INTERNER.with(|i| f(i.borrow().resolve(sym)))
199}
200
201#[must_use]
204pub fn lookup(s: &str) -> Option<Symbol> {
205 GLOBAL_INTERNER.with(|i| i.borrow().lookup(s))
206}
207
208pub fn prewarm() {
223 GLOBAL_INTERNER.with(|i| {
224 let mut guard = i.borrow_mut();
225 for s in HOT_SYMBOLS {
226 guard.intern(s);
227 }
228 });
229}
230
231const HOT_SYMBOLS: &[&str] = &[
235 "",
237 "name",
239 "pname",
240 "version",
241 "src",
242 "system",
243 "builder",
244 "args",
245 "outputs",
246 "out",
247 "dev",
248 "bin",
249 "man",
250 "doc",
251 "outputHash",
252 "outputHashAlgo",
253 "outputHashMode",
254 "passAsFile",
255 "preferLocalBuild",
256 "allowSubstitutes",
257 "buildInputs",
259 "nativeBuildInputs",
260 "propagatedBuildInputs",
261 "propagatedNativeBuildInputs",
262 "checkInputs",
263 "nativeCheckInputs",
264 "buildPhase",
265 "installPhase",
266 "configurePhase",
267 "patchPhase",
268 "unpackPhase",
269 "config",
271 "options",
272 "imports",
273 "_module",
274 "mkOption",
275 "mkDefault",
276 "mkForce",
277 "mkIf",
278 "mkMerge",
279 "mkOverride",
280 "type",
281 "default",
282 "description",
283 "example",
284 "visible",
285 "internal",
286 "readOnly",
287 "inputs",
289 "url",
290 "flake",
291 "follows",
292 "packages",
293 "devShells",
294 "apps",
295 "overlays",
296 "nixosModules",
297 "nixosConfigurations",
298 "darwinModules",
299 "darwinConfigurations",
300 "homeModules",
301 "homeConfigurations",
302 "templates",
303 "checks",
304 "formatter",
305 "legacyPackages",
306 "nixpkgs",
308 "pkgs",
309 "stdenv",
310 "lib",
311 "hostPlatform",
312 "buildPlatform",
313 "targetPlatform",
314 "isDarwin",
315 "isLinux",
316 "x86_64-linux",
317 "aarch64-linux",
318 "x86_64-darwin",
319 "aarch64-darwin",
320 "meta",
322 "platforms",
323 "homepage",
324 "license",
325 "maintainers",
326 "mainProgram",
327 "available",
328 "broken",
329 "insecure",
330 "unsupported",
331 "recurseIntoAttrs",
333 "__functor",
334 "__toString",
335 "__ignoreNulls",
336 "outPath",
337 "drvPath",
338 "attrs",
339 "outputName",
340 "true",
342 "false",
343 "null",
344];
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349
350 #[test]
351 fn intern_returns_same_symbol() {
352 let mut interner = Interner::new();
353 let s1 = interner.intern("hello");
354 let s2 = interner.intern("hello");
355 assert_eq!(s1, s2);
356 }
357
358 #[test]
359 fn different_strings_different_symbols() {
360 let mut interner = Interner::new();
361 let s1 = interner.intern("hello");
362 let s2 = interner.intern("world");
363 assert_ne!(s1, s2);
364 }
365
366 #[test]
367 fn resolve_roundtrip() {
368 let mut interner = Interner::new();
369 let sym = interner.intern("foo");
370 assert_eq!(interner.resolve(sym), "foo");
371 }
372
373 #[test]
374 fn resolve_rc_shares_allocation() {
375 let mut interner = Interner::new();
376 let sym = interner.intern("shared");
377 let a = interner.resolve_rc(sym);
378 let b = interner.resolve_rc(sym);
379 assert_eq!(&*a, "shared");
380 assert_eq!(&*b, "shared");
381 assert!(Rc::ptr_eq(&a, &b));
383 }
384
385 #[test]
386 fn lookup_existing() {
387 let mut interner = Interner::new();
388 let sym = interner.intern("bar");
389 assert_eq!(interner.lookup("bar"), Some(sym));
390 }
391
392 #[test]
393 fn lookup_missing() {
394 let interner = Interner::new();
395 assert_eq!(interner.lookup("missing"), None);
396 }
397
398 #[test]
399 fn len_and_empty() {
400 let mut interner = Interner::new();
401 assert!(interner.is_empty());
402 assert_eq!(interner.len(), 0);
403 interner.intern("a");
404 interner.intern("b");
405 interner.intern("a"); assert_eq!(interner.len(), 2);
407 assert!(!interner.is_empty());
408 }
409
410 #[test]
411 fn symbol_ordering() {
412 let mut interner = Interner::new();
413 let s1 = interner.intern("alpha");
414 let s2 = interner.intern("beta");
415 assert!(s1 < s2);
417 }
418
419 #[test]
420 fn try_resolve_valid() {
421 let mut interner = Interner::new();
422 let sym = interner.intern("test");
423 assert_eq!(interner.try_resolve(sym), Some("test"));
424 }
425
426 #[test]
427 fn try_resolve_invalid() {
428 let interner = Interner::new();
429 assert_eq!(interner.try_resolve(Symbol(999)), None);
430 }
431
432 #[test]
433 fn clone_interner() {
434 let mut interner = Interner::new();
435 let s1 = interner.intern("hello");
436 let cloned = interner.clone();
437 assert_eq!(cloned.resolve(s1), "hello");
438 assert_eq!(cloned.len(), 1);
439 }
440
441 #[test]
442 fn with_capacity_preallocates() {
443 let interner = Interner::with_capacity(256);
444 assert!(interner.is_empty());
445 assert_eq!(interner.len(), 0);
447 }
448
449 #[test]
453 fn thread_local_intern_resolve() {
454 std::thread::spawn(|| {
455 let sym = intern("thread_local_test");
456 let resolved = resolve(sym);
457 assert_eq!(resolved, "thread_local_test");
458 })
459 .join()
460 .unwrap();
461 }
462
463 #[test]
464 fn thread_local_intern_dedup() {
465 std::thread::spawn(|| {
466 let s1 = intern("dedup");
467 let s2 = intern("dedup");
468 assert_eq!(s1, s2);
469 })
470 .join()
471 .unwrap();
472 }
473
474 #[test]
475 fn thread_local_resolve_rc_zero_copy() {
476 std::thread::spawn(|| {
477 let sym = intern("tl_rc");
478 let a = resolve_rc(sym);
479 let b = resolve_rc(sym);
480 assert!(Rc::ptr_eq(&a, &b));
481 })
482 .join()
483 .unwrap();
484 }
485
486 #[test]
487 fn thread_local_with_resolved_no_alloc() {
488 std::thread::spawn(|| {
489 let sym = intern("borrowed");
490 let len = with_resolved(sym, str::len);
491 assert_eq!(len, "borrowed".len());
492 })
493 .join()
494 .unwrap();
495 }
496
497 #[test]
498 fn thread_local_lookup() {
499 std::thread::spawn(|| {
500 assert_eq!(lookup("never_interned_here"), None);
501 let sym = intern("findme");
502 assert_eq!(lookup("findme"), Some(sym));
503 })
504 .join()
505 .unwrap();
506 }
507
508 #[test]
509 fn prewarm_populates_hot_set() {
510 std::thread::spawn(|| {
511 prewarm();
513 for &s in HOT_SYMBOLS {
514 assert!(
515 lookup(s).is_some(),
516 "prewarm should have interned {s:?}"
517 );
518 }
519 let before = HOT_SYMBOLS.len();
521 prewarm();
522 let sym_first = lookup("name").expect("name interned by prewarm");
526 assert!(sym_first.index() < u32::try_from(before).unwrap());
527 })
528 .join()
529 .unwrap();
530 }
531
532 #[test]
533 fn hot_symbols_unique() {
534 let mut seen = std::collections::HashSet::new();
537 for &s in HOT_SYMBOLS {
538 assert!(seen.insert(s), "HOT_SYMBOLS contains duplicate {s:?}");
539 }
540 }
541}