Architecturealpha
What are the moving parts, and how does a program get from .mo to running actor?
moxzi is one compiler, compiled to one wasm module, plus four hosts that run it — and the compiler is itself a canister, so every host is an Internet Computer runtime first and a build tool second.
That is the constraint the whole design falls out of. compiler.wasm is not a program with a main; it is an actor with an update method called step. To compile something you have to be able to run an actor: upload files into its virtual filesystem, start a job, deliver messages to it until it answers #done. A laptop, a server, a browser tab and a canister can all do that, which is why there are four front ends and only one compiler.
The parts#
| Part | Language | What it is |
|---|---|---|
bin/compiler-canister/Main.mo → compiler.wasm | Motoko | The compiler, as an actor. VFS, job records, the step machine, the payment/escrow machine. |
bin/linker-canister/Main.mo → linker.wasm | Motoko | Embeds the RTS into an unlinked module, resolves relocations, strips link-only exports, re-attaches custom sections. |
mops/mo-* (16 packages) | Motoko | The compiler's source: lexer, parser, typer, lowering, IR passes, codegen, linker, scheduler. |
moxzi/runtime | Rust | The actor runtime — ic0 system API, message queues, rollback, timers, upgrades. Engine-agnostic behind a 7-method Machine trait. |
moxzi/src | Rust | The moxzi CLI: dependency resolution, the upload/step/link driver, the AOT module cache, --remote. |
moxzi/server | Rust | moxzid — the HTTP actor server. |
moxzi/web + moxzi/web/lib | Rust → wasm32, JS | moxzi-web — the same runtime for a browser, plus the page/worker client library. |
The compiler core is pure Motoko with no dependency on any host. mops/mo-sched is the one package that exists because of the Internet Computer rather than because of Motoko: it owns the decision of when to end a message.
The source rings#
The mops/ decomposition mirrors the reference compiler's src/ layout one directory at a time, which is what makes a port checkable against an original.
| Package | Owns | Size |
|---|---|---|
mo-lib, mo-langutils | Diagnostics, source positions, environments | ~3.1k lines |
mo-def, mo-types, mo-values | Surface AST, type representation, runtime values | ~4.9k lines |
mo-frontend | Lexer, parser, Resolve, Typing, Definedness, the prelude and internals sources | ~14.3k lines |
mo-ir-def | The IR, Construct, Rename, Freevars, CheckIr | ~2.9k lines |
mo-lowering | Desugar — surface AST to IR | ~7.7k lines |
mo-ir-passes | EraseTypField, Show, Eq, AsyncLower, AwaitLower, Tailcall, ConstFold, DeadCode | ~4.3k lines |
mo-wasm-ast | Wasm AST plus the binary encoder/decoder | ~3.5k lines |
mo-codegen | CompileEnhanced and friends — IR to wasm | ~45k lines |
mo-linking | Link, Rts, RtsBlobs — the RTS blobs and the linker | ~1.7k lines |
mo-sched | Ctx, checkpoint, the heap and instruction valves | 438 lines |
mo-sys, mo-fs-ic, mo-fs-wasi | Capability-style Fs/Sys/Log, and the two filesystem backends | ~0.6k lines |
The RTS blobs in mo-linking/rts/ are the reference toolchain's runtime, embedded rather than reimplemented; scripts/embed-rts.mjs regenerates RtsBlobs.mo from them.
From .mo to a running actor#
The sequence below is the same whether the compiler is running under wasmtime on your laptop or as a canister on mainnet. moxzi/src/compile.rs and the on-chain driver make identical calls; only the host differs.
- Resolve the closure. The CLI walks
importdeclarations andmops sourcesto produce the full set of files a build needs.moxzi deps entry.moprints exactly this list with the VFS path each file will take. - Upload.
write(path, blob)for each file, into the caller's own VFS tree. On-chain this is metered (64 MiB per caller, 1 GiB total). A build from a registered mops tag uploads nothing — the canister already holds the content-addressed blobs. - Start.
start(Options)returns aJobId. The options record is closed, so an unknown field is a decode error rather than something silently ignored. - Step. Call
step(id)in a loop. Each call returns#more,#needClass path,#done blob, or#err (vec Message).#needClassmeans the job hit an imported actor class library and needs that library compiled and linked first; the driver builds it, callsprovideClassWasm, and resumes. On-chain the canister can also drive itself with a timer (sdStep/sdStatus) so no external caller has to hold an ingress open. - Link.
#donehands back an unlinked module — RTS imports unresolved, link-only exports still present, so a wasm engine will refuse to parse it. That is by design and matchesmoc -no-link. The linker canister consumes it (link, oraddChunk/linkChunks/getOutabove 2 MiB) and returns a deployable module. - Install. The result deploys to the IC with
dfx canister install --wasm, runs undermoxzid, and runs in a tab viamoxzi-web— same bytes in all three.
moxzi build hello.mo -o hello.wasm does all six and prints the provenance line naming the compiler that made the artifact, by hash.
The runtime side#
moxzi/runtime is a separate crate from the CLI for the same reason the compiler is a canister: three consumers would otherwise grow three implementations of IC message semantics. The scheduling logic — deliver a queued call as its own message, resume a caller's continuation through the function table, roll back a trapping message, fire a due timer between two messages — is written once against the Machine trait, and implemented twice: over wasmtime for the CLI and server, over the browser's own WebAssembly for the tab. moxzi/runtime/src/sys.rs holds the ic0 shims and the HostState both backends share.
Things you cannot do#
| Not possible | Why |
|---|---|
Run compiler.wasm as a standalone wasm binary | It exports canister methods, not _start. It needs an actor host. |
Deploy the compiler's #done output directly | It is unlinked. Link it first; a validator rejecting it is expected, not a bug. |
Compile a LibU (bare module) entry through the on-chain EOP path | The stage-7 assembler reports EOP: LibU not supported. Program and actor entries work. |
| Swap in a different RTS | The linker embeds fixed blobs; the flavour is chosen from an enum, not supplied. |
| Compile incrementally | A job compiles one entry file and its closure from scratch. The whole-build cache keys on the whole input, not on changed files. |
Next#
- The compile pipeline — which stage owns which part of the work.
- Resumability — why the step loop exists at all.
- Trust model — what each of the four hosts requires you to trust.