Every compiler you have ever used had these files in it — the token stream,
the tree, the flow graph, the liveness sets, the register map. Mica writes
them out. One flag, --export, and the build directory gains an export/
folder holding every intermediate representation of your program, numbered
in the order the phases ran, each in a text form for reading and a JSON form
for tools.
This page walks all fourteen over one real program,
examples/Choices
— the Collatz walk from 27, whose loops and branches give every stage
something honest to show.
One rule before the walk: these are diagnostic artifacts. Their names and
contents may change between releases — the export’s own index.txt says so
in its header — so read them, learn from them, build one-off tooling on them,
but do not build a product on their shape.
Getting the files
mica --compile --optimize release,full_optimizations --platform linux,arm64,utf-32 \
--source Choices.mica --build build --exportExporting intermediate representations of source 'Choices.mica' to 'build/export'The folder starts with index.txt, a one-page map of everything below it:
what compiled the program, which optimization set ran, and one line per
stage. The stages, in phase order:
| # | Stage | Produced by |
|---|---|---|
| 000 | build | the driver — platform, optimization set, link configuration |
| 020 | tokens | the scanner |
| 030 | ast | the parser and analyzer |
| 040 | spectra | the generator — the intermediate language |
| 050 | cfg | control-flow analysis |
| 060 | dominance | control-flow analysis |
| 070 | liveness | dataflow |
| 080 | reaching-definitions | dataflow |
| 090 | ssa | the middle end |
| 100 | out-of-ssa | the middle end |
| 110 | placement | the register and frame allocator |
| 120 | assembly | the emitter |
| 130 | optimization | every pass’s ledger |
| 140 | memory | the obligation analysis |
The front end: tokens and the tree
020.tokens is the source text as the scanner saw it — every token with its
line and column, which is exactly the coordinate every diagnostic prints:
19: program Choices;
19,1 program
19,9 identifier Choices
19,16 semicolon030.ast is the tree after semantic analysis: every node carries its
resolved type and its declaration, so what you read here is what every later
phase knows.
The intermediate language: Spectra
040.spectra is the program as flat three-address code — the language the
whole backend consumes, with the source lines riding as comments:
// n := 27;
t1.3:int64 = copyLiteral 27:int64
storeVariable t1.3:int64, v1.1:int64This is the seam of the architecture: everything above this file is the Mica front end, everything below it is the Dragon backend, and the file between them is one flat unit a different front end could produce.
The analyses: flow, dominance, liveness, reaching
050.cfg shows the basic blocks and edges the program’s control structure
became — the Collatz program’s for, while, case and repeat come out
as 30 blocks and 41 edges:
+- control_flow_graph
+- block_count: 30
+- edge_count: 41060.dominance is the dominator tree over those blocks, with each block’s
immediate dominator and dominance frontier — the structure SSA construction
stands on:
block 2 idom=1 rpo=2 children=[3 4 5] df=[2 8]070.liveness gives every name’s live range, and 080.reaching-definitions
says which definitions can reach each use. These are the classic dataflow
facts, computed over the same unit the flow graph describes, and reading
them beside a textbook chapter is the fastest way to make either concrete.
The middle end: into SSA and out
090.ssa is the unit in static-single-assignment form — every name defined
exactly once, the form the release optimizer works in. 100.out-of-ssa is
the same unit translated back, which is what release code generation
actually consumes. Diff the two against 040.spectra and you can watch the
optimizer’s work: stores forwarded to their loads, dead loads gone, loops
rotated.
The backend: places and instructions
110.placement answers the question every debugger session eventually asks
— where does everything live:
function main (block 1)
frame: locals 9, floats 0, caller-spilled 0 bytesEvery parameter, variable and temporary with its home, each function’s
activation record laid out field by field, and the registers held at each
point. 120.assembly is the instruction stream the backend built, before it
was written to the .s file — the same instructions, still carrying their
structure.
The ledgers: what the passes did, and what the heap owes
130.optimization is the honest accounting, pass by pass:
Static-single-assignment optimization statistics
Store-to-load forwards: 5
Dead loads eliminated: 14
Natural loops detected: 3
Loops rotated: 2
Division guards elided: 6No optimizer folklore — the numbers for your program, this build. And
140.memory is the heap narration: every allocation site and the lifetime
class the compiler proved for it. A program with no heap use exports an
empty narration, which is itself the statement.
Try it
- Export your own program at
--optimize debug, then atrelease,full_optimizations, and diff the two130.optimizationledgers: the debug tier’s ledger is nearly empty by design. - Find your innermost loop in
050.cfg, then find the same blocks in090.ssa— the loop preheader the optimizer inserted is a block your source never wrote. - Open
110.placementbeside agdbsession and check the frame layout against what the debugger shows. They agree, because they come from the same tables.