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