Compiling and linkingalpha
What do build, link and validate actually do, and when do I need each?
moxzi build is two steps glued together: the compiler turns Motoko into an unlinked wasm module, and the linker adds the runtime system that makes it a canister. The constraint worth internalising is that the compiler's own output is not deployable — it has no runtime system and still carries link-only exports — so linking is part of producing a canister, not an optional extra.
The pipeline#
| Step | Input | Output | Who runs it |
|---|---|---|---|
| resolve | entry .mo | the upload closure, VFS-named | moxzi (see moxzi deps) |
| compile | the closure | unlinked wasm + candid | compiler.wasm, driven message by message |
| link | unlinked wasm | deployable wasm | linker.wasm |
| validate | deployable wasm | pass/fail | an external wasm validator, not moxzi |
moxzi build does the first three by default. --no-link stops after the second. moxzi link runs the third on its own. Nothing in moxzi does the fourth.
Build#
moxzi build src/Main.mo -o main.wasm
Writes main.wasm and, unless --no-idl, main.did beside it. The .did is taken from the module's own candid:service metadata section, falling back to what the compiler reported. moc puts this behind --idl; here it is free and always wanted, because a canister without its interface file is not deliverable.
The metadata defaults follow dfx rather than moc: candid:service and candid:args are emitted public, so a deployed canister's interface is readable by dashboards and agents without controller rights. moc's raw default is all-private.
Two flags change how the compile runs without changing a byte of what it produces:
| Flag | What it changes | When to use it |
|---|---|---|
--stack-budget <N> | how often the compiler's scheduler yields (0 = the canister default, 200) | large projects — off-chain every yield is a self-call round trip, so a low ceiling costs minutes to hours |
--force-gc | collect garbage at every message boundary | when a long build's heap climbs; expensive in instructions, which does not matter locally |
Yields are checkpoints, so raising --stack-budget cannot change the output. The reason the default is low at all is the IC's small fixed stack, which does not apply locally.
Link#
moxzi build hello.mo -o u.wasm --no-link
moxzi link u.wasm -o u.linked.wasm
hello.mo -> u.wasm + u.did (66825 bytes, 1 file(s), 58 steps, 0.22s)
u.wasm -> u.linked.wasm (66825 -> 223251 bytes, 0.40s)
The size jump is the runtime system. Without -o, moxzi link writes <input>.linked.wasm.
You need moxzi link in exactly two situations:
- You compiled with
--no-link— usually to inspect the compiler's raw output. - You have an artifact from an on-chain compile. The compiler canister emits the same unlinked module;
--remotelinks it locally for you, andmoxzi linkis how you do it by hand.
Linking is deterministic and mechanical, which is why the on-chain path leaves it on the client without weakening its provenance claim. Moving it on-chain too is separate work.
The linker is found by the same search order as the compiler: --linker, then MOXZI_LINKER, then beside the binary, then ../lib/moxzi/linker.wasm, then bin/linker-canister/linker.wasm. moxzi info reports which one is loaded and its hash.
Note that a build containing an imported actor class needs the linker even with --no-link: each class library is compiled and linked as its own canister module before the entry starts, because the entry's compile consumes the linked bytes.
Validate#
moxzi has no validate subcommand. Validation is a wasm-level check, and the repo's gate does it with wasm-validate from wabt:
wasm-validate --enable-memory64 main.wasm
--enable-memory64 is required: the modules are memory64. scripts/moxzi_gate.sh runs exactly this after every gate build and reports ok build: … B, validates, or skips loudly when wasm-validate is not installed.
The trap to avoid: an unlinked module does not validate, and that is correct. If you validate a --no-link artifact and it fails, you have confirmed the flag worked, not found a bug. Validate the linked module.
Where the time goes#
moxzi build hello.mo -o hello.wasm --timings
rts: allocated 0.07 GB, reclaimed 0.06 GB, peak live 5 MB, heap 13 MB, committed 201 MB
on-chain estimate: ~79 rounds (~0.02 h at 1s/round, ~0.01 h at 0.4s) | dirty-budget floor 1 rounds
timings: module 0.033s (cached), sources 0.001s, instantiate 0.001s, compile 0.208s, link 0.358s (66825 -> 223251 bytes)
| Phase | What it is |
|---|---|
module | loading compiler.wasm; (cached) is the AOT cache, (JIT) is a cold ~1 s compile |
sources | resolving the closure from disk |
instantiate | creating the compiler instance |
compile | the compiler's own driver loop |
link | the linker canister over the unlinked module |
On a hello world the link genuinely costs more than the compile. That is the runtime system's fixed cost, and it does not grow with your program the way the compile does.
Watching a long build#
| Flag | Effect |
|---|---|
--progress <N> | report every N steps (0 = quiet, the default) |
--status-secs <S> | additionally name the compiler's current phase, at most every S seconds |
--max-steps <N> | give up after N steps (default 5,000,000) |
--status-secs costs a query per sample, and a query costs a state snapshot, so keep it coarse.
Things you cannot do#
| Why | |
|---|---|
Deploy a --no-link artifact | no runtime system, link-only exports still present — unlinked by design |
Link without linker.wasm | moxzi build fails up front and names every path it tried; --no-link is the escape only when no actor class libraries are involved |
| Type-check without emitting | no --check equivalent |
| Use the classical (non-EOP) backend | --no-eop is retired; its IR is never closed and it traps in ConstFold on any program, hello world included |
| Change the output by tuning the scheduler | --stack-budget and --force-gc change cost, never bytes — that is what makes them safe |
Next#
- Reading diagnostics — when the compile step fails instead of finishing.
- Coming from moc — how these steps line up with
moc -cand-no-link. - Projects and packages — how the closure that feeds this pipeline is built.