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
along the compiler’s chain from the token stream to the heap narration, each
in a text form for reading and a JSON form for tools.
This page walks all fifteen 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
cd mica-container/examples/Choices
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'Each stage is a file named by its number and stage, Choices.040.spectra,
with its JSON twin beside it, Choices.040.spectra.json: the stage is the
file’s extension, so an editor opens the Spectra family in its own colours.
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. Its numbers give the order to read the stages in, and two stages run
out of that order, as the index says: dominance (060) is computed after
liveness (070) and the reaching definitions (080), and the memory narration
(140) comes from the obligation analysis, which runs right after generation.
The stages, by number:
| # | 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 — the graph code generation consumed |
| 055 | cfg-before-ssa | control-flow analysis — the graph the SSA form was built on |
| 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:
// 40: n := 27;
t1.3:int64 = copyLiteral 27:int64 #7
storeVariable t1.3:int64, v1.1:int64 #8This 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: 41050.cfg is the graph of the unit code generation consumed. At this tier
that is the unit the middle end hands on after leaving SSA form, whose
blocks are numbered anew, so the export also writes 055.cfg-before-ssa,
the graph of the Spectra unit the analyses below were computed on, whose
block numbers they use. At the debug tier the two files hold one graph:
+- control_flow_graph
+- block_count: 31
+- edge_count: 43060.dominance is the dominator tree over the blocks of
055.cfg-before-ssa, 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 over the unit 050.cfg
describes, and 080.reaching-definitions says which definitions can reach
each use in the unit 055.cfg-before-ssa describes. These are the classic
dataflow facts, 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.
What the numbers behind # count
The listings write # before a number in three stages, and the number
counts something different in each:
- In
040.spectraand100.out-of-ssa, a#Nat the end of an instruction is its value id: the number its unit gives the value the instruction defines, counting one up for every instruction that defines a value. Each unit keeps its own count:055.cfg-before-ssaand080.reaching-definitionsquote the Spectra unit’s numbers,050.cfgquotes the out-of-SSA unit’s, and090.ssanumbers its values afresh. The liveness writes no value ids. Near the top of a program the counts often agree, as#8above does; further down they part. - In
090.ssa, a#Nright after a name is that name’s version. Every assignment of a name makes a new version, sov1.1#2names exactly one definition ofn; the value id still ends the line, the form’s own count:090.ssanumbers this store 24, as040.spectradoes, and100.out-of-ssanumbers it 25:
storeVariable t1.15:int64, v1.1#2:int64 #24- In the AArch64
120.assembly, a#Nis an immediate operand: the instruction carries the value itself, so no line defines it. The x86-64 assembly writes its immediates bare, and starts its comments with#.
movz x25, #27The Mica extension for VS Code says the same in a hover over any #N, and
leads from the number to the line that defines it.
The names behind v1.1
From 040.spectra on, every parameter and variable is written under a flat
name that is unique in the unit: n is v1.1, the first variable of block
- The JSON twins of
040.spectra,090.ssa,100.out-of-ssaand110.placementkeep the name the program gave it and the token of its declaration, which the token stage turns into a line and a column:
"result": {
"name": "v1.1",
"variant": "variable",
"type": {
"$ref": "2"
},A temporary such as t1.3, and storage the compiler made itself, keeps its
flat name alone. The extension’s hover over a placement or liveness line
names the variable, v1.1 is n, with a link to its declaration, and the
placement view labels each register lane the same way.
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
055.cfg-before-ssa, then find the same blocks in090.ssa, whose phi functions name them by those numbers — 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.