1use std::collections::{HashMap, HashSet, VecDeque};
4
5use crate::regalloc::{Allocation, LocalRegisterAllocator as MirRegisterAllocator};
6use lamina_mir::{Register, RegisterClass, VirtualReg};
7use lamina_platform::TargetOperatingSystem;
8
9pub 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"]; const SYSV_LEAF_SCRATCH_GPRS: &[&str] = &["rcx", "rdx"]; const 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
36pub const XMM_ALLOCATABLE: &[&str] = &[
38 "xmm4", "xmm5", "xmm6", "xmm7", "xmm8", "xmm9", "xmm10", "xmm11", "xmm12", "xmm13", "xmm14",
39 "xmm15",
40];
41const 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 pub fn new(target_os: TargetOperatingSystem) -> Self {
54 Self::with_target_os(target_os)
55 }
56
57 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 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 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 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 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 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 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(®));
379 }
380 for reg in ["rbx", "rcx", "rdx", "rsi", "rdi", "r8", "r9"] {
381 assert!(leaf.contains(®));
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}