use crate::errors::QalaError;
use crate::span::Span;
use crate::typed_ast::{TypedExpr, TypedInterpPart};
use super::Arm64Backend;
const MAX_PRINTF_HOLES: usize = super::expr::MAX_CALL_ARGS - 1;
impl Arm64Backend {
pub(super) fn compile_print_call(
&mut self,
name: &str,
args: &[TypedExpr],
span: Span,
) -> Result<(), QalaError> {
let [arg] = args else {
return Err(QalaError::Type {
span,
message: format!("the arm64 backend expects `{name}` to take exactly one argument"),
});
};
let newline = name == "println";
let (format, holes) = self.build_format(arg, newline)?;
if holes.len() > MAX_PRINTF_HOLES {
return Err(QalaError::Type {
span,
message: format!(
"the arm64 backend supports at most {MAX_PRINTF_HOLES} \
interpolation holes in a `{name}`"
),
});
}
self.emit_printf(&format, &holes)
}
fn build_format<'a>(
&self,
arg: &'a TypedExpr,
newline: bool,
) -> Result<(String, Vec<&'a TypedExpr>), QalaError> {
let mut format = String::new();
let mut holes: Vec<&TypedExpr> = Vec::new();
match arg {
TypedExpr::Str { value, .. } => {
format.push_str(&escape_percent(value));
}
TypedExpr::Interpolation { parts, .. } => {
for part in parts {
match part {
TypedInterpPart::Literal(text) => {
format.push_str(&escape_percent(text));
}
TypedInterpPart::Expr(expr) => {
if !matches!(expr.ty(), crate::types::QalaType::I64) {
return Err(QalaError::Type {
span: expr.span(),
message: format!(
"the arm64 backend does not yet support \
{} values in interpolation",
super::type_name(expr.ty())
),
});
}
format.push_str("%lld");
holes.push(expr);
}
}
}
}
other => {
return Err(QalaError::Type {
span: other.span(),
message: format!(
"the arm64 backend does not yet support {} as a \
`print` / `println` argument",
print_arg_description(other)
),
});
}
}
if newline {
format.push('\n');
}
Ok((format, holes))
}
fn emit_printf(&mut self, format: &str, holes: &[&TypedExpr]) -> Result<(), QalaError> {
let label = self.asm.intern_format(format);
let mut hole_slots = Vec::with_capacity(holes.len());
for (i, hole) in holes.iter().enumerate() {
self.compile_expr(hole)?;
let slot = self.frame_mut().claim_scratch();
self.asm.emit_insn_commented(
&format!("str x0, [fp, {slot}]"),
&format!("printf arg {}", i + 1),
);
hole_slots.push(slot);
}
for (i, slot) in hole_slots.iter().enumerate() {
self.asm
.emit_insn(&format!("ldr x{}, [fp, {slot}]", i + 1));
}
self.asm
.emit_insn_commented(&format!("ldr x0, ={label}"), "printf format");
self.asm.emit_insn("bl printf");
for _ in &hole_slots {
self.frame_mut().release_scratch();
}
Ok(())
}
}
fn escape_percent(text: &str) -> String {
text.replace('%', "%%")
}
fn print_arg_description(expr: &TypedExpr) -> &'static str {
match expr {
TypedExpr::Ident { .. } => "a string variable",
TypedExpr::Binary { .. } => "string concatenation",
TypedExpr::Call { .. } | TypedExpr::MethodCall { .. } => "a string-returning call",
TypedExpr::Paren { .. } => "a parenthesized string expression",
TypedExpr::Block { .. } => "a block expression",
TypedExpr::OrElse { .. } => "an `or` fallback expression",
TypedExpr::Match { .. } => "a match expression",
TypedExpr::FieldAccess { .. } => "a string field access",
TypedExpr::Index { .. } => "a string index expression",
_ => "this string expression",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lexer::Lexer;
use crate::parser::Parser;
use crate::typechecker::check_program;
fn compile_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 compile_err(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:?}");
let errors = super::super::compile_arm64(&typed, src)
.expect_err("the program must be rejected by the backend");
errors[0].message()
}
#[test]
fn escape_percent_doubles_a_literal_percent() {
assert_eq!(escape_percent("100% sure"), "100%% sure");
assert_eq!(escape_percent("no percent here"), "no percent here");
assert_eq!(escape_percent("%%"), "%%%%");
}
#[test]
fn a_string_literal_println_emits_a_printf_with_no_holes() {
let out = compile_ok("fn main() is io { println(\"done\") }");
assert!(
out.contains(" .data\n"),
"missing .data section: {out}"
);
assert!(
out.contains(".string \"done\\n\"\n"),
"missing the println format string: {out}"
);
assert!(
out.contains("ldr x0, =.Lfmt_0"),
"missing the format load: {out}"
);
assert!(
out.contains("bl printf"),
"missing the printf call: {out}"
);
}
#[test]
fn a_string_literal_print_omits_the_trailing_newline() {
let out = compile_ok("fn main() is io { print(\"hi\") }");
assert!(
out.contains(".string \"hi\"\n"),
"print must not add a newline: {out}"
);
assert!(
!out.contains(".string \"hi\\n\""),
"print added a newline: {out}"
);
}
#[test]
fn an_interpolation_hole_becomes_a_percent_lld_and_an_argument() {
let out = compile_ok("fn main() is io { let x = 7\nprintln(\"{x}\") }");
assert!(
out.contains(".string \"%lld\\n\"\n"),
"missing the %lld format: {out}"
);
assert!(
out.contains("str x0, [fp,") && out.contains("// printf arg 1"),
"missing the hole spill: {out}"
);
assert!(
out.contains("ldr x1, [fp,"),
"missing the x1 load: {out}"
);
assert!(
out.contains("ldr x0, =.Lfmt_0"),
"missing the format load: {out}"
);
assert!(
out.contains("bl printf"),
"missing the printf call: {out}"
);
}
#[test]
fn the_fibonacci_interpolation_builds_a_two_hole_format() {
let out = compile_ok(
"fn fib(n: i64) -> i64 { n }\n\
fn main() is io {\n\
\x20 for i in 0..3 { println(\"fib({i}) = {fib(i)}\") }\n\
}",
);
assert!(
out.contains(".string \"fib(%lld) = %lld\\n\"\n"),
"the two-hole format string is wrong: {out}"
);
assert!(
out.contains("ldr x1, [fp,"),
"missing the x1 load: {out}"
);
assert!(
out.contains("ldr x2, [fp,"),
"missing the x2 load: {out}"
);
assert!(
out.contains("bl printf"),
"missing the printf call: {out}"
);
}
#[test]
fn the_hole_arguments_are_loaded_after_every_spill() {
let out = compile_ok(
"fn fib(n: i64) -> i64 { n }\n\
fn main() is io {\n\
\x20 for i in 0..3 { println(\"fib({i}) = {fib(i)}\") }\n\
}",
);
let lines: Vec<&str> = out.lines().collect();
let last_spill = lines
.iter()
.rposition(|l| l.contains("str x0, [fp,") && l.contains("// printf arg "))
.expect("no hole spill");
let first_arg_load = lines
.iter()
.position(|l| l.contains("ldr x1, [fp,"))
.expect("no x1 load");
assert!(
last_spill < first_arg_load,
"every hole spill must precede the first argument load: {out}"
);
}
#[test]
fn a_literal_percent_in_the_text_is_doubled() {
let out = compile_ok("fn main() is io { println(\"100% done\") }");
assert!(
out.contains(".string \"100%% done\\n\"\n"),
"a literal percent must be doubled to %%: {out}"
);
}
#[test]
fn two_identical_format_strings_share_one_data_entry() {
let out = compile_ok("fn main() is io { println(\"done\")\nprintln(\"done\") }");
assert_eq!(
out.matches(".Lfmt_0:").count(),
1,
"an identical format string must be interned once: {out}"
);
assert!(
!out.contains(".Lfmt_1:"),
"no second entry for the same string: {out}"
);
}
#[test]
fn main_returns_zero_after_a_printf() {
let out = compile_ok("fn main() is io { println(\"done\") }");
assert!(
out.contains("mov x0, 0 // void return -> exit code 0"),
"main must return 0 after the printf: {out}"
);
}
#[test]
fn a_non_i64_interpolation_hole_is_rejected_cleanly() {
let msg = compile_err("fn main() is io { let b = true\nprintln(\"{b}\") }");
assert!(
msg.contains("bool") && msg.contains("interpolation"),
"the rejection must name the bool hole: {msg}"
);
}
#[test]
fn an_i64_typed_but_unsupported_hole_is_rejected_cleanly() {
let msg = compile_err("fn main() is io { let a = -3\nprintln(\"{abs(a)}\") }");
assert!(
msg.contains("abs"),
"an i64-typed unsupported hole must be rejected naming `abs`: {msg}"
);
}
#[test]
fn a_float_interpolation_hole_is_rejected_cleanly() {
use crate::types::QalaType;
let interp = TypedExpr::Interpolation {
parts: vec![TypedInterpPart::Expr(TypedExpr::Float {
value: 1.5,
ty: QalaType::F64,
span: Span::new(0, 3),
})],
ty: QalaType::Str,
span: Span::new(0, 5),
};
let mut backend = Arm64Backend::new("");
let err = backend
.compile_print_call("println", std::slice::from_ref(&interp), Span::new(0, 5))
.expect_err("an f64 hole must be rejected");
match err {
QalaError::Type { message, .. } => assert!(
message.contains("f64") && message.contains("interpolation"),
"the rejection must name the f64 hole: {message}"
),
other => panic!("expected QalaError::Type, got {other:?}"),
}
}
#[test]
fn a_string_variable_argument_is_rejected_cleanly() {
use crate::types::QalaType;
let ident = TypedExpr::Ident {
name: "s".to_string(),
ty: QalaType::Str,
span: Span::new(0, 1),
};
let mut backend = Arm64Backend::new("");
let err = backend
.compile_print_call("println", std::slice::from_ref(&ident), Span::new(0, 5))
.expect_err("a string variable argument must be rejected");
match err {
QalaError::Type { message, .. } => assert!(
message.contains("string variable") && message.contains("arm64"),
"the rejection must name the string variable: {message}"
),
other => panic!("expected QalaError::Type, got {other:?}"),
}
}
#[test]
fn a_string_concatenation_argument_is_rejected_cleanly() {
let msg = compile_err("fn main() is io { println(\"a\" + \"b\") }");
assert!(
msg.contains("string concatenation"),
"the rejection must name the concatenation: {msg}"
);
}
#[test]
fn more_than_seven_holes_is_rejected_cleanly() {
let src = "fn main() is io {\n\
\x20 let a = 1\n\
\x20 println(\"{a}{a}{a}{a}{a}{a}{a}{a}\")\n\
}";
let msg = compile_err(src);
assert!(
msg.contains("at most 7") && msg.contains("holes"),
"more than seven holes must be rejected: {msg}"
);
}
#[test]
fn seven_holes_is_accepted() {
let src = "fn main() is io {\n\
\x20 let a = 1\n\
\x20 println(\"{a}{a}{a}{a}{a}{a}{a}\")\n\
}";
let out = compile_ok(src);
assert!(
out.contains("ldr x7, [fp,"),
"the seventh hole must load x7: {out}"
);
assert!(
!out.contains("ldr x8, [fp,"),
"there is no eighth argument register: {out}"
);
}
#[test]
fn the_emitted_program_has_a_text_section_and_a_main() {
let out = compile_ok("fn main() is io { println(\"done\") }");
assert!(
out.starts_with("define(fp, x29)\n"),
"missing the m4 preamble: {out}"
);
assert!(
out.contains(" .text\n"),
"missing the .text section: {out}"
);
assert!(out.contains("\nmain:\n"), "missing the main label: {out}");
}
#[test]
fn a_print_call_is_not_treated_as_a_user_function() {
let out = compile_ok("fn main() is io { println(\"done\") }");
let bl_targets: Vec<&str> = out
.lines()
.filter_map(|l| l.trim().strip_prefix("bl "))
.collect();
assert_eq!(
bl_targets,
vec!["printf"],
"the only call the println path emits is `bl printf`: {out}"
);
}
#[test]
fn a_hole_that_is_a_call_evaluates_before_the_printf() {
let out = compile_ok(
"fn fib(n: i64) -> i64 { n }\n\
fn main() is io { println(\"{fib(3)}\") }",
);
let lines: Vec<&str> = out.lines().collect();
let bl_fib = lines
.iter()
.position(|l| l.contains("bl fib"))
.expect("missing bl fib");
let bl_printf = lines
.iter()
.position(|l| l.contains("bl printf"))
.expect("missing bl printf");
assert!(
bl_fib < bl_printf,
"the hole's call must run before printf: {out}"
);
}
#[test]
fn typed_items_have_no_extra_emission_for_the_printf_path() {
let out = compile_ok("struct P { x: i64 }\nfn main() is io { println(\"done\") }");
assert!(
out.contains("bl printf"),
"the printf call is missing: {out}"
);
assert!(
!out.contains("\nP:\n"),
"the struct must emit nothing: {out}"
);
}
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 a_string_literal_println_matches_the_snapshot() {
let emitted = compile_ok("fn main() is io {\n println(\"done\")\n}\n");
assert_eq!(
emitted,
read_snapshot("arm64_printf_literal.s"),
"arm64 printf literal emission drifted from snapshot"
);
}
#[test]
fn an_interpolation_println_matches_the_snapshot() {
let src = "fn square(n: i64) -> i64 {\n\
\x20 n * n\n\
}\n\
\n\
fn main() is io {\n\
\x20 let x = 6\n\
\x20 println(\"square({x}) = {square(x)}\")\n\
}\n";
let emitted = compile_ok(src);
assert_eq!(
emitted,
read_snapshot("arm64_printf_interpolation.s"),
"arm64 printf interpolation emission drifted from snapshot"
);
}
#[test]
fn a_non_i64_hole_rejection_matches_the_error_snapshot() {
use crate::diagnostics::Diagnostic;
let src = "fn main() is io {\n let b = true\n println(\"flag is {b}\")\n}\n";
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 errors = super::super::compile_arm64(&typed, src)
.expect_err("a non-i64 interpolation hole must be rejected");
let rendered = errors
.iter()
.map(|e| Diagnostic::from(e.clone()).render(src))
.collect::<Vec<_>>()
.join("\n");
assert_eq!(
rendered,
read_snapshot("arm64_printf_rejected.txt"),
"arm64 printf rejection diagnostic drifted from snapshot"
);
}
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_printf_with_deep_holes_keeps_every_slot_inside_the_frame() {
let asm = compile_ok(
"fn three(a: i64, b: i64, c: i64) -> i64 { a + b + c }\n\
fn main() is io {\n\
\x20 let a = 4\n\
\x20 println(\"{(a+1)*(a+2)} and {three(a, a+1, a+2)} and {a}\")\n\
}\n",
);
let main_fn: Vec<&str> = asm
.lines()
.skip_while(|l| l.trim() != "main:")
.take_while(|l| l.trim() != "ret" && !l.trim().is_empty())
.collect();
let dealloc: i64 = main_fn
.iter()
.find_map(|l| {
l.trim()
.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 &main_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> = main_fn
.iter()
.filter(|l| l.contains("str x0, [fp, "))
.filter_map(|l| fp_offset(l))
.collect();
assert!(
spill_slots.len() >= 3,
"the deep-hole printf must use several scratch slots: {main_fn:?}"
);
for reg in 4..=7 {
assert!(
!asm.contains(&format!("ldr x{reg}, [fp, ")),
"a three-hole printf must not load x{reg}: {asm}"
);
}
}
}