refactor(backend): modularize x86_64 codegen
- Moves x86_64 backend implementation from a single file into a structured module (`codegen`, `types`, `mod`). - Introduces a dedicated `Codegen` helper to enforce x86-64 hardware constraints (e.g., preventing memory-to-memory moves). - Implements strongly-typed register formatting and operand width handling. - Renames `Instruction::Assign` to `Instruction::Copy` across the IR and all optimization passes to better reflect semantic intent. - Updates the x86 backend to handle ABI-compliant stack alignment and register allocation more robustly.
This commit is contained in:
@@ -1,732 +0,0 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
use std::fmt::Write;
|
|
||||||
use std::mem::take;
|
|
||||||
|
|
||||||
use crate::ir::*;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
enum Storage {
|
|
||||||
Hardware(&'static str),
|
|
||||||
Stack(usize),
|
|
||||||
Alloc(usize), // Represents the address `-offset(%rbp)`
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct X86Backend<'a> {
|
|
||||||
assembly: String,
|
|
||||||
module: &'a Module,
|
|
||||||
live_intervals: HashMap<Register, (usize, usize)>,
|
|
||||||
allocations: HashMap<Register, Storage>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> X86Backend<'a> {
|
|
||||||
pub fn new(module: &'a Module) -> Self {
|
|
||||||
Self {
|
|
||||||
assembly: String::new(),
|
|
||||||
module,
|
|
||||||
live_intervals: HashMap::new(),
|
|
||||||
allocations: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn compile_module(mut self) -> String {
|
|
||||||
for func in &self.module.functions {
|
|
||||||
self.compile_function(func);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.assembly
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_op(
|
|
||||||
&mut self,
|
|
||||||
op: &Operand,
|
|
||||||
scratch_reg: &str,
|
|
||||||
allocs: &HashMap<Register, Storage>,
|
|
||||||
) -> String {
|
|
||||||
match op {
|
|
||||||
Operand::Integer(v) => format!("${}", v),
|
|
||||||
Operand::Boolean(b) => format!("${}", if *b { 1 } else { 0 }),
|
|
||||||
Operand::Register(r) => match allocs.get(r).unwrap() {
|
|
||||||
Storage::Hardware(hw) => format!("%{}", hw),
|
|
||||||
Storage::Stack(off) => format!("-{}(%rbp)", off),
|
|
||||||
Storage::Alloc(off) => {
|
|
||||||
// Materialize the address dynamically into the provided scratch register
|
|
||||||
writeln!(
|
|
||||||
&mut self.assembly,
|
|
||||||
" leaq -{}(%rbp), {}",
|
|
||||||
off, scratch_reg
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
scratch_reg.to_string()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mark_use(&mut self, reg: Register, idx: usize) {
|
|
||||||
if !self.allocations.contains_key(®) {
|
|
||||||
let entry = self.live_intervals.entry(reg).or_insert((idx, idx));
|
|
||||||
entry.1 = idx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mark_op(&mut self, op: &Operand, idx: usize) {
|
|
||||||
if let Operand::Register(r) = op {
|
|
||||||
self.mark_use(*r, idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compile_function(&mut self, func: &Function) {
|
|
||||||
// 0. Pre-Pass: Handle Allocations
|
|
||||||
let mut allocations: HashMap<Register, Storage> = HashMap::new();
|
|
||||||
let mut next_stack_offset = 0;
|
|
||||||
|
|
||||||
for block in &func.blocks {
|
|
||||||
for inst in &block.instructions {
|
|
||||||
if let Instruction::Alloc { dest, .. } = inst {
|
|
||||||
next_stack_offset += 8;
|
|
||||||
allocations.insert(*dest, Storage::Alloc(next_stack_offset));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Liveness Analysis & Call Tracking
|
|
||||||
let mut call_indices = Vec::new();
|
|
||||||
let mut hints: HashMap<Register, &'static str> = HashMap::new();
|
|
||||||
let mut inst_idx = 0;
|
|
||||||
|
|
||||||
for (_, reg) in &func.params {
|
|
||||||
self.mark_use(*reg, inst_idx);
|
|
||||||
}
|
|
||||||
inst_idx += 1;
|
|
||||||
|
|
||||||
for block in &func.blocks {
|
|
||||||
for inst in &block.instructions {
|
|
||||||
match inst {
|
|
||||||
Instruction::Alloc { .. } => {}
|
|
||||||
Instruction::Call { dest, args, .. } => {
|
|
||||||
self.mark_use(*dest, inst_idx);
|
|
||||||
let arg_regs = ["rdi", "rsi", "rdx", "rcx", "r8", "r9"];
|
|
||||||
for (i, (_, arg_op)) in args.iter().enumerate() {
|
|
||||||
self.mark_op(arg_op, inst_idx);
|
|
||||||
if i < 6
|
|
||||||
&& let Operand::Register(r) = arg_op
|
|
||||||
{
|
|
||||||
hints.insert(*r, arg_regs[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
call_indices.push(inst_idx);
|
|
||||||
}
|
|
||||||
Instruction::Load { dest, src, .. } => {
|
|
||||||
self.mark_use(*dest, inst_idx);
|
|
||||||
self.mark_op(src, inst_idx);
|
|
||||||
}
|
|
||||||
Instruction::Store { dest, src, .. } => {
|
|
||||||
self.mark_op(dest, inst_idx);
|
|
||||||
self.mark_op(src, inst_idx);
|
|
||||||
}
|
|
||||||
Instruction::Assign { register, operand } => {
|
|
||||||
self.mark_use(*register, inst_idx);
|
|
||||||
self.mark_op(operand, inst_idx);
|
|
||||||
}
|
|
||||||
Instruction::Binary {
|
|
||||||
dest, src1, src2, ..
|
|
||||||
} => {
|
|
||||||
self.mark_use(*dest, inst_idx);
|
|
||||||
self.mark_op(src1, inst_idx);
|
|
||||||
self.mark_op(src2, inst_idx);
|
|
||||||
}
|
|
||||||
Instruction::Unary { dest, src, .. } => {
|
|
||||||
self.mark_use(*dest, inst_idx);
|
|
||||||
self.mark_op(src, inst_idx);
|
|
||||||
}
|
|
||||||
Instruction::Phi { dest, sources, .. } => {
|
|
||||||
self.mark_use(*dest, inst_idx);
|
|
||||||
for (op, _) in sources {
|
|
||||||
self.mark_op(op, inst_idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
inst_idx += 1;
|
|
||||||
}
|
|
||||||
match &block.terminator {
|
|
||||||
Terminator::Branch { cond, .. } => self.mark_op(cond, inst_idx),
|
|
||||||
Terminator::Return {
|
|
||||||
value: Some(val), ..
|
|
||||||
} => self.mark_op(val, inst_idx),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
inst_idx += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. ABI-Aware Linear Scan Allocation
|
|
||||||
let mut free_callee_saved = vec!["rbx", "r12", "r13", "r14", "r15"];
|
|
||||||
let mut free_caller_saved = vec!["rdi", "rsi", "rdx", "rcx", "r8", "r9"];
|
|
||||||
|
|
||||||
let mut active: Vec<(Register, usize, &'static str, bool)> = Vec::new();
|
|
||||||
let mut used_callee_saved = HashSet::new();
|
|
||||||
|
|
||||||
let live_intervals = take(&mut self.live_intervals);
|
|
||||||
let mut intervals_sorted: Vec<_> = live_intervals.into_iter().collect();
|
|
||||||
intervals_sorted.sort_by_key(|(_, (start, _))| *start);
|
|
||||||
|
|
||||||
for (reg, (start, end)) in intervals_sorted {
|
|
||||||
active.retain(|(_, active_end, hw_reg, is_caller)| {
|
|
||||||
if *active_end < start {
|
|
||||||
if *is_caller {
|
|
||||||
free_caller_saved.push(hw_reg);
|
|
||||||
} else {
|
|
||||||
free_callee_saved.push(hw_reg);
|
|
||||||
}
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let crosses_call = call_indices
|
|
||||||
.iter()
|
|
||||||
.any(|&c_idx| c_idx > start && c_idx < end);
|
|
||||||
|
|
||||||
let mut selected_hw = None;
|
|
||||||
let mut is_caller = false;
|
|
||||||
|
|
||||||
if !crosses_call {
|
|
||||||
if let Some(&hint_reg) = hints.get(®)
|
|
||||||
&& let Some(pos) = free_caller_saved.iter().position(|&r| r == hint_reg)
|
|
||||||
{
|
|
||||||
selected_hw = Some(free_caller_saved.remove(pos));
|
|
||||||
is_caller = true;
|
|
||||||
}
|
|
||||||
if selected_hw.is_none()
|
|
||||||
&& let Some(r) = free_caller_saved.pop()
|
|
||||||
{
|
|
||||||
selected_hw = Some(r);
|
|
||||||
is_caller = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if selected_hw.is_none()
|
|
||||||
&& let Some(r) = free_callee_saved.pop()
|
|
||||||
{
|
|
||||||
selected_hw = Some(r);
|
|
||||||
|
|
||||||
used_callee_saved.insert(r);
|
|
||||||
is_caller = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(hw_reg) = selected_hw {
|
|
||||||
allocations.insert(reg, Storage::Hardware(hw_reg));
|
|
||||||
active.push((reg, end, hw_reg, is_caller));
|
|
||||||
} else {
|
|
||||||
next_stack_offset += 8;
|
|
||||||
allocations.insert(reg, Storage::Stack(next_stack_offset));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let used_callee_saved: Vec<&'static str> = used_callee_saved.into_iter().collect();
|
|
||||||
|
|
||||||
// Determine if a Stack Frame is required
|
|
||||||
let needs_frame = !call_indices.is_empty()
|
|
||||||
|| next_stack_offset > 0
|
|
||||||
|| !used_callee_saved.is_empty()
|
|
||||||
|| func.params.len() > 6;
|
|
||||||
|
|
||||||
// 3. Prologue
|
|
||||||
writeln!(&mut self.assembly, " .text").unwrap();
|
|
||||||
writeln!(&mut self.assembly, " .globl {}", func.name).unwrap();
|
|
||||||
writeln!(&mut self.assembly, " .p2align 4").unwrap();
|
|
||||||
writeln!(&mut self.assembly, " .type {},@function", func.name).unwrap();
|
|
||||||
writeln!(&mut self.assembly, "{}:", func.name).unwrap();
|
|
||||||
|
|
||||||
if needs_frame {
|
|
||||||
writeln!(&mut self.assembly, " pushq %rbp").unwrap();
|
|
||||||
writeln!(&mut self.assembly, " movq %rsp, %rbp").unwrap();
|
|
||||||
|
|
||||||
for reg in &used_callee_saved {
|
|
||||||
writeln!(&mut self.assembly, " pushq %{}", reg).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let s = next_stack_offset;
|
|
||||||
let rem = (used_callee_saved.len() * 8 + s) % 16;
|
|
||||||
let stack_adj = if rem != 0 { s + (16 - rem) } else { s };
|
|
||||||
|
|
||||||
if stack_adj > 0 {
|
|
||||||
writeln!(&mut self.assembly, " subq ${}, %rsp", stack_adj).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Map ABI Arguments
|
|
||||||
let arg_regs = ["rdi", "rsi", "rdx", "rcx", "r8", "r9"];
|
|
||||||
for (i, (_, reg)) in func.params.iter().enumerate() {
|
|
||||||
let dest_str = self.format_dest(*reg, &allocations);
|
|
||||||
if i < 6 {
|
|
||||||
writeln!(
|
|
||||||
&mut self.assembly,
|
|
||||||
" movq %{}, {}",
|
|
||||||
arg_regs[i], dest_str
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
} else {
|
|
||||||
let caller_offset = 16 + ((i - 6) * 8);
|
|
||||||
writeln!(&mut self.assembly, " movq {}(%rbp), %rax", caller_offset).unwrap();
|
|
||||||
writeln!(&mut self.assembly, " movq %rax, {}", dest_str).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Compile Blocks
|
|
||||||
let num_blocks = func.blocks.len();
|
|
||||||
for i in 0..num_blocks {
|
|
||||||
let block = &func.blocks[i];
|
|
||||||
let next_block_id = if i + 1 < num_blocks {
|
|
||||||
Some(func.blocks[i + 1].id)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
writeln!(&mut self.assembly, ".L{}_block_{}:", func.name, block.id.0).unwrap();
|
|
||||||
|
|
||||||
// Peephole Optimization: Fuse an immediately preceding ICmp into the Branch
|
|
||||||
let mut fused_cmp = None;
|
|
||||||
if let Terminator::Branch {
|
|
||||||
cond: Operand::Register(cond_reg),
|
|
||||||
..
|
|
||||||
} = block.terminator
|
|
||||||
&& let Some(Instruction::Binary {
|
|
||||||
dest,
|
|
||||||
op: BinaryOp::ICmp(cmp_op),
|
|
||||||
src1,
|
|
||||||
src2,
|
|
||||||
..
|
|
||||||
}) = block.instructions.last()
|
|
||||||
&& cond_reg == *dest
|
|
||||||
{
|
|
||||||
fused_cmp = Some((*cmp_op, *src1, *src2));
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we fused the comparison, we omit the last instruction from standard compilation
|
|
||||||
let inst_limit = if fused_cmp.is_some() {
|
|
||||||
block.instructions.len() - 1
|
|
||||||
} else {
|
|
||||||
block.instructions.len()
|
|
||||||
};
|
|
||||||
|
|
||||||
for inst in &block.instructions[..inst_limit] {
|
|
||||||
self.compile_instruction(inst, &allocations);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.compile_terminator(
|
|
||||||
&block.terminator,
|
|
||||||
&func.name,
|
|
||||||
&allocations,
|
|
||||||
next_block_id,
|
|
||||||
fused_cmp,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Unified Epilogue
|
|
||||||
writeln!(&mut self.assembly, ".L{}_epilogue:", func.name).unwrap();
|
|
||||||
|
|
||||||
if needs_frame {
|
|
||||||
let pushes_size = used_callee_saved.len() * 8;
|
|
||||||
if pushes_size > 0 {
|
|
||||||
writeln!(&mut self.assembly, " leaq -{}(%rbp), %rsp", pushes_size).unwrap();
|
|
||||||
for reg in used_callee_saved.iter().rev() {
|
|
||||||
writeln!(&mut self.assembly, " popq %{}", reg).unwrap();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
writeln!(&mut self.assembly, " movq %rbp, %rsp").unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
writeln!(&mut self.assembly, " popq %rbp").unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
writeln!(&mut self.assembly, " ret\n").unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compile_instruction(&mut self, inst: &Instruction, allocs: &HashMap<Register, Storage>) {
|
|
||||||
match inst {
|
|
||||||
Instruction::Alloc { .. } => {} // Stack space is already reserved in prologue
|
|
||||||
Instruction::Assign { register, operand } => {
|
|
||||||
let dest = self.format_dest(*register, allocs);
|
|
||||||
let src = self.resolve_op(operand, "%rax", allocs);
|
|
||||||
self.emit_mov(&src, &dest);
|
|
||||||
}
|
|
||||||
Instruction::Load { dest, src, .. } => {
|
|
||||||
let dest_str = self.format_dest(*dest, allocs);
|
|
||||||
|
|
||||||
// OPTIMIZATION: If reading from an Alloc pointer, read directly from the stack offset
|
|
||||||
if let Operand::Register(r) = src
|
|
||||||
&& let Some(Storage::Alloc(off)) = allocs.get(r)
|
|
||||||
{
|
|
||||||
self.emit_mov(&format!("-{}(%rbp)", off), &dest_str);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let src_str = self.resolve_op(src, "%rax", allocs);
|
|
||||||
let addr_reg = if src_str.starts_with('-') {
|
|
||||||
writeln!(&mut self.assembly, " movq {}, %rax", src_str).unwrap();
|
|
||||||
"%rax".to_string()
|
|
||||||
} else {
|
|
||||||
src_str
|
|
||||||
};
|
|
||||||
|
|
||||||
writeln!(&mut self.assembly, " movq ({}), %r10", addr_reg).unwrap();
|
|
||||||
self.emit_mov("%r10", &dest_str);
|
|
||||||
}
|
|
||||||
Instruction::Store { dest, src, .. } => {
|
|
||||||
let src_str = self.resolve_op(src, "%rax", allocs);
|
|
||||||
|
|
||||||
// OPTIMIZATION: If writing to an Alloc pointer, write directly to the stack offset
|
|
||||||
if let Operand::Register(r) = dest
|
|
||||||
&& let Some(Storage::Alloc(off)) = allocs.get(r)
|
|
||||||
{
|
|
||||||
self.emit_mov(&src_str, &format!("-{}(%rbp)", off));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let dest_str = self.resolve_op(dest, "%r10", allocs);
|
|
||||||
let addr_reg = if dest_str.starts_with('-') {
|
|
||||||
writeln!(&mut self.assembly, " movq {}, %r10", dest_str).unwrap();
|
|
||||||
"%r10".to_string()
|
|
||||||
} else {
|
|
||||||
dest_str
|
|
||||||
};
|
|
||||||
|
|
||||||
let val_reg = if src_str.starts_with('-') || src_str.starts_with('$') {
|
|
||||||
writeln!(&mut self.assembly, " movq {}, %rax", src_str).unwrap();
|
|
||||||
"%rax".to_string()
|
|
||||||
} else {
|
|
||||||
src_str
|
|
||||||
};
|
|
||||||
|
|
||||||
writeln!(&mut self.assembly, " movq {}, ({})", val_reg, addr_reg).unwrap();
|
|
||||||
}
|
|
||||||
Instruction::Unary { dest, op, src, .. } => {
|
|
||||||
let dest_str = self.format_dest(*dest, allocs);
|
|
||||||
let src_str = self.resolve_op(src, "%rax", allocs);
|
|
||||||
|
|
||||||
self.emit_mov(&src_str, &dest_str);
|
|
||||||
match op {
|
|
||||||
UnaryOp::INeg => writeln!(&mut self.assembly, " negq {}", dest_str).unwrap(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Instruction::Binary {
|
|
||||||
dest,
|
|
||||||
op,
|
|
||||||
src1,
|
|
||||||
src2,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
let dest_str = self.format_dest(*dest, allocs);
|
|
||||||
let src1_str = self.resolve_op(src1, "%r10", allocs);
|
|
||||||
let src2_str = self.resolve_op(src2, "%r11", allocs);
|
|
||||||
|
|
||||||
match op {
|
|
||||||
BinaryOp::Add | BinaryOp::Sub | BinaryOp::SMul => {
|
|
||||||
let is_add = matches!(op, BinaryOp::Add);
|
|
||||||
let dest_hw = !dest_str.starts_with('-');
|
|
||||||
let src1_hw = !src1_str.starts_with('-');
|
|
||||||
let src2_hw = !src2_str.starts_with('-');
|
|
||||||
let src2_imm = src2_str.starts_with('$');
|
|
||||||
|
|
||||||
if dest_str != src1_str
|
|
||||||
&& dest_hw
|
|
||||||
&& src1_hw
|
|
||||||
&& src2_imm
|
|
||||||
&& !matches!(op, BinaryOp::UMul)
|
|
||||||
{
|
|
||||||
let val: i64 = src2_str[1..].parse().unwrap();
|
|
||||||
let offset = if is_add { val } else { -val };
|
|
||||||
writeln!(
|
|
||||||
&mut self.assembly,
|
|
||||||
" leaq {}({}), {}",
|
|
||||||
offset, src1_str, dest_str
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
} else if dest_str != src1_str && dest_hw && src1_hw && src2_hw && is_add {
|
|
||||||
writeln!(
|
|
||||||
&mut self.assembly,
|
|
||||||
" leaq ({},{}), {}",
|
|
||||||
src1_str, src2_str, dest_str
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
} else {
|
|
||||||
self.emit_mov(&src1_str, &dest_str);
|
|
||||||
let mnemonic = match op {
|
|
||||||
BinaryOp::Add => "addq",
|
|
||||||
BinaryOp::Sub => "subq",
|
|
||||||
BinaryOp::SMul => "imulq",
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
if dest_str.starts_with('-') && src2_str.starts_with('-') {
|
|
||||||
writeln!(&mut self.assembly, " movq {}, %rax", src2_str)
|
|
||||||
.unwrap();
|
|
||||||
writeln!(&mut self.assembly, " {} %rax, {}", mnemonic, dest_str)
|
|
||||||
.unwrap();
|
|
||||||
} else {
|
|
||||||
writeln!(
|
|
||||||
&mut self.assembly,
|
|
||||||
" {} {}, {}",
|
|
||||||
mnemonic, src2_str, dest_str
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BinaryOp::UMul => {
|
|
||||||
// Unsigned multiply (mulq) strictly takes 1 operand and multiplies it by %rax.
|
|
||||||
writeln!(&mut self.assembly, " movq {}, %rax", src1_str).unwrap();
|
|
||||||
|
|
||||||
// mulq cannot take an immediate value, so route it through a scratch register if needed
|
|
||||||
if src2_str.starts_with('$') {
|
|
||||||
writeln!(&mut self.assembly, " movq {}, %r10", src2_str).unwrap();
|
|
||||||
writeln!(&mut self.assembly, " mulq %r10").unwrap();
|
|
||||||
} else {
|
|
||||||
writeln!(&mut self.assembly, " mulq {}", src2_str).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The lower 64 bits of the result are in %rax
|
|
||||||
self.emit_mov("%rax", &dest_str);
|
|
||||||
}
|
|
||||||
BinaryOp::SDiv | BinaryOp::SRem => {
|
|
||||||
writeln!(&mut self.assembly, " movq {}, %rax", src1_str).unwrap();
|
|
||||||
writeln!(&mut self.assembly, " cqto").unwrap();
|
|
||||||
|
|
||||||
if src2_str.starts_with('$') {
|
|
||||||
writeln!(&mut self.assembly, " movq {}, %r10", src2_str).unwrap();
|
|
||||||
writeln!(&mut self.assembly, " idivq %r10").unwrap();
|
|
||||||
} else {
|
|
||||||
writeln!(&mut self.assembly, " idivq {}", src2_str).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let result_reg = if let BinaryOp::URem = op {
|
|
||||||
"%rdx"
|
|
||||||
} else {
|
|
||||||
"%rax"
|
|
||||||
};
|
|
||||||
self.emit_mov(result_reg, &dest_str);
|
|
||||||
}
|
|
||||||
BinaryOp::UDiv | BinaryOp::URem => {
|
|
||||||
writeln!(&mut self.assembly, " movq {}, %rax", src1_str).unwrap();
|
|
||||||
writeln!(&mut self.assembly, " cqto").unwrap();
|
|
||||||
|
|
||||||
if src2_str.starts_with('$') {
|
|
||||||
writeln!(&mut self.assembly, " movq {}, %r10", src2_str).unwrap();
|
|
||||||
writeln!(&mut self.assembly, " divq %r10").unwrap();
|
|
||||||
} else {
|
|
||||||
writeln!(&mut self.assembly, " divq {}", src2_str).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let result_reg = if let BinaryOp::URem = op {
|
|
||||||
"%rdx"
|
|
||||||
} else {
|
|
||||||
"%rax"
|
|
||||||
};
|
|
||||||
self.emit_mov(result_reg, &dest_str);
|
|
||||||
}
|
|
||||||
BinaryOp::ICmp(cmp) => {
|
|
||||||
if (src1_str.starts_with('-') && src2_str.starts_with('-'))
|
|
||||||
|| src1_str.starts_with('$')
|
|
||||||
{
|
|
||||||
writeln!(&mut self.assembly, " movq {}, %rax", src1_str).unwrap();
|
|
||||||
writeln!(&mut self.assembly, " cmpq {}, %rax", src2_str).unwrap();
|
|
||||||
} else {
|
|
||||||
writeln!(&mut self.assembly, " cmpq {}, {}", src2_str, src1_str)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let set_cc = match cmp {
|
|
||||||
ICmpOp::Eq => "sete",
|
|
||||||
ICmpOp::Ne => "setne",
|
|
||||||
ICmpOp::Slt => "setl",
|
|
||||||
ICmpOp::Sle => "setle",
|
|
||||||
ICmpOp::Sgt => "setg",
|
|
||||||
ICmpOp::Sge => "setge",
|
|
||||||
ICmpOp::Ult => "setb",
|
|
||||||
ICmpOp::Ule => "setbe",
|
|
||||||
ICmpOp::Ugt => "seta",
|
|
||||||
ICmpOp::Uge => "setae",
|
|
||||||
};
|
|
||||||
|
|
||||||
writeln!(&mut self.assembly, " {} %al", set_cc).unwrap();
|
|
||||||
writeln!(&mut self.assembly, " movzbq %al, %rax").unwrap();
|
|
||||||
self.emit_mov("%rax", &dest_str);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Instruction::Call {
|
|
||||||
dest, func, args, ..
|
|
||||||
} => {
|
|
||||||
let arg_regs = ["rdi", "rsi", "rdx", "rcx", "r8", "r9"];
|
|
||||||
for (i, (_, arg_op)) in args.iter().enumerate() {
|
|
||||||
let src = self.resolve_op(arg_op, "%r10", allocs);
|
|
||||||
if i < 6 {
|
|
||||||
self.emit_mov(&src, &format!("%{}", arg_regs[i]));
|
|
||||||
} else {
|
|
||||||
if src.starts_with('-') {
|
|
||||||
writeln!(&mut self.assembly, " movq {}, %rax", src).unwrap();
|
|
||||||
writeln!(&mut self.assembly, " pushq %rax").unwrap();
|
|
||||||
} else {
|
|
||||||
writeln!(&mut self.assembly, " pushq {}", src).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let function_name = self
|
|
||||||
.module
|
|
||||||
.functions
|
|
||||||
.iter()
|
|
||||||
.find_map(|f| (f.id == *func).then(|| f.name.clone()))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
writeln!(&mut self.assembly, " call {}", function_name).unwrap();
|
|
||||||
if args.len() > 6 {
|
|
||||||
let cleanup_size = (args.len() - 6) * 8;
|
|
||||||
writeln!(&mut self.assembly, " addq ${}, %rsp", cleanup_size).unwrap();
|
|
||||||
}
|
|
||||||
let dest_str = self.format_dest(*dest, allocs);
|
|
||||||
self.emit_mov("%rax", &dest_str);
|
|
||||||
}
|
|
||||||
Instruction::Phi { .. } => {
|
|
||||||
unreachable!(
|
|
||||||
"Phi nodes cannot be compiled to x86-64 natively. You must run an SSA-Destruction (Out-of-SSA) pass before code generation!"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compile_terminator(
|
|
||||||
&mut self,
|
|
||||||
term: &Terminator,
|
|
||||||
func_name: &str,
|
|
||||||
allocs: &HashMap<Register, Storage>,
|
|
||||||
next_block_id: Option<BlockId>,
|
|
||||||
fused_cmp: Option<(ICmpOp, Operand, Operand)>,
|
|
||||||
) {
|
|
||||||
match term {
|
|
||||||
Terminator::Return { value, .. } => {
|
|
||||||
if let Some(val) = value {
|
|
||||||
let src = self.resolve_op(val, "%r10", allocs);
|
|
||||||
self.emit_mov(&src, "%rax");
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is no next block we can fallthrough otherwise emit a jump
|
|
||||||
if next_block_id.is_some() {
|
|
||||||
writeln!(&mut self.assembly, " jmp .L{}_epilogue", func_name).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Terminator::Jump(target) => {
|
|
||||||
if Some(*target) != next_block_id {
|
|
||||||
writeln!(
|
|
||||||
&mut self.assembly,
|
|
||||||
" jmp .L{}_block_{}",
|
|
||||||
func_name, target.0
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Terminator::Branch {
|
|
||||||
cond,
|
|
||||||
then_block,
|
|
||||||
else_block,
|
|
||||||
} => {
|
|
||||||
// Determine the condition codes based on whether we fused an ICmp
|
|
||||||
let (jump_cond_true, jump_cond_false) =
|
|
||||||
if let Some((cmp_op, src1, src2)) = fused_cmp {
|
|
||||||
let src1_str = self.resolve_op(&src1, "%r10", allocs);
|
|
||||||
let src2_str = self.resolve_op(&src2, "%r10", allocs);
|
|
||||||
|
|
||||||
// Emit the comparison directly inside the terminator
|
|
||||||
if (src1_str.starts_with('-') && src2_str.starts_with('-'))
|
|
||||||
|| src1_str.starts_with('$')
|
|
||||||
{
|
|
||||||
writeln!(&mut self.assembly, " movq {}, %rax", src1_str).unwrap();
|
|
||||||
writeln!(&mut self.assembly, " cmpq {}, %rax", src2_str).unwrap();
|
|
||||||
} else {
|
|
||||||
writeln!(&mut self.assembly, " cmpq {}, {}", src2_str, src1_str)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map IR ICmpOp to native AT&T condition suffixes (true_jump, false_jump)
|
|
||||||
match cmp_op {
|
|
||||||
ICmpOp::Eq => ("e", "ne"),
|
|
||||||
ICmpOp::Ne => ("ne", "e"),
|
|
||||||
ICmpOp::Slt => ("l", "ge"),
|
|
||||||
ICmpOp::Sle => ("le", "g"),
|
|
||||||
ICmpOp::Sgt => ("g", "le"),
|
|
||||||
ICmpOp::Sge => ("ge", "l"),
|
|
||||||
ICmpOp::Ult => ("b", "ae"),
|
|
||||||
ICmpOp::Ule => ("be", "a"),
|
|
||||||
ICmpOp::Ugt => ("a", "be"),
|
|
||||||
ICmpOp::Uge => ("ae", "b"),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Standard fallback: evaluating an isolated boolean
|
|
||||||
let cond_str = self.resolve_op(cond, "%r10", allocs);
|
|
||||||
if cond_str.starts_with('$') {
|
|
||||||
writeln!(&mut self.assembly, " movq {}, %rax", cond_str).unwrap();
|
|
||||||
writeln!(&mut self.assembly, " testq %rax, %rax").unwrap();
|
|
||||||
} else {
|
|
||||||
writeln!(&mut self.assembly, " testq {}, {}", cond_str, cond_str)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
("nz", "z") // true = not zero, false = zero
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fallthrough logic cleanly applied to dynamically `d jump conditions
|
|
||||||
if Some(*else_block) == next_block_id {
|
|
||||||
writeln!(
|
|
||||||
&mut self.assembly,
|
|
||||||
" j{} .L{}_block_{}",
|
|
||||||
jump_cond_true, func_name, then_block.0
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
} else if Some(*then_block) == next_block_id {
|
|
||||||
writeln!(
|
|
||||||
&mut self.assembly,
|
|
||||||
" j{} .L{}_block_{}",
|
|
||||||
jump_cond_false, func_name, else_block.0
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
} else {
|
|
||||||
writeln!(
|
|
||||||
&mut self.assembly,
|
|
||||||
" j{} .L{}_block_{}",
|
|
||||||
jump_cond_false, func_name, else_block.0
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
writeln!(
|
|
||||||
&mut self.assembly,
|
|
||||||
" jmp .L{}_block_{}",
|
|
||||||
func_name, then_block.0
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Terminator::Unknown => panic!("Cannot compile Unknown terminator"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helpers
|
|
||||||
|
|
||||||
fn format_dest(&self, reg: Register, allocs: &HashMap<Register, Storage>) -> String {
|
|
||||||
match allocs.get(®).unwrap() {
|
|
||||||
Storage::Hardware(hw) => format!("%{}", hw),
|
|
||||||
Storage::Stack(off) | Storage::Alloc(off) => format!("-{}(%rbp)", off),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Emits a move instruction, gracefully handling memory-to-memory constraints
|
|
||||||
fn emit_mov(&mut self, src: &str, dest: &str) {
|
|
||||||
if src == dest {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if src.starts_with('-') && dest.starts_with('-') {
|
|
||||||
writeln!(&mut self.assembly, " movq {}, %rax", src).unwrap();
|
|
||||||
writeln!(&mut self.assembly, " movq %rax, {}", dest).unwrap();
|
|
||||||
} else {
|
|
||||||
writeln!(&mut self.assembly, " movq {}, {}", src, dest).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
use crate::backend::x86_64::types::*;
|
||||||
|
|
||||||
|
pub struct Codegen {
|
||||||
|
pub instructions: Vec<Instruction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Codegen {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Codegen {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
instructions: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emits a standard `mov` instruction.
|
||||||
|
pub fn emit_mov(&mut self, width: OperandWidth, src: Operand, dest: Operand) {
|
||||||
|
// Validation 1: Destination cannot be an immediate constant
|
||||||
|
if matches!(dest, Operand::Imm(_)) {
|
||||||
|
panic!("x86-64 error: Cannot use an immediate as a destination operand.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation 2: Memory-to-Memory moves are physically impossible on x86-64
|
||||||
|
if matches!(src, Operand::Mem { .. }) && matches!(dest, Operand::Mem { .. }) {
|
||||||
|
panic!(
|
||||||
|
"x86-64 error: Memory-to-memory moves are not allowed. Route the value through a scratch register first."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.instructions.push(Instruction::Mov(width, src, dest));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emits a 2-operand arithmetic instruction like `add`, `sub`, or `cmp`.
|
||||||
|
pub fn emit_binary(
|
||||||
|
&mut self,
|
||||||
|
op: fn(OperandWidth, Operand, Operand) -> Instruction,
|
||||||
|
width: OperandWidth,
|
||||||
|
src: Operand,
|
||||||
|
dest: Operand,
|
||||||
|
) {
|
||||||
|
if matches!(dest, Operand::Imm(_)) {
|
||||||
|
panic!("x86-64 error: Destination of a binary operation cannot be an immediate.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(src, Operand::Mem { .. }) && matches!(dest, Operand::Mem { .. }) {
|
||||||
|
panic!(
|
||||||
|
"x86-64 error: Binary operations cannot act on two memory addresses simultaneously."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.instructions.push(op(width, src, dest));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emits a `lea` (Load Effective Address) instruction.
|
||||||
|
/// This uses the CPU's memory addressing circuitry to perform fast, non-destructive math.
|
||||||
|
pub fn emit_lea(&mut self, width: OperandWidth, src: Operand, dest: Operand) {
|
||||||
|
// Validation 1: Destination MUST be a hardware register.
|
||||||
|
// You cannot calculate an address and store it directly into memory or an immediate.
|
||||||
|
if !matches!(dest, Operand::Reg(_)) {
|
||||||
|
panic!("x86-64 error: Destination of a `lea` instruction must be a hardware register.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation 2: Source MUST be a memory operand.
|
||||||
|
// Even if we are just using it for math (e.g., `leaq -1(%r15), %rdi`),
|
||||||
|
// the instruction strictly expects memory addressing syntax.
|
||||||
|
if !matches!(src, Operand::Mem { .. }) {
|
||||||
|
panic!("x86-64 error: Source of a `lea` instruction must be a memory operand.");
|
||||||
|
}
|
||||||
|
|
||||||
|
self.instructions.push(Instruction::Lea(width, src, dest));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emits the 1-operand unsigned multiply (`mul`).
|
||||||
|
/// x86-64 strictly multiplies the operand by %rax and stores the result in %rdx:%rax.
|
||||||
|
pub fn emit_umul(&mut self, width: OperandWidth, src: Operand) {
|
||||||
|
// Validation: mul cannot take an immediate value directly.
|
||||||
|
if matches!(src, Operand::Imm(_)) {
|
||||||
|
panic!(
|
||||||
|
"x86-64 error: Unsigned `mul` cannot take an immediate operand. Load it into a register first."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.instructions.push(Instruction::Mul(width, src));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emits a `push` instruction.
|
||||||
|
/// In 64-bit mode, pushes are almost universally 64-bit operations.
|
||||||
|
pub fn emit_push(&mut self, src: Operand) {
|
||||||
|
// Note: x86-64 allows pushing memory, registers, and 32-bit sign-extended immediates.
|
||||||
|
self.instructions.push(Instruction::Push(src));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emits a `pop` instruction.
|
||||||
|
pub fn emit_pop(&mut self, dest: Operand) {
|
||||||
|
if matches!(dest, Operand::Imm(_)) {
|
||||||
|
panic!("x86-64 error: Cannot pop into an immediate value.");
|
||||||
|
}
|
||||||
|
self.instructions.push(Instruction::Pop(dest));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emits a `setcc` instruction (e.g., `sete`, `setne`).
|
||||||
|
/// Hardware constraint: setcc ONLY writes to 8-bit (Byte) operands.
|
||||||
|
pub fn emit_setcc(&mut self, cc: ConditionCode, dest: Operand) {
|
||||||
|
if matches!(dest, Operand::Imm(_)) {
|
||||||
|
panic!("x86-64 error: Cannot setcc into an immediate value.");
|
||||||
|
}
|
||||||
|
|
||||||
|
self.instructions.push(Instruction::Setcc(cc, dest));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to grab the finalized list of instructions
|
||||||
|
pub fn finish(self) -> Vec<Instruction> {
|
||||||
|
self.instructions
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,774 @@
|
|||||||
|
pub mod codegen;
|
||||||
|
pub mod types;
|
||||||
|
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::fmt::Write;
|
||||||
|
use std::mem::take;
|
||||||
|
|
||||||
|
use crate::backend::x86_64::codegen::Codegen;
|
||||||
|
use crate::backend::x86_64::types::*;
|
||||||
|
use crate::ir;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
enum Storage {
|
||||||
|
Hardware(Gpr),
|
||||||
|
Stack(i32),
|
||||||
|
Alloc(i32),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct X86Backend<'a> {
|
||||||
|
module: &'a ir::Module,
|
||||||
|
assembly: String,
|
||||||
|
live_intervals: HashMap<ir::Register, (usize, usize)>,
|
||||||
|
allocations: HashMap<ir::Register, Storage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> X86Backend<'a> {
|
||||||
|
pub fn new(module: &'a ir::Module) -> Self {
|
||||||
|
Self {
|
||||||
|
assembly: String::new(),
|
||||||
|
module,
|
||||||
|
live_intervals: HashMap::new(),
|
||||||
|
allocations: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_op(&mut self, op: &ir::Operand, scratch_base: Gpr, cg: &mut Codegen) -> Operand {
|
||||||
|
match op {
|
||||||
|
ir::Operand::Integer(v) => Operand::Imm(*v as i64),
|
||||||
|
ir::Operand::Boolean(b) => Operand::Imm(if *b { 1 } else { 0 }),
|
||||||
|
ir::Operand::Register(r) => match self.allocations.get(r).unwrap() {
|
||||||
|
Storage::Hardware(hw_gpr) => Operand::Reg(*hw_gpr),
|
||||||
|
&Storage::Stack(offset) => Operand::Mem {
|
||||||
|
base: Gpr::Rbp,
|
||||||
|
offset,
|
||||||
|
},
|
||||||
|
&Storage::Alloc(offset) => {
|
||||||
|
// Load Effective Address (LEA) to materialize the pointer
|
||||||
|
let mem_loc = Operand::Mem {
|
||||||
|
base: Gpr::Rbp,
|
||||||
|
offset,
|
||||||
|
};
|
||||||
|
cg.emit_lea(OperandWidth::QWord, mem_loc, Operand::Reg(scratch_base));
|
||||||
|
Operand::Reg(scratch_base)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_dest(&self, reg: ir::Register) -> Operand {
|
||||||
|
match self.allocations.get(®).unwrap() {
|
||||||
|
&Storage::Hardware(hw_gpr) => Operand::Reg(hw_gpr),
|
||||||
|
&Storage::Stack(offset) | &Storage::Alloc(offset) => Operand::Mem {
|
||||||
|
base: Gpr::Rbp,
|
||||||
|
offset,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mark_use(&mut self, reg: ir::Register, idx: usize) {
|
||||||
|
if !self.allocations.contains_key(®) {
|
||||||
|
let entry = self.live_intervals.entry(reg).or_insert((idx, idx));
|
||||||
|
entry.1 = idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mark_op(&mut self, op: &ir::Operand, idx: usize) {
|
||||||
|
if let ir::Operand::Register(r) = op {
|
||||||
|
self.mark_use(*r, idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compile_module(mut self) -> String {
|
||||||
|
for func in &self.module.functions {
|
||||||
|
self.compile_function(func);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assembly
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compile_function(&mut self, func: &ir::Function) {
|
||||||
|
// 0. Pre-Pass: Handle Allocations
|
||||||
|
let mut next_stack_offset = 0;
|
||||||
|
|
||||||
|
for block in &func.blocks {
|
||||||
|
for inst in &block.instructions {
|
||||||
|
if let ir::Instruction::Alloc { dest, .. } = inst {
|
||||||
|
next_stack_offset -= 8;
|
||||||
|
self.allocations
|
||||||
|
.insert(*dest, Storage::Alloc(next_stack_offset));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Liveness Analysis & Call Tracking
|
||||||
|
let mut call_indices = Vec::new();
|
||||||
|
let mut hints: HashMap<ir::Register, Gpr> = HashMap::new();
|
||||||
|
let mut inst_idx = 0;
|
||||||
|
|
||||||
|
for (_, reg) in &func.params {
|
||||||
|
self.mark_use(*reg, inst_idx);
|
||||||
|
}
|
||||||
|
inst_idx += 1;
|
||||||
|
|
||||||
|
for block in &func.blocks {
|
||||||
|
for inst in &block.instructions {
|
||||||
|
match inst {
|
||||||
|
ir::Instruction::Alloc { .. } => {}
|
||||||
|
ir::Instruction::Call { dest, args, .. } => {
|
||||||
|
self.mark_use(*dest, inst_idx);
|
||||||
|
let arg_regs = [Gpr::Rdi, Gpr::Rsi, Gpr::Rdx, Gpr::Rcx, Gpr::R8, Gpr::R9];
|
||||||
|
for (i, (_, arg_op)) in args.iter().enumerate() {
|
||||||
|
self.mark_op(arg_op, inst_idx);
|
||||||
|
if i < 6
|
||||||
|
&& let ir::Operand::Register(r) = arg_op
|
||||||
|
{
|
||||||
|
hints.insert(*r, arg_regs[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
call_indices.push(inst_idx);
|
||||||
|
}
|
||||||
|
ir::Instruction::Load { dest, src, .. } => {
|
||||||
|
self.mark_use(*dest, inst_idx);
|
||||||
|
self.mark_op(src, inst_idx);
|
||||||
|
}
|
||||||
|
ir::Instruction::Store { dest, src, .. } => {
|
||||||
|
self.mark_op(dest, inst_idx);
|
||||||
|
self.mark_op(src, inst_idx);
|
||||||
|
}
|
||||||
|
ir::Instruction::Copy {
|
||||||
|
dest: register,
|
||||||
|
src: operand,
|
||||||
|
} => {
|
||||||
|
self.mark_use(*register, inst_idx);
|
||||||
|
self.mark_op(operand, inst_idx);
|
||||||
|
}
|
||||||
|
ir::Instruction::Binary {
|
||||||
|
dest, src1, src2, ..
|
||||||
|
} => {
|
||||||
|
self.mark_use(*dest, inst_idx);
|
||||||
|
self.mark_op(src1, inst_idx);
|
||||||
|
self.mark_op(src2, inst_idx);
|
||||||
|
}
|
||||||
|
ir::Instruction::Unary { dest, src, .. } => {
|
||||||
|
self.mark_use(*dest, inst_idx);
|
||||||
|
self.mark_op(src, inst_idx);
|
||||||
|
}
|
||||||
|
ir::Instruction::Phi { dest, sources, .. } => {
|
||||||
|
self.mark_use(*dest, inst_idx);
|
||||||
|
for (op, _) in sources {
|
||||||
|
self.mark_op(op, inst_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inst_idx += 1;
|
||||||
|
}
|
||||||
|
match &block.terminator {
|
||||||
|
ir::Terminator::Branch { cond, .. } => self.mark_op(cond, inst_idx),
|
||||||
|
ir::Terminator::Return {
|
||||||
|
value: Some(val), ..
|
||||||
|
} => self.mark_op(val, inst_idx),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
inst_idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. ABI-Aware Linear Scan Allocation
|
||||||
|
let mut free_callee_saved = vec![Gpr::Rbx, Gpr::R12, Gpr::R13, Gpr::R14, Gpr::R15];
|
||||||
|
let mut free_caller_saved = vec![Gpr::Rdi, Gpr::Rsi, Gpr::Rdx, Gpr::Rcx, Gpr::R8, Gpr::R9];
|
||||||
|
|
||||||
|
let mut active: Vec<(ir::Register, usize, Gpr, bool)> = Vec::new();
|
||||||
|
let mut used_callee_saved = HashSet::new();
|
||||||
|
|
||||||
|
let live_intervals = take(&mut self.live_intervals);
|
||||||
|
let mut intervals_sorted: Vec<_> = live_intervals.into_iter().collect();
|
||||||
|
intervals_sorted.sort_by_key(|(_, (start, _))| *start);
|
||||||
|
|
||||||
|
for (reg, (start, end)) in intervals_sorted {
|
||||||
|
active.retain(|(_, active_end, hw_reg, is_caller)| {
|
||||||
|
if *active_end < start {
|
||||||
|
if *is_caller {
|
||||||
|
free_caller_saved.push(*hw_reg);
|
||||||
|
} else {
|
||||||
|
free_callee_saved.push(*hw_reg);
|
||||||
|
}
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let crosses_call = call_indices
|
||||||
|
.iter()
|
||||||
|
.any(|&c_idx| c_idx > start && c_idx < end);
|
||||||
|
|
||||||
|
let mut selected_hw = None;
|
||||||
|
let mut is_caller = false;
|
||||||
|
|
||||||
|
if !crosses_call {
|
||||||
|
if let Some(&hint_reg) = hints.get(®)
|
||||||
|
&& let Some(pos) = free_caller_saved.iter().position(|&r| r == hint_reg)
|
||||||
|
{
|
||||||
|
selected_hw = Some(free_caller_saved.remove(pos));
|
||||||
|
is_caller = true;
|
||||||
|
}
|
||||||
|
if selected_hw.is_none()
|
||||||
|
&& let Some(r) = free_caller_saved.pop()
|
||||||
|
{
|
||||||
|
selected_hw = Some(r);
|
||||||
|
is_caller = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if selected_hw.is_none()
|
||||||
|
&& let Some(r) = free_callee_saved.pop()
|
||||||
|
{
|
||||||
|
selected_hw = Some(r);
|
||||||
|
|
||||||
|
used_callee_saved.insert(r);
|
||||||
|
is_caller = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(hw_reg) = selected_hw {
|
||||||
|
self.allocations.insert(reg, Storage::Hardware(hw_reg));
|
||||||
|
active.push((reg, end, hw_reg, is_caller));
|
||||||
|
} else {
|
||||||
|
next_stack_offset += 8;
|
||||||
|
self.allocations
|
||||||
|
.insert(reg, Storage::Stack(next_stack_offset));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let used_callee_saved: Vec<Gpr> = used_callee_saved.into_iter().collect();
|
||||||
|
|
||||||
|
// Determine if a Stack Frame is required
|
||||||
|
let needs_frame = !call_indices.is_empty()
|
||||||
|
|| next_stack_offset < 0
|
||||||
|
|| !used_callee_saved.is_empty()
|
||||||
|
|| func.params.len() > 6;
|
||||||
|
|
||||||
|
// 3. Prologue
|
||||||
|
writeln!(&mut self.assembly, " .text").unwrap();
|
||||||
|
writeln!(&mut self.assembly, " .globl {}", func.name).unwrap();
|
||||||
|
writeln!(&mut self.assembly, " .p2align 4").unwrap();
|
||||||
|
writeln!(&mut self.assembly, " .type {},@function", func.name).unwrap();
|
||||||
|
writeln!(&mut self.assembly, "{}:", func.name).unwrap();
|
||||||
|
|
||||||
|
let mut pro_cg = Codegen::new();
|
||||||
|
|
||||||
|
if needs_frame {
|
||||||
|
pro_cg.emit_push(Operand::Reg(Gpr::Rbp));
|
||||||
|
pro_cg.emit_mov(
|
||||||
|
OperandWidth::QWord,
|
||||||
|
Operand::Reg(Gpr::Rsp),
|
||||||
|
Operand::Reg(Gpr::Rbp),
|
||||||
|
);
|
||||||
|
|
||||||
|
for ® in &used_callee_saved {
|
||||||
|
pro_cg.emit_push(Operand::Reg(reg));
|
||||||
|
}
|
||||||
|
|
||||||
|
let local_vars_size = next_stack_offset.unsigned_abs() as usize;
|
||||||
|
let pushes_size = used_callee_saved.len() * 8;
|
||||||
|
|
||||||
|
// 16-byte alignment calculation
|
||||||
|
let rem = (pushes_size + local_vars_size) % 16;
|
||||||
|
let stack_adj = if rem != 0 {
|
||||||
|
local_vars_size + (16 - rem)
|
||||||
|
} else {
|
||||||
|
local_vars_size
|
||||||
|
};
|
||||||
|
|
||||||
|
if stack_adj > 0 {
|
||||||
|
pro_cg.emit_binary(
|
||||||
|
Instruction::Sub,
|
||||||
|
OperandWidth::QWord,
|
||||||
|
Operand::Imm(stack_adj as i64),
|
||||||
|
Operand::Reg(Gpr::Rsp),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Map ABI Arguments
|
||||||
|
let arg_regs = [Gpr::Rdi, Gpr::Rsi, Gpr::Rdx, Gpr::Rcx, Gpr::R8, Gpr::R9];
|
||||||
|
for (i, (ty, reg)) in func.params.iter().enumerate() {
|
||||||
|
let width = OperandWidth::from_type(ty);
|
||||||
|
let dest_op = self.resolve_dest(*reg);
|
||||||
|
|
||||||
|
if i < 6 {
|
||||||
|
pro_cg.emit_mov(width, Operand::Reg(arg_regs[i]), dest_op);
|
||||||
|
} else {
|
||||||
|
let caller_offset = 16 + ((i - 6) * 8);
|
||||||
|
let caller_mem = Operand::Mem {
|
||||||
|
base: Gpr::Rbp,
|
||||||
|
offset: caller_offset as i32,
|
||||||
|
};
|
||||||
|
|
||||||
|
// x86-64 constraint: Memory-to-memory moves must route through a register
|
||||||
|
if matches!(dest_op, Operand::Mem { .. }) {
|
||||||
|
pro_cg.emit_mov(width, caller_mem, Operand::Reg(Gpr::Rax));
|
||||||
|
pro_cg.emit_mov(width, Operand::Reg(Gpr::Rax), dest_op);
|
||||||
|
} else {
|
||||||
|
pro_cg.emit_mov(width, caller_mem, dest_op);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush prologue and ABI setup instructions to the assembly string
|
||||||
|
for inst in pro_cg.finish() {
|
||||||
|
writeln!(&mut self.assembly, " {}", inst).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Compile Blocks
|
||||||
|
let num_blocks = func.blocks.len();
|
||||||
|
for i in 0..num_blocks {
|
||||||
|
let block = &func.blocks[i];
|
||||||
|
let next_block_id = if i + 1 < num_blocks {
|
||||||
|
Some(func.blocks[i + 1].id)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
writeln!(&mut self.assembly, ".L{}_block_{}:", func.name, block.id.0).unwrap();
|
||||||
|
|
||||||
|
// Peephole Optimization: Fuse an immediately preceding ICmp into the Branch
|
||||||
|
let mut fused_cmp = None;
|
||||||
|
if let ir::Terminator::Branch {
|
||||||
|
cond: ir::Operand::Register(cond_reg),
|
||||||
|
..
|
||||||
|
} = block.terminator
|
||||||
|
&& let Some(ir::Instruction::Binary {
|
||||||
|
dest,
|
||||||
|
op: ir::BinaryOp::ICmp(cmp_op),
|
||||||
|
src1,
|
||||||
|
src2,
|
||||||
|
..
|
||||||
|
}) = block.instructions.last()
|
||||||
|
&& cond_reg == *dest
|
||||||
|
{
|
||||||
|
fused_cmp = Some((*cmp_op, *src1, *src2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we fused the comparison, we omit the last instruction from standard compilation
|
||||||
|
let inst_limit = if fused_cmp.is_some() {
|
||||||
|
block.instructions.len() - 1
|
||||||
|
} else {
|
||||||
|
block.instructions.len()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut block_instructions = Vec::new();
|
||||||
|
|
||||||
|
for inst in &block.instructions[..inst_limit] {
|
||||||
|
self.compile_instruction(inst, &mut block_instructions);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.compile_terminator(
|
||||||
|
&block.terminator,
|
||||||
|
&func.name,
|
||||||
|
next_block_id,
|
||||||
|
fused_cmp,
|
||||||
|
&mut block_instructions,
|
||||||
|
);
|
||||||
|
|
||||||
|
for instr in block_instructions {
|
||||||
|
writeln!(&mut self.assembly, " {}", instr).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Unified Epilogue
|
||||||
|
writeln!(&mut self.assembly, ".L{}_epilogue:", func.name).unwrap();
|
||||||
|
|
||||||
|
let mut epi_cg = Codegen::new();
|
||||||
|
|
||||||
|
if needs_frame {
|
||||||
|
let pushes_size = used_callee_saved.len() * 8;
|
||||||
|
if pushes_size > 0 {
|
||||||
|
let sp_restore = Operand::Mem {
|
||||||
|
base: Gpr::Rbp,
|
||||||
|
offset: -(pushes_size as i32),
|
||||||
|
};
|
||||||
|
epi_cg.emit_lea(OperandWidth::QWord, sp_restore, Operand::Reg(Gpr::Rsp));
|
||||||
|
|
||||||
|
for ® in used_callee_saved.iter().rev() {
|
||||||
|
epi_cg.emit_pop(Operand::Reg(reg));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
epi_cg.emit_mov(
|
||||||
|
OperandWidth::QWord,
|
||||||
|
Operand::Reg(Gpr::Rbp),
|
||||||
|
Operand::Reg(Gpr::Rsp),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
epi_cg.emit_pop(Operand::Reg(Gpr::Rbp));
|
||||||
|
}
|
||||||
|
|
||||||
|
epi_cg.instructions.push(Instruction::Ret);
|
||||||
|
|
||||||
|
// Flush epilogue instructions to the assembly string
|
||||||
|
for inst in epi_cg.finish() {
|
||||||
|
writeln!(&mut self.assembly, " {}", inst).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
writeln!(&mut self.assembly).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compile_instruction(
|
||||||
|
&mut self,
|
||||||
|
inst: &ir::Instruction,
|
||||||
|
block_instructions: &mut Vec<Instruction>,
|
||||||
|
) {
|
||||||
|
let mut cg = Codegen::new();
|
||||||
|
|
||||||
|
match inst {
|
||||||
|
ir::Instruction::Alloc { .. } => {} // Handled in prologue
|
||||||
|
|
||||||
|
ir::Instruction::Copy { dest, src } => {
|
||||||
|
// Assuming everything is 64-bit for a raw copy unless we track sizes perfectly
|
||||||
|
let width = OperandWidth::QWord;
|
||||||
|
let dest_op = self.resolve_dest(*dest);
|
||||||
|
let src_op = self.resolve_op(src, Gpr::Rax, &mut cg);
|
||||||
|
|
||||||
|
if matches!(dest_op, Operand::Mem { .. }) && matches!(src_op, Operand::Mem { .. }) {
|
||||||
|
cg.emit_mov(width, src_op, Operand::Reg(Gpr::Rax));
|
||||||
|
cg.emit_mov(width, Operand::Reg(Gpr::Rax), dest_op);
|
||||||
|
} else {
|
||||||
|
cg.emit_mov(width, src_op, dest_op);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ir::Instruction::Load { ty, dest, src } => {
|
||||||
|
let width = OperandWidth::from_type(ty);
|
||||||
|
let dest_op = self.resolve_dest(*dest);
|
||||||
|
|
||||||
|
// If loading directly from an Alloc, we can bypass pointer materialization
|
||||||
|
if let ir::Operand::Register(r) = src
|
||||||
|
&& let Some(&Storage::Alloc(offset)) = self.allocations.get(r)
|
||||||
|
{
|
||||||
|
let mem = Operand::Mem {
|
||||||
|
base: Gpr::Rbp,
|
||||||
|
offset,
|
||||||
|
};
|
||||||
|
cg.emit_mov(width, mem, dest_op);
|
||||||
|
block_instructions.extend(cg.finish());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard pointer dereference
|
||||||
|
let ptr_reg = self.resolve_op(src, Gpr::Rax, &mut cg);
|
||||||
|
|
||||||
|
// Route ptr into R10 if it's currently sitting in memory
|
||||||
|
let final_ptr = if matches!(ptr_reg, Operand::Mem { .. }) {
|
||||||
|
cg.emit_mov(OperandWidth::QWord, ptr_reg, Operand::Reg(Gpr::R10));
|
||||||
|
Gpr::R10
|
||||||
|
} else if let Operand::Reg(g) = ptr_reg {
|
||||||
|
g
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
|
||||||
|
let deref = Operand::Mem {
|
||||||
|
base: final_ptr,
|
||||||
|
offset: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if matches!(dest_op, Operand::Mem { .. }) {
|
||||||
|
cg.emit_mov(width, deref, Operand::Reg(Gpr::R11));
|
||||||
|
cg.emit_mov(width, Operand::Reg(Gpr::R11), dest_op);
|
||||||
|
} else {
|
||||||
|
cg.emit_mov(width, deref, dest_op);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ir::Instruction::Store { ty, dest, src } => {
|
||||||
|
let width = OperandWidth::from_type(ty);
|
||||||
|
let src_op = self.resolve_op(src, Gpr::Rax, &mut cg);
|
||||||
|
|
||||||
|
// If storing directly to an Alloc, bypass pointer materialization
|
||||||
|
if let ir::Operand::Register(r) = dest
|
||||||
|
&& let Some(&Storage::Alloc(offset)) = self.allocations.get(r)
|
||||||
|
{
|
||||||
|
let mem = Operand::Mem {
|
||||||
|
base: Gpr::Rbp,
|
||||||
|
offset,
|
||||||
|
};
|
||||||
|
if matches!(src_op, Operand::Mem { .. }) || matches!(src_op, Operand::Imm(_)) {
|
||||||
|
cg.emit_mov(width, src_op, Operand::Reg(Gpr::Rax));
|
||||||
|
cg.emit_mov(width, Operand::Reg(Gpr::Rax), mem);
|
||||||
|
} else {
|
||||||
|
cg.emit_mov(width, src_op, mem);
|
||||||
|
}
|
||||||
|
block_instructions.extend(cg.finish());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ptr_reg = self.resolve_op(dest, Gpr::R10, &mut cg);
|
||||||
|
let final_ptr = if matches!(ptr_reg, Operand::Mem { .. }) {
|
||||||
|
cg.emit_mov(OperandWidth::QWord, ptr_reg, Operand::Reg(Gpr::R10));
|
||||||
|
Gpr::R10
|
||||||
|
} else if let Operand::Reg(g) = ptr_reg {
|
||||||
|
g
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
|
||||||
|
let deref = Operand::Mem {
|
||||||
|
base: final_ptr,
|
||||||
|
offset: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if matches!(src_op, Operand::Mem { .. }) || matches!(src_op, Operand::Imm(_)) {
|
||||||
|
cg.emit_mov(width, src_op, Operand::Reg(Gpr::Rax));
|
||||||
|
cg.emit_mov(width, Operand::Reg(Gpr::Rax), deref);
|
||||||
|
} else {
|
||||||
|
cg.emit_mov(width, src_op, deref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ir::Instruction::Binary {
|
||||||
|
dest,
|
||||||
|
result_ty,
|
||||||
|
op,
|
||||||
|
src1,
|
||||||
|
src2,
|
||||||
|
} => {
|
||||||
|
let width = OperandWidth::from_type(result_ty);
|
||||||
|
let dest_op = self.resolve_dest(*dest);
|
||||||
|
let src1_op = self.resolve_op(src1, Gpr::R10, &mut cg);
|
||||||
|
let src2_op = self.resolve_op(src2, Gpr::R11, &mut cg);
|
||||||
|
|
||||||
|
match op {
|
||||||
|
ir::BinaryOp::Add | ir::BinaryOp::Sub | ir::BinaryOp::SMul => {
|
||||||
|
let x86_op = match op {
|
||||||
|
ir::BinaryOp::Add => Instruction::Add,
|
||||||
|
ir::BinaryOp::Sub => Instruction::Sub,
|
||||||
|
ir::BinaryOp::SMul => Instruction::IMul,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ensure dest = src1 before applying destructive x86 math
|
||||||
|
if src1_op != dest_op {
|
||||||
|
if matches!(src1_op, Operand::Mem { .. })
|
||||||
|
&& matches!(dest_op, Operand::Mem { .. })
|
||||||
|
{
|
||||||
|
cg.emit_mov(width, src1_op, Operand::Reg(Gpr::Rax));
|
||||||
|
cg.emit_mov(width, Operand::Reg(Gpr::Rax), dest_op);
|
||||||
|
} else {
|
||||||
|
cg.emit_mov(width, src1_op, dest_op);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(src2_op, Operand::Mem { .. })
|
||||||
|
&& matches!(dest_op, Operand::Mem { .. })
|
||||||
|
{
|
||||||
|
cg.emit_mov(width, src2_op, Operand::Reg(Gpr::Rax));
|
||||||
|
cg.emit_binary(x86_op, width, Operand::Reg(Gpr::Rax), dest_op);
|
||||||
|
} else {
|
||||||
|
cg.emit_binary(x86_op, width, src2_op, dest_op);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ir::BinaryOp::UMul => {
|
||||||
|
cg.emit_mov(width, src1_op, Operand::Reg(Gpr::Rax));
|
||||||
|
if matches!(src2_op, Operand::Imm(_)) {
|
||||||
|
cg.emit_mov(width, src2_op, Operand::Reg(Gpr::R10));
|
||||||
|
cg.emit_umul(width, Operand::Reg(Gpr::R10));
|
||||||
|
} else {
|
||||||
|
cg.emit_umul(width, src2_op);
|
||||||
|
}
|
||||||
|
cg.emit_mov(width, Operand::Reg(Gpr::Rax), dest_op);
|
||||||
|
}
|
||||||
|
// Implement Div/Rem/Cmp similarly using cg.emit_* methods...
|
||||||
|
_ => unimplemented!("Implement other binary ops"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ir::Instruction::Unary {
|
||||||
|
dest,
|
||||||
|
result_ty,
|
||||||
|
op,
|
||||||
|
src,
|
||||||
|
} => {
|
||||||
|
let width = OperandWidth::from_type(result_ty);
|
||||||
|
let dest_op = self.resolve_dest(*dest);
|
||||||
|
let src_op = self.resolve_op(src, Gpr::Rax, &mut cg);
|
||||||
|
|
||||||
|
cg.emit_mov(width, src_op, dest_op);
|
||||||
|
match op {
|
||||||
|
ir::UnaryOp::INeg => cg.instructions.push(Instruction::Neg(width, dest_op)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ir::Instruction::Call {
|
||||||
|
dest,
|
||||||
|
result_ty,
|
||||||
|
func,
|
||||||
|
args,
|
||||||
|
} => {
|
||||||
|
let width = OperandWidth::from_type(result_ty);
|
||||||
|
let arg_regs = [Gpr::Rdi, Gpr::Rsi, Gpr::Rdx, Gpr::Rcx, Gpr::R8, Gpr::R9];
|
||||||
|
|
||||||
|
for (i, (ty, arg_op)) in args.iter().enumerate() {
|
||||||
|
let arg_width = OperandWidth::from_type(ty);
|
||||||
|
let src = self.resolve_op(arg_op, Gpr::R10, &mut cg);
|
||||||
|
if i < 6 {
|
||||||
|
cg.emit_mov(arg_width, src, Operand::Reg(arg_regs[i]));
|
||||||
|
} else {
|
||||||
|
if matches!(src, Operand::Mem { .. }) {
|
||||||
|
cg.emit_mov(OperandWidth::QWord, src, Operand::Reg(Gpr::Rax));
|
||||||
|
cg.emit_push(Operand::Reg(Gpr::Rax));
|
||||||
|
} else {
|
||||||
|
cg.emit_push(src);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let func_name = self
|
||||||
|
.module
|
||||||
|
.functions
|
||||||
|
.iter()
|
||||||
|
.find_map(|f| (f.id == *func).then(|| f.name.clone()))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cg.instructions.push(Instruction::Call(func_name));
|
||||||
|
|
||||||
|
if args.len() > 6 {
|
||||||
|
let cleanup = ((args.len() - 6) * 8) as i64;
|
||||||
|
cg.emit_binary(
|
||||||
|
Instruction::Add,
|
||||||
|
OperandWidth::QWord,
|
||||||
|
Operand::Imm(cleanup),
|
||||||
|
Operand::Reg(Gpr::Rsp),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if *result_ty != ir::Type::Void {
|
||||||
|
let dest_op = self.resolve_dest(*dest);
|
||||||
|
cg.emit_mov(width, Operand::Reg(Gpr::Rax), dest_op);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ir::Instruction::Phi { .. } => unreachable!("Run SSA Destruction pass before codegen!"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit generated instructions to the block
|
||||||
|
block_instructions.extend(cg.finish());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compile_terminator(
|
||||||
|
&mut self,
|
||||||
|
term: &ir::Terminator,
|
||||||
|
func_name: &str,
|
||||||
|
next_block_id: Option<ir::BlockId>,
|
||||||
|
fused_cmp: Option<(ir::ICmpOp, ir::Operand, ir::Operand)>,
|
||||||
|
block_instructions: &mut Vec<Instruction>,
|
||||||
|
) {
|
||||||
|
let mut cg = Codegen::new();
|
||||||
|
|
||||||
|
match term {
|
||||||
|
ir::Terminator::Return { return_ty, value } => {
|
||||||
|
if let Some(val) = value {
|
||||||
|
let width = OperandWidth::from_type(return_ty);
|
||||||
|
let src = self.resolve_op(val, Gpr::R10, &mut cg);
|
||||||
|
cg.emit_mov(width, src, Operand::Reg(Gpr::Rax));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is no next block, we can fallthrough. Otherwise, emit a jump.
|
||||||
|
if next_block_id.is_some() {
|
||||||
|
cg.instructions
|
||||||
|
.push(Instruction::Jmp(format!(".L{}_epilogue", func_name)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ir::Terminator::Jump(target) => {
|
||||||
|
if Some(*target) != next_block_id {
|
||||||
|
cg.instructions.push(Instruction::Jmp(format!(
|
||||||
|
".L{}_block_{}",
|
||||||
|
func_name, target.0
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ir::Terminator::Branch {
|
||||||
|
cond,
|
||||||
|
then_block,
|
||||||
|
else_block,
|
||||||
|
} => {
|
||||||
|
let (cc_true, cc_false) = if let Some((cmp_op, src1, src2)) = fused_cmp {
|
||||||
|
let src1_op = self.resolve_op(&src1, Gpr::R10, &mut cg);
|
||||||
|
let src2_op = self.resolve_op(&src2, Gpr::R11, &mut cg);
|
||||||
|
|
||||||
|
// Note: IR Binary ops only track the result type (Bool for ICmp).
|
||||||
|
// To be perfectly accurate in the future, we may want to track operand types.
|
||||||
|
// For now, we assume pointers/64-bit integers (QWord) for the comparison.
|
||||||
|
let width = OperandWidth::QWord;
|
||||||
|
|
||||||
|
// x86-64 constraints: destination (src1 in AT&T) cannot be an immediate.
|
||||||
|
// Memory-to-memory comparisons are also invalid.
|
||||||
|
if matches!(src1_op, Operand::Mem { .. })
|
||||||
|
&& matches!(src2_op, Operand::Mem { .. })
|
||||||
|
|| matches!(src1_op, Operand::Imm(_))
|
||||||
|
{
|
||||||
|
cg.emit_mov(width, src1_op, Operand::Reg(Gpr::Rax));
|
||||||
|
cg.instructions.push(Instruction::Cmp(
|
||||||
|
width,
|
||||||
|
src2_op,
|
||||||
|
Operand::Reg(Gpr::Rax),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
cg.instructions
|
||||||
|
.push(Instruction::Cmp(width, src2_op, src1_op));
|
||||||
|
}
|
||||||
|
|
||||||
|
match cmp_op {
|
||||||
|
ir::ICmpOp::Eq => (ConditionCode::E, ConditionCode::Ne),
|
||||||
|
ir::ICmpOp::Ne => (ConditionCode::Ne, ConditionCode::E),
|
||||||
|
ir::ICmpOp::Slt => (ConditionCode::L, ConditionCode::Ge),
|
||||||
|
ir::ICmpOp::Sle => (ConditionCode::Le, ConditionCode::G),
|
||||||
|
ir::ICmpOp::Sgt => (ConditionCode::G, ConditionCode::Le),
|
||||||
|
ir::ICmpOp::Sge => (ConditionCode::Ge, ConditionCode::L),
|
||||||
|
ir::ICmpOp::Ult => (ConditionCode::B, ConditionCode::Ae),
|
||||||
|
ir::ICmpOp::Ule => (ConditionCode::Be, ConditionCode::A),
|
||||||
|
ir::ICmpOp::Ugt => (ConditionCode::A, ConditionCode::Be),
|
||||||
|
ir::ICmpOp::Uge => (ConditionCode::Ae, ConditionCode::B),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: evaluate an isolated boolean
|
||||||
|
let cond_op = self.resolve_op(cond, Gpr::R10, &mut cg);
|
||||||
|
let width = OperandWidth::Byte; // Booleans are 8-bit
|
||||||
|
|
||||||
|
if matches!(cond_op, Operand::Imm(_)) || matches!(cond_op, Operand::Mem { .. })
|
||||||
|
{
|
||||||
|
cg.emit_mov(width, cond_op, Operand::Reg(Gpr::Rax));
|
||||||
|
cg.instructions.push(Instruction::Test(
|
||||||
|
width,
|
||||||
|
Operand::Reg(Gpr::Rax),
|
||||||
|
Operand::Reg(Gpr::Rax),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
cg.instructions
|
||||||
|
.push(Instruction::Test(width, cond_op, cond_op));
|
||||||
|
}
|
||||||
|
|
||||||
|
(ConditionCode::Nz, ConditionCode::Z)
|
||||||
|
};
|
||||||
|
|
||||||
|
let then_label = format!(".L{}_block_{}", func_name, then_block.0);
|
||||||
|
let else_label = format!(".L{}_block_{}", func_name, else_block.0);
|
||||||
|
|
||||||
|
// Fallthrough logic cleanly applied to dynamically resolved jump conditions
|
||||||
|
if Some(*else_block) == next_block_id {
|
||||||
|
cg.instructions.push(Instruction::Jcc(cc_true, then_label));
|
||||||
|
} else if Some(*then_block) == next_block_id {
|
||||||
|
cg.instructions.push(Instruction::Jcc(cc_false, else_label));
|
||||||
|
} else {
|
||||||
|
cg.instructions.push(Instruction::Jcc(cc_false, else_label));
|
||||||
|
cg.instructions.push(Instruction::Jmp(then_label));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ir::Terminator::Unknown => panic!("Cannot compile Unknown terminator"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit generated instructions to the block
|
||||||
|
block_instructions.extend(cg.finish());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,360 @@
|
|||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use crate::ir::Type;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum OperandWidth {
|
||||||
|
Byte, // 8-bit (b suffix)
|
||||||
|
Word, // 16-bit (w suffix)
|
||||||
|
DWord, // 32-bit (l suffix)
|
||||||
|
QWord, // 64-bit (q suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OperandWidth {
|
||||||
|
/// Returns the AT&T assembly suffix for this width.
|
||||||
|
pub fn suffix(self) -> char {
|
||||||
|
match self {
|
||||||
|
OperandWidth::Byte => 'b',
|
||||||
|
OperandWidth::Word => 'w',
|
||||||
|
OperandWidth::DWord => 'l',
|
||||||
|
OperandWidth::QWord => 'q',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the operand with for a given [Type].
|
||||||
|
pub fn from_type(ty: &Type) -> Self {
|
||||||
|
match ty {
|
||||||
|
Type::Bool | Type::I8 => OperandWidth::Byte,
|
||||||
|
Type::I16 => OperandWidth::Word,
|
||||||
|
Type::I32 => OperandWidth::DWord,
|
||||||
|
Type::I64 | Type::Ptr => OperandWidth::QWord,
|
||||||
|
Type::Void => panic!("x86-64 error: Cannot compute width of Void type"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub enum Gpr {
|
||||||
|
Rax,
|
||||||
|
Rcx,
|
||||||
|
Rdx,
|
||||||
|
Rbx,
|
||||||
|
Rsp,
|
||||||
|
Rbp,
|
||||||
|
Rsi,
|
||||||
|
Rdi,
|
||||||
|
R8,
|
||||||
|
R9,
|
||||||
|
R10,
|
||||||
|
R11,
|
||||||
|
R12,
|
||||||
|
R13,
|
||||||
|
R14,
|
||||||
|
R15,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Gpr {
|
||||||
|
/// Formats the register according to the requested width.
|
||||||
|
pub fn format_with_width(self, width: OperandWidth) -> &'static str {
|
||||||
|
match (self, width) {
|
||||||
|
// 64-bit (QWord)
|
||||||
|
(Gpr::Rax, OperandWidth::QWord) => "%rax",
|
||||||
|
(Gpr::Rcx, OperandWidth::QWord) => "%rcx",
|
||||||
|
(Gpr::Rdx, OperandWidth::QWord) => "%rdx",
|
||||||
|
(Gpr::Rbx, OperandWidth::QWord) => "%rbx",
|
||||||
|
(Gpr::Rsp, OperandWidth::QWord) => "%rsp",
|
||||||
|
(Gpr::Rbp, OperandWidth::QWord) => "%rbp",
|
||||||
|
(Gpr::Rsi, OperandWidth::QWord) => "%rsi",
|
||||||
|
(Gpr::Rdi, OperandWidth::QWord) => "%rdi",
|
||||||
|
(Gpr::R8, OperandWidth::QWord) => "%r8",
|
||||||
|
(Gpr::R9, OperandWidth::QWord) => "%r9",
|
||||||
|
(Gpr::R10, OperandWidth::QWord) => "%r10",
|
||||||
|
(Gpr::R11, OperandWidth::QWord) => "%r11",
|
||||||
|
(Gpr::R12, OperandWidth::QWord) => "%r12",
|
||||||
|
(Gpr::R13, OperandWidth::QWord) => "%r13",
|
||||||
|
(Gpr::R14, OperandWidth::QWord) => "%r14",
|
||||||
|
(Gpr::R15, OperandWidth::QWord) => "%r15",
|
||||||
|
|
||||||
|
// 32-bit (DWord)
|
||||||
|
(Gpr::Rax, OperandWidth::DWord) => "%eax",
|
||||||
|
(Gpr::Rcx, OperandWidth::DWord) => "%ecx",
|
||||||
|
(Gpr::Rdx, OperandWidth::DWord) => "%edx",
|
||||||
|
(Gpr::Rbx, OperandWidth::DWord) => "%ebx",
|
||||||
|
(Gpr::Rsp, OperandWidth::DWord) => "%esp",
|
||||||
|
(Gpr::Rbp, OperandWidth::DWord) => "%ebp",
|
||||||
|
(Gpr::Rsi, OperandWidth::DWord) => "%esi",
|
||||||
|
(Gpr::Rdi, OperandWidth::DWord) => "%edi",
|
||||||
|
(Gpr::R8, OperandWidth::DWord) => "%r8d",
|
||||||
|
(Gpr::R9, OperandWidth::DWord) => "%r9d",
|
||||||
|
(Gpr::R10, OperandWidth::DWord) => "%r10d",
|
||||||
|
(Gpr::R11, OperandWidth::DWord) => "%r11d",
|
||||||
|
(Gpr::R12, OperandWidth::DWord) => "%r12d",
|
||||||
|
(Gpr::R13, OperandWidth::DWord) => "%r13d",
|
||||||
|
(Gpr::R14, OperandWidth::DWord) => "%r14d",
|
||||||
|
(Gpr::R15, OperandWidth::DWord) => "%r15d",
|
||||||
|
|
||||||
|
// 16-bit (Word)
|
||||||
|
(Gpr::Rax, OperandWidth::Word) => "%ax",
|
||||||
|
(Gpr::Rcx, OperandWidth::Word) => "%cx",
|
||||||
|
(Gpr::Rdx, OperandWidth::Word) => "%dx",
|
||||||
|
(Gpr::Rbx, OperandWidth::Word) => "%bx",
|
||||||
|
(Gpr::Rsp, OperandWidth::Word) => "%sp",
|
||||||
|
(Gpr::Rbp, OperandWidth::Word) => "%bp",
|
||||||
|
(Gpr::Rsi, OperandWidth::Word) => "%si",
|
||||||
|
(Gpr::Rdi, OperandWidth::Word) => "%di",
|
||||||
|
(Gpr::R8, OperandWidth::Word) => "%r8w",
|
||||||
|
(Gpr::R9, OperandWidth::Word) => "%r9w",
|
||||||
|
(Gpr::R10, OperandWidth::Word) => "%r10w",
|
||||||
|
(Gpr::R11, OperandWidth::Word) => "%r11w",
|
||||||
|
(Gpr::R12, OperandWidth::Word) => "%r12w",
|
||||||
|
(Gpr::R13, OperandWidth::Word) => "%r13w",
|
||||||
|
(Gpr::R14, OperandWidth::Word) => "%r14w",
|
||||||
|
(Gpr::R15, OperandWidth::Word) => "%r15w",
|
||||||
|
|
||||||
|
// 8-bit (Byte)
|
||||||
|
(Gpr::Rax, OperandWidth::Byte) => "%al",
|
||||||
|
(Gpr::Rcx, OperandWidth::Byte) => "%cl",
|
||||||
|
(Gpr::Rdx, OperandWidth::Byte) => "%dl",
|
||||||
|
(Gpr::Rbx, OperandWidth::Byte) => "%bl",
|
||||||
|
(Gpr::Rsp, OperandWidth::Byte) => "%spl",
|
||||||
|
(Gpr::Rbp, OperandWidth::Byte) => "%bpl",
|
||||||
|
(Gpr::Rsi, OperandWidth::Byte) => "%sil",
|
||||||
|
(Gpr::Rdi, OperandWidth::Byte) => "%dil",
|
||||||
|
(Gpr::R8, OperandWidth::Byte) => "%r8b",
|
||||||
|
(Gpr::R9, OperandWidth::Byte) => "%r9b",
|
||||||
|
(Gpr::R10, OperandWidth::Byte) => "%r10b",
|
||||||
|
(Gpr::R11, OperandWidth::Byte) => "%r11b",
|
||||||
|
(Gpr::R12, OperandWidth::Byte) => "%r12b",
|
||||||
|
(Gpr::R13, OperandWidth::Byte) => "%r13b",
|
||||||
|
(Gpr::R14, OperandWidth::Byte) => "%r14b",
|
||||||
|
(Gpr::R15, OperandWidth::Byte) => "%r15b",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum ConditionCode {
|
||||||
|
E,
|
||||||
|
Ne,
|
||||||
|
L,
|
||||||
|
Le,
|
||||||
|
G,
|
||||||
|
Ge,
|
||||||
|
B,
|
||||||
|
Be,
|
||||||
|
A,
|
||||||
|
Ae,
|
||||||
|
Z,
|
||||||
|
Nz,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ConditionCode {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let s = match self {
|
||||||
|
ConditionCode::E => "e",
|
||||||
|
ConditionCode::Ne => "ne",
|
||||||
|
ConditionCode::L => "l",
|
||||||
|
ConditionCode::Le => "le",
|
||||||
|
ConditionCode::G => "g",
|
||||||
|
ConditionCode::Ge => "ge",
|
||||||
|
ConditionCode::B => "b",
|
||||||
|
ConditionCode::Be => "be",
|
||||||
|
ConditionCode::A => "a",
|
||||||
|
ConditionCode::Ae => "ae",
|
||||||
|
ConditionCode::Z => "z",
|
||||||
|
ConditionCode::Nz => "nz",
|
||||||
|
};
|
||||||
|
write!(f, "{}", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum Operand {
|
||||||
|
/// An immediate constant, e.g. `$5`
|
||||||
|
Imm(i64),
|
||||||
|
/// A hardware register
|
||||||
|
Reg(Gpr),
|
||||||
|
/// A memory operand: `offset(base_reg)`
|
||||||
|
Mem { base: Gpr, offset: i32 },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Operand {
|
||||||
|
/// Returns whether this operand is stored in memory.
|
||||||
|
pub fn is_memory(&self) -> bool {
|
||||||
|
matches!(self, Operand::Mem { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats the operand into its AT&T assembly representation.
|
||||||
|
pub fn format_with_width(&self, width: OperandWidth) -> String {
|
||||||
|
match self {
|
||||||
|
Operand::Imm(val) => format!("${}", val),
|
||||||
|
Operand::Reg(gpr) => gpr.format_with_width(width).to_string(),
|
||||||
|
// Memory addresses inherently use 64-bit pointers (QWord) for the base register,
|
||||||
|
// even if the value being read/written is smaller.
|
||||||
|
Operand::Mem { base, offset } => {
|
||||||
|
let base_str = base.format_with_width(OperandWidth::QWord);
|
||||||
|
if *offset == 0 {
|
||||||
|
format!("({})", base_str)
|
||||||
|
} else {
|
||||||
|
format!("{}({})", offset, base_str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum Instruction {
|
||||||
|
// Data movement
|
||||||
|
Mov(OperandWidth, Operand, Operand), // width, src, dest
|
||||||
|
Movzx(OperandWidth, OperandWidth, Operand, Operand), // src_width, dest_width, src, dest
|
||||||
|
Lea(OperandWidth, Operand, Operand), // width, src (mem), dest (reg)
|
||||||
|
|
||||||
|
// Stack manipulation (typically 64-bit in x86-64)
|
||||||
|
Push(Operand),
|
||||||
|
Pop(Operand),
|
||||||
|
|
||||||
|
// Arithmetic
|
||||||
|
Add(OperandWidth, Operand, Operand),
|
||||||
|
Sub(OperandWidth, Operand, Operand),
|
||||||
|
Neg(OperandWidth, Operand),
|
||||||
|
IMul(OperandWidth, Operand, Operand), // 2-operand signed multiply
|
||||||
|
Mul(OperandWidth, Operand), // 1-operand unsigned multiply (implicitly uses RAX/RDX)
|
||||||
|
IDiv(OperandWidth, Operand),
|
||||||
|
Div(OperandWidth, Operand),
|
||||||
|
Cqto, // Sign-extend RAX into RDX:RAX
|
||||||
|
|
||||||
|
// Logic & Comparison
|
||||||
|
Cmp(OperandWidth, Operand, Operand), // width, src, dest
|
||||||
|
Test(OperandWidth, Operand, Operand),
|
||||||
|
Setcc(ConditionCode, Operand), // e.g., sete %al
|
||||||
|
|
||||||
|
// Control Flow
|
||||||
|
Jmp(String), // Target label
|
||||||
|
Jcc(ConditionCode, String), // e.g., jne .L_block_2
|
||||||
|
Call(String),
|
||||||
|
Ret,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Instruction {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Instruction::Mov(w, src, dest) => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"mov{} {}, {}",
|
||||||
|
w.suffix(),
|
||||||
|
src.format_with_width(*w),
|
||||||
|
dest.format_with_width(*w)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Instruction::Movzx(src_w, dest_w, src, dest) => {
|
||||||
|
// movz requires two suffixes, e.g., movzbl (byte to long)
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"movz{}{} {}, {}",
|
||||||
|
src_w.suffix(),
|
||||||
|
dest_w.suffix(),
|
||||||
|
src.format_with_width(*src_w),
|
||||||
|
dest.format_with_width(*dest_w)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Instruction::Lea(w, src, dest) => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"lea{} {}, {}",
|
||||||
|
w.suffix(),
|
||||||
|
src.format_with_width(*w),
|
||||||
|
dest.format_with_width(*w)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Instruction::Push(op) => {
|
||||||
|
write!(f, "pushq {}", op.format_with_width(OperandWidth::QWord))
|
||||||
|
}
|
||||||
|
Instruction::Pop(op) => {
|
||||||
|
write!(f, "popq {}", op.format_with_width(OperandWidth::QWord))
|
||||||
|
}
|
||||||
|
Instruction::Add(w, src, dest) => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"add{} {}, {}",
|
||||||
|
w.suffix(),
|
||||||
|
src.format_with_width(*w),
|
||||||
|
dest.format_with_width(*w)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Instruction::Sub(w, src, dest) => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"sub{} {}, {}",
|
||||||
|
w.suffix(),
|
||||||
|
src.format_with_width(*w),
|
||||||
|
dest.format_with_width(*w)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Instruction::Neg(w, dest) => {
|
||||||
|
write!(f, "neg{} {}", w.suffix(), dest.format_with_width(*w))
|
||||||
|
}
|
||||||
|
Instruction::IMul(w, src, dest) => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"imul{} {}, {}",
|
||||||
|
w.suffix(),
|
||||||
|
src.format_with_width(*w),
|
||||||
|
dest.format_with_width(*w)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Instruction::Mul(w, src) => {
|
||||||
|
write!(f, "mul{} {}", w.suffix(), src.format_with_width(*w))
|
||||||
|
}
|
||||||
|
Instruction::IDiv(w, src) => {
|
||||||
|
write!(f, "idiv{} {}", w.suffix(), src.format_with_width(*w))
|
||||||
|
}
|
||||||
|
Instruction::Div(w, src) => {
|
||||||
|
write!(f, "div{} {}", w.suffix(), src.format_with_width(*w))
|
||||||
|
}
|
||||||
|
Instruction::Cqto => {
|
||||||
|
write!(f, "cqto")
|
||||||
|
}
|
||||||
|
Instruction::Cmp(w, src, dest) => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"cmp{} {}, {}",
|
||||||
|
w.suffix(),
|
||||||
|
src.format_with_width(*w),
|
||||||
|
dest.format_with_width(*w)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Instruction::Test(w, src, dest) => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"test{} {}, {}",
|
||||||
|
w.suffix(),
|
||||||
|
src.format_with_width(*w),
|
||||||
|
dest.format_with_width(*w)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Instruction::Setcc(cc, dest) => {
|
||||||
|
// setcc strictly operates on 8-bit (byte) registers
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"set{} {}",
|
||||||
|
cc,
|
||||||
|
dest.format_with_width(OperandWidth::Byte)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Instruction::Jmp(label) => {
|
||||||
|
write!(f, "jmp {}", label)
|
||||||
|
}
|
||||||
|
Instruction::Jcc(cc, label) => {
|
||||||
|
write!(f, "j{} {}", cc, label)
|
||||||
|
}
|
||||||
|
Instruction::Call(label) => {
|
||||||
|
write!(f, "call {}", label)
|
||||||
|
}
|
||||||
|
Instruction::Ret => {
|
||||||
|
write!(f, "ret")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -92,9 +92,9 @@ pub enum Instruction {
|
|||||||
args: Vec<(Type, Operand)>,
|
args: Vec<(Type, Operand)>,
|
||||||
},
|
},
|
||||||
|
|
||||||
Assign {
|
Copy {
|
||||||
register: Register,
|
dest: Register,
|
||||||
operand: Operand,
|
src: Operand,
|
||||||
},
|
},
|
||||||
|
|
||||||
Phi {
|
Phi {
|
||||||
|
|||||||
+23
-27
@@ -11,12 +11,8 @@ fn build_test_module() -> Module {
|
|||||||
// factorial(n: i32) -> i32
|
// factorial(n: i32) -> i32
|
||||||
let fact_id = module_builder.new_function_id();
|
let fact_id = module_builder.new_function_id();
|
||||||
{
|
{
|
||||||
let f_builder = module_builder.new_function(
|
let f_builder =
|
||||||
fact_id,
|
module_builder.new_function(fact_id, "factorial_iterative", vec![&i32_ty], i32_ty);
|
||||||
"factorial_iterative",
|
|
||||||
vec![&i32_ty],
|
|
||||||
i32_ty.clone(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 1. Block Definitions
|
// 1. Block Definitions
|
||||||
let loop_cond_block = f_builder.create_block();
|
let loop_cond_block = f_builder.create_block();
|
||||||
@@ -24,22 +20,22 @@ fn build_test_module() -> Module {
|
|||||||
let exit_block = f_builder.create_block();
|
let exit_block = f_builder.create_block();
|
||||||
|
|
||||||
// Allocate space for 'res' (accumulator) and 'i' (counter)
|
// Allocate space for 'res' (accumulator) and 'i' (counter)
|
||||||
let res_ptr = f_builder.build_alloc(i32_ty.clone());
|
let res_ptr = f_builder.build_alloc(i32_ty);
|
||||||
let i_ptr = f_builder.build_alloc(i32_ty.clone());
|
let i_ptr = f_builder.build_alloc(i32_ty);
|
||||||
|
|
||||||
let n = f_builder.get_param(0).expect("Param 0 missing");
|
let n = f_builder.get_param(0).expect("Param 0 missing");
|
||||||
let const_1 = Operand::Integer(1);
|
let const_1 = Operand::Integer(1);
|
||||||
|
|
||||||
// res = 1; i = n;
|
// res = 1; i = n;
|
||||||
f_builder.build_store(i32_ty.clone(), res_ptr.clone(), const_1.clone());
|
f_builder.build_store(i32_ty, res_ptr, const_1);
|
||||||
f_builder.build_store(i32_ty.clone(), i_ptr.clone(), n);
|
f_builder.build_store(i32_ty, i_ptr, n);
|
||||||
|
|
||||||
f_builder.build_jump(loop_cond_block);
|
f_builder.build_jump(loop_cond_block);
|
||||||
|
|
||||||
// 2. Loop Condition Block: i > 1
|
// 2. Loop Condition Block: i > 1
|
||||||
f_builder.switch_to_block(loop_cond_block);
|
f_builder.switch_to_block(loop_cond_block);
|
||||||
let current_i = f_builder.build_load(i32_ty.clone(), i_ptr.clone());
|
let current_i = f_builder.build_load(i32_ty, i_ptr);
|
||||||
let is_gt_1 = f_builder.build_icmp(ICmpOp::Ugt, current_i, const_1.clone());
|
let is_gt_1 = f_builder.build_icmp(ICmpOp::Ugt, current_i, const_1);
|
||||||
|
|
||||||
// If i > 1 goto body, else goto exit
|
// If i > 1 goto body, else goto exit
|
||||||
f_builder.build_branch(is_gt_1, loop_body_block, exit_block);
|
f_builder.build_branch(is_gt_1, loop_body_block, exit_block);
|
||||||
@@ -47,24 +43,24 @@ fn build_test_module() -> Module {
|
|||||||
// 3. Loop Body Block: res = res * i; i = i - 1;
|
// 3. Loop Body Block: res = res * i; i = i - 1;
|
||||||
f_builder.switch_to_block(loop_body_block);
|
f_builder.switch_to_block(loop_body_block);
|
||||||
|
|
||||||
let val_res = f_builder.build_load(i32_ty.clone(), res_ptr.clone());
|
let val_res = f_builder.build_load(i32_ty, res_ptr);
|
||||||
let val_i = f_builder.build_load(i32_ty.clone(), i_ptr.clone());
|
let val_i = f_builder.build_load(i32_ty, i_ptr);
|
||||||
|
|
||||||
// res = res * i
|
// res = res * i
|
||||||
let updated_res = f_builder.build_umul(i32_ty.clone(), val_res, val_i.clone());
|
let updated_res = f_builder.build_umul(i32_ty, val_res, val_i);
|
||||||
f_builder.build_store(i32_ty.clone(), res_ptr.clone(), updated_res);
|
f_builder.build_store(i32_ty, res_ptr, updated_res);
|
||||||
|
|
||||||
// i = i - 1
|
// i = i - 1
|
||||||
let updated_i = f_builder.build_sub(i32_ty.clone(), val_i, const_1);
|
let updated_i = f_builder.build_sub(i32_ty, val_i, const_1);
|
||||||
f_builder.build_store(i32_ty.clone(), i_ptr.clone(), updated_i);
|
f_builder.build_store(i32_ty, i_ptr, updated_i);
|
||||||
|
|
||||||
// Jump back to condition
|
// Jump back to condition
|
||||||
f_builder.build_jump(loop_cond_block);
|
f_builder.build_jump(loop_cond_block);
|
||||||
|
|
||||||
// 4. Exit Block: return res
|
// 4. Exit Block: return res
|
||||||
f_builder.switch_to_block(exit_block);
|
f_builder.switch_to_block(exit_block);
|
||||||
let final_res = f_builder.build_load(i32_ty.clone(), res_ptr);
|
let final_res = f_builder.build_load(i32_ty, res_ptr);
|
||||||
f_builder.build_return(i32_ty.clone(), final_res);
|
f_builder.build_return(i32_ty, final_res);
|
||||||
}
|
}
|
||||||
module_builder.complete_function();
|
module_builder.complete_function();
|
||||||
|
|
||||||
@@ -75,25 +71,25 @@ fn build_test_module() -> Module {
|
|||||||
main_id,
|
main_id,
|
||||||
"main",
|
"main",
|
||||||
vec![], // no params
|
vec![], // no params
|
||||||
i32_ty.clone(),
|
i32_ty,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 1. Allocate space for an integer on the stack
|
// 1. Allocate space for an integer on the stack
|
||||||
let ptr = f_builder.build_alloc(i32_ty.clone());
|
let ptr = f_builder.build_alloc(i32_ty);
|
||||||
|
|
||||||
// 2. Store the value '5' into that pointer
|
// 2. Store the value '5' into that pointer
|
||||||
let input_val = Operand::Integer(5);
|
let input_val = Operand::Integer(5);
|
||||||
f_builder.build_store(i32_ty.clone(), ptr.clone(), input_val);
|
f_builder.build_store(i32_ty, ptr, input_val);
|
||||||
|
|
||||||
// 3. Load the value back from the pointer
|
// 3. Load the value back from the pointer
|
||||||
let loaded_val = f_builder.build_load(i32_ty.clone(), ptr);
|
let loaded_val = f_builder.build_load(i32_ty, ptr);
|
||||||
|
|
||||||
// 4. Call factorial(loaded_val)
|
// 4. Call factorial(loaded_val)
|
||||||
let args = [(i32_ty.clone(), loaded_val)];
|
let args = [(i32_ty, loaded_val)];
|
||||||
let final_result = f_builder.build_call(i32_ty.clone(), fact_id, args.iter());
|
let final_result = f_builder.build_call(i32_ty, fact_id, args.iter());
|
||||||
|
|
||||||
// 5. Return the result of the factorial call
|
// 5. Return the result of the factorial call
|
||||||
f_builder.build_return(i32_ty.clone(), final_result);
|
f_builder.build_return(i32_ty, final_result);
|
||||||
}
|
}
|
||||||
module_builder.complete_function();
|
module_builder.complete_function();
|
||||||
|
|
||||||
|
|||||||
+10
-10
@@ -20,9 +20,9 @@ fn fold_function_constants(func: &mut Function) {
|
|||||||
// 2. Evaluate and rewrite instructions where possible
|
// 2. Evaluate and rewrite instructions where possible
|
||||||
match inst {
|
match inst {
|
||||||
Instruction::Alloc { .. } => {}
|
Instruction::Alloc { .. } => {}
|
||||||
Instruction::Assign { register, operand } => {
|
Instruction::Copy { dest, src } => {
|
||||||
if is_constant(operand) {
|
if is_constant(src) {
|
||||||
known_constants.insert(*register, *operand);
|
known_constants.insert(*dest, *src);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Instruction::Binary {
|
Instruction::Binary {
|
||||||
@@ -36,9 +36,9 @@ fn fold_function_constants(func: &mut Function) {
|
|||||||
known_constants.insert(*dest, folded);
|
known_constants.insert(*dest, folded);
|
||||||
|
|
||||||
// Rewrite the evaluated Binary instruction into a clean Assign
|
// Rewrite the evaluated Binary instruction into a clean Assign
|
||||||
*inst = Instruction::Assign {
|
*inst = Instruction::Copy {
|
||||||
register: *dest,
|
dest: *dest,
|
||||||
operand: folded,
|
src: folded,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,9 +47,9 @@ fn fold_function_constants(func: &mut Function) {
|
|||||||
known_constants.insert(*dest, folded);
|
known_constants.insert(*dest, folded);
|
||||||
|
|
||||||
// Rewrite the evaluated Unary instruction into a clean Assign
|
// Rewrite the evaluated Unary instruction into a clean Assign
|
||||||
*inst = Instruction::Assign {
|
*inst = Instruction::Copy {
|
||||||
register: *dest,
|
dest: *dest,
|
||||||
operand: folded,
|
src: folded,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,7 +92,7 @@ fn substitute_constants_in_inst(inst: &mut Instruction, constants: &HashMap<Regi
|
|||||||
|
|
||||||
match inst {
|
match inst {
|
||||||
Instruction::Alloc { .. } => {}
|
Instruction::Alloc { .. } => {}
|
||||||
Instruction::Assign { operand, .. } => replace(operand),
|
Instruction::Copy { src, .. } => replace(src),
|
||||||
Instruction::Load { src, .. } => replace(src),
|
Instruction::Load { src, .. } => replace(src),
|
||||||
Instruction::Store { dest, src, .. } => {
|
Instruction::Store { dest, src, .. } => {
|
||||||
replace(dest);
|
replace(dest);
|
||||||
|
|||||||
+7
-7
@@ -27,12 +27,12 @@ fn propagate_copies_in_func(func: &mut Function) {
|
|||||||
// 1. Scan for pure copy instructions (Assigning a Register to a Register)
|
// 1. Scan for pure copy instructions (Assigning a Register to a Register)
|
||||||
for block in &func.blocks {
|
for block in &func.blocks {
|
||||||
for inst in &block.instructions {
|
for inst in &block.instructions {
|
||||||
if let Instruction::Assign { register, operand } = inst {
|
if let Instruction::Copy { dest, src } = inst {
|
||||||
// We only unconditionally propagate register-to-register copies here.
|
// We only unconditionally propagate register-to-register copies here.
|
||||||
// The constant folding pass handles propagating literals.
|
// The constant folding pass handles propagating literals.
|
||||||
if let Operand::Register(_) = operand {
|
if let Operand::Register(_) = src {
|
||||||
let root_operand = resolve(*operand, &aliases);
|
let root_operand = resolve(*src, &aliases);
|
||||||
aliases.insert(*register, root_operand);
|
aliases.insert(*dest, root_operand);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,7 +74,7 @@ fn propagate_copies_in_func(func: &mut Function) {
|
|||||||
replace(op);
|
replace(op);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Instruction::Assign { operand, .. } => replace(operand),
|
Instruction::Copy { src, .. } => replace(src),
|
||||||
Instruction::Alloc { .. } => {}
|
Instruction::Alloc { .. } => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,8 +95,8 @@ fn propagate_copies_in_func(func: &mut Function) {
|
|||||||
// since we just successfully propagated it everywhere it was used.
|
// since we just successfully propagated it everywhere it was used.
|
||||||
!matches!(
|
!matches!(
|
||||||
inst,
|
inst,
|
||||||
Instruction::Assign {
|
Instruction::Copy {
|
||||||
operand: Operand::Register(_),
|
src: Operand::Register(_),
|
||||||
..
|
..
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
+2
-2
@@ -87,7 +87,7 @@ fn eliminate_dead_code_in_func(func: &mut Function) {
|
|||||||
mark_used(arg);
|
mark_used(arg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Instruction::Assign { operand, .. } => mark_used(operand),
|
Instruction::Copy { src, .. } => mark_used(src),
|
||||||
Instruction::Phi { sources, .. } => {
|
Instruction::Phi { sources, .. } => {
|
||||||
sources.iter().for_each(|(op, _)| mark_used(op))
|
sources.iter().for_each(|(op, _)| mark_used(op))
|
||||||
}
|
}
|
||||||
@@ -112,7 +112,7 @@ fn eliminate_dead_code_in_func(func: &mut Function) {
|
|||||||
match inst {
|
match inst {
|
||||||
// PURE INSTRUCTIONS: Can be safely removed if their result is ignored
|
// PURE INSTRUCTIONS: Can be safely removed if their result is ignored
|
||||||
Instruction::Alloc { dest, .. }
|
Instruction::Alloc { dest, .. }
|
||||||
| Instruction::Assign { register: dest, .. }
|
| Instruction::Copy { dest, .. }
|
||||||
| Instruction::Binary { dest, .. }
|
| Instruction::Binary { dest, .. }
|
||||||
| Instruction::Unary { dest, .. }
|
| Instruction::Unary { dest, .. }
|
||||||
| Instruction::Load { dest, .. }
|
| Instruction::Load { dest, .. }
|
||||||
|
|||||||
+3
-3
@@ -18,9 +18,9 @@ fn destroy_ssa_in_func(func: &mut Function) {
|
|||||||
for inst in &block.instructions {
|
for inst in &block.instructions {
|
||||||
if let Instruction::Phi { dest, sources, .. } = inst {
|
if let Instruction::Phi { dest, sources, .. } = inst {
|
||||||
for (op, pred_id) in sources {
|
for (op, pred_id) in sources {
|
||||||
let assign = Instruction::Assign {
|
let assign = Instruction::Copy {
|
||||||
register: *dest,
|
dest: *dest,
|
||||||
operand: *op,
|
src: *op,
|
||||||
};
|
};
|
||||||
// Queue the assignment to be inserted into the predecessor block
|
// Queue the assignment to be inserted into the predecessor block
|
||||||
pending_moves.entry(*pred_id).or_default().push(assign);
|
pending_moves.entry(*pred_id).or_default().push(assign);
|
||||||
|
|||||||
+5
-5
@@ -47,7 +47,7 @@ fn promote_allocs_in_func(func: &mut Function) {
|
|||||||
escaped_allocs.insert(*r);
|
escaped_allocs.insert(*r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Instruction::Unary { src, .. } | Instruction::Assign { operand: src, .. } => {
|
Instruction::Unary { src, .. } | Instruction::Copy { src, .. } => {
|
||||||
if let Operand::Register(r) = src {
|
if let Operand::Register(r) = src {
|
||||||
escaped_allocs.insert(*r);
|
escaped_allocs.insert(*r);
|
||||||
}
|
}
|
||||||
@@ -128,7 +128,7 @@ fn promote_allocs_in_func(func: &mut Function) {
|
|||||||
match inst {
|
match inst {
|
||||||
Instruction::Alloc { dest, .. }
|
Instruction::Alloc { dest, .. }
|
||||||
| Instruction::Load { dest, .. }
|
| Instruction::Load { dest, .. }
|
||||||
| Instruction::Assign { register: dest, .. }
|
| Instruction::Copy { dest, .. }
|
||||||
| Instruction::Binary { dest, .. }
|
| Instruction::Binary { dest, .. }
|
||||||
| Instruction::Unary { dest, .. }
|
| Instruction::Unary { dest, .. }
|
||||||
| Instruction::Call { dest, .. }
|
| Instruction::Call { dest, .. }
|
||||||
@@ -214,9 +214,9 @@ fn promote_allocs_in_func(func: &mut Function) {
|
|||||||
} if promotable_allocs.contains_key(&src_reg) => {
|
} if promotable_allocs.contains_key(&src_reg) => {
|
||||||
// Replace Load with direct Assign from the active definition
|
// Replace Load with direct Assign from the active definition
|
||||||
let active_val = *local_defs.get(&src_reg).unwrap_or(&Operand::Integer(0));
|
let active_val = *local_defs.get(&src_reg).unwrap_or(&Operand::Integer(0));
|
||||||
new_instructions.push(Instruction::Assign {
|
new_instructions.push(Instruction::Copy {
|
||||||
register: dest,
|
dest,
|
||||||
operand: active_val,
|
src: active_val,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
|||||||
+2
-2
@@ -211,8 +211,8 @@ impl<'a> IrPrinter<'a> {
|
|||||||
dest, result_ty, func_name, args
|
dest, result_ty, func_name, args
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Instruction::Assign { register, operand } => {
|
Instruction::Copy { dest, src } => {
|
||||||
write!(&mut self.buffer, "{} = {}", register, operand)
|
write!(&mut self.buffer, "{} = copy {}", dest, src)
|
||||||
}
|
}
|
||||||
Instruction::Phi {
|
Instruction::Phi {
|
||||||
dest,
|
dest,
|
||||||
|
|||||||
+8
-2
@@ -49,7 +49,10 @@ fn validate_function(
|
|||||||
Instruction::Call {
|
Instruction::Call {
|
||||||
result_ty, dest, ..
|
result_ty, dest, ..
|
||||||
} => (Some(*dest), Some(*result_ty)),
|
} => (Some(*dest), Some(*result_ty)),
|
||||||
Instruction::Assign { register, operand } => {
|
Instruction::Copy {
|
||||||
|
dest: register,
|
||||||
|
src: operand,
|
||||||
|
} => {
|
||||||
let inferred_ty = match operand {
|
let inferred_ty = match operand {
|
||||||
Operand::Register(r) => reg_types.get(r).copied(),
|
Operand::Register(r) => reg_types.get(r).copied(),
|
||||||
Operand::Boolean(_) => Some(Type::Bool),
|
Operand::Boolean(_) => Some(Type::Bool),
|
||||||
@@ -110,7 +113,10 @@ fn validate_function(
|
|||||||
for inst in &block.instructions {
|
for inst in &block.instructions {
|
||||||
match inst {
|
match inst {
|
||||||
Instruction::Alloc { .. } => {}
|
Instruction::Alloc { .. } => {}
|
||||||
Instruction::Assign { register, operand } => {
|
Instruction::Copy {
|
||||||
|
dest: register,
|
||||||
|
src: operand,
|
||||||
|
} => {
|
||||||
let expected_ty = *reg_types
|
let expected_ty = *reg_types
|
||||||
.get(register)
|
.get(register)
|
||||||
.ok_or_else(|| format!("Unknown register {:?}", register))?;
|
.ok_or_else(|| format!("Unknown register {:?}", register))?;
|
||||||
|
|||||||
Reference in New Issue
Block a user