How the Maru LLVM Backend Emits Code: A Deep Dive into emit-llvm.l
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.
(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.
(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.
(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.
(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.
(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.land 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-instructionmacros), and buffer flushing (emit-gen-bufferfromemit-late.l). - Key functions include
emit-llvm-fn-typefor signatures,emit-llvm-callfor function calls, andcompile-definitionfor function bodies. - The entry point
run-compilercreates anllvm-compilerinstance 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →