Mettle

Inside the compiler

Every stage can be asked what it did. The diagnostics and the optimization report are not decoration on top of the pipeline; they are the pipeline explaining itself.

source
  │  lexer            src/lexer
  ▼
tokens
  │  parser           src/parser
  ▼
AST
  │  semantic         src/semantic     types, borrows, memory, comptime
  ▼
checked AST
  │  monomorphize     src/semantic     one copy per set of type arguments
  ▼
  │  IR lowering      src/ir
  ▼
IR  ◄──────────────────────────────────── another frontend enters here
  │  optimizer        src/ir/optimizer
  ▼
optimized IR
  │  code generation  src/codegen      x86-64 / ARM64 / PTX / SPIR-V
  ▼
object
  │  linker           src/linker       internal PE, or ld / gcc / link.exe
  ▼
executable

Everything runs in one process. There is no external assembler, no assembly text anywhere in the pipeline, and no LLVM.

Lexer

Keywords resolve through two length-bucketed sorted tables: language keywords matched exactly, inline-assembly mnemonics and register names matched case-insensitively. A binary search within one length bucket settles a lookup in a few comparisons, which matters because most identifiers are not keywords. Identifiers are interned, so later phases compare pointers.

Reports E0001 when a byte cannot become a token.

Parser

Recursive descent. Error recovery resynchronizes at block boundaries, so one missing brace reports once rather than cascading, which is why a Mettle syntax error is usually one diagnostic and not a page of them.

Reports E0002.

Semantic analysis

Types, scopes, traits, comptime expansion, and the analyses that make this the loudest stage in the compiler: the borrow checker, the memory diagnostics, and the range checks. They run at compile time on code that is otherwise legal, which is why most of what they print are warnings.

Reports E0003, E0004, E0005, and the whole M0101 to M0119 range: use after free, double free, stack addresses that escape, leaks, borrowed pointers invalidated by realloc or free, null and out-of-bounds access, shift and divide constants, loop indexes that run past the end, and narrowing conversions.

IR lowering

A function becomes a list of named-value instructions with labels and branches. Types are attached to instructions, so code generation never re-derives one. This is the seam another frontend would enter through: everything below is libmtlc, and knows nothing about Mettle's syntax.

See it with --emit-ir, or --dump-ast for the stage before.

Optimizer

Around forty passes. Loop canonical form runs first, because every recognizer behind it assumes that form. Then the core simplifications run to a fixpoint: copy and constant propagation, CSE, branch simplification, reassociation, dead write elimination, jump threading. After those comes a recognizer worklist, then a tail of lowering cleanups.

A convergence check fails the build when the fixpoint does not settle, so a pair of passes that undo each other is caught rather than spun on.

Reports every decision code: the 24 reasons a loop did not vectorize, the 15 reasons a call was not inlined, and the 11 things the optimizer applied. This is what --explain prints.

Code generation

Four targets and one shared object writer. On x86-64 there are two backends and most functions take the first: MIR, a machine-level IR with real register allocation. It colours by Chaitin-Briggs, over the full general set plus xmm0 to xmm15, holding R10 and R11 back as scratch.

When MIR cannot take a function it falls back to a spill-everything backend that keeps every value on the stack. The backend report names which functions fell back and why, because that fallback is the largest thing that can quietly cost you performance.

See it with --annotate-asm for per-instruction provenance, or --profile-blocks for a block profile.

Linker

A complete PE linker of its own, plus ELF reading. Its distinctive rule is that runtime objects' symbols are overridable defaults, so a program defining strlen replaces the runtime's without a duplicate-symbol error, while the runtime keeps calling its own. Link-time section collection drops what nothing reaches.

Reports E0006 for I/O.

The interpreter

src/ir/ir_interp.c executes IR with a real memory model: strings with their bytes, structs, arrays, globals, heap allocation, closures. Four features run on it.

The same machinery is what lets --explain apply a suggested fix to a clone of your function, re-run the optimizer on it, and report what actually happened rather than guessing that the advice would work. That is why the report can say proven.

Further