Skip to main content

wasm_pvm/
memory_layout.rs

1//! PVM memory address layout constants.
2
3// Memory layout constants often use negative i32s or large u32s that wrap.
4#![allow(
5    clippy::cast_possible_truncation,
6    clippy::cast_possible_wrap,
7    clippy::cast_sign_loss
8)]
9
10//! All WASM-to-PVM memory regions are defined here so the layout can be
11//! understood and modified in one place.
12//!
13//! ```text
14//! PVM Address Space:
15//!   0x00000 - 0x0FFFF   Reserved (fault on access)
16//!   0x10000 - 0x1FFFF   Read-only data segment (RO_DATA_BASE)
17//!   0x20000 - 0x2FFFF   Gap zone (unmapped, guard between RO and RW)
18//!   0x30000+            Globals window (GLOBAL_MEMORY_BASE; actual = globals_region_size(...))
19//!   globals_end+        Parameter overflow area (PARAM_OVERFLOW_SIZE bytes, 8-byte aligned)
20//!   next 4KB boundary   WASM linear memory (4KB-aligned, computed dynamically)
21//!   ...
22//!   0xFEFE0000          Stack segment end (stack grows downward)
23//!   0xFFFF0000          Exit address (EXIT_ADDRESS)
24//! ```
25
26/// Base address for the read-only data segment (dispatch tables, constant data).
27pub const RO_DATA_BASE: i32 = 0x10000;
28
29/// Base address for WASM globals in PVM memory.
30/// Each global occupies 4 bytes at `GLOBAL_MEMORY_BASE + index * 4`.
31pub const GLOBAL_MEMORY_BASE: i32 = 0x30000;
32
33/// Size of the parameter overflow area in bytes.
34/// Supports up to 32 overflow parameters (5th+ args) during `call_indirect`.
35/// Each overflow parameter occupies 8 bytes.
36pub const PARAM_OVERFLOW_SIZE: usize = 256;
37
38/// Compute the base address for the parameter overflow area.
39/// Placed right after the globals region, 8-byte aligned.
40#[must_use]
41pub fn compute_param_overflow_base(num_globals: usize, num_passive_segments: usize) -> i32 {
42    let globals_end =
43        GLOBAL_MEMORY_BASE as usize + globals_region_size(num_globals, num_passive_segments);
44    // Align to 8 bytes for clean parameter access.
45    ((globals_end + 7) & !7) as i32
46}
47
48/// Stack segment end address (where the stack pointer starts, grows downward).
49pub const STACK_SEGMENT_END: i32 = 0xFEFE_0000u32 as i32;
50
51/// Default stack size limit (64KB, matching SPI default).
52pub const DEFAULT_STACK_SIZE: u32 = 64 * 1024;
53
54/// Exit address: jumping here terminates the program.
55/// This is `0xFFFF0000` interpreted as a signed i32.
56pub const EXIT_ADDRESS: i32 = -65536;
57
58/// Minimum address the stack pointer can reach (`STACK_SEGMENT_END - stack_size`).
59/// If SP goes below this, we have a stack overflow.
60#[must_use]
61pub fn stack_limit(stack_size: u32) -> i32 {
62    (STACK_SEGMENT_END as u32).wrapping_sub(stack_size) as i32
63}
64
65/// Compute the base address for WASM linear memory in the PVM address space.
66/// Globals, the compiler-managed memory size slot, passive segment lengths,
67/// and the parameter overflow area are laid out starting at `GLOBAL_MEMORY_BASE`.
68/// The heap begins after all of these regions, aligned to a 4KB PVM page boundary.
69///
70/// # Why 4KB alignment (not 64KB)
71///
72/// The result is aligned to the PVM page size (4KB = 0x1000). This is correct
73/// because:
74/// - The SPI spec requires page-aligned (4KB) `rw_data` lengths, not 64KB.
75/// - The anan-as interpreter (`vendor/anan-as/assembly/spi.ts`) uses
76///   `alignToPageSize(rwLength)` (4KB) for the heap zeros start, not
77///   `alignToSegmentSize` (64KB).
78/// - The WASM page size (64KB) governs `memory.grow` granularity only — it
79///   controls how much memory grows per step, not where the base address must
80///   sit.
81/// - Using 4KB alignment saves ~52KB per program (the old 64KB alignment
82///   wasted up to 60KB of padding between `globals_end` and the heap start).
83#[must_use]
84pub fn compute_wasm_memory_base(num_globals: usize, num_passive_segments: usize) -> i32 {
85    let param_overflow_end = compute_param_overflow_base(num_globals, num_passive_segments)
86        as usize
87        + PARAM_OVERFLOW_SIZE;
88    // Align to PVM page size (4KB = 0x1000).
89    ((param_overflow_end + 0xFFF) & !0xFFF) as i32
90}
91
92/// Bytes reserved for globals, the compiler-managed memory size global, and
93/// passive data segment lengths.
94#[must_use]
95pub fn globals_region_size(num_globals: usize, num_passive_segments: usize) -> usize {
96    (num_globals + 1 + num_passive_segments) * 4
97}
98
99/// Offset within `GLOBAL_MEMORY_BASE` for the compiler-managed memory size global.
100/// This is stored AFTER all user globals: address = 0x30000 + (`num_globals` * 4).
101/// Value is the current memory size in 64KB pages (u32).
102#[must_use]
103pub fn memory_size_global_offset(num_globals: usize) -> i32 {
104    GLOBAL_MEMORY_BASE + (num_globals as i32 * 4)
105}
106
107/// Offset within `GLOBAL_MEMORY_BASE` for a passive data segment's effective length.
108/// Stored after the memory size global: `memory_size_offset + 4 + ordinal * 4`.
109/// Used for bounds checking in `memory.init` and zeroed by `data.drop`.
110#[must_use]
111pub fn data_segment_length_offset(num_globals: usize, ordinal: usize) -> i32 {
112    memory_size_global_offset(num_globals) + 4 + (ordinal as i32 * 4)
113}
114
115/// Compute the global variable address for a given global index.
116#[must_use]
117pub fn global_addr(idx: u32) -> i32 {
118    GLOBAL_MEMORY_BASE + (idx as i32) * 4
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn wasm_memory_base_typical_program() {
127        // Few globals, no passive segments:
128        // globals_end = 0x30000 + 24 = 0x30018
129        // param_overflow_base = align8(0x30018) = 0x30018
130        // param_overflow_end = 0x30018 + 256 = 0x30118
131        // aligned to 4KB = 0x31000
132        let base = compute_wasm_memory_base(5, 0);
133        assert_eq!(base, 0x31000);
134    }
135
136    #[test]
137    fn wasm_memory_base_zero_globals() {
138        let base = compute_wasm_memory_base(0, 0);
139        // globals_end = 0x30000 + 4 = 0x30004
140        // param_overflow_base = 0x30008 (8-aligned)
141        // param_overflow_end = 0x30108
142        // aligned = 0x31000
143        assert_eq!(base, 0x31000);
144    }
145
146    #[test]
147    fn wasm_memory_base_many_globals_pushes_base() {
148        // 2000 globals + 1 memory_size = 2001 * 4 = 8004 bytes
149        // globals_end = 0x30000 + 8004 = 0x31F44
150        // param_overflow_base = 0x31F48 (8-aligned)
151        // param_overflow_end = 0x32048
152        // aligned = 0x33000
153        let base = compute_wasm_memory_base(2000, 0);
154        assert_eq!(base, 0x33000);
155    }
156
157    #[test]
158    fn wasm_memory_base_is_4kb_aligned() {
159        for globals in [0, 1, 100, 500, 1000] {
160            for passive in [0, 1, 5] {
161                let base = compute_wasm_memory_base(globals, passive);
162                assert_eq!(
163                    base & 0xFFF,
164                    0,
165                    "base 0x{base:X} not 4KB-aligned for {globals} globals, {passive} passive"
166                );
167            }
168        }
169    }
170
171    #[test]
172    fn param_overflow_base_after_globals() {
173        // 5 globals → globals_end = 0x30018, 8-aligned = 0x30018
174        assert_eq!(compute_param_overflow_base(5, 0), 0x30018);
175        // 0 globals → globals_end = 0x30004, 8-aligned = 0x30008
176        assert_eq!(compute_param_overflow_base(0, 0), 0x30008);
177        // 5 globals + 3 passive → globals_end = 0x30000 + 36 = 0x30024, 8-aligned = 0x30028
178        assert_eq!(compute_param_overflow_base(5, 3), 0x30028);
179    }
180
181    #[test]
182    fn param_overflow_does_not_overlap_globals() {
183        // With many globals, the overflow area must start after all of them.
184        let globals = 1000;
185        let globals_end = GLOBAL_MEMORY_BASE as usize + globals_region_size(globals, 0);
186        let overflow_base = compute_param_overflow_base(globals, 0) as usize;
187        assert!(
188            overflow_base >= globals_end,
189            "overflow base 0x{overflow_base:X} overlaps globals_end 0x{globals_end:X}"
190        );
191    }
192
193    #[test]
194    fn globals_region_size_formula() {
195        assert_eq!(globals_region_size(0, 0), 4); // just memory_size slot
196        assert_eq!(globals_region_size(5, 0), 24); // 5 globals + 1 mem_size = 6 * 4
197        assert_eq!(globals_region_size(5, 3), 36); // (5 + 1 + 3) * 4
198    }
199
200    #[test]
201    fn global_addr_formula() {
202        assert_eq!(global_addr(0), 0x30000);
203        assert_eq!(global_addr(1), 0x30004);
204        assert_eq!(global_addr(10), 0x30028);
205    }
206
207    #[test]
208    fn memory_size_global_after_user_globals() {
209        assert_eq!(memory_size_global_offset(0), 0x30000);
210        assert_eq!(memory_size_global_offset(5), 0x30014);
211    }
212
213    #[test]
214    fn data_segment_length_after_memory_size() {
215        assert_eq!(data_segment_length_offset(5, 0), 0x30018); // mem_size + 4
216        assert_eq!(data_segment_length_offset(5, 1), 0x3001C); // + 4
217    }
218
219    #[test]
220    fn stack_limit_formula() {
221        assert_eq!(
222            stack_limit(DEFAULT_STACK_SIZE),
223            (0xFEFE_0000u32 - 64 * 1024) as i32
224        );
225    }
226}