Skip to main content

hara_native/lang/data/
symbol.rs

1use std::cell::{Cell, RefCell};
2use std::collections::HashMap;
3use std::rc::{Rc, Weak};
4
5use crate::lang::protocol::{
6    HashType, IDisplay, IHash, IHashCached, IMetadata, INamespaced, IObjType, MetaType, ObjType,
7};
8
9thread_local! {
10    static INTERNED: RefCell<HashMap<String, Weak<Data>>> = RefCell::new(HashMap::new());
11}
12
13#[derive(Debug)]
14struct Data {
15    namespace: Option<String>,
16    name: String,
17    full: String,
18    hash: Cell<u64>,
19}
20
21#[derive(Debug, Clone)]
22pub struct Symbol {
23    data: Rc<Data>,
24    metadata: Option<Rc<crate::lang::data::Metadata>>,
25}
26
27impl Symbol {
28    pub fn create(namespace: Option<&str>, name: &str) -> Self {
29        let full = namespace
30            .map(|ns| format!("{ns}/{name}"))
31            .unwrap_or_else(|| name.into());
32        Self::intern(namespace, name, &full)
33    }
34
35    pub fn parse(full: &str) -> Self {
36        let slash = if full == "/" {
37            None
38        } else {
39            full.find(char::from(47))
40        };
41        Self::intern(
42            slash.map(|i| &full[..i]),
43            slash.map(|i| &full[i + 1..]).unwrap_or(full),
44            full,
45        )
46    }
47
48    fn intern(namespace: Option<&str>, name: &str, full: &str) -> Self {
49        INTERNED.with(|cache| {
50            if let Some(value) = cache.borrow().get(full).and_then(Weak::upgrade) {
51                return Self {
52                    data: value,
53                    metadata: None,
54                };
55            }
56            let data = Rc::new(Data {
57                namespace: namespace.map(str::to_owned),
58                name: name.into(),
59                full: full.into(),
60                hash: Cell::new(0),
61            });
62            cache.borrow_mut().insert(full.into(), Rc::downgrade(&data));
63            let symbol = Self {
64                data,
65                metadata: None,
66            };
67            // Java parity: ObjPersistent.hashGet() is populated at creation,
68            // so the first hash lookup on an interned symbol is free.
69            symbol.hash_put(symbol.hash());
70            symbol
71        })
72    }
73
74    pub fn as_str(&self) -> &str {
75        &self.data.full
76    }
77    pub fn same_identity(&self, other: &Self) -> bool {
78        Rc::ptr_eq(&self.data, &other.data)
79    }
80}
81
82impl INamespaced for Symbol {
83    fn get_name(&self) -> &str {
84        &self.data.name
85    }
86    fn get_namespace(&self) -> Option<&str> {
87        self.data.namespace.as_deref()
88    }
89}
90impl IMetadata for Symbol {
91    type Metadata = Rc<crate::lang::data::Metadata>;
92
93    fn meta(&self) -> Option<&Self::Metadata> {
94        self.metadata.as_ref()
95    }
96
97    fn with_meta(&self, metadata: Option<Self::Metadata>) -> Self {
98        Self {
99            data: self.data.clone(),
100            metadata,
101        }
102    }
103
104    fn metatype(&self) -> MetaType {
105        MetaType::String
106    }
107}
108impl IDisplay for Symbol {
109    fn display(&self) -> String {
110        self.data.full.clone()
111    }
112}
113impl IObjType for Symbol {
114    fn obj_type(&self) -> ObjType {
115        ObjType::Symbol
116    }
117}
118impl IHash for Symbol {
119    fn hash_calc(&self, hash_type: HashType) -> u64 {
120        // Mirrors Java exactly: Symbol inherits ObjPersistent.toString() =
121        // class-name + "<" + display() + ">" (deterministic, class-qualified),
122        // so Java hashes "::SYMBOL|hara.lang.data.Symbol<ns/name>".
123        crate::lang::hash::hash_string_type(
124            hash_type,
125            &format!(
126                "{}|hara.lang.data.Symbol<{}>",
127                self.hash_seed(),
128                self.display()
129            ),
130        ) as u64
131    }
132    fn hash_get(&self) -> u64 {
133        self.hash_cached()
134    }
135    fn hash_get_as(&self, hash_type: HashType) -> u64 {
136        self.hash_cached_as(hash_type)
137    }
138}
139impl IHashCached for Symbol {
140    fn hash_current(&self) -> u64 {
141        self.data.hash.get()
142    }
143    fn hash_put(&self, hash: u64) {
144        self.data.hash.set(hash);
145    }
146}
147impl crate::lang::hash::JavaHash for Symbol {
148    fn java_hash(&self, hash_type: HashType) -> i64 {
149        self.hash_calc(hash_type) as i64
150    }
151}
152impl PartialEq for Symbol {
153    fn eq(&self, other: &Self) -> bool {
154        self.data.full == other.data.full
155    }
156}
157impl Eq for Symbol {}
158impl std::hash::Hash for Symbol {
159    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
160        self.data.full.hash(state);
161    }
162}
163
164impl From<&str> for Symbol {
165    fn from(value: &str) -> Self {
166        Self::parse(value)
167    }
168}
169impl From<String> for Symbol {
170    fn from(value: String) -> Self {
171        Self::parse(&value)
172    }
173}
174impl std::fmt::Display for Symbol {
175    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176        f.write_str(self.as_str())
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::Symbol;
183    use crate::lang::protocol::{
184        HashType, IDisplay, IHash, IMetadata, INamespaced, IObjType, MetaType, ObjType,
185    };
186
187    #[test]
188    fn matches_java_namespace_and_interning() {
189        let first = Symbol::parse("hara/name");
190        let second = Symbol::create(Some("hara"), "name");
191        assert!(first.same_identity(&second));
192        assert_eq!(first.get_namespace(), Some("hara"));
193        assert_eq!(first.get_name(), "name");
194        assert_eq!(first.display(), "hara/name");
195        assert_eq!(first.obj_type(), ObjType::Symbol);
196        assert_eq!(first.hash_seed(), "::SYMBOL");
197        assert_eq!(first.metatype(), MetaType::String);
198        assert_eq!(first.hash_get(), first.hash());
199        assert_eq!(first.hash_type(), HashType::Rapid);
200        assert_eq!(Symbol::parse("/").get_namespace(), None);
201        let nested = Symbol::parse("a/b/c");
202        assert_eq!(nested.get_namespace(), Some("a"));
203        assert_eq!(nested.get_name(), "b/c");
204        assert_eq!(nested.as_str(), "a/b/c");
205
206        let multipart = Symbol::create(Some("constructor/namespace"), "name");
207        assert_eq!(multipart.as_str(), "constructor/namespace/name");
208        assert_eq!(multipart.get_namespace(), Some("constructor/namespace"));
209        assert_eq!(multipart.get_name(), "name");
210        let interned = Symbol::parse("constructor/namespace/name");
211        assert!(multipart.same_identity(&interned));
212        assert_eq!(interned.get_namespace(), Some("constructor/namespace"));
213
214        let documented = first.with_meta(Some(crate::lang::data::Metadata::document("doc")));
215        assert_eq!(documented.meta().and_then(|value| value.doc()), Some("doc"));
216        assert_eq!(documented, first);
217        assert!(documented.same_identity(&first));
218    }
219
220    #[test]
221    fn intern_precomputes_the_hash() {
222        use crate::lang::protocol::IHashCached;
223        // Java precomputes the hash at creation (ObjPersistent.hashGet), so
224        // the cached hash is populated at intern time and first use is free.
225        let symbol = Symbol::parse("precomputed/hash");
226        let current = symbol.hash_current();
227        assert_ne!(current, 0);
228        assert_eq!(current, symbol.hash_calc(HashType::Rapid));
229        assert_eq!(symbol.hash_get(), current);
230        assert_eq!(symbol.hash_get_as(HashType::Rapid), current);
231        assert_eq!(
232            symbol.hash_get_as(HashType::Murmur3),
233            symbol.hash_calc(HashType::Murmur3)
234        );
235        // metadata variants share the interned precomputed hash
236        let documented = symbol.with_meta(Some(crate::lang::data::Metadata::document("doc")));
237        assert_eq!(documented.hash_current(), current);
238        assert_eq!(documented.hash_get(), current);
239    }
240}