Skip to main content

MEP-45 research note 10, Build system

Author: research pass for MEP-45. Date: 2026-05-22 (GMT+7).

This note covers the build driver. The driver is the thing that takes a Mochi source tree and turns it into a binary. The transpiler is one stage of the driver.

1. Command surface​

mochi build [PATH] # build the current module
mochi build --target=x86_64-linux-musl PATH # cross-compile
mochi build --target=wasm32-wasi PATH # WASM/WASI
mochi build --apex PATH # Cosmopolitan portable binary
mochi build --release PATH # release profile
mochi build --secure PATH # release + CFI + safe-stack
mochi build --debug PATH # sanitisers on
mochi build --emit=c PATH # stop after C emission
mochi build --emit=mir PATH # stop after MIR
mochi build --emit=ast PATH # stop after parse
mochi build --out PATH/bin/foo PATH # output path
mochi build -j N PATH # parallelism
mochi build --no-cache PATH # bypass cache
mochi build --reproducible PATH # extra reproducibility flags

mochi run builds with --debug defaults into a cache and then executes. mochi test is mochi run against tests/*.mochi.

2. Output layout​

.mochi/cache/
ast/
<module-hash>.bin
mir/
<module-hash>.bin
c/
<module-hash>.c
<module-hash>.h
obj/
<target-triple>/
<module-hash>.o
bin/
<target-triple>/
<output-name>

All filenames are deterministic functions of (source hash, compiler flags, runtime version). A cache hit is "is the file already there?"; no metadata DB needed.

<module-hash> is a 128-bit BLAKE3 hash of:

  • The module's source bytes.
  • The hashes of every module it imports (recursive).
  • The transpiler version string.
  • The compilation profile name.

This makes the build "Bazel-shaped" without Bazel.

3. Pipeline stages​

source.mochi
|
parse --> AST (.mochi -> .ast.bin)
|
type-check --> typed AST + errors
|
elaborate --> MIR (.ast.bin -> .mir.bin)
|
monomorphise --> MIR (specialised)
|
lower --> C IR
|
print --> .c + .h (.mir.bin -> .c)
|
cc --> .o (per-target)
|
link --> binary

Each arrow is a cacheable step. Failures surface at the producing stage; downstream stages don't re-run.

4. C compiler invocation​

The driver shells out to a cc selected per target:

TargetDriver chosenFlags appended
host (current)${CC} or cc-std=c23 -O2 -g -Wall
native crosszig cc -target ${triple}same
WASIzig cc -target wasm32-wasi-O2 -g
APEcosmocc-O2
Windows MSVCclang-cl --target=x86_64-pc-windows-msvc/std:c17 /O2

The driver does not require the user to install LLVM separately; the default install bundles zig (small, MIT) for cross-compilation.

5. Cache strategy​

  • Per-module compilation. A change to one module rebuilds that module and re-links; modules whose hashes did not change stay cached.
  • Content-addressed; no mutable index.
  • The cache root is .mochi/cache/ per project; a global root at $XDG_CACHE_HOME/mochi/ is shared across projects when content matches.
  • Cache eviction: LRU on a --cache-max=10GiB budget.

6. Parallelism​

  • AST/MIR/C phases parallelise across modules with no shared state.
  • cc invocations parallelise across object files.
  • The link step is serial.

-j defaults to the number of cores.

7. The runtime library​

libmochi.a is an amalgamated static archive containing:

  • mochi/core (memory, types, errors, panic)
  • mochi/sched (M:N scheduler, fibers, channels, streams, agents)
  • mochi/query (Swiss-table, omap, sort, arena)
  • mochi/io (file, network, time, env)
  • mochi/text (utf8proc, simdutf, regex-Phase2)
  • mochi/data (JSON via yyjson, YAML via libfyaml, CSV)
  • mochi/net (libcurl-backed HTTP)
  • mochi/llm (provider abstraction)
  • mochi/ffi (Python embedding, Go RPC, TS via QuickJS-NG)

Per-target builds of libmochi.a live in prebuilt/<triple>/libmochi.a in the install tree. The driver picks the right one based on --target. Cross-compilation of the runtime itself happens at install time, or on first use, via zig cc.

8. APE / cosmocc​

The --apex mode uses cosmocc instead of zig cc. Output is a single .com file that runs on macOS, Linux, Windows, FreeBSD, NetBSD, OpenBSD without modification. The runtime is rebuilt against Cosmopolitan libc for this profile.

Limitations:

  • No dlopen (so Python FFI and llama.cpp local LLM are disabled).
  • No threading on Windows under the polyfill (so streams degrade to single-threaded).
  • Binary size larger (~5 MB minimum).

This is a niche but valuable feature for "ship one binary" stories.

9. WASM / WASI​

--target=wasm32-wasi produces a .wasm binary that runs under wasmtime, wasmer, wasmedge, or in a browser via wasi-snapshot-preview2 shims.

Limitations:

  • BDWGC disabled; we use a precise allocator with a shadow stack.
  • No threads (wasi-threads is unstable in 2026); scheduler falls back to single-fiber cooperative mode.
  • Asyncify is applied post-link for coroutine support.
  • No subprocess; Go and Python FFI disabled.

A separate --target=wasm32-emscripten for browser playground use emits with emscripten and Asyncify; needed because wasi-libc lacks DOM bindings.

10. Static vs dynamic​

Default: fully static link. Single binary, no runtime dependencies besides libc.

Optional dynamic mode (--shared) builds libmochi.so and links the final binary against it. Useful for memory-saving in distros that ship many Mochi binaries.

11. Debug info​

DWARF 5 on Linux, BSD, macOS; CodeView on Windows. The transpiler emits #line directives so the C compiler attaches Mochi source spans to every instruction. Split-debug-info (--gsplit-dwarf) on by default; the binary stays small.

For --release builds, debug info goes to a sidecar (foo.dSYM on macOS, foo.debug on Linux, foo.pdb on Windows). The main binary is stripped.

12. Symbol resolution​

The mangling rules in note 05 §3 guarantee no symbol collisions across packages. The link step resolves them via the system linker's normal rules; we do not use a custom linker.

For the mochi-core runtime, every public symbol is prefixed with mochi_ and exposed via a single header mochi/core.h. No "surprise" exports.

13. Embedding mochi-core in a host C program​

libmochi.a plus mochi/core.h is a valid C library. A host C program can call mochi_runtime_init, mochi_eval_str, or link against a transpiled .o directly.

Use case: a game engine in C++ that wants to script with Mochi.

14. Source maps​

For the WASM target, the driver emits a .wasm.map (V3 source map JSON) that points back to Mochi source. This lets browser dev tools show the right file:line on a stack trace.

15. Diagnostics post-processing​

The driver wraps the C compiler's invocation. On non-zero exit, it parses the gcc -fdiagnostics-format=sarif (or clang's -fdiagnostics-print-source-range-info) output and rewrites file:line refs from generated C to Mochi source using the #line map. The user sees foo.mochi:42:1: error: ..., not a generated-C path.

This is critical UX: 90% of compile errors should surface from the type-checker; the remaining 10% (linker errors, header conflicts, ICEs) must still be navigable.

16. Distribution​

The Mochi CLI distribution includes:

  • mochi (Go driver binary, ~12 MB)
  • zig bundled (small, MIT)
  • cosmocc bundled (optional, large; can fetch on demand)
  • runtime/include/mochi/*.h (runtime headers)
  • runtime/lib/<triple>/libmochi.a (per-target prebuilds for tier 1)
  • runtime/src/ (runtime source for cross-compile fallback)

Install methods:

  • Single-binary download from releases.
  • brew install mochi.
  • apt install mochi (PPA).
  • winget install mochi.
  • cargo binstall mochi (since the project tracks rust-toolchain features anyway).

17. Versioning​

The CLI follows SemVer. The runtime ABI is not stable across major versions; users are expected to rebuild. The transpiler's IR (MIR) is internal and unstable. The generated C is reasonably stable; we publish diff stats per release so users tracking the C output can see drift.

18. Open questions​

  1. Whether to ship zig (~80 MB) bundled or fetch on first use.
  2. Whether --apex is a tier-1 feature or experimental.
  3. Whether the per-module C cache is on by default or opt-in.
  4. Whether to expose a mochi build --plugin=<so> API for codegen plugins (e.g., a custom backend).
  5. Whether mochi build --emit=ir-pretty is worth shipping (dumps a human-readable MIR for the user to inspect).