javm_cap/cache.rs
1//! `CacheDirectory<S>` — two-tier cap store.
2//!
3//! - **`blobs: HashMap<CapHash, Arc<C>>`** — content-addressed
4//! immutable resident caps. Pure cache: the host populates it; the
5//! kernel reads. If a lookup misses, the host hasn't published the cap yet.
6//!
7//! - **`instances: HashMap<u64, (CapRef, Arc<C>)>`** — identity-keyed
8//! mutable working state. The stored `CapRef` is the directory's
9//! self-reference; its `Arc::strong_count` is the number of live
10//! external holders + 1.
11//!
12//! Two callers exist: the Nub local backend (host's `Global`) and the
13//! Nub Hyperlight backend (guest's `Global` via talc). Both wrap
14//! `CacheDirectory<S>` in their own static / field; the directory's
15//! interior is `spin::Mutex`-protected so every public method takes
16//! `&self`.
17//!
18//! ## Cow + lazy promote
19//!
20//! Promotion (blob → instance) is a cheap `Arc::clone`:
21//!
22//! ```ignore
23//! let arc = blobs[&hash].clone(); // RC bump; no Cap copy.
24//! let id = self.next_ref;
25//! self.next_ref += 1;
26//! let capref = CapRef::new(id);
27//! instances.insert(id, (capref.clone(), arc)); // RC bump on capref.
28//! capref
29//! ```
30//!
31//! Mutation uses `Arc::make_mut`:
32//!
33//! ```ignore
34//! let mut arc = cache.get_instance(&capref).unwrap();
35//! let cap_mut = Arc::make_mut(&mut arc); // clones iff strong > 1.
36//! // ... mutate cap_mut ...
37//! cache.set_instance(&capref, arc);
38//! ```
39//!
40//! `Arc::make_mut` subsumes the legacy "sole-owner move-promote vs
41//! shared shallow-clone" branch — same decision, in fewer lines.
42//!
43//! ## GC sweep
44//!
45//! `sweep_instances` reclaims entries whose stored `CapRef.strong_count`
46//! is 1 (i.e., the directory is the sole holder). Removal drops the
47//! entry's `Arc<C>`; if that was the last strong ref to the resident cap, the
48//! Cap drops. Caps no longer hold `Ref(CapRef)` slot targets (a cnode
49//! slot is a `Hash` or an inline `Owned` cap), so a drop never cascades
50//! into other instance entries — the sweep is a single self-contained
51//! pass per orphan. The loop is retained as a cheap fixed-point guard.
52//!
53//! The instances tier is currently **dormant**: the recompiler keeps
54//! sub-VMs inline as `Owned` caps and never publishes a `CapRef`. The
55//! tier (and `CapRef`) survive as the host-side key for the future
56//! deferred-persist path.
57//!
58//! ## blob retention
59//!
60//! V0 blobs accumulate; the host pre-publishes every cap the invocation
61//! needs and lookups never miss. Future design: missing-blob lookups
62//! pause the kernel and ask the host to publish. Until that lands,
63//! `get_blob` returning `None` is treated as a hard failure by the
64//! caller.
65
66use alloc::boxed::Box;
67use alloc::sync::Arc;
68use alloc::vec::Vec;
69use core::hash::BuildHasher;
70
71use hashbrown::{DefaultHashBuilder, HashMap};
72use spin::Mutex;
73
74use super::cap::image::ImageConvertError;
75use super::cap::{Cap, CapHash};
76
77/// Cache-local lifetime handle to a working `Cap::Instance` in
78/// `CacheDirectory.instances`.
79///
80/// `Clone` bumps an inner `Arc` refcount; `Drop` decrements it. The
81/// directory owns one `CapRef` per live entry alongside the data; when
82/// external holders all drop their clones, [`CacheDirectory::sweep_instances`]
83/// finds entries whose stored handle has `strong_count == 1` and removes
84/// them. No callback-on-drop, no deadlock discipline.
85///
86/// Two separate `CacheDirectory` instances produce independent id
87/// namespaces — `CapRef`s must not cross caches.
88///
89/// The constructor is module-private: every handle in production traces
90/// back to [`CacheDirectory::put_instance`].
91#[derive(Clone, Debug)]
92pub struct CapRef {
93 id: u64,
94 /// Refcount tracker. The Arc's strong count is the number of
95 /// live `CapRef` holders for this id (including the directory's
96 /// own self-reference).
97 rc: Arc<()>,
98}
99
100impl CapRef {
101 fn new(id: u64) -> Self {
102 Self {
103 id,
104 rc: Arc::new(()),
105 }
106 }
107
108 /// The id this handle resolves to inside `CacheDirectory.instances`.
109 pub fn id(&self) -> u64 {
110 self.id
111 }
112
113 /// Number of live `CapRef` clones for this id, including the
114 /// directory's own self-reference. `sweep_instances` reclaims
115 /// entries whose stored handle has `strong_count == 1`.
116 pub fn strong_count(&self) -> usize {
117 Arc::strong_count(&self.rc)
118 }
119}
120
121impl PartialEq for CapRef {
122 fn eq(&self, other: &Self) -> bool {
123 self.id == other.id
124 }
125}
126impl Eq for CapRef {}
127
128impl core::hash::Hash for CapRef {
129 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
130 self.id.hash(state)
131 }
132}
133
134mod sealed {
135 pub trait Sealed {}
136 impl Sealed for alloc::boxed::Box<crate::cap::Cap> {}
137}
138
139/// Marker for `CapHashOrRef::Owned` payloads that participate in the
140/// content-addressed **wire** form. Implemented only for `Box<Cap>` (the
141/// default payload).
142///
143/// It is a deliberately *leaf* marker — implemented directly for
144/// `Box<Cap>` with no supertrait that recurses into `Cap: Archive` — so
145/// gating the wire impls (`HashTreeRoot` / `Encode` / `Decode` / rkyv
146/// `Archive` / `Serialize`) of `CapHashOrRef<O>` on `O: WireOwned`
147/// resolves by a single lookup, exactly as the original non-generic
148/// impls did (which required nothing of the inline `Box<Cap>`). Gating on
149/// `O: rkyv::Archive` instead would re-introduce the cyclic
150/// `Box<Cap>: Archive → Cap: Archive` bound and overflow the solver.
151///
152/// Engine-private cache payloads (e.g. `Box<CachedCap>`) deliberately do
153/// **not** implement it, so a cnode carrying one has no wire impl and
154/// cannot be hashed or serialised — a compile error, strictly stronger
155/// than the runtime `Owned` panic.
156///
157/// Sealed: only `javm-cap` implements it.
158pub trait WireOwned: sealed::Sealed {}
159impl WireOwned for Box<Cap> {}
160
161/// Slot/field reference: a content-addressed blob in `cache.blobs`
162/// (`Hash`), or a single-owner cap held **inline** by the running kernel
163/// frame (`Owned`).
164///
165/// **`Owned(Box<Cap>)`** is the zero-copy ownership form: a cap the
166/// kernel frame owns outright and moves between cnode slots (and between
167/// frames, at HALT) with no cache round-trip and no data copy — the move
168/// is a `Box` pointer swap. It is **runtime-only**: it never crosses the
169/// wire and is never hashed. The recompiler mints it on `derive_spawn`
170/// and moves it through `host_call`; it never `settle`s one (the
171/// host-side `settle` arm folds it into a blob for the deferred persist
172/// path).
173///
174/// **SSZ note**: `CapHashOrRef`'s `HashTreeRoot` impl is hand-rolled,
175/// not derived. The pass-through semantics — `Hash(h)` hashes to `h` —
176/// let a freshly-published cap substitute for a content reference without
177/// changing the hash of any cap that holds it. The `Owned` arm panics:
178/// callers must `settle` a cap graph before hashing it. `Encode` mirrors
179/// `HashTreeRoot` (panic on the runtime-only arm); `Decode` only ever
180/// produces `Hash` (the wire carries selector 0).
181///
182/// **Generic over the owned payload `O`** (default `Box<Cap>`). The wire
183/// form (the cnode inside a serialised `Cap`) is always
184/// `CapHashOrRef<Box<Cap>>`, so `Cap` and its hash are unaffected. An
185/// engine may instantiate a *running frame's* cnode with a richer,
186/// deliberately non-wire payload (e.g. `Box<CachedCap>`); the
187/// serialisation impls below are gated on `O: rkyv::Archive`, so such a
188/// payload makes the cnode non-hashable and non-serialisable at **compile
189/// time** (a strictly stronger guarantee than the runtime `Owned` panic).
190///
191/// **Not `Copy`**: the `Owned(O)` arm carries the (usually heap-allocated)
192/// payload, so the enum is `Clone`-only (when `O: Clone`).
193///
194/// **`PartialEq`/`Eq`/`Hash` are hand-written**: a `Box<Cap>` payload
195/// blocks the derive (`Cap` is not `Eq`/`Hash`). The `Hash` arm
196/// compares/hashes its 32-byte digest by value; the `Owned` arm uses
197/// pointer identity of the inline payload (sound — `Owned` is single-owner
198/// runtime state, never content-compared — and `O`-agnostic, so it needs no
199/// bound).
200#[derive(Clone, Debug)]
201pub enum CapHashOrRef<O = Box<Cap>> {
202 Hash(CapHash),
203 Owned(O),
204}
205
206impl<O> PartialEq for CapHashOrRef<O> {
207 fn eq(&self, other: &Self) -> bool {
208 match (self, other) {
209 (CapHashOrRef::Hash(a), CapHashOrRef::Hash(b)) => a == b,
210 // Owned is single-owner and never content-compared; pointer
211 // identity of the inline payload is reflexive (Eq-sound) and
212 // consistent with the pointer-keyed `Hash` impl below.
213 (CapHashOrRef::Owned(a), CapHashOrRef::Owned(b)) => core::ptr::eq(a, b),
214 _ => false,
215 }
216 }
217}
218impl<O> Eq for CapHashOrRef<O> {}
219
220impl<O> core::hash::Hash for CapHashOrRef<O> {
221 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
222 match self {
223 CapHashOrRef::Hash(h) => {
224 0u8.hash(state);
225 h.hash(state);
226 }
227 CapHashOrRef::Owned(o) => {
228 1u8.hash(state);
229 (o as *const O).hash(state);
230 }
231 }
232 }
233}
234
235// Gated on `O: WireOwned` — the wire-payload marker. `Box<Cap>` (the
236// content-addressed default) implements it; a non-wire payload such as
237// `Box<CachedCap>` does not, so a cache-carrying cnode has no `HashTreeRoot`
238// impl and cannot be content-hashed (a compile error, not a runtime panic).
239impl<O: WireOwned> ssz::HashTreeRoot for CapHashOrRef<O> {
240 fn hash_tree_root<D: ::ssz::digest::Digest<OutputSize = ::ssz::digest::typenum::U32>>(
241 &self,
242 ) -> [u8; 32] {
243 match self {
244 CapHashOrRef::Hash(h) => *h,
245 CapHashOrRef::Owned(_) => {
246 panic!("cap_hash: in-flight Owned cap in cap graph; settle first")
247 }
248 }
249 }
250}
251
252impl<O: WireOwned> ssz::Encode for CapHashOrRef<O> {
253 fn is_ssz_fixed_len() -> bool {
254 false
255 }
256 fn ssz_fixed_len() -> usize {
257 ssz::BYTES_PER_LENGTH_OFFSET
258 }
259 fn ssz_bytes_len(&self) -> usize {
260 match self {
261 CapHashOrRef::Hash(_) => 1 + 32,
262 // Owned must be settled before serialisation; matches the
263 // `HashTreeRoot` contract above. Reached only by buggy code.
264 CapHashOrRef::Owned(_) => {
265 panic!("ssz_bytes_len: in-flight Owned cap in cap graph; settle first")
266 }
267 }
268 }
269 fn ssz_append(&self, buf: &mut Vec<u8>) {
270 match self {
271 CapHashOrRef::Hash(h) => {
272 buf.push(0);
273 buf.extend_from_slice(h);
274 }
275 CapHashOrRef::Owned(_) => {
276 panic!("ssz_append: in-flight Owned cap in cap graph; settle first")
277 }
278 }
279 }
280}
281
282impl<O: WireOwned> ssz::Decode for CapHashOrRef<O> {
283 fn is_ssz_fixed_len() -> bool {
284 false
285 }
286 fn ssz_fixed_len() -> usize {
287 ssz::BYTES_PER_LENGTH_OFFSET
288 }
289 fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
290 if bytes.is_empty() {
291 return Err(ssz::DecodeError::UnexpectedEof {
292 expected: 1,
293 actual: 0,
294 });
295 }
296 match bytes[0] {
297 0 => {
298 if bytes.len() != 1 + 32 {
299 return Err(ssz::DecodeError::UnexpectedEof {
300 expected: 1 + 32,
301 actual: bytes.len(),
302 });
303 }
304 let mut h = [0u8; 32];
305 h.copy_from_slice(&bytes[1..1 + 32]);
306 Ok(CapHashOrRef::Hash(h))
307 }
308 // Selector 0 (`Hash`) is the only wire form; `Owned` is
309 // runtime-only and never serialises, so any other selector is
310 // invalid wire bytes.
311 v => Err(ssz::DecodeError::InvalidSelector(v)),
312 }
313 }
314}
315
316// --- rkyv: hand-rolled. ---
317//
318// `CapHashOrRef`'s archived form is the same as `CapHash`'s — a plain
319// 32-byte digest. Serialize errors out on `Ref` (cache-local lifetime
320// handles have no wire form); resolve panics defensively for the path
321// where someone hand-built a Resolver and called resolve without going
322// through Serialize. Deserialize always produces `Hash` because the
323// archived form structurally can't carry a `Ref`.
324
325/// Error returned by `<CapHashOrRef as rkyv::Serialize<_>>::serialize`
326/// when the cap graph still holds a runtime-only target — a
327/// [`CapHashOrRef::Owned`]. Callers must `settle` (or otherwise rewrite
328/// the target to a hash) before rkyv-encoding the cap.
329#[derive(Debug)]
330pub struct CapHasRefError;
331
332impl core::fmt::Display for CapHasRefError {
333 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
334 f.write_str(
335 "cap holds a runtime-only CapHashOrRef::Owned target; settle before rkyv encode",
336 )
337 }
338}
339
340impl core::error::Error for CapHasRefError {}
341
342impl<O: WireOwned> rkyv::Archive for CapHashOrRef<O> {
343 type Archived = <CapHash as rkyv::Archive>::Archived;
344 type Resolver = <CapHash as rkyv::Archive>::Resolver;
345
346 fn resolve(&self, resolver: Self::Resolver, out: rkyv::Place<Self::Archived>) {
347 match self {
348 CapHashOrRef::Hash(h) => <CapHash as rkyv::Archive>::resolve(h, resolver, out),
349 // Unreachable if Serialize was called first (it errors on the
350 // runtime-only arm). Defensive panic for the "hand-built
351 // resolver" path.
352 CapHashOrRef::Owned(_) => {
353 panic!(
354 "CapHashOrRef::Owned in archive resolve; Serialize should have rejected first"
355 )
356 }
357 }
358 }
359}
360
361impl<O, S> rkyv::Serialize<S> for CapHashOrRef<O>
362where
363 O: WireOwned,
364 S: rkyv::rancor::Fallible + ?Sized,
365 <S as rkyv::rancor::Fallible>::Error: rkyv::rancor::Source,
366 CapHash: rkyv::Serialize<S>,
367{
368 fn serialize(
369 &self,
370 serializer: &mut S,
371 ) -> Result<Self::Resolver, <S as rkyv::rancor::Fallible>::Error> {
372 match self {
373 CapHashOrRef::Hash(h) => <CapHash as rkyv::Serialize<S>>::serialize(h, serializer),
374 CapHashOrRef::Owned(_) => Err(rkyv::rancor::Source::new(CapHasRefError)),
375 }
376 }
377}
378
379// Use the concrete archived type `[u8; 32]` rather than
380// `<CapHash as Archive>::Archived` to avoid a coherence-checker false
381// conflict with rkyv's blanket `Deserialize for With<F, W>` impl
382// (associated-type opacity).
383impl<O, D> rkyv::Deserialize<CapHashOrRef<O>, D> for [u8; 32]
384where
385 D: rkyv::rancor::Fallible + ?Sized,
386{
387 fn deserialize(
388 &self,
389 _deserializer: &mut D,
390 ) -> Result<CapHashOrRef<O>, <D as rkyv::rancor::Fallible>::Error> {
391 Ok(CapHashOrRef::Hash(*self))
392 }
393}
394
395#[derive(Debug, thiserror::Error)]
396pub enum CacheError {
397 #[error("blob not found for hash")]
398 BlobMissing,
399 #[error("instance not found for ref {0}")]
400 InstanceMissing(u64),
401 #[error("image conversion failed: {0}")]
402 ImageConvertFailed(#[from] ImageConvertError),
403 #[error("paged data: page length mismatch (expected={expected}, got={got})")]
404 PageSizeMismatch { expected: u32, got: usize },
405 #[error("cnode slot index out of range")]
406 SlotOutOfRange,
407}
408
409/// Payload stored by a [`CacheDirectory`]. The public/wire directory stores
410/// plain [`Cap`]; engines may store a resident wrapper that carries derived
411/// runtime caches while still exposing the underlying wire cap for hashing and
412/// inspection.
413pub trait ResidentCap {
414 /// Wrap a public wire cap for resident storage.
415 fn from_cap(cap: Cap) -> Self;
416 /// Borrow the public wire cap.
417 fn as_cap(&self) -> ⋒
418 /// Consume the resident payload back into its wire cap.
419 fn into_cap(self) -> Cap;
420}
421
422impl ResidentCap for Cap {
423 fn from_cap(cap: Cap) -> Self {
424 cap
425 }
426
427 fn as_cap(&self) -> &Cap {
428 self
429 }
430
431 fn into_cap(self) -> Cap {
432 self
433 }
434}
435
436pub struct CacheDirectory<S = DefaultHashBuilder, C = Cap> {
437 inner: Mutex<DirectoryInner<S, C>>,
438}
439
440struct DirectoryInner<S, C> {
441 blobs: HashMap<CapHash, Arc<C>, S>,
442 instances: HashMap<u64, (CapRef, Arc<C>), S>,
443 next_ref: u64,
444}
445
446impl CacheDirectory<DefaultHashBuilder, Cap> {
447 /// Construct an empty cache using the default per-process-randomized
448 /// hasher.
449 pub fn new() -> Self {
450 Self::with_hasher(DefaultHashBuilder::default())
451 }
452}
453
454impl Default for CacheDirectory<DefaultHashBuilder, Cap> {
455 fn default() -> Self {
456 Self::new()
457 }
458}
459
460impl<S: BuildHasher, C> CacheDirectory<S, C> {
461 /// Construct an empty cache with an explicit hasher.
462 pub fn with_hasher(hasher: S) -> Self
463 where
464 S: Clone,
465 {
466 Self {
467 inner: Mutex::new(DirectoryInner {
468 blobs: HashMap::with_hasher(hasher.clone()),
469 instances: HashMap::with_hasher(hasher),
470 // CapRef id 0 is reserved; ids start at 1.
471 next_ref: 1,
472 }),
473 }
474 }
475}
476
477impl<S, C> CacheDirectory<S, C> {
478 /// `const fn` constructor for static initialisation. Used by the
479 /// guest's `state_cache::CACHE` static. Takes both hashers
480 /// separately because `const fn` can't call `S::clone()` and not
481 /// every `BuildHasher` (notably `foldhash::fast::FixedState`)
482 /// implements `Copy`. Callers normally pass two identically-seeded
483 /// instances so both maps hash to the same buckets.
484 pub const fn new_const(blobs_hasher: S, instances_hasher: S) -> Self {
485 Self {
486 inner: Mutex::new(DirectoryInner {
487 blobs: HashMap::with_hasher(blobs_hasher),
488 instances: HashMap::with_hasher(instances_hasher),
489 next_ref: 1,
490 }),
491 }
492 }
493}
494
495impl<S: BuildHasher, C> CacheDirectory<S, C> {
496 /// Number of blob entries.
497 pub fn blob_count(&self) -> usize {
498 self.inner.lock().blobs.len()
499 }
500 /// Number of instance entries (live or unswept).
501 pub fn instance_count(&self) -> usize {
502 self.inner.lock().instances.len()
503 }
504
505 /// Whether the blobs tier holds an entry under `hash`.
506 pub fn contains_blob(&self, hash: &CapHash) -> bool {
507 self.inner.lock().blobs.contains_key(hash)
508 }
509
510 /// Get an `Arc::clone` of the blob cap at `hash`, or `None` if
511 /// absent.
512 pub fn get_blob(&self, hash: &CapHash) -> Option<Arc<C>> {
513 self.inner.lock().blobs.get(hash).cloned()
514 }
515
516 /// Get an `Arc::clone` of the instance cap at `capref`, or `None`
517 /// if absent.
518 pub fn get_instance(&self, capref: &CapRef) -> Option<Arc<C>> {
519 self.inner
520 .lock()
521 .instances
522 .get(&capref.id())
523 .map(|(_, arc)| arc.clone())
524 }
525
526 /// Snapshot the blob tier into a `Vec<(CapHash, Arc<C>)>`. Order
527 /// is unspecified (HashMap iteration); callers that need
528 /// deterministic order (state-root computations) sort by hash.
529 pub fn iter_blobs(&self) -> Vec<(CapHash, Arc<C>)> {
530 self.inner
531 .lock()
532 .blobs
533 .iter()
534 .map(|(h, arc)| (*h, arc.clone()))
535 .collect()
536 }
537
538 /// Polymorphic lookup that dispatches on the `CapHashOrRef` arm.
539 /// Returns an `Arc::clone` of the matching cap.
540 pub fn get(&self, key: CapHashOrRef) -> Option<Arc<C>> {
541 match key {
542 CapHashOrRef::Hash(h) => self.get_blob(&h),
543 // `Owned` lives inline on the kernel frame, not in the
544 // directory; the holder dereferences the `Box` directly.
545 CapHashOrRef::Owned(_) => None,
546 }
547 }
548
549 /// Replace the instance at `capref` with a fresh `Arc<C>`. The
550 /// old `Arc<C>` drops outside the lock (so any cascading
551 /// `Cap::drop → CapRef::drop` chain doesn't try to re-enter the
552 /// directory while we hold the guard).
553 pub fn set_instance(&self, capref: &CapRef, new_arc: Arc<C>) -> Result<(), CacheError> {
554 let _old = {
555 let mut g = self.inner.lock();
556 let entry = g
557 .instances
558 .get_mut(&capref.id())
559 .ok_or_else(|| CacheError::InstanceMissing(capref.id()))?;
560 // Swap in the new Arc, keeping the existing self-ref CapRef.
561 // The old Arc<C> is returned and dropped after the lock guard.
562 core::mem::replace(&mut entry.1, new_arc)
563 };
564 Ok(())
565 }
566
567 /// Hash + insert into blobs. Idempotent: re-puts of identical
568 /// content are a no-op. Returns the content hash.
569 pub fn put_cap(&self, cap: &Cap) -> Result<CapHash, CacheError>
570 where
571 C: ResidentCap,
572 {
573 let hash = cap.cap_hash();
574 self.put_cap_with_hash(hash, cap)?;
575 Ok(hash)
576 }
577
578 /// Pre-hashed insert. Debug-asserts the claimed hash matches the
579 /// cap; release trusts the caller (the SSZ merkleize is the hot
580 /// cost on the publish path).
581 pub fn put_cap_with_hash(&self, hash: CapHash, cap: &Cap) -> Result<(), CacheError>
582 where
583 C: ResidentCap,
584 {
585 debug_assert_eq!(
586 cap.cap_hash(),
587 hash,
588 "put_cap_with_hash: claimed hash does not match cap content",
589 );
590 let mut g = self.inner.lock();
591 g.blobs
592 .entry(hash)
593 .or_insert_with(|| Arc::new(C::from_cap(cap.clone())));
594 Ok(())
595 }
596
597 /// Insert a freshly-built `Cap` as a new instance entry. Returns
598 /// the `CapRef` handle. The directory keeps its own clone of the
599 /// returned handle internally as the entry's self-reference; when
600 /// all external clones drop, `sweep_instances` will reclaim the
601 /// entry.
602 pub fn put_instance(&self, cap: Cap) -> CapRef
603 where
604 C: ResidentCap,
605 {
606 self.put_instance_arc(Arc::new(C::from_cap(cap)))
607 }
608
609 /// `put_instance` variant that takes a pre-built `Arc<C>`. Used
610 /// internally by [`Self::promote_blob_to_instance`] to share the
611 /// blob's Arc rather than deep-copying.
612 fn put_instance_arc(&self, arc: Arc<C>) -> CapRef {
613 let mut g = self.inner.lock();
614 let id = g.next_ref;
615 g.next_ref = g.next_ref.checked_add(1).expect("CapRef space exhausted");
616 let capref = CapRef::new(id);
617 g.instances.insert(id, (capref.clone(), arc));
618 capref
619 }
620
621 /// Lazily promote a blob to a fresh instance entry. The blob and
622 /// the new instance entry share the same `Arc<C>` (no resident cap
623 /// data deep-copy); the next `Arc::make_mut` call on either side
624 /// clones-on-write if both still hold the Arc.
625 ///
626 /// Returns `None` if the blob isn't published.
627 pub fn promote_blob_to_instance(&self, hash: &CapHash) -> Option<CapRef> {
628 let arc = self.get_blob(hash)?;
629 Some(self.put_instance_arc(arc))
630 }
631
632 /// **GC pass.** Walk the instances tier and remove every entry
633 /// whose stored `CapRef` has `strong_count == 1` (the directory is
634 /// the sole holder — no external `CapRef` clone exists). Loop
635 /// until a pass finds nothing to remove, so cascading drops can
636 /// orphan more entries which then get reclaimed in the next
637 /// iteration.
638 ///
639 /// Cycles are structurally impossible (data-flow principle), so
640 /// the loop is guaranteed to terminate.
641 pub fn sweep_instances(&self) {
642 loop {
643 let dead: Vec<u64> = {
644 let g = self.inner.lock();
645 g.instances
646 .iter()
647 .filter(|(_, (sr, _))| sr.strong_count() == 1)
648 .map(|(k, _)| *k)
649 .collect()
650 };
651 if dead.is_empty() {
652 break;
653 }
654 for id in dead {
655 let _removed = {
656 let mut g = self.inner.lock();
657 g.instances.remove(&id)
658 };
659 // _removed drops here, outside the lock. If its
660 // Arc<C> was the last strong ref, Cap::drop runs and
661 // cascades: nested CapRef::drop calls decrement other
662 // entries' refcounts. Those entries get reclaimed on
663 // the next pass.
664 }
665 }
666 }
667
668 /// Settle an inline `Owned` cap to a content hash: recursively rewrite
669 /// its nested `Owned` slot targets to `Hash`, flush a dirty `Data` cap's
670 /// CoW overlay, content-address it into `blobs`, and return the hash.
671 ///
672 /// For `Hash`-keyed input, returns it unchanged.
673 pub fn settle(&self, key: CapHashOrRef) -> Result<CapHash, CacheError>
674 where
675 C: ResidentCap,
676 {
677 match key {
678 CapHashOrRef::Hash(h) => Ok(h),
679 CapHashOrRef::Owned(b) => self.settle_owned(*b),
680 }
681 }
682
683 /// Settle an inline `Owned` cap: recursively rewrite its nested slot
684 /// targets (`Owned`) to `Hash`, flush a `Data` cap's CoW overlay so it
685 /// is hashable, then content-address it into `blobs` and return the
686 /// hash.
687 ///
688 /// The recompiler never calls this — it *moves* `Owned` caps and
689 /// drops them at frame pop. It exists for the host-side deferred
690 /// persist path (turn a finished frame's owned cap into a blob).
691 fn settle_owned(&self, mut cap: Cap) -> Result<CapHash, CacheError>
692 where
693 C: ResidentCap,
694 {
695 // 1. Settle every nested slot target to a Hash, in place.
696 self.settle_targets_in(&mut cap)?;
697 // 2. Hashing requires an empty overlay; fold a dirty Data cap.
698 if let Cap::Data(d) = &cap
699 && !d.overlay.is_empty()
700 {
701 cap = Cap::Data(d.flush());
702 }
703 // 3. Content-address into blobs.
704 let hash = cap.cap_hash();
705 self.put_cap_with_hash(hash, &cap)?;
706 Ok(hash)
707 }
708
709 /// Rewrite every direct slot target of `cap` (CNode slots, Instance
710 /// `root_cnode`) from a runtime-only `Owned` to its settled `Hash`,
711 /// recursing into nested `Owned` caps.
712 fn settle_targets_in(&self, cap: &mut Cap) -> Result<(), CacheError>
713 where
714 C: ResidentCap,
715 {
716 match cap {
717 Cap::CNode(cn) => {
718 for (_, mo) in cn.slots.iter_mut() {
719 if let ssz::MissingOr::Materialized(t) = mo {
720 self.settle_target(t)?;
721 }
722 }
723 }
724 Cap::Instance(inst) => self.settle_target(&mut inst.root_cnode)?,
725 Cap::Data(_) | Cap::Image(_) => {}
726 }
727 Ok(())
728 }
729
730 /// Settle one slot target in place: `Hash` unchanged, `Owned`
731 /// recursively via [`Self::settle_owned`].
732 fn settle_target(&self, t: &mut CapHashOrRef) -> Result<(), CacheError>
733 where
734 C: ResidentCap,
735 {
736 match t {
737 CapHashOrRef::Hash(_) => Ok(()),
738 CapHashOrRef::Owned(_) => {
739 let CapHashOrRef::Owned(b) = core::mem::replace(t, CapHashOrRef::Hash([0u8; 32]))
740 else {
741 unreachable!("matched Owned above")
742 };
743 let h = self.settle_owned(*b)?;
744 *t = CapHashOrRef::Hash(h);
745 Ok(())
746 }
747 }
748 }
749}