Skip to main content

lamina_codegen/x86_64/
regalloc.rs

1//! x86_64 register allocator with platform-aware register selection.
2
3use std::collections::{HashMap, HashSet, VecDeque};
4
5use crate::regalloc::{Allocation, LocalRegisterAllocator as MirRegisterAllocator};
6use lamina_mir::{Register, RegisterClass, VirtualReg};
7use lamina_platform::TargetOperatingSystem;
8
9/// x86_64 register allocator supporting System V AMD64 and Microsoft x64 ABIs.
10///
11/// Uses platform-appropriate GPR pools for stable virtual-to-physical register mappings
12/// and separate scratch pools for short-lived temporaries.
13pub struct X64RegAlloc {
14    target_os: TargetOperatingSystem,
15    free_gprs: VecDeque<&'static str>,
16    used_gprs: HashSet<&'static str>,
17    vreg_to_preg: HashMap<VirtualReg, &'static str>,
18    vreg_to_xmm: HashMap<VirtualReg, &'static str>,
19    scratch_free: VecDeque<&'static str>,
20    scratch_used: HashSet<&'static str>,
21    global_plan: bool,
22}
23
24const SYSV_MAP_GPRS: &[&str] = &["r12", "r13", "r14", "r15", "rbx"];
25const SYSV_LEAF_MAP_GPRS: &[&str] = &["r12", "r13", "r14", "r15", "rbx", "r10", "r11"];
26const SYSV_SCRATCH_GPRS: &[&str] = &["r10", "r11"]; // keep as-is for non-leaf
27const SYSV_LEAF_SCRATCH_GPRS: &[&str] = &["rcx", "rdx"]; // for leaf functions
28
29const WIN64_MAP_GPRS: &[&str] = &["rbx", "rsi", "rdi", "r12", "r13", "r14", "r15"];
30const WIN64_LEAF_MAP_GPRS: &[&str] = &[
31    "rbx", "rsi", "rdi", "r12", "r13", "r14", "r15", "r10", "r11",
32];
33const WIN64_SCRATCH_GPRS: &[&str] = &["r10", "r11"];
34const WIN64_LEAF_SCRATCH_GPRS: &[&str] = &["rcx", "rdx"];
35
36/// Vector registers available to global allocation. xmm0-xmm3 stay free as emitter scratch.
37pub const XMM_ALLOCATABLE: &[&str] = &[
38    "xmm4", "xmm5", "xmm6", "xmm7", "xmm8", "xmm9", "xmm10", "xmm11", "xmm12", "xmm13", "xmm14",
39    "xmm15",
40];
41/// Win64 treats xmm6-xmm15 as callee-saved, which the prologue does not preserve, so only the
42/// volatile registers are allocatable there.
43const WIN64_XMM_ALLOCATABLE: &[&str] = &["xmm4", "xmm5"];
44
45impl Default for X64RegAlloc {
46    fn default() -> Self {
47        Self::new_default()
48    }
49}
50
51impl X64RegAlloc {
52    /// Creates a new register allocator for the specified target OS.
53    pub fn new(target_os: TargetOperatingSystem) -> Self {
54        Self::with_target_os(target_os)
55    }
56
57    /// Creates a new register allocator with default target OS (Linux).
58    pub fn new_default() -> Self {
59        Self::with_target_os(TargetOperatingSystem::Linux)
60    }
61
62    fn with_target_os(target_os: TargetOperatingSystem) -> Self {
63        let (map_gprs, scratch_gprs) = match target_os {
64            TargetOperatingSystem::Windows => (WIN64_MAP_GPRS, WIN64_SCRATCH_GPRS),
65            _ => (SYSV_MAP_GPRS, SYSV_SCRATCH_GPRS),
66        };
67
68        let mut free_gprs = VecDeque::new();
69        for &r in map_gprs {
70            free_gprs.push_back(r);
71        }
72
73        let mut scratch_free = VecDeque::new();
74        for &r in scratch_gprs {
75            scratch_free.push_back(r);
76        }
77
78        Self {
79            target_os,
80            free_gprs,
81            used_gprs: HashSet::new(),
82            vreg_to_preg: HashMap::new(),
83            vreg_to_xmm: HashMap::new(),
84            scratch_free,
85            scratch_used: HashSet::new(),
86            global_plan: false,
87        }
88    }
89
90    /// Creates a new register allocator for leaf functions (no calls), expanding the available
91    /// register pool by including r10/r11 as mappable GPRs since they won't be clobbered.
92    pub fn new_leaf(target_os: TargetOperatingSystem) -> Self {
93        let (map_gprs, scratch_gprs) = match target_os {
94            TargetOperatingSystem::Windows => (WIN64_LEAF_MAP_GPRS, WIN64_LEAF_SCRATCH_GPRS),
95            _ => (SYSV_LEAF_MAP_GPRS, SYSV_LEAF_SCRATCH_GPRS),
96        };
97        let mut free_gprs = VecDeque::new();
98        for &r in map_gprs {
99            free_gprs.push_back(r);
100        }
101        let mut scratch_free = VecDeque::new();
102        for &r in scratch_gprs {
103            scratch_free.push_back(r);
104        }
105        Self {
106            target_os,
107            free_gprs,
108            used_gprs: HashSet::new(),
109            vreg_to_preg: HashMap::new(),
110            vreg_to_xmm: HashMap::new(),
111            scratch_free,
112            scratch_used: HashSet::new(),
113            global_plan: false,
114        }
115    }
116
117    /// GPR candidates for global allocation; emitter constraints apply separately.
118    pub fn gpr_pool_for_global_allocation(
119        _target_os: TargetOperatingSystem,
120        _leaf: bool,
121    ) -> Vec<&'static str> {
122        vec![
123            "r12", "r13", "r14", "r15", "rbx", "rsi", "rdi", "r8", "r9", "r10", "r11", "rdx", "rcx",
124        ]
125    }
126
127    /// Vector register candidates for global allocation of `RegisterClass::Vec` values.
128    pub fn xmm_pool_for_global_allocation(target_os: TargetOperatingSystem) -> Vec<&'static str> {
129        match target_os {
130            TargetOperatingSystem::Windows => WIN64_XMM_ALLOCATABLE.to_vec(),
131            _ => XMM_ALLOCATABLE.to_vec(),
132        }
133    }
134
135    /// Rebuild allocator state from a precomputed plan (global linear scan or graph coloring).
136    pub fn from_global_plan(
137        target_os: TargetOperatingSystem,
138        leaf: bool,
139        plan: &HashMap<VirtualReg, Allocation<&'static str>>,
140    ) -> Self {
141        let mut s = if leaf {
142            Self::new_leaf(target_os)
143        } else {
144            Self::new(target_os)
145        };
146        s.free_gprs.clear();
147        s.global_plan = true;
148        for (&vreg, alloc) in plan {
149            if vreg.class == RegisterClass::Vec {
150                if let Allocation::Register(phys) = alloc {
151                    s.vreg_to_xmm.insert(vreg, *phys);
152                }
153                continue;
154            }
155            if vreg.class != RegisterClass::Gpr {
156                continue;
157            }
158            if let Allocation::Register(phys) = alloc
159                && s.vreg_to_preg.insert(vreg, *phys).is_none()
160            {
161                s.used_gprs.insert(*phys);
162                if let Some(pos) = s.free_gprs.iter().position(|&p| p == *phys) {
163                    let _ = s.free_gprs.remove(pos);
164                }
165            }
166        }
167        s
168    }
169
170    /// Sets conservative mode for complex functions, using fewer registers to reduce pressure.
171    pub fn set_conservative_mode(&mut self) {
172        self.free_gprs.clear();
173
174        match self.target_os {
175            TargetOperatingSystem::Windows => {
176                self.free_gprs.push_back("rbx");
177                self.free_gprs.push_back("rsi");
178                self.free_gprs.push_back("r12");
179                self.free_gprs.push_back("r13");
180                self.free_gprs.push_back("r14");
181            }
182            _ => {
183                self.free_gprs.push_back("r12");
184                self.free_gprs.push_back("r13");
185                self.free_gprs.push_back("r14");
186                self.free_gprs.push_back("rbx");
187            }
188        }
189
190        self.used_gprs.retain(|r| self.free_gprs.contains(r));
191
192        let free_set: HashSet<&str> = self.free_gprs.iter().copied().collect();
193        self.vreg_to_preg.retain(|_, preg| free_set.contains(preg));
194    }
195
196    #[inline]
197    pub fn alloc_scratch(&mut self) -> Option<&'static str> {
198        MirRegisterAllocator::alloc_scratch(self)
199    }
200
201    #[inline]
202    pub fn free_scratch(&mut self, phys: &'static str) {
203        MirRegisterAllocator::free_scratch(self, phys);
204    }
205
206    #[inline]
207    pub fn is_occupied(&self, phys: &'static str) -> bool {
208        MirRegisterAllocator::is_occupied(self, phys)
209    }
210
211    #[inline]
212    pub fn occupy(&mut self, phys: &'static str) {
213        MirRegisterAllocator::occupy(self, phys);
214    }
215
216    #[inline]
217    pub fn release(&mut self, phys: &'static str) {
218        MirRegisterAllocator::release(self, phys);
219    }
220
221    #[inline]
222    pub fn get_mapping_for(&self, v: &VirtualReg) -> Option<&'static str> {
223        MirRegisterAllocator::get_mapping(self, v)
224    }
225
226    #[inline]
227    pub fn ensure_mapping(&mut self, v: VirtualReg) -> Option<&'static str> {
228        MirRegisterAllocator::ensure_mapping(self, v)
229    }
230
231    #[inline]
232    pub fn ensure_mapping_for_gpr(&mut self, v: VirtualReg) -> Option<&'static str> {
233        MirRegisterAllocator::ensure_mapping(self, v)
234    }
235
236    /// The vector register a globally allocated `RegisterClass::Vec` value lives in.
237    pub fn xmm_for(&self, v: &VirtualReg) -> Option<&'static str> {
238        self.vreg_to_xmm.get(v).copied()
239    }
240
241    #[inline]
242    pub fn mapped_for_register(&self, r: &Register) -> Option<&'static str> {
243        MirRegisterAllocator::mapped_for_register(self, r)
244    }
245}
246
247impl MirRegisterAllocator for X64RegAlloc {
248    type PhysReg = &'static str;
249
250    fn alloc_scratch(&mut self) -> Option<Self::PhysReg> {
251        if let Some(phys) = self.scratch_free.pop_front() {
252            self.scratch_used.insert(phys);
253            Some(phys)
254        } else {
255            None
256        }
257    }
258
259    fn free_scratch(&mut self, phys: Self::PhysReg) {
260        if self.scratch_used.remove(&phys) {
261            self.scratch_free.push_back(phys);
262        }
263    }
264
265    fn get_mapping(&self, vreg: &VirtualReg) -> Option<Self::PhysReg> {
266        self.vreg_to_preg.get(vreg).copied()
267    }
268
269    fn ensure_mapping(&mut self, vreg: VirtualReg) -> Option<Self::PhysReg> {
270        if let Some(&phys) = self.vreg_to_preg.get(&vreg) {
271            return Some(phys);
272        }
273
274        if self.global_plan || vreg.class != RegisterClass::Gpr {
275            return None;
276        }
277
278        if let Some(phys) = self.free_gprs.pop_front() {
279            self.used_gprs.insert(phys);
280            self.vreg_to_preg.insert(vreg, phys);
281            Some(phys)
282        } else if let Some((vreg_to_replace, &phys)) = self.vreg_to_preg.iter().next() {
283            let vreg_to_replace = *vreg_to_replace;
284            self.vreg_to_preg.remove(&vreg_to_replace);
285            self.vreg_to_preg.insert(vreg, phys);
286            Some(phys)
287        } else {
288            None
289        }
290    }
291
292    fn mapped_for_register(&self, reg: &Register) -> Option<Self::PhysReg> {
293        match reg {
294            Register::Virtual(v) => self.vreg_to_preg.get(v).copied(),
295            Register::Physical(p) => Some(p.name),
296        }
297    }
298
299    fn occupy(&mut self, phys: Self::PhysReg) {
300        if self.used_gprs.insert(phys)
301            && let Some(pos) = self.free_gprs.iter().position(|&p| p == phys)
302        {
303            self.free_gprs.remove(pos);
304        }
305    }
306
307    fn release(&mut self, phys: Self::PhysReg) {
308        if self.used_gprs.remove(&phys) {
309            self.free_gprs.push_back(phys);
310        }
311    }
312
313    fn is_occupied(&self, phys: Self::PhysReg) -> bool {
314        self.used_gprs.contains(&phys)
315    }
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321
322    #[test]
323    fn new_linux_has_sysv_pool() {
324        let ra = X64RegAlloc::new(TargetOperatingSystem::Linux);
325        for r in SYSV_MAP_GPRS {
326            assert!(ra.free_gprs.contains(r));
327        }
328    }
329
330    #[test]
331    fn new_windows_has_win64_pool() {
332        let ra = X64RegAlloc::new(TargetOperatingSystem::Windows);
333        assert!(WIN64_MAP_GPRS.iter().all(|reg| ra.free_gprs.contains(reg)));
334    }
335
336    #[test]
337    fn ensure_mapping_returns_same_reg_for_same_vreg() {
338        let mut ra = X64RegAlloc::new_default();
339        let vreg = VirtualReg::gpr(0);
340        let first = ra.ensure_mapping(vreg);
341        let second = ra.ensure_mapping(vreg);
342        assert!(first.is_some());
343        assert_eq!(first, second);
344    }
345
346    #[test]
347    fn alloc_and_free_scratch_cycle() {
348        let mut ra = X64RegAlloc::new_default();
349        let scratch = ra.alloc_scratch();
350        assert!(
351            scratch.is_some(),
352            "scratch should be available on fresh allocator"
353        );
354        let phys = scratch.unwrap();
355        assert!(ra.scratch_used.contains(phys));
356        ra.free_scratch(phys);
357        assert!(!ra.scratch_used.contains(phys));
358        assert!(ra.scratch_free.contains(&phys));
359    }
360
361    #[test]
362    fn conservative_mode_shrinks_pool() {
363        let mut ra = X64RegAlloc::new_default();
364        let full_size = ra.free_gprs.len();
365        ra.set_conservative_mode();
366        assert!(ra.free_gprs.len() < full_size);
367    }
368
369    #[test]
370    fn global_pool_includes_caller_saved_candidates() {
371        let non_leaf =
372            X64RegAlloc::gpr_pool_for_global_allocation(TargetOperatingSystem::Linux, false);
373        let leaf = X64RegAlloc::gpr_pool_for_global_allocation(TargetOperatingSystem::Linux, true);
374        assert_eq!(leaf, non_leaf);
375        assert!(non_leaf.contains(&"r10"));
376        assert!(non_leaf.contains(&"r11"));
377        for reg in ["rax", "rbp", "rsp"] {
378            assert!(!leaf.contains(&reg));
379        }
380        for reg in ["rbx", "rcx", "rdx", "rsi", "rdi", "r8", "r9"] {
381            assert!(leaf.contains(&reg));
382        }
383    }
384
385    #[test]
386    fn global_plan_keeps_shared_mappings_and_spills_fixed() {
387        let first = VirtualReg::gpr(0);
388        let second = VirtualReg::gpr(1);
389        let spilled = VirtualReg::gpr(2);
390        let plan = HashMap::from([
391            (first, Allocation::Register("r12")),
392            (second, Allocation::Register("r12")),
393            (spilled, Allocation::Spill(-8)),
394        ]);
395        let mut ra = X64RegAlloc::from_global_plan(TargetOperatingSystem::Linux, true, &plan);
396        assert_eq!(ra.ensure_mapping(spilled), None);
397        assert_eq!(ra.ensure_mapping(VirtualReg::gpr(3)), None);
398        assert_eq!(ra.get_mapping_for(&first), Some("r12"));
399        assert_eq!(ra.get_mapping_for(&second), Some("r12"));
400        assert_eq!(ra.alloc_scratch(), Some("rcx"));
401        assert_eq!(ra.alloc_scratch(), Some("rdx"));
402    }
403
404    #[test]
405    fn is_occupied_tracks_occupy_and_release() {
406        let mut ra = X64RegAlloc::new_default();
407        let phys = "rbx";
408        assert!(!ra.is_occupied(phys));
409        ra.occupy(phys);
410        assert!(ra.is_occupied(phys));
411        ra.release(phys);
412        assert!(!ra.is_occupied(phys));
413    }
414
415    #[test]
416    fn non_gpr_vreg_returns_none() {
417        let mut ra = X64RegAlloc::new_default();
418        let fp_vreg = VirtualReg {
419            id: 99,
420            class: RegisterClass::Fpr,
421        };
422        assert!(ra.ensure_mapping(fp_vreg).is_none());
423    }
424
425    #[test]
426    fn global_plan_maps_vector_registers() {
427        let vector = VirtualReg::vec(0);
428        let spilled = VirtualReg::vec(1);
429        let plan = HashMap::from([
430            (vector, Allocation::Register("xmm4")),
431            (spilled, Allocation::Spill(-8)),
432        ]);
433        let ra = X64RegAlloc::from_global_plan(TargetOperatingSystem::Linux, true, &plan);
434        assert_eq!(ra.xmm_for(&vector), Some("xmm4"));
435        assert_eq!(ra.xmm_for(&spilled), None);
436        assert_eq!(
437            X64RegAlloc::xmm_pool_for_global_allocation(TargetOperatingSystem::Windows),
438            ["xmm4", "xmm5"]
439        );
440    }
441}