const INDENT: &str = " ";
pub(crate) struct Asm {
data: String,
text: String,
format_strings: Vec<String>,
}
impl Asm {
pub(crate) fn new() -> Self {
Asm {
data: String::new(),
text: String::new(),
format_strings: Vec::new(),
}
}
pub(crate) fn intern_format(&mut self, content: &str) -> String {
if let Some(index) = self.format_strings.iter().position(|s| s == content) {
return format!(".Lfmt_{index}");
}
let index = self.format_strings.len();
let label = format!(".Lfmt_{index}");
self.format_strings.push(content.to_string());
self.data.push_str(&format!(
"{label}: .string \"{}\"\n",
escape_for_string_directive(content)
));
label
}
pub(crate) fn emit_line(&mut self, line: &str) {
self.text.push_str(line);
self.text.push('\n');
}
pub(crate) fn emit_label(&mut self, label: &str) {
self.text.push_str(label);
self.text.push_str(":\n");
}
pub(crate) fn emit_insn(&mut self, insn: &str) {
self.text.push_str(INDENT);
self.text.push_str(insn);
self.text.push('\n');
}
pub(crate) fn emit_insn_commented(&mut self, insn: &str, comment: &str) {
self.text.push_str(INDENT);
self.text.push_str(insn);
self.text.push_str(" // ");
self.text.push_str(comment);
self.text.push('\n');
}
pub(crate) fn finish(self) -> String {
let mut out = String::new();
out.push_str("define(fp, x29)\n");
out.push_str("define(lr, x30)\n");
out.push('\n');
if !self.data.is_empty() {
out.push_str(INDENT);
out.push_str(".data\n");
out.push_str(&self.data);
out.push('\n');
}
out.push_str(INDENT);
out.push_str(".text\n");
out.push_str(&self.text);
out
}
}
fn escape_for_string_directive(content: &str) -> String {
let mut out = String::with_capacity(content.len());
for ch in content.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
'\r' => out.push_str("\\r"),
other => out.push(other),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn finish_on_an_empty_builder_is_the_preamble_and_text_header() {
let asm = Asm::new();
let expected = "define(fp, x29)\n\
define(lr, x30)\n\
\n\
\x20\x20\x20\x20\x20\x20\x20\x20.text\n";
assert_eq!(asm.finish(), expected);
}
#[test]
fn finish_omits_the_data_section_when_the_buffer_is_empty() {
let asm = Asm::new();
assert!(!asm.finish().contains(".data"));
}
#[test]
fn emit_label_writes_the_label_at_column_zero() {
let mut asm = Asm::new();
asm.emit_label("main");
asm.emit_label(".Lmain_epilogue");
let out = asm.finish();
assert!(out.contains("\nmain:\n"));
assert!(out.contains("\n.Lmain_epilogue:\n"));
}
#[test]
fn emit_insn_indents_with_eight_spaces() {
let mut asm = Asm::new();
asm.emit_insn("mov x0, 0");
let out = asm.finish();
assert!(
out.contains("\n mov x0, 0\n"),
"missing 8-space indent: {out:?}"
);
}
#[test]
fn emit_insn_commented_appends_a_trailing_comment() {
let mut asm = Asm::new();
asm.emit_insn_commented("str x0, [fp, -8]", "a");
let out = asm.finish();
assert!(out.contains(" str x0, [fp, -8] // a\n"), "{out:?}");
}
#[test]
fn emit_line_writes_the_line_verbatim() {
let mut asm = Asm::new();
asm.emit_line("");
let out = asm.finish();
assert!(out.ends_with(".text\n\n"));
}
#[test]
fn finish_output_is_lf_only() {
let mut asm = Asm::new();
asm.emit_label("f");
asm.emit_insn("ret");
assert!(!asm.finish().contains('\r'));
}
#[test]
fn intern_format_registers_the_format_string_and_returns_a_unique_label() {
let mut asm = Asm::new();
assert_eq!(asm.intern_format("done\n"), ".Lfmt_0");
let out = asm.finish();
assert!(
out.contains(" .data\n"),
"missing .data section: {out:?}"
);
assert!(
out.contains(".Lfmt_0: .string \"done\\n\"\n"),
"missing the interned format string: {out:?}"
);
}
#[test]
fn intern_format_numbers_distinct_strings_monotonically() {
let mut asm = Asm::new();
assert_eq!(asm.intern_format("a"), ".Lfmt_0");
assert_eq!(asm.intern_format("b"), ".Lfmt_1");
let out = asm.finish();
assert!(out.contains(".Lfmt_0: .string \"a\"\n"), "{out:?}");
assert!(out.contains(".Lfmt_1: .string \"b\"\n"), "{out:?}");
}
#[test]
fn intern_format_deduplicates_by_content() {
let mut asm = Asm::new();
let first = asm.intern_format("fib(%lld) = %lld\n");
let second = asm.intern_format("fib(%lld) = %lld\n");
assert_eq!(
first, second,
"an identical format string must reuse its label"
);
assert_eq!(first, ".Lfmt_0");
let out = asm.finish();
assert_eq!(
out.matches(".Lfmt_0:").count(),
1,
"a repeated format string must be interned exactly once: {out:?}"
);
}
#[test]
fn intern_format_escapes_quotes_backslashes_and_newlines() {
let mut asm = Asm::new();
asm.intern_format("a\"b\\c\n");
let out = asm.finish();
assert!(
out.contains(".Lfmt_0: .string \"a\\\"b\\\\c\\n\"\n"),
"the format string was not escaped for the .string directive: {out:?}"
);
}
#[test]
fn intern_format_passes_percent_conversions_through_unescaped() {
let mut asm = Asm::new();
asm.intern_format("100%% done: %lld\n");
let out = asm.finish();
assert!(
out.contains(".Lfmt_0: .string \"100%% done: %lld\\n\"\n"),
"percent conversions must pass through unescaped: {out:?}"
);
}
#[test]
fn finish_still_omits_the_data_section_when_no_format_string_is_interned() {
let asm = Asm::new();
assert!(!asm.finish().contains(".data"));
}
#[test]
fn escape_for_string_directive_leaves_plain_text_untouched() {
assert_eq!(
escape_for_string_directive("fib(%lld) = %lld"),
"fib(%lld) = %lld"
);
}
}