use std::fmt;
use rucc_base::Interner;
use rucc_ir::{Block, Def, Extra, Func, Inst, Opcode, Type, Value};
use rucc_mir as mir;
use rucc_target::x86_64;
use rucc_target::{CallRegs, RegClass};
use crate::abi::{self, Missing, Refused};
use crate::frame::Layout;
use crate::select::{Match, Piece, Rule, Table};
use crate::term::{MAX_ARGS, PLAIN, Plan, Shown, Term, Terms};
const PREFIX: &str = "x64.";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Unsupported {
Inst {
inst: Inst,
term: Option<&'static str>,
},
Argument {
index: usize,
missing: Missing,
},
Call {
inst: Inst,
refused: Refused,
},
Indirect {
inst: Inst,
},
}
impl fmt::Display for Unsupported {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Unsupported::Inst { term: Some(term), .. } => write!(f, "no rule lowers `{term}`"),
Unsupported::Inst { term: None, .. } => f.write_str("no rule lowers this instruction"),
Unsupported::Argument { index, missing } => {
write!(f, "parameter {index} {}", missing.why())
}
Unsupported::Call { refused: Refused { argument: Some(index), missing }, .. } => {
write!(f, "argument {index} of this call {}", missing.why())
}
Unsupported::Call { refused: Refused { argument: None, missing }, .. } => {
write!(f, "what this call gives back {}", missing.why())
}
Unsupported::Indirect { .. } => f.write_str("no rule calls through an address"),
}
}
}
impl std::error::Error for Unsupported {}
#[derive(Debug)]
pub struct Lowered {
pub func: mir::Func,
pub calls: Option<u32>,
}
impl Lowered {
#[must_use]
pub fn layout<'a>(&self, base: Layout<'a>) -> Layout<'a> {
Layout { leaf: self.calls.is_none(), outgoing: self.calls.unwrap_or(0), ..base }
}
}
pub fn func(
source: &Func,
names: &mut Interner,
conv: &'static CallRegs,
) -> Result<Lowered, Unsupported> {
Lowering::new(source, names, conv).run()
}
struct Lowering<'a> {
source: &'a Func,
names: &'a mut Interner,
out: mir::Func,
regs: Vec<Option<mir::Reg>>,
uses: Vec<u32>,
at: Option<mir::Block>,
blocks: Vec<Option<mir::Block>>,
gpr: RegClass,
conv: &'static CallRegs,
calls: Option<u32>,
}
impl<'a> Lowering<'a> {
fn new(source: &'a Func, names: &'a mut Interner, conv: &'static CallRegs) -> Self {
let counts = source.counts();
let name = source.name;
let mut uses = vec![0; counts.values];
for block in source.blocks() {
for inst in source.insts(block) {
for &arg in &source[source[inst].args] {
uses[arg.index()] += 1;
}
for call in source.successors(inst) {
for &arg in &source[call.args] {
uses[arg.index()] += 1;
}
}
}
}
Self {
source,
names,
out: mir::Func::new(name),
regs: vec![None; counts.values],
blocks: vec![None; counts.blocks],
uses,
at: None,
gpr: x86_64::GPR,
conv,
calls: None,
}
}
fn run(mut self) -> Result<Lowered, Unsupported> {
for block in self.source.blocks() {
let out = self.out.create_block();
self.blocks[block.index()] = Some(out);
}
for block in self.source.blocks() {
self.block(block)?;
}
Ok(Lowered { func: self.out, calls: self.calls })
}
fn block(&mut self, block: Block) -> Result<(), Unsupported> {
let out = self.out_block(block);
self.at = Some(out);
if self.source.entry() == Some(block) {
self.arrive(block, out)?;
} else {
for ¶m in self.source[block].params.iter() {
let reg = self.out.append_param(out, self.gpr);
self.regs[param.index()] = Some(reg);
}
}
let insts: Vec<Inst> = self.source.insts(block).collect();
let mut found: Vec<Option<Match<Term>>> = (0..insts.len()).map(|_| None).collect();
let mut folded: Vec<Inst> = Vec::new();
for (index, &inst) in insts.iter().enumerate().rev() {
if folded.contains(&inst) {
continue;
}
if let Some((plan, matched)) = self.select(inst) {
folded.extend(self.folds(inst, plan));
found[index] = Some(matched);
}
}
for (&inst, matched) in insts.iter().zip(found) {
if folded.contains(&inst) || self.writes_nothing(inst) {
continue;
}
match self.source[inst].opcode {
Opcode::Call => {
self.called(inst)?;
continue;
}
Opcode::CallIndirect => return Err(Unsupported::Indirect { inst }),
_ => {}
}
let matched = matched.ok_or_else(|| self.unsupported(inst))?;
self.emit(inst, &matched)?;
}
self.edges(block, out)
}
fn called(&mut self, inst: Inst) -> Result<(), Unsupported> {
let data = &self.source[inst];
let Extra::Call(info) = data.extra else { return Err(self.unsupported(inst)) };
let info = self.source[info];
let Some(callee) = info.callee else { return Err(Unsupported::Indirect { inst }) };
let values: Vec<Value> = self.source[data.args].to_vec();
let mut args = Vec::with_capacity(values.len());
for value in values {
args.push((self.source[value].ty, self.reg_of(value)?));
}
let signature = &self.source[info.signature];
let variadic = signature.variadic;
let returns = signature.return_types().next();
if signature.return_types().count() > 1 {
return Err(self.unsupported(inst));
}
let block = self.at.expect("a block is being filled");
let what = abi::Calling { callee, args: &args, returns, variadic };
let made = abi::call(&mut self.out, block, &what, self.conv, self.names)
.map_err(|refused| Unsupported::Call { inst, refused })?;
self.calls = Some(self.calls.unwrap_or(0).max(made.outgoing));
if let (Some(result), Some(reg)) = (data.first_result, made.result) {
self.regs[result.index()] = Some(reg);
}
Ok(())
}
fn edges(&mut self, block: Block, out: mir::Block) -> Result<(), Unsupported> {
let Some(term) = self.source.terminator(block) else { return Ok(()) };
let calls: Vec<rucc_ir::BlockCall> = self.source.successors(term).collect();
let mut succs = Vec::with_capacity(calls.len());
for call in calls {
let args: Vec<Value> = self.source[call.args].to_vec();
let mut regs = Vec::with_capacity(args.len());
for value in args {
regs.push(self.reg_of(value)?);
}
succs.push(mir::BlockCall { block: self.out_block(call.block), args: regs });
}
*self.out.succs_mut(out) = succs;
Ok(())
}
fn out_block(&self, block: Block) -> mir::Block {
self.blocks[block.index()].expect("every block was created before any was filled")
}
fn arrive(&mut self, block: Block, out: mir::Block) -> Result<(), Unsupported> {
let params = self.source[block].params.clone();
let types: Vec<Type> = params.iter().map(|&value| self.source[value].ty).collect();
let regs = abi::entry(&mut self.out, out, &types, self.conv, self.names)
.map_err(|(index, missing)| Unsupported::Argument { index, missing })?;
for (¶m, reg) in params.iter().zip(regs) {
self.regs[param.index()] = Some(reg);
}
Ok(())
}
fn writes_nothing(&self, inst: Inst) -> bool {
let data = &self.source[inst];
match data.opcode {
Opcode::IConst | Opcode::Jump => true,
Opcode::Return => self.source[data.args].is_empty(),
_ => false,
}
}
fn select(&self, inst: Inst) -> Option<(Plan, Match<Term>)> {
for plan in self.plans(inst) {
let terms = Terms::new(self.source, inst, plan);
if let Some(matched) = TABLE.find(&terms, Term::Root) {
return Some((plan, matched));
}
}
None
}
fn plans(&self, inst: Inst) -> Vec<Plan> {
let args = &self.source[self.source[inst].args];
let mut plans = vec![PLAIN];
for (index, &arg) in args.iter().enumerate().take(MAX_ARGS) {
let mut ways = Vec::new();
if self.foldable(inst, arg) {
ways.push(Shown::Expand);
}
if Terms::new(self.source, inst, PLAIN).constant(arg).is_some() {
ways.push(Shown::Const);
}
ways.push(Shown::Reg);
plans = plans
.into_iter()
.flat_map(|plan| {
ways.iter().map(move |&way| {
let mut next = plan;
next[index] = way;
next
})
})
.collect();
}
plans
}
fn foldable(&self, into: Inst, value: Value) -> bool {
let Def::Result { inst, .. } = self.source[value].def else { return false };
if self.source[inst].opcode == Opcode::IConst || self.uses[value.index()] != 1 {
return false;
}
self.source.block_of(inst).is_some()
&& self.source.block_of(inst) == self.source.block_of(into)
}
fn folds(&self, inst: Inst, plan: Plan) -> Vec<Inst> {
let args = &self.source[self.source[inst].args];
args.iter()
.take(MAX_ARGS)
.enumerate()
.filter(|&(index, _)| plan[index] == Shown::Expand)
.filter_map(|(_, &arg)| match self.source[arg].def {
Def::Result { inst, .. } => Some(inst),
Def::Param { .. } => None,
})
.collect()
}
fn emit(&mut self, inst: Inst, matched: &Match<Term>) -> Result<(), Unsupported> {
let rule: &Rule = TABLE.rule(matched);
let pieces = rule.replacement;
let Some(Piece::App { head, arity }) = pieces.first() else {
return Err(self.unsupported(inst));
};
let opcode = head.strip_prefix(PREFIX).ok_or_else(|| self.unsupported(inst))?;
let form = x86_64::form(opcode).ok_or_else(|| self.unsupported(inst))?;
let mut read = Read::default();
let mut at = 1;
for _ in 0..*arity {
at = self.read(inst, pieces, at, &matched.bindings, &mut read)?;
}
let descs = form.operands();
let writes = descs.iter().take_while(|desc| desc.role.is_def()).count();
if descs.len() - writes != read.regs.len() {
return Err(self.unsupported(inst));
}
let mut regs = Vec::new();
if writes > 0 {
let result = self.source[inst].first_result.ok_or_else(|| self.unsupported(inst))?;
regs.push(self.new_reg(result));
regs.extend((1..writes).map(|_| self.out.new_vreg(self.gpr)));
} else if self.source[inst].first_result.is_some() {
return Err(self.unsupported(inst));
}
regs.extend(read.regs.iter().copied());
let block = self.at.expect("a block is being filled");
let opcode = mir::Opcode::new(self.names.intern(head));
let mut build = self.out.build(block, opcode).at(self.source.span(inst));
for (desc, reg) in descs.iter().zip(regs) {
let operand = mir::Operand {
reg,
class: desc.class,
role: desc.role,
constraint: desc.constraint,
};
build = build.operand(operand);
}
if let Some(mem) = read.mem {
build = build.mem(mem);
}
if let Some(imm) = read.imm {
build = build.imm(imm);
}
build.finish();
Ok(())
}
fn read(
&mut self,
inst: Inst,
pieces: &'static [Piece],
at: usize,
bindings: &[Term],
out: &mut Read,
) -> Result<usize, Unsupported> {
match pieces.get(at) {
Some(Piece::Int(value)) => {
out.imm = i64::try_from(*value).ok();
Ok(at + 1)
}
Some(Piece::Var { index, .. }) => {
match bindings.get(*index) {
Some(&Term::Reg(value)) => {
let reg = self.reg_of(value)?;
out.regs.push(reg);
}
Some(&Term::Num(value)) => out.imm = i64::try_from(value).ok(),
_ => return Err(self.unsupported(inst)),
}
Ok(at + 1)
}
Some(Piece::App { head, arity }) => {
let kind = x86_64::address(head).ok_or_else(|| self.unsupported(inst))?;
let mut inner = Read::default();
let mut next = at + 1;
for _ in 0..*arity {
next = self.read(inst, pieces, next, bindings, &mut inner)?;
}
let mem = address(kind, &inner, self.gpr).ok_or_else(|| self.unsupported(inst))?;
out.mem = Some(mem);
Ok(next)
}
None => Err(self.unsupported(inst)),
}
}
fn reg_of(&mut self, value: Value) -> Result<mir::Reg, Unsupported> {
if let Some(reg) = self.regs[value.index()] {
return Ok(reg);
}
let constant = match self.source[value].def {
Def::Result { inst, .. } => {
(self.source[inst].opcode == Opcode::IConst).then_some(inst)
}
Def::Param { .. } => None,
};
if let Some(inst) = constant {
let matched = self
.select(inst)
.map(|(_, matched)| matched)
.ok_or_else(|| self.unsupported(inst))?;
self.emit(inst, &matched)?;
return Ok(self.regs[value.index()].expect("a constant is written into a register"));
}
Ok(self.new_reg(value))
}
fn new_reg(&mut self, value: Value) -> mir::Reg {
if let Some(reg) = self.regs[value.index()] {
return reg;
}
let reg = self.out.new_vreg(self.gpr);
self.regs[value.index()] = Some(reg);
reg
}
fn unsupported(&self, inst: Inst) -> Unsupported {
Unsupported::Inst { inst, term: Terms::new(self.source, inst, PLAIN).name(inst) }
}
}
#[derive(Debug, Default)]
struct Read {
regs: Vec<mir::Reg>,
imm: Option<i64>,
mem: Option<mir::Mem>,
}
fn address(kind: x86_64::Address, read: &Read, gpr: RegClass) -> Option<mir::Mem> {
let mut regs = read.regs.iter().copied().map(|reg| mir::Operand::read(reg, gpr));
match kind {
x86_64::Address::BaseIndexScale => {
let base = regs.next()?;
let index = regs.next()?;
Some(mir::Mem::at(base).indexed(index, u8::try_from(read.imm?).ok()?))
}
x86_64::Address::IndexScale => Some(mir::Mem {
base: None,
index: Some(regs.next()?),
scale: u8::try_from(read.imm?).ok()?,
disp: 0,
symbol: None,
}),
x86_64::Address::Base => Some(mir::Mem::at(regs.next()?)),
x86_64::Address::BaseOffset => {
Some(mir::Mem { disp: i32::try_from(read.imm?).ok()?, ..mir::Mem::at(regs.next()?) })
}
}
}
static TABLE: &Table = &crate::select::x86_64::TABLE;
#[cfg(test)]
mod tests {
use rucc_ir::{Builder, CallInfo, Flags, InstData, MemInfo, MemOrder, Signature, Type};
use rucc_regalloc::assign::Env;
use rucc_target::x86_64::{FRAME, REGS, SYSV};
use super::*;
use crate::finish::finish;
use crate::frame::{Frame, Layout};
fn blank(params: &[Type]) -> (Interner, Func, Block, Vec<Value>) {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"), Signature::new());
let block = func.create_block();
let values = params.iter().map(|&ty| func.append_param(block, ty)).collect();
(names, func, block, values)
}
fn plain() -> MemInfo {
MemInfo { size: 0, align: 1, order: MemOrder::NotAtomic, tbaa: None }
}
fn env() -> Env {
const SCRATCH: [rucc_target::PhysReg; 2] = [x86_64::R10, x86_64::R11];
let order: Vec<rucc_target::PhysReg> =
SYSV.int_order.iter().copied().filter(|reg| !SCRATCH.contains(reg)).collect();
Env::new().with(x86_64::GPR, &order, &SCRATCH)
}
fn lower(names: &mut Interner, source: &Func) -> String {
let out = func(source, names, &SYSV).expect("every instruction has a rule");
mir::print_func(&out.func, names, ®S)
}
#[test]
fn an_addition_of_two_registers_is_one_instruction() {
let i32 = Type::int(32);
let (mut names, mut func, block, args) = blank(&[i32, i32]);
let mut build = Builder::new(&mut func, block);
build.binary(Opcode::Add, args[0], args[1], Flags::default());
assert_eq!(
lower(&mut names, &func),
"mfunc @f {\nblock0:\n %0:gpr($rdi) = x64.arg_val_32\n \
%1:gpr($rsi) = x64.arg_val_32\n %2:gpr(reuse 1) = x64.add_rr_32 %0, %1\n}\n"
);
}
#[test]
fn a_constant_operand_becomes_an_immediate() {
let i32 = Type::int(32);
let (mut names, mut func, block, args) = blank(&[i32]);
let mut build = Builder::new(&mut func, block);
let seven = build.iconst(i32, 7);
build.binary(Opcode::Add, args[0], seven, Flags::default());
assert_eq!(
lower(&mut names, &func),
"mfunc @f {\nblock0:\n %0:gpr($rdi) = x64.arg_val_32\n \
%1:gpr(reuse 1) = x64.add_ri_32 %0, 7\n}\n"
);
}
#[test]
fn a_constant_too_wide_for_an_immediate_goes_into_a_register() {
let i64 = Type::int(64);
let (mut names, mut func, block, args) = blank(&[i64]);
let mut build = Builder::new(&mut func, block);
let big = build.iconst(i64, i128::from(i32::MAX) + 1);
build.binary(Opcode::Add, args[0], big, Flags::default());
assert_eq!(
lower(&mut names, &func),
"mfunc @f {\nblock0:\n %0:gpr($rdi) = x64.arg_val_64\n \
%1:gpr = x64.mov_ri_64 2147483648\n %2:gpr(reuse 1) = x64.add_rr_64 %0, %1\n}\n"
);
}
#[test]
fn an_index_calculation_folds_into_an_address() {
let i64 = Type::int(64);
let (mut names, mut func, block, args) = blank(&[i64, i64]);
let mut build = Builder::new(&mut func, block);
let four = build.iconst(i64, 4);
let scaled = build.binary(Opcode::Mul, args[1], four, Flags::default());
build.binary(Opcode::Add, args[0], scaled, Flags::default());
assert_eq!(
lower(&mut names, &func),
"mfunc @f {\nblock0:\n %0:gpr($rdi) = x64.arg_val_64\n \
%1:gpr($rsi) = x64.arg_val_64\n %2:gpr = x64.lea_64 [%0 + %1*4]\n}\n"
);
}
#[test]
fn an_instruction_read_twice_is_not_folded_into_either_reader() {
let i64 = Type::int(64);
let (mut names, mut func, block, args) = blank(&[i64, i64]);
let mut build = Builder::new(&mut func, block);
let four = build.iconst(i64, 4);
let scaled = build.binary(Opcode::Mul, args[1], four, Flags::default());
let first = build.binary(Opcode::Add, args[0], scaled, Flags::default());
build.binary(Opcode::Add, first, scaled, Flags::default());
let text = lower(&mut names, &func);
assert!(text.contains("x64.lea_64 [%1*4]"), "{text}");
assert_eq!(text.matches("x64.add_rr_64").count(), 2, "{text}");
}
#[test]
fn a_shift_by_a_register_asks_for_it_in_cl() {
let i32 = Type::int(32);
let (mut names, mut func, block, args) = blank(&[i32, i32]);
let mut build = Builder::new(&mut func, block);
build.binary(Opcode::Shl, args[0], args[1], Flags::default());
let text = lower(&mut names, &func);
assert!(text.contains("x64.shl_rcl_32 %0, %1($rcx)"), "{text}");
}
#[test]
fn a_division_names_the_registers_and_the_register_it_destroys() {
let i32 = Type::int(32);
let (mut names, mut func, block, args) = blank(&[i32, i32]);
let mut build = Builder::new(&mut func, block);
build.binary(Opcode::SDiv, args[0], args[1], Flags::default());
let text = lower(&mut names, &func);
assert!(
text.contains("%2:gpr($rax), early %3:gpr($rdx) = x64.idiv_quo_32 %0($rax), %1"),
"{text}"
);
}
#[test]
fn a_load_reads_through_the_register_the_address_is_in() {
let i64 = Type::int(64);
let (mut names, mut func, block, args) = blank(&[i64]);
let mut build = Builder::new(&mut func, block);
build.load(Type::int(32), args[0], plain(), Flags::default());
assert_eq!(
lower(&mut names, &func),
"mfunc @f {\nblock0:\n %0:gpr($rdi) = x64.arg_val_64\n \
%1:gpr = x64.mov_rm_32 [%0]\n}\n"
);
}
#[test]
fn a_store_writes_no_register_and_the_value_it_writes_is_the_one_the_ir_gave_it() {
let (mut names, mut func, block, args) = blank(&[Type::int(32), Type::int(64)]);
let mut build = Builder::new(&mut func, block);
build.store(args[0], args[1], plain(), Flags::default());
assert_eq!(
lower(&mut names, &func),
"mfunc @f {\nblock0:\n %0:gpr($rdi) = x64.arg_val_32\n \
%1:gpr($rsi) = x64.arg_val_64\n x64.mov_mr_32 %0, [%1]\n}\n"
);
}
#[test]
fn an_address_with_a_constant_added_folds_into_the_access() {
let i64 = Type::int(64);
let (mut names, mut func, block, args) = blank(&[i64]);
let mut build = Builder::new(&mut func, block);
let twelve = build.iconst(i64, 12);
let field = build.binary(Opcode::Add, args[0], twelve, Flags::default());
build.load(Type::int(64), field, plain(), Flags::default());
assert_eq!(
lower(&mut names, &func),
"mfunc @f {\nblock0:\n %0:gpr($rdi) = x64.arg_val_64\n \
%1:gpr = x64.mov_rm_64 [%0 + 12]\n}\n"
);
}
#[test]
fn a_displacement_too_wide_to_encode_leaves_the_addition_where_it_is() {
let i64 = Type::int(64);
let (mut names, mut func, block, args) = blank(&[i64]);
let mut build = Builder::new(&mut func, block);
let big = build.iconst(i64, i128::from(i32::MAX) + 1);
let far = build.binary(Opcode::Add, args[0], big, Flags::default());
build.load(Type::int(32), far, plain(), Flags::default());
let text = lower(&mut names, &func);
assert!(text.contains("x64.mov_rm_32 [%2]"), "{text}");
assert!(text.contains("x64.add_rr_64"), "{text}");
}
#[test]
fn a_store_of_a_value_that_was_loaded_is_two_instructions_and_no_arithmetic() {
let i64 = Type::int(64);
let (mut names, mut func, block, args) = blank(&[i64, i64]);
let mut build = Builder::new(&mut func, block);
let got = build.load(Type::int(8), args[0], plain(), Flags::default());
build.store(got, args[1], plain(), Flags::default());
assert_eq!(
lower(&mut names, &func),
"mfunc @f {\nblock0:\n %0:gpr($rdi) = x64.arg_val_64\n \
%1:gpr($rsi) = x64.arg_val_64\n %2:gpr = x64.mov_rm_8 [%0]\n \
x64.mov_mr_8 %2, [%1]\n}\n"
);
}
#[test]
fn an_access_at_a_width_no_rule_is_written_at_is_reported() {
let i64 = Type::int(64);
let (mut names, mut source, block, args) = blank(&[i64]);
let mut build = Builder::new(&mut source, block);
build.load(Type::int(128), args[0], plain(), Flags::default());
let failed = func(&source, &mut names, &SYSV).expect_err("nothing loads 128 bits");
assert_eq!(failed.to_string(), "no rule lowers this instruction");
}
#[test]
fn a_return_asks_for_the_value_in_the_register_the_caller_reads() {
let (mut names, mut func, block, args) = blank(&[Type::int(32)]);
let mut build = Builder::new(&mut func, block);
build.ret(&[args[0]]);
assert_eq!(
lower(&mut names, &func),
"mfunc @f {\nblock0:\n %0:gpr($rdi) = x64.arg_val_32\n \
x64.ret_val_32 %0($rax)\n}\n"
);
}
#[test]
fn a_return_of_a_constant_puts_it_in_a_register_first() {
let (mut names, mut func, block, _) = blank(&[]);
let mut build = Builder::new(&mut func, block);
let zero = build.iconst(Type::int(32), 0);
build.ret(&[zero]);
assert_eq!(
lower(&mut names, &func),
"mfunc @f {\nblock0:\n %0:gpr = x64.mov_ri_32 0\n x64.ret_val_32 %0($rax)\n}\n"
);
}
#[test]
fn a_return_of_nothing_is_no_instruction_at_all() {
let (mut names, mut func, block, _) = blank(&[]);
let mut build = Builder::new(&mut func, block);
build.ret(&[]);
assert_eq!(lower(&mut names, &func), "mfunc @f {\nblock0:\n}\n");
}
#[test]
fn the_allocator_is_what_moves_the_answer_into_the_return_register() {
let (mut names, mut source, block, _) = blank(&[]);
let mut build = Builder::new(&mut source, block);
let zero = build.iconst(Type::int(32), 0);
build.ret(&[zero]);
let mut out = func(&source, &mut names, &SYSV).expect("every instruction has a rule").func;
let env = env();
let allocation = rucc_regalloc::run(&mut out, &env);
let frame = Frame::of(&out, &allocation, &Layout::new(&SYSV, REGS));
finish(&mut out, &allocation, &frame, &SYSV, &FRAME, &mut names);
assert_eq!(
mir::print_func(&out, &names, ®S),
"mfunc @f {\nblock0:\n $rcx = x64.mov_ri_32 0\n $rax = x64.mov_rr_64 $rcx\n \
x64.ret_val_32 $rax($rax)\n x64.ret\n}\n"
);
}
#[test]
fn a_function_of_two_arguments_is_a_whole_function_now() {
let i32 = Type::int(32);
let (mut names, mut source, block, args) = blank(&[i32, i32]);
let mut build = Builder::new(&mut source, block);
let sum = build.binary(Opcode::Add, args[0], args[1], Flags::default());
build.ret(&[sum]);
let mut out = func(&source, &mut names, &SYSV).expect("every instruction has a rule").func;
let env = env();
let allocation = rucc_regalloc::run(&mut out, &env);
let frame = Frame::of(&out, &allocation, &Layout::new(&SYSV, REGS));
finish(&mut out, &allocation, &frame, &SYSV, &FRAME, &mut names);
assert_eq!(
mir::print_func(&out, &names, ®S),
"mfunc @f {\nblock0:\n $rdi($rdi) = x64.arg_val_32\n \
$rax = x64.mov_rr_64 $rdi\n $rsi($rsi) = x64.arg_val_32\n \
$rcx = x64.mov_rr_64 $rsi\n $rdx = x64.mov_rr_64 $rax\n \
$rdx(reuse 1) = x64.add_rr_32 $rax, $rcx\n $rax = x64.mov_rr_64 $rdx\n \
x64.ret_val_32 $rax($rax)\n x64.ret\n}\n"
);
}
#[test]
fn an_argument_with_no_register_left_for_it_is_reported() {
let i64 = Type::int(64);
let (mut names, mut source, block, args) = blank(&[i64; 7]);
let mut build = Builder::new(&mut source, block);
build.ret(&[args[6]]);
let failed = func(&source, &mut names, &SYSV).expect_err("the seventh is on the stack");
assert_eq!(failed.to_string(), "parameter 6 is passed on the stack");
}
#[test]
fn a_jump_is_the_edge_and_nothing_else() {
let i32 = Type::int(32);
let (mut names, mut source, entry, args) = blank(&[i32]);
let next = source.create_block();
let got = source.append_param(next, i32);
Builder::new(&mut source, entry).jump(next, &[args[0]]);
Builder::new(&mut source, next).ret(&[got]);
assert_eq!(
lower(&mut names, &source),
"mfunc @f {\nblock0:\n %0:gpr($rdi) = x64.arg_val_32 block1(%0)\n\n\
block1(%1:gpr):\n x64.ret_val_32 %1($rax)\n}\n"
);
}
#[test]
fn a_conditional_branch_is_lowered_to_the_condition_and_nothing_about_where_it_goes() {
let i32 = Type::int(32);
let (mut names, mut source, entry, args) = blank(&[i32, i32]);
let then = source.create_block();
let other = source.create_block();
let mut build = Builder::new(&mut source, entry);
let cond = build.icmp(rucc_ir::IntPred::Slt, args[0], args[1]);
build.br_if(cond, then, &[], other, &[]);
Builder::new(&mut source, then).ret(&[args[0]]);
Builder::new(&mut source, other).ret(&[args[1]]);
assert_eq!(
lower(&mut names, &source),
"mfunc @f {\nblock0:\n %0:gpr($rdi) = x64.arg_val_32\n \
%1:gpr($rsi) = x64.arg_val_32\n %2:gpr = x64.cmp_set_l_32 %0, %1\n \
x64.br_cond_8 %2, block1, block2\n\n\
block1:\n x64.ret_val_32 %0($rax)\n\n\
block2:\n x64.ret_val_32 %1($rax)\n}\n"
);
}
#[test]
fn a_branch_over_a_block_is_a_whole_function_now() {
let i32 = Type::int(32);
let (mut names, mut source, entry, args) = blank(&[i32, i32]);
let then = source.create_block();
let other = source.create_block();
let join = source.create_block();
let got = source.append_param(join, i32);
let mut build = Builder::new(&mut source, entry);
let cond = build.icmp(rucc_ir::IntPred::Slt, args[0], args[1]);
build.br_if(cond, then, &[], other, &[]);
let mut build = Builder::new(&mut source, then);
let sum = build.binary(Opcode::Add, args[0], args[1], Flags::default());
build.jump(join, &[sum]);
Builder::new(&mut source, other).jump(join, &[args[1]]);
Builder::new(&mut source, join).ret(&[got]);
let mut out = func(&source, &mut names, &SYSV).expect("every instruction has a rule").func;
assert_eq!(crate::split::critical(&mut out), 0, "no edge here is critical");
let env = env();
let allocation = rucc_regalloc::run(&mut out, &env);
let frame = Frame::of(&out, &allocation, &Layout::new(&SYSV, REGS));
finish(&mut out, &allocation, &frame, &SYSV, &FRAME, &mut names);
let text = mir::print_func(&out, &names, ®S);
assert_eq!(text.matches("x64.ret\n").count(), 1, "{text}");
assert!(text.contains("x64.br_cond_8"), "{text}");
assert!(text.contains("x64.add_rr_32"), "{text}");
assert!(!text.contains('%'), "{text}");
}
#[test]
fn a_critical_edge_is_split_before_the_allocator_ever_sees_it() {
let i32 = Type::int(32);
let (mut names, mut source, entry, args) = blank(&[i32, i32]);
let then = source.create_block();
let join = source.create_block();
let got = source.append_param(join, i32);
let mut build = Builder::new(&mut source, entry);
let cond = build.icmp(rucc_ir::IntPred::Slt, args[0], args[1]);
build.br_if(cond, then, &[], join, &[args[1]]);
Builder::new(&mut source, then).jump(join, &[args[0]]);
let mut build = Builder::new(&mut source, join);
let twice = build.binary(Opcode::Add, got, got, Flags::default());
build.ret(&[twice]);
let mut out = func(&source, &mut names, &SYSV).expect("every instruction has a rule").func;
assert_eq!(crate::split::critical(&mut out), 1);
let env = env();
let allocation = rucc_regalloc::run(&mut out, &env);
let frame = Frame::of(&out, &allocation, &Layout::new(&SYSV, REGS));
finish(&mut out, &allocation, &frame, &SYSV, &FRAME, &mut names);
let text = mir::print_func(&out, &names, ®S);
assert_eq!(out.block_count(), 4, "{text}");
assert_eq!(text.matches("x64.ret\n").count(), 1, "{text}");
}
#[test]
fn a_call_passes_what_the_convention_says_and_takes_back_what_it_says() {
let i32 = Type::int(32);
let (mut names, mut source, block, args) = blank(&[i32, i32]);
let sig =
source.add_signature(Signature::new().with_params(&[i32, i32]).with_returns(&[i32]));
let callee = names.intern("g");
let call = Builder::new(&mut source, block).call(callee, sig, &[args[0], args[1]]);
let got = source[call].first_result.expect("an integer comes back");
Builder::new(&mut source, block).ret(&[got]);
let text = lower(&mut names, &source);
assert!(text.contains("= x64.call %0($rdi), %1($rsi), @g"), "{text}");
assert!(text.contains("x64.ret_val_32 %2($rax)"), "{text}");
assert!(text.contains("%2:gpr($rax), $rcx, $rdx, $r8, $r9, $r10, $r11, $xmm0,"), "{text}");
assert!(text.contains("$xmm15 = x64.call"), "{text}");
}
#[test]
fn what_the_frame_owes_a_call_comes_back_with_the_function() {
let i32 = Type::int(32);
let sig = |source: &mut Func| source.add_signature(Signature::new().with_params(&[i32]));
let (mut names, mut source, block, args) = blank(&[i32]);
let sig = sig(&mut source);
let callee = names.intern("g");
Builder::new(&mut source, block).call(callee, sig, &[args[0]]);
let out = func(&source, &mut names, &SYSV).expect("every instruction has a rule");
assert_eq!(out.calls, Some(0));
let layout = out.layout(Layout::new(&SYSV, REGS));
assert!(!layout.leaf);
assert_eq!(layout.outgoing, 0);
let out = func(&source, &mut names, &x86_64::WIN64).expect("every instruction has a rule");
assert_eq!(out.calls, Some(32));
let (mut names, mut source, block, args) = blank(&[i32]);
Builder::new(&mut source, block).ret(&[args[0]]);
let out = func(&source, &mut names, &SYSV).expect("every instruction has a rule");
assert_eq!(out.calls, None);
assert!(out.layout(Layout::new(&SYSV, REGS)).leaf);
}
#[test]
fn a_value_that_outlives_a_call_is_not_left_where_the_call_destroys_it() {
let i32 = Type::int(32);
let (mut names, mut source, block, args) = blank(&[i32]);
let sig = source.add_signature(Signature::new().with_params(&[i32]).with_returns(&[i32]));
let callee = names.intern("g");
let call = Builder::new(&mut source, block).call(callee, sig, &[args[0]]);
let got = source[call].first_result.expect("an integer comes back");
let mut build = Builder::new(&mut source, block);
let sum = build.binary(Opcode::Add, got, args[0], Flags::default());
build.ret(&[sum]);
let lowered = func(&source, &mut names, &SYSV).expect("every instruction has a rule");
let layout = lowered.layout(Layout::new(&SYSV, REGS));
let mut out = lowered.func;
let env = env();
let allocation = rucc_regalloc::run(&mut out, &env);
let frame = Frame::of(&out, &allocation, &layout);
finish(&mut out, &allocation, &frame, &SYSV, &FRAME, &mut names);
let text = mir::print_func(&out, &names, ®S);
assert!(text.contains("$rbx"), "{text}");
assert!(!text.contains('%'), "{text}");
assert_eq!(text.matches("x64.call").count(), 1, "{text}");
}
#[test]
fn a_call_this_cannot_make_is_reported_rather_than_made() {
let i64 = Type::int(64);
let (mut names, mut source, block, args) = blank(&[i64]);
let seven = vec![i64; 7];
let sig = source.add_signature(Signature::new().with_params(&seven));
let callee = names.intern("g");
let passed = vec![args[0]; 7];
Builder::new(&mut source, block).call(callee, sig, &passed);
let failed = func(&source, &mut names, &SYSV).expect_err("the seventh is on the stack");
assert_eq!(failed.to_string(), "argument 6 of this call is passed on the stack");
let (mut names, mut source, block, _) = blank(&[]);
let sig = source
.add_signature(Signature::new().with_returns(&[Type::float(rucc_ir::Float::F64)]));
let callee = names.intern("g");
Builder::new(&mut source, block).call(callee, sig, &[]);
let failed = func(&source, &mut names, &SYSV).expect_err("a double comes back in xmm0");
assert_eq!(failed.to_string(), "what this call gives back is in a vector register");
}
#[test]
fn a_call_through_an_address_is_reported_as_one() {
let i32 = Type::int(32);
let (mut names, mut source, block, args) = blank(&[i32]);
let sig = source.add_signature(Signature::new().with_params(&[i32]));
let varargs = source.push_abis(&[]);
let info = source.add_call(CallInfo { callee: None, signature: sig, varargs });
let mut build = Builder::new(&mut source, block);
let inst = InstData {
args: build.func().push_values(&[args[0], args[0]]),
extra: Extra::Call(info),
..InstData::new(Opcode::CallIndirect)
};
build.inst(inst, &[]);
let failed = func(&source, &mut names, &SYSV).expect_err("nothing calls through a value");
assert_eq!(failed.to_string(), "no rule calls through an address");
}
#[test]
fn an_instruction_no_rule_covers_is_reported() {
let i64 = Type::int(64);
let (mut names, mut source, block, args) = blank(&[i64, i64]);
let mut build = Builder::new(&mut source, block);
build.ret(&[args[0], args[1]]);
let failed = func(&source, &mut names, &SYSV).expect_err("nothing returns two values");
assert_eq!(failed.to_string(), "no rule lowers this instruction");
}
}