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.
mettle testruns@testfunctions with no code generation at all.mettle traceprints a function's values line by line.--pgointerpretsmain()at compile time and counts calls, so layout and inlining decisions have real frequencies without a profiling run.--verifyexecutes each function before and after every pass and compares. A pass that diverges is quarantined for that function and the build continues from validated IR.
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
- Reading a diagnostic
- Reading the report
- Every code
- docs/ARCHITECTURE.md, the same tour in more depth