← Back to projects
Summary

Where a verified AST becomes runnable code. Generation targets MLIR rather than LLVM IR directly, using the standard Arith, SCF, Func, MemRef, Math and ControlFlow dialects, then runs a fixed sequence of conversion passes down to the LLVM dialect and out to LLVM IR. The generator itself is deliberately not one visitor: it is a thin dispatcher over nine services that each own one category of emission.

Stack

What it's built with.

MLIRLLVM IRC++17Code GenerationPass PipelinesC Runtime
Details

How it works.

Why MLIR instead of raw LLVM IR

Emitting LLVM IR directly means owning basic-block construction, branch wiring, and phi placement for every loop and conditional in the language. It is entirely doable and it puts a large amount of fiddly, easily-broken bookkeeping in the same file as the language semantics.

Targeting MLIR's structured dialects instead lets a loop be emitted as a loop and a conditional as a conditional, then handed to a conversion pass that turns structured control flow into blocks and branches correctly every time. The generator gets to be about Gazprea, and the lowering to machine-shaped IR is delegated to passes that already solved it.

Nine services instead of one visitor

Code generation attracts size faster than any other part of a compiler, because every language feature eventually needs to be emitted and the obvious home for that is one visitor with one method per node type. At the scale of a full language with vectors, matrices, tuples, structs and intervals, that file stops being reviewable.

Splitting emission into services keeps each one small enough to hold in your head, and it makes the boundaries useful: the matrix service is where shape-aware emission lives, the casting service is where representation changes live, and a bug in one is a bug in a file with a name that describes it.

Highlights

The things I'm proudest of.

  • ▹Emits into six standard MLIR dialects (Arith, SCF, Func, MemRef, Math, ControlFlow) rather than hand-rolling LLVM IR, so structured control flow is expressed structurally and lowered by passes that already know how, instead of by hand-managed basic blocks.
  • ▹Lowers through a seven-stage pipeline, FuncToLLVM, SCFToCF, MathToLLVM, ArithToLLVM, ControlFlowToLLVM, FinalizeMemRefToLLVM, and a reconcile-unrealized-casts pass to clean up the type material the conversions leave behind, with each stage its own PassManager so a failure names the stage it failed at.
  • ▹Split emission into nine services by concern: binary operations, loops, calls, subroutines, compound and aggregate values, matrices, casting, and shared utilities. A 5,000-line switch statement over node types is the default outcome here and it is unmaintainable by the third feature.
  • ▹Built staged dump levels into the lowering so the IR can be inspected after each individual conversion pass, which is the difference between debugging a lowering bug in minutes and bisecting it by deleting code.
  • ▹Gave matrices and aggregates their own dedicated services, since Gazprea's sized vector and matrix semantics carry shape information that scalar emission has no concept of.
  • ▹Links against a small C runtime for the operations that genuinely belong at runtime rather than in generated code, including input handling and runtime error reporting.