Skip to main content

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 == ys and m == n lower 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>> and list<map<K, V>>. Both directions of collection nesting are now supported. The monomorphiser generates TU-local mochi_list_map_<K>_<V>_* and mochi_map_<K>_list_<V>_* helper families per translation unit.
  • List slices. xs[start:end] emits a mochi_list_<T>_slice call that bounds-checks both indices and returns a new list with a copied backing array.
  • In-place assignment. xs[i] = val and m[k] = val lower to mochi_list_<T>_set and mochi_map_<K>_<V>_set calls 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 strlen wrapped 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 using snprintf into a stack-local buffer that is then copied into the pool.
  • Format-string interpolation. f"hello {name}" lowers to a chain of mochi_str_concat calls 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. readFile and lines validate the entire buffer on load and abort with MOCHI_ERR_UTF8 on 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) and saveCSV(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:

TripleOSArch
x86_64-linux-gnuLinuxx86-64
aarch64-linux-gnuLinuxAArch64
x86_64-apple-darwinmacOSx86-64 (Rosetta)
aarch64-apple-darwinmacOSApple Silicon
x86_64-w64-mingw32Windowsx86-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 to api.openai.com.
  • Anthropic (claude-opus-4-7, claude-sonnet-4-6, claude-haiku-4-5) via libcurl to api.anthropic.com.
  • Google (gemini-1.5-pro, gemini-1.5-flash, gemini-2.0-flash) via libcurl to generativelanguage.googleapis.com.
  • llama.cpp local inference via the llama.cpp C API, gated behind MOCHI_LLM_HAVE_LLAMA at 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. TestPhase17IROrdering checks that the aotir IR 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.yml builds the hello fixture 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:

KernelInputMetric
binary_treesdepth 10wall time
fastaN=100,000wall time
spectral_normN=200wall time
n_bodyN=5,000wall time
nsieveN=10,000wall 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:

MEPTargetStatus
46Erlang/BEAMDraft, 20-phase impl spec landed
47JVM (Java bytecode)Draft, spec + 12 research notes
48.NET/CLRDraft, spec + 12 research notes
49SwiftDraft, spec + 12 research notes
50KotlinDraft, spec + 12 research notes
51PythonDraft, spec + 12 research notes
52TypeScript/JavaScriptDraft, 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, and int builtins 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-defined fun int(s: string) -> int works 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.