use rucc_mir::Func;
use rucc_regalloc::Allocation;
use rucc_regalloc::assign::Place;
use rucc_target::{CallRegs, PhysReg, RegClass, RegFile};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Save {
pub reg: PhysReg,
pub at: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Local {
pub size: u32,
pub align: u32,
}
#[derive(Debug, Clone, Copy)]
pub struct Layout<'a> {
pub conv: &'a CallRegs,
pub file: RegFile,
pub locals: &'a [Local],
pub outgoing: u32,
pub leaf: bool,
pub frame_pointer: bool,
pub red_zone: bool,
}
impl<'a> Layout<'a> {
#[must_use]
pub fn new(conv: &'a CallRegs, file: RegFile) -> Self {
Self {
conv,
file,
locals: &[],
outgoing: 0,
leaf: true,
frame_pointer: false,
red_zone: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Frame {
saved_int: Vec<PhysReg>,
saved_sse: Vec<Save>,
slots: Vec<i32>,
locals: Vec<i32>,
outgoing: u32,
size: u32,
realign: Option<u32>,
incoming: Option<i32>,
frame_pointer: bool,
}
impl Frame {
#[must_use]
pub fn of(func: &Func, allocation: &Allocation, layout: &Layout<'_>) -> Self {
let conv = layout.conv;
let word = conv.word;
let (saved_int, vectors) = saved(func, allocation, layout);
let vector = width(layout, conv.sse_class);
let mut top = 0;
let mut align = word;
let mut saved_sse = Vec::with_capacity(vectors.len());
for reg in vectors {
align = align.max(vector);
saved_sse.push(Save { reg, at: offset(top) });
top += vector;
}
let mut locals = vec![0; layout.locals.len()];
let mut order: Vec<usize> = (0..layout.locals.len()).collect();
order.sort_by_key(|&local| std::cmp::Reverse(layout.locals[local].align));
for local in order {
let Local { size, align: want } = layout.locals[local];
assert!(
want.is_power_of_two(),
"a local aligned to something that is not a power of 2"
);
align = align.max(want);
top = top.next_multiple_of(want);
locals[local] = offset(top);
top += size;
}
let mut slots = Vec::with_capacity(allocation.assignment.slots().len());
for &class in allocation.assignment.slots() {
let size = width(layout, class);
align = align.max(size);
top = top.next_multiple_of(size);
slots.push(offset(top));
top += size;
}
let outgoing = if layout.leaf { 0 } else { layout.outgoing.max(conv.shadow) };
let body = (top + outgoing).next_multiple_of(word);
let pushed =
u32::from(layout.frame_pointer) + u32::try_from(saved_int.len()).expect("a frame");
let entry = wrap(conv.stack_align, conv.return_address);
let after = (entry + wrap(conv.stack_align, word * pushed)) % conv.stack_align;
let realign = (align > conv.stack_align).then_some(align);
let free = layout.leaf
&& layout.red_zone
&& realign.is_none()
&& align <= word
&& body <= conv.red_zone;
let size = match realign {
_ if free => 0,
Some(to) => body.next_multiple_of(to),
None if layout.leaf && align <= word => body,
None => body + (after + conv.stack_align - body % conv.stack_align) % conv.stack_align,
};
let shift = if free { -offset(body) } else { offset(outgoing) };
for at in slots
.iter_mut()
.chain(locals.iter_mut())
.chain(saved_sse.iter_mut().map(|save| &mut save.at))
{
*at += shift;
}
Self {
saved_int,
saved_sse,
slots,
locals,
outgoing,
size,
realign,
incoming: realign.is_none().then(|| offset(size + word * pushed + conv.return_address)),
frame_pointer: layout.frame_pointer || realign.is_some(),
}
}
#[must_use]
pub fn saved_int(&self) -> &[PhysReg] {
&self.saved_int
}
#[must_use]
pub fn saved_sse(&self) -> &[Save] {
&self.saved_sse
}
#[must_use]
pub fn slot(&self, slot: u32) -> Option<i32> {
self.slots.get(usize::try_from(slot).ok()?).copied()
}
#[must_use]
pub fn local(&self, local: usize) -> Option<i32> {
self.locals.get(local).copied()
}
#[must_use]
pub fn size(&self) -> u32 {
self.size
}
#[must_use]
pub fn outgoing(&self) -> u32 {
self.outgoing
}
#[must_use]
pub fn realign(&self) -> Option<u32> {
self.realign
}
#[must_use]
pub fn incoming(&self) -> Option<i32> {
self.incoming
}
#[must_use]
pub fn frame_pointer(&self) -> bool {
self.frame_pointer
}
}
fn saved(
func: &Func,
allocation: &Allocation,
layout: &Layout<'_>,
) -> (Vec<PhysReg>, Vec<PhysReg>) {
let mut used: Vec<(RegClass, PhysReg)> = Vec::new();
let mut note = |class: RegClass, at: PhysReg| {
if !used.contains(&(class, at)) {
used.push((class, at));
}
};
for block in func.blocks() {
for inst in func.insts(block) {
for operand in &func[func[inst].operands] {
if let Some(at) = operand.reg.phys() {
note(operand.class, at);
}
}
}
}
for edit in &allocation.edits {
for place in [edit.mov.from, edit.mov.to] {
if let Place::Reg(at) = place {
note(edit.class, at);
}
}
}
let conv = layout.conv;
let wanted = |class: RegClass, at: PhysReg| used.contains(&(class, at));
let saved_int = conv
.int_saved
.iter()
.copied()
.filter(|&at| wanted(conv.int_class, at))
.filter(|&at| !(layout.frame_pointer && at == conv.frame_pointer))
.collect();
let saved_sse =
conv.sse_saved.iter().copied().filter(|&at| wanted(conv.sse_class, at)).collect();
(saved_int, saved_sse)
}
fn width(layout: &Layout<'_>, class: RegClass) -> u32 {
let bits = layout.file.class(class).map_or(0, |info| info.bits);
bits.div_ceil(8).max(layout.conv.word).next_power_of_two()
}
fn wrap(align: u32, value: u32) -> u32 {
(align - value % align) % align
}
fn offset(bytes: u32) -> i32 {
i32::try_from(bytes).expect("a frame under two gigabytes")
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_mir::{Opcode, Operand, Reg};
use rucc_regalloc::assign::Env;
use rucc_target::x86_64::{GPR, RBP, REGS, SYSV, WIN64, XMM};
use super::*;
fn env(conv: &CallRegs, count: usize) -> Env {
Env::new().with(GPR, &conv.int_order[..count], &conv.int_order[count..])
}
fn pressure(conv: &CallRegs, values: usize, count: usize) -> (Func, Allocation) {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"));
let opcode = Opcode::new(names.intern("x64.nop"));
let block = func.create_block();
let regs: Vec<Reg> = (0..values).map(|_| func.new_vreg(GPR)).collect();
for ® in ®s {
func.build(block, opcode).def(reg, GPR).finish();
}
for ® in ®s {
func.build(block, opcode).uses(reg, GPR).finish();
}
let allocation = rucc_regalloc::run(&mut func, &env(conv, count));
(func, allocation)
}
fn named(regs: &[PhysReg]) -> Vec<&'static str> {
regs.iter().map(|®| REGS.name(GPR, reg).expect("a register")).collect()
}
#[test]
fn a_function_that_needs_nothing_of_the_stack_has_no_frame_at_all() {
let (func, allocation) = pressure(&SYSV, 2, 4);
let frame = Frame::of(&func, &allocation, &Layout::new(&SYSV, REGS));
assert_eq!(frame.size(), 0);
assert_eq!(named(frame.saved_int()), Vec::<&str>::new());
assert_eq!(frame.slot(0), None);
assert_eq!(frame.incoming(), Some(8));
}
#[test]
fn a_small_leaf_function_puts_its_spills_in_the_red_zone_and_moves_nothing() {
let (func, allocation) = pressure(&SYSV, 4, 2);
let frame = Frame::of(&func, &allocation, &Layout::new(&SYSV, REGS));
assert_eq!(frame.size(), 0);
assert_eq!((frame.slot(0), frame.slot(1)), (Some(-16), Some(-8)));
assert_eq!(frame.slot(2), None);
assert_eq!(frame.incoming(), Some(8));
}
#[test]
fn a_leaf_function_told_it_has_no_red_zone_takes_the_bytes_instead() {
let (func, allocation) = pressure(&SYSV, 4, 2);
let base = Layout::new(&SYSV, REGS);
let frame = Frame::of(&func, &allocation, &Layout { red_zone: false, ..base });
assert_eq!(frame.size(), 16);
assert_eq!((frame.slot(0), frame.slot(1)), (Some(0), Some(8)));
assert_eq!(frame.incoming(), Some(24));
}
#[test]
fn a_frame_too_big_for_the_red_zone_takes_the_bytes_whatever_else_is_true() {
let (func, allocation) = pressure(&SYSV, 40, 2);
let frame = Frame::of(&func, &allocation, &Layout::new(&SYSV, REGS));
assert_eq!(frame.size(), 304);
assert_eq!(frame.slot(0), Some(0));
assert_eq!(frame.slot(37), Some(296));
}
#[test]
fn a_function_that_calls_something_leaves_the_stack_pointer_where_a_call_wants_it() {
let (func, allocation) = pressure(&SYSV, 4, 2);
let base = Layout::new(&SYSV, REGS);
let frame = Frame::of(&func, &allocation, &Layout { leaf: false, ..base });
assert_eq!(frame.size(), 24);
assert_eq!((frame.slot(0), frame.slot(1)), (Some(0), Some(8)));
assert_eq!(frame.incoming(), Some(32));
}
#[test]
fn a_push_is_counted_in_the_alignment_the_frame_has_to_produce() {
let (func, allocation) = pressure(&SYSV, 12, 12);
let base = Layout::new(&SYSV, REGS);
let frame = Frame::of(&func, &allocation, &Layout { leaf: false, ..base });
assert_eq!(named(frame.saved_int()), ["rbx", "r12", "r13"]);
assert_eq!(frame.size(), 0);
assert_eq!(frame.incoming(), Some(32));
}
#[test]
fn the_registers_a_call_leaves_alone_are_saved_in_the_order_the_convention_lists_them() {
let (func, allocation) = pressure(&SYSV, 13, 13);
let frame = Frame::of(&func, &allocation, &Layout::new(&SYSV, REGS));
assert_eq!(named(frame.saved_int()), ["rbx", "r12", "r13", "r14"]);
}
#[test]
fn a_function_that_keeps_a_frame_pointer_does_not_save_it_twice() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"));
let opcode = Opcode::new(names.intern("x64.nop"));
let block = func.create_block();
func.build(block, opcode).operand(Operand::write(Reg::physical(RBP), GPR)).finish();
let allocation = rucc_regalloc::run(&mut func, &env(&SYSV, 4));
let base = Layout::new(&SYSV, REGS);
let kept = Frame::of(&func, &allocation, &Layout { frame_pointer: true, ..base });
let dropped = Frame::of(&func, &allocation, &base);
assert_eq!(named(dropped.saved_int()), ["rbp"]);
assert_eq!(named(kept.saved_int()), Vec::<&str>::new());
assert!(kept.frame_pointer());
}
#[test]
fn locals_are_placed_widest_alignment_first_and_reported_in_the_order_they_arrived() {
let (func, allocation) = pressure(&SYSV, 2, 4);
let locals = [
Local { size: 1, align: 1 },
Local { size: 16, align: 16 },
Local { size: 8, align: 8 },
];
let base = Layout::new(&SYSV, REGS);
let frame = Frame::of(&func, &allocation, &Layout { locals: &locals, ..base });
assert_eq!((frame.local(1), frame.local(2), frame.local(0)), (Some(0), Some(16), Some(24)));
assert_eq!(frame.local(3), None);
assert_eq!(frame.size(), 40);
assert_eq!(frame.realign(), None);
}
#[test]
fn a_local_wanting_more_alignment_than_a_call_gives_makes_the_prologue_force_it() {
let (func, allocation) = pressure(&SYSV, 2, 4);
let locals = [Local { size: 64, align: 32 }];
let base = Layout::new(&SYSV, REGS);
let frame = Frame::of(&func, &allocation, &Layout { locals: &locals, ..base });
assert_eq!(frame.realign(), Some(32));
assert_eq!(frame.local(0), Some(0));
assert_eq!(frame.size(), 64);
assert!(frame.frame_pointer());
assert_eq!(frame.incoming(), None);
}
#[test]
fn a_call_reads_its_stack_arguments_from_the_bottom_of_the_frame() {
let (func, allocation) = pressure(&SYSV, 4, 2);
let base = Layout::new(&SYSV, REGS);
let frame = Frame::of(&func, &allocation, &Layout { leaf: false, outgoing: 24, ..base });
assert_eq!(frame.outgoing(), 24);
assert_eq!((frame.slot(0), frame.slot(1)), (Some(24), Some(32)));
assert_eq!(frame.size(), 40);
}
#[test]
fn a_windows_call_gets_the_thirty_two_bytes_below_it_even_when_it_passes_nothing() {
let (func, allocation) = pressure(&WIN64, 2, 4);
let base = Layout::new(&WIN64, REGS);
let frame = Frame::of(&func, &allocation, &Layout { leaf: false, ..base });
assert_eq!(frame.outgoing(), 32);
assert_eq!(frame.size(), 40);
assert_eq!(frame.incoming(), Some(48));
}
#[test]
fn a_slot_is_as_wide_as_the_widest_thing_of_its_class() {
let base = Layout::new(&SYSV, REGS);
assert_eq!(width(&base, GPR), 8);
assert_eq!(width(&base, XMM), 16);
assert_eq!(width(&base, REGS.class_named("x87").expect("a class")), 16);
}
}