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