moxzi
Docs / Concepts / The compile pipeline

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:

phaseOwnsDiagnostics it can produce
parse-entryLex and parse the entry file, incrementally, scanning its importsM0000 category parse (including lexer: …), M0192, M0217, M0220 from the parser's post-parse transform
scan-lib-depsWalk a parsed library's own import declarations
parse-libsLex and parse each imported file in the closureas parse-entry
resolveSubstitute imports, apply --default-persistent-actors, run the frontend checksM0006 import cycle (Resolve), M0016 (Definedness), M0069
compile-enhancedThe 16-stage EOP pipeline: typecheck, desugar, IR passes, codegenM0020 unresolved import, M0050, M0220, and the other 60-odd M0* codes Typing emits
codegen-pausedOptional. Codegen inputs snapshotted, stage 15 pending
doneThe 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.

StageWhat runsModule
0Parse the compiler's own internals sourceParser, InternalsSource
1Internals typecheck init and gather (mints the first $top)Typing
2Internals typecheck, batching top-level declarationsTyping
3User typecheck init and gather (the second $top)Typing
4User typecheck, one declaration per stepTyping
5User desugar — surface AST to IRDesugar
6Internals desugarDesugar
7Assemble the pass input: prelude concat, actor-class rename, shared asyncFresh seedRename, Construct
8Erase type fieldsEraseTypField
9show expansionShow
10Structural equality expansionEq
11Async loweringAsyncLower
12Await loweringAwaitLower
13Tail-call optimisationTailcall
14Constant foldingConstFold
15Codegen: concludeModule / concludeActor, then the -no-link emitCompileEnhanced

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#

SymptomRead 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 errorStage 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 runNot a diagnostic path. See the instruction-limit discussion in Resumability.
#needClass <path> from stepNot 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 possibleWhy
Compile a bare module entry through the EOP pathStage 7 reports EOP: LibU not supported. Program and actor entries are supported.
Get partial output from a failed stageA job that reports #err is done; there is no "compile as far as you can".
Re-enter an earlier stageStages advance monotonically. To recompile, start a new job.
Run stages out of order or skip oneThe internals typecheck (0–2) runs on every build; there is no cached-prelude path today.
Attribute a cost to one stage from instructionsUsedA 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#

On this pageThe phasesInside compile-enhancedWhich stage produced my errorWhere the work actually isThings you cannot doNext