use std::collections::HashMap;
use std::fmt::Write as _;
use rucc_base::Interner;
use rucc_mir::{Amode, Block, CfiOp, Func, Inst, Opcode, Operand, Reach, defs};
use rucc_object::{Alias, FUNC_ALIGN, Output, Sections};
use rucc_target::x86_64::{self, Arg, Width};
use rucc_target::{PhysReg, RegClass, Segment, TargetInfo, aarch64};
use rucc_tuple::Arch;
use crate::Error;
use crate::a64;
use crate::data::{Globals, Piece, Variable};
use crate::format::{Directives, binding, visibility};
const PREFIX: &str = "x64.";
pub fn print(
funcs: &[Func],
globals: &Globals,
aliases: &[Alias],
names: &Interner,
target: &TargetInfo,
unwind: bool,
output: Output,
) -> Result<String, Error> {
listing(funcs, globals, aliases, names, target, unwind, output, false)
}
pub fn print_marked(
funcs: &[Func],
globals: &Globals,
aliases: &[Alias],
names: &Interner,
target: &TargetInfo,
unwind: bool,
output: Output,
) -> Result<String, Error> {
listing(funcs, globals, aliases, names, target, unwind, output, true)
}
#[must_use]
pub fn mark(target: &TargetInfo, func: usize, inst: Inst) -> String {
spell_mark(Directives::of(target.object_format), func, inst)
}
#[must_use]
pub fn mark_end(target: &TargetInfo, func: usize) -> String {
format!("{}rucc_end{func}", Directives::of(target.object_format).local())
}
fn spell_mark(directives: Directives, func: usize, inst: Inst) -> String {
format!("{}rucc_row{func}_{}", directives.local(), inst.index())
}
#[allow(clippy::too_many_arguments)]
fn listing(
funcs: &[Func],
globals: &Globals,
aliases: &[Alias],
names: &Interner,
target: &TargetInfo,
unwind: bool,
output: Output,
marks: bool,
) -> Result<String, Error> {
let Output { sections, property } = output;
let arch = target.tuple.arch();
if !matches!(arch, Arch::X86_64 | Arch::Aarch64) {
return Err(Error::Machine { triple: target.tuple.to_string() });
}
let directives = Directives::of(target.object_format);
let mut writer = Writer {
arch,
names,
directives,
unwind: unwind && directives != Directives::Coff,
out: String::new(),
labels: Vec::new(),
sections,
marks: marks.then_some(0),
};
writer.out.push_str(writer.directives.text());
writer.out.push('\n');
if !globals.file_asm.is_empty() {
for text in &globals.file_asm {
let _ = writeln!(writer.out, "#APP\n{text}\n#NO_APP");
}
writer.out.push_str(writer.directives.text());
writer.out.push('\n');
}
for func in funcs {
writer.func(func)?;
}
for var in &globals.vars {
writer.variable(var);
}
for alias in aliases {
writer.directives.alias(&mut writer.out, alias);
}
for name in &globals.weak {
writer.directives.absent(&mut writer.out, name);
}
writer.directives.end(&mut writer.out, property);
Ok(writer.out)
}
pub(crate) fn template(
func: &Func,
block: Block,
inst: Inst,
names: &Interner,
directives: Directives,
) -> Result<String, Error> {
let mut writer = Writer {
arch: Arch::X86_64,
names,
directives,
unwind: false,
out: String::new(),
labels: Vec::new(),
sections: Sections::default(),
marks: None,
};
writer.inst(func, block, inst, names.resolve(func.name))?;
Ok(writer.out)
}
struct Writer<'a> {
arch: Arch,
names: &'a Interner,
directives: Directives,
unwind: bool,
out: String,
labels: Vec<u32>,
sections: Sections,
marks: Option<usize>,
}
impl Writer<'_> {
fn func(&mut self, func: &Func) -> Result<(), Error> {
let name = self.names.resolve(func.name).to_owned();
self.number(func);
let binding = binding(func.binding);
let seen = visibility(func.visibility);
let align = func.align.unwrap_or(FUNC_ALIGN);
self.directives.code(&mut self.out, &name, self.sections);
let patch =
func.patch.map(|patch| (patch, format!("{}pfe_{name}", self.directives.local())));
let mut ahead = String::new();
if let Some((patch, label)) = &patch {
let back = if self.sections.functions {
format!("\t.section\t.text.{name}")
} else {
self.directives.text().to_owned()
};
self.directives.patchable(&mut ahead, label, &back);
if patch.before > 0 {
let _ = writeln!(ahead, "{label}:");
self.pad(&mut ahead, patch.pad, patch.before);
}
}
let fill = self.fill();
self.directives.open(&mut self.out, &name, align, fill, binding, seen, &ahead);
let unwind = self.unwind;
if unwind {
let _ = writeln!(self.out, "\t.cfi_startproc");
}
let pads: HashMap<Inst, Block> = if self.directives == Directives::Elf {
func.landings.iter().copied().collect()
} else {
HashMap::new()
};
let local = self.directives.local();
if unwind && !pads.is_empty() {
let _ = writeln!(self.out, "\t.cfi_personality 0x9b,{}", rucc_ir::PERSONALITY_REF);
let _ = writeln!(self.out, "\t.cfi_lsda 0x1b,{local}LSDA_{name}");
}
let mut sites = Vec::new();
let end = func.cfi_end();
let loops = crate::bytes::loop_sizes(self.names, self.directives, func).unwrap_or_default();
for (index, block) in func.blocks().enumerate() {
let size = loops.get(block.index()).copied().unwrap_or(0);
if let Some(most) = crate::loop_room(size) {
let _ = writeln!(self.out, "\t.p2align\t6,,{most}");
}
let _ = writeln!(self.out, "{}{name}_{index}:", self.directives.local());
if let Some(label) = func.block_name(block) {
let _ = writeln!(self.out, "{}:", self.names.resolve(label));
}
for inst in func.insts(block) {
if let Some((patch, label)) = &patch {
if patch.before == 0 && patch.after == Some(inst) {
let _ = writeln!(self.out, "{label}:");
}
}
if let Some(which) = self.marks {
let _ = writeln!(self.out, "{}:", spell_mark(self.directives, which, inst));
}
let site = pads.get(&inst).map(|&pad| (sites.len(), pad));
if let Some((at, _)) = site {
let _ = writeln!(self.out, "{local}EHB{at}_{name}:");
}
self.inst(func, block, inst, &name)?;
if let Some((at, pad)) = site {
let _ = writeln!(self.out, "{local}EHE{at}_{name}:");
sites.push(pad);
}
if unwind && Some(inst) != end {
for op in func.cfi_after(inst) {
self.cfi(op);
}
}
}
}
if let Some(which) = self.marks {
let _ = writeln!(self.out, "{}rucc_end{which}:", self.directives.local());
}
self.tables(func, &name);
if unwind {
let _ = writeln!(self.out, "\t.cfi_endproc");
if !sites.is_empty() {
self.call_sites(&name, &sites);
}
}
self.directives.close(&mut self.out, &name);
if let Some(which) = &mut self.marks {
*which += 1;
}
Ok(())
}
fn pad(&self, out: &mut String, pad: Opcode, count: u32) {
let spelled = self.names.resolve(pad.name());
let opcode = spelled.strip_prefix(self.prefix()).unwrap_or(spelled);
for _ in 0..count {
let _ = writeln!(out, "\t{opcode}");
}
}
fn cfi(&mut self, op: CfiOp) {
let _ = match op {
CfiOp::DefCfa { reg, offset } => {
writeln!(self.out, "\t.cfi_def_cfa {reg}, {offset}")
}
CfiOp::DefCfaOffset(offset) => writeln!(self.out, "\t.cfi_def_cfa_offset {offset}"),
CfiOp::DefCfaRegister(reg) => writeln!(self.out, "\t.cfi_def_cfa_register {reg}"),
CfiOp::Offset { reg, offset } => writeln!(self.out, "\t.cfi_offset {reg}, {offset}"),
CfiOp::Restore(reg) => writeln!(self.out, "\t.cfi_restore {reg}"),
CfiOp::RememberState => writeln!(self.out, "\t.cfi_remember_state"),
CfiOp::RestoreState => writeln!(self.out, "\t.cfi_restore_state"),
};
}
fn variable(&mut self, var: &Variable) {
if !self.directives.variable(&mut self.out, var, self.sections) {
return;
}
for piece in &var.pieces {
self.piece(piece);
}
self.directives.close(&mut self.out, &var.name);
self.directives.descriptor(&mut self.out, var);
}
fn piece(&mut self, piece: &Piece) {
match piece {
Piece::Zero(bytes) => {
let _ = writeln!(self.out, "\t.space\t{bytes}");
}
Piece::Bytes(bytes) => {
let _ = writeln!(self.out, "\t.ascii\t\"{}\"", escape(bytes));
}
Piece::Scalar(bytes) => match width(bytes.len()) {
Some(directive) => {
let mut value = [0u8; 16];
value[..bytes.len()].copy_from_slice(bytes);
let _ = writeln!(self.out, "\t{directive}\t{}", u128::from_le_bytes(value));
}
None => {
let list = bytes.iter().map(u8::to_string).collect::<Vec<_>>().join(", ");
let _ = writeln!(self.out, "\t.byte\t{list}");
}
},
Piece::Away { symbol, addend } => {
let name = format!("{}{symbol}", self.directives.symbol());
match addend {
0 => {
let _ = writeln!(self.out, "\t.long\t{name} - .");
}
_ => {
let sign = if *addend < 0 { '-' } else { '+' };
let _ = writeln!(self.out, "\t.long\t{name}{sign}{} - .", addend.abs());
}
}
}
Piece::Addr { symbol, addend, bytes } => {
let directive = if *bytes == 8 { ".quad" } else { ".long" };
let name = format!("{}{symbol}", self.directives.symbol());
match addend {
0 => {
let _ = writeln!(self.out, "\t{directive}\t{name}");
}
_ => {
let sign = if *addend < 0 { '-' } else { '+' };
let _ = writeln!(self.out, "\t{directive}\t{name}{sign}{}", addend.abs());
}
}
}
Piece::Apart { to, from, addend, bytes } => {
let directive = width(usize::from(*bytes)).unwrap_or(".long");
let prefix = self.directives.symbol();
let _ = write!(self.out, "\t{directive}\t{prefix}{to}-{prefix}{from}");
let _ = match addend.signum() {
1 => writeln!(self.out, "+{addend}"),
-1 => writeln!(self.out, "-{}", addend.unsigned_abs()),
_ => writeln!(self.out),
};
}
}
}
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> {
if self.arch == Arch::Aarch64 {
let spelling = match self.directives {
Directives::MachO => aarch64::Spelling::Apple,
Directives::Elf | Directives::Coff => aarch64::Spelling::Gnu,
};
let at = a64::Context {
names: self.names,
symbol: self.directives.symbol(),
spelling,
func_name,
};
let mut line = String::new();
let label = |to| self.label(func_name, to);
let table = |at: u32| self.table(func_name, at as usize);
a64::inst(&mut line, &at, func, block, inst, label, table)?;
self.out.push_str(&line);
return Ok(());
}
let data = func[inst];
let spelled = self.names.resolve(data.opcode.name());
let opcode = spelled.strip_prefix(PREFIX).unwrap_or(spelled);
if opcode == x86_64::ALIGN {
let bytes = data.imm.map_or(0, |imm| func[imm].0);
let boundary = u32::try_from(bytes).ok().filter(|at| at.is_power_of_two());
let Some(boundary) = boundary else {
return Err(Error::Opcode {
func: func_name.to_owned(),
opcode: spelled.to_owned(),
});
};
let _ = writeln!(self.out, "\t.p2align\t{}, 0x90", boundary.trailing_zeros());
return Ok(());
}
if opcode == x86_64::LITERAL {
let bytes: Vec<u8> =
data.imm.map(|imm| x86_64::unpacked(func[imm].0).collect()).unwrap_or_default();
if bytes.is_empty() {
return Err(Error::Opcode {
func: func_name.to_owned(),
opcode: spelled.to_owned(),
});
}
let written: Vec<String> = bytes.iter().map(|byte| format!("0x{byte:02x}")).collect();
let _ = writeln!(self.out, "\t.byte\t{}", written.join(", "));
return Ok(());
}
if opcode == x86_64::TEMPLATE {
let Some(text) = data.symbol else {
return Err(Error::Opcode {
func: func_name.to_owned(),
opcode: spelled.to_owned(),
});
};
let mem = match data.mem {
Some(mem) => self.amode(&func[data.operands], &func[mem], func_name, spelled)?,
None => String::new(),
};
let operands = &func[data.operands];
if operands.iter().any(|operand| operand.reg.phys().is_none()) {
return Err(Error::Virtual {
func: func_name.to_owned(),
opcode: spelled.to_owned(),
});
}
let prefix = self.directives.symbol();
let reg = |at: usize, width: char| {
let Some(operand) = operands.get(at) else { return String::from("?") };
let phys = operand.reg.phys().expect("every operand was checked above");
let named = match width {
'b' => name_of(operand.class, phys, Width::Byte),
'w' => name_of(operand.class, phys, Width::Word),
'k' => name_of(operand.class, phys, Width::Long),
'h' => x86_64::gpr_high(phys).unwrap_or("?"),
_ => name_of(operand.class, phys, Width::Quad),
};
format!("%{named}")
};
let filled = x86_64::template_filled(
self.names.resolve(text),
&mem,
|name| format!("{prefix}{name}"),
reg,
);
for line in filled.lines() {
let _ = writeln!(self.out, "\t{}", line.trim_start());
}
return Ok(());
}
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::Xmm(at) => {
let operand = operands[usize::from(at)];
self.reg(operand, Width::Quad, func_name, spelled)?
}
Arg::Low(at) => {
let operand = operands[usize::from(at)];
self.reg(operand, Width::Byte, func_name, spelled)?
}
Arg::High(at) => {
let operand = operands[usize::from(at)];
let Some(phys) = operand.reg.phys() else {
return Err(Error::Virtual {
func: func_name.to_owned(),
opcode: spelled.to_owned(),
});
};
format!("%{}", x86_64::gpr_high(phys).unwrap_or("?"))
}
Arg::Named(register) => format!("%{register}"),
Arg::Stack(depth) => format!("%st({depth})"),
Arg::Lit(lane) => format!("${lane}"),
Arg::Through => {
let operand = operands[defs(operands)];
format!("*{}", self.reg(operand, Width::Quad, func_name, spelled)?)
}
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();
match amode.segment {
Some(Segment::Fs) => out.push_str("%fs:"),
Some(Segment::Gs) => out.push_str("%gs:"),
None => {}
}
if let Some(symbol) = amode.symbol {
let _ = write!(out, "{}{}", self.directives.symbol(), self.names.resolve(symbol));
match amode.reach {
Reach::Itself => {}
Reach::Table => out.push_str("@GOTPCREL"),
Reach::Thread if self.directives == Directives::MachO => out.push_str("@TLVP"),
Reach::Thread => out.push_str("@GOTTPOFF"),
Reach::Section => out.push_str("@SECREL32"),
}
if amode.disp != 0 {
let sign = if amode.disp < 0 { '-' } else { '+' };
let _ = write!(out, "{sign}{}", i64::from(amode.disp).abs());
}
} else if let Some(block) = amode.block {
out.push_str(&self.label(func_name, block));
if amode.disp != 0 {
let sign = if amode.disp < 0 { '-' } else { '+' };
let _ = write!(out, "{sign}{}", i64::from(amode.disp).abs());
}
} else if let Some(table) = amode.table {
out.push_str(&self.table(func_name, table as usize));
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() || amode.block.is_some() || amode.table.is_some() {
out.push_str("(%rip)");
}
Ok(out)
}
fn tables(&mut self, func: &Func, func_name: &str) {
if func.tables.is_empty() {
return;
}
let apart = self.arch == Arch::X86_64 && self.directives == Directives::Elf;
if apart {
let _ = if self.sections.data {
writeln!(self.out, "\t.section\t.rodata.{func_name},\"a\",@progbits")
} else {
writeln!(self.out, "\t.section\t.rodata")
};
let _ = writeln!(self.out, "\t.p2align\t2");
} else {
let _ = match self.fill() {
Some(byte) => writeln!(self.out, "\t.p2align\t2, {byte:#x}"),
None => writeln!(self.out, "\t.p2align\t2"),
};
}
for (index, table) in func.tables.iter().enumerate() {
let label = self.table(func_name, index);
let _ = writeln!(self.out, "{label}:");
let block = func.block_of(table.jump).expect("a table read by a jump in no block");
let succs = &func[block].succs;
for &cell in &table.cells {
let to = self.label(func_name, succs[cell as usize].block);
let _ = writeln!(self.out, "\t.long\t{to}-{label}");
}
}
if apart {
if self.sections.functions {
let _ = writeln!(self.out, "\t.section\t.text.{func_name},\"ax\",@progbits");
} else {
let _ = writeln!(self.out, "{}", self.directives.text());
}
}
}
fn call_sites(&mut self, name: &str, sites: &[Block]) {
let local = self.directives.local();
let _ = writeln!(self.out, "\t.section\t{},\"a\",@progbits", rucc_object::EXCEPT_TABLE);
let _ = writeln!(self.out, "\t.p2align\t2");
let _ = writeln!(self.out, "{local}LSDA_{name}:");
let _ = writeln!(self.out, "\t.byte\t0xff\n\t.byte\t0xff\n\t.byte\t0x1");
let _ = writeln!(self.out, "\t.uleb128\t{local}LSDACSE_{name}-{local}LSDACSB_{name}");
let _ = writeln!(self.out, "{local}LSDACSB_{name}:");
for (at, &pad) in sites.iter().enumerate() {
let pad = self.label(name, pad);
let _ = writeln!(self.out, "\t.uleb128\t{local}EHB{at}_{name}-{name}");
let _ = writeln!(self.out, "\t.uleb128\t{local}EHE{at}_{name}-{local}EHB{at}_{name}");
let _ = writeln!(self.out, "\t.uleb128\t{pad}-{name}");
let _ = writeln!(self.out, "\t.uleb128\t0");
}
let _ = writeln!(self.out, "{local}LSDACSE_{name}:");
if self.sections.functions {
let _ = writeln!(self.out, "\t.section\t.text.{name},\"ax\",@progbits");
} else {
let _ = writeln!(self.out, "{}", self.directives.text());
}
}
fn table(&self, func_name: &str, index: usize) -> String {
format!("{}{func_name}_j{index}", self.directives.local())
}
fn prefix(&self) -> &'static str {
if self.arch == Arch::Aarch64 { a64::PREFIX } else { PREFIX }
}
fn fill(&self) -> Option<u8> {
if self.arch == Arch::Aarch64 { None } else { Some(0x90) }
}
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 width(bytes: usize) -> Option<&'static str> {
match bytes {
1 => Some(".byte"),
2 => Some(".short"),
4 => Some(".long"),
8 => Some(".quad"),
_ => None,
}
}
fn escape(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len());
for byte in bytes {
match byte {
b'"' => out.push_str("\\\""),
b'\\' => out.push_str("\\\\"),
0x20..=0x7e => out.push(char::from(*byte)),
_ => {
let _ = write!(out, "\\{byte:03o}");
}
}
}
out
}
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_object::{Binding, Place, Visibility};
use rucc_target::x86_64::{GPR, RAX, RCX, RDX, RSP};
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],
&Globals::default(),
&[],
&names,
&target(Os::Linux),
true,
Output::default(),
)
.expect("a function that was allocated")
}
fn data(vars: Vec<Variable>, os: Os) -> String {
let names = Interner::new();
print(
&[],
&Globals { vars, ..Globals::default() },
&[],
&names,
&target(os),
true,
Output::default(),
)
.expect("a machine with a writer")
}
fn split(vars: Vec<Variable>, os: Os) -> String {
let names = Interner::new();
let sections =
Output { sections: Sections { functions: false, data: true }, ..Output::default() };
print(
&[],
&Globals { vars, ..Globals::default() },
&[],
&names,
&target(os),
true,
sections,
)
.expect("a machine with a writer")
}
fn split_code(first: &str, second: &str, os: Os) -> String {
let mut names = Interner::new();
let mut funcs = Vec::new();
for name in [first, second] {
let mut func = Func::new(names.intern(name));
func.create_block();
funcs.push(func);
}
let sections =
Output { sections: Sections { functions: true, data: false }, ..Output::default() };
print(&funcs, &Globals::default(), &[], &names, &target(os), true, sections)
.expect("a machine with a writer")
}
fn var(name: &str, place: Place, pieces: Vec<Piece>) -> Variable {
Variable {
name: name.to_owned(),
size: 4,
align: 4,
place,
binding: Binding::Global,
visibility: Visibility::Default,
pieces,
}
}
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 = 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 = 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 = 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_alignment_is_written_as_the_directive_that_asks_for_it() {
let text = write(|func, names| {
let block = func.create_block();
let align = Opcode::new(names.intern("x64.align"));
func.build(block, align).imm(32).finish();
});
assert!(text.contains("\n\t.p2align\t5, 0x90\n"), "{text}");
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 = 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_in_a_thread_s_own_block_names_the_segment_and_no_register() {
let text = write(|func, names| {
let block = func.create_block();
let load = Opcode::new(names.intern("x64.mov_rm_64"));
func.build(block, load)
.operand(Operand::write(Reg::physical(RAX), GPR))
.mem(Mem::in_segment(Segment::Fs, 40))
.finish();
});
assert_eq!(body(&text), ["movq\t%fs:40, %rax"]);
}
#[test]
fn the_touch_a_probing_prologue_writes_is_an_immediate_and_then_an_address() {
let text = write(|func, names| {
let block = func.create_block();
let touch = Opcode::new(names.intern("x64.or_mi_8"));
func.build(block, touch)
.imm(0)
.mem(Mem::at(Operand::read(Reg::physical(RSP), GPR)))
.finish();
});
assert_eq!(body(&text), ["orb\t$0, (%rsp)"]);
}
#[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 = 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 an_address_with_a_label_in_it_is_the_label_and_is_relative_as_well() {
let text = write(|func, names| {
let head = func.create_block();
let there = func.create_block();
let lea = Opcode::new(names.intern("x64.lea_64"));
let jump = Opcode::new(names.intern("x64.jmp_reg"));
func.build(head, lea)
.operand(Operand::write(Reg::physical(RAX), GPR))
.mem(Mem::block(there))
.finish();
func.build(head, jump).operand(Operand::read(Reg::physical(RAX), GPR)).finish();
func.build(there, Opcode::new(names.intern("x64.ret"))).finish();
});
assert_eq!(body(&text), ["leaq\t.Lf_1(%rip), %rax", "jmp\t*%rax", "ret"]);
}
fn switching(os: Os, sections: Sections) -> String {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"));
let head = func.create_block();
let first = func.create_block();
let second = func.create_block();
let lea = Opcode::new(names.intern("x64.lea_64"));
let jmp = Opcode::new(names.intern("x64.jmp_reg"));
func.build(head, lea)
.operand(Operand::write(Reg::physical(RAX), GPR))
.mem(Mem::table(0))
.finish();
let jump = func.build(head, jmp).operand(Operand::read(Reg::physical(RAX), GPR)).finish();
func.succs_mut(head).push(rucc_mir::BlockCall::to(first));
func.succs_mut(head).push(rucc_mir::BlockCall::to(second));
func.build(first, Opcode::new(names.intern("x64.ret"))).finish();
func.build(second, Opcode::new(names.intern("x64.ret"))).finish();
func.tables.push(rucc_mir::Table { jump, cells: vec![0, 1, 0] });
let output = Output { sections, ..Output::default() };
print(&[func], &Globals::default(), &[], &names, &target(os), true, output)
.expect("a function that was allocated")
}
fn around_table(text: &str) -> Vec<&str> {
let lines: Vec<&str> = text.lines().collect();
let at = lines.iter().position(|line| *line == ".Lf_j0:").expect("a table");
lines[at - 2..at + 5].to_vec()
}
#[test]
fn a_jump_table_is_a_label_and_the_distance_to_each_block_from_it_in_rodata() {
let text = switching(Os::Linux, Sections::default());
assert_eq!(body(&text), ["leaq\t.Lf_j0(%rip), %rax", "jmp\t*%rax", "ret", "ret"]);
assert_eq!(
around_table(&text),
[
"\t.section\t.rodata",
"\t.p2align\t2",
".Lf_j0:",
"\t.long\t.Lf_1-.Lf_j0",
"\t.long\t.Lf_2-.Lf_j0",
"\t.long\t.Lf_1-.Lf_j0",
"\t.text",
],
"{text}"
);
let closed = text.find("\t.size\tf").expect("a size");
assert!(text[..closed].ends_with("\t.text\n\t.cfi_endproc\n"), "{text}");
}
#[test]
fn a_jump_table_under_data_sections_goes_in_a_section_named_after_its_function() {
let text = switching(Os::Linux, Sections { functions: true, data: true });
assert_eq!(around_table(&text)[0], "\t.section\t.rodata.f,\"a\",@progbits", "{text}");
assert_eq!(around_table(&text)[6], "\t.section\t.text.f,\"ax\",@progbits", "{text}");
let text = switching(Os::Linux, Sections { functions: true, data: false });
assert_eq!(around_table(&text)[0], "\t.section\t.rodata", "{text}");
assert_eq!(around_table(&text)[6], "\t.section\t.text.f,\"ax\",@progbits", "{text}");
}
#[test]
fn a_jump_table_on_windows_stays_after_the_code() {
let text = switching(Os::Windows, Sections::default());
assert!(!text.contains("rodata"), "{text}");
let lines: Vec<&str> = text.lines().collect();
let at = lines.iter().position(|line| *line == ".Lf_j0:").expect("a table");
assert_eq!(lines[at - 2..=at], ["\tret", "\t.p2align\t2, 0x90", ".Lf_j0:"], "{text}");
}
#[test]
fn an_address_that_reads_the_offset_table_says_so_on_the_symbol() {
let text = write(|func, names| {
let block = func.create_block();
let load = Opcode::new(names.intern("x64.mov_rm_64"));
let away = names.intern("away");
func.build(block, load)
.operand(Operand::write(Reg::physical(RAX), GPR))
.mem(Mem::got(away))
.finish();
});
assert_eq!(body(&text), ["movq\taway@GOTPCREL(%rip), %rax"]);
}
#[test]
fn an_address_that_reads_the_offset_of_a_thread_local_says_so_on_the_symbol_as_well() {
let text = write(|func, names| {
let block = func.create_block();
let load = Opcode::new(names.intern("x64.mov_rm_64"));
let away = names.intern("away");
func.build(block, load)
.operand(Operand::write(Reg::physical(RAX), GPR))
.mem(Mem::thread(away))
.finish();
});
assert_eq!(body(&text), ["movq\taway@GOTTPOFF(%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 = 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],
&Globals::default(),
&[],
&names,
&target(Os::Linux),
true,
Output::default(),
)
.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_marked_listing_has_a_label_in_front_of_every_instruction_and_numbers_the_functions() {
let mut names = Interner::new();
let jmp = Opcode::new(names.intern("x64.jmp"));
let mut funcs = Vec::new();
for name in ["f", "g"] {
let mut func = Func::new(names.intern(name));
let first = func.create_block();
let second = func.create_block();
func.build(first, jmp).finish();
func.succs_mut(first).push(rucc_mir::BlockCall::to(second));
funcs.push(func);
}
let target = target(Os::Linux);
let text = print_marked(
&funcs,
&Globals::default(),
&[],
&names,
&target,
true,
Output::default(),
)
.expect("two functions");
let inst = funcs[1].insts(funcs[1].blocks().next().unwrap()).next().unwrap();
let label = mark(&target, 1, inst);
assert!(label.starts_with(".L"), "{label}");
assert!(text.contains(&format!("\n{label}:\n\tjmp\t.Lg_1\n")), "{text}");
assert!(text.contains(&format!("\n{}:\n", mark(&target, 0, inst))), "{text}");
let plain =
print(&funcs, &Globals::default(), &[], &names, &target, true, Output::default())
.expect("two functions");
assert!(!plain.contains("rucc_row"), "{plain}");
}
#[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 = 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),
&Globals::default(),
&[],
&names,
&target(Os::Linux),
true,
Output::default(),
)
.expect("elf");
assert!(elf.contains("\tcall\tputs\n"), "{elf}");
assert!(elf.contains("\n.Lf_0:\n"), "{elf}");
let macho = print(
&[func],
&Globals::default(),
&[],
&names,
&target(Os::Darwin),
true,
Output::default(),
)
.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 = Opcode::new(names.intern("x64.neg_r_32"));
func.build(block, neg).operand(Operand::write(vreg, GPR)).finish();
let error = print(
&[func],
&Globals::default(),
&[],
&names,
&target(Os::Linux),
true,
Output::default(),
)
.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 = Opcode::new(names.intern("x64.frobnicate"));
func.build(block, made_up).finish();
let error = print(
&[func],
&Globals::default(),
&[],
&names,
&target(Os::Linux),
true,
Output::default(),
)
.expect_err("no such instruction");
assert_eq!(
error,
Error::Opcode { func: "f".to_owned(), opcode: "x64.frobnicate".to_owned() }
);
}
#[test]
fn a_function_no_other_file_can_see_is_not_announced_to_the_linker() {
let mut names = Interner::new();
let mut hidden = Func::new(names.intern("hidden"));
hidden.binding = rucc_mir::Binding::Local;
hidden.create_block();
let text = print(
&[hidden],
&Globals::default(),
&[],
&names,
&target(Os::Linux),
true,
Output::default(),
)
.expect("elf");
assert!(text.contains("\nhidden:\n"), "{text}");
assert!(text.contains("\t.type\thidden, @function\n"), "{text}");
assert!(!text.contains(".globl"), "{text}");
}
#[test]
fn a_function_that_may_lose_to_another_definition_is_written_weak() {
let mut names = Interner::new();
let mut shared = Func::new(names.intern("shared"));
shared.binding = rucc_mir::Binding::Weak;
shared.create_block();
let text = print(
&[shared],
&Globals::default(),
&[],
&names,
&target(Os::Linux),
true,
Output::default(),
)
.expect("elf");
assert!(text.contains("\t.weak\tshared\n"), "{text}");
assert!(!text.contains(".globl"), "{text}");
}
#[test]
fn a_second_name_is_a_binding_and_a_set_and_nothing_else() {
let names = Interner::new();
let aliases = [
Alias {
name: "b".to_owned(),
target: "a".to_owned(),
binding: Binding::Global,
visibility: Visibility::Default,
},
Alias {
name: "c".to_owned(),
target: "a".to_owned(),
binding: Binding::Weak,
visibility: Visibility::Default,
},
Alias {
name: "d".to_owned(),
target: "a".to_owned(),
binding: Binding::Local,
visibility: Visibility::Default,
},
];
let vars = vec![var("a", Place::Written, vec![Piece::Scalar(vec![1, 0, 0, 0])])];
let text = print(
&[],
&Globals { vars, ..Globals::default() },
&aliases,
&names,
&target(Os::Linux),
true,
Output::default(),
)
.expect("a machine with a writer");
assert!(text.contains("\t.globl\tb\n\t.set\tb,a\n"), "{text}");
assert!(text.contains("\t.weak\tc\n\t.set\tc,a\n"), "{text}");
assert!(text.contains("\t.set\td,a\n"), "{text}");
assert!(!text.contains("\t.type\tb"), "the type comes from what it points at: {text}");
assert!(!text.contains("\t.size\tb"), "and so does the size: {text}");
assert_eq!(text.matches(".long\t1").count(), 1, "{text}");
}
#[test]
fn a_variable_is_a_section_a_name_and_the_bytes_between_them() {
let text = data(
vec![var("counter", Place::Written, vec![Piece::Scalar(vec![42, 0, 0, 0])])],
Os::Linux,
);
assert!(text.contains("\t.data\n"), "{text}");
assert!(text.contains("\t.globl\tcounter\n"), "{text}");
assert!(text.contains("\t.p2align\t2\n"), "{text}");
assert!(text.contains("\t.type\tcounter, @object\n"), "{text}");
assert!(text.contains("\ncounter:\n\t.long\t42\n"), "{text}");
assert!(text.contains("\t.size\tcounter, .-counter\n"), "{text}");
}
#[test]
fn a_thread_local_variable_is_a_section_with_the_flag_on_it_and_a_type_of_its_own() {
let text = data(
vec![var(
"counter",
Place::Thread { zero: false },
vec![Piece::Scalar(vec![42, 0, 0, 0])],
)],
Os::Linux,
);
assert!(text.contains("\t.section\t.tdata,\"awT\",@progbits\n"), "{text}");
assert!(text.contains("\t.type\tcounter, @tls_object\n"), "{text}");
assert!(text.contains("\ncounter:\n\t.long\t42\n"), "{text}");
}
#[test]
fn a_thread_local_variable_with_no_image_to_carry_goes_in_the_section_that_carries_none() {
let text = data(
vec![var("counter", Place::Thread { zero: true }, vec![Piece::Zero(4)])],
Os::Linux,
);
assert!(text.contains("\t.section\t.tbss,\"awT\",@nobits\n"), "{text}");
assert!(text.contains("\t.type\tcounter, @tls_object\n"), "{text}");
assert!(text.contains("\ncounter:\n\t.space\t4\n"), "{text}");
}
#[test]
fn a_thread_local_variable_on_mach_o_is_an_image_and_a_descriptor() {
let text = data(
vec![var(
"counter",
Place::Thread { zero: false },
vec![Piece::Scalar(vec![42, 0, 0, 0])],
)],
Os::Darwin,
);
let image = "\t.section\t__DATA,__thread_data,thread_local_regular\n\t.p2align\t2\n\
_counter$tlv$init:\n\t.long\t42\n";
assert!(text.contains(image), "{text}");
let descriptor = "\t.section\t__DATA,__thread_vars,thread_local_variables\n\
\t.globl\t_counter\n\t.p2align\t3\n_counter:\n\
\t.quad\t__tlv_bootstrap\n\t.quad\t0\n\t.quad\t_counter$tlv$init\n";
assert!(text.contains(descriptor), "{text}");
let zero = data(
vec![var("counter", Place::Thread { zero: true }, vec![Piece::Zero(4)])],
Os::Darwin,
);
assert!(zero.contains("\t.tbss\t_counter$tlv$init,4,2\n"), "{zero}");
assert!(zero.contains(descriptor), "{zero}");
}
#[test]
fn a_variable_no_other_file_can_see_is_not_announced_to_the_linker() {
let mut hidden = var("hidden", Place::Zero, vec![Piece::Zero(4)]);
hidden.binding = Binding::Local;
let text = data(vec![hidden], Os::Linux);
assert!(text.contains("\t.bss\n"), "{text}");
assert!(text.contains("\nhidden:\n\t.space\t4\n"), "{text}");
assert!(!text.contains(".globl"), "{text}");
}
#[test]
fn a_tentative_definition_is_a_request_rather_than_a_section_and_a_label() {
let text = data(vec![var("x", Place::Merged, vec![Piece::Zero(4)])], Os::Linux);
assert_eq!(text.lines().find(|line| line.contains(".comm")), Some("\t.comm\tx,4,4"));
assert!(!text.contains("\nx:\n"), "nothing here says where it is: {text}");
}
#[test]
fn every_function_gets_a_section_of_its_own_when_that_is_what_was_asked_for() {
let text = split_code("first", "second", Os::Linux);
assert!(text.starts_with("\t.text\n"), "{text}");
assert!(text.contains("\t.section\t.text.first,\"ax\",@progbits\n"), "{text}");
assert!(text.contains("\t.section\t.text.second,\"ax\",@progbits\n"), "{text}");
let opened = text.find(".section\t.text.first").expect("a section");
assert!(opened < text.find("\nfirst:\n").expect("a label"), "{text}");
let plain = write(|_, _| {});
assert!(!plain.contains(".text."), "{plain}");
}
#[test]
fn a_format_that_already_lets_the_linker_split_a_section_is_not_asked_to_split_it_again() {
let text = split_code("first", "second", Os::Darwin);
assert!(text.contains("\t.subsections_via_symbols\n"), "{text}");
assert_eq!(text.matches(".section").count(), 1, "the one it opens with: {text}");
let vars = vec![var("counter", Place::Written, vec![Piece::Scalar(vec![1, 0, 0, 0])])];
assert_eq!(split(vars.clone(), Os::Darwin), data(vars, Os::Darwin));
}
#[test]
fn every_variable_gets_a_section_named_after_it_when_that_is_what_was_asked_for() {
let vars = vec![
var("g", Place::Written, vec![Piece::Scalar(vec![1, 0, 0, 0])]),
var("z", Place::Zero, vec![Piece::Zero(4)]),
var("r", Place::ReadOnly, vec![Piece::Scalar(vec![3, 0, 0, 0])]),
];
let text = split(vars.clone(), Os::Linux);
assert!(text.contains("\t.section\t.data.g,\"aw\"\n\t.globl\tg\n"), "{text}");
assert!(text.contains("\t.section\t.bss.z,\"aw\",@nobits\n"), "{text}");
assert!(text.contains("\t.section\t.rodata.r,\"a\"\n"), "{text}");
assert!(text.contains("\ng:\n\t.long\t1\n"), "{text}");
assert!(text.contains("\t.size\tg, .-g\n"), "{text}");
assert!(text.contains("\t.space\t4\n"), "{text}");
assert!(!text.contains(".text."), "{text}");
let plain = data(vars, Os::Linux);
assert!(plain.contains("\t.data\n") && plain.contains("\t.bss\n"), "{plain}");
assert!(!plain.contains(".data.g"), "{plain}");
}
#[test]
fn the_object_format_decides_how_a_variable_is_written_as_much_as_a_function() {
let text = data(vec![var("x", Place::Zero, vec![Piece::Zero(4)])], Os::Darwin);
assert!(text.contains("\t.globl\t_x\n\t.zerofill\t__DATA,__bss,_x,4,2\n"), "{text}");
let read_only = data(vec![var("x", Place::ReadOnly, vec![Piece::Zero(4)])], Os::Darwin);
assert!(read_only.contains("\t.section\t__TEXT,__const\n"), "{read_only}");
assert!(read_only.contains("\n_x:\n"), "the underscore, without which nothing links");
}
#[test]
fn a_run_of_bytes_is_written_so_that_it_reads_back_as_the_same_bytes() {
let bytes = Piece::Bytes(b"a\"b\\\n\0\x801".to_vec());
let text = data(vec![var("s", Place::ReadOnly, vec![bytes])], Os::Linux);
assert!(text.contains("\t.ascii\t\"a\\\"b\\\\\\012\\000\\2001\"\n"), "{text}");
}
#[test]
fn the_address_of_a_name_in_an_image_is_written_as_the_name() {
let addr = Piece::Addr { symbol: "y".to_owned(), addend: 16, bytes: 8 };
let text = data(vec![var("p", Place::Written, vec![addr])], Os::Linux);
assert!(text.contains("\np:\n\t.quad\ty+16\n"), "{text}");
}
#[test]
fn a_distance_in_an_image_is_written_as_the_name_less_where_it_is() {
let away = Piece::Away { symbol: "y".to_owned(), addend: 0 };
let text = data(vec![var("d", Place::ReadOnly, vec![away])], Os::Linux);
assert!(text.contains("\nd:\n\t.long\ty - .\n"), "{text}");
let away = Piece::Away { symbol: "y".to_owned(), addend: -3 };
let text = data(vec![var("d", Place::ReadOnly, vec![away])], Os::Linux);
assert!(text.contains("\nd:\n\t.long\ty-3 - .\n"), "{text}");
}
#[test]
fn a_distance_between_two_labels_is_written_as_one_less_the_other() {
let piece = |addend, bytes| Piece::Apart {
to: ".Llbl.1".to_owned(),
from: ".Llbl.0".to_owned(),
addend,
bytes,
};
let text = data(vec![var("b", Place::ReadOnly, vec![piece(0, 4)])], Os::Linux);
assert!(text.contains("\nb:\n\t.long\t.Llbl.1-.Llbl.0\n"), "{text}");
let text = data(vec![var("b", Place::ReadOnly, vec![piece(-2, 2)])], Os::Linux);
assert!(text.contains("\nb:\n\t.short\t.Llbl.1-.Llbl.0-2\n"), "{text}");
}
#[test]
fn a_machine_with_no_writer_here_is_said_so_rather_than_written_as_x86_64() {
let names = Interner::new();
let riscv = TargetInfo::new(Triple::new(Arch::Riscv64, Os::Linux, Env::Gnu));
let error = print(&[], &Globals::default(), &[], &names, &riscv, true, Output::default())
.expect_err("no writer");
assert!(matches!(error, Error::Machine { .. }), "{error:?}");
}
fn write_a64(build: impl FnOnce(&mut Func, &mut Interner)) -> Result<String, Error> {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"));
build(&mut func, &mut names);
let target = TargetInfo::new(Triple::new(Arch::Aarch64, Os::Linux, Env::Gnu));
print(&[func], &Globals::default(), &[], &names, &target, true, Output::default())
}
#[test]
fn an_aarch64_instruction_is_written_the_way_its_own_table_says() {
use rucc_target::aarch64::{self, x};
let text = write_a64(|func, names| {
let block = func.create_block();
let add = Opcode::new(names.intern("a64.add_rr_32"));
func.build(block, add)
.operand(Operand::write(Reg::physical(x(0)), aarch64::GPR))
.operand(Operand::read(Reg::physical(x(1)), aarch64::GPR))
.operand(Operand::read(Reg::physical(x(2)), aarch64::GPR))
.finish();
let load = Opcode::new(names.intern("a64.ldr_64"));
let base = Operand::read(Reg::physical(aarch64::SP), aarch64::GPR);
func.build(block, load)
.operand(Operand::write(Reg::physical(x(3)), aarch64::GPR))
.mem(Mem::at(base).plus(16))
.finish();
})
.expect("an allocated function");
assert_eq!(body(&text), ["add w0, w1, w2", "ldr x3, [sp, #16]"]);
assert!(text.contains("\t.p2align\t4\n"), "{text}");
assert!(!text.contains("0x90"), "{text}");
}
#[test]
fn an_aarch64_register_left_virtual_is_refused() {
let error = write_a64(|func, names| {
let block = func.create_block();
let mov = Opcode::new(names.intern("a64.mov_rr_64"));
let class = aarch64::GPR;
let v0 = func.new_vreg(class);
let v1 = func.new_vreg(class);
func.build(block, mov)
.operand(Operand::write(v0, class))
.operand(Operand::read(v1, class))
.finish();
})
.expect_err("a register was never allocated");
assert!(matches!(error, Error::Virtual { .. }), "{error:?}");
}
#[test]
fn an_aarch64_access_through_an_index_shifts_it_by_the_size() {
use rucc_target::aarch64::{self, x};
let text = write_a64(|func, names| {
let block = func.create_block();
let store = Opcode::new(names.intern("a64.str_32"));
let base = Operand::read(Reg::physical(x(0)), aarch64::GPR);
let index = Operand::read(Reg::physical(x(2)), aarch64::GPR);
func.build(block, store)
.operand(Operand::read(Reg::physical(x(3)), aarch64::GPR))
.mem(Mem::at(base).indexed(index, 4))
.finish();
let load = Opcode::new(names.intern("a64.ldr_64"));
func.build(block, load)
.operand(Operand::write(Reg::physical(x(1)), aarch64::GPR))
.mem(Mem::at(base).indexed(index, 1))
.finish();
})
.expect("an allocated function");
assert_eq!(body(&text), ["str w3, [x0, x2, lsl #2]", "ldr x1, [x0, x2]"]);
}
#[test]
fn an_aarch64_offset_the_encoder_refuses_is_not_written() {
use rucc_target::aarch64::{self, x};
let error = write_a64(|func, names| {
let block = func.create_block();
let load = Opcode::new(names.intern("a64.ldr_64"));
let base = Operand::read(Reg::physical(x(0)), aarch64::GPR);
func.build(block, load)
.operand(Operand::write(Reg::physical(x(1)), aarch64::GPR))
.mem(Mem::at(base).plus(1 << 20))
.finish();
})
.expect_err("an offset no load can hold");
assert!(matches!(error, Error::Encode { .. }), "{error:?}");
}
#[test]
fn an_opcode_aarch64_does_not_have_is_refused_rather_than_written_as_x86() {
let error = write_a64(|func, names| {
let block = func.create_block();
func.build(block, Opcode::new(names.intern("x64.ret"))).finish();
})
.expect_err("not an AArch64 opcode");
assert!(matches!(error, Error::Opcode { .. }), "{error:?}");
}
#[test]
fn the_head_of_a_loop_is_asked_to_stay_inside_one_line() {
let mut names = Interner::new();
let mut func = Func::new(names.intern("f"));
func.create_block();
let head = func.create_block();
let jmp = Opcode::new(names.intern("x64.jmp"));
func.build(head, jmp).finish();
func.succs_mut(head).push(rucc_mir::BlockCall::to(head));
func.heads = vec![head];
let text = print(
&[func],
&Globals::default(),
&[],
&names,
&target(Os::Linux),
true,
Output::default(),
)
.expect("a function with a loop in it");
let wanted = "\n.Lf_0:\n\t.p2align\t6,,4\n.Lf_1:\n";
assert!(text.contains(wanted), "{text}");
}
}