diff --git a/src/asm.rs b/src/asm.rs index 314683a..106aa20 100644 --- a/src/asm.rs +++ b/src/asm.rs @@ -1,5 +1,5 @@ -//! This module provides the assembly code generation backend, -//! translating the compiler's IR into target-specific assembly. +//! Assembly code generation backend: translates the compiler's IR +//! into target-specific assembly. pub mod aarch64; pub mod common; diff --git a/src/asm/aarch64.rs b/src/asm/aarch64.rs index 95e4ce4..689a98a 100644 --- a/src/asm/aarch64.rs +++ b/src/asm/aarch64.rs @@ -1,3 +1,7 @@ +//! AArch64 backend driver: lowers an IR module to instructions over +//! virtual registers, register-allocates each function, and delegates +//! textual emission of the final assembly to [`printer::AsmPrinter`]. + mod aapcs; mod frame; mod function_generator; @@ -7,20 +11,18 @@ mod printer; mod register_allocator; mod types; -pub use inst::Instruction; -pub use types::{BinOp, Operand, Register}; - use crate::asm::common::StructLayouts; use crate::asm::error::Error; use crate::common::{Generator, Target}; use crate::ir; use aapcs::{classify_args, ArgumentLocation}; use frame::FrameLayout; -use function_generator::FunctionGenerator; -use printer::{AsmPrint, AsmPrinter}; +use function_generator::AsmFunctionGenerator; +use inst::Instruction; +use printer::AsmPrinter; use register_allocator::RegisterAllocator; use std::io::Write; -use types::RegisterSize; +use types::{InstOperand, Register, RegisterSize}; struct GeneratedGlobal { symbol: String, @@ -70,6 +72,9 @@ fn stream_uses_fp(insts: &[Instruction]) -> bool { }) } +/// AArch64 assembly generator: runs instruction selection and register +/// allocation for every function of the module, together with data +/// layout for globals; the `Generator` impl drives both phases. pub struct AArch64AsmGenerator<'a> { module: &'a ir::Module, registry: &'a ir::Registry, @@ -121,42 +126,11 @@ impl<'a> Generator for AArch64AsmGenerator<'a> { Ok(()) } + /// Delegates to [`AsmPrinter::emit_program`], mirroring how the ir + /// layer's `output` delegates to `IrPrinter::emit_module` — the + /// printer owns section layout, symbol emission, and prologues. fn output(&self, w: &mut W) -> Result<(), Error> { - let mut printer = AsmPrinter::new(w, self.target); - - if !self.globals.is_empty() { - printer.emit_section("data")?; - for g in &self.globals { - printer.emit_global(&g.symbol)?; - printer.emit_align(2)?; - printer.emit_label(&g.symbol)?; - match &g.data { - GlobalData::Word { value } => printer.emit_word(*value)?, - GlobalData::Array { words, zero_bytes } => { - for v in words { - printer.emit_word(*v)?; - } - if *zero_bytes > 0 { - printer.emit_zero(*zero_bytes)?; - } - } - } - } - printer.emit_newline()?; - } - - printer.emit_section("text")?; - for func in &self.functions { - printer.emit_global(&func.symbol)?; - printer.emit_align(2)?; - printer.emit_label(&func.symbol)?; - printer.set_uses_fp(func.uses_fp); - printer.emit_prologue(func.frame_size)?; - printer.emit_insts(&func.insts)?; - printer.emit_newline()?; - } - - Ok(()) + AsmPrinter::new(w, self.target).emit_program(&self.globals, &self.functions) } } @@ -179,7 +153,7 @@ impl<'a> AArch64AsmGenerator<'a> { ArgumentLocation::Gpr(n) => Instruction::Mov { size, dst, - src: Operand::Register(Register::Physical(n)), + src: InstOperand::Register(Register::Physical(n)), }, ArgumentLocation::Fpr(_) => todo!( "asmt-4: lift an incoming f32 argument out of its `s_` register \ @@ -253,7 +227,7 @@ impl<'a> AArch64AsmGenerator<'a> { let mut frame = FrameLayout::from_blocks(&body.blocks, layouts)?; let mut insts = Self::handle_arguments(body)?; insts.extend( - FunctionGenerator::new(&symbol, &frame, layouts, target, body.next_vreg) + AsmFunctionGenerator::new(&symbol, &frame, layouts, target, body.next_vreg) .generate(&body.blocks)?, ); diff --git a/src/asm/aarch64/aapcs.rs b/src/asm/aarch64/aapcs.rs index efb1a9a..16f914d 100644 --- a/src/asm/aarch64/aapcs.rs +++ b/src/asm/aarch64/aapcs.rs @@ -41,8 +41,8 @@ pub const OUTGOING_ARG_SLOT_BYTES: i64 = 8; /// AAPCS64 location of a single argument at the call boundary. /// /// `Gpr(n)` / `Fpr(n)` carry the architectural register index; pair -/// the index with the operand's [`super::types::RegSize`] to obtain -/// the concrete `w_`/`x_`/`s_` form. +/// the index with the operand's [`super::types::RegisterSize`] to +/// obtain the concrete `w_`/`x_`/`s_` form. /// /// `Stack { offset }` is the byte offset within the outgoing-arg /// area. The two sides interpret the offset against different bases: @@ -91,10 +91,9 @@ where /// advances. Kept internal to this module; callers consume the /// resolved [`ArgumentLocation`] instead. /// -/// `Float` is currently unreachable because `Dtype::F32` is not yet a -/// variant of [`ir::Dtype`]; the arm is part of the classifier's -/// AAPCS64 specification and becomes live as soon as the -/// `TryFrom<&ir::Dtype>` impl learns to map `Dtype::F32` to it. +/// `Float` is unreachable while [`ir::Dtype`] lacks an `F32` variant; +/// the arm is kept as part of the classifier's AAPCS64 specification +/// so adding the variant only extends the `TryFrom<&ir::Dtype>` impl. enum ArgumentClass { Int, #[allow(dead_code)] diff --git a/src/asm/aarch64/function_generator.rs b/src/asm/aarch64/function_generator.rs index 4fcbcbd..a07e12b 100644 --- a/src/asm/aarch64/function_generator.rs +++ b/src/asm/aarch64/function_generator.rs @@ -1,9 +1,13 @@ +//! Instruction selection: lowers IR statements to AArch64 instructions +//! over virtual registers, including ABI handling for arguments, calls, +//! and returns. + use super::aapcs::{classify_args, ArgumentLocation}; use super::frame::{outgoing_arg_addr, outgoing_stack_bytes, FrameLayout}; use super::inst::Instruction; use super::phi_lowering::{self, ParallelCopy, SplitEdge}; use super::types::{ - Addr, BinOp, Cond, IndexOperand, Operand, Register, RegisterSize, REG_IP0, REG_X0, + Addr, Cond, InstBinOp, InstOperand, Register, RegisterSize, REG_IP0, REG_X0, }; use crate::asm::common::{StackSlot, StructLayouts}; use crate::asm::error::Error; @@ -29,7 +33,7 @@ pub enum PtrBase { Register(usize), } -pub struct FunctionGenerator<'a> { +pub struct AsmFunctionGenerator<'a> { func_id: &'a str, frame: &'a FrameLayout, layouts: &'a StructLayouts, @@ -39,7 +43,7 @@ pub struct FunctionGenerator<'a> { cond_map: HashMap, } -impl<'a> FunctionGenerator<'a> { +impl<'a> AsmFunctionGenerator<'a> { /// Creates a generator for one function body. `next_vreg` seeds the /// virtual-register counter from the IR body's high-water mark so the /// copies introduced during phi lowering get fresh ids. @@ -167,15 +171,15 @@ impl<'a> FunctionGenerator<'a> { let addr = self.lower_ptr_as_addr(&s.ptr)?; match src { - Operand::Register(r) => { + InstOperand::Register(r) => { self.insts.push(Instruction::Str { size, src: r, addr }); } - Operand::Immediate(imm) => { + InstOperand::Immediate(imm) => { let tmp = self.fresh_vreg(); self.insts.push(Instruction::Mov { size, dst: Register::Virtual(tmp), - src: Operand::Immediate(imm), + src: InstOperand::Immediate(imm), }); self.insts.push(Instruction::Str { size, @@ -451,11 +455,11 @@ impl<'a> FunctionGenerator<'a> { }); if offset != 0 { self.insts.push(Instruction::BinOp { - op: BinOp::Add, + op: InstBinOp::Add, size: RegisterSize::X64, dst: Register::Virtual(dst), lhs: Register::Virtual(dst), - rhs: Operand::Immediate(offset), + rhs: InstOperand::Immediate(offset), }); } } @@ -528,13 +532,13 @@ impl<'a> FunctionGenerator<'a> { let (op, size) = self.lower_value(arg)?; let src_reg = match op { - Operand::Register(r) => r, - Operand::Immediate(imm) => { + InstOperand::Register(r) => r, + InstOperand::Immediate(imm) => { let scratch = Register::Physical(REG_IP0); self.insts.push(Instruction::Mov { size, dst: scratch, - src: Operand::Immediate(imm), + src: InstOperand::Immediate(imm), }); scratch } @@ -549,9 +553,10 @@ impl<'a> FunctionGenerator<'a> { /// Lifts the AAPCS64 return value out of `x0` (or `s0` for FP /// returns) into the vreg named by `res`. Dispatch is driven by - /// the return dtype's [`RegSize`], so adding a new scalar class - /// (e.g. `Dtype::F32 -> RegSize::S32`) requires only that - /// `RegisterSize`'s `TryFrom<&ir::Dtype>` learns the new mapping. + /// the return dtype's [`RegisterSize`], so adding a new scalar + /// class (e.g. `Dtype::F32 -> RegisterSize::S32`) requires only + /// that `RegisterSize`'s `TryFrom<&ir::Dtype>` learns the new + /// mapping. fn emit_call_result(&mut self, res: &ir::Local) -> Result<(), Error> { let dst = Register::Virtual(res.id.0); let size = RegisterSize::try_from(&res.dtype)?; @@ -566,7 +571,7 @@ impl<'a> FunctionGenerator<'a> { self.insts.push(Instruction::Mov { size: RegisterSize::X64, dst, - src: Operand::Register(Register::Virtual(v)), + src: InstOperand::Register(Register::Virtual(v)), }); } PtrBase::Stack => { @@ -586,9 +591,9 @@ impl<'a> FunctionGenerator<'a> { Ok(()) } - fn lower_int(&self, val: &ir::Operand) -> Result { + fn lower_int(&self, val: &ir::Operand) -> Result { match val { - ir::Operand::Const(c) => Ok(Operand::Immediate(c.val)), + ir::Operand::Const(c) => Ok(InstOperand::Immediate(c.val)), ir::Operand::Local(l) => { if !matches!(l.dtype, ir::Dtype::I1 | ir::Dtype::I32) { return Err(Error::UnsupportedDtype { @@ -600,7 +605,7 @@ impl<'a> FunctionGenerator<'a> { what: format!("int operand references alloca pointer %r{}", l.id.0), }); } - Ok(Operand::Register(Register::Virtual(l.id.0))) + Ok(InstOperand::Register(Register::Virtual(l.id.0))) } ir::Operand::Global(_) => Err(Error::UnsupportedOperand { what: format!("unsupported int operand: {}", val), @@ -610,22 +615,22 @@ impl<'a> FunctionGenerator<'a> { fn lower_int_to_reg(&mut self, val: &ir::Operand) -> Result { match self.lower_int(val)? { - Operand::Register(r) => Ok(r), - Operand::Immediate(imm) => { + InstOperand::Register(r) => Ok(r), + InstOperand::Immediate(imm) => { let tmp = self.fresh_vreg(); self.insts.push(Instruction::Mov { size: RegisterSize::W32, dst: Register::Virtual(tmp), - src: Operand::Immediate(imm), + src: InstOperand::Immediate(imm), }); Ok(Register::Virtual(tmp)) } } } - fn lower_value(&self, val: &ir::Operand) -> Result<(Operand, RegisterSize), Error> { + fn lower_value(&self, val: &ir::Operand) -> Result<(InstOperand, RegisterSize), Error> { match val { - ir::Operand::Const(c) => Ok((Operand::Immediate(c.val), RegisterSize::W32)), + ir::Operand::Const(c) => Ok((InstOperand::Immediate(c.val), RegisterSize::W32)), ir::Operand::Local(l) => { let size = match &l.dtype { ir::Dtype::I1 | ir::Dtype::I32 => RegisterSize::W32, @@ -646,7 +651,7 @@ impl<'a> FunctionGenerator<'a> { }) } }; - Ok((Operand::Register(Register::Virtual(l.id.0)), size)) + Ok((InstOperand::Register(Register::Virtual(l.id.0)), size)) } ir::Operand::Global(_) => Err(Error::UnsupportedOperand { what: "unexpected global variable in value position".into(), @@ -673,18 +678,16 @@ impl<'a> FunctionGenerator<'a> { match val { ir::Operand::Local(l) => { let vreg_index = l.id.0; - // Check if this local is a stack allocation (alloca). - // Allocas have their address implicitly defined by their stack slot, - // rather than being stored in a register. + // An alloca's address is its stack slot, not a value held + // in a register. if let Some(slot) = self.frame.alloca_slot(vreg_index) { return Ok((PtrBase::Stack, Some(slot))); } - // Otherwise, if it's a pointer type, the pointer value itself - // lives in a virtual register (e.g., result of a GEP or load). + // A non-alloca pointer local holds its address in a virtual + // register (e.g., the result of a GEP or load). if matches!(l.dtype, ir::Dtype::Pointer { .. }) { return Ok((PtrBase::Register(vreg_index), None)); } - // Non-pointer locals cannot be used as pointer operands. Err(Error::UnsupportedDtype { dtype: l.dtype.clone(), }) @@ -699,9 +702,9 @@ impl<'a> FunctionGenerator<'a> { } } - fn lower_index(&self, val: &ir::Operand) -> Result { + fn lower_index(&self, val: &ir::Operand) -> Result { match val { - ir::Operand::Const(c) => Ok(IndexOperand::Imm(c.val)), + ir::Operand::Const(c) => Ok(InstOperand::Immediate(c.val)), ir::Operand::Local(l) => { if !matches!(l.dtype, ir::Dtype::I1 | ir::Dtype::I32) { return Err(Error::UnsupportedDtype { @@ -713,7 +716,7 @@ impl<'a> FunctionGenerator<'a> { what: format!("index operand references alloca pointer %r{}", l.id.0), }); } - Ok(IndexOperand::Reg(Register::Virtual(l.id.0))) + Ok(InstOperand::Register(Register::Virtual(l.id.0))) } ir::Operand::Global(_) => Err(Error::UnsupportedOperand { what: format!("unsupported index operand: {}", val), @@ -761,8 +764,8 @@ impl<'a> FunctionGenerator<'a> { let size = RegisterSize::try_from(dst.dtype())?; let src_op = match src { - ir::Operand::Const(c) => Operand::Immediate(c.val), - ir::Operand::Local(l) => Operand::Register(Register::Virtual(l.id.0)), + ir::Operand::Const(c) => InstOperand::Immediate(c.val), + ir::Operand::Local(l) => InstOperand::Register(Register::Virtual(l.id.0)), ir::Operand::Global(_) => { return Err(Error::UnsupportedOperand { what: "global variable in phi copy".into(), @@ -801,12 +804,12 @@ impl<'a> FunctionGenerator<'a> { } } -fn arith_op_to_binop(op: &ir::stmt::ArithBinOp) -> BinOp { +fn arith_op_to_binop(op: &ir::stmt::ArithBinOp) -> InstBinOp { match op { - ir::stmt::ArithBinOp::Add => BinOp::Add, - ir::stmt::ArithBinOp::Sub => BinOp::Sub, - ir::stmt::ArithBinOp::Mul => BinOp::Mul, - ir::stmt::ArithBinOp::SDiv => BinOp::SDiv, + ir::stmt::ArithBinOp::Add => InstBinOp::Add, + ir::stmt::ArithBinOp::Sub => InstBinOp::Sub, + ir::stmt::ArithBinOp::Mul => InstBinOp::Mul, + ir::stmt::ArithBinOp::SDiv => InstBinOp::SDiv, } } @@ -815,7 +818,7 @@ fn arith_op_to_binop(op: &ir::stmt::ArithBinOp) -> BinOp { /// and `s0` for floating-point returns. Selection is driven by /// `size` so that adding a new scalar class only requires /// `RegisterSize`'s `TryFrom<&ir::Dtype>` to learn the new mapping. -fn return_inst(size: RegisterSize, src: Operand) -> Instruction { +fn return_inst(size: RegisterSize, src: InstOperand) -> Instruction { match size { RegisterSize::W32 | RegisterSize::X64 => Instruction::Mov { size, @@ -836,7 +839,7 @@ fn return_value_load(size: RegisterSize, dst: Register) -> Instruction { RegisterSize::W32 | RegisterSize::X64 => Instruction::Mov { size, dst, - src: Operand::Register(Register::Physical(REG_X0)), + src: InstOperand::Register(Register::Physical(REG_X0)), }, RegisterSize::S32 => todo!( "asmt-4: lift an f32 call result out of the FP return register `s0` \ diff --git a/src/asm/aarch64/inst.rs b/src/asm/aarch64/inst.rs index 6342088..f28ac7b 100644 --- a/src/asm/aarch64/inst.rs +++ b/src/asm/aarch64/inst.rs @@ -1,6 +1,10 @@ +//! The AArch64 instruction enum shared by lowering and printing: +//! labels, data movement, arithmetic, memory access, and control flow, +//! with virtual registers still in place until register allocation. + use std::collections::{HashMap, HashSet}; -use super::types::{Addr, BinOp, Cond, FBinOp, IndexOperand, Operand, RegisterSize, Register}; +use super::types::{Addr, Cond, FBinOp, InstBinOp, InstOperand, RegisterSize, Register}; use crate::common::graph::CfgNode; #[derive(Debug, Clone, PartialEq, Eq)] @@ -11,20 +15,21 @@ pub enum Instruction { Mov { size: RegisterSize, dst: Register, - src: Operand, + src: InstOperand, }, BinOp { - op: BinOp, + op: InstBinOp, size: RegisterSize, dst: Register, lhs: Register, - rhs: Operand, + rhs: InstOperand, }, /// Single-precision floating-point binary operation, e.g. /// `fadd s_d, s_n, s_m`. Both operands and the destination live in - /// the Fpr bank; the result is always 32-bit (`RegSize::S32`). + /// the Fpr bank; the result is always 32-bit + /// (`RegisterSize::S32`). FBinOp { op: FBinOp, dst: Register, @@ -52,18 +57,19 @@ pub enum Instruction { Gep { dst: Register, base: Register, - index: IndexOperand, + index: InstOperand, scale: i64, }, Cmp { size: RegisterSize, lhs: Register, - rhs: Operand, + rhs: InstOperand, }, /// Single-precision floating-point comparison `fcmp s_n, s_m`. - /// Sets NZCV, which is later consumed by a `B { Cond, label }` arm. + /// Sets NZCV, which is later consumed by a `BCond { cond, label }` + /// arm. FCmp { lhs: Register, rhs: Register, @@ -86,10 +92,10 @@ pub enum Instruction { /// `fmov s_d, s_n` (Fpr-to-Fpr) or `fmov s_d, w_n` (Gpr-to-Fpr). /// Used in phi lowering, in the AAPCS64 entry/exit shims for `f32` /// arguments and return values, and to materialise a float constant - /// from a literal `Operand::Immediate`. + /// from a literal `InstOperand::Immediate`. Fmov { dst: Register, - src: Operand, + src: InstOperand, }, B { @@ -165,8 +171,8 @@ impl Instruction { } }; - let add_operand = |s: &mut HashSet, op: &Operand| { - if let Operand::Register(Register::Virtual(v)) = op { + let add_operand = |s: &mut HashSet, op: &InstOperand| { + if let InstOperand::Register(Register::Virtual(v)) = op { s.insert(*v); } }; @@ -199,7 +205,7 @@ impl Instruction { Instruction::Lea { addr, .. } => add_addr(&mut used, addr), Instruction::Gep { base, index, .. } => { add_reg(&mut used, base); - if let IndexOperand::Reg(r) = index { + if let InstOperand::Register(r) = index { add_reg(&mut used, r); } } @@ -227,9 +233,10 @@ impl Instruction { } /// Returns the virtual register defined by this instruction together - /// with the [`RegSize`] it carries, if exactly one virtual register - /// is defined. Instructions with no destination (`Str`, `Cmp`, - /// `Jump`, ...), or whose destination is physical, return `None`. + /// with the [`RegisterSize`] it carries, if exactly one virtual + /// register is defined. Instructions with no destination (`Str`, + /// `Cmp`, `Jump`, ...), or whose destination is physical, return + /// `None`. /// /// `Lea`/`Gep` always produce pointer-sized (`X64`) destinations. /// The integer/float scalar ops carry their dtype-derived size: diff --git a/src/asm/aarch64/phi_lowering.rs b/src/asm/aarch64/phi_lowering.rs index 8e2c716..15a4b4a 100644 --- a/src/asm/aarch64/phi_lowering.rs +++ b/src/asm/aarch64/phi_lowering.rs @@ -1,3 +1,8 @@ +//! SSA phi elimination planning: computes the parallel copies each +//! control-flow edge needs, splits critical edges into fresh blocks, +//! and retargets terminators so the function generator can emit the +//! plan as straight-line instructions. + use std::collections::HashMap; use crate::ir::function::{BasicBlock, BlockLabel}; diff --git a/src/asm/aarch64/printer.rs b/src/asm/aarch64/printer.rs index 0ea485c..78eabb5 100644 --- a/src/asm/aarch64/printer.rs +++ b/src/asm/aarch64/printer.rs @@ -1,42 +1,19 @@ +//! Textual emitter for the finalized instruction stream: resolves +//! register names and immediate encodings (falling back to scratch +//! registers where an operand does not fit), and orchestrates +//! whole-program assembly output — sections, globals, and functions. + use std::io::Write; use super::frame::SCRATCH_SPILL_SLOT; use super::inst::Instruction; use super::types::{ - Addr, BinOp, Cond, FBinOp, IndexOperand, Operand, RegisterSize, Register, SCRATCH0, SCRATCH1, + Addr, Cond, FBinOp, InstBinOp, InstOperand, RegisterSize, Register, SCRATCH0, SCRATCH1, }; +use super::{GeneratedFunction, GeneratedGlobal, GlobalData}; use crate::asm::error::Error; use crate::common::Target; -pub trait AsmPrint { - fn emit_inst(&mut self, inst: &Instruction) -> Result<(), Error>; - - fn emit_insts(&mut self, insts: &[Instruction]) -> Result<(), Error> { - for inst in insts { - self.emit_inst(inst)?; - } - Ok(()) - } - - fn emit_sub_sp(&mut self, imm: i64) -> Result<(), Error>; - - fn emit_global(&mut self, sym: &str) -> Result<(), Error>; - - fn emit_align(&mut self, power: u32) -> Result<(), Error>; - - fn emit_label(&mut self, name: &str) -> Result<(), Error>; - - fn emit_prologue(&mut self, frame_size: i64) -> Result<(), Error>; - - fn emit_section(&mut self, name: &str) -> Result<(), Error>; - - fn emit_word(&mut self, value: i64) -> Result<(), Error>; - - fn emit_zero(&mut self, bytes: i64) -> Result<(), Error>; - - fn emit_newline(&mut self) -> Result<(), Error>; -} - pub struct AsmPrinter { writer: W, target: Target, @@ -63,6 +40,59 @@ impl AsmPrinter { self.current_fn_uses_fp = uses_fp; } + /// Emits the complete assembly file: a `.data` section holding + /// every global (when any exist), then the `.text` section with + /// every function. The single source of truth for what a teac `.s` + /// file looks like; `AArch64AsmGenerator::output` delegates here + /// wholesale, mirroring how the ir layer's `output` delegates to + /// [`crate::ir::printer::IrPrinter::emit_module`]. + pub fn emit_program( + &mut self, + globals: &[GeneratedGlobal], + functions: &[GeneratedFunction], + ) -> Result<(), Error> { + if !globals.is_empty() { + self.emit_section("data")?; + for g in globals { + self.emit_global(&g.symbol)?; + self.emit_align(2)?; + self.emit_label(&g.symbol)?; + match &g.data { + GlobalData::Word { value } => self.emit_word(*value)?, + GlobalData::Array { words, zero_bytes } => { + for v in words { + self.emit_word(*v)?; + } + if *zero_bytes > 0 { + self.emit_zero(*zero_bytes)?; + } + } + } + } + self.emit_newline()?; + } + + self.emit_section("text")?; + for func in functions { + self.emit_function(func)?; + } + + Ok(()) + } + + /// Emits one function: symbol directive, alignment, label, the + /// FP-usage bracket flag, prologue, the finalized instruction + /// stream, and a trailing blank line. + pub fn emit_function(&mut self, func: &GeneratedFunction) -> Result<(), Error> { + self.emit_global(&func.symbol)?; + self.emit_align(2)?; + self.emit_label(&func.symbol)?; + self.set_uses_fp(func.uses_fp); + self.emit_prologue(func.frame_size)?; + self.emit_insts(&func.insts)?; + self.emit_newline() + } + fn reg_name(&self, r: Register, size: RegisterSize) -> String { match r { Register::StackPointer => "sp".to_string(), @@ -158,19 +188,19 @@ impl AsmPrinter { /// [`emit_add_x_imm_with`]: when `scratch == dst`, the initial `mov` /// overwrites `dst` with `#imm`, and the subsequent `add dst, base, /// dst` still computes `base + imm` correctly because `dst` carries - /// `imm` at that point. Do not use this helper for any other op - /// sequence — the identity does not generalise. + /// `imm` at that point. Using this helper for any other op + /// sequence is unsound — the identity does not generalise. fn pick_scratch_or_clobber_dst(&self, regs: &[Register], dst: Register) -> Register { self.pick_free_scratch(regs).unwrap_or(dst) } - fn emit_mov(&mut self, size: RegisterSize, dst: Register, src: Operand) -> Result<(), Error> { + fn emit_mov(&mut self, size: RegisterSize, dst: Register, src: InstOperand) -> Result<(), Error> { let dst_s = self.reg_name(dst, size); match src { - Operand::Immediate(imm) => { + InstOperand::Immediate(imm) => { writeln!(self.writer, "\tmov {dst_s}, #{imm}")?; } - Operand::Register(r) => { + InstOperand::Register(r) => { let src_s = self.reg_name(r, size); writeln!(self.writer, "\tmov {dst_s}, {src_s}")?; } @@ -180,22 +210,22 @@ impl AsmPrinter { fn emit_binop( &mut self, - op: BinOp, + op: InstBinOp, size: RegisterSize, dst: Register, lhs: Register, - rhs: Operand, + rhs: InstOperand, ) -> Result<(), Error> { let dst_s = self.reg_name(dst, size); let lhs_s = self.reg_name(lhs, size); match (op, rhs) { - (BinOp::Add | BinOp::Sub, Operand::Immediate(imm)) => { + (InstBinOp::Add | InstBinOp::Sub, InstOperand::Immediate(imm)) => { let (op_mn, imm_abs) = match (op, imm < 0) { - (BinOp::Add, true) => ("sub", -imm), - (BinOp::Sub, true) => ("add", -imm), - (BinOp::Add, false) => ("add", imm), - (BinOp::Sub, false) => ("sub", imm), + (InstBinOp::Add, true) => ("sub", -imm), + (InstBinOp::Sub, true) => ("add", -imm), + (InstBinOp::Add, false) => ("add", imm), + (InstBinOp::Sub, false) => ("sub", imm), _ => unreachable!(), }; if self.is_addsub_imm_encodable(imm_abs) { @@ -204,26 +234,26 @@ impl AsmPrinter { self.emit_op_via_imm_scratch(op_mn, size, dst, lhs, imm_abs as u64)?; } } - (BinOp::Add, Operand::Register(r)) => { + (InstBinOp::Add, InstOperand::Register(r)) => { let rhs_s = self.reg_name(r, size); writeln!(self.writer, "\tadd {dst_s}, {lhs_s}, {rhs_s}")?; } - (BinOp::Sub, Operand::Register(r)) => { + (InstBinOp::Sub, InstOperand::Register(r)) => { let rhs_s = self.reg_name(r, size); writeln!(self.writer, "\tsub {dst_s}, {lhs_s}, {rhs_s}")?; } - (BinOp::Mul, Operand::Register(r)) => { + (InstBinOp::Mul, InstOperand::Register(r)) => { let rhs_s = self.reg_name(r, size); writeln!(self.writer, "\tmul {dst_s}, {lhs_s}, {rhs_s}")?; } - (BinOp::Mul, Operand::Immediate(imm)) => { + (InstBinOp::Mul, InstOperand::Immediate(imm)) => { self.emit_op_via_imm_scratch("mul", size, dst, lhs, imm as u64)?; } - (BinOp::SDiv, Operand::Register(r)) => { + (InstBinOp::SDiv, InstOperand::Register(r)) => { let rhs_s = self.reg_name(r, size); writeln!(self.writer, "\tsdiv {dst_s}, {lhs_s}, {rhs_s}")?; } - (BinOp::SDiv, Operand::Immediate(imm)) => { + (InstBinOp::SDiv, InstOperand::Immediate(imm)) => { self.emit_op_via_imm_scratch("sdiv", size, dst, lhs, imm as u64)?; } } @@ -375,14 +405,14 @@ impl AsmPrinter { &mut self, dst: Register, base: Register, - index: IndexOperand, + index: InstOperand, scale: i64, ) -> Result<(), Error> { let dst_s = self.reg_name(dst, RegisterSize::X64); let base_s = self.reg_name(base, RegisterSize::X64); match index { - IndexOperand::Imm(i) => { + InstOperand::Immediate(i) => { let off = i * scale; if off == 0 { writeln!(self.writer, "\tmov {dst_s}, {base_s}")?; @@ -391,7 +421,7 @@ impl AsmPrinter { self.emit_add_x_imm_with(dst, base, off, scratch)?; } } - IndexOperand::Reg(r) => { + InstOperand::Register(r) => { let idx_s = self.reg_name(r, RegisterSize::W32); if let Some(shift) = self.scale_to_shift(scale) { @@ -429,10 +459,10 @@ impl AsmPrinter { { // `dst == base` and the shared register lives in one // of the scratch slots. The other scratch is free, - // but we still need a third register for the partial - // product; bounce the original `base` value through - // the stack so its register can be reused as the - // sxtw/mul accumulator. + // but a third register is still required for the + // partial product; bounce the original `base` value + // through the stack so its register can be reused as + // the sxtw/mul accumulator. let other = if matches!(base, Register::Physical(r) if r == SCRATCH0) { Register::Physical(SCRATCH1) } else { @@ -461,16 +491,16 @@ impl AsmPrinter { Ok(()) } - fn emit_cmp(&mut self, size: RegisterSize, lhs: Register, rhs: Operand) -> Result<(), Error> { + fn emit_cmp(&mut self, size: RegisterSize, lhs: Register, rhs: InstOperand) -> Result<(), Error> { let lhs_s = self.reg_name(lhs, size); match rhs { - Operand::Register(r) => { + InstOperand::Register(r) => { writeln!(self.writer, "\tcmp {lhs_s}, {}", self.reg_name(r, size))? } - Operand::Immediate(imm) if self.is_addsub_imm_encodable(imm) => { + InstOperand::Immediate(imm) if self.is_addsub_imm_encodable(imm) => { writeln!(self.writer, "\tcmp {lhs_s}, #{imm}")? } - Operand::Immediate(imm) => { + InstOperand::Immediate(imm) => { let scratch_reg = if matches!(lhs, Register::Physical(r) if r == SCRATCH0) { Register::Physical(SCRATCH1) } else { @@ -513,7 +543,8 @@ impl AsmPrinter { let chunk2 = ((value >> 32) & 0xFFFF) as u16; let chunk3 = ((value >> 48) & 0xFFFF) as u16; - // Find the first non-zero chunk to use movz + // The first non-zero 16-bit chunk is emitted with movz (which + // zeroes the rest of the register); later chunks merge with movk. let mut first = true; if chunk0 != 0 || (chunk1 == 0 && chunk2 == 0 && chunk3 == 0) { writeln!(self.writer, "\tmovz {reg}, #{chunk0}")?; @@ -627,7 +658,7 @@ impl AsmPrinter { /// Emits `fmov s_d, s_n` (Fpr-to-Fpr) or `fmov s_d, w_n` /// (Gpr-to-Fpr) depending on the source operand's register class. - fn emit_fmov(&mut self, _dst: Register, _src: Operand) -> Result<(), Error> { + fn emit_fmov(&mut self, _dst: Register, _src: InstOperand) -> Result<(), Error> { todo!("asmt-4: emit fmov s_d, {{s|w}}_n") } @@ -680,23 +711,20 @@ impl AsmPrinter { Ok(()) } - /// Inverse of [`::emit_prologue`]: restores - /// `sp` to its post-prologue position (i.e. discards the local - /// frame), pops the saved fp/lr pair, and branches to the link - /// register. Emitted by the [`Instruction::Ret`] arm of - /// [`::emit_inst`]. + /// Inverse of [`Self::emit_prologue`]: restores `sp` to its + /// post-prologue position (i.e. discards the local frame), pops the + /// saved fp/lr pair, and branches to the link register. Emitted by + /// the [`Instruction::Ret`] arm of [`Self::emit_inst`]. fn emit_epilogue(&mut self) -> Result<(), Error> { writeln!(self.writer, "\tmov sp, x29")?; writeln!(self.writer, "\tldp x29, x30, [sp], #16")?; writeln!(self.writer, "\tret")?; Ok(()) } -} -impl AsmPrint for AsmPrinter { fn emit_inst(&mut self, inst: &Instruction) -> Result<(), Error> { match inst { - Instruction::Label(name) => writeln!(self.writer, "{name}:")?, + Instruction::Label(name) => self.emit_label(name)?, Instruction::Mov { size, dst, src } => self.emit_mov(*size, *dst, *src)?, Instruction::BinOp { op, @@ -734,6 +762,13 @@ impl AsmPrint for AsmPrinter { Ok(()) } + fn emit_insts(&mut self, insts: &[Instruction]) -> Result<(), Error> { + for inst in insts { + self.emit_inst(inst)?; + } + Ok(()) + } + fn emit_sub_sp(&mut self, imm: i64) -> Result<(), Error> { if imm == 0 { return Ok(()); diff --git a/src/asm/aarch64/register_allocator.rs b/src/asm/aarch64/register_allocator.rs index 2557db5..0fbaac2 100644 --- a/src/asm/aarch64/register_allocator.rs +++ b/src/asm/aarch64/register_allocator.rs @@ -1,8 +1,13 @@ +//! Register allocation by graph coloring: builds liveness and +//! interference over virtual registers, spills what cannot be colored, +//! and rewrites the instruction stream with physical registers plus +//! spill loads/stores around the frame. + use std::collections::{HashMap, HashSet, VecDeque}; use super::frame::FrameLayout; use super::inst::Instruction; -use super::types::{Addr, IndexOperand, Operand, RegisterSize, Register, SCRATCH0, SCRATCH1}; +use super::types::{Addr, InstBinOp, InstOperand, RegisterSize, Register, SCRATCH0, SCRATCH1}; use crate::asm::common::StackSlot; use crate::asm::error::Error; use crate::common::bitset::Bitset; @@ -70,8 +75,8 @@ impl RegisterAllocation { /// reserves spill slots in the borrowed [`FrameLayout`] for the vregs /// that do not fit, and rewrites the stream into physical-register form. /// Allocation and rewriting form one phase with no externally observable -/// intermediate, mirroring the other backend stages (`FunctionGenerator`, -/// `AsmPrinter`, `InstRewriter`). +/// intermediate, mirroring the other backend stages +/// (`AsmFunctionGenerator`, `AsmPrinter`, `InstRewriter`). pub struct RegisterAllocator<'a> { insts: &'a [Instruction], frame: &'a mut FrameLayout, @@ -422,13 +427,13 @@ impl<'a> InstRewriter<'a> { fn load_src_operand( &mut self, - op: Operand, + op: InstOperand, size: RegisterSize, scratch: u8, - ) -> Result { + ) -> Result { match op { - Operand::Immediate(i) => Ok(Operand::Immediate(i)), - Operand::Register(r) => Ok(Operand::Register(self.load_src_reg(r, size, scratch)?)), + InstOperand::Immediate(i) => Ok(InstOperand::Immediate(i)), + InstOperand::Register(r) => Ok(InstOperand::Register(self.load_src_reg(r, size, scratch)?)), } } @@ -466,7 +471,6 @@ impl<'a> InstRewriter<'a> { index, scale, } => self.rewrite_gep(*dst, *base, *index, *scale)?, - // Pass-through instructions. Instruction::B { label } => self.output.push(Instruction::B { label: label.clone(), }), @@ -484,7 +488,7 @@ impl<'a> InstRewriter<'a> { Ok(()) } - fn rewrite_mov(&mut self, size: RegisterSize, dst: Register, src: Operand) -> Result<(), Error> { + fn rewrite_mov(&mut self, size: RegisterSize, dst: Register, src: InstOperand) -> Result<(), Error> { let src_op = self.load_src_operand(src, size, SCRATCH1)?; match self.map_reg(dst)? { @@ -505,11 +509,11 @@ impl<'a> InstRewriter<'a> { fn rewrite_binop( &mut self, - op: crate::asm::aarch64::BinOp, + op: InstBinOp, size: RegisterSize, dst: Register, lhs: Register, - rhs: Operand, + rhs: InstOperand, ) -> Result<(), Error> { let lhs_reg = self.load_src_reg(lhs, size, SCRATCH0)?; let rhs_op = self.load_src_operand(rhs, size, SCRATCH1)?; @@ -523,7 +527,7 @@ impl<'a> InstRewriter<'a> { }) } - fn rewrite_cmp(&mut self, size: RegisterSize, lhs: Register, rhs: Operand) -> Result<(), Error> { + fn rewrite_cmp(&mut self, size: RegisterSize, lhs: Register, rhs: InstOperand) -> Result<(), Error> { let lhs_reg = self.load_src_reg(lhs, size, SCRATCH0)?; let rhs_op = self.load_src_operand(rhs, size, SCRATCH1)?; @@ -573,16 +577,16 @@ impl<'a> InstRewriter<'a> { &mut self, dst: Register, base: Register, - index: IndexOperand, + index: InstOperand, scale: i64, ) -> Result<(), Error> { let base_reg = self.load_src_reg(base, RegisterSize::X64, SCRATCH0)?; let base_used_scratch = matches!(base_reg, Register::Physical(r) if r == SCRATCH0); let index_scratch = scratch_after_base(base_used_scratch); let index_rewritten = match index { - IndexOperand::Imm(i) => IndexOperand::Imm(i), - IndexOperand::Reg(r) => { - IndexOperand::Reg(self.load_src_reg(r, RegisterSize::W32, index_scratch)?) + InstOperand::Immediate(i) => InstOperand::Immediate(i), + InstOperand::Register(r) => { + InstOperand::Register(self.load_src_reg(r, RegisterSize::W32, index_scratch)?) } }; let dst_scratch = scratch_after_base(base_used_scratch); @@ -653,24 +657,24 @@ impl<'a> InstRewriter<'a> { fn operand_to_phys_reg( &mut self, - op: Operand, + op: InstOperand, size: RegisterSize, scratch: u8, ) -> Result { match op { - Operand::Immediate(imm) => { + InstOperand::Immediate(imm) => { self.output.push(Instruction::Mov { size, dst: Register::Physical(scratch), - src: Operand::Immediate(imm), + src: InstOperand::Immediate(imm), }); Ok(scratch) } - Operand::Register(Register::Physical(n)) => Ok(n), - Operand::Register(Register::StackPointer) => { + InstOperand::Register(Register::Physical(n)) => Ok(n), + InstOperand::Register(Register::StackPointer) => { Err(Error::Internal("cannot use SP as source".into())) } - Operand::Register(Register::Virtual(_)) => { + InstOperand::Register(Register::Virtual(_)) => { Err(Error::Internal("unexpected vreg in operand".into())) } } diff --git a/src/asm/aarch64/types.rs b/src/asm/aarch64/types.rs index 85c42da..fa4eab8 100644 --- a/src/asm/aarch64/types.rs +++ b/src/asm/aarch64/types.rs @@ -1,10 +1,18 @@ +//! Core type definitions for the AArch64 backend: the AAPCS64 +//! register-role constants, registers, operands, addressing modes, +//! conditions, and the dtype-to-register-width mapping. +//! +//! The names are instruction-local (`InstOperand`, `InstBinOp`) so they +//! cannot be confused with the front-end's `ir::Operand` or +//! `ir::stmt::ArithBinOp`. + use crate::asm::error::Error; use crate::ir; // AAPCS64 register identifiers used throughout the aarch64 backend. // The numeric values are the ARM architectural register indices and feed // directly into `Register::Physical(_)`. Bank (`x`/`w`/`s`) is implicit -// in the `RegSize` that accompanies the operand. +// in the `RegisterSize` that accompanies the operand. /// First integer argument register and integer return register (`x0`). pub const REG_X0: u8 = 0; @@ -17,12 +25,11 @@ pub const NUM_INT_ARG_REGS: u8 = 8; /// First floating-point argument register and FP return register /// (`s0` / `d0` / `v0`). The FP register file is architecturally /// independent of the GPR file: pairing `Register::Physical(REG_S0)` -/// with `RegSize::S32` denotes `s0`, while pairing the same physical -/// index with `RegSize::W32` / `RegSize::X64` denotes `w0` / `x0` +/// with `RegisterSize::S32` denotes `s0`, while pairing the same physical +/// index with `RegisterSize::W32` / `RegisterSize::X64` denotes `w0` / `x0` /// (i.e. [`REG_X0`]). /// -/// Referenced by the AAPCS64 FP argument / return shim that asmt-4 -/// asks you to implement; see asmt-4.md §3.3. +/// Consumed by the AAPCS64 FP argument / return shim (asmt-4.md §3.3). #[allow(dead_code)] pub const REG_S0: u8 = 0; @@ -66,8 +73,8 @@ pub enum Register { /// instruction can simultaneously source one operand from each bank. /// The register allocator uses this class to split vregs into two /// interference graphs that are coloured against disjoint pools. The -/// enum is consumed only by asmt-4's solution; at the asmt-4 skeleton -/// stage no code constructs the variants. +/// skeleton's integer-only colouring constructs no variants, hence the +/// dead-code allowance. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[allow(dead_code)] pub enum RegisterClass { @@ -92,7 +99,8 @@ pub enum RegisterSize { impl RegisterSize { /// The register class implied by this width. `W32`/`X64` live in /// the general-purpose bank; `S32` is a floating-point register. - /// Used by asmt-4's register allocator to bucket vregs. + /// Consumed by the register allocator's class bucketing + /// (asmt-4.md §3.4). #[allow(dead_code)] pub fn class(&self) -> RegisterClass { match self { @@ -124,7 +132,7 @@ impl TryFrom<&ir::Dtype> for RegisterSize { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum BinOp { +pub enum InstBinOp { Add, Sub, Mul, @@ -132,10 +140,9 @@ pub enum BinOp { } /// Single-precision floating-point binary operators corresponding 1:1 -/// to the aarch64 `fadd`/`fsub`/`fmul`/`fdiv` instructions. The -/// variants are unused at the asmt-4 skeleton stage; asmt-4's solution -/// produces them from the IR's `FBiOpStmt`. The `F` prefix mirrors the -/// aarch64 mnemonic family and is preserved deliberately. +/// to the aarch64 `fadd`/`fsub`/`fmul`/`fdiv` instructions. Unused by +/// the skeleton; asmt-4's solution produces them from the IR's +/// `FBiOpStmt`. The `F` prefix mirrors the aarch64 mnemonic family. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[allow(dead_code, clippy::enum_variant_names)] pub enum FBinOp { @@ -155,8 +162,10 @@ pub enum Cond { Ge, } +/// One operand of an integer instruction: a register or a 64-bit +/// immediate. Also serves as a `Gep` index. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Operand { +pub enum InstOperand { Register(Register), Immediate(i64), } @@ -166,9 +175,3 @@ pub enum Addr { BaseOff { base: Register, offset: i64 }, Global(String), } - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum IndexOperand { - Reg(Register), - Imm(i64), -} diff --git a/src/asm/common.rs b/src/asm/common.rs index f9ee67e..28820f3 100644 --- a/src/asm/common.rs +++ b/src/asm/common.rs @@ -1,3 +1,6 @@ +//! Target-independent support utilities for the assembly backend: +//! data-type layout computation and virtual stack-frame management. + mod layout; mod stack; diff --git a/src/asm/common/layout.rs b/src/asm/common/layout.rs index fb2c9f9..12b8f04 100644 --- a/src/asm/common/layout.rs +++ b/src/asm/common/layout.rs @@ -1,3 +1,6 @@ +//! Size and alignment computation for IR data types, including struct +//! field offsets, following the layout rules the backend emits code for. + use crate::asm::error::Error; use crate::ir; use std::collections::HashMap; diff --git a/src/asm/common/stack.rs b/src/asm/common/stack.rs index 27bdb44..6ce1aa1 100644 --- a/src/asm/common/stack.rs +++ b/src/asm/common/stack.rs @@ -1,3 +1,7 @@ +//! Virtual stack-frame builder: assigns alloca and register-spill slots +//! at negative offsets from the frame pointer and tracks the total size +//! as the frame grows. + use super::{align_up, StructLayouts}; use crate::asm::error::Error; use crate::ir; diff --git a/src/asm/error.rs b/src/asm/error.rs index 5cbb5bf..6ad921b 100644 --- a/src/asm/error.rs +++ b/src/asm/error.rs @@ -1,3 +1,7 @@ +//! Error type for the assembly backend: unsupported IR constructs, +//! missing layout or condition information, and internal invariant +//! violations raised during lowering or register allocation. + use thiserror::Error; use crate::ir;