The compile pipelinealpha
Which stage owns which part of the work, and which stage produced my error?
A moxzi compile is a job that moves through a sequence of named phases, each of which owns one transformation and reports its own diagnostics. The job's current phase is a query, not a log line.
The constraint is that the pipeline is a state machine on a job record rather than a stack of function calls. Nothing runs to completion in one go, so "which stage am I in" is observable at every moment — and so is "which stage did that come from".
The phases#
status(id) returns a Status record with phase, plus messages, yields and instructionsUsed. phase moves through this sequence:
phase | Owns | Diagnostics it can produce |
|---|---|---|
parse-entry | Lex and parse the entry file, incrementally, scanning its imports | M0000 category parse (including lexer: …), M0192, M0217, M0220 from the parser's post-parse transform |
scan-lib-deps | Walk a parsed library's own import declarations | — |
parse-libs | Lex and parse each imported file in the closure | as parse-entry |
resolve | Substitute imports, apply --default-persistent-actors, run the frontend checks | M0006 import cycle (Resolve), M0016 (Definedness), M0069 |
compile-enhanced | The 16-stage EOP pipeline: typecheck, desugar, IR passes, codegen | M0020 unresolved import, M0050, M0220, and the other 60-odd M0* codes Typing emits |
codegen-paused | Optional. Codegen inputs snapshotted, stage 15 pending | — |
done | The unlinked module and the .did are on the job | — |
A second, older path exists for non-EOP builds — desugar, compile-lower, compile-batch, compile-codegen-done, compile-link — routing through CompileActor / CompileActorNew rather than CompileEnhanced. Everything the four front ends ship uses the EOP path.
Inside compile-enhanced#
phaseDetail carries the stage. The stage list is fixed, and each stage's output is the next stage's input; job.eopStage is the number in the record.
| Stage | What runs | Module |
|---|---|---|
| 0 | Parse the compiler's own internals source | Parser, InternalsSource |
| 1 | Internals typecheck init and gather (mints the first $top) | Typing |
| 2 | Internals typecheck, batching top-level declarations | Typing |
| 3 | User typecheck init and gather (the second $top) | Typing |
| 4 | User typecheck, one declaration per step | Typing |
| 5 | User desugar — surface AST to IR | Desugar |
| 6 | Internals desugar | Desugar |
| 7 | Assemble the pass input: prelude concat, actor-class rename, shared asyncFresh seed | Rename, Construct |
| 8 | Erase type fields | EraseTypField |
| 9 | show expansion | Show |
| 10 | Structural equality expansion | Eq |
| 11 | Async lowering | AsyncLower |
| 12 | Await lowering | AwaitLower |
| 13 | Tail-call optimisation | Tailcall |
| 14 | Constant folding | ConstFold |
| 15 | Codegen: concludeModule / concludeActor, then the -no-link emit | CompileEnhanced |
Stages 8–14 are each a pull-driven { step; finish } machine stored on the job: step() processes one declaration synchronously and the message yields by returning, with no calls outstanding. The walk order is identical to the one-shot version, deliberately — order is byte-relevant, because con stamps, fresh ids and $r/0 minting all depend on it.
The pipeline is not "user code only". Stages 0–2 compile the compiler's own internals source on every build, which is why a hello-world still takes 52 messages on-chain.
Which stage produced my error#
| Symptom | Read it as |
|---|---|
M0000 with parse category, or a message beginning lexer: | parse-entry or parse-libs. Syntax, not types. |
M0006 import cycle through <path> | resolve. |
M0020 unresolved import <url> reported as a type error | Stage 2/4 typecheck — the import resolved to a path but not to a module. Check moxzi deps entry.mo for what the closure actually contains. |
M0220 this actor or actor class should be declared 'persistent' | Typecheck. --default-persistent-actors rewrites bare actors instead. |
| A trap with no diagnostic, after a long run | Not a diagnostic path. See the instruction-limit discussion in Resumability. |
#needClass <path> from step | Not an error. The job hit an imported actor class library; the driver compiles and links that library, calls provideClassWasm, and the next step resumes. |
On-chain, a trapping message rolls back state, so phaseDetail written inside it is lost — four stage labels were added that way and none ever appeared. Debug output goes to the canister log instead and is retained, which is why each IR pass prints its name before running: the last MEMPROF stage= line in the log names the stage that exceeded the message cap.
Locally, moxzi build --timings and the CLI's periodic step N … phase lines report the same thing without a query.
Where the work actually is#
Measured on the compiler self-compiling, codegen dominates everything else — stage 15 is where a multi-thousand-message build spends most of its messages, and where every memory governor in the system is pointed. The IR passes (8–14) are cheap in instructions but expensive in garbage: each pass rebuilds the whole IR, so the previous IR becomes garbage immediately, and the collector only runs between messages.
Things you cannot do#
| Not possible | Why |
|---|---|
Compile a bare module entry through the EOP path | Stage 7 reports EOP: LibU not supported. Program and actor entries are supported. |
| Get partial output from a failed stage | A job that reports #err is done; there is no "compile as far as you can". |
| Re-enter an earlier stage | Stages advance monotonically. To recompile, start a new job. |
| Run stages out of order or skip one | The internals typecheck (0–2) runs on every build; there is no cached-prelude path today. |
Attribute a cost to one stage from instructionsUsed | A step can span several stages via the batch loop, so per-stage profiling (PHASE_PROF) reports an upper bound, and the counter itself under-reports real billed work. |
Next#
- Resumability — why each of these stages checkpoints internally.
- Architecture — the modules behind each stage, and what happens after
done. - Byte-identical output — why stage order is not free to change.