rune/core/object/
hashtable.rs

1//! The design of the hashtable is based on the requirments we have. First is we
2//! need it to support being both thread local and global. Second we need
3//! iterate and mutate at the same time. Third we need to be able to clean up
4//! the heap allocation when it is garbage collected.
5use super::{CloneIn, Gc, IntoObject, ObjCell, Object, WithLifetime};
6use crate::core::env::INTERNED_SYMBOLS;
7use crate::core::gc::{Block, GcHeap, GcState, Trace};
8use crate::derive_GcMoveable;
9use rune_core::hashmap::{HashSet, IndexMap};
10use rune_macros::Trace;
11use std::cell::RefCell;
12use std::fmt::{self, Debug, Display, Write};
13use std::ptr::NonNull;
14use std::sync::Mutex;
15
16pub(crate) type HashTable<'ob> = IndexMap<Object<'ob>, Object<'ob>>;
17
18#[derive(PartialEq, Trace)]
19pub(crate) struct LispHashTable(GcHeap<HashTableCore<'static>>);
20
21impl Eq for LispHashTable {}
22
23derive_GcMoveable!(LispHashTable);
24
25impl Debug for LispHashTable {
26    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
27        self.display_walk(f, &mut HashSet::default())
28    }
29}
30
31impl Display for LispHashTable {
32    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
33        self.display_walk(f, &mut HashSet::default())
34    }
35}
36
37impl LispHashTable {
38    pub(in crate::core) unsafe fn new(table: HashTable, constant: bool) -> Self {
39        Self(GcHeap::new(HashTableCore::new(table, constant), constant))
40    }
41
42    pub(in crate::core) fn forwarding_ptr(&self) -> Option<NonNull<u8>> {
43        use crate::core::gc::AllocState as A;
44        match self.0.allocation_state() {
45            A::Forwarded(f) => Some(f),
46            A::Global => panic!("global hashtable allocation found in local heap"),
47            A::Unmoved => None,
48        }
49    }
50}
51
52struct HashTableCore<'ob>(HashTableType<'ob>);
53
54// Hashtables are currently the only data structure that can be shared between
55// threads. This is because it is used for caching in some functions in
56// cl-generic.
57enum HashTableType<'a> {
58    Local(RefCell<HashTableInner<'a>>),
59    Global(Mutex<HashTableInner<'a>>),
60}
61
62struct HashTableInner<'ob> {
63    // The current index of a [`maphash`] iterator. This is needed because we
64    // can't hold the hashtable across calls to elisp (it might mutate it).
65    iter_idx: usize,
66    inner: HashTable<'ob>,
67}
68
69impl LispHashTable {
70    pub(crate) fn len(&self) -> usize {
71        self.0.with(|x| x.len())
72    }
73
74    pub(crate) fn get(&self, key: Object) -> Option<Object<'_>> {
75        self.0.with(|x| x.get(&key).copied())
76    }
77
78    pub(crate) fn get_index(&self, index: usize) -> Option<(Object, Object)> {
79        self.0.with(|x| x.get_index(index).map(|(k, v)| (*k, *v)))
80    }
81
82    pub(crate) fn get_index_of(&self, key: Object) -> Option<usize> {
83        self.0.with(|x| x.get_index_of(&key))
84    }
85
86    pub(crate) fn insert(&self, key: Object, value: Object) {
87        match &self.0.0 {
88            HashTableType::Local(table) => {
89                let key = unsafe { key.with_lifetime() };
90                let value = unsafe { value.with_lifetime() };
91                table.borrow_mut().inner.insert(key, value)
92            }
93            HashTableType::Global(table) => {
94                let map = INTERNED_SYMBOLS.lock().unwrap();
95                let block = map.global_block();
96                // Need to clone these objects in the global block since this
97                // hashtable is globally shared
98                let key = unsafe { key.clone_in(block).with_lifetime() };
99                let value = unsafe { value.clone_in(block).with_lifetime() };
100                table.lock().unwrap().inner.insert(key, value)
101            }
102        };
103    }
104
105    pub(crate) fn shift_remove(&self, key: Object) {
106        let key = unsafe { key.with_lifetime() };
107        self.0.with(|x| x.shift_remove(&key));
108    }
109
110    pub(crate) fn get_iter_index(&self) -> usize {
111        match &self.0.0 {
112            HashTableType::Local(table) => table.borrow().iter_idx,
113            HashTableType::Global(table) => table.lock().unwrap().iter_idx,
114        }
115    }
116
117    pub(crate) fn set_iter_index(&self, index: usize) {
118        match &self.0.0 {
119            HashTableType::Local(table) => table.borrow_mut().iter_idx = index,
120            HashTableType::Global(table) => table.lock().unwrap().iter_idx = index,
121        }
122    }
123}
124
125impl<'a> HashTableCore<'a> {
126    unsafe fn new(table: HashTable, constant: bool) -> Self {
127        let table = std::mem::transmute::<HashTable<'_>, HashTable<'a>>(table);
128        let inner = HashTableInner { iter_idx: 0, inner: table };
129        if constant {
130            HashTableCore(HashTableType::Global(Mutex::new(inner)))
131        } else {
132            HashTableCore(HashTableType::Local(RefCell::new(inner)))
133        }
134    }
135
136    fn with<F, T>(&self, mut f: F) -> T
137    where
138        F: FnMut(&mut HashTable<'a>) -> T,
139    {
140        match &self.0 {
141            HashTableType::Local(table) => f(&mut table.borrow_mut().inner),
142            HashTableType::Global(table) => f(&mut table.lock().unwrap().inner),
143        }
144    }
145}
146
147impl Trace for HashTableCore<'_> {
148    fn trace(&self, state: &mut GcState) {
149        let HashTableType::Local(table) = &self.0 else {
150            panic!("Global hash table should not be traced")
151        };
152        let table = &mut table.borrow_mut().inner;
153        // ObjCell are updated in place when traced, so casting to ObjCell will
154        // allow all the objects to be updated.
155        let table = unsafe {
156            std::mem::transmute::<&mut IndexMap<Object, Object>, &mut IndexMap<ObjCell, ObjCell>>(
157                table,
158            )
159        };
160        table.rehash_keys(|key, val| {
161            key.trace(state);
162            val.trace(state);
163        });
164    }
165}
166
167impl PartialEq for HashTableCore<'_> {
168    fn eq(&self, other: &Self) -> bool {
169        std::ptr::eq(self, other)
170    }
171}
172
173impl<'new> CloneIn<'new, &'new Self> for LispHashTable {
174    fn clone_in<const C: bool>(&self, bk: &'new Block<C>) -> Gc<&'new Self> {
175        let mut table = HashTable::default();
176        self.0.with(|x| {
177            for (key, value) in x {
178                let new_key = key.clone_in(bk);
179                let new_value = value.clone_in(bk);
180                table.insert(new_key, new_value);
181            }
182        });
183        table.into_obj(bk)
184    }
185}
186
187impl LispHashTable {
188    pub(super) fn display_walk(
189        &self,
190        f: &mut fmt::Formatter,
191        seen: &mut HashSet<*const u8>,
192    ) -> fmt::Result {
193        let ptr = (&*self.0 as *const HashTableCore).cast();
194        if seen.contains(&ptr) {
195            return write!(f, "#0");
196        }
197        seen.insert(ptr);
198
199        write!(f, "#s(hash-table (")?;
200        self.0.with(|x| {
201            for (i, (k, v)) in x.iter().enumerate() {
202                if i != 0 {
203                    f.write_char(' ')?;
204                }
205                k.untag().display_walk(f, seen)?;
206                f.write_char(' ')?;
207                v.untag().display_walk(f, seen)?;
208            }
209            Ok(())
210        })?;
211        write!(f, "))")
212    }
213}