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> {
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.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 debug_assert!(
412 intervals
413 .iter()
414 .all(|interval| interval.start <= interval.end)
415 );
416 let mut starts: Vec<usize> = intervals.iter().map(|interval| interval.start).collect();
417 let mut ends: Vec<usize> = intervals.iter().map(|interval| interval.end).collect();
418 starts.sort_unstable();
419 ends.sort_unstable();
420 let degrees: Vec<usize> = intervals
421 .iter()
422 .map(|interval| {
423 let overlapping_starts = starts.partition_point(|&s| s <= interval.end);
424 let ended_before = ends.partition_point(|&e| e < interval.start);
425 overlapping_starts - ended_before - 1
426 })
427 .collect();
428 let mut order: Vec<usize> = (0..intervals.len()).collect();
429 order.sort_by(|&i, &j| {
430 intervals[j]
431 .weight
432 .cmp(&intervals[i].weight)
433 .then_with(|| degrees[j].cmp(°rees[i]))
434 .then_with(|| intervals[i].end.cmp(&intervals[j].end))
435 .then_with(|| intervals[i].vreg.cmp(&intervals[j].vreg))
436 });
437
438 let mut result: HashMap<VirtualReg, Allocation<R>> = HashMap::new();
439 let mut next_spill: i32 = -8;
440 let mut occupied: HashMap<R, BTreeMap<usize, usize>> = HashMap::new();
441
442 for idx in order {
443 let interval = &intervals[idx];
444 let overlaps = |spans: &BTreeMap<usize, usize>| {
445 spans
446 .range(..=interval.end)
447 .next_back()
448 .is_some_and(|(_, &end)| end >= interval.start)
449 };
450
451 let mut picked: Option<R> = None;
452 for reg in available_regs {
453 let blocked = occupied.get(reg).is_some_and(overlaps);
454 if !blocked && allowed(interval, *reg) {
455 picked = Some(*reg);
456 break;
457 }
458 }
459
460 match picked {
461 Some(r) => {
462 result.insert(interval.vreg, Allocation::Register(r));
463 occupied
464 .entry(r)
465 .or_default()
466 .insert(interval.start, interval.end);
467 }
468 None => {
469 result.insert(interval.vreg, Allocation::Spill(next_spill));
470 next_spill -= 8;
471 }
472 }
473 }
474
475 result
476 }
477}
478
479impl<T> LocalRegisterAllocatorDyn for T
480where
481 T: LocalRegisterAllocator,
482{
483 fn alloc_scratch_dyn(&mut self) -> Option<PhysRegHandle> {
484 self.alloc_scratch().map(PhysRegConvertible::into_handle)
485 }
486
487 fn free_scratch_dyn(&mut self, phys: PhysRegHandle) {
488 if let Some(reg) = <T::PhysReg as PhysRegConvertible>::from_handle(phys) {
489 self.free_scratch(reg);
490 } else {
491 debug_assert!(false, "failed to decode physical register handle");
492 }
493 }
494
495 fn get_mapping_dyn(&self, vreg: &VirtualReg) -> Option<PhysRegHandle> {
496 self.get_mapping(vreg).map(PhysRegConvertible::into_handle)
497 }
498
499 fn ensure_mapping_dyn(&mut self, vreg: VirtualReg) -> Option<PhysRegHandle> {
500 self.ensure_mapping(vreg)
501 .map(PhysRegConvertible::into_handle)
502 }
503
504 fn mapped_for_register_dyn(&self, reg: &Register) -> Option<PhysRegHandle> {
505 self.mapped_for_register(reg)
506 .map(PhysRegConvertible::into_handle)
507 }
508
509 fn occupy_dyn(&mut self, phys: PhysRegHandle) {
510 if let Some(reg) = <T::PhysReg as PhysRegConvertible>::from_handle(phys) {
511 self.occupy(reg);
512 } else {
513 debug_assert!(false, "failed to decode physical register handle");
514 }
515 }
516
517 fn release_dyn(&mut self, phys: PhysRegHandle) {
518 if let Some(reg) = <T::PhysReg as PhysRegConvertible>::from_handle(phys) {
519 self.release(reg);
520 } else {
521 debug_assert!(false, "failed to decode physical register handle");
522 }
523 }
524
525 fn is_occupied_dyn(&self, phys: PhysRegHandle) -> bool {
526 if let Some(reg) = <T::PhysReg as PhysRegConvertible>::from_handle(phys) {
527 self.is_occupied(reg)
528 } else {
529 debug_assert!(false, "failed to decode physical register handle");
530 false
531 }
532 }
533}
534
535#[cfg(test)]
540mod tests {
541 use super::*;
542 use lamina_mir::{
543 Block, FunctionBuilder, Immediate, Instruction, IntBinOp, MirType, Operand, Register,
544 ScalarType, VirtualReg,
545 };
546
547 fn make_add_function() -> Function {
548 let v0 = Register::Virtual(VirtualReg::gpr(0));
554 let v1 = Register::Virtual(VirtualReg::gpr(1));
555 let v2 = Register::Virtual(VirtualReg::gpr(2));
556 let i64_ty = MirType::Scalar(ScalarType::I64);
557
558 FunctionBuilder::new("add")
559 .param(v0.clone(), i64_ty)
560 .param(v1.clone(), i64_ty)
561 .returns(i64_ty)
562 .block("entry")
563 .instr(Instruction::IntBinary {
564 op: IntBinOp::Add,
565 ty: i64_ty,
566 dst: v2.clone(),
567 lhs: Operand::Register(v0),
568 rhs: Operand::Register(v1),
569 })
570 .instr(Instruction::Ret {
571 value: Some(Operand::Register(v2)),
572 })
573 .build()
574 }
575
576 #[test]
577 fn weighted_spills_preserve_hot_values_and_constraints() {
578 let mut intervals = vec![
579 LiveInterval {
580 vreg: VirtualReg::gpr(0),
581 start: 0,
582 end: 100,
583 weight: 1000,
584 },
585 LiveInterval {
586 vreg: VirtualReg::gpr(1),
587 start: 1,
588 end: 80,
589 weight: 10,
590 },
591 LiveInterval {
592 vreg: VirtualReg::gpr(2),
593 start: 2,
594 end: 90,
595 weight: 100,
596 },
597 LiveInterval {
598 vreg: VirtualReg::gpr(3),
599 start: 3,
600 end: 4,
601 weight: 1,
602 },
603 ];
604 for allocate in [LinearScanAllocator::allocate, GraphColorAllocator::allocate] {
605 let plan = allocate(&intervals, &["a", "b"]);
606 assert!(matches!(plan[&intervals[0].vreg], Allocation::Register(_)));
607 assert!(matches!(plan[&intervals[1].vreg], Allocation::Spill(_)));
608 assert!(matches!(plan[&intervals[2].vreg], Allocation::Register(_)));
609 assert!(matches!(plan[&intervals[3].vreg], Allocation::Spill(_)));
610 }
611 intervals[2].weight = 10;
612 let plan = LinearScanAllocator::allocate(&intervals, &["a", "b"]);
613 assert!(matches!(plan[&intervals[2].vreg], Allocation::Spill(_)));
614 let plan =
615 LinearScanAllocator::allocate_with_constraints(&intervals, &["a", "b"], |i, r| {
616 i.vreg.id != 2 || r == "a"
617 });
618 assert!(matches!(plan[&intervals[2].vreg], Allocation::Spill(_)));
619 }
620
621 #[test]
622 fn nested_loop_weights_merge_latches_and_cap_depth() {
623 let value = VirtualReg::gpr(0);
624 let mut function = FunctionBuilder::new("nested")
625 .param(Register::Virtual(value), MirType::Scalar(ScalarType::I64))
626 .block("entry")
627 .instr(Instruction::Jmp {
628 target: "h0".into(),
629 })
630 .build();
631 for depth in 0..8 {
632 let mut block = Block::new(format!("h{depth}"));
633 block.push(Instruction::Br {
634 cond: Register::Virtual(value),
635 true_target: if depth == 7 {
636 "tail".into()
637 } else {
638 format!("h{}", depth + 1)
639 },
640 false_target: if depth == 0 {
641 "exit".into()
642 } else {
643 format!("h{}", depth - 1)
644 },
645 });
646 function.add_block(block);
647 }
648 let mut tail = Block::new("tail");
649 tail.push(Instruction::Br {
650 cond: Register::Virtual(value),
651 true_target: "h7".into(),
652 false_target: "tail2".into(),
653 });
654 function.add_block(tail);
655 let mut tail2 = Block::new("tail2");
656 tail2.push(Instruction::Jmp {
657 target: "h7".into(),
658 });
659 function.add_block(tail2);
660 let mut exit = Block::new("exit");
661 exit.push(Instruction::Ret {
662 value: Some(Operand::Register(Register::Virtual(value))),
663 });
664 function.add_block(exit);
665 let mut dead = Block::new("dead");
666 dead.push(Instruction::Jmp {
667 target: "dead".into(),
668 });
669 function.add_block(dead);
670 assert_eq!(
671 loop_depths(&function),
672 vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 8, 8, 0, 0]
673 );
674 let intervals = LinearScanAllocator::compute_intervals(&function);
675 assert_eq!(
676 intervals[0].weight,
677 10 + 100 + 1000 + 10000 + 100000 + 4 * 1000000 + 2
678 );
679 }
680
681 #[test]
682 fn constrained_allocators_respect_fixed_registers_and_eviction() {
683 let intervals = [
684 LiveInterval {
685 vreg: VirtualReg::gpr(0),
686 start: 0,
687 end: 10,
688 weight: 1,
689 },
690 LiveInterval {
691 vreg: VirtualReg::gpr(1),
692 start: 1,
693 end: 9,
694 weight: 1,
695 },
696 LiveInterval {
697 vreg: VirtualReg::gpr(2),
698 start: 2,
699 end: 3,
700 weight: 1,
701 },
702 LiveInterval {
703 vreg: VirtualReg::gpr(3),
704 start: 3,
705 end: 4,
706 weight: 1,
707 },
708 ];
709 let allowed = |interval: &LiveInterval, reg| match interval.vreg.id {
710 0 | 2 => reg == "callee",
711 1 => reg == "caller",
712 _ => false,
713 };
714 for plan in [
715 LinearScanAllocator::allocate_with_constraints(
716 &intervals,
717 &["callee", "caller"],
718 allowed,
719 ),
720 GraphColorAllocator::allocate_with_constraints(
721 &intervals,
722 &["callee", "caller"],
723 allowed,
724 ),
725 ] {
726 assert!(matches!(plan[&intervals[3].vreg], Allocation::Spill(_)));
727 for interval in &intervals {
728 if let Allocation::Register(reg) = plan[&interval.vreg] {
729 assert!(allowed(interval, reg));
730 for other in &intervals {
731 if other.vreg != interval.vreg && intervals_interfere(interval, other) {
732 assert_ne!(plan[&other.vreg], Allocation::Register(reg));
733 }
734 }
735 }
736 }
737 }
738 let linear = LinearScanAllocator::allocate_with_constraints(
739 &intervals,
740 &["callee", "caller"],
741 allowed,
742 );
743 assert!(matches!(linear[&intervals[0].vreg], Allocation::Spill(_)));
744 assert_eq!(linear[&intervals[2].vreg], Allocation::Register("callee"));
745 }
746
747 #[test]
748 fn test_compute_intervals_basic() {
749 let func = make_add_function();
750 let intervals = LinearScanAllocator::compute_intervals(&func);
751
752 assert!(!intervals.is_empty());
754 let vreg_ids: Vec<u32> = intervals.iter().map(|i| i.vreg.id).collect();
756 assert!(vreg_ids.contains(&0));
757 assert!(vreg_ids.contains(&1));
758 assert!(vreg_ids.contains(&2));
759 }
760
761 #[test]
762 fn test_compute_intervals_sorted_by_start() {
763 let func = make_add_function();
764 let intervals = LinearScanAllocator::compute_intervals(&func);
765 let starts: Vec<usize> = intervals.iter().map(|i| i.start).collect();
766 let mut sorted = starts.clone();
767 sorted.sort_unstable();
768 assert_eq!(starts, sorted, "intervals should be sorted by start");
769 }
770
771 #[test]
772 fn copy_contributes_source_use_and_destination_def() {
773 let f64_ty = MirType::Scalar(ScalarType::F64);
774 let src = Register::Virtual(VirtualReg::fpr(0));
775 let dst = Register::Virtual(VirtualReg::fpr(1));
776 let func = FunctionBuilder::new("copy")
777 .param(src.clone(), f64_ty)
778 .returns(f64_ty)
779 .block("entry")
780 .instr(Instruction::Copy {
781 ty: f64_ty,
782 dst: dst.clone(),
783 src: Operand::Register(src),
784 })
785 .instr(Instruction::Ret {
786 value: Some(Operand::Register(dst)),
787 })
788 .build();
789
790 let intervals = LinearScanAllocator::compute_intervals(&func);
791 let ids: Vec<u32> = intervals.iter().map(|interval| interval.vreg.id).collect();
792 assert!(ids.contains(&0));
793 assert!(ids.contains(&1));
794 }
795
796 #[test]
797 fn intervals_cover_values_live_across_back_edges() {
798 let invariant = VirtualReg::gpr(0);
799 let temporary = VirtualReg::gpr(1);
800 let mut function = FunctionBuilder::new("loop")
801 .param(
802 Register::Virtual(invariant),
803 MirType::Scalar(ScalarType::I64),
804 )
805 .block("entry")
806 .instr(Instruction::Jmp {
807 target: "header".into(),
808 })
809 .build();
810 let mut header = Block::new("header");
811 header.push(Instruction::Br {
812 cond: Register::Virtual(invariant),
813 true_target: "body".into(),
814 false_target: "exit".into(),
815 });
816 function.add_block(header);
817 let mut exit = Block::new("exit");
818 exit.push(Instruction::Ret { value: None });
819 function.add_block(exit);
820 let mut body = Block::new("body");
821 body.push(Instruction::Copy {
822 ty: MirType::Scalar(ScalarType::I64),
823 dst: Register::Virtual(temporary),
824 src: Operand::Immediate(Immediate::I64(42)),
825 });
826 body.push(Instruction::Jmp {
827 target: "header".into(),
828 });
829 function.add_block(body);
830 let intervals = LinearScanAllocator::compute_intervals(&function);
831 let live = intervals
832 .iter()
833 .find(|interval| interval.vreg == invariant)
834 .expect("invariant");
835 assert_eq!((live.start, live.end), (0, 4));
836 for plan in [
837 LinearScanAllocator::allocate(&intervals, &["r12"]),
838 GraphColorAllocator::allocate(&intervals, &["r12"]),
839 ] {
840 assert!(
841 matches!(plan[&invariant], Allocation::Spill(_))
842 || matches!(plan[&temporary], Allocation::Spill(_))
843 );
844 }
845 }
846
847 #[test]
848 fn stack_address_slots_participate_in_intervals() {
849 let slot = VirtualReg::gpr(0);
850 let pointer = VirtualReg::gpr(1);
851 let function = FunctionBuilder::new("address")
852 .block("entry")
853 .instr(Instruction::StackAddr {
854 dst: Register::Virtual(pointer),
855 slot,
856 })
857 .instr(Instruction::Ret {
858 value: Some(Operand::Register(Register::Virtual(pointer))),
859 })
860 .build();
861 let intervals = LinearScanAllocator::compute_intervals(&function);
862 assert_eq!(intervals.len(), 2);
863 assert!(intervals.iter().any(|interval| interval.vreg == slot));
864 }
865
866 #[test]
867 fn test_allocate_fits_in_registers() {
868 let func = make_add_function();
869 let intervals = LinearScanAllocator::compute_intervals(&func);
870 let regs = ["r12", "r13", "r14", "r15"];
871 let alloc = LinearScanAllocator::allocate(&intervals, ®s);
872
873 for interval in &intervals {
875 let a = alloc
876 .get(&interval.vreg)
877 .expect("every vreg should be allocated");
878 assert!(
879 matches!(a, Allocation::Register(_)),
880 "vreg {:?} should be in a register, got {:?}",
881 interval.vreg,
882 a
883 );
884 }
885 }
886
887 #[test]
888 fn test_allocate_spills_when_registers_exhausted() {
889 let i64_ty = MirType::Scalar(ScalarType::I64);
891 let mut func = FunctionBuilder::new("spill_test").returns(i64_ty).build();
892 let mut block = Block::new("entry");
893
894 for i in 0u32..8 {
896 let vi = Register::Virtual(VirtualReg::gpr(i));
897 let vj = Register::Virtual(VirtualReg::gpr(i + 1));
898 let vd = Register::Virtual(VirtualReg::gpr(i + 2));
899 block.push(Instruction::IntBinary {
900 op: IntBinOp::Add,
901 ty: i64_ty,
902 dst: vd,
903 lhs: Operand::Register(vi),
904 rhs: Operand::Register(vj),
905 });
906 }
907 block.push(Instruction::Ret { value: None });
908 func.add_block(block);
909
910 let intervals = LinearScanAllocator::compute_intervals(&func);
911 let regs = ["r12", "r13"]; let alloc = LinearScanAllocator::allocate(&intervals, ®s);
913
914 let has_spill = alloc.values().any(|a| matches!(a, Allocation::Spill(_)));
916 assert!(has_spill, "expected spills when registers are exhausted");
917
918 for a in alloc.values() {
920 if let Allocation::Spill(offset) = a {
921 assert!(*offset < 0, "spill offset should be negative");
922 assert_eq!(offset % 8, 0, "spill offset should be 8-byte aligned");
923 }
924 }
925 }
926
927 #[test]
928 fn graph_color_fits_three_vregs_without_spill() {
929 let func = make_add_function();
930 let intervals = LinearScanAllocator::compute_intervals(&func);
931 let regs = ["r12", "r13", "r14", "r15"];
932 let gc = GraphColorAllocator::allocate(&intervals, ®s);
933 for interval in &intervals {
934 let a = gc.get(&interval.vreg).expect("allocated");
935 assert!(
936 matches!(a, Allocation::Register(_)),
937 "graph color should keep simple add in registers, got {:?}",
938 a
939 );
940 }
941 }
942
943 #[test]
944 fn graph_color_spills_when_register_count_exhausted() {
945 let i64_ty = MirType::Scalar(ScalarType::I64);
946 let mut func = FunctionBuilder::new("spill_gc").returns(i64_ty).build();
947 let mut block = Block::new("entry");
948 for i in 0u32..8 {
949 let vi = Register::Virtual(VirtualReg::gpr(i));
950 let vj = Register::Virtual(VirtualReg::gpr(i + 1));
951 let vd = Register::Virtual(VirtualReg::gpr(i + 2));
952 block.push(Instruction::IntBinary {
953 op: IntBinOp::Add,
954 ty: i64_ty,
955 dst: vd,
956 lhs: Operand::Register(vi),
957 rhs: Operand::Register(vj),
958 });
959 }
960 block.push(Instruction::Ret { value: None });
961 func.add_block(block);
962
963 let intervals = LinearScanAllocator::compute_intervals(&func);
964 let regs = ["r12", "r13"];
965 let gc = GraphColorAllocator::allocate(&intervals, ®s);
966 let has_spill = gc.values().any(|a| matches!(a, Allocation::Spill(_)));
967 assert!(
968 has_spill,
969 "graph color should spill when k=2 and pressure is high"
970 );
971 }
972}