Skip to main content

geam_core/host/external/
store.rs

1use crate::host::{HostExternalEquality, HostExternalHashing, HostExternalInspection};
2use ecow::EcoString;
3use std::cell::{OnceCell, RefCell};
4use std::collections::HashMap;
5use std::ops::Deref;
6use std::rc::{Rc, Weak};
7use std::sync::atomic::{AtomicU64, Ordering};
8
9static NEXT_EXTERNAL_VALUE_ID: AtomicU64 = AtomicU64::new(0);
10
11pub struct HostExternalStore<Payload> {
12    values: Rc<RefCell<HashMap<u64, Rc<StoredExternalPayload<Payload>>>>>,
13}
14
15pub(crate) struct ExternalPayloadLease {
16    release: Rc<dyn ExternalPayloadRelease>,
17    value: Rc<dyn ExternalPayload>,
18}
19
20pub(crate) struct ExternalPayloadView<Payload> {
21    value: Rc<StoredExternalPayload<Payload>>,
22}
23
24struct StoredExternalPayload<Payload> {
25    id: u64,
26    value: Payload,
27    values: Weak<RefCell<HashMap<u64, Rc<StoredExternalPayload<Payload>>>>>,
28    source_equal: for<'context> fn(&HostExternalEquality<'context>, &Payload, &Payload) -> bool,
29    source_hash: for<'context> fn(&HostExternalHashing<'context>, &Payload) -> u64,
30    inspect: for<'context> fn(&HostExternalInspection<'context>, &Payload) -> EcoString,
31    source_hash_cache: OnceCell<u64>,
32    inspection_cache: OnceCell<EcoString>,
33}
34
35struct ExternalPayloadReleaseGuard<Payload> {
36    id: u64,
37    values: Weak<RefCell<HashMap<u64, Rc<StoredExternalPayload<Payload>>>>>,
38}
39
40trait ExternalPayload {
41    fn id(&self) -> u64;
42    fn source_hash(&self, context: &HostExternalHashing<'_>) -> u64;
43    fn inspection<'payload>(
44        &'payload self,
45        context: &HostExternalInspection<'_>,
46    ) -> &'payload EcoString;
47    fn source_equal(
48        &self,
49        context: &HostExternalEquality<'_>,
50        other: &ExternalPayloadLease,
51    ) -> bool;
52}
53
54trait ExternalPayloadRelease {}
55
56impl<Payload> Default for HostExternalStore<Payload> {
57    fn default() -> Self {
58        Self {
59            values: Rc::new(RefCell::new(HashMap::new())),
60        }
61    }
62}
63
64impl<Payload> HostExternalStore<Payload>
65where
66    Payload: 'static,
67{
68    pub(crate) fn clone_handle(&self) -> Self {
69        Self {
70            values: Rc::clone(&self.values),
71        }
72    }
73
74    pub(crate) fn insert(
75        &self,
76        value: Payload,
77        source_equal: for<'context> fn(&HostExternalEquality<'context>, &Payload, &Payload) -> bool,
78        source_hash: for<'context> fn(&HostExternalHashing<'context>, &Payload) -> u64,
79        inspect: for<'context> fn(&HostExternalInspection<'context>, &Payload) -> EcoString,
80    ) -> ExternalPayloadLease {
81        let id = NEXT_EXTERNAL_VALUE_ID.fetch_add(1, Ordering::Relaxed);
82        let value = Rc::new(StoredExternalPayload {
83            id,
84            value,
85            values: Rc::downgrade(&self.values),
86            source_equal,
87            source_hash,
88            inspect,
89            source_hash_cache: OnceCell::new(),
90            inspection_cache: OnceCell::new(),
91        });
92        self.values.borrow_mut().insert(id, Rc::clone(&value));
93        ExternalPayloadLease {
94            release: Rc::new(ExternalPayloadReleaseGuard {
95                id,
96                values: Rc::downgrade(&self.values),
97            }),
98            value,
99        }
100    }
101
102    pub(crate) fn view(&self, lease: &ExternalPayloadLease) -> ExternalPayloadView<Payload> {
103        let value = Rc::clone(&self.values.borrow()[&lease.id()]);
104        ExternalPayloadView { value }
105    }
106}
107
108impl ExternalPayloadLease {
109    pub(crate) fn id(&self) -> u64 {
110        self.value.id()
111    }
112
113    pub(crate) fn source_hash(&self, context: &HostExternalHashing<'_>) -> u64 {
114        self.value.source_hash(context)
115    }
116
117    pub(crate) fn inspection(&self, context: &HostExternalInspection<'_>) -> &EcoString {
118        self.value.inspection(context)
119    }
120
121    pub(crate) fn source_equal(&self, context: &HostExternalEquality<'_>, other: &Self) -> bool {
122        self.value.source_equal(context, other)
123    }
124}
125
126impl Clone for ExternalPayloadLease {
127    fn clone(&self) -> Self {
128        Self {
129            release: Rc::clone(&self.release),
130            value: Rc::clone(&self.value),
131        }
132    }
133}
134
135impl<Payload> Deref for ExternalPayloadView<Payload> {
136    type Target = Payload;
137
138    fn deref(&self) -> &Self::Target {
139        &self.value.value
140    }
141}
142
143impl<Payload> ExternalPayload for StoredExternalPayload<Payload>
144where
145    Payload: 'static,
146{
147    fn id(&self) -> u64 {
148        self.id
149    }
150
151    fn source_hash(&self, context: &HostExternalHashing<'_>) -> u64 {
152        *self
153            .source_hash_cache
154            .get_or_init(|| (self.source_hash)(context, &self.value))
155    }
156
157    fn inspection<'payload>(
158        &'payload self,
159        context: &HostExternalInspection<'_>,
160    ) -> &'payload EcoString {
161        self.inspection_cache
162            .get_or_init(|| (self.inspect)(context, &self.value))
163    }
164
165    fn source_equal(
166        &self,
167        context: &HostExternalEquality<'_>,
168        other: &ExternalPayloadLease,
169    ) -> bool {
170        if self.id == other.id() {
171            return (self.source_equal)(context, &self.value, &self.value);
172        }
173        let Some(values) = self.values.upgrade() else {
174            return false;
175        };
176        values
177            .borrow()
178            .get(&other.id())
179            .is_some_and(|other| (self.source_equal)(context, &self.value, &other.value))
180    }
181}
182
183impl<Payload> ExternalPayloadRelease for ExternalPayloadReleaseGuard<Payload> {}
184
185impl<Payload> Drop for ExternalPayloadReleaseGuard<Payload> {
186    fn drop(&mut self) {
187        if let Some(values) = self.values.upgrade() {
188            values.borrow_mut().remove(&self.id);
189        }
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::HostExternalStore;
196    use crate::host::{HostExternalHashing, HostExternalInspection};
197    use ecow::EcoString;
198    use std::cell::Cell;
199    use std::rc::Rc;
200
201    struct Payload {
202        value: usize,
203        drops: Rc<Cell<usize>>,
204        hashes: Rc<Cell<usize>>,
205        inspections: Rc<Cell<usize>>,
206    }
207
208    impl Drop for Payload {
209        fn drop(&mut self) {
210            self.drops.set(self.drops.get() + 1);
211        }
212    }
213
214    fn equal(_: &crate::host::HostExternalEquality<'_>, left: &Payload, right: &Payload) -> bool {
215        left.value == right.value
216    }
217
218    fn source_hash(_: &HostExternalHashing<'_>, value: &Payload) -> u64 {
219        value.hashes.set(value.hashes.get() + 1);
220        value.value as u64
221    }
222
223    fn inspect(context: &HostExternalInspection<'_>, value: &Payload) -> EcoString {
224        value.inspections.set(value.inspections.get() + 1);
225        let stored = crate::host::HostStoredValue::<num_bigint::BigInt>::new(
226            crate::runtime::StoredRuntimeValue::test_int(value.value.into()),
227        );
228        format!("Payload({})", context.inspect_stored_value(&stored)).into()
229    }
230
231    fn payload(value: usize, drops: &Rc<Cell<usize>>) -> Payload {
232        Payload {
233            value,
234            drops: Rc::clone(drops),
235            hashes: Rc::new(Cell::new(0)),
236            inspections: Rc::new(Cell::new(0)),
237        }
238    }
239
240    #[test]
241    fn lease_controls_typed_index_and_payload_lifetime() {
242        let drops = Rc::new(Cell::new(0));
243        let store = HostExternalStore::default();
244        let lease = store.insert(payload(7, &drops), equal, source_hash, inspect);
245        let clone = lease.clone();
246
247        assert_eq!(store.values.borrow().len(), 1);
248        assert_eq!((*store.view(&lease)).value, 7);
249        drop(lease);
250        assert_eq!(store.values.borrow().len(), 1);
251        assert_eq!(drops.get(), 0);
252
253        drop(clone);
254        assert!(store.values.borrow().is_empty());
255        assert_eq!(drops.get(), 1);
256    }
257
258    #[test]
259    fn escaped_lease_remains_self_contained_after_store_drop() {
260        let drops = Rc::new(Cell::new(0));
261        let store = HostExternalStore::default();
262        let first_payload = payload(7, &drops);
263        let first_hashes = Rc::clone(&first_payload.hashes);
264        let first_inspections = Rc::clone(&first_payload.inspections);
265        let first = store.insert(first_payload, equal, source_hash, inspect);
266        let second = store.insert(payload(7, &drops), equal, source_hash, inspect);
267
268        let stored_equal =
269            |_: &crate::runtime::StoredRuntimeValue, _: &crate::runtime::StoredRuntimeValue| false;
270        let equality = crate::host::HostExternalEquality::new(&stored_equal);
271        let stored_hash = |_: &crate::runtime::StoredRuntimeValue| 17;
272        let stored_inspect = |_: &crate::runtime::StoredRuntimeValue| EcoString::from("7");
273        let hashing = HostExternalHashing::new(&stored_hash);
274        let inspection = HostExternalInspection::new(&stored_inspect);
275
276        assert!(first.source_equal(&equality, &first));
277        assert!(first.source_equal(&equality, &second));
278        assert_eq!(first_hashes.get(), 0);
279        assert_eq!(first_inspections.get(), 0);
280        assert_eq!(first.source_hash(&hashing), 7);
281        assert_eq!(first.source_hash(&hashing), 7);
282        assert_eq!(first.inspection(&inspection), "Payload(7)");
283        assert_eq!(first.inspection(&inspection), "Payload(7)");
284        assert_eq!(first_hashes.get(), 1);
285        assert_eq!(first_inspections.get(), 1);
286
287        drop(store);
288
289        assert!(!first.source_equal(&equality, &second));
290        assert_eq!(first.inspection(&inspection), "Payload(7)");
291        drop(first);
292        drop(second);
293        assert_eq!(drops.get(), 2);
294    }
295
296    #[test]
297    fn source_equality_does_not_cross_typed_store_instances() {
298        let drops = Rc::new(Cell::new(0));
299        let first_store = HostExternalStore::default();
300        let second_store = HostExternalStore::default();
301        let first = first_store.insert(payload(7, &drops), equal, source_hash, inspect);
302        let second = second_store.insert(payload(7, &drops), equal, source_hash, inspect);
303
304        let stored_equal =
305            |_: &crate::runtime::StoredRuntimeValue, _: &crate::runtime::StoredRuntimeValue| false;
306        let equality = crate::host::HostExternalEquality::new(&stored_equal);
307
308        assert!(!first.source_equal(&equality, &second));
309    }
310
311    #[test]
312    fn source_equality_does_not_assume_opaque_identity_is_reflexive() {
313        fn source_hash(
314            context: &HostExternalHashing<'_>,
315            value: &crate::host::HostStoredValue<num_bigint::BigInt>,
316        ) -> u64 {
317            context.stored_value_hash(value)
318        }
319
320        fn inspect(
321            context: &HostExternalInspection<'_>,
322            value: &crate::host::HostStoredValue<num_bigint::BigInt>,
323        ) -> EcoString {
324            context.inspect_stored_value(value)
325        }
326
327        let store = HostExternalStore::default();
328        let lease = store.insert(
329            crate::host::HostStoredValue::<num_bigint::BigInt>::new(
330                crate::runtime::StoredRuntimeValue::test_int(7.into()),
331            ),
332            |context, left, right| context.stored_values_equal(left, right),
333            source_hash,
334            inspect,
335        );
336        let stored_equal =
337            |_: &crate::runtime::StoredRuntimeValue, _: &crate::runtime::StoredRuntimeValue| false;
338        let equality = crate::host::HostExternalEquality::new(&stored_equal);
339        let stored_hash = |_: &crate::runtime::StoredRuntimeValue| 7;
340        let stored_inspect = |_: &crate::runtime::StoredRuntimeValue| EcoString::from("7");
341
342        assert!(!lease.source_equal(&equality, &lease));
343        assert_eq!(
344            lease.source_hash(&HostExternalHashing::new(&stored_hash)),
345            7
346        );
347        assert_eq!(
348            lease.inspection(&HostExternalInspection::new(&stored_inspect)),
349            "7",
350        );
351    }
352}