Skip to main content

monkey_asm/
emitter.rs

1//! AArch64 text emitter (design §6): buffers, labels, span stack, and the
2//! encoding-limit helpers (`load_imm64`, frame/sp/global addressing) that
3//! `lower.rs` must never bypass.
4//!
5//! Two dialects (design §9): Linux GNU `as`/ELF (bare symbols, `.L` locals,
6//! `:lo12:` relocations, `.rodata`/`.bss`) and macOS Mach-O via clang
7//! (`_`-prefixed C symbols, `L` locals, `@PAGE`/`@PAGEOFF`,
8//! `__TEXT,__const`/`.zerofill`). Instructions are identical on both.
9
10use std::collections::HashMap;
11
12use parser::lexer::token::Span;
13
14use crate::runtime_core::NULL_VALUE;
15
16/// Negative-offset load/store (`ldur`/`stur`) immediates cover `[-256, 255]`
17/// (design §6); larger frame offsets go through address materialization.
18const MAX_UNSCALED_OFFSET: u64 = 256;
19/// `add`/`sub` 12-bit immediate.
20const MAX_ARITH_IMM: u64 = 4095;
21/// `add`/`sub` with `lsl #12` covers another 12 bits.
22const MAX_ARITH_IMM_SHIFTED: u64 = 0xFF_F000;
23/// Positive scaled `ldr`/`str` x-register immediate: 8-byte multiples up to
24/// `4095 * 8`.
25const MAX_SCALED_OFFSET: u64 = 32760;
26
27/// Byte offset below `x29` of the hidden closure slot (design §6).
28pub const CLOSURE_SLOT_OFFSET: u64 = 16;
29
30/// Byte offset below `x29` of symbol slot `index` (design §6:
31/// `[x29, #-16*(i+2)]`).
32pub fn slot_offset(index: usize) -> u64 {
33    16 * (index as u64 + 2)
34}
35
36/// Call-site argument area size: 8-byte packed callee + args, 16-aligned
37/// (design §7.1).
38pub fn call_area_size(argc: usize) -> u64 {
39    let packed = 8 * (argc as u64 + 1);
40    (packed + 15) & !15
41}
42
43/// 8-byte packed scratch area (array/hash/free-variable lists), 16-aligned.
44pub fn scratch_area_size(len: usize) -> u64 {
45    let packed = 8 * len as u64;
46    (packed + 15) & !15
47}
48
49/// Assembler/object-format dialect (design §9). Everything the two supported
50/// platforms disagree on — C symbol prefixes, private-label spelling, `adrp`
51/// relocation syntax, and data-section directives — routes through here; the
52/// instruction stream itself is identical.
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub enum AsmDialect {
55    /// Linux GNU `as` + ELF (also what the playground shows).
56    LinuxElf,
57    /// macOS Apple Silicon: clang assembler + Mach-O.
58    MachO,
59}
60
61impl AsmDialect {
62    /// Spells a C-ABI symbol the way the platform assembler expects it:
63    /// Mach-O prefixes every C-visible name with `_` (`_main`, `_rt_add`),
64    /// ELF uses the bare name.
65    pub fn global_symbol(self, name: &str) -> String {
66        match self {
67            AsmDialect::LinuxElf => name.to_string(),
68            AsmDialect::MachO => format!("_{}", name),
69        }
70    }
71
72    /// Prefix that keeps a label out of the object's symbol table: `.L` on
73    /// ELF, `L` on Mach-O.
74    pub(crate) fn local_label_prefix(self) -> &'static str {
75        match self {
76            AsmDialect::LinuxElf => ".L",
77            AsmDialect::MachO => "L",
78        }
79    }
80}
81
82#[derive(Clone, Debug)]
83struct Line {
84    text: String,
85    span: Option<(usize, usize)>,
86}
87
88/// Finished assembly module plus a per-line source span map for snapshots
89/// and the playground (design §6.2, §12).
90#[derive(Clone, Debug)]
91pub struct Assembly {
92    pub text: String,
93    pub line_spans: Vec<Option<(usize, usize)>>,
94}
95
96/// Everything `end_function` needs to splice prologue and epilogue around a
97/// finished body (design §6.1).
98pub struct FunctionFrame {
99    pub label: String,
100    /// Human-readable signature for the label comment, e.g. `fn add(a, b)`.
101    pub comment: String,
102    /// Parameters already counting a method's implicit `this`; spilled from
103    /// `x1..x{n}` into symbol slots `0..n-1`.
104    pub num_parameters: usize,
105    /// `SymbolTable::num_definitions` of the finished scope.
106    pub num_definitions: usize,
107    /// Label the body's return paths branch to; the epilogue lands here.
108    pub epilogue_label: String,
109    /// Comments for the spilled parameter slots (name per parameter).
110    pub parameter_names: Vec<String>,
111}
112
113pub struct Emitter {
114    dialect: AsmDialect,
115    main_body: Vec<Line>,
116    functions: Vec<Vec<Line>>,
117    rodata: Vec<Line>,
118    /// Stack of in-progress function bodies; instructions go to the top one,
119    /// or to `main_body` when empty (design §6.1).
120    open_functions: Vec<Vec<Line>>,
121    /// `with_span` pushes `Some`, `without_span` pushes `None` for synthetic
122    /// prologue/epilogue code (design §6.2).
123    span_stack: Vec<Option<(usize, usize)>>,
124    label_count: usize,
125    function_count: usize,
126    strings: HashMap<Vec<u8>, (String, u64)>,
127}
128
129impl Emitter {
130    pub fn new(dialect: AsmDialect) -> Emitter {
131        Emitter {
132            dialect,
133            main_body: vec![],
134            functions: vec![],
135            rodata: vec![],
136            open_functions: vec![],
137            span_stack: vec![],
138            label_count: 0,
139            function_count: 0,
140            strings: HashMap::new(),
141        }
142    }
143
144    fn current_span(&self) -> Option<(usize, usize)> {
145        self.span_stack.last().copied().flatten()
146    }
147
148    fn buffer(&mut self) -> &mut Vec<Line> {
149        self.open_functions
150            .last_mut()
151            .unwrap_or(&mut self.main_body)
152    }
153
154    fn push_line(&mut self, text: String) {
155        let span = self.current_span();
156        self.buffer().push(Line {
157            text,
158            span,
159        });
160    }
161
162    pub fn with_span<R>(&mut self, span: &Span, f: impl FnOnce(&mut Emitter) -> R) -> R {
163        self.span_stack.push(Some((span.start, span.end)));
164        let result = f(self);
165        self.span_stack.pop();
166        result
167    }
168
169    pub fn without_span<R>(&mut self, f: impl FnOnce(&mut Emitter) -> R) -> R {
170        self.span_stack.push(None);
171        let result = f(self);
172        self.span_stack.pop();
173        result
174    }
175
176    /// One instruction or directive line.
177    pub fn ins(&mut self, text: &str) {
178        self.push_line(format!("    {}", text));
179    }
180
181    /// Instruction with a trailing `//` comment (source snippets, slot names).
182    pub fn ins_cmt(&mut self, text: &str, comment: &str) {
183        self.push_line(format!("    {:<31} // {}", text, comment));
184    }
185
186    /// Standalone comment line.
187    pub fn comment(&mut self, comment: &str) {
188        self.push_line(format!("    // {}", comment));
189    }
190
191    pub fn label(&mut self, name: &str) {
192        self.push_line(format!("{}:", name));
193    }
194
195    pub fn label_cmt(&mut self, name: &str, comment: &str) {
196        self.push_line(format!("{:<35} // {}", format!("{}:", name), comment));
197    }
198
199    pub fn new_label(&mut self) -> String {
200        let label = format!("{}{}", self.dialect.local_label_prefix(), self.label_count);
201        self.label_count += 1;
202        label
203    }
204
205    pub fn new_function_label(&mut self) -> String {
206        let label = format!("{}fn{}", self.dialect.local_label_prefix(), self.function_count);
207        self.function_count += 1;
208        label
209    }
210
211    /// Interns a string literal into `.rodata`, deduplicated; returns
212    /// `(label, byte length)`.
213    pub fn intern_string(&mut self, bytes: &[u8]) -> (String, u64) {
214        if let Some((label, len)) = self.strings.get(bytes) {
215            return (label.clone(), *len);
216        }
217        let label = format!("{}str{}", self.dialect.local_label_prefix(), self.strings.len());
218        let len = bytes.len() as u64;
219        let preview: String = String::from_utf8_lossy(bytes)
220            .chars()
221            .take(32)
222            .map(|c| if c == '\n' || c == '\r' { ' ' } else { c })
223            .collect();
224        self.rodata.push(Line {
225            text: format!("{:<35} // \"{}\"", format!("{}:", label), preview),
226            span: None,
227        });
228        // `.byte` lists sidestep GNU as string escaping for arbitrary UTF-8.
229        for chunk in bytes.chunks(16) {
230            let rendered: Vec<String> = chunk.iter().map(|b| format!("0x{:02x}", b)).collect();
231            self.rodata.push(Line {
232                text: format!("    .byte {}", rendered.join(", ")),
233                span: None,
234            });
235        }
236        self.strings.insert(bytes.to_vec(), (label.clone(), len));
237        (label, len)
238    }
239
240    /// Materializes any 64-bit constant: one `movz` plus up to three `movk`,
241    /// skipping zero halfwords (design §6). Never assumes `mov reg, #imm`
242    /// encodes.
243    pub fn load_imm64(&mut self, reg: &str, value: u64, comment: &str) {
244        let halfwords: Vec<u64> = (0..4).map(|i| (value >> (16 * i)) & 0xFFFF).collect();
245        let mut emitted = false;
246        for (index, halfword) in halfwords.iter().enumerate() {
247            if *halfword == 0 {
248                continue;
249            }
250            let shift = 16 * index;
251            let text = if !emitted {
252                if shift == 0 {
253                    format!("movz {}, #0x{:x}", reg, halfword)
254                } else {
255                    format!("movz {}, #0x{:x}, lsl #{}", reg, halfword, shift)
256                }
257            } else if shift == 0 {
258                format!("movk {}, #0x{:x}", reg, halfword)
259            } else {
260                format!("movk {}, #0x{:x}, lsl #{}", reg, halfword, shift)
261            };
262            if !emitted && !comment.is_empty() {
263                self.ins_cmt(&text, comment);
264            } else {
265                self.ins(&text);
266            }
267            emitted = true;
268        }
269        if !emitted {
270            let text = format!("movz {}, #0", reg);
271            if comment.is_empty() {
272                self.ins(&text);
273            } else {
274                self.ins_cmt(&text, comment);
275            }
276        }
277    }
278
279    /// `reg = address of label` via `adrp` plus the low-12-bits add, spelled
280    /// `:lo12:` on ELF and `@PAGE`/`@PAGEOFF` on Mach-O (design §6, §9).
281    pub fn load_label_address(&mut self, reg: &str, label: &str, comment: &str) {
282        let (page, low) = match self.dialect {
283            AsmDialect::LinuxElf => (
284                format!("adrp {}, {}", reg, label),
285                format!("add {}, {}, :lo12:{}", reg, reg, label),
286            ),
287            AsmDialect::MachO => (
288                format!("adrp {}, {}@PAGE", reg, label),
289                format!("add {}, {}, {}@PAGEOFF", reg, reg, label),
290            ),
291        };
292        if comment.is_empty() {
293            self.ins(&page);
294        } else {
295            self.ins_cmt(&page, comment);
296        }
297        self.ins(&low);
298    }
299
300    /// `bl` to an `rt_*` entry point by its C name; the dialect supplies the
301    /// Mach-O `_` prefix (`bl rt_add` vs `bl _rt_add`). Every runtime call
302    /// must go through here so no `bl` hardcodes a platform spelling.
303    pub fn call_runtime(&mut self, name: &str, comment: &str) {
304        let text = format!("bl {}", self.dialect.global_symbol(name));
305        if comment.is_empty() {
306            self.ins(&text);
307        } else {
308            self.ins_cmt(&text, comment);
309        }
310    }
311
312    /// Pushes the accumulator, one value per 16-byte slot (design §6).
313    pub fn push_acc(&mut self, comment: &str) {
314        if comment.is_empty() {
315            self.ins("str x0, [sp, #-16]!");
316        } else {
317            self.ins_cmt("str x0, [sp, #-16]!", comment);
318        }
319    }
320
321    pub fn pop(&mut self, reg: &str, comment: &str) {
322        let text = format!("ldr {}, [sp], #16", reg);
323        if comment.is_empty() {
324            self.ins(&text);
325        } else {
326            self.ins_cmt(&text, comment);
327        }
328    }
329
330    fn adjust_sp(&mut self, mnemonic: &str, bytes: u64) {
331        if bytes == 0 {
332            return;
333        }
334        if bytes <= MAX_ARITH_IMM {
335            self.ins(&format!("{} sp, sp, #{}", mnemonic, bytes));
336            return;
337        }
338        if bytes <= MAX_ARITH_IMM + MAX_ARITH_IMM_SHIFTED {
339            let high = bytes >> 12;
340            let low = bytes & 0xFFF;
341            self.ins(&format!("{} sp, sp, #{}, lsl #12", mnemonic, high));
342            if low != 0 {
343                self.ins(&format!("{} sp, sp, #{}", mnemonic, low));
344            }
345            return;
346        }
347        self.load_imm64("x8", bytes, "sp adjustment beyond immediate range");
348        self.ins(&format!("{} sp, sp, x8", mnemonic));
349    }
350
351    /// Grows the stack by `bytes` (16-aligned by contract), splitting or
352    /// materializing when the immediate does not encode (design §6).
353    pub fn sp_sub(&mut self, bytes: u64) {
354        debug_assert_eq!(bytes % 16, 0);
355        self.adjust_sp("sub", bytes);
356    }
357
358    pub fn sp_add(&mut self, bytes: u64) {
359        debug_assert_eq!(bytes % 16, 0);
360        self.adjust_sp("add", bytes);
361    }
362
363    /// Stores `reg` at `[x29, #-offset]`, going through `x8` when the
364    /// unscaled immediate cannot encode the slot (design §6). `reg` must not
365    /// be `x8`.
366    pub fn frame_store(&mut self, reg: &str, offset: u64, comment: &str) {
367        debug_assert!(reg != "x8");
368        if offset <= MAX_UNSCALED_OFFSET {
369            let text = format!("stur {}, [x29, #-{}]", reg, offset);
370            if comment.is_empty() {
371                self.ins(&text);
372            } else {
373                self.ins_cmt(&text, comment);
374            }
375            return;
376        }
377        self.load_imm64("x8", offset, comment);
378        self.ins("sub x8, x29, x8");
379        self.ins(&format!("str {}, [x8]", reg));
380    }
381
382    pub fn frame_load(&mut self, reg: &str, offset: u64, comment: &str) {
383        debug_assert!(reg != "x8");
384        if offset <= MAX_UNSCALED_OFFSET {
385            let text = format!("ldur {}, [x29, #-{}]", reg, offset);
386            if comment.is_empty() {
387                self.ins(&text);
388            } else {
389                self.ins_cmt(&text, comment);
390            }
391            return;
392        }
393        self.load_imm64("x8", offset, comment);
394        self.ins("sub x8, x29, x8");
395        self.ins(&format!("ldr {}, [x8]", reg));
396    }
397
398    /// Stores `reg` at `[sp, #offset]` (call/scratch area fill); offsets
399    /// beyond the scaled immediate go through `x8`. `reg` must not be `x8`.
400    pub fn sp_store(&mut self, reg: &str, offset: u64, comment: &str) {
401        debug_assert!(reg != "x8");
402        debug_assert_eq!(offset % 8, 0);
403        if offset <= MAX_SCALED_OFFSET {
404            let text = if offset == 0 {
405                format!("str {}, [sp]", reg)
406            } else {
407                format!("str {}, [sp, #{}]", reg, offset)
408            };
409            if comment.is_empty() {
410                self.ins(&text);
411            } else {
412                self.ins_cmt(&text, comment);
413            }
414            return;
415        }
416        self.load_imm64("x8", offset, comment);
417        self.ins("add x8, sp, x8");
418        self.ins(&format!("str {}, [x8]", reg));
419    }
420
421    /// `reg = sp + offset` (argv base and friends).
422    pub fn sp_address(&mut self, reg: &str, offset: u64, comment: &str) {
423        if offset <= MAX_ARITH_IMM {
424            let text = format!("add {}, sp, #{}", reg, offset);
425            if comment.is_empty() {
426                self.ins(&text);
427            } else {
428                self.ins_cmt(&text, comment);
429            }
430            return;
431        }
432        self.load_imm64(reg, offset, comment);
433        self.ins(&format!("add {}, sp, {}", reg, reg));
434    }
435
436    fn global_slot(&mut self, mnemonic: &str, reg: &str, index: usize, comment: &str) {
437        debug_assert!(reg != "x8" && reg != "x9");
438        self.load_label_address("x8", "g_globals", comment);
439        let offset = 8 * index as u64;
440        if offset <= MAX_SCALED_OFFSET {
441            if offset == 0 {
442                self.ins(&format!("{} {}, [x8]", mnemonic, reg));
443            } else {
444                self.ins(&format!("{} {}, [x8, #{}]", mnemonic, reg, offset));
445            }
446            return;
447        }
448        self.load_imm64("x9", offset, "global slot beyond immediate range");
449        self.ins(&format!("{} {}, [x8, x9]", mnemonic, reg));
450    }
451
452    /// Reads global slot `index` from the single `g_globals` array
453    /// (design §5.2); clobbers `x8`/`x9`.
454    pub fn global_load(&mut self, reg: &str, index: usize, comment: &str) {
455        self.global_slot("ldr", reg, index, comment);
456    }
457
458    pub fn global_store(&mut self, reg: &str, index: usize, comment: &str) {
459        self.global_slot("str", reg, index, comment);
460    }
461
462    /// Runs `f` collecting lines into a detached buffer (prologue/epilogue
463    /// splicing).
464    fn capture(&mut self, f: impl FnOnce(&mut Emitter)) -> Vec<Line> {
465        self.open_functions.push(vec![]);
466        self.without_span(f);
467        self.open_functions.pop().unwrap()
468    }
469
470    /// Opens a fresh function body buffer; instructions emitted until the
471    /// matching `end_function` go to it (design §6.1).
472    pub fn begin_function(&mut self) {
473        self.open_functions.push(vec![]);
474    }
475
476    /// Closes the current function body: computes the frame from the final
477    /// symbol count, splices prologue (fp/lr save, frame, spills, null
478    /// initialization) and the shared epilogue, and appends the finished
479    /// function (design §6, §6.1).
480    pub fn end_function(&mut self, frame: FunctionFrame) {
481        let body = self
482            .open_functions
483            .pop()
484            .expect("end_function without begin_function");
485        let num_slots = 1 + frame.num_definitions;
486        let frame_bytes = 16 * num_slots as u64;
487
488        let prologue = self.capture(|emitter| {
489            emitter.ins("stp x29, x30, [sp, #-16]!");
490            emitter.ins("mov x29, sp");
491            emitter.sp_sub(frame_bytes);
492            emitter.frame_store("x0", CLOSURE_SLOT_OFFSET, "closure (hidden argument)");
493            for index in 0..frame.num_parameters {
494                let name = frame
495                    .parameter_names
496                    .get(index)
497                    .map(String::as_str)
498                    .unwrap_or("parameter");
499                let register = format!("x{}", index + 1);
500                emitter.frame_store(&register, slot_offset(index), name);
501            }
502            if frame.num_parameters < frame.num_definitions {
503                emitter.load_imm64("x9", NULL_VALUE, "null-initialize locals");
504                for index in frame.num_parameters..frame.num_definitions {
505                    emitter.frame_store("x9", slot_offset(index), "");
506                }
507            }
508        });
509        let epilogue = self.capture(|emitter| {
510            emitter.label(&frame.epilogue_label);
511            emitter.ins("mov sp, x29");
512            emitter.ins("ldp x29, x30, [sp], #16");
513            emitter.ins("ret");
514        });
515
516        let mut lines = Vec::with_capacity(prologue.len() + body.len() + epilogue.len() + 1);
517        lines.push(Line {
518            text: format!("{:<35} // {}", format!("{}:", frame.label), frame.comment),
519            span: None,
520        });
521        lines.extend(prologue);
522        lines.extend(body);
523        lines.extend(epilogue);
524        self.functions.push(lines);
525    }
526
527    /// Assembles the final module: `main` (with observer/globals init and the
528    /// fixed `mov w0, #0` exit), then every finished function, `.rodata`, and
529    /// the `g_globals` `.bss` array (design §6, §6.1).
530    pub fn finish(
531        mut self,
532        globals_count: usize,
533        main_epilogue_label: &str,
534        observe: bool,
535    ) -> Assembly {
536        debug_assert!(self.open_functions.is_empty(), "unfinished function buffer");
537        let main_body = std::mem::take(&mut self.main_body);
538
539        let prologue = self.capture(|emitter| {
540            emitter.ins("stp x29, x30, [sp, #-16]!");
541            emitter.ins("mov x29, sp");
542            if observe {
543                emitter.load_imm64("x0", 3, "observer channel fd");
544                emitter.call_runtime("rt_observer_init", "");
545            }
546            emitter.load_label_address("x0", "g_globals", "");
547            emitter.load_imm64("x1", globals_count as u64, "global slot count");
548            emitter.call_runtime("rt_globals_init", "");
549        });
550        let epilogue = self.capture(|emitter| {
551            emitter.label(main_epilogue_label);
552            if observe {
553                emitter.call_runtime("rt_observe_result", "program result record");
554            }
555            emitter.ins_cmt("mov w0, #0", "exit code is never the tagged value");
556            emitter.ins("mov sp, x29");
557            emitter.ins("ldp x29, x30, [sp], #16");
558            emitter.ins("ret");
559        });
560
561        let mut lines: Vec<Line> = vec![];
562        let mut raw = |text: &str| {
563            lines.push(Line {
564                text: text.to_string(),
565                span: None,
566            });
567        };
568        let main_symbol = self.dialect.global_symbol("main");
569        raw("// Generated by monkey-asm (docs/arm64-asm-backend-design.md). Do not edit.");
570        raw("    .text");
571        raw(&format!("    .globl {}", main_symbol));
572        raw("    .p2align 2");
573        raw(&format!("{}:", main_symbol));
574        lines.extend(prologue);
575        lines.extend(main_body);
576        lines.extend(epilogue);
577        for function in std::mem::take(&mut self.functions) {
578            lines.push(Line {
579                text: String::new(),
580                span: None,
581            });
582            lines.extend(function);
583        }
584        if !self.rodata.is_empty() {
585            lines.push(Line {
586                text: String::new(),
587                span: None,
588            });
589            let rodata_section = match self.dialect {
590                AsmDialect::LinuxElf => "    .section .rodata",
591                AsmDialect::MachO => "    .section __TEXT,__const",
592            };
593            lines.push(Line {
594                text: rodata_section.to_string(),
595                span: None,
596            });
597            lines.append(&mut self.rodata);
598        }
599        lines.push(Line {
600            text: String::new(),
601            span: None,
602        });
603        match self.dialect {
604            AsmDialect::LinuxElf => {
605                lines.push(Line {
606                    text: "    .bss".to_string(),
607                    span: None,
608                });
609                lines.push(Line {
610                    text: "    .balign 8".to_string(),
611                    span: None,
612                });
613                lines.push(Line {
614                    text: format!("{:<35} // {} global slot(s)", "g_globals:", globals_count),
615                    span: None,
616                });
617                lines.push(Line {
618                    text: format!("    .skip {}", 8 * globals_count),
619                    span: None,
620                });
621            }
622            AsmDialect::MachO => {
623                // One directive declares section, symbol, size, and log2
624                // alignment; a program without globals still reserves one
625                // slot rather than betting on zero-size `.zerofill` symbols.
626                let size = (8 * globals_count).max(8);
627                lines.push(Line {
628                    text: format!(
629                        "    {:<31} // {} global slot(s)",
630                        format!(".zerofill __DATA,__bss,g_globals,{},3", size),
631                        globals_count
632                    ),
633                    span: None,
634                });
635            }
636        }
637
638        let mut text = String::new();
639        let mut line_spans = Vec::with_capacity(lines.len());
640        for line in &lines {
641            text.push_str(&line.text);
642            text.push('\n');
643            line_spans.push(line.span);
644        }
645        Assembly {
646            text,
647            line_spans,
648        }
649    }
650}