Skip to main content

lamina_codegen/
regalloc.rs

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/// Opaque handle that allows dynamic dispatch over register allocators without
9/// leaking architecture-specific physical register types.
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
11pub enum PhysRegHandle {
12    /// Physical registers represented by their canonical assembly name.
13    Named(&'static str),
14}
15
16impl PhysRegHandle {
17    /// Retrieve the assembly name if the handle stores one.
18    pub fn as_named(self) -> Option<&'static str> {
19        match self {
20            PhysRegHandle::Named(name) => Some(name),
21        }
22    }
23}
24
25/// Conversion helpers that allow a concrete physical register type to be
26/// converted to/from a [`PhysRegHandle`] for dynamic dispatch.
27pub trait PhysRegConvertible: Copy + Eq {
28    /// Convert the register into an opaque handle.
29    fn into_handle(self) -> PhysRegHandle;
30
31    /// Try to rebuild the register from an opaque handle.
32    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
49/// Target-facing interface for MIR register allocation (per-function/incremental).
50///
51/// The trait stays purposefully small: code generators typically need a
52/// lightweight scratch register pool, a stable mapping from virtual to
53/// physical registers, and explicit hooks to reserve or release physical
54/// registers that are pre-coloured by the ABI. Architecture backends can build
55/// richer policies on top of this contract without forcing every target to
56/// adopt the same strategy.
57pub trait LocalRegisterAllocator {
58    /// Architecture-specific physical register handle.
59    type PhysReg: PhysRegConvertible;
60
61    /// Acquire a short-lived scratch register. Returns `None` when the dedicated
62    /// pool is exhausted so the caller may spill or choose an alternate path.
63    fn alloc_scratch(&mut self) -> Option<Self::PhysReg>;
64
65    /// Release a scratch register obtained through [`LocalRegisterAllocator::alloc_scratch`].
66    fn free_scratch(&mut self, phys: Self::PhysReg);
67
68    /// Look up the physical register currently assigned to the virtual
69    /// register, when available.
70    fn get_mapping(&self, vreg: &VirtualReg) -> Option<Self::PhysReg>;
71
72    /// Ensure that the virtual register has a permanent mapping. Implementers
73    /// can reject unsupported register classes by returning `None`, signalling
74    /// that the caller should spill.
75    fn ensure_mapping(&mut self, vreg: VirtualReg) -> Option<Self::PhysReg>;
76
77    /// Resolve the backing physical register for an arbitrary MIR register
78    /// (virtual or physical).
79    fn mapped_for_register(&self, reg: &Register) -> Option<Self::PhysReg>;
80
81    /// Mark a physical register as occupied, removing it from the allocator's
82    /// free pool if necessary.
83    fn occupy(&mut self, phys: Self::PhysReg);
84
85    /// Release a previously occupied physical register back to the pool.
86    fn release(&mut self, phys: Self::PhysReg);
87
88    /// Test whether the allocator currently treats the physical register as
89    /// occupied.
90    fn is_occupied(&self, phys: Self::PhysReg) -> bool;
91}
92
93/// Backward-compatible alias for [`LocalRegisterAllocator`].
94pub use LocalRegisterAllocator as RegisterAllocator;
95
96/// Object-safe wrapper around [`LocalRegisterAllocator`] permitting dynamic dispatch
97/// via `dyn LocalRegisterAllocatorDyn`.
98pub 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
109/// Backward-compatible alias for [`LocalRegisterAllocatorDyn`].
110pub use LocalRegisterAllocatorDyn as RegisterAllocatorDyn;
111
112/// Result of allocating a virtual register: either a physical register or a
113/// stack spill slot (byte offset from the frame base pointer).
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum Allocation<R: Copy> {
116    Register(R),
117    /// Byte offset from the frame base (negative = below the saved frame pointer).
118    Spill(i32),
119}
120
121/// Live interval: the range `[start, end]` of instruction indices (0-based,
122/// counting sequentially across all blocks in program order) during which
123/// `vreg` must be kept alive.
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub struct LiveInterval {
126    pub vreg: VirtualReg,
127    /// Index of the instruction that first defines `vreg`.
128    pub start: usize,
129    /// Index of the last instruction that uses `vreg`.
130    pub end: usize,
131    /// Estimated execution frequency summed over uses and definitions.
132    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
179/// Classic linear scan register allocator (Poletto & Sarkar, 1999).
180///
181/// Usage:
182/// ```ignore
183/// let intervals = LinearScanAllocator::compute_intervals(&function);
184/// let free_regs = vec!["r12", "r13", "r14", "r15", "rbx"];
185/// let alloc_map = LinearScanAllocator::allocate(&intervals, &free_regs);
186/// ```
187pub struct LinearScanAllocator;
188
189impl LinearScanAllocator {
190    /// Compute interval hulls from instruction operands and control-flow liveness.
191    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    /// Run the linear scan allocation algorithm.
291    ///
292    /// `intervals` must be sorted by `start` (as returned by `compute_intervals`).
293    /// `available_regs` is the pool of physical registers to draw from (any `Copy + Eq`
294    /// type works — for x86_64 pass `&["r12", "r13", ...]`).
295    ///
296    /// Returns a map from each `VirtualReg` to either a physical register or a
297    /// spill slot (negative byte offset from frame base).
298    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    /// Allocate registers permitted by each interval's target constraints.
306    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        // Active intervals: sorted by end point (ascending).
313        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; // first spill at [rbp - 8]
316
317        for interval in intervals {
318            // Expire intervals that ended before this one started.
319            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                // Assign a free register.
334                result.insert(interval.vreg, Allocation::Register(reg));
335                // Insert into active, keeping it sorted by end point.
336                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                        // Spill the existing active interval; give its register to `interval`.
354                        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                        // Spill the current interval.
365                        result.insert(interval.vreg, Allocation::Spill(next_spill));
366                        next_spill -= 8;
367                    }
368                }
369            }
370        }
371
372        result
373    }
374}
375
376/// Interference check for live intervals: overlap on the global instruction index line.
377#[inline]
378pub fn intervals_interfere(a: &LiveInterval, b: &LiveInterval) -> bool {
379    a.start <= b.end && b.start <= a.end
380}
381
382/// Greedy graph-coloring register allocator (interference graph from live intervals).
383///
384/// Intervals that overlap cannot share a register. Nodes are colored in **descending
385/// weight**, then descending interference degree to reduce spill pressure.
386/// When no color is available among `available_regs`, the interval is spilled using the
387/// same slot layout as [`LinearScanAllocator::allocate`].
388pub struct GraphColorAllocator;
389
390impl GraphColorAllocator {
391    /// Color `intervals` using at most `available_regs.len()` registers.
392    ///
393    /// `intervals` may be in any order; internally sorted by weight and interference degree.
394    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    /// Color intervals using registers permitted by target constraints.
402    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(&deg_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// ---------------------------------------------------------------------------
526// Tests
527// ---------------------------------------------------------------------------
528
529#[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        // fn add(v0: i64, v1: i64) -> i64 {
539        // entry:
540        //   v2 = add v0, v1
541        //   ret v2
542        // }
543        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        // v0, v1 are params (start=0), v2 is defined at instruction 0
743        assert!(!intervals.is_empty());
744        // All three vregs should appear
745        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, &regs);
859
860        // With 3 vregs and 4 registers, no spills should occur.
861        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        // Build a function with more vregs than available registers.
877        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        // 8 vregs all live simultaneously: v0+v1 → v2, v3+v4 → v5, v6+v7 → v8
882        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"]; // only 2 registers
899        let alloc = LinearScanAllocator::allocate(&intervals, &regs);
900
901        // Some vregs must be spilled since we have more live vregs than registers.
902        let has_spill = alloc.values().any(|a| matches!(a, Allocation::Spill(_)));
903        assert!(has_spill, "expected spills when registers are exhausted");
904
905        // All spill offsets should be multiples of 8 and negative.
906        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, &regs);
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, &regs);
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}