1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::hash::{BuildHasherDefault, Hasher};
4use std::sync::Arc;
5
6#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14pub struct Symbol {
15 pub namespace: Option<Arc<str>>,
16 pub name: Arc<str>,
17 pub version: Option<Arc<str>>,
19}
20
21impl Symbol {
22 pub fn simple(name: impl Into<Arc<str>>) -> Self {
24 Self {
25 namespace: None,
26 name: name.into(),
27 version: None,
28 }
29 }
30
31 pub fn qualified(ns: impl Into<Arc<str>>, name: impl Into<Arc<str>>) -> Self {
33 Self {
34 namespace: Some(ns.into()),
35 name: name.into(),
36 version: None,
37 }
38 }
39
40 pub fn parse(s: &str) -> Self {
53 INTERNED
54 .try_with(|table| {
55 if let Some(sym) = table.borrow().get(s).cloned() {
56 return sym;
57 }
58 let sym = Self::parse_uncached(s);
59 let mut table = table.borrow_mut();
60 if table.len() < INTERN_CAP {
65 table.insert(Box::from(s), sym.clone());
66 }
67 sym
68 })
69 .unwrap_or_else(|_| Self::parse_uncached(s))
71 }
72
73 fn parse_uncached(s: &str) -> Self {
75 let (ns_part, name_part) = match s.find('/') {
77 Some(idx) if idx > 0 && idx < s.len() - 1 => (Some(&s[..idx]), &s[idx + 1..]),
78 _ => (None, s),
79 };
80
81 let (base_name, version) = split_version(name_part);
84
85 Symbol {
86 namespace: ns_part.map(Arc::from),
87 name: Arc::from(base_name),
88 version: version.map(Arc::from),
89 }
90 }
91
92 pub fn full_name(&self) -> String {
94 match &self.namespace {
95 Some(ns) => format!("{}/{}", ns, self.name),
96 None => self.name.to_string(),
97 }
98 }
99
100 pub fn versioned_name(&self) -> String {
103 match (&self.namespace, &self.version) {
104 (Some(ns), Some(v)) => format!("{}/{}@{}", ns, self.name, v),
105 (Some(ns), None) => format!("{}/{}", ns, self.name),
106 (None, Some(v)) => format!("{}@{}", self.name, v),
107 (None, None) => self.name.to_string(),
108 }
109 }
110}
111
112const INTERN_CAP: usize = 4096;
114
115thread_local! {
116 static INTERNED: RefCell<HashMap<Box<str>, Symbol, BuildHasherDefault<FnvHasher>>> =
122 RefCell::new(HashMap::default());
123}
124
125#[derive(Debug)]
131pub struct FnvHasher(u64);
132
133impl Default for FnvHasher {
134 fn default() -> Self {
135 Self(0xcbf2_9ce4_8422_2325)
136 }
137}
138
139impl Hasher for FnvHasher {
140 fn write(&mut self, bytes: &[u8]) {
141 for b in bytes {
142 self.0 ^= u64::from(*b);
143 self.0 = self.0.wrapping_mul(0x0000_0100_0000_01b3);
144 }
145 }
146
147 fn finish(&self) -> u64 {
148 self.0
149 }
150}
151
152pub fn split_version(name_part: &str) -> (&str, Option<&str>) {
155 if let Some(at_pos) = name_part.rfind('@') {
156 let candidate = &name_part[at_pos + 1..];
157 if is_commit_hash(candidate) {
158 return (&name_part[..at_pos], Some(candidate));
159 }
160 }
161 (name_part, None)
162}
163
164pub fn is_commit_hash(s: &str) -> bool {
167 (7..=40).contains(&s.len()) && s.bytes().all(|b| b.is_ascii_hexdigit())
168}
169
170impl std::fmt::Display for Symbol {
171 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172 write!(f, "{}", self.versioned_name())
173 }
174}
175
176impl cljrs_gc::Trace for Symbol {
177 fn trace(&self, _: &mut cljrs_gc::MarkVisitor) {}
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn test_simple() {
186 let s = Symbol::simple("foo");
187 assert_eq!(s.name.as_ref(), "foo");
188 assert!(s.namespace.is_none());
189 assert!(s.version.is_none());
190 assert_eq!(s.full_name(), "foo");
191 }
192
193 #[test]
194 fn test_qualified() {
195 let s = Symbol::qualified("clojure.core", "map");
196 assert_eq!(s.full_name(), "clojure.core/map");
197 assert!(s.version.is_none());
198 }
199
200 #[test]
201 fn test_parse_unversioned() {
202 assert_eq!(Symbol::parse("foo"), Symbol::simple("foo"));
203 assert_eq!(Symbol::parse("a/b"), Symbol::qualified("a", "b"));
204 assert_eq!(Symbol::parse("/").namespace, None);
205 }
206
207 #[test]
208 fn test_parse_versioned_simple() {
209 let s = Symbol::parse("my-fn@abc1234");
210 assert_eq!(s.name.as_ref(), "my-fn");
211 assert!(s.namespace.is_none());
212 assert_eq!(s.version.as_deref(), Some("abc1234"));
213 assert_eq!(s.versioned_name(), "my-fn@abc1234");
214 }
215
216 #[test]
217 fn test_parse_versioned_qualified() {
218 let s = Symbol::parse("my.ns/my-fn@abc1234");
219 assert_eq!(s.namespace.as_deref(), Some("my.ns"));
220 assert_eq!(s.name.as_ref(), "my-fn");
221 assert_eq!(s.version.as_deref(), Some("abc1234"));
222 }
223
224 #[test]
225 fn test_at_without_valid_hash_is_part_of_name() {
226 let s = Symbol::parse("my-fn@abc");
228 assert_eq!(s.name.as_ref(), "my-fn@abc");
229 assert!(s.version.is_none());
230 }
231
232 #[test]
233 fn test_at_followed_by_non_hex_is_part_of_name() {
234 let s = Symbol::parse("my-fn@not-hex");
235 assert_eq!(s.name.as_ref(), "my-fn@not-hex");
236 assert!(s.version.is_none());
237 }
238
239 #[test]
242 fn memoized_parse_agrees_with_the_uncached_parse() {
243 for s in [
244 "foo",
245 "a/b",
246 "b",
247 "/",
248 "a/b@abc1234",
249 "b@abc1234",
250 "my-fn@abc",
251 "clojure.core/map",
252 "map",
253 ] {
254 assert_eq!(Symbol::parse(s), Symbol::parse_uncached(s), "first {s}");
256 assert_eq!(Symbol::parse(s), Symbol::parse_uncached(s), "cached {s}");
257 }
258 }
259
260 #[test]
262 fn parse_is_correct_beyond_the_intern_cap() {
263 for i in 0..(INTERN_CAP + 64) {
264 let text = format!("gen-{i}/sym-{i}@abc1234");
265 let sym = Symbol::parse(&text);
266 assert_eq!(sym, Symbol::parse_uncached(&text));
267 assert_eq!(sym.namespace.as_deref(), Some(format!("gen-{i}").as_str()));
268 assert_eq!(sym.version.as_deref(), Some("abc1234"));
269 }
270 }
271}