Skip to main content

cljrs_value/
intern.rs

1//! Global keyword and symbol intern tables (Phase B3).
2//!
3//! Interned keywords and symbols are allocated once into program-lifetime
4//! memory and reused for every subsequent request with the same identity.
5//! This gives each unique (namespace, name) pair a stable address that is
6//! consistent across all isolates — required for correct hash-map key lookups
7//! when maps move between isolates via the structured-clone boundary.
8//!
9//! ## Design
10//!
11//! Each table is a `OnceLock<Mutex<HashMap<…, StaticGcPtr<T>>>>`.  On the
12//! first call for a given key the value is allocated via [`cljrs_gc::static_alloc`]
13//! (arena in `no-gc` builds, `Box::leak` in GC builds) and the pointer is
14//! inserted.  Subsequent calls return a clone of the stored pointer (O(1),
15//! just copies a `NonNull`).
16//!
17//! ## Contention
18//!
19//! The global `Mutex` is only held during the brief table lookup + optional
20//! insert.  Keywords are created at read/compile time, not in hot evaluation
21//! loops, so contention is not a concern for now.  A sharded or lock-free
22//! table can replace this if profiling ever shows otherwise.
23
24use std::collections::HashMap;
25use std::sync::{Arc, Mutex, OnceLock};
26
27use cljrs_gc::{StaticGcPtr, static_alloc};
28
29use crate::keyword::Keyword;
30use crate::symbol::Symbol;
31
32// ── Keyword intern table ──────────────────────────────────────────────────────
33
34type KwKey = (Option<Arc<str>>, Arc<str>);
35static KEYWORD_TABLE: OnceLock<Mutex<HashMap<KwKey, StaticGcPtr<Keyword>>>> = OnceLock::new();
36
37fn kw_table() -> &'static Mutex<HashMap<KwKey, StaticGcPtr<Keyword>>> {
38    KEYWORD_TABLE.get_or_init(|| Mutex::new(HashMap::new()))
39}
40
41/// Intern a keyword into program-lifetime memory.
42///
43/// The first call for a given `(namespace, name)` pair allocates the
44/// `Keyword` and stores it; subsequent calls return the same `StaticGcPtr`.
45/// The returned pointer is `Send + Sync` and valid for the lifetime of the
46/// process.
47pub fn intern_keyword(namespace: Option<&str>, name: &str) -> StaticGcPtr<Keyword> {
48    let ns_arc: Option<Arc<str>> = namespace.map(Arc::from);
49    let name_arc: Arc<str> = Arc::from(name);
50    let key = (ns_arc.clone(), name_arc.clone());
51
52    let mut table = kw_table().lock().unwrap();
53    if let Some(existing) = table.get(&key) {
54        return existing.clone();
55    }
56    let kw = Keyword {
57        namespace: ns_arc,
58        name: name_arc,
59    };
60    let ptr = static_alloc(kw);
61    table.insert(key, ptr.clone());
62    ptr
63}
64
65// ── Symbol intern table ───────────────────────────────────────────────────────
66
67type SymKey = (Option<Arc<str>>, Arc<str>, Option<Arc<str>>);
68static SYMBOL_TABLE: OnceLock<Mutex<HashMap<SymKey, StaticGcPtr<Symbol>>>> = OnceLock::new();
69
70fn sym_table() -> &'static Mutex<HashMap<SymKey, StaticGcPtr<Symbol>>> {
71    SYMBOL_TABLE.get_or_init(|| Mutex::new(HashMap::new()))
72}
73
74/// Intern a symbol into program-lifetime memory.
75///
76/// The first call for a given `(namespace, name, version)` triple allocates
77/// the `Symbol`; subsequent calls return the same `StaticGcPtr`.
78pub fn intern_symbol(
79    namespace: Option<&str>,
80    name: &str,
81    version: Option<&str>,
82) -> StaticGcPtr<Symbol> {
83    let ns_arc: Option<Arc<str>> = namespace.map(Arc::from);
84    let name_arc: Arc<str> = Arc::from(name);
85    let ver_arc: Option<Arc<str>> = version.map(Arc::from);
86    let key = (ns_arc.clone(), name_arc.clone(), ver_arc.clone());
87
88    let mut table = sym_table().lock().unwrap();
89    if let Some(existing) = table.get(&key) {
90        return existing.clone();
91    }
92    let sym = Symbol {
93        namespace: ns_arc,
94        name: name_arc,
95        version: ver_arc,
96    };
97    let ptr = static_alloc(sym);
98    table.insert(key, ptr.clone());
99    ptr
100}
101
102// ── Tests ─────────────────────────────────────────────────────────────────────
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use cljrs_gc::StaticGcPtr;
108
109    #[test]
110    fn keyword_intern_returns_same_ptr_for_same_name() {
111        let a = intern_keyword(None, "foo");
112        let b = intern_keyword(None, "foo");
113        assert!(
114            StaticGcPtr::ptr_eq(&a, &b),
115            "same name must return same pointer"
116        );
117    }
118
119    #[test]
120    fn keyword_intern_qualified() {
121        let a = intern_keyword(Some("clojure.core"), "map");
122        let b = intern_keyword(Some("clojure.core"), "map");
123        assert!(StaticGcPtr::ptr_eq(&a, &b));
124        assert_eq!(a.get().namespace.as_deref(), Some("clojure.core"));
125        assert_eq!(a.get().name.as_ref(), "map");
126    }
127
128    #[test]
129    fn keyword_intern_different_names_differ() {
130        let a = intern_keyword(None, "foo");
131        let b = intern_keyword(None, "bar");
132        assert!(!StaticGcPtr::ptr_eq(&a, &b));
133    }
134
135    #[test]
136    fn symbol_intern_returns_same_ptr() {
137        let a = intern_symbol(None, "foo", None);
138        let b = intern_symbol(None, "foo", None);
139        assert!(StaticGcPtr::ptr_eq(&a, &b));
140    }
141
142    #[test]
143    fn symbol_intern_versioned() {
144        let a = intern_symbol(Some("my.ns"), "myfn", Some("abc1234"));
145        let b = intern_symbol(Some("my.ns"), "myfn", Some("abc1234"));
146        assert!(StaticGcPtr::ptr_eq(&a, &b));
147        assert_eq!(a.get().version.as_deref(), Some("abc1234"));
148    }
149}