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::{BTreeMap, 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.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        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(&degrees[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// ---------------------------------------------------------------------------
536// Tests
537// ---------------------------------------------------------------------------
538
539#[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        // fn add(v0: i64, v1: i64) -> i64 {
549        // entry:
550        //   v2 = add v0, v1
551        //   ret v2
552        // }
553        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        // v0, v1 are params (start=0), v2 is defined at instruction 0
753        assert!(!intervals.is_empty());
754        // All three vregs should appear
755        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, &regs);
872
873        // With 3 vregs and 4 registers, no spills should occur.
874        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        // Build a function with more vregs than available registers.
890        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        // 8 vregs all live simultaneously: v0+v1 → v2, v3+v4 → v5, v6+v7 → v8
895        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"]; // only 2 registers
912        let alloc = LinearScanAllocator::allocate(&intervals, &regs);
913
914        // Some vregs must be spilled since we have more live vregs than registers.
915        let has_spill = alloc.values().any(|a| matches!(a, Allocation::Spill(_)));
916        assert!(has_spill, "expected spills when registers are exhausted");
917
918        // All spill offsets should be multiples of 8 and negative.
919        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, &regs);
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, &regs);
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}