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}