# How the Maru LLVM Backend Emits Code: A Deep Dive into emit-llvm.l

> Discover how the Maru LLVM backend emits code by printing textual LLVM-IR directly from compilation buffers. Learn about its efficient three stage pipeline.

- Repository: [Attila Lendvai/maru](https://github.com/attila-lendvai/maru)
- Tags: deep-dive
- Published: 2026-02-25

---

**The Maru LLVM backend emits code by printing textual LLVM-IR directly from high-level compilation buffers, using a three-stage pipeline implemented in `source/compiler/emit-llvm.l` that never constructs an in-memory LLVM object model.**

The Maru compiler translates its intermediate representation into executable machine code through multiple backends. When targeting LLVM, the system relies on a **pure text emitter** architecture that converts Maru IR into human-readable LLVM-IR. This article examines how the LLVM backend emits code by analyzing the implementation in `attila-lendvai/maru`, specifically the `source/compiler/emit-llvm.l` module and its supporting files.

## Three-Stage Architecture of the LLVM Emitter

The LLVM backend operates through a distinct three-stage pipeline that separates prelude setup, instruction translation, and final output generation.

### Stage 1: Prelude Generation with emit-prelude

Before any function bodies are processed, `emit-prelude` establishes the LLVM type system and platform constants. Located at lines 610-672 in `source/compiler/emit-llvm.l`, this function prints essential type definitions including `%word`, `%oop` (object pointer), and `%\"<header>\"` structures.

```lisp
(define-function emit-prelude <llvm-compiler> ()
  (let ((word-size (target-value +word-size-in-bits+))
        (-c- self))
    (emit INLINE (list "%word = type i" word-size "\n"))
    (emit INLINE "%oop = type ptr\n")
    (emit INLINE "%\"<header>\" = type { %word, %word }\n")
    (emit INLINE "declare ptr @llvm.frameaddress(i32)\n")))

```

### Stage 2: Instruction Emission via define-instruction

Maru primitives map to LLVM operations through the `define-instruction` macro declared in `source/compiler/emit-early.l`. Each instruction definition expands into a specialized emitter function that prints specific LLVM-IR text. For example, the `ADD` instruction (lines 83-110 in `emit-llvm.l`) generates `ptrtoint`, `add`, and `inttoptr` sequences to handle Maru's pointer arithmetic.

```lisp
(define-instruction ADD (true 4 true)
  ((LOCAL LOCAL)
   (println "	" $reg1 " = ptrtoint %oop " $1 " to %word")
   (println "	" $reg2 " = ptrtoint %oop " $2 " to %word")
   (println "	" $reg3 " = add %word " $reg1 ", " $reg2)
   (println "	" $reg4 " = inttoptr %word " $reg3 " to %oop")))

```

### Stage 3: Buffer Flushing with emit-gen-buffer

The final stage converts accumulated operations into text. The `emit-gen-buffer` function in `source/compiler/emit-late.l` (lines 45-71) iterates over the compiler's generation buffer, invoking the appropriate emitters for each stored operation. This produces the complete LLVM-IR file ready for `clang` or `llc` processing.

```lisp
(define-function emit-gen-buffer (gen-buffer)
  (while gen-buffer
    (let* ((op (pop gen-buffer))
           (fn (car op))
           (args (cdr op)))
      (apply fn args))))

```

## Key Emitter Functions in source/compiler/emit-llvm.l

The core emission logic resides in specific functions that handle LLVM's type system and calling conventions.

### Function Type Emission (emit-llvm-fn-type)

The `emit-llvm-fn-type` function (lines 176-197) prints LLVM function signatures, handling both fixed-arity and variadic functions. It maps Maru type symbols to LLVM type strings, ensuring proper type decoration for the `call` instruction.

### Call Instruction Emission (emit-llvm-call)

Located at lines 248-271, `emit-llvm-call` constructs LLVM `call` instructions. It manages argument type coercion, handles the `%oop` (object pointer) type for Maru values, and properly formats variadic argument lists using the `...` syntax.

```lisp
(define-function emit-llvm-call (result-reg name args return-type param-types vararg?)
  (let ((void? (= return-type 'void)))
    (print "	")
    (unless void? (print result-reg " = "))
    (print "call ")
    (if vararg?
        (emit-llvm-fn-type return-type param-types vararg?)
        (print return-type))
    (print " " name "(")
    ;; ... argument emission logic ...
    (println ")")))

```

### Definition Compilation (compile-definition)

The `compile-definition` function (lines 290-338) orchestrates the emission of function bodies. It creates LLVM labels for compiled lambdas, emits global aliases for entry points like `main` or `_start` when needed, and triggers the generation buffer flush to output the complete function definition.

## From Maru IR to LLVM-IR: The Compilation Flow

Understanding the entry points reveals how high-level Maru code traverses the emission pipeline.

The process begins with `run-compiler` (lines 93-98 in `emit-llvm.l`), which instantiates an `llvm-compiler` object and invokes `compile-env` on the target environment. This establishes the compilation context for the LLVM backend.

```lisp
(define-function run-compiler (env)
  (compile-env env (llvm-compiler 1)))

```

During expression compilation, `compile-expr` (via `emit-expr-code` in `emit-llvm.l`) builds a list of high-level **gen** operations such as `GEN`, `LOAD`, and `CALL`. Each operation stores its instruction prototype and arguments in the compiler's `gen-buffer`, creating a deferred emission queue rather than immediate text output.

This architecture separates semantic analysis from code generation, allowing the backend to perform peephole optimizations or instruction scheduling on the buffered operations before the final text emission stage.

## Summary

- The Maru LLVM backend is implemented in `source/compiler/emit-llvm.l` and operates as a **pure text emitter** that prints LLVM-IR directly without using the LLVM C++ API.
- Code generation follows a **three-stage pipeline**: prelude generation (`emit-prelude`), instruction emission (`define-instruction` macros), and buffer flushing (`emit-gen-buffer` from `emit-late.l`).
- Key functions include `emit-llvm-fn-type` for signatures, `emit-llvm-call` for function calls, and `compile-definition` for function bodies.
- The entry point `run-compiler` creates an `llvm-compiler` instance that buffers high-level operations before converting them to textual LLVM-IR.

## Frequently Asked Questions

### What is the entry point for LLVM code generation in Maru?

The `run-compiler` function in `source/compiler/emit-llvm.l` serves as the primary entry point. It instantiates an `llvm-compiler` object and invokes `compile-env` to begin the compilation process for the target environment.

### Does the Maru LLVM backend use the LLVM C++ API?

No. According to the source code in `attila-lendvai/maru`, the backend is a **pure text emitter** that prints human-readable LLVM-IR directly to the output stream. It never constructs an in-memory LLVM object model or links against the LLVM libraries.

### How are Maru primitives mapped to LLVM instructions?

Primitives map through the `define-instruction` macro declared in `source/compiler/emit-early.l`. Each primitive, such as `ADD` or `CALL`, expands into a specialized emitter function that prints specific LLVM-IR text sequences. For example, the `ADD` instruction generates `ptrtoint`, `add`, and `inttoptr` operations to handle Maru's pointer arithmetic.

### Where is the generation buffer flushed to produce final output?

The `emit-gen-buffer` function in `source/compiler/emit-late.l` (lines 45-71) handles the final emission stage. It iterates over the compiler's generation buffer and invokes the appropriate instruction emitters, writing the complete LLVM-IR text to the output stream.