use rucc_base::{Interner, Symbol};
use rucc_ir::{Abi, Param, Type};
use rucc_mir as mir;
use rucc_target::x86_64;
use rucc_target::{CallRegs, Constraint, PhysReg, Places, RegClass, Where};
use crate::varargs::Area;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Missing {
OnX87,
InBothFiles,
Width,
NoRoom,
TooBig,
}
impl Missing {
#[must_use]
pub fn why(self) -> &'static str {
match self {
Missing::OnX87 => "is on the x87 stack",
Missing::InBothFiles => "is a float passed to a variadic callee on this convention",
Missing::Width => "is a width no argument register holds",
Missing::NoRoom => "takes more registers than this convention has for it",
Missing::TooBig => "is more bytes than a copy into the argument area unrolls to",
}
}
}
fn class_of(ty: Type, conv: &CallRegs) -> RegClass {
if ty.is_float() { conv.sse_class } else { conv.int_class }
}
#[must_use]
pub fn refuses(ty: Type) -> Option<Missing> {
if head_of(ty).is_some() {
return None;
}
if ty.is_float() && ty.bits() == 80 {
return Some(Missing::OnX87);
}
Some(Missing::Width)
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Arrived {
pub regs: Vec<mir::Reg>,
pub stack: Vec<(mir::Inst, u32)>,
pub took: (usize, usize),
pub used: u32,
pub spare: Vec<(mir::Reg, RegClass, u32)>,
}
pub fn entry(
out: &mut mir::Func,
block: mir::Block,
params: &[Param],
conv: &CallRegs,
names: &mut Interner,
save: Option<Area>,
) -> Result<Arrived, (usize, Missing)> {
let mut places = Places::new(conv);
let mut where_from = Vec::with_capacity(params.len());
for (index, &Param { ty, abi }) in params.iter().enumerate() {
if let Abi::ByVal { size, align } = abi {
let size = u32::try_from(size).map_err(|_| (index, Missing::TooBig))?;
where_from.push((ty, places.on_stack(size, align), abi));
continue;
}
let at = if ty.is_float() { places.float() } else { places.integer() };
if let Some(missing) = refuses(ty) {
return Err((index, missing));
}
where_from.push((ty, at, abi));
}
let mut arrived = Arrived {
regs: Vec::with_capacity(params.len()),
took: (places.integers(), places.floats()),
used: places.size(),
..Arrived::default()
};
for (index, &(ty, at, _)) in where_from.iter().enumerate() {
let class = class_of(ty, conv);
let reg = out.new_vreg(class);
arrived.regs.push(reg);
let Where::Reg(arrived_in) = at else { continue };
let head = head_of(ty).ok_or((index, Missing::Width))?;
let opcode = mir::Opcode::new(names.intern(head));
let operand = mir::Operand::write(reg, class).with(Constraint::Fixed(arrived_in));
out.build(block, opcode).operand(operand).finish();
}
if let Some(area) = save {
arrived.spare = spare(out, block, conv, names, area, arrived.took);
}
let lea = format!("{}{}", crate::lower::PREFIX, x86_64::FRAME.lea);
for (index, &(ty, at, abi)) in where_from.iter().enumerate() {
let Where::Stack(up) = at else { continue };
let class = class_of(ty, conv);
let name = match abi {
Abi::ByVal { .. } => lea.as_str(),
_ => load_of(ty).ok_or((index, Missing::Width))?,
};
let opcode = mir::Opcode::new(names.intern(name));
let sp = mir::Operand::read(mir::Reg::physical(conv.stack_pointer), conv.int_class);
let made =
out.build(block, opcode).def(arrived.regs[index], class).mem(mir::Mem::at(sp)).finish();
arrived.stack.push((made, up));
}
Ok(arrived)
}
fn spare(
out: &mut mir::Func,
block: mir::Block,
conv: &CallRegs,
names: &mut Interner,
area: Area,
took: (usize, usize),
) -> Vec<(mir::Reg, RegClass, u32)> {
let word = Type::int(64);
let double = Type::float(rucc_ir::Float::F64);
let files = [(conv.int_args, took.0, word, false), (conv.sse_args, took.1, double, true)];
let mut spare = Vec::new();
for (regs, taken, ty, float) in files {
let Some(head) = head_of(ty) else { continue };
let class = class_of(ty, conv);
for (index, &arrived_in) in regs.iter().enumerate().skip(taken) {
let reg = out.new_vreg(class);
let opcode = mir::Opcode::new(names.intern(head));
let operand = mir::Operand::write(reg, class).with(Constraint::Fixed(arrived_in));
out.build(block, opcode).operand(operand).finish();
let at = area.starts_at(float) + area.stride(float) * u32::try_from(index).unwrap_or(0);
spare.push((reg, class, at));
}
}
spare
}
pub const CALL: &str = "x64.call";
pub const CALL_REG: &str = "x64.call_reg";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Passing {
pub ty: Type,
pub reg: mir::Reg,
pub abi: Abi,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Made {
pub results: Vec<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, PartialEq, Eq)]
pub enum Callee {
Named(Symbol),
Through(mir::Reg),
}
#[derive(Debug, Clone, Copy)]
pub struct Calling<'a> {
pub callee: Callee,
pub args: &'a [Passing],
pub returns: &'a [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());
let mut on_stack = Vec::new();
let mut vectors = 0u32;
let mut as_bytes = Vec::new();
for (index, &Passing { ty, reg, abi }) in args.iter().enumerate() {
let refused = |missing| Refused { argument: Some(index), missing };
if let Abi::ByVal { size, align } = abi {
let size = u32::try_from(size).map_err(|_| refused(Missing::TooBig))?;
let Where::Stack(up) = places.on_stack(size, align) else {
unreachable!("an object in the argument area is in the argument area")
};
let plan = crate::expand::plan(u64::from(size), align, conv.word)
.ok_or(refused(Missing::TooBig))?;
as_bytes.push((reg, up, plan));
continue;
}
let at = if ty.is_float() { places.float() } else { places.integer() };
if let Some(missing) = refuses(ty) {
return Err(refused(missing));
}
if ty.is_float() && variadic && conv.shared_positions {
return Err(refused(Missing::InBothFiles));
}
let class = class_of(ty, conv);
match at {
Where::Reg(at) => {
if class == conv.sse_class {
vectors += 1;
}
passed.push((reg, at, class));
}
Where::Stack(up) => {
let store = store_of(ty).ok_or(refused(Missing::Width))?;
on_stack.push((reg, class, names.intern(store), up));
}
}
}
let comes_back = places_back(returns, conv)?;
let counted = if variadic { conv.vector_count } else { None };
for (reg, class, store, up) in on_stack {
let sp = mir::Operand::read(mir::Reg::physical(conv.stack_pointer), conv.int_class);
let up = i32::try_from(up).expect("an argument area under two gigabytes");
let build = out.build(block, mir::Opcode::new(store));
build.uses(reg, class).mem(mir::Mem::at(sp).plus(up)).finish();
}
for (from, up, plan) in as_bytes {
let up = i32::try_from(up).expect("an argument area under two gigabytes");
for (at, width) in plan {
let ty = Type::int(width * 8);
let at = i32::try_from(at).expect("an object under two gigabytes");
let word = out.new_vreg(conv.int_class);
let load = names
.intern(load_of(ty).ok_or(Refused { argument: None, missing: Missing::Width })?);
let there = mir::Operand::read(from, conv.int_class);
let build = out.build(block, mir::Opcode::new(load));
build.def(word, conv.int_class).mem(mir::Mem::at(there).plus(at)).finish();
let store = names
.intern(store_of(ty).ok_or(Refused { argument: None, missing: Missing::Width })?);
let sp = mir::Operand::read(mir::Reg::physical(conv.stack_pointer), conv.int_class);
let build = out.build(block, mir::Opcode::new(store));
build.uses(word, conv.int_class).mem(mir::Mem::at(sp).plus(up + at)).finish();
}
}
let mut operands = Vec::with_capacity(args.len() + conv.int_order.len() + 2);
let results: Vec<mir::Reg> = comes_back
.iter()
.map(|&(at, class)| {
let reg = out.new_vreg(class);
operands.push(mir::Operand::write(reg, class).with(Constraint::Fixed(at)));
reg
})
.collect();
let spoken_for = |class: RegClass| -> Vec<PhysReg> {
comes_back
.iter()
.filter(|&&(_, at)| at == class)
.map(|&(reg, _)| reg)
.chain(counted.filter(|_| class == conv.int_class))
.chain(passed.iter().filter(|&&(_, _, at)| at == class).map(|&(_, reg, _)| reg))
.collect()
};
let named = spoken_for(conv.int_class);
for ® in conv.int_order {
if !conv.preserves_int(reg) && !named.contains(®) {
operands.push(mir::Operand::write(mir::Reg::physical(reg), conv.int_class));
}
}
let named = spoken_for(conv.sse_class);
for ® in conv.sse_order {
if !conv.preserves_sse(reg) && !named.contains(®) {
operands.push(mir::Operand::write(mir::Reg::physical(reg), conv.sse_class));
}
}
if let Callee::Through(reg) = callee {
operands.push(mir::Operand::read(reg, conv.int_class));
}
for (reg, at, class) in passed {
operands.push(mir::Operand::read(reg, 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(i64::from(vectors)).finish();
operands.push(mir::Operand::read(count, conv.int_class).with(Constraint::Fixed(at)));
}
let opcode = mir::Opcode::new(names.intern(match callee {
Callee::Named(_) => CALL,
Callee::Through(_) => CALL_REG,
}));
let mut build = out.build(block, opcode);
if let Callee::Named(symbol) = callee {
build = build.symbol(symbol);
}
for operand in operands {
build = build.operand(operand);
}
build.finish();
Ok(Made { results, outgoing: places.size() })
}
fn places_back(returns: &[Type], conv: &CallRegs) -> Result<Vec<(PhysReg, RegClass)>, Refused> {
let refused = |missing| Refused { argument: None, missing };
let mut back = Vec::with_capacity(returns.len());
let (mut ints, mut sses) = (0usize, 0usize);
for &ty in returns {
if let Some(missing) = refuses(ty) {
return Err(refused(missing));
}
let class = class_of(ty, conv);
let (file, at) = if class == conv.sse_class {
(conv.sse_returns, &mut sses)
} else {
(conv.int_returns, &mut ints)
};
let reg = *file.get(*at).ok_or_else(|| refused(Missing::NoRoom))?;
*at += 1;
back.push((reg, class));
}
Ok(back)
}
#[must_use]
pub fn head_of(ty: Type) -> Option<&'static str> {
if let Some(at) = crate::term::float_slot(ty) {
return Some(["x64.arg_val_f32", "x64.arg_val_f64"][at]);
}
let names = ["x64.arg_val_8", "x64.arg_val_16", "x64.arg_val_32", "x64.arg_val_64"];
Some(names[crate::term::slot(ty)?])
}
#[must_use]
pub fn load_of(ty: Type) -> Option<&'static str> {
if let Some(at) = crate::term::float_slot(ty) {
return Some(["x64.movss_rm", "x64.movsd_rm"][at]);
}
let names = ["x64.mov_rm_8", "x64.mov_rm_16", "x64.mov_rm_32", "x64.mov_rm_64"];
Some(names[crate::term::slot(ty)?])
}
#[must_use]
pub fn store_of(ty: Type) -> Option<&'static str> {
if let Some(at) = crate::term::float_slot(ty) {
return Some(["x64.movss_mr", "x64.movsd_mr"][at]);
}
let names = ["x64.mov_mr_8", "x64.mov_mr_16", "x64.mov_mr_32", "x64.mov_mr_64"];
Some(names[crate::term::slot(ty)?])
}
#[must_use]
pub fn ret_of(ty: Type, at: usize) -> Option<&'static str> {
if let Some(width) = crate::term::float_slot(ty) {
let names =
[["x64.ret_val_f32", "x64.ret_val_f64"], ["x64.ret_val2_f32", "x64.ret_val2_f64"]];
return Some(names.get(at)?[width]);
}
let names = [
["x64.ret_val_8", "x64.ret_val_16", "x64.ret_val_32", "x64.ret_val_64"],
["x64.ret_val2_8", "x64.ret_val2_16", "x64.ret_val2_32", "x64.ret_val2_64"],
];
Some(names.get(at)?[crate::term::slot(ty)?])
}
#[cfg(test)]
mod tests {
use rucc_target::x86_64::{REGS, SYSV, WIN64};
use super::*;
fn plain(params: &[Type]) -> Vec<Param> {
params.iter().copied().map(Param::new).collect()
}
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, &plain(params), conv, &mut names, None)
.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"
);
}
fn arrive(params: &[Type], conv: &CallRegs) -> (String, Vec<u32>) {
let mut names = Interner::new();
let mut out = mir::Func::new(names.intern("f"));
let block = out.create_block();
let arrived = entry(&mut out, block, &plain(params), conv, &mut names, None)
.expect("every parameter");
let up = arrived.stack.iter().map(|&(_, up)| up).collect();
(mir::print_func(&out, &names, ®S), up)
}
#[test]
fn an_argument_past_the_last_register_is_read_out_of_the_caller_s_stack() {
let (text, up) = arrive(&[Type::int(64); 7], &SYSV);
assert_eq!(up, [0]);
assert!(text.contains("%6:gpr = x64.mov_rm_64 [$rsp]"), "{text}");
assert_eq!(text.matches("x64.arg_val_64").count(), 6, "{text}");
}
#[test]
fn the_other_convention_runs_out_of_registers_three_arguments_earlier() {
let (text, up) = arrive(&[Type::int(64); 7], &WIN64);
assert_eq!(up, [32, 40, 48]);
assert_eq!(text.matches("x64.arg_val_64").count(), 4, "{text}");
assert!(text.contains("%4:gpr = x64.mov_rm_64 [$rsp]"), "{text}");
}
fn arrive_with(params: &[Param], conv: &CallRegs) -> (String, Vec<u32>) {
let mut names = Interner::new();
let mut out = mir::Func::new(names.intern("f"));
let block = out.create_block();
let arrived =
entry(&mut out, block, params, conv, &mut names, None).expect("every parameter");
let up = arrived.stack.iter().map(|&(_, up)| up).collect();
(mir::print_func(&out, &names, ®S), up)
}
#[test]
fn a_structure_that_arrived_as_bytes_is_an_address_and_not_a_load() {
let byval = Param::with_abi(Type::PTR, Abi::ByVal { size: 32, align: 8 });
let (text, up) =
arrive_with(&[Param::new(Type::int(32)), byval, Param::new(Type::int(32))], &SYSV);
assert_eq!(up, [0]);
assert_eq!(text.matches("x64.arg_val_32").count(), 2, "{text}");
assert!(text.contains("%2:gpr = x64.lea_64 [$rsp]"), "{text}");
assert!(!text.contains("mov_rm"), "nothing is read out of the bytes: {text}");
}
#[test]
fn the_argument_behind_a_structure_that_travelled_as_bytes_is_above_all_of_them() {
let byval = Param::with_abi(Type::PTR, Abi::ByVal { size: 24, align: 16 });
let params: Vec<Param> = (0..7).map(|_| Param::new(Type::int(64))).collect();
let (_, up) = arrive_with(&[¶ms[..], &[byval], ¶ms[..1]].concat(), &SYSV);
assert_eq!(up, [0, 16, 40]);
}
#[test]
fn what_a_stack_argument_is_read_with_is_its_own_width_and_its_own_file() {
let f32 = Type::float(rucc_ir::Float::F32);
let params = [Type::int(64), Type::int(64), Type::int(64), Type::int(64), Type::int(8)];
let (text, up) = arrive(¶ms, &WIN64);
assert_eq!(up, [32]);
assert!(text.contains("x64.mov_rm_8 [$rsp]"), "{text}");
let floats = [f32; 5];
let (text, up) = arrive(&floats, &WIN64);
assert_eq!(up, [32]);
assert!(text.contains("%4:xmm = x64.movss_rm [$rsp]"), "{text}");
}
#[test]
fn the_two_lists_of_widths_answer_for_the_same_types() {
let types = [
Type::int(1),
Type::int(8),
Type::int(16),
Type::int(32),
Type::int(64),
Type::int(128),
Type::PTR,
Type::float(rucc_ir::Float::F32),
Type::float(rucc_ir::Float::F64),
Type::float(rucc_ir::Float::F80),
];
for ty in types {
assert_eq!(head_of(ty).is_some(), load_of(ty).is_some(), "{ty:?}");
}
}
#[test]
fn a_float_arrives_in_a_vector_register_and_is_counted_apart_from_the_integers() {
let f32 = Type::float(rucc_ir::Float::F32);
let f64 = Type::float(rucc_ir::Float::F64);
assert_eq!(
bind(&[Type::int(32), f64, f32], &SYSV),
"mfunc @f {\nblock0:\n %0:gpr($rdi) = x64.arg_val_32\n \
%1:xmm($xmm0) = x64.arg_val_f64\n %2:xmm($xmm1) = x64.arg_val_f32\n}\n"
);
}
#[test]
fn the_other_convention_counts_the_two_files_as_one_run_of_positions() {
let f64 = Type::float(rucc_ir::Float::F64);
assert_eq!(
bind(&[Type::int(32), f64, Type::int(64)], &WIN64),
"mfunc @f {\nblock0:\n %0:gpr($rcx) = x64.arg_val_32\n \
%1:xmm($xmm1) = x64.arg_val_f64\n %2:gpr($r8) = x64.arg_val_64\n}\n"
);
}
#[test]
fn a_long_double_is_reported_as_the_x87_stack_it_travels_on() {
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::F80)];
let made = entry(&mut out, block, &plain(¶ms), &SYSV, &mut names, None);
assert_eq!(made, Err((1, Missing::OnX87)));
assert_eq!(
make(&[], &[Type::float(rucc_ir::Float::F80)], false, &SYSV).2,
Err(Refused { argument: None, missing: Missing::OnX87 })
);
}
fn make(
args: &[Type],
returns: &[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<Passing> = args
.iter()
.map(|&ty| Passing {
ty,
reg: out.append_param(block, class_of(ty, conv)),
abi: Abi::Plain,
})
.collect();
let callee = Callee::Named(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], &[], false, &SYSV);
assert_eq!(made.expect("three integers all fit in registers").results, []);
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], &[], 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(&[], &[Type::int(32)], false, &SYSV);
let made = made.expect("an integer comes back");
let [result] = made.results[..] else { panic!("one 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)], &[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)], &[], 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")
);
let f64 = Type::float(rucc_ir::Float::F64);
let (names, func, made) = make(&[Type::int(64), f64, f64], &[], true, &SYSV);
made.expect("one integer and two floats all fit in registers");
assert_eq!(operands(&func).1, ["rdi", "xmm0", "xmm1", "rax"]);
assert!(mir::print_func(&func, &names, ®S).contains("x64.mov_ri_32 2"));
}
#[test]
fn a_float_passed_to_a_variadic_callee_on_windows_is_reported() {
let f64 = Type::float(rucc_ir::Float::F64);
assert_eq!(
make(&[Type::int(32), f64], &[], true, &WIN64).2,
Err(Refused { argument: Some(1), missing: Missing::InBothFiles })
);
assert!(make(&[Type::int(32), f64], &[], false, &WIN64).2.is_ok());
}
#[test]
fn a_call_through_an_address_reads_it_in_front_of_the_arguments() {
let i32 = Type::int(32);
let mut names = Interner::new();
let mut out = mir::Func::new(names.intern("f"));
let block = out.create_block();
let address = out.append_param(block, SYSV.int_class);
let reg = out.append_param(block, SYSV.int_class);
let passed = vec![Passing { ty: i32, reg, abi: Abi::Plain }];
let what = Calling {
callee: Callee::Through(address),
args: &passed,
returns: &[i32],
variadic: false,
};
call(&mut out, block, &what, &SYSV, &mut names).expect("one integer fits in a register");
let text = mir::print_func(&out, &names, ®S);
assert!(text.contains("= x64.call_reg %0, %1($rdi)\n"), "{text}");
assert!(!text.contains("@g"), "a call through an address names nobody: {text}");
}
#[test]
fn a_call_with_no_register_left_writes_the_argument_into_the_outgoing_area() {
let i64 = Type::int(64);
let (names, func, made) = make(&[i64; 7], &[], false, &SYSV);
let made = made.expect("the seventh goes to memory");
let text = mir::print_func(&func, &names, ®S);
assert!(text.contains("x64.mov_mr_64 %6, [$rsp]\n"), "{text}");
let store = text.find("x64.mov_mr_64").expect("the store");
assert!(store < text.find("x64.call").expect("the call"), "{text}");
assert_eq!(made.outgoing, 8);
}
fn pass_bytes(
before: usize,
size: u64,
align: u32,
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 mut args: Vec<Passing> = (0..before)
.map(|_| Passing {
ty: Type::int(64),
reg: out.append_param(block, conv.int_class),
abi: Abi::Plain,
})
.collect();
args.push(Passing {
ty: Type::PTR,
reg: out.append_param(block, conv.int_class),
abi: Abi::ByVal { size, align },
});
args.push(Passing {
ty: Type::int(32),
reg: out.append_param(block, conv.int_class),
abi: Abi::Plain,
});
let callee = Callee::Named(names.intern("g"));
let what = Calling { callee, args: &args, returns: &[], variadic: false };
let made = call(&mut out, block, &what, conv, &mut names);
(names, out, made)
}
#[test]
fn a_structure_passed_by_value_in_memory_is_copied_into_the_outgoing_area() {
let (names, func, made) = pass_bytes(1, 24, 8, &SYSV);
let made = made.expect("an object of three words is copied a word at a time");
let text = mir::print_func(&func, &names, ®S);
assert!(text.contains("x64.mov_mr_64 %3, [$rsp]\n"), "{text}");
assert!(text.contains("x64.mov_mr_64 %4, [$rsp + 8]\n"), "{text}");
assert!(text.contains("x64.mov_mr_64 %5, [$rsp + 16]\n"), "{text}");
assert_eq!(text.matches("x64.mov_rm_64").count(), 3, "{text}");
assert!(text.find("x64.mov_mr_64") < text.find("x64.call"), "{text}");
assert_eq!(made.outgoing, 24);
}
#[test]
fn the_integers_beside_it_still_travel_in_registers() {
let (_, func, _) = pass_bytes(1, 24, 8, &SYSV);
let (clobbered, read) = operands(&func);
assert_eq!(read, ["rdi", "rsi"]);
assert!(clobbered.contains(&"rdx".to_owned()), "the third is free: {clobbered:?}");
}
#[test]
fn an_object_wanting_more_alignment_than_a_word_gets_it() {
let (names, func, made) = pass_bytes(7, 24, 16, &SYSV);
let made = made.expect("an object of three words");
let text = mir::print_func(&func, &names, ®S);
assert!(text.contains("x64.mov_mr_64 %9, [$rsp + 16]\n"), "{text}");
assert!(text.contains("x64.mov_mr_32 %8, [$rsp + 40]\n"), "{text}");
assert_eq!(made.outgoing, 48);
}
#[test]
fn an_object_too_large_to_copy_a_word_at_a_time_is_reported_rather_than_passed() {
let (_, _, made) = pass_bytes(1, 4096, 8, &SYSV);
assert_eq!(made, Err(Refused { argument: Some(1), missing: Missing::TooBig }));
assert_eq!(
Missing::TooBig.why(),
"is more bytes than a copy into the argument area unrolls to"
);
}
#[test]
fn where_the_outgoing_area_starts_is_the_convention_s_answer() {
let i64 = Type::int(64);
let (names, func, made) = make(&[i64; 7], &[], false, &WIN64);
assert_eq!(made.expect("the last three go to memory").outgoing, 56);
let text = mir::print_func(&func, &names, ®S);
assert!(text.contains("x64.mov_mr_64 %4, [$rsp + 32]\n"), "{text}");
assert!(text.contains("x64.mov_mr_64 %5, [$rsp + 40]\n"), "{text}");
assert!(text.contains("x64.mov_mr_64 %6, [$rsp + 48]\n"), "{text}");
}
#[test]
fn a_narrow_or_floating_argument_keeps_its_own_store() {
let i64 = Type::int(64);
let narrow = [i64, i64, i64, i64, i64, i64, Type::int(8)];
let (names, func, made) = make(&narrow, &[], false, &SYSV);
made.expect("the seventh goes to memory");
let text = mir::print_func(&func, &names, ®S);
assert!(text.contains("x64.mov_mr_8 %6, [$rsp]\n"), "{text}");
let f32 = Type::float(rucc_ir::Float::F32);
let (names, func, made) = make(&[f32; 9], &[], false, &SYSV);
made.expect("the ninth goes to memory");
let text = mir::print_func(&func, &names, ®S);
assert!(text.contains("x64.movss_mr %8, [$rsp]\n"), "{text}");
}
#[test]
fn an_argument_in_memory_is_not_counted_as_a_vector_register() {
let f64 = Type::float(rucc_ir::Float::F64);
let (names, func, made) = make(&[f64; 9], &[], true, &SYSV);
made.expect("the ninth goes to memory");
let text = mir::print_func(&func, &names, ®S);
assert!(text.contains("x64.mov_ri_32 8\n"), "eight registers, not nine: {text}");
}
#[test]
fn what_can_be_read_can_be_written() {
let types = [
Type::int(1),
Type::int(8),
Type::int(16),
Type::int(32),
Type::int(64),
Type::int(128),
Type::PTR,
Type::float(rucc_ir::Float::F32),
Type::float(rucc_ir::Float::F64),
Type::float(rucc_ir::Float::F80),
];
for ty in types {
assert_eq!(load_of(ty).is_some(), store_of(ty).is_some(), "{ty:?}");
}
}
#[test]
fn a_call_passes_and_returns_a_float_in_a_vector_register() {
let f64 = Type::float(rucc_ir::Float::F64);
let (_, func, made) = make(&[Type::int(32), f64], &[f64], false, &SYSV);
let result = made.expect("an integer and a float both fit in registers");
let (written, read) = operands(&func);
assert_eq!(read, ["rdi", "xmm0"]);
assert_eq!(written.first().map(String::as_str), Some("xmm0"));
assert_eq!(func.class_of(result.results[0]), Some(SYSV.sse_class));
assert_eq!(written.iter().filter(|name| *name == "xmm0").count(), 1);
assert!(written.contains(&"rax".to_string()));
}
#[test]
fn a_call_at_a_width_no_register_holds_is_reported_on_either_side() {
let i128 = Type::int(128);
assert_eq!(
make(&[i128], &[], false, &SYSV).2,
Err(Refused { argument: Some(0), missing: Missing::Width })
);
assert_eq!(
make(&[], &[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], &[Type::PTR], false, &SYSV).2.is_ok());
}
}