use crate::ast::{BinOp, UnaryOp};
use crate::errors::QalaError;
use crate::span::Span;
use crate::typed_ast::TypedExpr;
use super::Arm64Backend;
pub(super) const MAX_CALL_ARGS: usize = 8;
const MOV_IMM_MAX: i64 = 65535;
impl Arm64Backend {
pub(super) fn compile_expr(&mut self, expr: &TypedExpr) -> Result<(), QalaError> {
match expr {
TypedExpr::Int { value, .. } => {
self.emit_int_literal(*value);
Ok(())
}
TypedExpr::Bool { value, .. } => {
self.asm.emit_insn(if *value {
"mov x0, 1"
} else {
"mov x0, 0"
});
Ok(())
}
TypedExpr::Ident { name, span, .. } => {
let slot = self.resolve_name(name).ok_or_else(|| QalaError::Type {
span: *span,
message: format!("arm64 backend: name `{name}` has no stack slot"),
})?;
self.asm
.emit_insn_commented(&format!("ldr x0, [fp, {slot}]"), name);
Ok(())
}
TypedExpr::Paren { inner, .. } => {
self.compile_expr(inner)
}
TypedExpr::Unary { op, operand, .. } => self.compile_unary(op, operand),
TypedExpr::Binary { op, lhs, rhs, .. } => self.compile_binary(op, lhs, rhs),
TypedExpr::Block { block, .. } => self.compile_block(block),
TypedExpr::Call {
callee, args, span, ..
} => self.compile_call(callee, args, *span),
TypedExpr::Range { span, .. } => Err(QalaError::Type {
span: *span,
message: "the arm64 backend does not yet support ranges".to_string(),
}),
_ => Err(QalaError::Type {
span: expr.span(),
message: format!(
"the arm64 backend does not yet support {}",
unsupported_expr_name(expr)
),
}),
}
}
fn emit_int_literal(&mut self, value: i64) {
if (0..=MOV_IMM_MAX).contains(&value) {
self.asm.emit_insn(&format!("mov x0, {value}"));
} else {
self.asm.emit_insn(&format!("ldr x0, ={value}"));
}
}
fn compile_unary(&mut self, op: &UnaryOp, operand: &TypedExpr) -> Result<(), QalaError> {
self.compile_expr(operand)?;
match op {
UnaryOp::Not => self.asm.emit_insn("eor x0, x0, 1"),
UnaryOp::Neg => self.asm.emit_insn("neg x0, x0"),
}
Ok(())
}
fn compile_binary(
&mut self,
op: &BinOp,
lhs: &TypedExpr,
rhs: &TypedExpr,
) -> Result<(), QalaError> {
match op {
BinOp::And => self.compile_short_circuit(lhs, rhs, ShortCircuit::And),
BinOp::Or => self.compile_short_circuit(lhs, rhs, ShortCircuit::Or),
_ => self.compile_spilled_binary(op, lhs, rhs),
}
}
fn compile_spilled_binary(
&mut self,
op: &BinOp,
lhs: &TypedExpr,
rhs: &TypedExpr,
) -> Result<(), QalaError> {
let scratch = self.frame_mut().claim_scratch();
self.compile_expr(lhs)?;
self.asm
.emit_insn_commented(&format!("str x0, [fp, {scratch}]"), "spill lhs");
self.compile_expr(rhs)?;
self.asm
.emit_insn_commented(&format!("ldr x9, [fp, {scratch}]"), "reload lhs");
self.frame_mut().release_scratch();
self.emit_binop(op);
Ok(())
}
fn emit_binop(&mut self, op: &BinOp) {
match op {
BinOp::Add => self.asm.emit_insn("add x0, x9, x0"),
BinOp::Sub => self.asm.emit_insn("sub x0, x9, x0"),
BinOp::Mul => self.asm.emit_insn("mul x0, x9, x0"),
BinOp::Div => self.asm.emit_insn("sdiv x0, x9, x0"),
BinOp::Rem => {
self.asm.emit_insn("sdiv x10, x9, x0");
self.asm.emit_insn("msub x0, x10, x0, x9");
}
BinOp::Eq => {
self.asm.emit_insn("cmp x9, x0");
self.asm.emit_insn("cset x0, eq");
}
BinOp::Ne => {
self.asm.emit_insn("cmp x9, x0");
self.asm.emit_insn("cset x0, ne");
}
BinOp::Lt => {
self.asm.emit_insn("cmp x9, x0");
self.asm.emit_insn("cset x0, lt");
}
BinOp::Le => {
self.asm.emit_insn("cmp x9, x0");
self.asm.emit_insn("cset x0, le");
}
BinOp::Gt => {
self.asm.emit_insn("cmp x9, x0");
self.asm.emit_insn("cset x0, gt");
}
BinOp::Ge => {
self.asm.emit_insn("cmp x9, x0");
self.asm.emit_insn("cset x0, ge");
}
BinOp::And | BinOp::Or => {}
}
}
fn compile_short_circuit(
&mut self,
lhs: &TypedExpr,
rhs: &TypedExpr,
kind: ShortCircuit,
) -> Result<(), QalaError> {
let settle = self.labels.fresh(kind.settle_prefix());
let done = self.labels.fresh(kind.done_prefix());
let branch = kind.branch_insn();
self.compile_expr(lhs)?;
self.asm.emit_insn(&format!("{branch} x0, {settle}"));
self.compile_expr(rhs)?;
self.asm.emit_insn(&format!("{branch} x0, {settle}"));
self.asm
.emit_insn(&format!("mov x0, {}", kind.fallthrough_value()));
self.asm.emit_insn(&format!("b {done}"));
self.asm.emit_label(&settle);
self.asm
.emit_insn(&format!("mov x0, {}", kind.settle_value()));
self.asm.emit_label(&done);
Ok(())
}
fn compile_call(
&mut self,
callee: &TypedExpr,
args: &[TypedExpr],
span: Span,
) -> Result<(), QalaError> {
let name = match callee {
TypedExpr::Ident { name, .. } => name,
_ => {
return Err(QalaError::Type {
span,
message: "the arm64 backend does not yet support computed callees".to_string(),
});
}
};
if !self.fn_names.contains(name) {
if name == "print" || name == "println" {
return self.compile_print_call(name, args, span);
}
return Err(QalaError::Type {
span,
message: format!("the arm64 backend does not yet support the `{name}` function"),
});
}
if args.len() > MAX_CALL_ARGS {
return Err(QalaError::Type {
span,
message: "the arm64 backend supports at most 8 arguments".to_string(),
});
}
let mut arg_offsets = Vec::with_capacity(args.len());
for (i, arg) in args.iter().enumerate() {
self.compile_expr(arg)?;
let slot = self.frame_mut().claim_scratch();
self.asm
.emit_insn_commented(&format!("str x0, [fp, {slot}]"), &format!("arg {i}"));
arg_offsets.push(slot);
}
for (i, slot) in arg_offsets.iter().enumerate() {
self.asm.emit_insn(&format!("ldr x{i}, [fp, {slot}]"));
}
self.asm.emit_insn(&format!("bl {name}"));
for _ in &arg_offsets {
self.frame_mut().release_scratch();
}
Ok(())
}
}
#[derive(Clone, Copy)]
enum ShortCircuit {
And,
Or,
}
impl ShortCircuit {
fn branch_insn(self) -> &'static str {
match self {
ShortCircuit::And => "cbz ",
ShortCircuit::Or => "cbnz",
}
}
fn settle_prefix(self) -> &'static str {
match self {
ShortCircuit::And => "and_false",
ShortCircuit::Or => "or_true",
}
}
fn done_prefix(self) -> &'static str {
match self {
ShortCircuit::And => "and_done",
ShortCircuit::Or => "or_done",
}
}
fn settle_value(self) -> u8 {
match self {
ShortCircuit::And => 0,
ShortCircuit::Or => 1,
}
}
fn fallthrough_value(self) -> u8 {
match self {
ShortCircuit::And => 1,
ShortCircuit::Or => 0,
}
}
}
fn unsupported_expr_name(expr: &TypedExpr) -> &'static str {
match expr {
TypedExpr::Float { .. } => "floats",
TypedExpr::Byte { .. } => "byte values",
TypedExpr::Str { .. } => "strings",
TypedExpr::Tuple { .. } => "tuples",
TypedExpr::ArrayLit { .. } | TypedExpr::ArrayRepeat { .. } => "arrays",
TypedExpr::StructLit { .. } => "struct literals",
TypedExpr::FieldAccess { .. } => "field access",
TypedExpr::MethodCall { .. } => "method calls",
TypedExpr::Index { .. } => "indexing",
TypedExpr::Try { .. } => "the `?` operator",
TypedExpr::Pipeline { .. } => "the pipeline operator",
TypedExpr::Comptime { .. } => "comptime blocks",
TypedExpr::Match { .. } => "match expressions",
TypedExpr::OrElse { .. } => "the `or` fallback",
TypedExpr::Interpolation { .. } => "string interpolation",
TypedExpr::Int { .. }
| TypedExpr::Bool { .. }
| TypedExpr::Ident { .. }
| TypedExpr::Paren { .. }
| TypedExpr::Unary { .. }
| TypedExpr::Binary { .. }
| TypedExpr::Block { .. }
| TypedExpr::Call { .. }
| TypedExpr::Range { .. } => {
"this construct (arm64 backend bug: a \
supported expression reached the unsupported-construct path)"
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lexer::Lexer;
use crate::parser::Parser;
use crate::typechecker::check_program;
use crate::typed_ast::TypedItem;
fn trailing_expr(src: &str, fn_name: &str) -> TypedExpr {
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:?}");
let decl = typed
.iter()
.find_map(|item| match item {
TypedItem::Fn(d) if d.name == fn_name => Some(d.clone()),
_ => None,
})
.unwrap_or_else(|| panic!("function `{fn_name}` not found"));
*decl
.body
.value
.expect("the test function has no trailing value")
}
fn emit_expr(src: &str, fn_name: &str) -> String {
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:?}");
let decl = typed
.iter()
.find_map(|item| match item {
TypedItem::Fn(d) if d.name == fn_name => Some(d.clone()),
_ => None,
})
.unwrap_or_else(|| panic!("function `{fn_name}` not found"));
let mut backend = Arm64Backend::new(src);
backend.begin_function(&decl);
let expr = decl.body.value.clone().expect("no trailing value");
backend.compile_expr(&expr).expect("compile_expr failed");
backend.take_text()
}
#[test]
fn an_integer_literal_emits_a_mov() {
let asm = emit_expr("fn f() -> i64 { 42 }", "f");
assert!(asm.contains("mov x0, 42"), "{asm}");
}
#[test]
fn a_large_integer_literal_uses_the_literal_pool() {
let asm = emit_expr("fn f() -> i64 { 100000 }", "f");
assert!(asm.contains("ldr x0, =100000"), "{asm}");
assert!(!asm.contains("mov x0, 100000"), "{asm}");
}
#[test]
fn the_i64_min_magnitude_is_rejected_at_the_lexer_before_the_backend() {
let over = i64::MAX as u64 + 1; let src = format!("fn f() -> i64 {{ -{over} }}");
let lexed = Lexer::tokenize(&src);
assert!(
lexed.is_err(),
"the i64::MIN magnitude must be rejected by the lexer, not lowered"
);
}
#[test]
fn an_extreme_negative_literal_round_trips_through_the_backend() {
let src = format!("fn f() -> i64 {{ -{} }}", i64::MAX);
let asm = emit_expr(&src, "f");
assert!(
asm.contains(&format!("ldr x0, ={}", i64::MAX)),
"the i64::MAX magnitude must use the literal pool: {asm}"
);
assert!(
asm.contains("neg x0, x0"),
"the unary minus must negate: {asm}"
);
}
#[test]
fn a_bool_literal_emits_one_or_zero() {
assert!(emit_expr("fn f() -> bool { true }", "f").contains("mov x0, 1"));
assert!(emit_expr("fn f() -> bool { false }", "f").contains("mov x0, 0"));
}
#[test]
fn an_ident_loads_its_parameter_slot() {
let asm = emit_expr("fn f(a: i64) -> i64 { a }", "f");
assert!(asm.contains("ldr x0, [fp, 16]"), "{asm}");
}
#[test]
fn paren_is_transparent() {
let asm = emit_expr("fn f() -> i64 { (7) }", "f");
assert!(asm.contains("mov x0, 7"), "{asm}");
}
#[test]
fn addition_emits_the_spill_discipline_and_add() {
let asm = emit_expr("fn f() -> i64 { 1 + 2 }", "f");
assert!(asm.contains("str x0, [fp, "), "missing spill: {asm}");
assert!(asm.contains("ldr x9, [fp, "), "missing reload: {asm}");
assert!(asm.contains("add x0, x9, x0"), "missing add: {asm}");
}
#[test]
fn subtraction_and_multiplication_emit_sub_and_mul() {
assert!(emit_expr("fn f() -> i64 { 5 - 3 }", "f").contains("sub x0, x9, x0"));
assert!(emit_expr("fn f() -> i64 { 5 * 3 }", "f").contains("mul x0, x9, x0"));
}
#[test]
fn division_emits_signed_sdiv() {
let asm = emit_expr("fn f() -> i64 { 9 / 3 }", "f");
assert!(asm.contains("sdiv x0, x9, x0"), "{asm}");
assert!(!asm.contains("udiv"), "i64 division must be signed: {asm}");
}
#[test]
fn modulo_emits_the_sdiv_msub_idiom() {
let asm = emit_expr("fn f() -> i64 { 9 % 4 }", "f");
assert!(
asm.contains("sdiv x10, x9, x0"),
"missing quotient: {asm}"
);
assert!(
asm.contains("msub x0, x10, x0, x9"),
"missing msub: {asm}"
);
}
#[test]
fn each_comparison_emits_cmp_and_the_signed_condition() {
for (src, cond) in [
("fn f() -> bool { 1 == 2 }", "cset x0, eq"),
("fn f() -> bool { 1 != 2 }", "cset x0, ne"),
("fn f() -> bool { 1 < 2 }", "cset x0, lt"),
("fn f() -> bool { 1 <= 2 }", "cset x0, le"),
("fn f() -> bool { 1 > 2 }", "cset x0, gt"),
("fn f() -> bool { 1 >= 2 }", "cset x0, ge"),
] {
let asm = emit_expr(src, "f");
assert!(
asm.contains("cmp x9, x0"),
"missing cmp for {src}: {asm}"
);
assert!(asm.contains(cond), "missing `{cond}` for {src}: {asm}");
}
}
#[test]
fn comparisons_use_signed_not_unsigned_conditions() {
let asm = emit_expr("fn f() -> bool { 1 < 2 }", "f");
for unsigned in ["lo", "ls", "hi", "hs"] {
assert!(!asm.contains(&format!("cset x0, {unsigned}")), "{asm}");
}
}
#[test]
fn logical_and_emits_the_short_circuit_labels() {
let asm = emit_expr("fn f() -> bool { true && false }", "f");
assert!(asm.contains(".Land_false_"), "missing settle label: {asm}");
assert!(asm.contains(".Land_done_"), "missing done label: {asm}");
assert!(
asm.contains("cbz "),
"&& must short-circuit with cbz: {asm}"
);
}
#[test]
fn logical_or_emits_the_short_circuit_labels() {
let asm = emit_expr("fn f() -> bool { true || false }", "f");
assert!(asm.contains(".Lor_true_"), "missing settle label: {asm}");
assert!(asm.contains(".Lor_done_"), "missing done label: {asm}");
assert!(
asm.contains("cbnz"),
"|| must short-circuit with cbnz: {asm}"
);
}
#[test]
fn not_emits_an_eor() {
let asm = emit_expr("fn f() -> bool { !true }", "f");
assert!(asm.contains("eor x0, x0, 1"), "{asm}");
}
#[test]
fn neg_emits_a_neg() {
let asm = emit_expr("fn f() -> i64 { -5 }", "f");
assert!(asm.contains("neg x0, x0"), "{asm}");
}
#[test]
fn a_nested_expression_claims_distinct_scratch_slots() {
let asm = emit_expr("fn f() -> i64 { (1 + 2) * (3 + 4) }", "f");
let mut spill_slots: Vec<&str> = asm
.lines()
.filter(|l| l.contains("str x0, [fp, ") && l.contains("spill lhs"))
.collect();
spill_slots.sort();
spill_slots.dedup();
assert!(
spill_slots.len() >= 2,
"nested ops must use >= 2 distinct scratch slots: {asm}"
);
assert!(asm.contains("mul x0, x9, x0"), "{asm}");
}
#[test]
fn an_unsupported_construct_returns_an_error_not_a_panic() {
let src = "fn f() -> f64 { 3.5 }";
let expr = trailing_expr(src, "f");
let mut backend = Arm64Backend::new(src);
let err = backend
.compile_expr(&expr)
.expect_err("a float must be rejected");
match err {
QalaError::Type { message, .. } => {
assert!(message.contains("float"), "message: {message}");
}
other => panic!("expected QalaError::Type, got {other:?}"),
}
}
fn compile_program_ok(src: &str) -> String {
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:?}");
super::super::compile_arm64(&typed, src).unwrap_or_else(|e| panic!("arm64 errors: {e:?}"))
}
fn caller_body(asm: &str) -> Vec<String> {
asm.lines()
.skip_while(|l| !l.starts_with("caller:"))
.take_while(|l| !l.trim_start().starts_with(".Lcaller_epilogue"))
.map(|l| l.to_string())
.collect()
}
#[test]
fn a_call_to_a_user_function_emits_argument_spills_loads_and_a_bl() {
let asm = compile_program_ok(
"fn add3(a: i64, b: i64, c: i64) -> i64 { a + b + c }\n\
fn caller() -> i64 { add3(1, 2, 3) }",
);
let body = caller_body(&asm).join("\n");
let spills = body.lines().filter(|l| l.contains("// arg ")).count();
assert_eq!(spills, 3, "three arguments -> three spills: {body}");
assert!(
body.contains("ldr x0, [fp, "),
"missing x0 load: {body}"
);
assert!(
body.contains("ldr x1, [fp, "),
"missing x1 load: {body}"
);
assert!(
body.contains("ldr x2, [fp, "),
"missing x2 load: {body}"
);
assert!(body.contains("bl add3"), "missing the bl: {body}");
}
#[test]
fn a_call_loads_arguments_after_every_spill_not_interleaved() {
let asm = compile_program_ok(
"fn add3(a: i64, b: i64, c: i64) -> i64 { a }\n\
fn caller() -> i64 { add3(1, 2, 3) }",
);
let body = caller_body(&asm);
let last_spill = body
.iter()
.rposition(|l| l.contains("str x0, [fp, ") && l.contains("// arg "))
.expect("no argument spill");
let first_load = body
.iter()
.position(|l| l.contains("ldr x0, [fp, "))
.expect("no x0 load");
assert!(
last_spill < first_load,
"every spill must precede the first load: {body:?}"
);
}
#[test]
fn a_call_with_no_arguments_emits_just_a_bl() {
let asm = compile_program_ok(
"fn answer() -> i64 { 42 }\n\
fn caller() -> i64 { answer() }",
);
let body = caller_body(&asm).join("\n");
assert!(body.contains("bl answer"), "missing the bl: {body}");
assert!(
!body.contains("// arg "),
"a no-arg call spills nothing: {body}"
);
}
#[test]
fn a_nested_call_emits_two_bls_with_the_inner_call_first() {
let asm = compile_program_ok(
"fn inner(n: i64) -> i64 { n + 1 }\n\
fn outer(n: i64) -> i64 { n * 2 }\n\
fn caller() -> i64 { outer(inner(5)) }",
);
let body = caller_body(&asm);
let inner_bl = body
.iter()
.position(|l| l.contains("bl inner"))
.expect("missing bl inner");
let outer_bl = body
.iter()
.position(|l| l.contains("bl outer"))
.expect("missing bl outer");
assert!(
inner_bl < outer_bl,
"the inner call must run first: {body:?}"
);
let outer_spill = body
.iter()
.enumerate()
.find(|(idx, l)| {
*idx > inner_bl && l.contains("str x0, [fp, ") && l.contains("// arg ")
})
.map(|(idx, _)| idx)
.expect("missing the outer-call argument spill after bl inner");
assert!(
outer_spill < outer_bl,
"the inner result is spilled between the two bls: {body:?}"
);
}
#[test]
fn a_call_to_a_stdlib_function_is_rejected_cleanly() {
let src = "fn caller() -> i64 { abs(-1) }";
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:?}");
let err =
super::super::compile_arm64(&typed, src).expect_err("a stdlib call must be rejected");
assert!(
err[0].message().contains("abs"),
"message: {:?}",
err[0].message()
);
}
#[test]
fn a_user_function_named_println_routes_as_a_user_call_not_the_printf_path() {
let asm = compile_program_ok(
"fn println(n: i64) -> i64 { n }\n\
fn main() -> i64 { println(5) }",
);
let body = fn_body(&asm, "main");
assert!(
body.iter().any(|l| l.trim() == "bl println"),
"the shadowed `println` must lower to a `bl println` user call: {body:?}"
);
assert!(
!asm.contains("bl printf"),
"a shadowed `println` must not route to the printf lowering: {asm}"
);
assert!(
body.iter().any(|l| l.contains("// arg 0")),
"the i64 argument must spill as a user-call argument: {body:?}"
);
}
#[test]
fn a_call_with_a_non_ident_callee_is_rejected_cleanly() {
use crate::types::QalaType;
let call = TypedExpr::Call {
callee: Box::new(TypedExpr::Paren {
inner: Box::new(TypedExpr::Int {
value: 0,
ty: QalaType::I64,
span: Span::new(0, 1),
}),
ty: QalaType::I64,
span: Span::new(0, 3),
}),
args: vec![],
ty: QalaType::I64,
span: Span::new(0, 5),
};
let mut backend = Arm64Backend::new("");
let err = backend
.compile_expr(&call)
.expect_err("a computed callee must be rejected");
match err {
QalaError::Type { message, .. } => {
assert!(message.contains("computed callee"), "message: {message}");
}
other => panic!("expected QalaError::Type, got {other:?}"),
}
}
fn fn_body(asm: &str, name: &str) -> Vec<String> {
let label = format!("{name}:");
let epilogue = format!(".L{name}_epilogue");
asm.lines()
.skip_while(|l| l.trim() != label)
.take_while(|l| !l.trim_start().starts_with(&epilogue))
.map(|l| l.to_string())
.collect()
}
fn fp_offset(line: &str) -> Option<i64> {
let start = line.find("[fp, ")? + "[fp, ".len();
let rest = &line[start..];
let end = rest.find(']')?;
rest[..end].trim().parse().ok()
}
#[test]
fn a_nested_call_in_a_later_argument_does_not_clobber_an_earlier_argument() {
let asm = compile_program_ok(
"fn id(x: i64) -> i64 { x }\n\
fn h(a: i64, b: i64) -> i64 { a }\n\
fn f() -> i64 { h(100, id(7)) }",
);
let body = fn_body(&asm, "f");
let mov_100 = body
.iter()
.position(|l| l.trim() == "mov x0, 100")
.expect("missing `mov x0, 100` for the outer call's first argument");
let spill_line = &body[mov_100 + 1];
assert!(
spill_line.contains("str x0, [fp, ") && spill_line.contains("// arg 0"),
"the 100 must be spilled as argument 0 right after the mov: {body:?}"
);
let arg0_slot = fp_offset(spill_line).expect("argument 0 spill has no [fp, N]");
let bl_h = body
.iter()
.position(|l| l.trim() == "bl h")
.expect("missing `bl h`");
let clobber = body[mov_100 + 2..bl_h]
.iter()
.find(|l| l.contains("str ") && fp_offset(l) == Some(arg0_slot));
assert!(
clobber.is_none(),
"argument 0 (100) at [fp, {arg0_slot}] was overwritten before `bl h`: \
{clobber:?} in {body:?}"
);
assert!(
body[mov_100 + 2..bl_h]
.iter()
.any(|l| l.trim() == "bl id"),
"the nested `id(7)` call must run between the two outer arguments: {body:?}"
);
}
#[test]
fn a_deep_arithmetic_call_argument_keeps_every_slot_inside_the_frame() {
let asm = compile_program_ok(
"fn id(x: i64) -> i64 { x }\n\
fn f(a: i64) -> i64 { id(((a+1)*(a+2)) - ((a+3)*(a+4))) }",
);
let f_fn: Vec<&str> = asm
.lines()
.skip_while(|l| l.trim() != "f:")
.take_while(|l| l.trim() != "ret" && !l.trim().is_empty())
.collect();
let dealloc: i64 = f_fn
.iter()
.find_map(|l| {
let t = l.trim();
t.strip_prefix("ldp fp, lr, [sp], ")
.and_then(|n| n.trim().parse().ok())
})
.expect("missing the epilogue `ldp` line with the frame size");
for line in &f_fn {
if let Some(offset) = fp_offset(line) {
assert!(
offset < dealloc,
"`{}` writes [fp, {offset}] -- outside the {dealloc}-byte frame",
line.trim()
);
assert!(offset >= 0, "`{}` has a negative fp offset", line.trim());
}
}
let spill_slots: std::collections::BTreeSet<i64> = f_fn
.iter()
.filter(|l| l.contains("str x0, [fp, "))
.filter_map(|l| fp_offset(l))
.collect();
assert!(
spill_slots.len() >= 3,
"a deep arithmetic argument must use several scratch slots: {f_fn:?}"
);
}
}