1use lamina_mir::{Function, Instruction, Register, VirtualReg, verify::calculate_dominators};
2use std::{
3 cmp::Reverse,
4 collections::{BTreeMap, HashMap, HashSet},
5 hash::Hash,
6};
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
11pub enum PhysRegHandle {
12 Named(&'static str),
14}
15
16impl PhysRegHandle {
17 pub fn as_named(self) -> Option<&'static str> {
19 match self {
20 PhysRegHandle::Named(name) => Some(name),
21 }
22 }
23}
24
25pub trait PhysRegConvertible: Copy + Eq {
28 fn into_handle(self) -> PhysRegHandle;
30
31 fn from_handle(handle: PhysRegHandle) -> Option<Self>
33 where
34 Self: Sized;
35}
36
37impl PhysRegConvertible for &'static str {
38 fn into_handle(self) -> PhysRegHandle {
39 PhysRegHandle::Named(self)
40 }
41
42 fn from_handle(handle: PhysRegHandle) -> Option<Self> {
43 match handle {
44 PhysRegHandle::Named(name) => Some(name),
45 }
46 }
47}
48
49pub trait LocalRegisterAllocator {
58 type PhysReg: PhysRegConvertible;
60
61 fn alloc_scratch(&mut self) -> Option<Self::PhysReg>;
64
65 fn free_scratch(&mut self, phys: Self::PhysReg);
67
68 fn get_mapping(&self, vreg: &VirtualReg) -> Option<Self::PhysReg>;
71
72 fn ensure_mapping(&mut self, vreg: VirtualReg) -> Option<Self::PhysReg>;
76
77 fn mapped_for_register(&self, reg: &Register) -> Option<Self::PhysReg>;
80
81 fn occupy(&mut self, phys: Self::PhysReg);
84
85 fn release(&mut self, phys: Self::PhysReg);
87
88 fn is_occupied(&self, phys: Self::PhysReg) -> bool;
91}
92
93pub use LocalRegisterAllocator as RegisterAllocator;
95
96pub trait LocalRegisterAllocatorDyn {
99 fn alloc_scratch_dyn(&mut self) -> Option<PhysRegHandle>;
100 fn free_scratch_dyn(&mut self, phys: PhysRegHandle);
101 fn get_mapping_dyn(&self, vreg: &VirtualReg) -> Option<PhysRegHandle>;
102 fn ensure_mapping_dyn(&mut self, vreg: VirtualReg) -> Option<PhysRegHandle>;
103 fn mapped_for_register_dyn(&self, reg: &Register) -> Option<PhysRegHandle>;
104 fn occupy_dyn(&mut self, phys: PhysRegHandle);
105 fn release_dyn(&mut self, phys: PhysRegHandle);
106 fn is_occupied_dyn(&self, phys: PhysRegHandle) -> bool;
107}
108
109pub use LocalRegisterAllocatorDyn as RegisterAllocatorDyn;
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum Allocation<R: Copy> {
116 Register(R),
117 Spill(i32),
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
125pub struct LiveInterval {
126 pub vreg: VirtualReg,
127 pub start: usize,
129 pub end: usize,
131 pub weight: u64,
133}
134
135fn loop_depths(function: &Function) -> Vec<u32> {
136 let labels: HashMap<_, _> = function
137 .blocks
138 .iter()
139 .enumerate()
140 .map(|(index, block)| (block.label.as_str(), index))
141 .collect();
142 let dominators = calculate_dominators(function, &labels);
143 let mut predecessors = vec![Vec::new(); function.blocks.len()];
144 let mut latches = vec![Vec::new(); function.blocks.len()];
145 for (index, block) in function.blocks.iter().enumerate() {
146 for successor in block.successors() {
147 if let Some(&target) = labels.get(successor) {
148 predecessors[target].push(index);
149 if dominators.dominates(target, index) {
150 latches[target].push(index);
151 }
152 }
153 }
154 }
155 let mut depths = vec![0; function.blocks.len()];
156 for (header, tails) in latches.into_iter().enumerate() {
157 if tails.is_empty() {
158 continue;
159 }
160 let mut members = HashSet::from([header]);
161 let mut pending = tails;
162 while let Some(index) = pending.pop() {
163 if members.insert(index) {
164 pending.extend(
165 predecessors[index]
166 .iter()
167 .copied()
168 .filter(|predecessor| dominators.dominates(header, *predecessor)),
169 );
170 }
171 }
172 for index in members {
173 depths[index] += 1;
174 }
175 }
176 depths
177}
178
179pub struct LinearScanAllocator;
188
189impl LinearScanAllocator {
190 pub fn compute_intervals(function: &Function) -> Vec<LiveInterval> {
199 let mut intervals: HashMap<VirtualReg, LiveInterval> = HashMap::new();
200 let mut positions = Vec::new();
201 let mut uses = Vec::new();
202 let mut defs = Vec::new();
203 let depths = loop_depths(function);
204 let mut pos = 0;
205 let record = |intervals: &mut HashMap<VirtualReg, LiveInterval>, vreg, pos, weight| {
206 let interval = intervals.entry(vreg).or_insert(LiveInterval {
207 vreg,
208 start: pos,
209 end: pos,
210 weight: 0,
211 });
212 interval.start = interval.start.min(pos);
213 interval.end = interval.end.max(pos);
214 interval.weight += weight;
215 };
216 for param in &function.sig.params {
217 if let Register::Virtual(vreg) = param.reg {
218 record(&mut intervals, vreg, 0, 1);
219 }
220 }
221 for (block_index, block) in function.blocks.iter().enumerate() {
222 let weight = 10u64.pow(depths[block_index].min(6));
223 let start = pos;
224 let mut block_uses = HashSet::new();
225 let mut block_defs = HashSet::new();
226 for instr in &block.instructions {
227 let mut used: Vec<_> = instr
228 .use_regs()
229 .into_iter()
230 .filter_map(|reg| {
231 if let Register::Virtual(vreg) = reg {
232 Some(*vreg)
233 } else {
234 None
235 }
236 })
237 .collect();
238 if let Instruction::StackAddr { slot, .. } = &instr.instr {
239 used.push(*slot);
240 }
241 for vreg in used {
242 record(&mut intervals, vreg, pos, weight);
243 if !block_defs.contains(&vreg) {
244 block_uses.insert(vreg);
245 }
246 }
247 if let Some(Register::Virtual(vreg)) = instr.def_reg() {
248 record(&mut intervals, *vreg, pos, weight);
249 block_defs.insert(*vreg);
250 }
251 pos += 1;
252 }
253 positions.push((start, pos.saturating_sub(1).max(start)));
254 uses.push(block_uses);
255 defs.push(block_defs);
256 }
257 let labels: HashMap<_, _> = function
258 .blocks
259 .iter()
260 .enumerate()
261 .map(|(index, block)| (block.label.as_str(), index))
262 .collect();
263 let mut live_in = vec![HashSet::new(); function.blocks.len()];
264 let mut live_out = live_in.clone();
265 loop {
266 let mut changed = false;
267 for (index, block) in function.blocks.iter().enumerate().rev() {
268 let out: HashSet<_> = block
269 .successors()
270 .iter()
271 .filter_map(|label| labels.get(label))
272 .flat_map(|successor| live_in[*successor].iter().copied())
273 .collect();
274 let mut input = uses[index].clone();
275 input.extend(out.difference(&defs[index]).copied());
276 changed |= input != live_in[index] || out != live_out[index];
277 live_in[index] = input;
278 live_out[index] = out;
279 }
280 if !changed {
281 break;
282 }
283 }
284 for (index, (start, end)) in positions.into_iter().enumerate() {
285 for vreg in &live_in[index] {
286 record(&mut intervals, *vreg, start, 0);
287 }
288 for vreg in &live_out[index] {
289 record(&mut intervals, *vreg, end, 0);
290 }
291 }
292 let mut result: Vec<_> = intervals.into_values().collect();
293 result.sort_by_key(|interval| (interval.start, interval.vreg));
294 result
295 }
296
297 pub fn allocate<R: Copy + Eq>(
306 intervals: &[LiveInterval],
307 available_regs: &[R],
308 ) -> HashMap<VirtualReg, Allocation<R>> {
309 Self::allocate_with_constraints(intervals, available_regs, |_, _| true)
310 }
311
312 pub fn allocate_with_constraints<R: Copy + Eq>(
314 intervals: &[LiveInterval],
315 available_regs: &[R],
316 allowed: impl Fn(&LiveInterval, R) -> bool,
317 ) -> HashMap<VirtualReg, Allocation<R>> {
318 let mut result: HashMap<VirtualReg, Allocation<R>> = HashMap::new();
319 let mut active: Vec<(&LiveInterval, R)> = Vec::new();
321 let mut free: Vec<R> = available_regs.to_vec();
322 let mut next_spill: i32 = -8; for interval in intervals {
325 let current_start = interval.start;
327 let mut freed: Vec<R> = Vec::new();
328 active.retain(|(ai, reg)| {
329 if ai.end < current_start {
330 freed.push(*reg);
331 false
332 } else {
333 true
334 }
335 });
336 free.extend(freed);
337
338 if let Some(index) = free.iter().rposition(|reg| allowed(interval, *reg)) {
339 let reg = free.remove(index);
340 result.insert(interval.vreg, Allocation::Register(reg));
342 let pos = active
344 .binary_search_by_key(&interval.end, |(ai, _)| ai.end)
345 .unwrap_or_else(|i| i);
346 active.insert(pos, (interval, reg));
347 } else {
348 let candidate = active
349 .iter()
350 .enumerate()
351 .filter(|(_, (_, reg))| allowed(interval, *reg))
352 .min_by_key(|(_, (ai, _))| (ai.weight, Reverse(ai.end), ai.vreg))
353 .map(|(index, _)| index);
354 match candidate.map(|index| (index, active[index])) {
355 Some((index, (spill_interval, spill_reg)))
356 if spill_interval.weight < interval.weight
357 || (spill_interval.weight == interval.weight
358 && spill_interval.end > interval.end) =>
359 {
360 result.insert(spill_interval.vreg, Allocation::Spill(next_spill));
362 next_spill -= 8;
363 active.remove(index);
364 result.insert(interval.vreg, Allocation::Register(spill_reg));
365 let pos = active
366 .binary_search_by_key(&interval.end, |(ai, _)| ai.end)
367 .unwrap_or_else(|i| i);
368 active.insert(pos, (interval, spill_reg));
369 }
370 _ => {
371 result.insert(interval.vreg, Allocation::Spill(next_spill));
373 next_spill -= 8;
374 }
375 }
376 }
377 }
378
379 result
380 }
381}
382
383#[inline]
385pub fn intervals_interfere(a: &LiveInterval, b: &LiveInterval) -> bool {
386 a.start <= b.end && b.start <= a.end
387}
388
389pub struct GraphColorAllocator;
396
397impl GraphColorAllocator {
398 pub fn allocate<R: Copy + Eq + Hash>(
402 intervals: &[LiveInterval],
403 available_regs: &[R],
404 ) -> HashMap<VirtualReg, Allocation<R>> {
405 Self::allocate_with_constraints(intervals, available_regs, |_, _| true)
406 }
407
408 pub fn allocate_with_constraints<R: Copy + Eq + Hash>(
410 intervals: &[LiveInterval],
411 available_regs: &[R],
412 allowed: impl Fn(&LiveInterval, R) -> bool,
413 ) -> HashMap<VirtualReg, Allocation<R>> {
414 if intervals.is_empty() {
415 return HashMap::new();
416 }
417
418 debug_assert!(
419 intervals
420 .iter()
421 .all(|interval| interval.start <= interval.end)
422 );
423 let mut starts: Vec<usize> = intervals.iter().map(|interval| interval.start).collect();
424 let mut ends: Vec<usize> = intervals.iter().map(|interval| interval.end).collect();
425 starts.sort_unstable();
426 ends.sort_unstable();
427 let degrees: Vec<usize> = intervals
428 .iter()
429 .map(|interval| {
430 let overlapping_starts = starts.partition_point(|&s| s <= interval.end);
431 let ended_before = ends.partition_point(|&e| e < interval.start);
432 overlapping_starts - ended_before - 1
433 })
434 .collect();
435 let mut order: Vec<usize> = (0..intervals.len()).collect();
436 order.sort_by(|&i, &j| {
437 intervals[j]
438 .weight
439 .cmp(&intervals[i].weight)
440 .then_with(|| degrees[j].cmp(°rees[i]))
441 .then_with(|| intervals[i].end.cmp(&intervals[j].end))
442 .then_with(|| intervals[i].vreg.cmp(&intervals[j].vreg))
443 });
444
445 let mut result: HashMap<VirtualReg, Allocation<R>> = HashMap::new();
446 let mut next_spill: i32 = -8;
447 let mut occupied: HashMap<R, BTreeMap<usize, usize>> = HashMap::new();
448
449 for idx in order {
450 let interval = &intervals[idx];
451 let overlaps = |spans: &BTreeMap<usize, usize>| {
452 spans
453 .range(..=interval.end)
454 .next_back()
455 .is_some_and(|(_, &end)| end >= interval.start)
456 };
457
458 let mut picked: Option<R> = None;
459 for reg in available_regs {
460 let blocked = occupied.get(reg).is_some_and(overlaps);
461 if !blocked && allowed(interval, *reg) {
462 picked = Some(*reg);
463 break;
464 }
465 }
466
467 match picked {
468 Some(r) => {
469 result.insert(interval.vreg, Allocation::Register(r));
470 occupied
471 .entry(r)
472 .or_default()
473 .insert(interval.start, interval.end);
474 }
475 None => {
476 result.insert(interval.vreg, Allocation::Spill(next_spill));
477 next_spill -= 8;
478 }
479 }
480 }
481
482 result
483 }
484}
485
486impl<T> LocalRegisterAllocatorDyn for T
487where
488 T: LocalRegisterAllocator,
489{
490 fn alloc_scratch_dyn(&mut self) -> Option<PhysRegHandle> {
491 self.alloc_scratch().map(PhysRegConvertible::into_handle)
492 }
493
494 fn free_scratch_dyn(&mut self, phys: PhysRegHandle) {
495 if let Some(reg) = <T::PhysReg as PhysRegConvertible>::from_handle(phys) {
496 self.free_scratch(reg);
497 } else {
498 debug_assert!(false, "failed to decode physical register handle");
499 }
500 }
501
502 fn get_mapping_dyn(&self, vreg: &VirtualReg) -> Option<PhysRegHandle> {
503 self.get_mapping(vreg).map(PhysRegConvertible::into_handle)
504 }
505
506 fn ensure_mapping_dyn(&mut self, vreg: VirtualReg) -> Option<PhysRegHandle> {
507 self.ensure_mapping(vreg)
508 .map(PhysRegConvertible::into_handle)
509 }
510
511 fn mapped_for_register_dyn(&self, reg: &Register) -> Option<PhysRegHandle> {
512 self.mapped_for_register(reg)
513 .map(PhysRegConvertible::into_handle)
514 }
515
516 fn occupy_dyn(&mut self, phys: PhysRegHandle) {
517 if let Some(reg) = <T::PhysReg as PhysRegConvertible>::from_handle(phys) {
518 self.occupy(reg);
519 } else {
520 debug_assert!(false, "failed to decode physical register handle");
521 }
522 }
523
524 fn release_dyn(&mut self, phys: PhysRegHandle) {
525 if let Some(reg) = <T::PhysReg as PhysRegConvertible>::from_handle(phys) {
526 self.release(reg);
527 } else {
528 debug_assert!(false, "failed to decode physical register handle");
529 }
530 }
531
532 fn is_occupied_dyn(&self, phys: PhysRegHandle) -> bool {
533 if let Some(reg) = <T::PhysReg as PhysRegConvertible>::from_handle(phys) {
534 self.is_occupied(reg)
535 } else {
536 debug_assert!(false, "failed to decode physical register handle");
537 false
538 }
539 }
540}
541
542#[cfg(test)]
547mod tests {
548 use super::*;
549 use lamina_mir::{
550 Block, FunctionBuilder, Immediate, Instruction, IntBinOp, MirType, Operand, Register,
551 ScalarType, VirtualReg,
552 };
553
554 fn make_add_function() -> Function {
555 let v0 = Register::Virtual(VirtualReg::gpr(0));
561 let v1 = Register::Virtual(VirtualReg::gpr(1));
562 let v2 = Register::Virtual(VirtualReg::gpr(2));
563 let i64_ty = MirType::Scalar(ScalarType::I64);
564
565 FunctionBuilder::new("add")
566 .param(v0.clone(), i64_ty)
567 .param(v1.clone(), i64_ty)
568 .returns(i64_ty)
569 .block("entry")
570 .instr(Instruction::IntBinary {
571 op: IntBinOp::Add,
572 ty: i64_ty,
573 dst: v2.clone(),
574 lhs: Operand::Register(v0),
575 rhs: Operand::Register(v1),
576 })
577 .instr(Instruction::Ret {
578 value: Some(Operand::Register(v2)),
579 })
580 .build()
581 }
582
583 #[test]
584 fn weighted_spills_preserve_hot_values_and_constraints() {
585 let mut intervals = vec![
586 LiveInterval {
587 vreg: VirtualReg::gpr(0),
588 start: 0,
589 end: 100,
590 weight: 1000,
591 },
592 LiveInterval {
593 vreg: VirtualReg::gpr(1),
594 start: 1,
595 end: 80,
596 weight: 10,
597 },
598 LiveInterval {
599 vreg: VirtualReg::gpr(2),
600 start: 2,
601 end: 90,
602 weight: 100,
603 },
604 LiveInterval {
605 vreg: VirtualReg::gpr(3),
606 start: 3,
607 end: 4,
608 weight: 1,
609 },
610 ];
611 for allocate in [LinearScanAllocator::allocate, GraphColorAllocator::allocate] {
612 let plan = allocate(&intervals, &["a", "b"]);
613 assert!(matches!(plan[&intervals[0].vreg], Allocation::Register(_)));
614 assert!(matches!(plan[&intervals[1].vreg], Allocation::Spill(_)));
615 assert!(matches!(plan[&intervals[2].vreg], Allocation::Register(_)));
616 assert!(matches!(plan[&intervals[3].vreg], Allocation::Spill(_)));
617 }
618 intervals[2].weight = 10;
619 let plan = LinearScanAllocator::allocate(&intervals, &["a", "b"]);
620 assert!(matches!(plan[&intervals[2].vreg], Allocation::Spill(_)));
621 let plan =
622 LinearScanAllocator::allocate_with_constraints(&intervals, &["a", "b"], |i, r| {
623 i.vreg.id != 2 || r == "a"
624 });
625 assert!(matches!(plan[&intervals[2].vreg], Allocation::Spill(_)));
626 }
627
628 #[test]
629 fn nested_loop_weights_merge_latches_and_cap_depth() {
630 let value = VirtualReg::gpr(0);
631 let mut function = FunctionBuilder::new("nested")
632 .param(Register::Virtual(value), MirType::Scalar(ScalarType::I64))
633 .block("entry")
634 .instr(Instruction::Jmp {
635 target: "h0".into(),
636 })
637 .build();
638 for depth in 0..8 {
639 let mut block = Block::new(format!("h{depth}"));
640 block.push(Instruction::Br {
641 cond: Register::Virtual(value),
642 true_target: if depth == 7 {
643 "tail".into()
644 } else {
645 format!("h{}", depth + 1)
646 },
647 false_target: if depth == 0 {
648 "exit".into()
649 } else {
650 format!("h{}", depth - 1)
651 },
652 });
653 function.add_block(block);
654 }
655 let mut tail = Block::new("tail");
656 tail.push(Instruction::Br {
657 cond: Register::Virtual(value),
658 true_target: "h7".into(),
659 false_target: "tail2".into(),
660 });
661 function.add_block(tail);
662 let mut tail2 = Block::new("tail2");
663 tail2.push(Instruction::Jmp {
664 target: "h7".into(),
665 });
666 function.add_block(tail2);
667 let mut exit = Block::new("exit");
668 exit.push(Instruction::Ret {
669 value: Some(Operand::Register(Register::Virtual(value))),
670 });
671 function.add_block(exit);
672 let mut dead = Block::new("dead");
673 dead.push(Instruction::Jmp {
674 target: "dead".into(),
675 });
676 function.add_block(dead);
677 assert_eq!(
678 loop_depths(&function),
679 vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 8, 8, 0, 0]
680 );
681 let intervals = LinearScanAllocator::compute_intervals(&function);
682 assert_eq!(
683 intervals[0].weight,
684 10 + 100 + 1000 + 10000 + 100000 + 4 * 1000000 + 2
685 );
686 }
687
688 #[test]
689 fn constrained_allocators_respect_fixed_registers_and_eviction() {
690 let intervals = [
691 LiveInterval {
692 vreg: VirtualReg::gpr(0),
693 start: 0,
694 end: 10,
695 weight: 1,
696 },
697 LiveInterval {
698 vreg: VirtualReg::gpr(1),
699 start: 1,
700 end: 9,
701 weight: 1,
702 },
703 LiveInterval {
704 vreg: VirtualReg::gpr(2),
705 start: 2,
706 end: 3,
707 weight: 1,
708 },
709 LiveInterval {
710 vreg: VirtualReg::gpr(3),
711 start: 3,
712 end: 4,
713 weight: 1,
714 },
715 ];
716 let allowed = |interval: &LiveInterval, reg| match interval.vreg.id {
717 0 | 2 => reg == "callee",
718 1 => reg == "caller",
719 _ => false,
720 };
721 for plan in [
722 LinearScanAllocator::allocate_with_constraints(
723 &intervals,
724 &["callee", "caller"],
725 allowed,
726 ),
727 GraphColorAllocator::allocate_with_constraints(
728 &intervals,
729 &["callee", "caller"],
730 allowed,
731 ),
732 ] {
733 assert!(matches!(plan[&intervals[3].vreg], Allocation::Spill(_)));
734 for interval in &intervals {
735 if let Allocation::Register(reg) = plan[&interval.vreg] {
736 assert!(allowed(interval, reg));
737 for other in &intervals {
738 if other.vreg != interval.vreg && intervals_interfere(interval, other) {
739 assert_ne!(plan[&other.vreg], Allocation::Register(reg));
740 }
741 }
742 }
743 }
744 }
745 let linear = LinearScanAllocator::allocate_with_constraints(
746 &intervals,
747 &["callee", "caller"],
748 allowed,
749 );
750 assert!(matches!(linear[&intervals[0].vreg], Allocation::Spill(_)));
751 assert_eq!(linear[&intervals[2].vreg], Allocation::Register("callee"));
752 }
753
754 #[test]
755 fn test_compute_intervals_basic() {
756 let func = make_add_function();
757 let intervals = LinearScanAllocator::compute_intervals(&func);
758
759 assert!(!intervals.is_empty());
761 let vreg_ids: Vec<u32> = intervals.iter().map(|i| i.vreg.id).collect();
763 assert!(vreg_ids.contains(&0));
764 assert!(vreg_ids.contains(&1));
765 assert!(vreg_ids.contains(&2));
766 }
767
768 #[test]
769 fn test_compute_intervals_sorted_by_start() {
770 let func = make_add_function();
771 let intervals = LinearScanAllocator::compute_intervals(&func);
772 let starts: Vec<usize> = intervals.iter().map(|i| i.start).collect();
773 let mut sorted = starts.clone();
774 sorted.sort_unstable();
775 assert_eq!(starts, sorted, "intervals should be sorted by start");
776 }
777
778 #[test]
779 fn copy_contributes_source_use_and_destination_def() {
780 let f64_ty = MirType::Scalar(ScalarType::F64);
781 let src = Register::Virtual(VirtualReg::fpr(0));
782 let dst = Register::Virtual(VirtualReg::fpr(1));
783 let func = FunctionBuilder::new("copy")
784 .param(src.clone(), f64_ty)
785 .returns(f64_ty)
786 .block("entry")
787 .instr(Instruction::Copy {
788 ty: f64_ty,
789 dst: dst.clone(),
790 src: Operand::Register(src),
791 })
792 .instr(Instruction::Ret {
793 value: Some(Operand::Register(dst)),
794 })
795 .build();
796
797 let intervals = LinearScanAllocator::compute_intervals(&func);
798 let ids: Vec<u32> = intervals.iter().map(|interval| interval.vreg.id).collect();
799 assert!(ids.contains(&0));
800 assert!(ids.contains(&1));
801 }
802
803 #[test]
804 fn intervals_cover_values_live_across_back_edges() {
805 let invariant = VirtualReg::gpr(0);
806 let temporary = VirtualReg::gpr(1);
807 let mut function = FunctionBuilder::new("loop")
808 .param(
809 Register::Virtual(invariant),
810 MirType::Scalar(ScalarType::I64),
811 )
812 .block("entry")
813 .instr(Instruction::Jmp {
814 target: "header".into(),
815 })
816 .build();
817 let mut header = Block::new("header");
818 header.push(Instruction::Br {
819 cond: Register::Virtual(invariant),
820 true_target: "body".into(),
821 false_target: "exit".into(),
822 });
823 function.add_block(header);
824 let mut exit = Block::new("exit");
825 exit.push(Instruction::Ret { value: None });
826 function.add_block(exit);
827 let mut body = Block::new("body");
828 body.push(Instruction::Copy {
829 ty: MirType::Scalar(ScalarType::I64),
830 dst: Register::Virtual(temporary),
831 src: Operand::Immediate(Immediate::I64(42)),
832 });
833 body.push(Instruction::Jmp {
834 target: "header".into(),
835 });
836 function.add_block(body);
837 let intervals = LinearScanAllocator::compute_intervals(&function);
838 let live = intervals
839 .iter()
840 .find(|interval| interval.vreg == invariant)
841 .expect("invariant");
842 assert_eq!((live.start, live.end), (0, 4));
843 for plan in [
844 LinearScanAllocator::allocate(&intervals, &["r12"]),
845 GraphColorAllocator::allocate(&intervals, &["r12"]),
846 ] {
847 assert!(
848 matches!(plan[&invariant], Allocation::Spill(_))
849 || matches!(plan[&temporary], Allocation::Spill(_))
850 );
851 }
852 }
853
854 #[test]
855 fn stack_address_slots_participate_in_intervals() {
856 let slot = VirtualReg::gpr(0);
857 let pointer = VirtualReg::gpr(1);
858 let function = FunctionBuilder::new("address")
859 .block("entry")
860 .instr(Instruction::StackAddr {
861 dst: Register::Virtual(pointer),
862 slot,
863 })
864 .instr(Instruction::Ret {
865 value: Some(Operand::Register(Register::Virtual(pointer))),
866 })
867 .build();
868 let intervals = LinearScanAllocator::compute_intervals(&function);
869 assert_eq!(intervals.len(), 2);
870 assert!(intervals.iter().any(|interval| interval.vreg == slot));
871 }
872
873 #[test]
874 fn test_allocate_fits_in_registers() {
875 let func = make_add_function();
876 let intervals = LinearScanAllocator::compute_intervals(&func);
877 let regs = ["r12", "r13", "r14", "r15"];
878 let alloc = LinearScanAllocator::allocate(&intervals, ®s);
879
880 for interval in &intervals {
882 let a = alloc
883 .get(&interval.vreg)
884 .expect("every vreg should be allocated");
885 assert!(
886 matches!(a, Allocation::Register(_)),
887 "vreg {:?} should be in a register, got {:?}",
888 interval.vreg,
889 a
890 );
891 }
892 }
893
894 #[test]
895 fn test_allocate_spills_when_registers_exhausted() {
896 let i64_ty = MirType::Scalar(ScalarType::I64);
898 let mut func = FunctionBuilder::new("spill_test").returns(i64_ty).build();
899 let mut block = Block::new("entry");
900
901 for i in 0u32..8 {
903 let vi = Register::Virtual(VirtualReg::gpr(i));
904 let vj = Register::Virtual(VirtualReg::gpr(i + 1));
905 let vd = Register::Virtual(VirtualReg::gpr(i + 2));
906 block.push(Instruction::IntBinary {
907 op: IntBinOp::Add,
908 ty: i64_ty,
909 dst: vd,
910 lhs: Operand::Register(vi),
911 rhs: Operand::Register(vj),
912 });
913 }
914 block.push(Instruction::Ret { value: None });
915 func.add_block(block);
916
917 let intervals = LinearScanAllocator::compute_intervals(&func);
918 let regs = ["r12", "r13"]; let alloc = LinearScanAllocator::allocate(&intervals, ®s);
920
921 let has_spill = alloc.values().any(|a| matches!(a, Allocation::Spill(_)));
923 assert!(has_spill, "expected spills when registers are exhausted");
924
925 for a in alloc.values() {
927 if let Allocation::Spill(offset) = a {
928 assert!(*offset < 0, "spill offset should be negative");
929 assert_eq!(offset % 8, 0, "spill offset should be 8-byte aligned");
930 }
931 }
932 }
933
934 #[test]
935 fn graph_color_fits_three_vregs_without_spill() {
936 let func = make_add_function();
937 let intervals = LinearScanAllocator::compute_intervals(&func);
938 let regs = ["r12", "r13", "r14", "r15"];
939 let gc = GraphColorAllocator::allocate(&intervals, ®s);
940 for interval in &intervals {
941 let a = gc.get(&interval.vreg).expect("allocated");
942 assert!(
943 matches!(a, Allocation::Register(_)),
944 "graph color should keep simple add in registers, got {:?}",
945 a
946 );
947 }
948 }
949
950 #[test]
951 fn graph_color_spills_when_register_count_exhausted() {
952 let i64_ty = MirType::Scalar(ScalarType::I64);
953 let mut func = FunctionBuilder::new("spill_gc").returns(i64_ty).build();
954 let mut block = Block::new("entry");
955 for i in 0u32..8 {
956 let vi = Register::Virtual(VirtualReg::gpr(i));
957 let vj = Register::Virtual(VirtualReg::gpr(i + 1));
958 let vd = Register::Virtual(VirtualReg::gpr(i + 2));
959 block.push(Instruction::IntBinary {
960 op: IntBinOp::Add,
961 ty: i64_ty,
962 dst: vd,
963 lhs: Operand::Register(vi),
964 rhs: Operand::Register(vj),
965 });
966 }
967 block.push(Instruction::Ret { value: None });
968 func.add_block(block);
969
970 let intervals = LinearScanAllocator::compute_intervals(&func);
971 let regs = ["r12", "r13"];
972 let gc = GraphColorAllocator::allocate(&intervals, ®s);
973 let has_spill = gc.values().any(|a| matches!(a, Allocation::Spill(_)));
974 assert!(
975 has_spill,
976 "graph color should spill when k=2 and pressure is high"
977 );
978 }
979}