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}