Refactor x86_64 backend and add first assembly methods

This commit is contained in:
Brendan Hansknecht 2020-11-15 20:43:14 -08:00
parent a8986087f9
commit 0fcb1df344
3 changed files with 130 additions and 40 deletions

View file

@ -11,7 +11,7 @@
// re-enable this when working on performance optimizations than have it block PRs. // re-enable this when working on performance optimizations than have it block PRs.
#![allow(clippy::large_enum_variant)] #![allow(clippy::large_enum_variant)]
use bumpalo::{collections::Vec, Bump}; use bumpalo::Bump;
use object::write::Object; use object::write::Object;
use roc_collections::all::{MutMap, MutSet}; use roc_collections::all::{MutMap, MutSet};
use roc_module::symbol::{Interns, Symbol}; use roc_module::symbol::{Interns, Symbol};
@ -59,26 +59,25 @@ trait Backend<'a> {
fn build_proc(&mut self, proc: Proc<'a>) -> &'a [u8] { fn build_proc(&mut self, proc: Proc<'a>) -> &'a [u8] {
self.reset(); self.reset();
// TODO: let the backend know of all the arguments. // TODO: let the backend know of all the arguments.
let mut buf = bumpalo::vec!(in self.env().arena); self.build_stmt(&proc.body);
self.build_stmt(&mut buf, &proc.body); self.finalize()
self.wrap_proc(buf).into_bump_slice()
} }
/// wrap_proc does any setup and cleanup that should happen around the procedure. /// finalize does any setup and cleanup that should happen around the procedure.
/// finalize does setup because things like stack size and jump locations are not know until the function is written.
/// For example, this can store the frame pionter and setup stack space. /// For example, this can store the frame pionter and setup stack space.
/// wrap_proc is run at the end of build_proc when all internal code is finalized. /// finalize is run at the end of build_proc when all internal code is finalized.
/// wrap_proc returns a Vec because it is expected to prepend data. fn finalize(&mut self) -> &'a [u8];
fn wrap_proc(&mut self, buf: Vec<'a, u8>) -> Vec<'a, u8>;
/// build_stmt builds a statement and outputs at the end of the buffer. /// build_stmt builds a statement and outputs at the end of the buffer.
fn build_stmt(&mut self, buf: &mut Vec<'a, u8>, stmt: &Stmt<'a>) { fn build_stmt(&mut self, stmt: &Stmt<'a>) {
match stmt { match stmt {
Stmt::Let(sym, expr, layout, following) => { Stmt::Let(sym, expr, layout, following) => {
self.build_expr(buf, sym, expr, layout); self.build_expr(sym, expr, layout);
self.build_stmt(buf, following); self.build_stmt(following);
} }
Stmt::Ret(sym) => { Stmt::Ret(sym) => {
self.return_symbol(buf, sym); self.return_symbol(sym);
} }
x => unimplemented!("the statement, {:?}, is not yet implemented", x), x => unimplemented!("the statement, {:?}, is not yet implemented", x),
} }
@ -87,13 +86,7 @@ trait Backend<'a> {
/// build_expr builds the expressions for the specified symbol. /// build_expr builds the expressions for the specified symbol.
/// The builder must keep track of the symbol because it may be refered to later. /// The builder must keep track of the symbol because it may be refered to later.
/// In many cases values can be lazy loaded, like literals. /// In many cases values can be lazy loaded, like literals.
fn build_expr( fn build_expr(&mut self, sym: &Symbol, expr: &Expr<'a>, layout: &Layout<'a>) {
&mut self,
_buf: &mut Vec<'a, u8>,
sym: &Symbol,
expr: &Expr<'a>,
layout: &Layout<'a>,
) {
match expr { match expr {
Expr::Literal(lit) => { Expr::Literal(lit) => {
self.set_symbol_to_lit(sym, lit, layout); self.set_symbol_to_lit(sym, lit, layout);
@ -107,5 +100,5 @@ trait Backend<'a> {
fn set_symbol_to_lit(&mut self, sym: &Symbol, lit: &Literal<'a>, layout: &Layout<'a>); fn set_symbol_to_lit(&mut self, sym: &Symbol, lit: &Literal<'a>, layout: &Layout<'a>);
/// return_symbol moves a symbol to the correct return location for the backend. /// return_symbol moves a symbol to the correct return location for the backend.
fn return_symbol(&mut self, buf: &mut Vec<'a, u8>, sym: &Symbol); fn return_symbol(&mut self, sym: &Symbol);
} }

View file

@ -0,0 +1,70 @@
use bumpalo::collections::Vec;
// Not sure exactly how I want to represent registers.
// If we want max speed, we would likely make them structs that impl the same trait to avoid ifs.
#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Register {
RAX,
RCX,
RDX,
RBX,
RSP,
RBP,
RSI,
RDI,
R8,
R9,
R10,
R11,
R12,
R13,
R14,
R15,
}
const REX_W: u8 = 0x48;
fn add_rm_extension(reg: Register, byte: u8) -> u8 {
if reg as u8 > 7 {
byte + 1
} else {
byte
}
}
fn add_reg_extension(reg: Register, byte: u8) -> u8 {
if reg as u8 > 7 {
byte + 4
} else {
byte
}
}
/// Below here are the functions for all of the assembly instructions.
/// Their names are based on the instruction and operators combined.
/// Please call buf.reserve if you push or extend more than once.
/// Also, please keep these in alphanumeric order.
pub fn mov_register64bit_immediate32bit<'a>(buf: &mut Vec<'a, u8>, dst: Register, imm: i32) {
let rex = add_rm_extension(dst, REX_W);
let dst_mod = dst as u8 % 8;
buf.reserve(7);
buf.extend(&[rex, 0xC7, 0xC0 + dst_mod]);
buf.extend(&imm.to_le_bytes());
}
pub fn mov_register64bit_immediate64bit<'a>(buf: &mut Vec<'a, u8>, dst: Register, imm: i64) {
let rex = add_rm_extension(dst, REX_W);
let dst_mod = dst as u8 % 8;
buf.reserve(10);
buf.extend(&[rex, 0xB8 + dst_mod]);
buf.extend(&imm.to_le_bytes());
}
pub fn mov_register64bit_register64bit<'a>(buf: &mut Vec<'a, u8>, dst: Register, src: Register) {
let rex = add_rm_extension(dst, REX_W);
let rex = add_reg_extension(src, rex);
let dst_mod = dst as u8 % 8;
let src_mod = (src as u8 % 8) << 3;
buf.extend(&[rex, 0x89, 0xC0 + dst_mod + src_mod]);
}

View file

@ -5,8 +5,19 @@ use roc_module::symbol::Symbol;
use roc_mono::ir::Literal; use roc_mono::ir::Literal;
use roc_mono::layout::Layout; use roc_mono::layout::Layout;
mod asm;
use asm::Register;
const RETURN_REG: Register = Register::RAX;
pub struct X86_64Backend<'a> { pub struct X86_64Backend<'a> {
env: &'a Env<'a>, env: &'a Env<'a>,
buf: Vec<'a, u8>,
/// leaf_proc is true if the only calls this function makes are tail calls.
/// If that is the case, we can skip emitting the frame pointer and updating the stack.
leaf_proc: bool,
// This will need to hold info a symbol is held in a register or on the stack as well. // This will need to hold info a symbol is held in a register or on the stack as well.
symbols_map: MutMap<Symbol, (Literal<'a>, Layout<'a>)>, symbols_map: MutMap<Symbol, (Literal<'a>, Layout<'a>)>,
@ -27,6 +38,8 @@ impl<'a> Backend<'a> for X86_64Backend<'a> {
fn new(env: &'a Env) -> Self { fn new(env: &'a Env) -> Self {
X86_64Backend { X86_64Backend {
env, env,
leaf_proc: true,
buf: bumpalo::vec!(in env.arena),
symbols_map: MutMap::default(), symbols_map: MutMap::default(),
} }
} }
@ -37,40 +50,54 @@ impl<'a> Backend<'a> for X86_64Backend<'a> {
fn reset(&mut self) { fn reset(&mut self) {
self.symbols_map.clear(); self.symbols_map.clear();
self.buf.clear();
} }
fn wrap_proc(&mut self, body: Vec<'a, u8>) -> Vec<'a, u8> { fn finalize(&mut self) -> &'a [u8] {
// push rbp (0x55)
// mov rbp, rsp (0x48, 0x89, 0xE5)
let mut out = bumpalo::vec![in self.env.arena; 0x55, 0x48, 0x89, 0xE5];
out.reserve(body.len() + 2);
// TODO: handle allocating and cleaning up data on the stack. // TODO: handle allocating and cleaning up data on the stack.
let mut out = bumpalo::vec![in self.env.arena];
if !self.leaf_proc {
// push rbp (0x55)
// mov rbp, rsp (0x48, 0x89, 0xE5)
out.extend(&[0x55]);
asm::mov_register64bit_register64bit(&mut out, Register::RBP, Register::RSP)
}
out.extend(&self.buf);
out.extend(body); if !self.leaf_proc {
// pop rbp
// pop rbp out.push(0x5D);
out.push(0x5D); }
// ret // ret
out.push(0xC3); out.push(0xC3);
out out.into_bump_slice()
} }
fn set_symbol_to_lit(&mut self, sym: &Symbol, lit: &Literal<'a>, layout: &Layout<'a>) { fn set_symbol_to_lit(&mut self, sym: &Symbol, lit: &Literal<'a>, layout: &Layout<'a>) {
self.symbols_map self.symbols_map.insert(*sym, (lit.clone(), layout.clone()));
.insert(sym.clone(), (lit.clone(), layout.clone()));
} }
fn return_symbol(&mut self, body: &mut Vec<'a, u8>, sym: &Symbol) { fn return_symbol(&mut self, sym: &Symbol) {
//let body = bumpalo::vec![in env.arena; 0xb8, 0x06, 0x00, 0x00, 0x00]; self.load_symbol(RETURN_REG, sym);
match self.symbols_map.get(sym) { }
}
/// This impl block is for ir related instructions that need backend specific information.
/// For example, loading a symbol for doing a computation.
impl<'a> X86_64Backend<'a> {
fn load_symbol(&mut self, dst: Register, sym: &Symbol) {
let val = self.symbols_map.get(sym);
match val {
Some((Literal::Int(x), _)) => { Some((Literal::Int(x), _)) => {
// movabs rax, ... let val = *x;
body.extend(&[0x48, 0xB8]); if val <= i32::MAX as i64 && val >= i32::MIN as i64 {
body.extend(&x.to_le_bytes()); asm::mov_register64bit_immediate32bit(&mut self.buf, dst, val as i32);
} else {
asm::mov_register64bit_immediate64bit(&mut self.buf, dst, val);
}
} }
Some(x) => unimplemented!("return value, {:?}, is not yet implemented", x), Some(x) => unimplemented!("symbol, {:?}, is not yet implemented", x),
None => panic!("Unknown return symbol: {}", sym), None => panic!("Unknown return symbol: {}", sym),
} }
} }