use std::fmt::Write as _;
use rucc_base::Interner;
use rucc_mir::{Amode, Block, Func, Inst, Operand};
use rucc_target::x86_64::{self, Arg, Width};
use rucc_target::{Arch, PhysReg, RegClass, TargetInfo};
use crate::Error;
use crate::format::Directives;
const PREFIX: &str = "x64.";
pub fn print(funcs: &[Func], names: &Interner, target: &TargetInfo) -> Result<String, Error> {
if target.triple.arch != Arch::X86_64 {
return Err(Error::Machine { triple: target.triple.to_string() });
}
let mut writer = Writer {
names,
directives: Directives::of(target.object_format),
out: String::new(),
labels: Vec::new(),
};
writer.out.push_str(writer.directives.text());
writer.out.push('\n');
for func in funcs {
writer.func(func)?;
}
writer.directives.end(&mut writer.out);
Ok(writer.out)
}
struct Writer<'a> {
names: &'a Interner,
directives: Directives,
out: String,
labels: Vec<u32>,
}
impl Writer<'_> {
fn func(&mut self, func: &Func) -> Result<(), Error> {
let name = self.names.resolve(func.name).to_owned();
self.number(func);
self.directives.open(&mut self.out, &name);
for (index, block) in func.blocks().enumerate() {
let _ = writeln!(self.out, "{}{name}_{index}:", self.directives.local());
for inst in func.insts(block) {
self.inst(func, block, inst, &name)?;
}
}
self.directives.close(&mut self.out, &name);
Ok(())
}
fn number(&mut self, func: &Func) {
self.labels.clear();
self.labels.resize(func.block_count(), u32::MAX);
for (index, block) in func.blocks().enumerate() {
self.labels[block.index()] = u32::try_from(index).expect("a block number");
}
}
fn inst(
&mut self,
func: &Func,
block: Block,
inst: Inst,
func_name: &str,
) -> Result<(), Error> {
let data = func[inst];
let spelled = self.names.resolve(data.opcode.name());
let opcode = spelled.strip_prefix(PREFIX).unwrap_or(spelled);
let Some(written) = x86_64::written(opcode) else {
return Err(Error::Opcode { func: func_name.to_owned(), opcode: spelled.to_owned() });
};
let operands = &func[data.operands];
for machine in written {
let mut args = Vec::with_capacity(machine.args.len());
for arg in machine.args {
args.push(match *arg {
Arg::Reg(at, width) => {
let operand = operands[usize::from(at)];
self.reg(operand, width, func_name, spelled)?
}
Arg::Named(register) => format!("%{register}"),
Arg::Imm => match data.imm {
Some(imm) => format!("${}", func[imm].0),
None => "$0".to_owned(),
},
Arg::Mem => match data.mem {
Some(mem) => self.amode(operands, &func[mem], func_name, spelled)?,
None => "0".to_owned(),
},
Arg::Symbol => match data.symbol {
Some(symbol) => {
format!("{}{}", self.directives.symbol(), self.names.resolve(symbol))
}
None => "0".to_owned(),
},
Arg::Label => match func[block].succs.first() {
Some(call) => self.label(func_name, call.block),
None => "0".to_owned(),
},
});
}
if args.is_empty() {
let _ = writeln!(self.out, "\t{}", machine.mnemonic);
} else {
let _ = writeln!(self.out, "\t{}\t{}", machine.mnemonic, args.join(", "));
}
}
Ok(())
}
fn reg(
&self,
operand: Operand,
width: Width,
func_name: &str,
opcode: &str,
) -> Result<String, Error> {
let Some(phys) = operand.reg.phys() else {
return Err(Error::Virtual { func: func_name.to_owned(), opcode: opcode.to_owned() });
};
Ok(format!("%{}", name_of(operand.class, phys, width)))
}
fn amode(
&self,
operands: &[Operand],
amode: &Amode,
func_name: &str,
opcode: &str,
) -> Result<String, Error> {
let mut out = String::new();
if let Some(symbol) = amode.symbol {
let _ = write!(out, "{}{}", self.directives.symbol(), self.names.resolve(symbol));
if amode.disp != 0 {
let sign = if amode.disp < 0 { '-' } else { '+' };
let _ = write!(out, "{sign}{}", i64::from(amode.disp).abs());
}
} else if amode.disp != 0 || (amode.base.is_none() && amode.index.is_none()) {
let _ = write!(out, "{}", amode.disp);
}
let base = amode.base.and_then(|at| operands.get(usize::from(at)));
let index = amode.index.and_then(|at| operands.get(usize::from(at)));
if base.is_some() || index.is_some() {
out.push('(');
if let Some(operand) = base {
out.push_str(&self.reg(*operand, Width::Quad, func_name, opcode)?);
}
if let Some(operand) = index {
let reg = self.reg(*operand, Width::Quad, func_name, opcode)?;
let _ = write!(out, ",{reg},{}", amode.scale);
}
out.push(')');
} else if amode.symbol.is_some() {
out.push_str("(%rip)");
}
Ok(out)
}
fn label(&self, func_name: &str, block: Block) -> String {
match self.labels.get(block.index()).copied() {
Some(u32::MAX) | None => format!("{}{func_name}_?", self.directives.local()),
Some(number) => format!("{}{func_name}_{number}", self.directives.local()),
}
}
}
fn name_of(class: RegClass, reg: PhysReg, width: Width) -> &'static str {
let named = if class == x86_64::GPR {
x86_64::gpr_name(reg, width)
} else {
x86_64::REGS.name(class, reg)
};
named.unwrap_or("?")
}
#[cfg(test)]
mod tests {
use super::*;
use rucc_base::Interner;
use rucc_mir::{Func, Mem, Operand, Reg};
use rucc_target::x86_64::{GPR, RAX, RCX, RDX};
use rucc_target::{Arch, Env, Os, TargetInfo, Triple};
fn target(os: Os) -> TargetInfo {
TargetInfo::new(Triple::new(Arch::X86_64, os, Env::Gnu))
}
fn write(build: impl FnOnce(&mut Func, &mut Interner)) -> String {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"));
build(&mut func, &mut names);
print(&[func], &names, &target(Os::Linux)).expect("a function that was allocated")
}
fn body(text: &str) -> Vec<&str> {
text.lines()
.filter(|line| line.starts_with('\t') && !line.trim_start().starts_with('.'))
.map(|line| line.trim_start())
.collect()
}
#[test]
fn an_instruction_is_written_the_way_the_target_says_it_is() {
let text = write(|func, names| {
let block = func.create_block();
let add = rucc_mir::Opcode::new(names.intern("x64.add_rr_32"));
func.build(block, add)
.operand(Operand::write(Reg::physical(RAX), GPR))
.operand(Operand::read(Reg::physical(RAX), GPR))
.operand(Operand::read(Reg::physical(RCX), GPR))
.finish();
});
assert_eq!(body(&text), ["addl\t%ecx, %eax"]);
}
#[test]
fn an_opcode_the_machine_has_no_single_instruction_for_is_written_as_the_ones_it_has() {
let text = write(|func, names| {
let block = func.create_block();
let cmp = rucc_mir::Opcode::new(names.intern("x64.cmp_set_l_64"));
func.build(block, cmp)
.operand(Operand::write(Reg::physical(RAX), GPR))
.operand(Operand::read(Reg::physical(RCX), GPR))
.operand(Operand::read(Reg::physical(RDX), GPR))
.finish();
});
assert_eq!(body(&text), ["cmpq\t%rdx, %rcx", "setl\t%al"]);
}
#[test]
fn an_opcode_that_is_not_an_instruction_is_written_as_nothing() {
let text = write(|func, names| {
let block = func.create_block();
let ret = rucc_mir::Opcode::new(names.intern("x64.ret_val_32"));
func.build(block, ret).operand(Operand::read(Reg::physical(RAX), GPR)).finish();
});
assert_eq!(body(&text), Vec::<&str>::new());
}
#[test]
fn an_address_is_a_displacement_and_then_the_registers_it_names() {
let text = write(|func, names| {
let block = func.create_block();
let lea = rucc_mir::Opcode::new(names.intern("x64.lea_64"));
func.build(block, lea)
.operand(Operand::write(Reg::physical(RAX), GPR))
.mem(
Mem::at(Operand::read(Reg::physical(RCX), GPR))
.indexed(Operand::read(Reg::physical(RDX), GPR), 4)
.plus(-16),
)
.finish();
});
assert_eq!(body(&text), ["leaq\t-16(%rcx,%rdx,4), %rax"]);
}
#[test]
fn an_address_with_nothing_but_a_symbol_in_it_is_relative_to_the_instruction_pointer() {
let text = write(|func, names| {
let block = func.create_block();
let load = rucc_mir::Opcode::new(names.intern("x64.mov_rm_64"));
let global = names.intern("counter");
func.build(block, load)
.operand(Operand::write(Reg::physical(RAX), GPR))
.mem(Mem::of(global))
.finish();
});
assert_eq!(body(&text), ["movq\tcounter(%rip), %rax"]);
}
#[test]
fn a_jump_goes_to_the_label_of_the_block_the_first_arm_names() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"));
let first = func.create_block();
let second = func.create_block();
let jmp = rucc_mir::Opcode::new(names.intern("x64.jmp"));
func.build(first, jmp).finish();
func.succs_mut(first).push(rucc_mir::BlockCall::to(second));
let text = print(&[func], &names, &target(Os::Linux)).expect("a function of two blocks");
assert!(text.contains("\tjmp\t.Lf_1\n"), "{text}");
assert!(text.contains("\n.Lf_1:\n"), "{text}");
}
#[test]
fn a_symbol_is_spelled_the_way_the_object_format_spells_one() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"));
let block = func.create_block();
let call = rucc_mir::Opcode::new(names.intern("x64.call"));
let callee = names.intern("puts");
func.build(block, call).symbol(callee).finish();
let elf = print(std::slice::from_ref(&func), &names, &target(Os::Linux)).expect("elf");
assert!(elf.contains("\tcall\tputs\n"), "{elf}");
assert!(elf.contains("\n.Lf_0:\n"), "{elf}");
let macho = print(&[func], &names, &target(Os::Darwin)).expect("mach-o");
assert!(macho.contains("\tcall\t_puts\n"), "{macho}");
assert!(macho.contains("\n_f:\n"), "{macho}");
assert!(macho.contains("\nLf_0:\n"), "{macho}");
}
#[test]
fn a_function_that_was_never_allocated_is_refused_rather_than_written_wrongly() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"));
let block = func.create_block();
let vreg = func.new_vreg(GPR);
let neg = rucc_mir::Opcode::new(names.intern("x64.neg_r_32"));
func.build(block, neg).operand(Operand::write(vreg, GPR)).finish();
let error = print(&[func], &names, &target(Os::Linux)).expect_err("a virtual register");
assert_eq!(
error,
Error::Virtual { func: "f".to_owned(), opcode: "x64.neg_r_32".to_owned() }
);
}
#[test]
fn an_opcode_the_target_does_not_describe_is_refused() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"));
let block = func.create_block();
let made_up = rucc_mir::Opcode::new(names.intern("x64.frobnicate"));
func.build(block, made_up).finish();
let error = print(&[func], &names, &target(Os::Linux)).expect_err("no such instruction");
assert_eq!(
error,
Error::Opcode { func: "f".to_owned(), opcode: "x64.frobnicate".to_owned() }
);
}
#[test]
fn a_machine_with_no_writer_here_is_said_so_rather_than_written_as_x86_64() {
let names = Interner::new();
let aarch64 = TargetInfo::new(Triple::new(Arch::Aarch64, Os::Linux, Env::Gnu));
let error = print(&[], &names, &aarch64).expect_err("no writer");
assert!(matches!(error, Error::Machine { .. }), "{error:?}");
}
}