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

#StageProduced by
000buildthe driver — platform, optimization set, link configuration
020tokensthe scanner
030astthe parser and analyzer
040spectrathe generator — the intermediate language
050cfgcontrol-flow analysis — the graph code generation consumed
055cfg-before-ssacontrol-flow analysis — the graph the SSA form was built on
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:

    // 40: n := 27;
    t1.3:int64 = copyLiteral 27:int64 #7
    storeVariable t1.3:int64, v1.1:int64 #8

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

050.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: 43

060.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.spectra and 100.out-of-ssa, a #N at 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-ssa and 080.reaching-definitions quote the Spectra unit’s numbers, 050.cfg quotes the out-of-SSA unit’s, and 090.ssa numbers its values afresh. The liveness writes no value ids. Near the top of a program the counts often agree, as #8 above does; further down they part.
  • In 090.ssa, a #N right after a name is that name’s version. Every assignment of a name makes a new version, so v1.1#2 names exactly one definition of n; the value id still ends the line, the form’s own count: 090.ssa numbers this store 24, as 040.spectra does, and 100.out-of-ssa numbers it 25:
    storeVariable t1.15:int64, v1.1#2:int64 #24
  • In the AArch64 120.assembly, a #N is 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, #27

The 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

  1. The JSON twins of 040.spectra, 090.ssa, 100.out-of-ssa and 110.placement keep 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 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 055.cfg-before-ssa, then find the same blocks in 090.ssa, whose phi functions name them by those numbers — 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.