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 --export
Exporting 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:

#StageProduced by
000buildthe driver — platform, optimization set, link configuration
020tokensthe scanner
030astthe parser and analyzer
040spectrathe generator — the intermediate language
050cfgcontrol-flow analysis
060dominancecontrol-flow analysis
070livenessdataflow
080reaching-definitionsdataflow
090ssathe middle end
100out-of-ssathe middle end
110placementthe register and frame allocator
120assemblythe emitter
130optimizationevery pass’s ledger
140memorythe 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    semicolon

030.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:int64

This 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: 41

060.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 bytes

Every 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: 6

No 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

  1. Export your own program at --optimize debug, then at release,full_optimizations, and diff the two 130.optimization ledgers: the debug tier’s ledger is nearly empty by design.
  2. Find your innermost loop in 050.cfg, then find the same blocks in 090.ssa — the loop preheader the optimizer inserted is a block your source never wrote.
  3. Open 110.placement beside a gdb session and check the frame layout against what the debugger shows. They agree, because they come from the same tables.