use crate::isel::{MInstr, MOpcode, MOperand, MachineFunction, PReg, VReg};
pub use crate::regalloc_gc::graph_color;
use std::collections::HashMap;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LiveInterval {
pub vreg: VReg,
pub start: usize,
pub end: usize,
}
fn block_starts(mf: &MachineFunction) -> Vec<usize> {
let mut starts = Vec::with_capacity(mf.blocks.len());
let mut pos = 0;
for b in &mf.blocks {
starts.push(pos);
pos += b.instrs.len();
}
starts
}
pub fn compute_live_intervals(mf: &MachineFunction) -> Vec<LiveInterval> {
let _ = block_starts(mf); let mut map: HashMap<VReg, (usize, usize)> = HashMap::new();
let mut pos = 0usize;
for block in &mf.blocks {
for instr in &block.instrs {
if let Some(dst) = instr.dst {
let e = map.entry(dst).or_insert((pos, pos + 1));
if pos < e.0 {
e.0 = pos;
}
if pos + 1 > e.1 {
e.1 = pos + 1;
}
}
for op in &instr.operands {
if let MOperand::VReg(vr) = op {
let e = map.entry(*vr).or_insert((pos, pos + 1));
if pos < e.0 {
e.0 = pos;
}
if pos + 1 > e.1 {
e.1 = pos + 1;
}
}
}
pos += 1;
}
}
let mut intervals: Vec<LiveInterval> = map
.into_iter()
.map(|(vreg, (start, end))| LiveInterval { vreg, start, end })
.collect();
intervals.sort_by_key(|i| (i.start, i.end, i.vreg.0));
intervals
}
#[derive(Debug, Default)]
pub struct RegAllocResult {
pub vreg_to_preg: HashMap<VReg, PReg>,
pub spilled: Vec<VReg>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum RegAllocStrategy {
#[default]
LinearScan,
GraphColor,
}
pub fn allocate_registers(
intervals: &[LiveInterval],
allocatable: &[PReg],
strategy: RegAllocStrategy,
) -> RegAllocResult {
match strategy {
RegAllocStrategy::LinearScan => linear_scan(intervals, allocatable),
RegAllocStrategy::GraphColor => graph_color(intervals, allocatable),
}
}
pub fn linear_scan(intervals: &[LiveInterval], allocatable: &[PReg]) -> RegAllocResult {
if allocatable.is_empty() {
return RegAllocResult {
vreg_to_preg: HashMap::new(),
spilled: intervals.iter().map(|i| i.vreg).collect(),
};
}
let mut sorted: Vec<&LiveInterval> = intervals.iter().collect();
sorted.sort_by_key(|i| (i.start, i.end, i.vreg.0));
let mut free: Vec<PReg> = allocatable.to_vec();
let mut active: Vec<(usize, VReg, PReg)> = Vec::new();
let mut result = RegAllocResult::default();
for interval in &sorted {
let mut returned = Vec::new();
active.retain(|&(end, _vr, pr)| {
if end <= interval.start {
returned.push(pr);
false
} else {
true
}
});
free.extend(returned);
if free.is_empty() {
let spill_idx = if active.is_empty() {
None
} else {
Some(active.len() - 1)
};
if let Some(idx) = spill_idx {
let (spill_end, spill_vr, spill_pr) = active[idx];
if spill_end > interval.end {
active.remove(idx);
result.vreg_to_preg.remove(&spill_vr); result.vreg_to_preg.insert(interval.vreg, spill_pr);
let pos = active.partition_point(|&(e, vr, _)| {
(e, vr.0) <= (interval.end, interval.vreg.0)
});
active.insert(pos, (interval.end, interval.vreg, spill_pr));
result.spilled.push(spill_vr);
} else {
result.spilled.push(interval.vreg);
}
} else {
result.spilled.push(interval.vreg);
}
} else {
let pr = free.remove(0);
result.vreg_to_preg.insert(interval.vreg, pr);
let pos = active.partition_point(|&(e, vr, _)| {
(e, vr.0) <= (interval.end, interval.vreg.0)
});
active.insert(pos, (interval.end, interval.vreg, pr));
}
}
result
}
pub fn insert_spill_reloads(
mf: &mut MachineFunction,
result: &mut RegAllocResult,
load_op: MOpcode,
store_op: MOpcode,
) {
if result.spilled.is_empty() {
return;
}
let spilled: Vec<VReg> = result.spilled.drain(..).collect();
for &vr in &spilled {
mf.alloc_spill_slot(vr);
}
for bi in 0..mf.blocks.len() {
let old_instrs: Vec<MInstr> = std::mem::take(&mut mf.blocks[bi].instrs);
let mut new_instrs: Vec<MInstr> = Vec::with_capacity(old_instrs.len() * 2);
for mut instr in old_instrs {
for op in &mut instr.operands {
if let MOperand::VReg(vr) = op {
if let Some(&slot) = mf.spill_slots.get(vr) {
let fresh = mf.fresh_vreg();
new_instrs.push(MInstr::new(load_op).with_dst(fresh).with_imm(slot as i64));
*op = MOperand::VReg(fresh);
}
}
}
let store_after = if let Some(dst_vr) = instr.dst {
if let Some(&slot) = mf.spill_slots.get(&dst_vr) {
let fresh = mf.fresh_vreg();
instr.dst = Some(fresh);
Some((fresh, slot))
} else {
None
}
} else {
None
};
new_instrs.push(instr);
if let Some((fresh, slot)) = store_after {
new_instrs.push(MInstr::new(store_op).with_imm(slot as i64).with_vreg(fresh));
}
}
mf.blocks[bi].instrs = new_instrs;
}
let fresh_intervals = compute_live_intervals(mf);
let fresh_only: Vec<LiveInterval> = fresh_intervals
.into_iter()
.filter(|iv| !result.vreg_to_preg.contains_key(&iv.vreg))
.collect();
if !fresh_only.is_empty() {
let alloc = mf.allocatable_pregs.clone();
let second = linear_scan(&fresh_only, &alloc);
for (vr, pr) in second.vreg_to_preg {
result.vreg_to_preg.insert(vr, pr);
}
if !second.spilled.is_empty() {
let fallback = alloc.first().copied().unwrap_or(PReg(0));
for vr in second.spilled {
result.vreg_to_preg.entry(vr).or_insert(fallback);
}
}
}
}
pub fn apply_allocation(mf: &mut MachineFunction, result: &RegAllocResult) {
let callee_saved: std::collections::HashSet<PReg> =
mf.callee_saved_pregs.iter().copied().collect();
let mut used_cs: std::collections::HashSet<PReg> = std::collections::HashSet::new();
for &pr in result.vreg_to_preg.values() {
if callee_saved.contains(&pr) {
used_cs.insert(pr);
}
}
mf.used_callee_saved = used_cs.into_iter().collect();
let order: HashMap<PReg, usize> = mf
.callee_saved_pregs
.iter()
.enumerate()
.map(|(i, &p)| (p, i))
.collect();
mf.used_callee_saved
.sort_by_key(|p| order.get(p).copied().unwrap_or(usize::MAX));
for block in &mut mf.blocks {
for instr in &mut block.instrs {
if let Some(ref mut vr) = instr.dst {
if let Some(&pr) = result.vreg_to_preg.get(vr) {
*vr = VReg(pr.0 as u32);
}
}
for op in &mut instr.operands {
if let MOperand::VReg(vr) = op {
if let Some(&pr) = result.vreg_to_preg.get(vr) {
*op = MOperand::PReg(pr);
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn iv(vreg: u32, start: usize, end: usize) -> LiveInterval {
LiveInterval {
vreg: VReg(vreg),
start,
end,
}
}
#[test]
fn compute_intervals_returns_deterministic_order_for_equal_starts() {
use crate::isel::{MInstr, MOpcode, MachineFunction};
let mut mf = MachineFunction::new("f".into());
let b = mf.add_block("entry");
let v0 = mf.fresh_vreg();
let v1 = mf.fresh_vreg();
let v2 = mf.fresh_vreg();
mf.push(
b,
MInstr::new(MOpcode(0))
.with_dst(v2)
.with_vreg(v1)
.with_vreg(v0),
);
let intervals = compute_live_intervals(&mf);
let keys: Vec<(usize, usize, u32)> = intervals
.iter()
.map(|iv| (iv.start, iv.end, iv.vreg.0))
.collect();
assert_eq!(keys, vec![(0, 1, 0), (0, 1, 1), (0, 1, 2)]);
}
#[test]
fn linear_scan_tie_breaks_equal_start_intervals_by_vreg() {
let intervals = vec![iv(2, 0, 1), iv(0, 0, 1), iv(1, 0, 1)];
let alloc = vec![PReg(0), PReg(1)];
let result = linear_scan(&intervals, &alloc);
assert_eq!(result.vreg_to_preg[&VReg(0)], PReg(0));
assert_eq!(result.vreg_to_preg[&VReg(1)], PReg(1));
assert_eq!(result.spilled, vec![VReg(2)]);
}
#[test]
fn two_non_overlapping_one_reg() {
let intervals = vec![iv(0, 0, 2), iv(1, 3, 5)];
let alloc = vec![PReg(0)];
let result = linear_scan(&intervals, &alloc);
assert!(result.spilled.is_empty(), "no spills expected");
assert_eq!(result.vreg_to_preg.len(), 2);
assert_eq!(result.vreg_to_preg[&VReg(0)], PReg(0));
assert_eq!(result.vreg_to_preg[&VReg(1)], PReg(0));
}
#[test]
fn two_overlapping_one_register_causes_spill() {
let intervals = vec![iv(0, 0, 10), iv(1, 1, 8)];
let alloc = vec![PReg(0)];
let result = linear_scan(&intervals, &alloc);
assert_eq!(result.spilled.len(), 1, "exactly one must spill");
assert_eq!(result.vreg_to_preg.len(), 1);
}
#[test]
fn three_intervals_two_registers() {
let intervals = vec![iv(0, 0, 3), iv(1, 1, 4), iv(2, 5, 8)];
let alloc = vec![PReg(0), PReg(1)];
let result = linear_scan(&intervals, &alloc);
assert!(result.spilled.is_empty());
assert_eq!(result.vreg_to_preg.len(), 3);
}
#[test]
fn empty_intervals() {
let result = linear_scan(&[], &[PReg(0)]);
assert!(result.spilled.is_empty());
assert!(result.vreg_to_preg.is_empty());
}
#[test]
fn no_allocatable_registers_all_spill() {
let intervals = vec![iv(0, 0, 5), iv(1, 2, 7)];
let result = linear_scan(&intervals, &[]);
assert_eq!(result.spilled.len(), 2);
assert!(result.vreg_to_preg.is_empty());
}
#[test]
fn apply_allocation_rewrites_operands() {
use crate::isel::{MInstr, MOpcode, MachineFunction};
let mut mf = MachineFunction::new("f".into());
let b = mf.add_block("entry");
let v0 = mf.fresh_vreg();
let v1 = mf.fresh_vreg();
mf.push(b, MInstr::new(MOpcode(0)).with_dst(v0).with_vreg(v1));
mf.push(b, MInstr::new(MOpcode(1)).with_vreg(v0));
let mut result = RegAllocResult::default();
result.vreg_to_preg.insert(v0, PReg(3));
result.vreg_to_preg.insert(v1, PReg(7));
apply_allocation(&mut mf, &result);
assert_eq!(mf.blocks[0].instrs[0].dst, Some(VReg(3))); assert_eq!(mf.blocks[0].instrs[0].operands[0], MOperand::PReg(PReg(7)));
assert_eq!(mf.blocks[0].instrs[1].operands[0], MOperand::PReg(PReg(3)));
}
#[test]
fn apply_allocation_rewrites_dst_register() {
use crate::isel::{MInstr, MOpcode, MachineFunction};
let mut mf = MachineFunction::new("f".into());
let b = mf.add_block("entry");
let v5 = VReg(5); mf.next_vreg = 6;
mf.push(b, MInstr::new(MOpcode(0)).with_dst(v5));
let mut result = RegAllocResult::default();
result.vreg_to_preg.insert(v5, PReg(2));
apply_allocation(&mut mf, &result);
assert_eq!(mf.blocks[0].instrs[0].dst, Some(VReg(2)));
}
#[test]
fn many_overlapping_intervals_all_assigned() {
let intervals = vec![
iv(0, 0, 10),
iv(1, 0, 8),
iv(2, 0, 6),
iv(3, 0, 4),
iv(4, 0, 2),
];
let alloc: Vec<PReg> = (0u8..5).map(PReg).collect();
let result = linear_scan(&intervals, &alloc);
assert!(
result.spilled.is_empty(),
"no spills: 5 regs for 5 simultaneous live ranges"
);
assert_eq!(result.vreg_to_preg.len(), 5);
}
#[test]
fn compute_intervals_single_block() {
use crate::isel::{MInstr, MOpcode, MachineFunction};
let mut mf = MachineFunction::new("f".into());
let b = mf.add_block("entry");
let v0 = mf.fresh_vreg();
let v1 = mf.fresh_vreg();
mf.push(b, MInstr::new(MOpcode(0)).with_dst(v0));
mf.push(b, MInstr::new(MOpcode(1)).with_dst(v1).with_vreg(v0));
let intervals = compute_live_intervals(&mf);
let v0_iv = intervals.iter().find(|i| i.vreg == v0).unwrap();
let v1_iv = intervals.iter().find(|i| i.vreg == v1).unwrap();
assert_eq!(v0_iv.start, 0);
assert_eq!(v0_iv.end, 2);
assert_eq!(v1_iv.start, 1);
}
#[test]
fn insert_spill_reloads_inserts_load_store() {
use crate::isel::{MInstr, MOpcode, MachineFunction};
const LOAD_OP: MOpcode = MOpcode(0xA0);
const STORE_OP: MOpcode = MOpcode(0xA1);
let mut mf = MachineFunction::new("spill_test".into());
mf.allocatable_pregs = vec![PReg(0)];
let b = mf.add_block("entry");
let v0 = mf.fresh_vreg();
let v1 = mf.fresh_vreg();
mf.push(b, MInstr::new(MOpcode(1)).with_dst(v0));
mf.push(b, MInstr::new(MOpcode(2)).with_dst(v1).with_vreg(v0));
mf.push(b, MInstr::new(MOpcode(3)).with_vreg(v1));
let intervals = compute_live_intervals(&mf);
let mut result = linear_scan(&intervals, &mf.allocatable_pregs);
assert_eq!(
result.spilled.len(),
1,
"one VReg must spill with 1 reg, 2 simultaneously live"
);
insert_spill_reloads(&mut mf, &mut result, LOAD_OP, STORE_OP);
assert!(
result.spilled.is_empty(),
"spilled list must be empty after insert_spill_reloads"
);
assert_eq!(mf.spill_slots.len(), 1, "one spill slot must be allocated");
assert!(mf.frame_size > 0, "frame_size must be non-zero after spill");
assert!(
mf.blocks[0].instrs.len() > 3,
"instructions must be inserted for load/store around the spilled VReg"
);
for instr in &mf.blocks[0].instrs {
if instr.opcode == LOAD_OP {
assert!(instr.dst.is_some(), "LOAD_OP must have a dst VReg");
assert!(
matches!(instr.operands.first(), Some(MOperand::Imm(_))),
"LOAD_OP first operand must be Imm(slot)"
);
} else if instr.opcode == STORE_OP {
assert!(instr.dst.is_none(), "STORE_OP must have no dst");
assert!(
matches!(instr.operands.first(), Some(MOperand::Imm(_))),
"STORE_OP first operand must be Imm(slot)"
);
}
}
}
#[test]
fn apply_allocation_populates_used_callee_saved() {
use crate::isel::{MInstr, MOpcode, MachineFunction};
let mut mf = MachineFunction::new("cs_test".into());
mf.allocatable_pregs = vec![PReg(0), PReg(3)]; mf.callee_saved_pregs = vec![PReg(3)]; let b = mf.add_block("entry");
let v0 = mf.fresh_vreg();
let v1 = mf.fresh_vreg();
mf.push(b, MInstr::new(MOpcode(0)).with_dst(v0));
mf.push(b, MInstr::new(MOpcode(1)).with_dst(v1).with_vreg(v0));
let intervals = compute_live_intervals(&mf);
let result = linear_scan(&intervals, &mf.allocatable_pregs);
assert_eq!(result.spilled.len(), 0);
apply_allocation(&mut mf, &result);
let used_pregs: std::collections::HashSet<PReg> =
result.vreg_to_preg.values().copied().collect();
if used_pregs.contains(&PReg(3)) {
assert!(
mf.used_callee_saved.contains(&PReg(3)),
"used callee-saved PReg must appear in mf.used_callee_saved"
);
} else {
assert!(
!mf.used_callee_saved.contains(&PReg(3)),
"unused callee-saved PReg must not appear in mf.used_callee_saved"
);
}
}
}