init: initial commit of the Scarlett framework
- Initialize Rust project configuration (Cargo) and .gitignore - Implement core Intermediate Representation (IR), printer, and builder utilities - Add IR validation module for type checking and constraint verification - Introduce optimization passes: Mem2Reg, Constant Folding, Copy Propagation, Dead Code Elimination, and SSA Destruction - Implement x86_64 backend for assembly code generation - Add a C test harness and main entry point to generate, compile, and test a GCD assembly function
This commit is contained in:
@@ -0,0 +1 @@
|
||||
pub mod x86_64;
|
||||
@@ -0,0 +1,708 @@
|
||||
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::Mul => {
|
||||
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::Mul)
|
||||
{
|
||||
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::Mul => "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::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: target_id,
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
writeln!(&mut self.assembly, " call function_{}", target_id.0).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");
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user