use rucc_base::{Interner, Symbol};
use rucc_ir::Type;
use rucc_mir as mir;
use rucc_target::{CallRegs, Constraint, PhysReg, Places, Where};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Missing {
OnStack,
InVector,
Width,
}
impl Missing {
#[must_use]
pub fn why(self) -> &'static str {
match self {
Missing::OnStack => "is passed on the stack",
Missing::InVector => "is in a vector register",
Missing::Width => "is a width no argument register holds",
}
}
}
pub fn entry(
out: &mut mir::Func,
block: mir::Block,
params: &[Type],
conv: &CallRegs,
names: &mut Interner,
) -> Result<Vec<mir::Reg>, (usize, Missing)> {
let mut places = Places::new(conv);
let mut regs = Vec::with_capacity(params.len());
for (index, &ty) in params.iter().enumerate() {
let at = if ty.is_float() { places.float() } else { places.integer() };
if ty.is_float() {
return Err((index, Missing::InVector));
}
let head = head_of(ty).ok_or((index, Missing::Width))?;
let Where::Reg(arrived) = at else { return Err((index, Missing::OnStack)) };
let reg = out.new_vreg(conv.int_class);
let opcode = mir::Opcode::new(names.intern(head));
let operand = mir::Operand::write(reg, conv.int_class).with(Constraint::Fixed(arrived));
out.build(block, opcode).operand(operand).finish();
regs.push(reg);
}
Ok(regs)
}
pub const CALL: &str = "x64.call";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Made {
pub result: Option<mir::Reg>,
pub outgoing: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Refused {
pub argument: Option<usize>,
pub missing: Missing,
}
#[derive(Debug, Clone, Copy)]
pub struct Calling<'a> {
pub callee: Symbol,
pub args: &'a [(Type, mir::Reg)],
pub returns: Option<Type>,
pub variadic: bool,
}
pub fn call(
out: &mut mir::Func,
block: mir::Block,
made: &Calling<'_>,
conv: &CallRegs,
names: &mut Interner,
) -> Result<Made, Refused> {
let &Calling { callee, args, returns, variadic } = made;
let mut places = Places::new(conv);
let mut passed = Vec::with_capacity(args.len());
for (index, &(ty, reg)) in args.iter().enumerate() {
let refused = |missing| Refused { argument: Some(index), missing };
let at = if ty.is_float() { places.float() } else { places.integer() };
if ty.is_float() {
return Err(refused(Missing::InVector));
}
if head_of(ty).is_none() {
return Err(refused(Missing::Width));
}
let Where::Reg(at) = at else { return Err(refused(Missing::OnStack)) };
passed.push((reg, at));
}
let comes_back = match returns {
None => None,
Some(ty) if ty.is_float() => {
return Err(Refused { argument: None, missing: Missing::InVector });
}
Some(ty) if head_of(ty).is_none() => {
return Err(Refused { argument: None, missing: Missing::Width });
}
Some(_) => Some(
*conv.int_returns.first().ok_or(Refused { argument: None, missing: Missing::Width })?,
),
};
let counted = if variadic { conv.vector_count } else { None };
let mut operands = Vec::with_capacity(args.len() + conv.int_order.len() + 2);
let result = comes_back.map(|at| {
let reg = out.new_vreg(conv.int_class);
operands.push(mir::Operand::write(reg, conv.int_class).with(Constraint::Fixed(at)));
reg
});
let named: Vec<PhysReg> =
comes_back.into_iter().chain(counted).chain(passed.iter().map(|&(_, at)| at)).collect();
for ® in conv.int_order {
if !conv.preserves_int(reg) && !named.contains(®) {
operands.push(mir::Operand::write(mir::Reg::physical(reg), conv.int_class));
}
}
for ® in conv.sse_order {
if !conv.preserves_sse(reg) {
operands.push(mir::Operand::write(mir::Reg::physical(reg), conv.sse_class));
}
}
for (reg, at) in passed {
operands.push(mir::Operand::read(reg, conv.int_class).with(Constraint::Fixed(at)));
}
if let Some(at) = counted {
let count = out.new_vreg(conv.int_class);
let zero = mir::Opcode::new(names.intern("x64.mov_ri_32"));
out.build(block, zero).def(count, conv.int_class).imm(0).finish();
operands.push(mir::Operand::read(count, conv.int_class).with(Constraint::Fixed(at)));
}
let opcode = mir::Opcode::new(names.intern(CALL));
let mut build = out.build(block, opcode).symbol(callee);
for operand in operands {
build = build.operand(operand);
}
build.finish();
Ok(Made { result, outgoing: places.size() })
}
#[must_use]
pub fn head_of(ty: Type) -> Option<&'static str> {
let names = ["x64.arg_val_8", "x64.arg_val_16", "x64.arg_val_32", "x64.arg_val_64"];
Some(names[crate::term::slot(ty)?])
}
#[cfg(test)]
mod tests {
use rucc_target::x86_64::{REGS, SYSV, WIN64};
use super::*;
fn bind(params: &[Type], conv: &CallRegs) -> String {
let mut names = Interner::new();
let mut out = mir::Func::new(names.intern("f"));
let block = out.create_block();
entry(&mut out, block, params, conv, &mut names).expect("every parameter arrives");
mir::print_func(&out, &names, ®S)
}
#[test]
fn the_first_arguments_arrive_where_the_convention_puts_them() {
let i32 = Type::int(32);
assert_eq!(
bind(&[i32, i32, Type::int(64)], &SYSV),
"mfunc @f {\nblock0:\n %0:gpr($rdi) = x64.arg_val_32\n \
%1:gpr($rsi) = x64.arg_val_32\n %2:gpr($rdx) = x64.arg_val_64\n}\n"
);
}
#[test]
fn the_other_convention_puts_the_same_arguments_somewhere_else() {
let i64 = Type::int(64);
assert_eq!(
bind(&[i64, i64], &WIN64),
"mfunc @f {\nblock0:\n %0:gpr($rcx) = x64.arg_val_64\n \
%1:gpr($rdx) = x64.arg_val_64\n}\n"
);
}
#[test]
fn an_argument_past_the_last_register_is_reported_rather_than_read_from_nowhere() {
let i64 = Type::int(64);
let mut names = Interner::new();
let mut out = mir::Func::new(names.intern("f"));
let block = out.create_block();
let seven = vec![i64; 7];
assert_eq!(entry(&mut out, block, &seven, &SYSV, &mut names), Err((6, Missing::OnStack)));
assert_eq!(entry(&mut out, block, &seven, &WIN64, &mut names), Err((4, Missing::OnStack)));
}
#[test]
fn an_argument_in_a_vector_register_is_reported_because_nothing_here_uses_one() {
let mut names = Interner::new();
let mut out = mir::Func::new(names.intern("f"));
let block = out.create_block();
let params = [Type::int(32), Type::float(rucc_ir::Float::F64)];
assert_eq!(entry(&mut out, block, ¶ms, &SYSV, &mut names), Err((1, Missing::InVector)));
}
fn make(
args: &[Type],
returns: Option<Type>,
variadic: bool,
conv: &CallRegs,
) -> (Interner, mir::Func, Result<Made, Refused>) {
let mut names = Interner::new();
let mut out = mir::Func::new(names.intern("f"));
let block = out.create_block();
let passed: Vec<(Type, mir::Reg)> =
args.iter().map(|&ty| (ty, out.append_param(block, conv.int_class))).collect();
let callee = names.intern("g");
let what = Calling { callee, args: &passed, returns, variadic };
let made = call(&mut out, block, &what, conv, &mut names);
(names, out, made)
}
fn operands(func: &mir::Func) -> (Vec<String>, Vec<String>) {
let block = func.entry().expect("a function with a block in it");
let call = func.terminator(block).expect("the call is the last thing in the block");
let name = |operand: &mir::Operand| match (operand.reg.phys(), operand.constraint) {
(Some(reg), _) | (None, Constraint::Fixed(reg)) => {
REGS.name(operand.class, reg).expect("a register the file describes").to_string()
}
_ => format!("{:?}", operand.reg),
};
let mut written = Vec::new();
let mut read = Vec::new();
for operand in &func[func[call].operands] {
let into = if operand.role == mir::Role::Use { &mut read } else { &mut written };
into.push(name(operand));
}
(written, read)
}
#[test]
fn a_call_passes_its_arguments_where_the_convention_puts_them() {
let i32 = Type::int(32);
let (_, func, made) = make(&[i32, i32, i32], None, false, &SYSV);
assert_eq!(made.expect("three integers all fit in registers").result, None);
assert_eq!(operands(&func).1, ["rdi", "rsi", "rdx"]);
}
#[test]
fn the_other_convention_passes_the_same_arguments_somewhere_else() {
let i64 = Type::int(64);
let (_, func, made) = make(&[i64, i64], None, false, &WIN64);
assert_eq!(made.expect("two integers fit in registers").outgoing, 32);
assert_eq!(operands(&func).1, ["rcx", "rdx"]);
}
#[test]
fn what_a_call_gives_back_comes_out_of_the_register_the_convention_returns_in() {
let (names, func, made) = make(&[], Some(Type::int(32)), false, &SYSV);
let result = made.expect("an integer comes back").result.expect("in a register");
assert_eq!(operands(&func).0.first().map(String::as_str), Some("rax"));
assert_eq!(func.class_of(result), Some(SYSV.int_class));
assert!(mir::print_func(&func, &names, ®S).contains("x64.call"));
}
#[test]
fn every_register_the_callee_may_destroy_is_written_by_the_call() {
let (_, func, _) = make(&[Type::int(64)], Some(Type::int(64)), false, &SYSV);
let (written, read) = operands(&func);
for saved in ["rbx", "rbp", "r12", "r13", "r14", "r15"] {
assert!(!written.contains(&saved.to_string()), "{saved} survives a call");
}
for destroyed in ["rcx", "rdx", "rsi", "r8", "r9", "r10", "r11"] {
let count = written.iter().filter(|name| *name == destroyed).count();
assert_eq!(count, 1, "{destroyed} is destroyed by a call and is written {count} times");
}
assert_eq!(written.iter().filter(|name| *name == "rax").count(), 1);
assert_eq!(read, ["rdi"]);
assert!(written.contains(&"xmm0".to_string()));
}
#[test]
fn a_variadic_call_says_how_many_vector_registers_it_passed_arguments_in() {
let (names, func, made) = make(&[Type::int(64)], None, true, &SYSV);
made.expect("an integer argument to a variadic callee");
let (_, read) = operands(&func);
assert_eq!(read, ["rdi", "rax"]);
assert_eq!(
mir::print_func(&func, &names, ®S).lines().nth(2),
Some(" %1:gpr = x64.mov_ri_32 0")
);
}
#[test]
fn a_call_that_would_pass_an_argument_on_the_stack_is_reported() {
let i64 = Type::int(64);
let seven = vec![i64; 7];
let (_, func, made) = make(&seven, None, false, &SYSV);
assert_eq!(made, Err(Refused { argument: Some(6), missing: Missing::OnStack }));
let block = func.entry().expect("a function with a block in it");
assert_eq!(func.insts(block).count(), 0);
assert_eq!(
make(&seven, None, false, &WIN64).2,
Err(Refused { argument: Some(4), missing: Missing::OnStack })
);
}
#[test]
fn a_call_that_travels_in_a_vector_register_is_reported_on_either_side() {
let f64 = Type::float(rucc_ir::Float::F64);
assert_eq!(
make(&[Type::int(32), f64], None, false, &SYSV).2,
Err(Refused { argument: Some(1), missing: Missing::InVector })
);
assert_eq!(
make(&[], Some(f64), false, &SYSV).2,
Err(Refused { argument: None, missing: Missing::InVector })
);
}
#[test]
fn a_call_at_a_width_no_register_holds_is_reported_on_either_side() {
let i128 = Type::int(128);
assert_eq!(
make(&[i128], None, false, &SYSV).2,
Err(Refused { argument: Some(0), missing: Missing::Width })
);
assert_eq!(
make(&[], Some(i128), false, &SYSV).2,
Err(Refused { argument: None, missing: Missing::Width })
);
}
#[test]
fn an_argument_wider_than_a_register_has_no_name() {
assert_eq!(head_of(Type::int(128)), None);
assert_eq!(head_of(Type::int(8)), Some("x64.arg_val_8"));
assert_eq!(head_of(Type::int(64)), Some("x64.arg_val_64"));
}
#[test]
fn an_address_arrives_in_a_register_like_the_integer_it_is() {
assert_eq!(head_of(Type::PTR), Some("x64.arg_val_64"));
assert_eq!(
bind(&[Type::PTR], &SYSV),
"mfunc @f {\nblock0:\n %0:gpr($rdi) = x64.arg_val_64\n}\n"
);
assert!(make(&[Type::PTR], Some(Type::PTR), false, &SYSV).2.is_ok());
}
}