Skip to content

Architecture

Eira edited this page Oct 31, 2026 · 7 revisions

Architecture

How a program gets from Lamina IR text to a running executable, and which crate owns each step.

The pipeline

  program.lamina          IR text
        │
        ▼  parser
   Lamina IR              typed, block-based, target independent
        │
        ▼  lowering
      LUMIR               machine IR: registers, machine types, still target independent
        │
        ▼  transform pipeline (--opt-level 0 .. 3)
      LUMIR               optimized
        │
        ▼  mir_codegen, per target
   program.s              native assembly
        │
        ▼  ras  (or the system as)
   program.o              relocatable object: ELF, Mach-O, COFF/PE
        │
        ▼  weld (or ld, lld, mold)
   program                executable

There is a second exit. With the encoder feature the JIT skips assembly text and encodes LUMIR straight to bytes in executable memory, which is what --jit and the lamina! macro use.

The crates

Crate Owns
lamina The driver and CLI, the parser, the transform pipeline, the per-target instruction emitters in src/mir_codegen/, and the JIT runtime
lamina-ir The IR itself: types, instructions, modules, IRBuilder, and the owned builder the C bindings use
lamina-mir LUMIR type definitions: modules, functions, instructions, registers, machine types
lamina-codegen Shared codegen machinery: ABI, stack frames, register allocation, and the target-support tables
lamina-platform Target vocabulary and host detection, including the architecture table every per-architecture property comes from. Split out to avoid a dependency cycle with ras
lamina-c The C API. Opaque handles, explicit ownership. Reports its own version through lia_version()
ras Assembler. Assembly text to object file, and the encoder the JIT uses
weld Linker. Objects to executable

A split that is easy to miss

Code generation lives in two places, and the split is not obvious from the names. lamina-codegen holds the shared pieces: ABI rules, frame layout, register allocation, and the table saying which architecture and OS pairs are supported. Everything an architecture states about itself — widths, byte order, alignments, interpreters, environments — is one step further out, in lamina-platform. The actual per-instruction emitters live in the root crate under src/mir_codegen/. If you are adding an architecture you will touch both, which is exactly the problem the codegen trait in 0.2 is meant to solve.

The IR's meaning is stated separately from its syntax. IR semantics is the normative page: numbered clauses saying what each operation computes, what traps, and which targets refuse it. Where this page or another one describes a behaviour in passing, that page is the one that decides.

Why two IRs

Lamina IR is what a frontend targets. It is typed and block-based, and knows nothing about registers or calling conventions. Keeping it target independent means a frontend author never has to think about machine details.

LUMIR is what the optimizer and the backends work on. It is assembly-shaped: virtual registers, machine types, explicit memory operands. Most optimization passes are easier to write here because the representation is closer to what will actually run.

The split costs a lowering step. It buys a frontend-facing layer that stays stable while backends change underneath it, which is the whole proposition of using Lamina as a backend at all.

Glossary

Lamina IR — the typed intermediate representation a frontend produces, in text form or through IRBuilder.

LUMIR — Lamina Unified Machine Intermediate Representation, the machine-level IR produced from Lamina IR. The lamina-mir crate defines it. Usually just called MIR in conversation and in the source.

Transform — one optimization pass over LUMIR. The set that runs depends on the --opt-level the driver was given.

Backend — the per-architecture emitter that turns LUMIR into assembly text.

Target name — <architecture>_<operating-system>[_<environment>], written x86_64_linux or x86_64_linux_musl. A sub-architecture may stand in place of the bare architecture: rv64gc, armv7a-neon, aarch64v8.2, ppc64le-power9. The parser recognizes more names than the driver accepts, and the driver accepts more than any one backend finishes. See the target table.

Environment — the fourth axis of a target: gnu, musl, lamina, eabi, eabihf or none. It decides the C library, the ELF interpreter, the C runtime startup objects and the compiler runtime. Left out of a target name, each architecture and operating system pair declares its own; none is freestanding and is what an operating system that supplies its own runtime takes. lamina names Lamina's own C library. It parses on x86_64 and aarch64 and nothing implements it, so the link refuses by name rather than borrowing another libc's loader and startup objects.

Compiler runtime — the routines code generation calls that the user did not write, such as __divti3. Lamina's own are in builtins/, written in Lamina IR and compiled per program for the target. They cover the 128-bit division, remainder and float conversion routines and the 64-bit forms of the same, so an architecture whose integer registers hold 32 bits gets its __divdi3 family from the same place. No host libgcc is required on any target. Where the host happens to have one, the GNU link path still names it after the compiled routines.

Host detection — working out the target of the machine the compiler is running on. It fails with a message naming the architecture or operating system when there is no backend for it, rather than guessing one. There is no unknown-architecture fallback.

AOT — ahead of time. The normal compile-to-executable path.

JIT — compiling into executable memory and calling it in the same process. --jit runs in a default build; the C bindings' JIT extension is the part behind nightly. See Releases.

ABI — the calling convention plus layout rules for a target. Which registers carry arguments, who saves what, how a struct is passed.

Relocation — a note in an object file saying "patch this address once you know where the symbol landed". The linker resolves them.

Red zone — a region below the stack pointer that leaf functions may use without adjusting rsp. Unusable in anything that takes interrupts.

Where to go next

Clone this wiki locally