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}