Skip to main content

solar_codegen/backend/evm/stack/
spill.rs

1//! Spill management for handling >16 live values.
2//!
3//! When more than 16 values are live simultaneously (the maximum accessible
4//! via DUP16/SWAP16), we spill values to memory.
5
6use crate::mir::ValueId;
7use solar_data_structures::map::{FxHashMap, FxHashSet};
8
9/// A slot in memory where a spilled value is stored.
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
11pub struct SpillSlot {
12    /// Offset in the spill area (in 32-byte words).
13    pub offset: u32,
14}
15
16impl SpillSlot {
17    /// Returns the memory offset in bytes for this spill slot.
18    #[must_use]
19    pub const fn byte_offset(&self) -> u32 {
20        // Memory layout in Solidity:
21        // - 0x00-0x3F: Scratch space (used for hashing, CALL address/calldata spills, etc.)
22        // - 0x40-0x5F: Free memory pointer location
23        // - 0x60-0x7F: Zero slot (used for empty dynamic arrays)
24        // - 0x80+: Dynamic allocations (structs, arrays, calldata encoding, etc.)
25        //
26        // IMPORTANT: We CANNOT use:
27        // - 0x00-0x3F: Used as scratch by lower_member_call for CALL address/calldata
28        // - 0x60-0x7F: Used as zero slot
29        // - 0x80-0x????: Dynamic allocations can grow arbitrarily large
30        //
31        // The safest approach is to use a very high memory address that's unlikely to
32        // conflict with normal memory usage. We use 0x1000000 (16MB) as the base.
33        // This wastes some gas on memory expansion but guarantees no conflicts.
34        //
35        // A proper solution would be to allocate spill slots from the heap (update
36        // free memory pointer), but that would require tracking the allocation in MIR.
37        //
38        // We use 0x1000 (4KB) as a compromise - high enough to avoid conflicts with
39        // typical dynamic allocations, but low enough to not cause excessive gas usage.
40        // In practice, most contracts use less than 4KB of dynamic memory.
41        //
42        // A proper solution would be to allocate spill slots from the heap, but that
43        // requires tracking the allocation in MIR.
44        0x1000 + self.offset * 32
45    }
46}
47
48/// Manages spill slots for values that cannot fit on the stack.
49#[derive(Clone, Debug)]
50pub struct SpillManager {
51    /// Map from value to its spill slot.
52    slots: FxHashMap<ValueId, SpillSlot>,
53    /// Values whose reserved spill slot can be loaded at the current program point.
54    reloadable: FxHashSet<ValueId>,
55    /// Values whose reserved spill slot was stored by already-emitted code.
56    stored: FxHashSet<ValueId>,
57    /// Next available spill slot offset.
58    next_offset: u32,
59    /// Maximum offset used (for tracking spill area size).
60    max_offset: u32,
61}
62
63impl SpillManager {
64    /// Creates a new spill manager.
65    #[must_use]
66    pub fn new() -> Self {
67        Self {
68            slots: FxHashMap::default(),
69            reloadable: FxHashSet::default(),
70            stored: FxHashSet::default(),
71            next_offset: 0,
72            max_offset: 0,
73        }
74    }
75
76    /// Allocates a spill slot for a value.
77    /// If the value already has a slot, returns the existing one.
78    pub fn allocate(&mut self, value: ValueId) -> SpillSlot {
79        if let Some(&slot) = self.slots.get(&value) {
80            return slot;
81        }
82
83        let slot = SpillSlot { offset: self.next_offset };
84        self.slots.insert(value, slot);
85        self.next_offset += 1;
86        self.max_offset = self.max_offset.max(self.next_offset);
87        slot
88    }
89
90    /// Returns the spill slot for a value, if one exists.
91    #[must_use]
92    pub fn get(&self, value: ValueId) -> Option<SpillSlot> {
93        self.slots.get(&value).copied()
94    }
95
96    /// Returns true if the value is currently spilled.
97    #[must_use]
98    pub fn is_spilled(&self, value: ValueId) -> bool {
99        self.slots.contains_key(&value)
100    }
101
102    /// Marks a value's spill slot as reloadable at the current program point.
103    pub fn mark_reloadable(&mut self, value: ValueId) {
104        debug_assert!(self.slots.contains_key(&value));
105        self.reloadable.insert(value);
106    }
107
108    /// Marks a value's spill slot as written by emitted code.
109    pub fn mark_stored(&mut self, value: ValueId) {
110        debug_assert!(self.slots.contains_key(&value));
111        self.reloadable.insert(value);
112        self.stored.insert(value);
113    }
114
115    /// Returns true if the value has a spill slot that can be loaded.
116    #[must_use]
117    pub fn is_reloadable(&self, value: ValueId) -> bool {
118        self.reloadable.contains(&value)
119    }
120
121    /// Returns true if already-emitted code has stored this value.
122    #[must_use]
123    pub fn is_stored(&self, value: ValueId) -> bool {
124        self.stored.contains(&value)
125    }
126
127    /// Frees a spill slot (when the value is reloaded and no longer needed in memory).
128    /// Note: Simple implementation doesn't reuse slots.
129    pub fn free(&mut self, value: ValueId) {
130        self.slots.remove(&value);
131        self.reloadable.remove(&value);
132        self.stored.remove(&value);
133    }
134
135    /// Returns the total size of the spill area in bytes.
136    #[must_use]
137    pub fn spill_area_size(&self) -> u32 {
138        self.max_offset * 32
139    }
140
141    /// Clears all spill slots (used at function boundaries).
142    pub fn clear(&mut self) {
143        self.slots.clear();
144        self.reloadable.clear();
145        self.stored.clear();
146        self.next_offset = 0;
147        self.max_offset = 0;
148    }
149
150    /// Returns the number of currently spilled values.
151    #[must_use]
152    pub fn count(&self) -> usize {
153        self.slots.len()
154    }
155}
156
157impl Default for SpillManager {
158    fn default() -> Self {
159        Self::new()
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn test_allocate() {
169        let mut manager = SpillManager::new();
170        let v0 = ValueId::from_usize(0);
171        let v1 = ValueId::from_usize(1);
172        let v2 = ValueId::from_usize(2);
173
174        let slot0 = manager.allocate(v0);
175        let slot1 = manager.allocate(v1);
176        let slot2 = manager.allocate(v2);
177
178        assert_eq!(slot0.offset, 0);
179        assert_eq!(slot1.offset, 1);
180        assert_eq!(slot2.offset, 2);
181        // Spill slots use high memory at 0x1000+
182        assert_eq!(slot0.byte_offset(), 0x1000);
183        assert_eq!(slot1.byte_offset(), 0x1000 + 32);
184        assert_eq!(slot2.byte_offset(), 0x1000 + 64);
185    }
186
187    #[test]
188    fn test_allocate_idempotent() {
189        let mut manager = SpillManager::new();
190        let v0 = ValueId::from_usize(0);
191
192        let slot1 = manager.allocate(v0);
193        let slot2 = manager.allocate(v0);
194
195        assert_eq!(slot1, slot2);
196        assert_eq!(manager.count(), 1);
197    }
198
199    #[test]
200    fn test_free() {
201        let mut manager = SpillManager::new();
202        let v0 = ValueId::from_usize(0);
203
204        manager.allocate(v0);
205        assert!(manager.is_spilled(v0));
206
207        manager.free(v0);
208        assert!(!manager.is_spilled(v0));
209    }
210
211    #[test]
212    fn test_reloadable_and_stored_are_distinct() {
213        let mut manager = SpillManager::new();
214        let v0 = ValueId::from_usize(0);
215        let v1 = ValueId::from_usize(1);
216
217        manager.allocate(v0);
218        assert!(!manager.is_reloadable(v0));
219        assert!(!manager.is_stored(v0));
220
221        manager.mark_reloadable(v0);
222        assert!(manager.is_reloadable(v0));
223        assert!(!manager.is_stored(v0));
224
225        manager.allocate(v1);
226        manager.mark_stored(v1);
227        assert!(manager.is_reloadable(v1));
228        assert!(manager.is_stored(v1));
229    }
230}