Skip to main content

javm/
frame.rs

1//! `MainFrame` and `BareFrame` views over the active Instance's
2//! cnode.
3//!
4//! Per v3 spec §3 and §22:
5//!
6//! - **MainFrame**: the active Instance's root cnode (256 slots).
7//!   Holds all caps accessible to apply — both user-mutable slots
8//!   and Image-declared pinned slots.
9//!
10//! - **BareFrame**: the read-only subset of the MainFrame consisting
11//!   of the Image's declared pinned slots. These are kernel-issued
12//!   caps (SetGasMeter, OOGMarker, ...) at chain init, or
13//!   Image-baked Data/Image references injected at set_image. The
14//!   kernel rejects mutations to these slots.
15//!
16//! Both are lightweight borrow views; the data lives on
17//! `InstanceEntry::{root_cnode, pinned_slots}` plus the `CacheDirectory` that
18//! resolves nested-cnode walks. The MGMT dispatcher (Stage 3.6) uses
19//! these views to:
20//! 1. Resolve a `SlotPath` to a cap target (CapHashOrRef).
21//! 2. Enforce pinned-slot read-only semantics on writes.
22//! 3. Expose pinned-cap content to host calls that need it (e.g.,
23//!    `host_yield` reads the Cap::Instance\[YieldCatcher\] from the
24//!    Image-declared `yield_marker_slot`).
25
26use javm_cap::{CNodeCap, CacheDirectory, Cap, CapHashOrRef, SlotIdx, SlotPath};
27
28use crate::error::VmError;
29
30/// Read-only view of an active Instance's MainFrame cnode.
31pub struct MainFrame<'a> {
32    cnode: &'a CNodeCap,
33    pinned: &'a [SlotIdx],
34    cache: &'a CacheDirectory,
35}
36
37impl<'a> MainFrame<'a> {
38    pub fn new(cnode: &'a CNodeCap, pinned: &'a [SlotIdx], cache: &'a CacheDirectory) -> Self {
39        Self {
40            cnode,
41            pinned,
42            cache,
43        }
44    }
45
46    /// Cnode size as `log2(slots)`.
47    pub fn size_log(&self) -> u8 {
48        self.cnode.size_log
49    }
50
51    /// True if `idx` is declared pinned by the Image.
52    pub fn is_pinned(&self, idx: SlotIdx) -> bool {
53        self.pinned.binary_search(&idx).is_ok()
54    }
55
56    /// Read a single root-cnode slot target.
57    pub fn get(&self, idx: SlotIdx) -> Option<CapHashOrRef> {
58        self.cnode.get(idx)
59    }
60
61    /// Resolve a `SlotPath` against this MainFrame. Walks nested
62    /// `Cap::CNode` slots via the cache; returns the cap target at
63    /// the path's terminal slot (or `None` if the slot is empty).
64    ///
65    /// Error: if any intermediate step fails to land on a
66    /// `Cap::CNode` or hits an empty slot, returns
67    /// `VmError::{SlotKindMismatch, SlotEmpty}`.
68    pub fn resolve(&self, path: &SlotPath) -> Result<Option<CapHashOrRef>, VmError> {
69        let prefix = path.prefix();
70        if prefix.is_empty() {
71            return Ok(self.cnode.get(path.target()));
72        }
73        // Walk intermediate cnodes by cloning. `cache.get` returns an
74        // owned `Arc<Cap>` rather than the old `&Cap`, so we can no
75        // longer chain borrows; cloning the CNodeCap (cheap for
76        // sparsely-populated tables) sidesteps the lifetime issue.
77        // SlotPath depth is bounded to MAX_SOURCE_DEPTH = 8.
78        let mut current = self.cnode.clone();
79        for step in prefix {
80            let target = current.get(*step).ok_or(VmError::SlotEmpty(step.get()))?;
81            let cap = self
82                .cache
83                .get(target)
84                .ok_or(VmError::SlotKindMismatch(step.get()))?;
85            match &*cap {
86                Cap::CNode(inner) => {
87                    current = inner.clone();
88                }
89                _ => return Err(VmError::SlotKindMismatch(step.get())),
90            }
91        }
92        Ok(current.get(path.target()))
93    }
94}
95
96/// Read-only view of just the pinned slots of an Instance's MainFrame.
97/// This is the "BareFrame" surface in §22: kernel-issued caps live
98/// here, addressed by `SlotIdx` matching the Image's pinning
99/// declarations.
100pub struct BareFrame<'a> {
101    main: MainFrame<'a>,
102}
103
104impl<'a> BareFrame<'a> {
105    pub fn new(cnode: &'a CNodeCap, pinned: &'a [SlotIdx], cache: &'a CacheDirectory) -> Self {
106        Self {
107            main: MainFrame::new(cnode, pinned, cache),
108        }
109    }
110
111    /// Read the cap target at a pinned slot. Returns `None` if the
112    /// slot is either not pinned by this Image's declarations or
113    /// empty.
114    pub fn get(&self, idx: SlotIdx) -> Option<CapHashOrRef> {
115        if self.main.is_pinned(idx) {
116            self.main.get(idx)
117        } else {
118            None
119        }
120    }
121
122    /// Image-declared pinned-slot iterator.
123    pub fn pinned_slots(&self) -> impl Iterator<Item = &SlotIdx> {
124        self.main.pinned.iter()
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use javm_cap::CacheDirectory;
132
133    fn empty_cache() -> CacheDirectory {
134        CacheDirectory::new()
135    }
136
137    #[test]
138    fn mainframe_get_root_slot() {
139        let mut cn = CNodeCap::new(8).unwrap();
140        cn.set(SlotIdx(5), Some(CapHashOrRef::Hash([1u8; 32])))
141            .unwrap();
142        let cache = empty_cache();
143        let m = MainFrame::new(&cn, &[], &cache);
144        assert!(m.get(SlotIdx(5)).is_some());
145        assert!(m.get(SlotIdx(6)).is_none());
146    }
147
148    #[test]
149    fn mainframe_is_pinned_reads_pinned_list() {
150        let cn = CNodeCap::new(8).unwrap();
151        let cache = empty_cache();
152        let pinned = vec![SlotIdx(2), SlotIdx(5)];
153        let m = MainFrame::new(&cn, &pinned, &cache);
154        assert!(m.is_pinned(SlotIdx(2)));
155        assert!(!m.is_pinned(SlotIdx(3)));
156        assert!(m.is_pinned(SlotIdx(5)));
157    }
158
159    #[test]
160    fn mainframe_resolve_root_path() {
161        let mut cn = CNodeCap::new(8).unwrap();
162        cn.set(SlotIdx(9), Some(CapHashOrRef::Hash([7u8; 32])))
163            .unwrap();
164        let cache = empty_cache();
165        let m = MainFrame::new(&cn, &[], &cache);
166        let p = SlotPath::root(SlotIdx(9));
167        let got = m.resolve(&p).unwrap();
168        assert_eq!(got, Some(CapHashOrRef::Hash([7u8; 32])));
169    }
170
171    #[test]
172    fn mainframe_resolve_empty_intermediate_errors() {
173        let cn = CNodeCap::new(8).unwrap();
174        let cache = empty_cache();
175        let m = MainFrame::new(&cn, &[], &cache);
176        let p = SlotPath::new(vec![SlotIdx(7), SlotIdx(0)]).unwrap();
177        let res = m.resolve(&p);
178        assert!(matches!(res, Err(VmError::SlotEmpty(7))));
179    }
180
181    #[test]
182    fn bareframe_only_reads_pinned() {
183        let mut cn = CNodeCap::new(8).unwrap();
184        cn.set(SlotIdx(2), Some(CapHashOrRef::Hash([3u8; 32])))
185            .unwrap();
186        cn.set(SlotIdx(3), Some(CapHashOrRef::Hash([4u8; 32])))
187            .unwrap();
188        let cache = empty_cache();
189        let pinned = vec![SlotIdx(2)];
190        let b = BareFrame::new(&cn, &pinned, &cache);
191        // Slot 2 is pinned and present → readable.
192        assert!(b.get(SlotIdx(2)).is_some());
193        // Slot 3 is present but NOT pinned → hidden.
194        assert!(b.get(SlotIdx(3)).is_none());
195    }
196}