moxzi
Docs / Concepts / Architecture

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#

PartLanguageWhat it is
bin/compiler-canister/Main.mocompiler.wasmMotokoThe compiler, as an actor. VFS, job records, the step machine, the payment/escrow machine.
bin/linker-canister/Main.molinker.wasmMotokoEmbeds the RTS into an unlinked module, resolves relocations, strips link-only exports, re-attaches custom sections.
mops/mo-* (16 packages)MotokoThe compiler's source: lexer, parser, typer, lowering, IR passes, codegen, linker, scheduler.
moxzi/runtimeRustThe actor runtime — ic0 system API, message queues, rollback, timers, upgrades. Engine-agnostic behind a 7-method Machine trait.
moxzi/srcRustThe moxzi CLI: dependency resolution, the upload/step/link driver, the AOT module cache, --remote.
moxzi/serverRustmoxzid — the HTTP actor server.
moxzi/web + moxzi/web/libRust → wasm32, JSmoxzi-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.

PackageOwnsSize
mo-lib, mo-langutilsDiagnostics, source positions, environments~3.1k lines
mo-def, mo-types, mo-valuesSurface AST, type representation, runtime values~4.9k lines
mo-frontendLexer, parser, Resolve, Typing, Definedness, the prelude and internals sources~14.3k lines
mo-ir-defThe IR, Construct, Rename, Freevars, CheckIr~2.9k lines
mo-loweringDesugar — surface AST to IR~7.7k lines
mo-ir-passesEraseTypField, Show, Eq, AsyncLower, AwaitLower, Tailcall, ConstFold, DeadCode~4.3k lines
mo-wasm-astWasm AST plus the binary encoder/decoder~3.5k lines
mo-codegenCompileEnhanced and friends — IR to wasm~45k lines
mo-linkingLink, Rts, RtsBlobs — the RTS blobs and the linker~1.7k lines
mo-schedCtx, checkpoint, the heap and instruction valves438 lines
mo-sys, mo-fs-ic, mo-fs-wasiCapability-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.

  1. Resolve the closure. The CLI walks import declarations and mops sources to produce the full set of files a build needs. moxzi deps entry.mo prints exactly this list with the VFS path each file will take.
  2. 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.
  3. Start. start(Options) returns a JobId. The options record is closed, so an unknown field is a decode error rather than something silently ignored.
  4. Step. Call step(id) in a loop. Each call returns #more, #needClass path, #done blob, or #err (vec Message). #needClass means the job hit an imported actor class library and needs that library compiled and linked first; the driver builds it, calls provideClassWasm, 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.
  5. Link. #done hands 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 matches moc -no-link. The linker canister consumes it (link, or addChunk/linkChunks/getOut above 2 MiB) and returns a deployable module.
  6. Install. The result deploys to the IC with dfx canister install --wasm, runs under moxzid, and runs in a tab via moxzi-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 possibleWhy
Run compiler.wasm as a standalone wasm binaryIt exports canister methods, not _start. It needs an actor host.
Deploy the compiler's #done output directlyIt 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 pathThe stage-7 assembler reports EOP: LibU not supported. Program and actor entries work.
Swap in a different RTSThe linker embeds fixed blobs; the flavour is chosen from an enum, not supplied.
Compile incrementallyA job compiles one entry file and its closure from scratch. The whole-build cache keys on the whole input, not on changed files.

Next#

On this pageThe partsThe source ringsFrom .mo to a running actorThe runtime sideThings you cannot doNext