May 2026 (v0.12.0)
v0.12.0 ships the complete Mochi-to-C AOT transpiler. Every phase of MEP-45 is now landed and the MEP is marked Final. You can take a Mochi source file and get a native binary on any of five tier-1 triples, a WASM/WASI module, or a single APE binary that runs on Linux, macOS, and Windows without recompiling. The runtime covers agents, channels, streams, try/catch, the query DSL, Datalog, and LLM generation. FFI reaches C directly and three scripting runtimes over subprocess RPC. The sanitiser suite (ASan, UBSan, TSan, MSan) is green across the full fixture corpus. Builds are reproducible: SHA-256 of every output binary is checked in CI and the IR ordering is deterministic.
This release also opens the next wave. Seven new MEPs (46-52) spec out the transpiler targets that follow C: Erlang/BEAM, JVM, .NET/CLR, Swift, Kotlin, Python, and TypeScript/JavaScript. MEP-46 ships full 20-phase implementation tracking pages alongside the spec.
1. C AOT transpiler: MEP-45 Final
The transpiler pipeline lives under compiler3/. The entry point is
mochi build, which parses Mochi source, runs the type checker,
lowers to the aotir IR, verifies the IR, emits C, and hands off to
a C compiler. The output is a self-contained native binary with no
runtime dependency on Go or the Mochi interpreter.
mochi build hello.mochi # host triple
mochi build hello.mochi --triple=aarch64-linux-gnu # cross-compile
mochi build hello.mochi --target=c-aot --emit=c # emit C source
1.1 Collections and nested types (phases 3.4c through 3.4h)
v0.11.1 shipped lists of scalars and lists of records. v0.12.0 adds the remaining nested forms:
- Typed-empty literal inference.
var xs: list<int> = []compiles without an explicit element-type annotation. The lower pass threads the declared type down into the literal so the monomorphiser picks the right per-T helpers. - Collection equality.
xs == ysandm == nlower to deep element-wise comparison helpers. The verifier checks that both sides carry the same concrete collection type before emitting the call. map<K, list<V>>andlist<map<K, V>>. Both directions of collection nesting are now supported. The monomorphiser generates TU-localmochi_list_map_<K>_<V>_*andmochi_map_<K>_list_<V>_*helper families per translation unit.- List slices.
xs[start:end]emits amochi_list_<T>_slicecall that bounds-checks both indices and returns a new list with a copied backing array. - In-place assignment.
xs[i] = valandm[k] = vallower tomochi_list_<T>_setandmochi_map_<K>_<V>_setcalls with the same bounds-check and key-miss behaviour as the read paths.
1.2 Sum types and pattern matching (phase 4)
Sum types lower to tagged C unions. Each variant is a struct with an integer discriminant followed by payload fields, and the whole family shares a tagged union type at the aotir level.
Pattern matching compiles through a Maranget decision-tree pass before
code generation. The pass is in lower/match.go and produces a
canonical decision tree over the discriminant field, avoiding
redundant tests when multiple arms share a common prefix. Exhaustiveness
is checked in the type checker (error T050) and a fallback abort()
is emitted in the switch default for defence in depth. A property
test drives 10,000 random ADT inputs through the Maranget pass and
checks that the emitted switch covers every input correctly.
1.3 Closures (phase 5)
Non-capturing closures emit as plain static C functions. Capturing closures heap-allocate an env struct that holds the free variables by value, and the closure itself is a fat pointer: a function pointer plus an env pointer. The fat-pointer ABI is consistent across HOF arguments, returns, and stored closure fields.
Free-function shims (__shim_<name>) let a named Mochi function be
passed as a first-class closure without wrapping it at every call
site. Agent methods use a variant of the shim where the receiver is
baked into the env (env == &receiver), so calling agent.method
as a closure works without any extra machinery at the call site.
1.4 Strings (phase 6)
The string runtime (mochi/strings.h + src/strings.c) uses
null-terminated C strings internally with a separate explicit-length
variant for binary data. Builtin operations:
- Concatenation via
mochi_str_concat(allocates into a pool per expression, not a global arena). - Length via
strlenwrapped in a bounds-checked accessor. - Indexing (
s[i]), substring (s[start:end]), contains, reverse. upper,lower,split,join.str(x)conversion from any scalar type usingsnprintfinto a stack-local buffer that is then copied into the pool.- Format-string interpolation.
f"hello {name}"lowers to a chain ofmochi_str_concatcalls over the literal segments and the converted sub-expressions. - File I/O:
readFile(path),writeFile(path, data),appendFile(path, data),lines(path). - UTF-8 validation on file read.
readFileandlinesvalidate the entire buffer on load and abort withMOCHI_ERR_UTF8on invalid sequences.
1.5 Error model (phase 7)
Error handling uses a setjmp/longjmp jump-buffer stack. Each function
that contains a try block allocates a mochi_jmp_t on its C stack
and pushes it onto a thread-local stack before the guarded body. A
catch pops the buffer and handles the thrown value. panic(msg)
longjmps to the nearest enclosing handler or terminates the process
with a formatted error if the stack is empty.
User-defined errors are ordinary Mochi values passed through the
throw mechanism. The type checker ensures catch arms cover the
thrown type. There is no hidden boxing: scalars passed to panic are
stored directly in the jmp payload union.
1.6 Query DSL (phase 8)
The query DSL compiles to C without any runtime library. A
from x in xs where pred select proj expression lowers to a for loop
over the source collection with an inline predicate test and a push
into a fresh output list.
More complex queries use a mochi_arena_t bump allocator scoped to the query expression. The arena is stack-allocated before the first loop and freed after the last projection, so intermediate join tables have no heap-allocation overhead.
Supported query forms:
from,where,select(basic filter-map).order by,skip,take(sort and pagination).- Inner join, left join, cross join.
loadCSV(path)andsaveCSV(path, rows)using a home-grown RFC 4180 CSV parser that handles quoted fields and escaped quotes.
1.7 Concurrency (phase 9)
The AOT runtime grows an M:N scheduler (sched.h + sched.c) built
on POSIX threads with a work-stealing run queue. Green goroutines are
not used; the scheduler maps Mochi tasks directly onto OS threads
with configurable parallelism.
Three concurrency primitives:
chan<T>: a bounded ring channel. Sends block when the ring is full; receives block when it is empty. Capacity is set at construction time.stream<T>: an MPMC broadcast channel. Every subscriber sees every message published after it subscribed, up to the stream's backpressure capacity. Overflow drops the oldest message and increments a dropped-message counter.- Agents. An agent declaration emits a C struct for the state, an
intent enum, and a dispatch loop that runs on a dedicated thread.
Synchronous intents (
call) block the caller until the agent responds. Asynchronous intents (send) return immediately.
Graceful shutdown is coordinated through a signal handler that sets a
global mochi_shutdown flag, drains in-flight intents, and waits for
agent threads to exit before returning from main.
1.8 FFI (phase 10)
Four FFI modes ship in v0.12.0:
C-direct. extern fun declarations lower to extern <ctype> <name>(<params>); in the C prologue and call through the standard C
ABI. No marshalling, no subprocess, no overhead beyond the function
call itself. A neighbour .c file is compiled alongside the generated
main.c so the extern symbols resolve at link time.
Boxed mochi_value_t. A 16-byte tagged union covering nil, bool,
int, float, str, and handle. Mochi functions that need to pass
heterogeneous values across an FFI boundary can declare parameters
and returns as value. The union fits in two 64-bit registers on
both System V AMD64 and AArch64 ABIs, so no stack spill for scalar
types.
Go FFI. extern go fun declarations compile the neighbour .go
file into a companion binary placed next to the output binary as
<out>_gorpc. The generated C wraps each Go function in a
mochi_go_<name>() helper that sends a newline-delimited JSON request
over a pipe pair and reads the JSON response. The companion binary
reads requests from stdin and writes responses to stdout.
Python and JavaScript FFI. extern python fun and extern js fun
follow the same subprocess JSON RPC protocol. The Python companion is
run via python3 <stem>.py and the JS companion via node <stem>.js.
All three non-C backends share the emitRPCFuncWrappers helper in
the emitter, so adding a fourth backend is a matter of wiring up the
subprocess launch and the per-type serialisation.
1.9 Cross-compilation (phase 11)
mochi build --triple=<triple> cross-compiles to any of five tier-1
triples:
| Triple | OS | Arch |
|---|---|---|
x86_64-linux-gnu | Linux | x86-64 |
aarch64-linux-gnu | Linux | AArch64 |
x86_64-apple-darwin | macOS | x86-64 (Rosetta) |
aarch64-apple-darwin | macOS | Apple Silicon |
x86_64-w64-mingw32 | Windows | x86-64 |
resolveCCForTarget in the driver selects the correct cross-compiler
(clang with a --target flag, or a prefixed GCC). The CI matrix
builds every fixture on every triple and checks output byte-equality
against the vm3 oracle.
1.10 WASM/WASI (phase 12)
mochi build --target=wasm32-wasi compiles to a WASM module via
zig cc --target=wasm32-wasi. The gate runs every fixture under
wasmtime run and compares output against the vm3 oracle.
The agent and stream surfaces are narrowed for WASM: chan<T>,
stream<T>, and agent declarations are accepted by the parser and
type checker but emit a compile-time error if the WASM target is
active, since the M:N scheduler requires pthreads. The full fixture
corpus (31 suites) passes under wasmtime on the CI matrix.
1.11 APE polyglot binaries (phase 13)
mochi build --target=c-aot-ape produces a single binary that runs
on Linux, macOS, and Windows without recompiling. The build uses
cosmocc from the vendored Cosmopolitan libc package.
resolveCosmoCC in the driver locates the toolchain and sets the
required linker flags. A cross-OS CI workflow builds the hello
fixture as an APE binary and runs it on ubuntu-latest, macos-latest,
and windows-latest in the same workflow run.
The Cosmopolitan libc imposes a few constraints that the runtime
respects: no sigaction with SA_ONSTACK, no pthread_attr_setstack,
and the shutdown handler uses atexit instead of signal disposition.
1.12 LLM generation (phase 14)
Mochi programs can call LLM providers through the generate
expression. The provider is selected at build time via --llm-provider
and at runtime via MOCHI_LLM_PROVIDER.
Provider support:
- OpenAI (
gpt-4o,gpt-4o-mini,gpt-4-turbo,gpt-3.5-turbo) via libcurl toapi.openai.com. - Anthropic (
claude-opus-4-7,claude-sonnet-4-6,claude-haiku-4-5) via libcurl toapi.anthropic.com. - Google (
gemini-1.5-pro,gemini-1.5-flash,gemini-2.0-flash) via libcurl togenerativelanguage.googleapis.com. - llama.cpp local inference via the llama.cpp C API, gated behind
MOCHI_LLM_HAVE_LLAMAat compile time.
The default mode is cassette replay. A cassette is a directory of
.json files keyed by a DJB2 hash of the prompt. If the cassette
contains a matching entry the response is returned immediately without
making a network call. MOCHI_LLM_CASSETTE_RECORD=1 switches the
live providers to record mode: responses are written to the cassette
directory so subsequent CI runs can replay them without credentials.
1.13 Datalog (phase 15)
The query DSL extends to Datalog through mochi_datalog.c. Facts are
loaded from Mochi lists into typed ETS-style C arrays at startup.
Rules are Mochi functions evaluated by a semi-naive fixpoint
evaluator: the evaluator maintains a delta table per relation and
iterates until no new tuples are added.
The magic-set transform (phase 15.1) rewrites goal-directed Datalog queries so only the relevant portion of each relation is evaluated. A compile-time range-restriction check ensures every rule body variable appears in at least one bound position so the evaluator terminates.
Stratified negation (phase 15.2) allows not in rule bodies when
the negation can be stratified: no stratum may depend on a negation
of itself. The stratifier runs at compile time and rejects programs
that would require unstratified negation.
1.14 Sanitiser matrix (phase 16)
The full fixture corpus runs under four sanitisers on every CI push:
- ASan + UBSan. Enabled by default for
--profile=debug. Catches buffer overflows, use-after-free, integer overflow, and undefined shifts. The full corpus is clean. - TSan. Thread sanitiser on the streams and agents corpus. All MPMC and agent interaction patterns are TSan-clean. The TSan run is separate because ASan and TSan cannot be combined.
- MSan. Memory sanitiser on Linux via clang. Catches reads from uninitialised memory. Runs on a curated subset of the corpus that excludes file I/O fixtures (MSan requires instrumented libc).
A nightly CI workflow runs the full corpus under each sanitiser independently and posts a summary comment on the weekly tracking issue.
1.15 Reproducibility (phase 17)
Two SHA-256 gates run on every PR:
- IR ordering gate.
TestPhase17IROrderingchecks that theaotirIR serialises to an identical byte string across two independent lowering runs of the same source. This catches any non-determinism in the lower pass (map iteration order, pointer comparison, etc.). - Binary gate.
transpiler3-c-release-sha256.ymlbuilds thehellofixture on two separate CI runners and compares the SHA-256 of the output binary. The runners use identical clang flags and strip the binary before hashing so symbol table pointer differences do not count as divergence.
Static linking (--profile=release --static) is supported on Linux
and Windows. Darwin does not support fully static executables, so
the release profile on Darwin links dynamically against the system
frameworks only.
Release SHA-256 checksums for every tier-1 triple are attached to
each GitHub release as sha256sums.txt.
1.16 Performance (phase 18)
Five BG kernel benchmarks run in CI for every release:
| Kernel | Input | Metric |
|---|---|---|
binary_trees | depth 10 | wall time |
fasta | N=100,000 | wall time |
spectral_norm | N=200 | wall time |
n_body | N=5,000 | wall time |
nsieve | N=10,000 | wall time |
The gate requires that the AOT binary runs within 3x of the Go
equivalent compiled with go build -o. Extended metrics beyond wall
time are also collected: peak RSS, compile time, and stripped binary
size. A per-release HTML bench report is generated by
bench/out/v0.12.0/ and attached to the GitHub release.
All five kernels pass the 3x gate on the CI hardware (ubuntu-22.04, 4-core x86_64) in this release.
2. Seven new transpiler MEPs
Seven MEP drafts open the multi-target expansion that follows MEP-45:
| MEP | Target | Status |
|---|---|---|
| 46 | Erlang/BEAM | Draft, 20-phase impl spec landed |
| 47 | JVM (Java bytecode) | Draft, spec + 12 research notes |
| 48 | .NET/CLR | Draft, spec + 12 research notes |
| 49 | Swift | Draft, spec + 12 research notes |
| 50 | Kotlin | Draft, spec + 12 research notes |
| 51 | Python | Draft, spec + 12 research notes |
| 52 | TypeScript/JavaScript | Draft, spec + 12 research notes |
Each spec covers the same territory as MEP-45: IR reuse, target-
specific runtime modules, type mapping, concurrency model, FFI
approach, sanitiser/reproducibility/performance gates, and packaging.
MEP-46 goes furthest: the 20 implementation tracking pages under
website/docs/implementation/0046/ are fully written with concrete
Go code structure, Erlang module layouts, Core Erlang constructor
calls, and test fixture descriptions for each phase.
The Erlang target is first in line because the BEAM's actor model
maps cleanly onto Mochi agents and streams. Agents lower to
gen_server callback modules. Streams use pg process groups for
MPMC broadcast. Async/await maps to spawn_monitor with the OTP 24+
recv-marker optimization for O(1) selective receive. There is no NIF
in the v0.1 cut: the entire Mochi runtime is pure Erlang.
3. Website improvements
The documentation site picks up two generator-level changes:
Wikilink plugin. [[MEP-45]] and [[phase-3.4b]] references in
any .mdx file are automatically resolved and rendered as links by a
remark plugin (website/scripts/remark-mochi-links.js). This makes
cross-referencing between specs, implementation tracking pages, and
release notes work without maintaining explicit href attributes.
Release sidebar. The changelog sidebar is now flat: every release gets its own entry rather than being grouped by minor version. This makes it easier to navigate directly to a specific patch release.
Sitemap coverage validation. website/scripts/check-sitemap.js
is added to the CI pipeline and verifies that every HTML route in the
built site appears in sitemap.xml. This catches pages that are
accessible but unlisted.
4. Bug fixes
min,max, andintbuiltins no longer shadow user-defined functions of the same name. The resolver now checks the user function table before falling back to builtins, so a user-definedfun int(s: string) -> intworks correctly.
5. Compatibility
The C AOT backend is additive. All existing Mochi programs continue to
run under vm3 with no changes. mochi run still uses vm3 by default.
mochi build is the entry point to the AOT path.
The --target=c-aot flag is stable. The --emit=c flag emits C
source instead of compiling it, which is useful for inspection and
for integrating into external build systems.
The -vm=vm2 flag is removed. It was deprecated in v0.11.0 and
promised removal in v0.12.0. Pass -vm=vm3 or nothing; vm3 is the
only supported interpreter.
6. Upgrade
curl -fsSL https://get.mochi-lang.dev | sh
mochi --version # 0.12.0
Or, with Docker:
docker pull ghcr.io/mochilang/mochi:0.12.0
Or, from source:
git pull && make build
Pre-built binaries for all five tier-1 triples are available on the
GitHub release page
along with sha256sums.txt.