1use lamina_mir::{Function, Instruction, Register, VirtualReg, verify::calculate_dominators};
2use std::{
3 cmp::Reverse,
4 collections::{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> {
192 let mut intervals: HashMap<VirtualReg, LiveInterval> = HashMap::new();
193 let mut positions = Vec::new();
194 let mut uses = Vec::new();
195 let mut defs = Vec::new();
196 let depths = loop_depths(function);
197 let mut pos = 0;
198 let record = |intervals: &mut HashMap<VirtualReg, LiveInterval>, vreg, pos, weight| {
199 let interval = intervals.entry(vreg).or_insert(LiveInterval {
200 vreg,
201 start: pos,
202 end: pos,
203 weight: 0,
204 });
205 interval.start = interval.start.min(pos);
206 interval.end = interval.end.max(pos);
207 interval.weight += weight;
208 };
209 for param in &function.sig.params {
210 if let Register::Virtual(vreg) = param.reg {
211 record(&mut intervals, vreg, 0, 1);
212 }
213 }
214 for (block_index, block) in function.blocks.iter().enumerate() {
215 let weight = 10u64.pow(depths[block_index].min(6));
216 let start = pos;
217 let mut block_uses = HashSet::new();
218 let mut block_defs = HashSet::new();
219 for instr in &block.instructions {
220 let mut used: Vec<_> = instr
221 .use_regs()
222 .into_iter()
223 .filter_map(|reg| {
224 if let Register::Virtual(vreg) = reg {
225 Some(*vreg)
226 } else {
227 None
228 }
229 })
230 .collect();
231 if let Instruction::StackAddr { slot, .. } = instr {
232 used.push(*slot);
233 }
234 for vreg in used {
235 record(&mut intervals, vreg, pos, weight);
236 if !block_defs.contains(&vreg) {
237 block_uses.insert(vreg);
238 }
239 }
240 if let Some(Register::Virtual(vreg)) = instr.def_reg() {
241 record(&mut intervals, *vreg, pos, weight);
242 block_defs.insert(*vreg);
243 }
244 pos += 1;
245 }
246 positions.push((start, pos.saturating_sub(1).max(start)));
247 uses.push(block_uses);
248 defs.push(block_defs);
249 }
250 let labels: HashMap<_, _> = function
251 .blocks
252 .iter()
253 .enumerate()
254 .map(|(index, block)| (block.label.as_str(), index))
255 .collect();
256 let mut live_in = vec![HashSet::new(); function.blocks.len()];
257 let mut live_out = live_in.clone();
258 loop {
259 let mut changed = false;
260 for (index, block) in function.blocks.iter().enumerate().rev() {
261 let out: HashSet<_> = block
262 .successors()
263 .iter()
264 .filter_map(|label| labels.get(label))
265 .flat_map(|successor| live_in[*successor].iter().copied())
266 .collect();
267 let mut input = uses[index].clone();
268 input.extend(out.difference(&defs[index]).copied());
269 changed |= input != live_in[index] || out != live_out[index];
270 live_in[index] = input;
271 live_out[index] = out;
272 }
273 if !changed {
274 break;
275 }
276 }
277 for (index, (start, end)) in positions.into_iter().enumerate() {
278 for vreg in &live_in[index] {
279 record(&mut intervals, *vreg, start, 0);
280 }
281 for vreg in &live_out[index] {
282 record(&mut intervals, *vreg, end, 0);
283 }
284 }
285 let mut result: Vec<_> = intervals.into_values().collect();
286 result.sort_by_key(|interval| (interval.start, interval.vreg));
287 result
288 }
289
290 pub fn allocate<R: Copy + Eq>(
299 intervals: &[LiveInterval],
300 available_regs: &[R],
301 ) -> HashMap<VirtualReg, Allocation<R>> {
302 Self::allocate_with_constraints(intervals, available_regs, |_, _| true)
303 }
304
305 pub fn allocate_with_constraints<R: Copy + Eq>(
307 intervals: &[LiveInterval],
308 available_regs: &[R],
309 allowed: impl Fn(&LiveInterval, R) -> bool,
310 ) -> HashMap<VirtualReg, Allocation<R>> {
311 let mut result: HashMap<VirtualReg, Allocation<R>> = HashMap::new();
312 let mut active: Vec<(&LiveInterval, R)> = Vec::new();
314 let mut free: Vec<R> = available_regs.to_vec();
315 let mut next_spill: i32 = -8; for interval in intervals {
318 let current_start = interval.start;
320 let mut freed: Vec<R> = Vec::new();
321 active.retain(|(ai, reg)| {
322 if ai.end < current_start {
323 freed.push(*reg);
324 false
325 } else {
326 true
327 }
328 });
329 free.extend(freed);
330
331 if let Some(index) = free.iter().rposition(|reg| allowed(interval, *reg)) {
332 let reg = free.remove(index);
333 result.insert(interval.vreg, Allocation::Register(reg));
335 let pos = active
337 .binary_search_by_key(&interval.end, |(ai, _)| ai.end)
338 .unwrap_or_else(|i| i);
339 active.insert(pos, (interval, reg));
340 } else {
341 let candidate = active
342 .iter()
343 .enumerate()
344 .filter(|(_, (_, reg))| allowed(interval, *reg))
345 .min_by_key(|(_, (ai, _))| (ai.weight, Reverse(ai.end), ai.vreg))
346 .map(|(index, _)| index);
347 match candidate.map(|index| (index, active[index])) {
348 Some((index, (spill_interval, spill_reg)))
349 if spill_interval.weight < interval.weight
350 || (spill_interval.weight == interval.weight
351 && spill_interval.end > interval.end) =>
352 {
353 result.insert(spill_interval.vreg, Allocation::Spill(next_spill));
355 next_spill -= 8;
356 active.remove(index);
357 result.insert(interval.vreg, Allocation::Register(spill_reg));
358 let pos = active
359 .binary_search_by_key(&interval.end, |(ai, _)| ai.end)
360 .unwrap_or_else(|i| i);
361 active.insert(pos, (interval, spill_reg));
362 }
363 _ => {
364 result.insert(interval.vreg, Allocation::Spill(next_spill));
366 next_spill -= 8;
367 }
368 }
369 }
370 }
371
372 result
373 }
374}
375
376#[inline]
378pub fn intervals_interfere(a: &LiveInterval, b: &LiveInterval) -> bool {
379 a.start <= b.end && b.start <= a.end
380}
381
382pub struct GraphColorAllocator;
389
390impl GraphColorAllocator {
391 pub fn allocate<R: Copy + Eq + Hash>(
395 intervals: &[LiveInterval],
396 available_regs: &[R],
397 ) -> HashMap<VirtualReg, Allocation<R>> {
398 Self::allocate_with_constraints(intervals, available_regs, |_, _| true)
399 }
400
401 pub fn allocate_with_constraints<R: Copy + Eq + Hash>(
403 intervals: &[LiveInterval],
404 available_regs: &[R],
405 allowed: impl Fn(&LiveInterval, R) -> bool,
406 ) -> HashMap<VirtualReg, Allocation<R>> {
407 if intervals.is_empty() {
408 return HashMap::new();
409 }
410
411 let mut order: Vec<usize> = (0..intervals.len()).collect();
412 order.sort_by(|&i, &j| {
413 let deg_i = intervals
414 .iter()
415 .enumerate()
416 .filter(|(k, other)| *k != i && intervals_interfere(&intervals[i], other))
417 .count();
418 let deg_j = intervals
419 .iter()
420 .enumerate()
421 .filter(|(k, other)| *k != j && intervals_interfere(&intervals[j], other))
422 .count();
423 intervals[j]
424 .weight
425 .cmp(&intervals[i].weight)
426 .then_with(|| deg_j.cmp(°_i))
427 .then_with(|| intervals[i].end.cmp(&intervals[j].end))
428 .then_with(|| intervals[i].vreg.cmp(&intervals[j].vreg))
429 });
430
431 let mut result: HashMap<VirtualReg, Allocation<R>> = HashMap::new();
432 let mut next_spill: i32 = -8;
433
434 for idx in order {
435 let interval = &intervals[idx];
436 let mut blocked: HashSet<R> = HashSet::new();
437 for (j, other) in intervals.iter().enumerate() {
438 if j == idx || !intervals_interfere(interval, other) {
439 continue;
440 }
441 if let Some(Allocation::Register(r)) = result.get(&other.vreg) {
442 blocked.insert(*r);
443 }
444 }
445
446 let mut picked: Option<R> = None;
447 for reg in available_regs {
448 if !blocked.contains(reg) && allowed(interval, *reg) {
449 picked = Some(*reg);
450 break;
451 }
452 }
453
454 match picked {
455 Some(r) => {
456 result.insert(interval.vreg, Allocation::Register(r));
457 }
458 None => {
459 result.insert(interval.vreg, Allocation::Spill(next_spill));
460 next_spill -= 8;
461 }
462 }
463 }
464
465 result
466 }
467}
468
469impl<T> LocalRegisterAllocatorDyn for T
470where
471 T: LocalRegisterAllocator,
472{
473 fn alloc_scratch_dyn(&mut self) -> Option<PhysRegHandle> {
474 self.alloc_scratch().map(PhysRegConvertible::into_handle)
475 }
476
477 fn free_scratch_dyn(&mut self, phys: PhysRegHandle) {
478 if let Some(reg) = <T::PhysReg as PhysRegConvertible>::from_handle(phys) {
479 self.free_scratch(reg);
480 } else {
481 debug_assert!(false, "failed to decode physical register handle");
482 }
483 }
484
485 fn get_mapping_dyn(&self, vreg: &VirtualReg) -> Option<PhysRegHandle> {
486 self.get_mapping(vreg).map(PhysRegConvertible::into_handle)
487 }
488
489 fn ensure_mapping_dyn(&mut self, vreg: VirtualReg) -> Option<PhysRegHandle> {
490 self.ensure_mapping(vreg)
491 .map(PhysRegConvertible::into_handle)
492 }
493
494 fn mapped_for_register_dyn(&self, reg: &Register) -> Option<PhysRegHandle> {
495 self.mapped_for_register(reg)
496 .map(PhysRegConvertible::into_handle)
497 }
498
499 fn occupy_dyn(&mut self, phys: PhysRegHandle) {
500 if let Some(reg) = <T::PhysReg as PhysRegConvertible>::from_handle(phys) {
501 self.occupy(reg);
502 } else {
503 debug_assert!(false, "failed to decode physical register handle");
504 }
505 }
506
507 fn release_dyn(&mut self, phys: PhysRegHandle) {
508 if let Some(reg) = <T::PhysReg as PhysRegConvertible>::from_handle(phys) {
509 self.release(reg);
510 } else {
511 debug_assert!(false, "failed to decode physical register handle");
512 }
513 }
514
515 fn is_occupied_dyn(&self, phys: PhysRegHandle) -> bool {
516 if let Some(reg) = <T::PhysReg as PhysRegConvertible>::from_handle(phys) {
517 self.is_occupied(reg)
518 } else {
519 debug_assert!(false, "failed to decode physical register handle");
520 false
521 }
522 }
523}
524
525#[cfg(test)]
530mod tests {
531 use super::*;
532 use lamina_mir::{
533 Block, FunctionBuilder, Immediate, Instruction, IntBinOp, MirType, Operand, Register,
534 ScalarType, VirtualReg,
535 };
536
537 fn make_add_function() -> Function {
538 let v0 = Register::Virtual(VirtualReg::gpr(0));
544 let v1 = Register::Virtual(VirtualReg::gpr(1));
545 let v2 = Register::Virtual(VirtualReg::gpr(2));
546 let i64_ty = MirType::Scalar(ScalarType::I64);
547
548 FunctionBuilder::new("add")
549 .param(v0.clone(), i64_ty)
550 .param(v1.clone(), i64_ty)
551 .returns(i64_ty)
552 .block("entry")
553 .instr(Instruction::IntBinary {
554 op: IntBinOp::Add,
555 ty: i64_ty,
556 dst: v2.clone(),
557 lhs: Operand::Register(v0),
558 rhs: Operand::Register(v1),
559 })
560 .instr(Instruction::Ret {
561 value: Some(Operand::Register(v2)),
562 })
563 .build()
564 }
565
566 #[test]
567 fn weighted_spills_preserve_hot_values_and_constraints() {
568 let mut intervals = vec![
569 LiveInterval {
570 vreg: VirtualReg::gpr(0),
571 start: 0,
572 end: 100,
573 weight: 1000,
574 },
575 LiveInterval {
576 vreg: VirtualReg::gpr(1),
577 start: 1,
578 end: 80,
579 weight: 10,
580 },
581 LiveInterval {
582 vreg: VirtualReg::gpr(2),
583 start: 2,
584 end: 90,
585 weight: 100,
586 },
587 LiveInterval {
588 vreg: VirtualReg::gpr(3),
589 start: 3,
590 end: 4,
591 weight: 1,
592 },
593 ];
594 for allocate in [LinearScanAllocator::allocate, GraphColorAllocator::allocate] {
595 let plan = allocate(&intervals, &["a", "b"]);
596 assert!(matches!(plan[&intervals[0].vreg], Allocation::Register(_)));
597 assert!(matches!(plan[&intervals[1].vreg], Allocation::Spill(_)));
598 assert!(matches!(plan[&intervals[2].vreg], Allocation::Register(_)));
599 assert!(matches!(plan[&intervals[3].vreg], Allocation::Spill(_)));
600 }
601 intervals[2].weight = 10;
602 let plan = LinearScanAllocator::allocate(&intervals, &["a", "b"]);
603 assert!(matches!(plan[&intervals[2].vreg], Allocation::Spill(_)));
604 let plan =
605 LinearScanAllocator::allocate_with_constraints(&intervals, &["a", "b"], |i, r| {
606 i.vreg.id != 2 || r == "a"
607 });
608 assert!(matches!(plan[&intervals[2].vreg], Allocation::Spill(_)));
609 }
610
611 #[test]
612 fn nested_loop_weights_merge_latches_and_cap_depth() {
613 let value = VirtualReg::gpr(0);
614 let mut function = FunctionBuilder::new("nested")
615 .param(Register::Virtual(value), MirType::i64())
616 .block("entry")
617 .instr(Instruction::Jmp {
618 target: "h0".into(),
619 })
620 .build();
621 for depth in 0..8 {
622 let mut block = Block::new(format!("h{depth}"));
623 block.push(Instruction::Br {
624 cond: Register::Virtual(value),
625 true_target: if depth == 7 {
626 "tail".into()
627 } else {
628 format!("h{}", depth + 1)
629 },
630 false_target: if depth == 0 {
631 "exit".into()
632 } else {
633 format!("h{}", depth - 1)
634 },
635 });
636 function.add_block(block);
637 }
638 let mut tail = Block::new("tail");
639 tail.push(Instruction::Br {
640 cond: Register::Virtual(value),
641 true_target: "h7".into(),
642 false_target: "tail2".into(),
643 });
644 function.add_block(tail);
645 let mut tail2 = Block::new("tail2");
646 tail2.push(Instruction::Jmp {
647 target: "h7".into(),
648 });
649 function.add_block(tail2);
650 let mut exit = Block::new("exit");
651 exit.push(Instruction::Ret {
652 value: Some(Operand::Register(Register::Virtual(value))),
653 });
654 function.add_block(exit);
655 let mut dead = Block::new("dead");
656 dead.push(Instruction::Jmp {
657 target: "dead".into(),
658 });
659 function.add_block(dead);
660 assert_eq!(
661 loop_depths(&function),
662 vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 8, 8, 0, 0]
663 );
664 let intervals = LinearScanAllocator::compute_intervals(&function);
665 assert_eq!(
666 intervals[0].weight,
667 10 + 100 + 1000 + 10000 + 100000 + 4 * 1000000 + 2
668 );
669 }
670
671 #[test]
672 fn constrained_allocators_respect_fixed_registers_and_eviction() {
673 let intervals = [
674 LiveInterval {
675 vreg: VirtualReg::gpr(0),
676 start: 0,
677 end: 10,
678 weight: 1,
679 },
680 LiveInterval {
681 vreg: VirtualReg::gpr(1),
682 start: 1,
683 end: 9,
684 weight: 1,
685 },
686 LiveInterval {
687 vreg: VirtualReg::gpr(2),
688 start: 2,
689 end: 3,
690 weight: 1,
691 },
692 LiveInterval {
693 vreg: VirtualReg::gpr(3),
694 start: 3,
695 end: 4,
696 weight: 1,
697 },
698 ];
699 let allowed = |interval: &LiveInterval, reg| match interval.vreg.id {
700 0 | 2 => reg == "callee",
701 1 => reg == "caller",
702 _ => false,
703 };
704 for plan in [
705 LinearScanAllocator::allocate_with_constraints(
706 &intervals,
707 &["callee", "caller"],
708 allowed,
709 ),
710 GraphColorAllocator::allocate_with_constraints(
711 &intervals,
712 &["callee", "caller"],
713 allowed,
714 ),
715 ] {
716 assert!(matches!(plan[&intervals[3].vreg], Allocation::Spill(_)));
717 for interval in &intervals {
718 if let Allocation::Register(reg) = plan[&interval.vreg] {
719 assert!(allowed(interval, reg));
720 for other in &intervals {
721 if other.vreg != interval.vreg && intervals_interfere(interval, other) {
722 assert_ne!(plan[&other.vreg], Allocation::Register(reg));
723 }
724 }
725 }
726 }
727 }
728 let linear = LinearScanAllocator::allocate_with_constraints(
729 &intervals,
730 &["callee", "caller"],
731 allowed,
732 );
733 assert!(matches!(linear[&intervals[0].vreg], Allocation::Spill(_)));
734 assert_eq!(linear[&intervals[2].vreg], Allocation::Register("callee"));
735 }
736
737 #[test]
738 fn test_compute_intervals_basic() {
739 let func = make_add_function();
740 let intervals = LinearScanAllocator::compute_intervals(&func);
741
742 assert!(!intervals.is_empty());
744 let vreg_ids: Vec<u32> = intervals.iter().map(|i| i.vreg.id).collect();
746 assert!(vreg_ids.contains(&0));
747 assert!(vreg_ids.contains(&1));
748 assert!(vreg_ids.contains(&2));
749 }
750
751 #[test]
752 fn test_compute_intervals_sorted_by_start() {
753 let func = make_add_function();
754 let intervals = LinearScanAllocator::compute_intervals(&func);
755 let starts: Vec<usize> = intervals.iter().map(|i| i.start).collect();
756 let mut sorted = starts.clone();
757 sorted.sort_unstable();
758 assert_eq!(starts, sorted, "intervals should be sorted by start");
759 }
760
761 #[test]
762 fn copy_contributes_source_use_and_destination_def() {
763 let f64_ty = MirType::Scalar(ScalarType::F64);
764 let src = Register::Virtual(VirtualReg::fpr(0));
765 let dst = Register::Virtual(VirtualReg::fpr(1));
766 let func = FunctionBuilder::new("copy")
767 .param(src.clone(), f64_ty)
768 .returns(f64_ty)
769 .block("entry")
770 .instr(Instruction::Copy {
771 ty: f64_ty,
772 dst: dst.clone(),
773 src: Operand::Register(src),
774 })
775 .instr(Instruction::Ret {
776 value: Some(Operand::Register(dst)),
777 })
778 .build();
779
780 let intervals = LinearScanAllocator::compute_intervals(&func);
781 let ids: Vec<u32> = intervals.iter().map(|interval| interval.vreg.id).collect();
782 assert!(ids.contains(&0));
783 assert!(ids.contains(&1));
784 }
785
786 #[test]
787 fn intervals_cover_values_live_across_back_edges() {
788 let invariant = VirtualReg::gpr(0);
789 let temporary = VirtualReg::gpr(1);
790 let mut function = FunctionBuilder::new("loop")
791 .param(Register::Virtual(invariant), MirType::i64())
792 .block("entry")
793 .instr(Instruction::Jmp {
794 target: "header".into(),
795 })
796 .build();
797 let mut header = Block::new("header");
798 header.push(Instruction::Br {
799 cond: Register::Virtual(invariant),
800 true_target: "body".into(),
801 false_target: "exit".into(),
802 });
803 function.add_block(header);
804 let mut exit = Block::new("exit");
805 exit.push(Instruction::Ret { value: None });
806 function.add_block(exit);
807 let mut body = Block::new("body");
808 body.push(Instruction::Copy {
809 ty: MirType::i64(),
810 dst: Register::Virtual(temporary),
811 src: Operand::Immediate(Immediate::I64(42)),
812 });
813 body.push(Instruction::Jmp {
814 target: "header".into(),
815 });
816 function.add_block(body);
817 let intervals = LinearScanAllocator::compute_intervals(&function);
818 let live = intervals
819 .iter()
820 .find(|interval| interval.vreg == invariant)
821 .expect("invariant");
822 assert_eq!((live.start, live.end), (0, 4));
823 for plan in [
824 LinearScanAllocator::allocate(&intervals, &["r12"]),
825 GraphColorAllocator::allocate(&intervals, &["r12"]),
826 ] {
827 assert!(
828 matches!(plan[&invariant], Allocation::Spill(_))
829 || matches!(plan[&temporary], Allocation::Spill(_))
830 );
831 }
832 }
833
834 #[test]
835 fn stack_address_slots_participate_in_intervals() {
836 let slot = VirtualReg::gpr(0);
837 let pointer = VirtualReg::gpr(1);
838 let function = FunctionBuilder::new("address")
839 .block("entry")
840 .instr(Instruction::StackAddr {
841 dst: Register::Virtual(pointer),
842 slot,
843 })
844 .instr(Instruction::Ret {
845 value: Some(Operand::Register(Register::Virtual(pointer))),
846 })
847 .build();
848 let intervals = LinearScanAllocator::compute_intervals(&function);
849 assert_eq!(intervals.len(), 2);
850 assert!(intervals.iter().any(|interval| interval.vreg == slot));
851 }
852
853 #[test]
854 fn test_allocate_fits_in_registers() {
855 let func = make_add_function();
856 let intervals = LinearScanAllocator::compute_intervals(&func);
857 let regs = ["r12", "r13", "r14", "r15"];
858 let alloc = LinearScanAllocator::allocate(&intervals, ®s);
859
860 for interval in &intervals {
862 let a = alloc
863 .get(&interval.vreg)
864 .expect("every vreg should be allocated");
865 assert!(
866 matches!(a, Allocation::Register(_)),
867 "vreg {:?} should be in a register, got {:?}",
868 interval.vreg,
869 a
870 );
871 }
872 }
873
874 #[test]
875 fn test_allocate_spills_when_registers_exhausted() {
876 let i64_ty = MirType::Scalar(ScalarType::I64);
878 let mut func = FunctionBuilder::new("spill_test").returns(i64_ty).build();
879 let mut block = Block::new("entry");
880
881 for i in 0u32..8 {
883 let vi = Register::Virtual(VirtualReg::gpr(i));
884 let vj = Register::Virtual(VirtualReg::gpr(i + 1));
885 let vd = Register::Virtual(VirtualReg::gpr(i + 2));
886 block.push(Instruction::IntBinary {
887 op: IntBinOp::Add,
888 ty: i64_ty,
889 dst: vd,
890 lhs: Operand::Register(vi),
891 rhs: Operand::Register(vj),
892 });
893 }
894 block.push(Instruction::Ret { value: None });
895 func.add_block(block);
896
897 let intervals = LinearScanAllocator::compute_intervals(&func);
898 let regs = ["r12", "r13"]; let alloc = LinearScanAllocator::allocate(&intervals, ®s);
900
901 let has_spill = alloc.values().any(|a| matches!(a, Allocation::Spill(_)));
903 assert!(has_spill, "expected spills when registers are exhausted");
904
905 for a in alloc.values() {
907 if let Allocation::Spill(offset) = a {
908 assert!(*offset < 0, "spill offset should be negative");
909 assert_eq!(offset % 8, 0, "spill offset should be 8-byte aligned");
910 }
911 }
912 }
913
914 #[test]
915 fn graph_color_fits_three_vregs_without_spill() {
916 let func = make_add_function();
917 let intervals = LinearScanAllocator::compute_intervals(&func);
918 let regs = ["r12", "r13", "r14", "r15"];
919 let gc = GraphColorAllocator::allocate(&intervals, ®s);
920 for interval in &intervals {
921 let a = gc.get(&interval.vreg).expect("allocated");
922 assert!(
923 matches!(a, Allocation::Register(_)),
924 "graph color should keep simple add in registers, got {:?}",
925 a
926 );
927 }
928 }
929
930 #[test]
931 fn graph_color_spills_when_register_count_exhausted() {
932 let i64_ty = MirType::Scalar(ScalarType::I64);
933 let mut func = FunctionBuilder::new("spill_gc").returns(i64_ty).build();
934 let mut block = Block::new("entry");
935 for i in 0u32..8 {
936 let vi = Register::Virtual(VirtualReg::gpr(i));
937 let vj = Register::Virtual(VirtualReg::gpr(i + 1));
938 let vd = Register::Virtual(VirtualReg::gpr(i + 2));
939 block.push(Instruction::IntBinary {
940 op: IntBinOp::Add,
941 ty: i64_ty,
942 dst: vd,
943 lhs: Operand::Register(vi),
944 rhs: Operand::Register(vj),
945 });
946 }
947 block.push(Instruction::Ret { value: None });
948 func.add_block(block);
949
950 let intervals = LinearScanAllocator::compute_intervals(&func);
951 let regs = ["r12", "r13"];
952 let gc = GraphColorAllocator::allocate(&intervals, ®s);
953 let has_spill = gc.values().any(|a| matches!(a, Allocation::Spill(_)));
954 assert!(
955 has_spill,
956 "graph color should spill when k=2 and pressure is high"
957 );
958 }
959}