Skip to main content

llvm_codegen/
regalloc.rs

1//! Linear-scan register allocator.
2//!
3//! Reference: Poletto & Sarkar, "Linear Scan Register Allocation", TOPLAS 1999.
4//!
5//! Algorithm:
6//! 1. Compute per-VReg live intervals by a single pass over all instructions.
7//! 2. Sort intervals by start point.
8//! 3. Scan forward: maintain an "active" set sorted by end point.
9//!    - Expire intervals whose end ≤ current start (return their regs to free pool).
10//!    - If a free register exists, assign it.
11//!    - Otherwise spill the interval with the largest end point.
12
13use crate::isel::{MInstr, MOpcode, MOperand, MachineFunction, PReg, VReg};
14/// Public API for `re-export`.
15pub use crate::regalloc_gc::graph_color;
16use std::collections::HashMap;
17
18// ── live intervals ─────────────────────────────────────────────────────────
19
20/// Half-open live interval `[start, end)` in flat instruction program order.
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct LiveInterval {
23    /// Public API for `vreg`.
24    pub vreg: VReg,
25    /// Index of the first instruction that defines (or uses) this vreg.
26    pub start: usize,
27    /// One past the index of the last instruction that uses this vreg.
28    pub end: usize,
29}
30
31/// Compute flat program-order starting positions for each block.
32fn block_starts(mf: &MachineFunction) -> Vec<usize> {
33    let mut starts = Vec::with_capacity(mf.blocks.len());
34    let mut pos = 0;
35    for b in &mf.blocks {
36        starts.push(pos);
37        pos += b.instrs.len();
38    }
39    starts
40}
41
42/// Build live intervals by scanning all instructions of `mf`.
43pub fn compute_live_intervals(mf: &MachineFunction) -> Vec<LiveInterval> {
44    let _ = block_starts(mf); // kept for potential future use
45    let mut map: HashMap<VReg, (usize, usize)> = HashMap::new();
46    let mut pos = 0usize;
47
48    for block in &mf.blocks {
49        for instr in &block.instrs {
50            // Definition extends interval to at least [pos, pos+1).
51            if let Some(dst) = instr.dst {
52                let e = map.entry(dst).or_insert((pos, pos + 1));
53                if pos < e.0 {
54                    e.0 = pos;
55                }
56                if pos + 1 > e.1 {
57                    e.1 = pos + 1;
58                }
59            }
60            // Uses extend the end of the interval.
61            for op in &instr.operands {
62                if let MOperand::VReg(vr) = op {
63                    let e = map.entry(*vr).or_insert((pos, pos + 1));
64                    if pos < e.0 {
65                        e.0 = pos;
66                    }
67                    if pos + 1 > e.1 {
68                        e.1 = pos + 1;
69                    }
70                }
71            }
72            pos += 1;
73        }
74    }
75
76    let mut intervals: Vec<LiveInterval> = map
77        .into_iter()
78        .map(|(vreg, (start, end))| LiveInterval { vreg, start, end })
79        .collect();
80    intervals.sort_by_key(|i| (i.start, i.end, i.vreg.0));
81    intervals
82}
83
84// ── register allocation result ─────────────────────────────────────────────
85
86/// Maps each VReg to the PReg it was assigned, or records it as spilled.
87#[derive(Debug, Default)]
88pub struct RegAllocResult {
89    /// Public API for `vreg_to_preg`.
90    pub vreg_to_preg: HashMap<VReg, PReg>,
91    /// VRegs for which no physical register was available.
92    pub spilled: Vec<VReg>,
93}
94
95/// Register allocation strategy used by [`allocate_registers`].
96#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
97pub enum RegAllocStrategy {
98    #[default]
99    LinearScan,
100    /// `GraphColor` variant.
101    GraphColor,
102}
103
104/// Allocate registers with the selected strategy.
105pub fn allocate_registers(
106    intervals: &[LiveInterval],
107    allocatable: &[PReg],
108    strategy: RegAllocStrategy,
109) -> RegAllocResult {
110    match strategy {
111        RegAllocStrategy::LinearScan => linear_scan(intervals, allocatable),
112        RegAllocStrategy::GraphColor => graph_color(intervals, allocatable),
113    }
114}
115
116// ── linear scan ────────────────────────────────────────────────────────────
117
118/// Run linear-scan allocation over `intervals` using `allocatable` registers.
119///
120/// Spilled VRegs are recorded in [`RegAllocResult::spilled`].
121pub fn linear_scan(intervals: &[LiveInterval], allocatable: &[PReg]) -> RegAllocResult {
122    if allocatable.is_empty() {
123        return RegAllocResult {
124            vreg_to_preg: HashMap::new(),
125            spilled: intervals.iter().map(|i| i.vreg).collect(),
126        };
127    }
128
129    // Sort by a full stable key. `compute_live_intervals` internally uses a
130    // HashMap, so equal-start intervals must not inherit randomized map order.
131    let mut sorted: Vec<&LiveInterval> = intervals.iter().collect();
132    sorted.sort_by_key(|i| (i.start, i.end, i.vreg.0));
133
134    let mut free: Vec<PReg> = allocatable.to_vec();
135    // Active set: (end, vreg, assigned_preg), sorted by end.
136    let mut active: Vec<(usize, VReg, PReg)> = Vec::new();
137    let mut result = RegAllocResult::default();
138
139    for interval in &sorted {
140        // Expire intervals that ended before the current start.
141        let mut returned = Vec::new();
142        active.retain(|&(end, _vr, pr)| {
143            if end <= interval.start {
144                returned.push(pr);
145                false
146            } else {
147                true
148            }
149        });
150        free.extend(returned);
151
152        if free.is_empty() {
153            // Spill: the active set is kept sorted by end, so the interval with
154            // the largest end is always at the back — O(1) lookup instead of O(n).
155            let spill_idx = if active.is_empty() {
156                None
157            } else {
158                Some(active.len() - 1)
159            };
160
161            if let Some(idx) = spill_idx {
162                let (spill_end, spill_vr, spill_pr) = active[idx];
163                if spill_end > interval.end {
164                    // Spill the active interval; reclaim its register for current.
165                    active.remove(idx);
166                    result.vreg_to_preg.remove(&spill_vr); // revoke previous assignment
167                    result.vreg_to_preg.insert(interval.vreg, spill_pr);
168                    // Insert in sorted position to maintain invariant.
169                    let pos = active.partition_point(|&(e, vr, _)| {
170                        (e, vr.0) <= (interval.end, interval.vreg.0)
171                    });
172                    active.insert(pos, (interval.end, interval.vreg, spill_pr));
173                    result.spilled.push(spill_vr);
174                } else {
175                    // Current interval has the largest end — spill it.
176                    result.spilled.push(interval.vreg);
177                }
178            } else {
179                result.spilled.push(interval.vreg);
180            }
181        } else {
182            let pr = free.remove(0);
183            result.vreg_to_preg.insert(interval.vreg, pr);
184            // Insert in sorted position to maintain the end-sorted invariant without
185            // a full O(n log n) sort on every push.
186            let pos = active.partition_point(|&(e, vr, _)| {
187                (e, vr.0) <= (interval.end, interval.vreg.0)
188            });
189            active.insert(pos, (interval.end, interval.vreg, pr));
190        }
191    }
192
193    result
194}
195
196// ── spill/reload insertion ─────────────────────────────────────────────────
197
198/// Insert spill stores and reload loads for all spilled VRegs.
199///
200/// For each spilled VReg `v`:
201/// - After every instruction that **defines** `v`, insert a STORE of `v` to
202///   its frame slot using a fresh VReg.
203/// - Before every instruction that **uses** `v`, insert a LOAD from its frame
204///   slot into a fresh VReg, and replace the use with that fresh VReg.
205///
206/// Fresh VRegs are then allocated in a second linear-scan pass (they have very
207/// short live intervals so they virtually never cause additional spills).  The
208/// result is merged into `result`.
209///
210/// `load_op` / `store_op` are target-provided opcodes.  The instructions
211/// have the layout:
212/// - LOAD:  `dst = VReg(fresh), operands = [Imm(slot)]`
213/// - STORE: `dst = None,        operands = [Imm(slot), VReg(src)]`
214pub fn insert_spill_reloads(
215    mf: &mut MachineFunction,
216    result: &mut RegAllocResult,
217    load_op: MOpcode,
218    store_op: MOpcode,
219) {
220    if result.spilled.is_empty() {
221        return;
222    }
223
224    // Collect spilled VRegs and assign each a frame slot.
225    let spilled: Vec<VReg> = result.spilled.drain(..).collect();
226    for &vr in &spilled {
227        mf.alloc_spill_slot(vr);
228    }
229
230    // Rebuild every block's instruction list with loads/stores inserted.
231    for bi in 0..mf.blocks.len() {
232        let old_instrs: Vec<MInstr> = std::mem::take(&mut mf.blocks[bi].instrs);
233        let mut new_instrs: Vec<MInstr> = Vec::with_capacity(old_instrs.len() * 2);
234
235        for mut instr in old_instrs {
236            // 1) Before the instruction: reload every spilled VReg that appears
237            //    as a source operand, replacing the operand with a fresh VReg.
238            for op in &mut instr.operands {
239                if let MOperand::VReg(vr) = op {
240                    if let Some(&slot) = mf.spill_slots.get(vr) {
241                        let fresh = mf.fresh_vreg();
242                        // Insert LOAD before this instruction.
243                        new_instrs.push(MInstr::new(load_op).with_dst(fresh).with_imm(slot as i64));
244                        *op = MOperand::VReg(fresh);
245                    }
246                }
247            }
248
249            // 2) If the instruction defines a spilled VReg, rewrite dst to a
250            //    fresh VReg, emit the instruction, then insert a STORE.
251            let store_after = if let Some(dst_vr) = instr.dst {
252                if let Some(&slot) = mf.spill_slots.get(&dst_vr) {
253                    let fresh = mf.fresh_vreg();
254                    instr.dst = Some(fresh);
255                    Some((fresh, slot))
256                } else {
257                    None
258                }
259            } else {
260                None
261            };
262
263            new_instrs.push(instr);
264
265            if let Some((fresh, slot)) = store_after {
266                new_instrs.push(MInstr::new(store_op).with_imm(slot as i64).with_vreg(fresh));
267            }
268        }
269
270        mf.blocks[bi].instrs = new_instrs;
271    }
272
273    // Second allocation pass for the fresh VRegs just created.
274    let fresh_intervals = compute_live_intervals(mf);
275    // Only allocate the truly-fresh VRegs (those not yet in result).
276    let fresh_only: Vec<LiveInterval> = fresh_intervals
277        .into_iter()
278        .filter(|iv| !result.vreg_to_preg.contains_key(&iv.vreg))
279        .collect();
280
281    if !fresh_only.is_empty() {
282        let alloc = mf.allocatable_pregs.clone();
283        let second = linear_scan(&fresh_only, &alloc);
284        // Merge — fresh VRegs should not spill given their tiny intervals.
285        for (vr, pr) in second.vreg_to_preg {
286            result.vreg_to_preg.insert(vr, pr);
287        }
288        // Any unexpected spills from fresh VRegs: just assign the first allocatable reg.
289        if !second.spilled.is_empty() {
290            let fallback = alloc.first().copied().unwrap_or(PReg(0));
291            for vr in second.spilled {
292                result.vreg_to_preg.entry(vr).or_insert(fallback);
293            }
294        }
295    }
296}
297
298// ── apply allocation ───────────────────────────────────────────────────────
299
300/// Replace `MOperand::VReg` with `MOperand::PReg` in `mf` according to
301/// `result`.  Also rewrites `instr.dst` so that the destination register
302/// number reflects the assigned physical register rather than the raw VReg
303/// counter.  Spilled VRegs are left unchanged (the caller must handle them).
304///
305/// Also populates `mf.used_callee_saved` with any callee-saved registers
306/// that were actually assigned during allocation.
307pub fn apply_allocation(mf: &mut MachineFunction, result: &RegAllocResult) {
308    // Collect callee-saved registers that are actually used.
309    let callee_saved: std::collections::HashSet<PReg> =
310        mf.callee_saved_pregs.iter().copied().collect();
311    let mut used_cs: std::collections::HashSet<PReg> = std::collections::HashSet::new();
312
313    for &pr in result.vreg_to_preg.values() {
314        if callee_saved.contains(&pr) {
315            used_cs.insert(pr);
316        }
317    }
318    mf.used_callee_saved = used_cs.into_iter().collect();
319    // Keep a stable order matching the original callee_saved_pregs list.
320    let order: HashMap<PReg, usize> = mf
321        .callee_saved_pregs
322        .iter()
323        .enumerate()
324        .map(|(i, &p)| (p, i))
325        .collect();
326    mf.used_callee_saved
327        .sort_by_key(|p| order.get(p).copied().unwrap_or(usize::MAX));
328
329    for block in &mut mf.blocks {
330        for instr in &mut block.instrs {
331            // Rewrite destination VReg.
332            if let Some(ref mut vr) = instr.dst {
333                if let Some(&pr) = result.vreg_to_preg.get(vr) {
334                    // Store the physical register number in the VReg wrapper
335                    // so encoder casts `PReg(dst.0 as u8)` yield the correct reg.
336                    *vr = VReg(pr.0 as u32);
337                }
338            }
339            // Rewrite source operands.
340            for op in &mut instr.operands {
341                if let MOperand::VReg(vr) = op {
342                    if let Some(&pr) = result.vreg_to_preg.get(vr) {
343                        *op = MOperand::PReg(pr);
344                    }
345                }
346            }
347        }
348    }
349}
350
351// ── tests ──────────────────────────────────────────────────────────────────
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356
357    fn iv(vreg: u32, start: usize, end: usize) -> LiveInterval {
358        LiveInterval {
359            vreg: VReg(vreg),
360            start,
361            end,
362        }
363    }
364
365
366    #[test]
367    fn compute_intervals_returns_deterministic_order_for_equal_starts() {
368        use crate::isel::{MInstr, MOpcode, MachineFunction};
369        let mut mf = MachineFunction::new("f".into());
370        let b = mf.add_block("entry");
371        let v0 = mf.fresh_vreg();
372        let v1 = mf.fresh_vreg();
373        let v2 = mf.fresh_vreg();
374
375        mf.push(
376            b,
377            MInstr::new(MOpcode(0))
378                .with_dst(v2)
379                .with_vreg(v1)
380                .with_vreg(v0),
381        );
382
383        let intervals = compute_live_intervals(&mf);
384        let keys: Vec<(usize, usize, u32)> = intervals
385            .iter()
386            .map(|iv| (iv.start, iv.end, iv.vreg.0))
387            .collect();
388        assert_eq!(keys, vec![(0, 1, 0), (0, 1, 1), (0, 1, 2)]);
389    }
390
391    #[test]
392    fn linear_scan_tie_breaks_equal_start_intervals_by_vreg() {
393        let intervals = vec![iv(2, 0, 1), iv(0, 0, 1), iv(1, 0, 1)];
394        let alloc = vec![PReg(0), PReg(1)];
395        let result = linear_scan(&intervals, &alloc);
396
397        assert_eq!(result.vreg_to_preg[&VReg(0)], PReg(0));
398        assert_eq!(result.vreg_to_preg[&VReg(1)], PReg(1));
399        assert_eq!(result.spilled, vec![VReg(2)]);
400    }
401
402    #[test]
403    fn two_non_overlapping_one_reg() {
404        // [0,2) and [3,5) — only one register needed.
405        let intervals = vec![iv(0, 0, 2), iv(1, 3, 5)];
406        let alloc = vec![PReg(0)];
407        let result = linear_scan(&intervals, &alloc);
408        assert!(result.spilled.is_empty(), "no spills expected");
409        assert_eq!(result.vreg_to_preg.len(), 2);
410        assert_eq!(result.vreg_to_preg[&VReg(0)], PReg(0));
411        assert_eq!(result.vreg_to_preg[&VReg(1)], PReg(0));
412    }
413
414    #[test]
415    fn two_overlapping_one_register_causes_spill() {
416        // [0,10) and [1,8) both want a register but only one is available.
417        let intervals = vec![iv(0, 0, 10), iv(1, 1, 8)];
418        let alloc = vec![PReg(0)];
419        let result = linear_scan(&intervals, &alloc);
420        assert_eq!(result.spilled.len(), 1, "exactly one must spill");
421        assert_eq!(result.vreg_to_preg.len(), 1);
422    }
423
424    #[test]
425    fn three_intervals_two_registers() {
426        // Intervals [0,3), [1,4), [5,8) — max pressure = 2.
427        let intervals = vec![iv(0, 0, 3), iv(1, 1, 4), iv(2, 5, 8)];
428        let alloc = vec![PReg(0), PReg(1)];
429        let result = linear_scan(&intervals, &alloc);
430        assert!(result.spilled.is_empty());
431        assert_eq!(result.vreg_to_preg.len(), 3);
432    }
433
434    #[test]
435    fn empty_intervals() {
436        let result = linear_scan(&[], &[PReg(0)]);
437        assert!(result.spilled.is_empty());
438        assert!(result.vreg_to_preg.is_empty());
439    }
440
441    #[test]
442    fn no_allocatable_registers_all_spill() {
443        let intervals = vec![iv(0, 0, 5), iv(1, 2, 7)];
444        let result = linear_scan(&intervals, &[]);
445        assert_eq!(result.spilled.len(), 2);
446        assert!(result.vreg_to_preg.is_empty());
447    }
448
449    #[test]
450    fn apply_allocation_rewrites_operands() {
451        use crate::isel::{MInstr, MOpcode, MachineFunction};
452        let mut mf = MachineFunction::new("f".into());
453        let b = mf.add_block("entry");
454        let v0 = mf.fresh_vreg();
455        let v1 = mf.fresh_vreg();
456        mf.push(b, MInstr::new(MOpcode(0)).with_dst(v0).with_vreg(v1));
457        mf.push(b, MInstr::new(MOpcode(1)).with_vreg(v0));
458
459        let mut result = RegAllocResult::default();
460        result.vreg_to_preg.insert(v0, PReg(3));
461        result.vreg_to_preg.insert(v1, PReg(7));
462
463        apply_allocation(&mut mf, &result);
464
465        // instr 0: dst=v0→PReg(3), src operand v1→PReg(7)
466        assert_eq!(mf.blocks[0].instrs[0].dst, Some(VReg(3))); // physical reg 3
467        assert_eq!(mf.blocks[0].instrs[0].operands[0], MOperand::PReg(PReg(7)));
468        // instr 1: src operand v0→PReg(3)
469        assert_eq!(mf.blocks[0].instrs[1].operands[0], MOperand::PReg(PReg(3)));
470    }
471
472    #[test]
473    fn apply_allocation_rewrites_dst_register() {
474        use crate::isel::{MInstr, MOpcode, MachineFunction};
475        let mut mf = MachineFunction::new("f".into());
476        let b = mf.add_block("entry");
477        let v5 = VReg(5); // VReg 5 allocated to PReg 2
478        mf.next_vreg = 6;
479        mf.push(b, MInstr::new(MOpcode(0)).with_dst(v5));
480
481        let mut result = RegAllocResult::default();
482        result.vreg_to_preg.insert(v5, PReg(2));
483
484        apply_allocation(&mut mf, &result);
485
486        // dst should now contain VReg(2) so that PReg(dst.0 as u8) == PReg(2)
487        assert_eq!(mf.blocks[0].instrs[0].dst, Some(VReg(2)));
488    }
489
490    #[test]
491    fn many_overlapping_intervals_all_assigned() {
492        // Issue #38: stress test with many intervals to verify that the
493        // partition_point insertion correctly maintains the sorted invariant.
494        // 5 intervals all starting at 0 with different ends — need 5 registers.
495        let intervals = vec![
496            iv(0, 0, 10),
497            iv(1, 0, 8),
498            iv(2, 0, 6),
499            iv(3, 0, 4),
500            iv(4, 0, 2),
501        ];
502        let alloc: Vec<PReg> = (0u8..5).map(PReg).collect();
503        let result = linear_scan(&intervals, &alloc);
504        assert!(
505            result.spilled.is_empty(),
506            "no spills: 5 regs for 5 simultaneous live ranges"
507        );
508        assert_eq!(result.vreg_to_preg.len(), 5);
509    }
510
511    #[test]
512    fn compute_intervals_single_block() {
513        use crate::isel::{MInstr, MOpcode, MachineFunction};
514        let mut mf = MachineFunction::new("f".into());
515        let b = mf.add_block("entry");
516        let v0 = mf.fresh_vreg();
517        let v1 = mf.fresh_vreg();
518        // instr 0: v0 = ...
519        mf.push(b, MInstr::new(MOpcode(0)).with_dst(v0));
520        // instr 1: v1 = ... v0 ...
521        mf.push(b, MInstr::new(MOpcode(1)).with_dst(v1).with_vreg(v0));
522
523        let intervals = compute_live_intervals(&mf);
524        let v0_iv = intervals.iter().find(|i| i.vreg == v0).unwrap();
525        let v1_iv = intervals.iter().find(|i| i.vreg == v1).unwrap();
526        // v0 defined at 0, last used at 1 → end = 2
527        assert_eq!(v0_iv.start, 0);
528        assert_eq!(v0_iv.end, 2);
529        // v1 defined at 1, never used → end = 2
530        assert_eq!(v1_iv.start, 1);
531    }
532
533    #[test]
534    fn insert_spill_reloads_inserts_load_store() {
535        // Create a MachineFunction with a single register available and two
536        // simultaneously live VRegs → one must spill.
537        use crate::isel::{MInstr, MOpcode, MachineFunction};
538        const LOAD_OP: MOpcode = MOpcode(0xA0);
539        const STORE_OP: MOpcode = MOpcode(0xA1);
540
541        let mut mf = MachineFunction::new("spill_test".into());
542        mf.allocatable_pregs = vec![PReg(0)];
543        let b = mf.add_block("entry");
544
545        let v0 = mf.fresh_vreg();
546        let v1 = mf.fresh_vreg();
547        // instr 0: v0 = ...  (defines v0)
548        mf.push(b, MInstr::new(MOpcode(1)).with_dst(v0));
549        // instr 1: v1 = ... v0 ...  (defines v1, uses v0 — both live simultaneously)
550        mf.push(b, MInstr::new(MOpcode(2)).with_dst(v1).with_vreg(v0));
551        // instr 2: use v1
552        mf.push(b, MInstr::new(MOpcode(3)).with_vreg(v1));
553
554        let intervals = compute_live_intervals(&mf);
555        let mut result = linear_scan(&intervals, &mf.allocatable_pregs);
556
557        // With only 1 register, one VReg must spill.
558        assert_eq!(
559            result.spilled.len(),
560            1,
561            "one VReg must spill with 1 reg, 2 simultaneously live"
562        );
563
564        insert_spill_reloads(&mut mf, &mut result, LOAD_OP, STORE_OP);
565
566        // After insertion: no VReg should remain in result.spilled (they were processed).
567        assert!(
568            result.spilled.is_empty(),
569            "spilled list must be empty after insert_spill_reloads"
570        );
571
572        // The spill slot must have been allocated.
573        assert_eq!(mf.spill_slots.len(), 1, "one spill slot must be allocated");
574        assert!(mf.frame_size > 0, "frame_size must be non-zero after spill");
575
576        // The block must have more instructions than the original 3 (loads/stores inserted).
577        assert!(
578            mf.blocks[0].instrs.len() > 3,
579            "instructions must be inserted for load/store around the spilled VReg"
580        );
581
582        // Every LOAD_OP must have a dst, every STORE_OP must have no dst.
583        for instr in &mf.blocks[0].instrs {
584            if instr.opcode == LOAD_OP {
585                assert!(instr.dst.is_some(), "LOAD_OP must have a dst VReg");
586                assert!(
587                    matches!(instr.operands.first(), Some(MOperand::Imm(_))),
588                    "LOAD_OP first operand must be Imm(slot)"
589                );
590            } else if instr.opcode == STORE_OP {
591                assert!(instr.dst.is_none(), "STORE_OP must have no dst");
592                assert!(
593                    matches!(instr.operands.first(), Some(MOperand::Imm(_))),
594                    "STORE_OP first operand must be Imm(slot)"
595                );
596            }
597        }
598    }
599
600    #[test]
601    fn apply_allocation_populates_used_callee_saved() {
602        use crate::isel::{MInstr, MOpcode, MachineFunction};
603
604        let mut mf = MachineFunction::new("cs_test".into());
605        mf.allocatable_pregs = vec![PReg(0), PReg(3)]; // RAX and RBX
606        mf.callee_saved_pregs = vec![PReg(3)]; // RBX is callee-saved
607        let b = mf.add_block("entry");
608        let v0 = mf.fresh_vreg();
609        let v1 = mf.fresh_vreg();
610        mf.push(b, MInstr::new(MOpcode(0)).with_dst(v0));
611        mf.push(b, MInstr::new(MOpcode(1)).with_dst(v1).with_vreg(v0));
612
613        let intervals = compute_live_intervals(&mf);
614        let result = linear_scan(&intervals, &mf.allocatable_pregs);
615        // Ensure both are allocated (not spilled)
616        assert_eq!(result.spilled.len(), 0);
617
618        apply_allocation(&mut mf, &result);
619
620        // If RBX (PReg(3)) was used, it must appear in used_callee_saved.
621        let used_pregs: std::collections::HashSet<PReg> =
622            result.vreg_to_preg.values().copied().collect();
623        if used_pregs.contains(&PReg(3)) {
624            assert!(
625                mf.used_callee_saved.contains(&PReg(3)),
626                "used callee-saved PReg must appear in mf.used_callee_saved"
627            );
628        } else {
629            assert!(
630                !mf.used_callee_saved.contains(&PReg(3)),
631                "unused callee-saved PReg must not appear in mf.used_callee_saved"
632            );
633        }
634    }
635}