Skip to main content

revmc_codegen/compiler/translate/
vstack.rs

1//! Section-local virtual stack that tracks EVM stack values as SSA values.
2//!
3//! Instead of immediately storing/loading to the stack alloca, values are kept
4//! in a logical cache. Physical stores are deferred until a "materialization"
5//! point (builtin call, section boundary, suspend, etc.).
6
7use core::ops::Range;
8
9/// State of a single virtual stack slot.
10#[derive(Clone, Copy, Debug)]
11pub(super) enum VSlot<T> {
12    /// Physical memory already contains the canonical value.
13    Materialized,
14    /// The canonical value exists only as an SSA value; memory may be stale.
15    Virtual(T),
16}
17
18/// A section-local virtual stack indexed by section-relative offsets.
19///
20/// Offsets are relative to `section_start_sp`:
21/// - Negative offsets (`-inputs .. 0`) represent values that existed on the stack before the
22///   section started (the section's inputs).
23/// - Non-negative offsets represent values pushed within the section.
24#[derive(Clone, Debug)]
25pub(super) struct VStack<T> {
26    /// Lowest tracked offset (inclusive), relative to `section_start_sp`.
27    base_offset: i32,
28    /// One-past-top offset, relative to `section_start_sp`.
29    /// Equivalent to `section_len_offset + len_offset` in `FunctionCx`.
30    top_offset: i32,
31    /// Slot states, indexed by `(offset - base_offset)`.
32    slots: Vec<Option<VSlot<T>>>,
33}
34
35impl<T> Default for VStack<T> {
36    fn default() -> Self {
37        Self { base_offset: 0, top_offset: 0, slots: Vec::new() }
38    }
39}
40
41impl<T: Copy> VStack<T> {
42    /// Resets the virtual stack for a new section.
43    ///
44    /// `inputs` is the number of stack values consumed by the section (the section's
45    /// minimum required stack depth). `max_growth` is the maximum number of slots the
46    /// section pushes beyond the entry height.
47    ///
48    /// All input slots start as [`VSlot::Materialized`] because they were written by
49    /// the previous section (or the function entry) and are already in memory.
50    pub(super) fn reset(&mut self, inputs: usize, max_growth: usize) {
51        self.base_offset = -(inputs as i32);
52        self.top_offset = 0;
53        let capacity = inputs + max_growth;
54        self.slots.clear();
55        self.slots.resize(capacity, None);
56
57        // Section inputs are already in memory.
58        for i in 0..inputs {
59            self.slots[i] = Some(VSlot::Materialized);
60        }
61    }
62
63    /// Returns the current top offset (one-past-top, relative to section start).
64    pub(super) fn top_offset(&self) -> i32 {
65        self.top_offset
66    }
67
68    /// Returns the section-relative offset for a given operand depth
69    /// (0 = TOS, 1 = second from top, etc.).
70    pub(super) fn offset_at_depth(&self, depth: usize) -> i32 {
71        self.top_offset - 1 - depth as i32
72    }
73
74    fn idx(&self, offset: i32) -> usize {
75        let idx = offset - self.base_offset;
76        debug_assert!(
77            idx >= 0 && (idx as usize) < self.slots.len(),
78            "VStack: offset {offset} out of range [{}..{}), base={}, top={}, cap={}",
79            self.base_offset,
80            self.base_offset + self.slots.len() as i32,
81            self.base_offset,
82            self.top_offset,
83            self.slots.len(),
84        );
85        idx as usize
86    }
87
88    /// Returns the slot state at the given operand depth.
89    pub(super) fn get(&self, depth: usize) -> VSlot<T> {
90        let idx = self.idx(self.offset_at_depth(depth));
91        self.slots[idx].unwrap()
92    }
93
94    /// Returns the slot state at the given section-relative offset.
95    /// Returns `VSlot::Materialized` if the offset is outside the tracked range.
96    pub(super) fn get_at_offset(&self, offset: i32) -> VSlot<T> {
97        let idx = offset - self.base_offset;
98        if idx < 0 || idx as usize >= self.slots.len() {
99            return VSlot::Materialized;
100        }
101        self.slots[idx as usize].unwrap_or(VSlot::Materialized)
102    }
103
104    /// Sets a slot to virtual at the given operand depth.
105    pub(super) fn set(&mut self, depth: usize, value: T) {
106        let idx = self.idx(self.offset_at_depth(depth));
107        self.slots[idx] = Some(VSlot::Virtual(value));
108    }
109
110    /// Sets a slot to virtual at a section-relative offset.
111    pub(super) fn set_at_offset(&mut self, offset: i32, value: T) {
112        let idx = self.idx(offset);
113        self.slots[idx] = Some(VSlot::Virtual(value));
114    }
115
116    /// Pushes a virtual value onto the top of the stack.
117    pub(super) fn push(&mut self, value: T) {
118        let idx = self.idx(self.top_offset);
119        self.slots[idx] = Some(VSlot::Virtual(value));
120        self.top_offset += 1;
121    }
122
123    /// Pushes a materialized marker onto the top of the stack.
124    /// Used when a builtin writes directly to the physical stack slot.
125    pub(super) fn push_mem(&mut self) {
126        let idx = self.idx(self.top_offset);
127        self.slots[idx] = Some(VSlot::Materialized);
128        self.top_offset += 1;
129    }
130
131    /// Drops `n` elements from the top of the stack.
132    pub(super) fn drop_top(&mut self, n: usize) {
133        self.top_offset -= n as i32;
134        debug_assert!(
135            self.top_offset >= self.base_offset,
136            "VStack::drop_top({n}): top={} dropped below base={}",
137            self.top_offset,
138            self.base_offset,
139        );
140    }
141
142    /// Returns the live range of offsets (base_offset..top_offset).
143    pub(super) fn live_range(&self) -> Range<i32> {
144        self.base_offset..self.top_offset
145    }
146
147    /// Returns the number of virtual (non-materialized) slots in the live range.
148    pub(super) fn virtual_count(&self) -> usize {
149        self.pending_stores(self.live_range()).count()
150    }
151
152    /// Returns pending virtual slots that need physical stores in the given range,
153    /// yielding `(offset, value)` pairs.
154    ///
155    /// Offsets outside the tracked range are silently skipped (they are already materialized
156    /// by the previous section or not yet live).
157    pub(super) fn pending_stores(&self, range: Range<i32>) -> impl Iterator<Item = (i32, T)> + '_ {
158        range.filter_map(|off| {
159            let idx = off - self.base_offset;
160            if idx < 0 || idx as usize >= self.slots.len() {
161                return None;
162            }
163            match self.slots.get(idx as usize)?.as_ref()? {
164                VSlot::Virtual(v) => Some((off, *v)),
165                VSlot::Materialized => None,
166            }
167        })
168    }
169
170    /// Marks all slots in the given range as materialized.
171    ///
172    /// Offsets outside the tracked range are silently skipped.
173    pub(super) fn mark_materialized_range(&mut self, range: Range<i32>) {
174        for off in range {
175            let idx = off - self.base_offset;
176            if idx < 0 || idx as usize >= self.slots.len() {
177                continue;
178            }
179            if let Some(slot) = self.slots.get_mut(idx as usize)
180                && slot.is_some()
181            {
182                *slot = Some(VSlot::Materialized);
183            }
184        }
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn push_pop_basic() {
194        let mut vs = VStack::<i32>::default();
195        // Section with 0 inputs, max_growth 4.
196        vs.reset(0, 4);
197        assert_eq!(vs.top_offset(), 0);
198
199        vs.push(10);
200        vs.push(20);
201        assert_eq!(vs.top_offset(), 2);
202
203        // Depth 0 = TOS = 20, depth 1 = 10.
204        match vs.get(0) {
205            VSlot::Virtual(v) => assert_eq!(v, 20),
206            _ => panic!("expected virtual"),
207        }
208        match vs.get(1) {
209            VSlot::Virtual(v) => assert_eq!(v, 10),
210            _ => panic!("expected virtual"),
211        }
212
213        vs.drop_top(1);
214        assert_eq!(vs.top_offset(), 1);
215        match vs.get(0) {
216            VSlot::Virtual(v) => assert_eq!(v, 10),
217            _ => panic!("expected virtual"),
218        }
219    }
220
221    #[test]
222    fn section_inputs_materialized() {
223        let mut vs = VStack::<i32>::default();
224        // Section with 2 inputs, max_growth 2.
225        vs.reset(2, 2);
226        assert_eq!(vs.top_offset(), 0);
227        assert_eq!(vs.live_range(), -2..0);
228
229        // Input slots (depth 1 and 2 from top of pre-existing stack) are materialized.
230        // depth 0 is at offset -1, depth 1 is at offset -2.
231        // But top_offset = 0, so get(depth) requires depth < 2 (2 elements exist: -2, -1).
232        // Wait — these are below top, so we access them as "from TOS".
233        // With top=0, depth 0 = offset -1, depth 1 = offset -2.
234        // But top is the one-past-end; the values are at -2 and -1.
235        // We can't use get() because top=0 and nothing is "on" the stack from the
236        // virtual stack's perspective (inputs are below the section start).
237        // Instead, verify via pending_stores.
238        let pending: Vec<_> = vs.pending_stores(-2..0).collect();
239        assert!(pending.is_empty(), "inputs should be materialized");
240    }
241
242    #[test]
243    fn set_swap() {
244        let mut vs = VStack::<i32>::default();
245        vs.reset(0, 4);
246        vs.push(10);
247        vs.push(20);
248        vs.push(30);
249
250        // Simulate swap: read both, then set.
251        let a = match vs.get(0) {
252            VSlot::Virtual(v) => v,
253            _ => panic!("expected virtual"),
254        };
255        let b = match vs.get(2) {
256            VSlot::Virtual(v) => v,
257            _ => panic!("expected virtual"),
258        };
259        vs.set(0, b);
260        vs.set(2, a);
261
262        match vs.get(0) {
263            VSlot::Virtual(v) => assert_eq!(v, 10),
264            _ => panic!("expected virtual"),
265        }
266        match vs.get(2) {
267            VSlot::Virtual(v) => assert_eq!(v, 30),
268            _ => panic!("expected virtual"),
269        }
270    }
271
272    #[test]
273    fn materialize_range() {
274        let mut vs = VStack::<i32>::default();
275        vs.reset(0, 4);
276        vs.push(10);
277        vs.push(20);
278        vs.push(30);
279
280        let pending: Vec<_> = vs.pending_stores(0..3).collect();
281        assert_eq!(pending.len(), 3);
282
283        vs.mark_materialized_range(0..3);
284        let pending: Vec<_> = vs.pending_stores(0..3).collect();
285        assert_eq!(pending.len(), 0);
286    }
287}