mod emit;
mod expr;
mod frame;
mod labels;
mod print;
mod stmt;
use crate::errors::QalaError;
use crate::span::LineIndex;
use crate::typed_ast::{TypedAst, TypedFnDecl, TypedItem};
use crate::types::QalaType;
use std::collections::HashSet;
use emit::Asm;
use frame::FrameLayout;
use labels::LabelGen;
use stmt::LoopLabels;
pub fn compile_arm64(ast: &TypedAst, src: &str) -> Result<String, Vec<QalaError>> {
let mut backend = Arm64Backend::new(src);
backend.scan_fn_names(ast);
for item in ast {
if let Err(e) = backend.compile_item(item) {
backend.errors.push(e);
}
}
if !backend.errors.is_empty() {
backend
.errors
.sort_by_key(|e| (e.span().start, e.span().len));
return Err(backend.errors);
}
Ok(backend.finish())
}
struct Arm64Backend {
asm: Asm,
labels: LabelGen,
frame: Option<FrameLayout>,
current_fn: Option<String>,
scopes: Vec<Vec<(String, i64)>>,
loops: Vec<LoopLabels>,
binding_cursor: usize,
fn_names: HashSet<String>,
errors: Vec<QalaError>,
#[allow(dead_code)]
src: String,
#[allow(dead_code)]
line_index: LineIndex,
}
impl Arm64Backend {
fn new(src: &str) -> Self {
Arm64Backend {
asm: Asm::new(),
labels: LabelGen::new(),
frame: None,
current_fn: None,
scopes: Vec::new(),
loops: Vec::new(),
binding_cursor: 0,
fn_names: HashSet::new(),
errors: Vec::new(),
src: src.to_string(),
line_index: LineIndex::new(src),
}
}
fn scan_fn_names(&mut self, ast: &TypedAst) {
for item in ast {
if let TypedItem::Fn(decl) = item {
self.fn_names.insert(decl.name.clone());
}
}
}
fn frame(&self) -> &FrameLayout {
self.frame
.as_ref()
.expect("arm64: frame accessed with no function in progress")
}
fn frame_mut(&mut self) -> &mut FrameLayout {
self.frame
.as_mut()
.expect("arm64: frame accessed with no function in progress")
}
fn compile_item(&mut self, item: &TypedItem) -> Result<(), QalaError> {
match item {
TypedItem::Fn(decl) => self.compile_fn(decl),
TypedItem::Struct(_) | TypedItem::Enum(_) | TypedItem::Interface(_) => Ok(()),
}
}
fn compile_fn(&mut self, decl: &TypedFnDecl) -> Result<(), QalaError> {
self.validate_fn(decl)?;
self.frame = Some(FrameLayout::plan_frame(decl, &self.fn_names));
self.current_fn = Some(decl.name.clone());
self.binding_cursor = 0;
self.scopes.clear();
self.loops.clear();
self.asm.emit_line("");
self.asm.emit_insn(".balign 4");
self.asm.emit_insn(&format!(".global {}", decl.name));
self.asm
.emit_insn(&format!(".type {}, @function", decl.name));
self.asm.emit_label(&decl.name);
for insn in self.frame().prologue() {
self.asm.emit_insn(&insn);
}
for (i, param) in decl.params.iter().enumerate() {
let slot = self
.frame()
.slot_of(¶m.name)
.ok_or_else(|| QalaError::Type {
span: param.span,
message: format!("arm64 backend: parameter `{}` has no slot", param.name),
})?;
self.asm
.emit_insn_commented(&format!("str x{i}, [fp, {slot}]"), ¶m.name);
}
self.compile_block(&decl.body)?;
let epilogue_label = self.epilogue_label(&decl.name);
self.asm.emit_label(&epilogue_label);
if decl.ret_ty == QalaType::Void {
self.asm
.emit_insn_commented("mov x0, 0", "void return -> exit code 0");
}
for insn in self.frame().epilogue() {
self.asm.emit_insn(&insn);
}
self.frame = None;
self.current_fn = None;
Ok(())
}
fn validate_fn(&self, decl: &TypedFnDecl) -> Result<(), QalaError> {
if decl.type_name.is_some() {
return Err(QalaError::Type {
span: decl.span,
message: "the arm64 backend does not yet support methods".to_string(),
});
}
if decl.params.len() > 8 {
return Err(QalaError::Type {
span: decl.span,
message: "the arm64 backend supports at most 8 parameters".to_string(),
});
}
for param in &decl.params {
if param.default.is_some() {
return Err(QalaError::Type {
span: param.span,
message: "the arm64 backend does not yet support default parameters"
.to_string(),
});
}
if !is_integer_core_type(¶m.ty) {
return Err(QalaError::Type {
span: param.span,
message: format!(
"the arm64 backend does not yet support {} parameters",
type_name(¶m.ty)
),
});
}
}
if decl.ret_ty != QalaType::Void && !is_integer_core_type(&decl.ret_ty) {
return Err(QalaError::Type {
span: decl.span,
message: format!(
"the arm64 backend does not yet support a {} return type",
type_name(&decl.ret_ty)
),
});
}
Ok(())
}
fn epilogue_label(&self, fn_name: &str) -> String {
format!(".L{fn_name}_epilogue")
}
fn finish(self) -> String {
self.asm.finish()
}
}
fn is_integer_core_type(ty: &QalaType) -> bool {
matches!(ty, QalaType::I64 | QalaType::Bool)
}
pub(crate) fn type_name(ty: &QalaType) -> &'static str {
match ty {
QalaType::I64 => "i64",
QalaType::F64 => "f64",
QalaType::Bool => "bool",
QalaType::Str => "str",
QalaType::Byte => "byte",
QalaType::Void => "void",
QalaType::Array(..) => "array",
QalaType::Tuple(_) => "tuple",
QalaType::Function { .. } => "function-typed",
QalaType::Named(_) => "struct or enum",
QalaType::Result(..) => "Result",
QalaType::Option(_) => "Option",
QalaType::FileHandle => "file-handle",
QalaType::Unknown => "unresolved",
}
}
#[cfg(test)]
impl Arm64Backend {
fn begin_function(&mut self, decl: &TypedFnDecl) {
self.frame = Some(FrameLayout::plan_frame(decl, &self.fn_names));
self.current_fn = Some(decl.name.clone());
}
fn take_text(&mut self) -> String {
std::mem::replace(&mut self.asm, Asm::new()).finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lexer::Lexer;
use crate::parser::Parser;
use crate::typechecker::check_program;
fn typecheck(src: &str) -> TypedAst {
let tokens = Lexer::tokenize(src).expect("lex failed");
let ast = Parser::parse(&tokens).expect("parse failed");
let (typed, terrors, _) = check_program(&ast, src);
assert!(terrors.is_empty(), "typecheck errors: {terrors:?}");
typed
}
fn compile_ok(src: &str) -> String {
let typed = typecheck(src);
compile_arm64(&typed, src).unwrap_or_else(|e| panic!("arm64 errors: {e:?}"))
}
fn read_snapshot(name: &str) -> String {
let path = format!("{}/tests/snapshots/{name}", env!("CARGO_MANIFEST_DIR"));
std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("read {path}: {e}"))
.replace("\r\n", "\n")
}
#[test]
fn an_empty_program_emits_just_the_preamble() {
let out = compile_arm64(&Vec::new(), "").expect("empty program compiles");
assert!(out.starts_with("define(fp, x29)\ndefine(lr, x30)\n"));
assert!(out.contains(".text"));
}
#[test]
fn a_function_emits_its_directives_and_prologue() {
let out = compile_ok("fn main() { }");
assert!(out.contains(".balign 4"), "{out}");
assert!(out.contains(".global main"), "{out}");
assert!(out.contains(".type main, @function"), "{out}");
assert!(out.contains("\nmain:\n"), "{out}");
assert!(
out.contains("stp fp, lr, [sp, "),
"missing prologue: {out}"
);
assert!(out.contains("mov fp, sp"), "{out}");
}
#[test]
fn a_function_emits_one_epilogue_block() {
let out = compile_ok("fn main() { }");
assert!(out.contains(".Lmain_epilogue:"), "{out}");
assert!(
out.contains("ldp fp, lr, [sp], "),
"missing epilogue: {out}"
);
assert!(out.contains("\n ret\n"), "{out}");
}
#[test]
fn a_void_function_zeroes_x0_before_the_epilogue() {
let out = compile_ok("fn main() { }");
assert!(out.contains("mov x0, 0"), "{out}");
}
#[test]
fn parameters_are_spilled_into_their_stack_slots() {
let out = compile_ok("fn add(a: i64, b: i64) -> i64 { a + b }");
assert!(out.contains("str x0, [fp, 16] // a"), "{out}");
assert!(out.contains("str x1, [fp, 24] // b"), "{out}");
}
#[test]
fn a_returning_function_leaves_its_value_in_x0() {
let out = compile_ok("fn seven() -> i64 { 7 }");
assert!(out.contains("mov x0, 7"), "{out}");
assert!(
!out.contains("mov x0, 0"),
"an i64 function must not zero x0: {out}"
);
}
#[test]
fn an_explicit_return_branches_to_the_epilogue() {
let out = compile_ok("fn f() -> i64 { return 3 }");
assert!(out.contains("b .Lf_epilogue"), "{out}");
}
#[test]
fn structs_and_enums_emit_nothing() {
let out = compile_ok("struct P { x: i64 }\nenum E { A, B }\nfn main() { }");
assert!(out.contains("\nmain:\n"));
assert!(!out.contains("\nP:\n"));
assert!(!out.contains("\nE:\n"));
}
#[test]
fn a_method_is_rejected_cleanly() {
let typed = typecheck("struct P { x: i64 }\nfn P.get(self) -> i64 { self.x }");
let err = compile_arm64(
&typed,
"struct P { x: i64 }\nfn P.get(self) -> i64 { self.x }",
)
.expect_err("a method must be rejected");
assert!(err[0].message().contains("method"), "{:?}", err);
}
#[test]
fn a_float_parameter_is_rejected_cleanly() {
let src = "fn f(x: f64) -> f64 { x }";
let typed = typecheck(src);
let err = compile_arm64(&typed, src).expect_err("a float param must be rejected");
assert!(err[0].message().contains("f64"), "{:?}", err);
}
#[test]
fn more_than_eight_parameters_is_rejected_cleanly() {
let src = "fn f(a: i64, b: i64, c: i64, d: i64, e: i64, g: i64, h: i64, i: i64, j: i64) -> i64 { a }";
let typed = typecheck(src);
let err = compile_arm64(&typed, src).expect_err(">8 params must be rejected");
assert!(
err[0].message().contains("at most 8 parameters"),
"{:?}",
err
);
}
#[test]
fn an_unsupported_statement_is_rejected_cleanly() {
let src = "fn f() { let x = 1\ndefer x }";
let typed = typecheck(src);
let err = compile_arm64(&typed, src).expect_err("a defer must be rejected");
assert!(err[0].message().contains("defer"), "{:?}", err);
}
#[test]
fn emission_is_deterministic() {
let src = "fn f(a: i64, b: i64) -> bool { (a + b) > 0 && a != b }";
let first = compile_ok(src);
let second = compile_ok(src);
assert_eq!(first, second, "arm64 emission must be deterministic");
}
#[test]
fn arithmetic_matches_the_snapshot() {
let src = "fn arith(a: i64, b: i64) -> i64 { (a + b) * (a - b) / 2 % 7 }";
let emitted = compile_ok(src);
assert_eq!(
emitted,
read_snapshot("arm64_arithmetic.s"),
"arm64 arithmetic emission drifted from snapshot"
);
}
#[test]
fn comparisons_match_the_snapshot() {
let src = "fn cmp(a: i64, b: i64) -> bool { a == b }\n\
fn lt(a: i64, b: i64) -> bool { a < b }\n\
fn ge(a: i64, b: i64) -> bool { a >= b }";
let emitted = compile_ok(src);
assert_eq!(
emitted,
read_snapshot("arm64_comparisons.s"),
"arm64 comparison emission drifted from snapshot"
);
}
#[test]
fn booleans_match_the_snapshot() {
let src = "fn andor(a: bool, b: bool) -> bool { a && b || !a }";
let emitted = compile_ok(src);
assert_eq!(
emitted,
read_snapshot("arm64_booleans.s"),
"arm64 boolean emission drifted from snapshot"
);
}
#[test]
fn a_function_call_matches_the_snapshot() {
let src = "fn add3(a: i64, b: i64, c: i64) -> i64 {\n\
\x20 let sum = a + b\n\
\x20 sum + c\n\
}\n\
fn main() {\n\
\x20 let r = add3(10, 20, 12)\n\
}\n";
let emitted = compile_ok(src);
assert_eq!(
emitted,
read_snapshot("arm64_function_call.s"),
"arm64 function-call emission drifted from snapshot"
);
}
#[test]
fn recursion_matches_the_snapshot() {
let src = "fn factorial(n: i64) -> i64 {\n\
\x20 if n <= 1 { return 1 }\n\
\x20 n * factorial(n - 1)\n\
}\n\
fn main() {\n\
\x20 let f = factorial(5)\n\
}\n";
let emitted = compile_ok(src);
assert_eq!(
emitted,
read_snapshot("arm64_recursion.s"),
"arm64 recursion emission drifted from snapshot"
);
}
#[test]
fn an_unsupported_construct_matches_the_error_snapshot() {
use crate::diagnostics::Diagnostic;
let src = "fn main() {\n let x = 1.5\n}\n";
let typed = typecheck(src);
let errors = compile_arm64(&typed, src)
.expect_err("a float program must be rejected by the backend");
let rendered = errors
.iter()
.map(|e| Diagnostic::from(e.clone()).render(src))
.collect::<Vec<_>>()
.join("\n");
assert_eq!(
rendered,
read_snapshot("arm64_unsupported.txt"),
"arm64 unsupported-construct rejection drifted from snapshot"
);
}
#[test]
fn every_unsupported_construct_is_rejected_with_a_specific_named_diagnostic() {
let cases: [(&str, &str); 11] = [
("fn f() -> i64 { let x = 1.5\n0 }", "floats"),
("fn f() -> i64 { let s = \"hi\"\n0 }", "strings"),
("fn f() -> i64 { let b = b'a'\n0 }", "byte values"),
("fn f() -> i64 { let a = [1, 2, 3]\n0 }", "arrays"),
("fn f() -> i64 { let t = (1, 2)\n0 }", "tuples"),
(
"struct P { x: i64 }\nfn f() -> i64 { let p = P { x: 1 }\n0 }",
"struct literals",
),
(
"fn f(n: i64) -> i64 { match n { _ => 0 } }",
"match expressions",
),
("fn f() -> i64 { comptime { 1 + 1 } }", "comptime blocks"),
("fn f() { let x = 1\ndefer x }", "defer"),
("fn f(x: f64) -> f64 { x }", "f64 parameters"),
(
"struct P { x: i64 }\nfn P.get(self) -> i64 { self.x }",
"method",
),
];
for (src, needle) in cases {
let typed = typecheck(src);
let errors = compile_arm64(&typed, src)
.expect_err("the program must be rejected by the backend");
let message = errors[0].message();
assert!(
message.contains(needle),
"the rejection of `{src}` must name `{needle}`: got {message:?}"
);
assert!(
message.contains("arm64"),
"the rejection of `{src}` must use the lowercase `arm64` spelling: \
got {message:?}"
);
}
}
}