use std::cell::RefCell;
use std::collections::HashMap;
use std::hash::{BuildHasherDefault, Hasher};
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Symbol {
pub namespace: Option<Arc<str>>,
pub name: Arc<str>,
pub version: Option<Arc<str>>,
}
impl Symbol {
pub fn simple(name: impl Into<Arc<str>>) -> Self {
Self {
namespace: None,
name: name.into(),
version: None,
}
}
pub fn qualified(ns: impl Into<Arc<str>>, name: impl Into<Arc<str>>) -> Self {
Self {
namespace: Some(ns.into()),
name: name.into(),
version: None,
}
}
pub fn parse(s: &str) -> Self {
INTERNED
.try_with(|table| {
if let Some(sym) = table.borrow().get(s).cloned() {
return sym;
}
let sym = Self::parse_uncached(s);
let mut table = table.borrow_mut();
if table.len() < INTERN_CAP {
table.insert(Box::from(s), sym.clone());
}
sym
})
.unwrap_or_else(|_| Self::parse_uncached(s))
}
fn parse_uncached(s: &str) -> Self {
let (ns_part, name_part) = match s.find('/') {
Some(idx) if idx > 0 && idx < s.len() - 1 => (Some(&s[..idx]), &s[idx + 1..]),
_ => (None, s),
};
let (base_name, version) = split_version(name_part);
Symbol {
namespace: ns_part.map(Arc::from),
name: Arc::from(base_name),
version: version.map(Arc::from),
}
}
pub fn full_name(&self) -> String {
match &self.namespace {
Some(ns) => format!("{}/{}", ns, self.name),
None => self.name.to_string(),
}
}
pub fn versioned_name(&self) -> String {
match (&self.namespace, &self.version) {
(Some(ns), Some(v)) => format!("{}/{}@{}", ns, self.name, v),
(Some(ns), None) => format!("{}/{}", ns, self.name),
(None, Some(v)) => format!("{}@{}", self.name, v),
(None, None) => self.name.to_string(),
}
}
}
const INTERN_CAP: usize = 4096;
thread_local! {
static INTERNED: RefCell<HashMap<Box<str>, Symbol, BuildHasherDefault<FnvHasher>>> =
RefCell::new(HashMap::default());
}
#[derive(Debug)]
pub struct FnvHasher(u64);
impl Default for FnvHasher {
fn default() -> Self {
Self(0xcbf2_9ce4_8422_2325)
}
}
impl Hasher for FnvHasher {
fn write(&mut self, bytes: &[u8]) {
for b in bytes {
self.0 ^= u64::from(*b);
self.0 = self.0.wrapping_mul(0x0000_0100_0000_01b3);
}
}
fn finish(&self) -> u64 {
self.0
}
}
pub fn split_version(name_part: &str) -> (&str, Option<&str>) {
if let Some(at_pos) = name_part.rfind('@') {
let candidate = &name_part[at_pos + 1..];
if is_commit_hash(candidate) {
return (&name_part[..at_pos], Some(candidate));
}
}
(name_part, None)
}
pub fn is_commit_hash(s: &str) -> bool {
(7..=40).contains(&s.len()) && s.bytes().all(|b| b.is_ascii_hexdigit())
}
impl std::fmt::Display for Symbol {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.versioned_name())
}
}
impl cljrs_gc::Trace for Symbol {
fn trace(&self, _: &mut cljrs_gc::MarkVisitor) {}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple() {
let s = Symbol::simple("foo");
assert_eq!(s.name.as_ref(), "foo");
assert!(s.namespace.is_none());
assert!(s.version.is_none());
assert_eq!(s.full_name(), "foo");
}
#[test]
fn test_qualified() {
let s = Symbol::qualified("clojure.core", "map");
assert_eq!(s.full_name(), "clojure.core/map");
assert!(s.version.is_none());
}
#[test]
fn test_parse_unversioned() {
assert_eq!(Symbol::parse("foo"), Symbol::simple("foo"));
assert_eq!(Symbol::parse("a/b"), Symbol::qualified("a", "b"));
assert_eq!(Symbol::parse("/").namespace, None);
}
#[test]
fn test_parse_versioned_simple() {
let s = Symbol::parse("my-fn@abc1234");
assert_eq!(s.name.as_ref(), "my-fn");
assert!(s.namespace.is_none());
assert_eq!(s.version.as_deref(), Some("abc1234"));
assert_eq!(s.versioned_name(), "my-fn@abc1234");
}
#[test]
fn test_parse_versioned_qualified() {
let s = Symbol::parse("my.ns/my-fn@abc1234");
assert_eq!(s.namespace.as_deref(), Some("my.ns"));
assert_eq!(s.name.as_ref(), "my-fn");
assert_eq!(s.version.as_deref(), Some("abc1234"));
}
#[test]
fn test_at_without_valid_hash_is_part_of_name() {
let s = Symbol::parse("my-fn@abc");
assert_eq!(s.name.as_ref(), "my-fn@abc");
assert!(s.version.is_none());
}
#[test]
fn test_at_followed_by_non_hex_is_part_of_name() {
let s = Symbol::parse("my-fn@not-hex");
assert_eq!(s.name.as_ref(), "my-fn@not-hex");
assert!(s.version.is_none());
}
#[test]
fn memoized_parse_agrees_with_the_uncached_parse() {
for s in [
"foo",
"a/b",
"b",
"/",
"a/b@abc1234",
"b@abc1234",
"my-fn@abc",
"clojure.core/map",
"map",
] {
assert_eq!(Symbol::parse(s), Symbol::parse_uncached(s), "first {s}");
assert_eq!(Symbol::parse(s), Symbol::parse_uncached(s), "cached {s}");
}
}
#[test]
fn parse_is_correct_beyond_the_intern_cap() {
for i in 0..(INTERN_CAP + 64) {
let text = format!("gen-{i}/sym-{i}@abc1234");
let sym = Symbol::parse(&text);
assert_eq!(sym, Symbol::parse_uncached(&text));
assert_eq!(sym.namespace.as_deref(), Some(format!("gen-{i}").as_str()));
assert_eq!(sym.version.as_deref(), Some("abc1234"));
}
}
}