Skip to main content

javm_exec/
mat.rs

1//! Shared per-page memory-materialization (#3) state machine.
2//!
3//! Both engines drive this same state machine so they charge
4//! bit-identical category-#3 gas (consensus determinism): the
5//! interpreter does software first-touch accounting (no real unmap),
6//! the x86 recompiler does the same accounting off hardware page-table
7//! faults. See `~/docs/spec-staging/gas-cost.md` §3.
8//!
9//! The charge rules key strictly on the per-page [`PageState`] and the
10//! static [`PageKind`] — never on the instruction — so a load-then-store
11//! to one page charges identically regardless of engine. The **sole
12//! fault-driven #3 charge is copy-on-write** (the first write to a writable
13//! page), charged at most once per page (per frame). **Read-only page-in is
14//! not charged at the fault**: bringing a read-only region into the working
15//! set is accounted eagerly at the CALL that maps it
16//! ([`crate::gas_const::call_frame_cost`], computed statically from the
17//! callee Image), so a read — first or not — debits nothing here. A read
18//! only records the residency [`PageState`] transition. The one re-touch
19//! event is a `MGMT_MOVE`/`MGMT_DROP` slot eviction, a block terminator.
20//!
21//! ## Read-only materialization is a mapping event, not a charge
22//!
23//! Read-only ([`PageKind::PinnedCapRo`]) regions — the program's code and
24//! pinned data caps — are still materialized in **units**, where a unit is
25//! the intersection of one DataCap with one 2 MiB cluster
26//! ([`CLUSTER_SHIFT`], [`unit_base`]): the recompiler fault-arounds a whole
27//! unit's RO pages on first touch so later reads in the unit hit no further
28//! faults, and both engines record the same unit set. That clustering is
29//! now purely a **mapping / fault-reduction** optimization with **zero
30//! gas** — the read-only page-in cost is charged once, eagerly, at the CALL
31//! (one [`crate::gas_const::PAGE_IN_COST`] per declared 2 MiB unit), not
32//! lazily per touched unit at the fault. Copy-on-write (RW) regions stay
33//! 4 KiB-granular: a write copies one page and charges [`COW_COST`]. Both
34//! engines key on the same [`unit_base`], so the (gas-free) unit set and the
35//! per-page CoW charge match bit-for-bit.
36
37use crate::gas_const::COW_COST;
38use crate::mem::PAGE_SIZE;
39
40/// log2 of the read-only materialization cluster size. `21` → 2 MiB, the
41/// common large-page size on x86 (PDE), AArch64 (L2 block), and RISC-V
42/// (megapage), so the clustered charge model is arch-portable.
43/// TODO(gas-calibration): cluster size is subject to change.
44pub const CLUSTER_SHIFT: u32 = 21;
45
46/// Absolute 2 MiB cluster index of `addr` (`addr >> CLUSTER_SHIFT`).
47#[inline]
48pub fn cluster_of(addr: u32) -> u32 {
49    addr >> CLUSTER_SHIFT
50}
51
52/// Identity of the read-only materialization **unit** containing `addr`: the
53/// `cap ∩ cluster` region, named by its base address `max(cluster_lo,
54/// cap_start)`. The recompiler fault-arounds exactly the unit's pages on its
55/// first touch (mapping them all read-only in one go), so a unit is mapped
56/// at most once — a pure fault-reduction key. It carries **no gas**:
57/// read-only page-in is charged eagerly at the CALL
58/// ([`crate::gas_const::call_frame_cost`]), not per touched unit.
59///
60/// A unit is keyed by **both** the cap (`cap_start`) and the 2 MiB cluster,
61/// so two distinct caps sharing a cluster are two units — a single
62/// fault/map event touches at most one DataCap. Because caps are disjoint,
63/// at most one cap per cluster starts at/below the cluster boundary
64/// (yielding `unit_base == cluster_lo`); every other cap in that cluster
65/// starts strictly later with a distinct `cap_start`, so this single
66/// `max(cluster_lo, cap_start)` value uniquely names one `(cap, cluster)`
67/// pair.
68#[inline]
69pub fn unit_base(addr: u32, cap_start: u32) -> u32 {
70    let cluster_lo = cluster_of(addr) << CLUSTER_SHIFT;
71    cluster_lo.max(cap_start)
72}
73
74/// Static per-page source kind, derived once from the Image's declared
75/// memory mappings (pinned slot vs initial slot vs ephemeral / zero tail).
76#[derive(Clone, Copy, Debug, PartialEq, Eq)]
77pub enum PageKind {
78    /// Sourced from a **pinned** cnode slot: read-only forever. A store
79    /// is a hard fault, never a CoW.
80    PinnedCapRo,
81    /// Sourced from an **unpinned** (initial) slot: readable; the first
82    /// write copies-on-write.
83    UnpinnedCapCow,
84    /// Declared mapping with no / empty source (ephemeral working area,
85    /// or the zero-padded tail of an under-sized DataCap): reads see
86    /// zero, the first write materializes a fresh zero page.
87    EphemeralZero,
88}
89
90impl PageKind {
91    /// One-byte tag for the per-page side arrays both engines keep.
92    #[inline]
93    pub fn as_u8(self) -> u8 {
94        match self {
95            PageKind::PinnedCapRo => 0,
96            PageKind::UnpinnedCapCow => 1,
97            PageKind::EphemeralZero => 2,
98        }
99    }
100
101    /// Inverse of [`PageKind::as_u8`]. `None` for an undeclared page.
102    #[inline]
103    pub fn from_u8(v: u8) -> Option<PageKind> {
104        match v {
105            0 => Some(PageKind::PinnedCapRo),
106            1 => Some(PageKind::UnpinnedCapCow),
107            2 => Some(PageKind::EphemeralZero),
108            _ => None,
109        }
110    }
111}
112
113/// Dynamic per-page first-touch state.
114#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
115pub enum PageState {
116    /// Never touched; the next read pages-in, the next write CoWs.
117    #[default]
118    NotPresent,
119    /// Paged-in read-only (a read happened, or a pinned-cap page).
120    PresentRo,
121    /// CoW'd (a write happened): writable.
122    PresentRw,
123}
124
125impl PageState {
126    #[inline]
127    pub fn as_u8(self) -> u8 {
128        match self {
129            PageState::NotPresent => 0,
130            PageState::PresentRo => 1,
131            PageState::PresentRw => 2,
132        }
133    }
134
135    #[inline]
136    pub fn from_u8(v: u8) -> PageState {
137        match v {
138            1 => PageState::PresentRo,
139            2 => PageState::PresentRw,
140            _ => PageState::NotPresent,
141        }
142    }
143}
144
145/// A write to a read-only (pinned) page — a permanent PVM2-level fault.
146/// (Accesses outside any declared mapping are rejected by the caller
147/// before reaching [`charge_for`].) Charge nothing.
148#[derive(Clone, Copy, Debug, PartialEq, Eq)]
149pub struct HardFault;
150
151/// The category-#3 charge and resulting state for one page touch.
152///
153/// Read-only page-in is **free at the fault** — the cost of bringing a
154/// region into the working set is charged eagerly at the CALL that maps it
155/// ([`crate::gas_const::call_frame_cost`]). Copy-on-write (the first write
156/// to a writable page) is the only fault-driven #3 charge:
157///
158/// - first read (`NotPresent`)  → `0`,         → `PresentRo`
159/// - first write (`NotPresent`) → `COW_COST`,  → `PresentRw`
160/// - write after read (`PresentRo`) → `COW_COST`,  → `PresentRw`
161/// - already present for this access kind → `0`
162/// - write to `PinnedCapRo` → [`HardFault`]
163#[inline]
164pub fn charge_for(
165    state: PageState,
166    kind: PageKind,
167    is_write: bool,
168) -> Result<(u64, PageState), HardFault> {
169    if is_write && kind == PageKind::PinnedCapRo {
170        return Err(HardFault);
171    }
172    Ok(match (state, is_write) {
173        // First read pages the page in read-only and records residency —
174        // but read-only page-in no longer charges at the fault (it is
175        // accounted eagerly at the CALL).
176        (PageState::NotPresent, false) => (0, PageState::PresentRo),
177        // First write copies-on-write: the sole remaining fault-driven #3
178        // charge (the page-in half is free, as above).
179        (PageState::NotPresent, true) => (COW_COST, PageState::PresentRw),
180        (PageState::PresentRo, true) => (COW_COST, PageState::PresentRw),
181        (PageState::PresentRo, false) => (0, PageState::PresentRo),
182        (PageState::PresentRw, _) => (0, PageState::PresentRw),
183    })
184}
185
186/// The set of consensus 4 KiB pages a single `width`-byte access at
187/// `addr` touches: the base page, plus the next page iff the access
188/// straddles a page boundary. At most [`crate::gas_const::MAX_PAGES_PER_ACCESS`]
189/// (= 2) pages. Pages are ordered **low → high** — the fixed order both
190/// engines iterate, so the charged page set and total match exactly.
191#[derive(Clone, Copy, Debug, PartialEq, Eq)]
192pub struct PageSet {
193    pages: [u32; 2],
194    len: u8,
195}
196
197impl PageSet {
198    /// The page numbers (page-aligned base addresses), low → high.
199    #[inline]
200    pub fn as_slice(&self) -> &[u32] {
201        &self.pages[..self.len as usize]
202    }
203
204    #[inline]
205    pub fn len(&self) -> usize {
206        self.len as usize
207    }
208
209    #[inline]
210    pub fn is_empty(&self) -> bool {
211        self.len == 0
212    }
213}
214
215/// Compute the page set for a `width`-byte access at `addr`. Keyed only
216/// on `addr` and `width` so the recompiler (which learns `width` from a
217/// compile-time side table) and the interpreter (which knows it from the
218/// opcode) agree byte-for-byte. `width` must be in `1..=8`.
219#[inline]
220pub fn access_pages(addr: u32, width: u32) -> PageSet {
221    let mask = PAGE_SIZE - 1;
222    let base = addr & !mask;
223    let off = addr & mask;
224    // Straddles iff the last touched byte lands in the next page.
225    if off + width > PAGE_SIZE {
226        PageSet {
227            pages: [base, base.wrapping_add(PAGE_SIZE)],
228            len: 2,
229        }
230    } else {
231        PageSet {
232            pages: [base, 0],
233            len: 1,
234        }
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    #[test]
243    fn first_read_is_free() {
244        // Read-only page-in is charged eagerly at the CALL, not here.
245        let (c, s) = charge_for(PageState::NotPresent, PageKind::UnpinnedCapCow, false).unwrap();
246        assert_eq!(c, 0);
247        assert_eq!(s, PageState::PresentRo);
248    }
249
250    #[test]
251    fn first_write_pays_cow_only() {
252        // The page-in half is free; only the CoW copy is charged.
253        let (c, s) = charge_for(PageState::NotPresent, PageKind::EphemeralZero, true).unwrap();
254        assert_eq!(c, COW_COST);
255        assert_eq!(s, PageState::PresentRw);
256    }
257
258    #[test]
259    fn write_after_read_pays_cow_only() {
260        let (c, s) = charge_for(PageState::PresentRo, PageKind::UnpinnedCapCow, true).unwrap();
261        assert_eq!(c, COW_COST);
262        assert_eq!(s, PageState::PresentRw);
263    }
264
265    #[test]
266    fn second_touch_is_free() {
267        assert_eq!(
268            charge_for(PageState::PresentRo, PageKind::UnpinnedCapCow, false).unwrap(),
269            (0, PageState::PresentRo)
270        );
271        assert_eq!(
272            charge_for(PageState::PresentRw, PageKind::EphemeralZero, true).unwrap(),
273            (0, PageState::PresentRw)
274        );
275    }
276
277    #[test]
278    fn pinned_store_hard_faults_and_reads_are_always_free() {
279        assert_eq!(
280            charge_for(PageState::PresentRo, PageKind::PinnedCapRo, true),
281            Err(HardFault)
282        );
283        // A read of a pinned page is free (RO page-in is charged at CALL),
284        // and only records residency.
285        let (c, s) = charge_for(PageState::NotPresent, PageKind::PinnedCapRo, false).unwrap();
286        assert_eq!((c, s), (0, PageState::PresentRo));
287    }
288
289    #[test]
290    fn access_pages_single_and_straddle() {
291        // Aligned 8-byte: one page.
292        let s = access_pages(0x1000, 8);
293        assert_eq!(s.as_slice(), &[0x1000]);
294        // 8-byte at offset 0xFFC: straddles into the next page.
295        let s = access_pages(0x1FFC, 8);
296        assert_eq!(s.as_slice(), &[0x1000, 0x2000]);
297        // 4-byte at 0xFFE: straddles.
298        let s = access_pages(0x1FFE, 4);
299        assert_eq!(s.as_slice(), &[0x1000, 0x2000]);
300        // 1-byte never straddles.
301        let s = access_pages(0x1FFF, 1);
302        assert_eq!(s.as_slice(), &[0x1000]);
303        // 8-byte ending exactly at the boundary (offset 0xFF8): one page.
304        let s = access_pages(0x1FF8, 8);
305        assert_eq!(s.as_slice(), &[0x1000]);
306    }
307}