Skip to main content

MEP 45. Mochi-to-C transpiler: ahead-of-time native binaries via C as target language

FieldValue
MEP45
TitleMochi-to-C transpiler
AuthorMochi core
StatusFinal 2026-05-26 09:06 (GMT+7)
TypeStandards Track
Created2026-05-22 17:55 (GMT+7)
DependsMEP-4 (Type System), MEP-5 (Type Inference), MEP-13 (ADTs and Match), MEP-41 (Memory Safety)
Research~/notes/Spec/0045/01..12
Tracking/docs/implementation/0045/

Abstract

Mochi today ships vm3 (mochi run) and a Go-embedded build path (mochi build) that bundles the existing runtime into a Go binary. Neither produces a portable single-file native artefact, and neither reaches architectures outside Go's host matrix. MEP-45 specifies a standalone ahead-of-time pipeline that closes both gaps from first principles: read Mochi source, type-check via the existing frontend (parser + type checker), monomorphise, lower to a small purpose-built IR (aotir), emit ISO C23 source plus a call into libmochi.a, drive a vendored cross-cc, and ship a statically linked native binary. The implementation lives in a fresh tree transpiler3/c/ and reuses only the parser and type checker; no code, IR, or runtime piece is shared with prior backends.

The pipeline owns every stage past the type checker. The build driver vendors zig cc (zig 0.16 series) as the cross-compiler so every tier-1 triple is buildable from any host: x86_64-linux-{gnu,musl}, aarch64-linux-{gnu,musl}, aarch64-darwin, x86_64-darwin, x86_64-windows-msvc (via clang-cl), x86_64-windows-gnu (via zig cc + mingw), and wasm32-wasi. The master correctness gate is byte-equal stdout from the produced binary versus vm3 on the entire fixture corpus, across every tier-1 target (via qemu-user-static for cross-arch on Linux CI). vm3 serves as the recording oracle for expect.txt goldens; the transpiler does not link against, embed, or otherwise depend on vm3's implementation.

Three load-bearing decisions:

  1. Full monomorphisation, not selective boxing. Every concrete instantiation gets a fresh C struct and a fresh set of helpers. Polymorphic recursion is forbidden at type-check time (Mochi already rejects it), so the instantiation set is finite. The boxed-value mochi_value type exists only at FFI boundaries and for the dynamic-dispatch corner of the query DSL, never on hot user-code paths.
  2. BDWGC default for v1, with the runtime layer kept GC-agnostic. MMTk (Rust-based, ~3 MB), Perceus reference counting (Koka), Bacon-Rajan cycle collection, and Immix variants are all viable; BDWGC wins v1 on integration cost, platform coverage (every tier-1 triple, including WASI with patches), and proven scale (Crystal, Vala, multiple Schemes). The runtime headers segregate GC operations behind mochi_gc_* thunks so a v2 swap to a precise GC does not touch generated code. WASM target disables BDWGC and uses a precise allocator with a shadow stack.
  3. setjmp/longjmp for try/catch, not zero-cost table-driven unwind. Itanium-style table unwind is faster on the happy path (zero runtime cost when no exception fires) but requires per-architecture frame metadata that we would have to emit or rely on libgcc_s/libunwind for. setjmp/longjmp works on every libc with no extra emit work, costs a register-save on try entry, and matches the Erlang-VM and the early-OCaml strategies. v2 may swap to libunwind once the cross-arch story is proven on the happy path.

The gate for each delivery phase is empirical: every Mochi source file in tests/vm/valid/, examples/v0.3/, examples/v0.4/, and examples/v0.5/ must compile via the AOT pipeline and produce stdout that diffs clean against the expect.txt recorded by vm3. Sanitiser-clean (ASan, UBSan, TSan, MSan, LeakSan) is the secondary gate, run nightly. Reproducible-bytes-across-two-hosts is the tertiary gate, run per release.

Motivation

Mochi today ships vm3 (mochi run) and a Go-embedded build path (mochi build) that wraps vm3 into a Go binary. Neither produces a distributable single-file native artefact. The Go-embedded variant requires the Go runtime, weighs in the tens of megabytes, and can only target architectures the Go toolchain reaches.

A from-first-principles AOT pipeline that emits C resolves three concrete user-facing limitations:

  1. Distribution shape. Shipping a Mochi program today means shipping vm3 (or the Go-embedded wrapper). The AOT binary is one statically linked native ELF/Mach-O/PE per triple, with no runtime dependency beyond libc, ranging from ~3 MB hello-world (release/strip) up to ~12 MB for a program that pulls in the full runtime (HTTP, LLM, FFI).
  2. Architecture surface. Every Linux distro on every architecture, every modern macOS, every Windows host, every WASI runtime, every BSD, eventually Android and iOS. zig cc cross-compiles to all of these from any host. The Go-embedded build path covers fewer targets and binds Mochi's release cycle to Go's.
  3. Performance ceiling. vm3 lands in the 3-5x-of-Go band (MEP-39 §6.16 close-out, recorded as the prior baseline). The AOT path inherits Clang/GCC -O2/-O3 codegen quality, link-time optimisation, dead-code elimination, and architecture-specific tuning, which on tight numeric and dataset workloads consistently lands within 1.5x of equivalent hand-written C.

A secondary motivation: AOT opens embedding. libmochi.a plus mochi/core.h is a valid C library; a host C/C++ program (game engine, simulator, embedded controller) can call mochi_runtime_init and link a transpiled .o directly. Cosmopolitan via cosmocc produces a single APE binary that runs unmodified on macOS, Linux, Windows, FreeBSD, NetBSD, and OpenBSD. Neither is reachable via the Go-embedded path.

Specification

This section is normative. Sub-notes under ~/notes/Spec/0045/01..12 are informative.

1. Pipeline and IR

MEP-45 owns every stage past the type checker. The pass order:

Mochi source
│ parser (MEP-1/2/3, reused)

AST
│ type checker (MEP-4/5/6, reused)

Typed AST
│ monomorphise (MEP-45 pass 1)

Monomorphic typed AST
│ lower (MEP-45 pass 2)

aotir (MEP-45's own IR; see below)
│ match-to-decision-tree (MEP-45 pass 3; Maranget)

aotir (matches lowered)
│ closure-convert (MEP-45 pass 4)

aotir (closures lowered)
│ emit (MEP-45 pass 5)

ISO C23 source + #line side-map
│ cc (vendored zig cc by default)

Native single-file binary

aotir is MEP-45's own lowering IR, sized to the C codegen's needs. It is not reused from any other backend. Properties:

  • Fully monomorphised. No type variables; every collection and ADT carries concrete type arguments.
  • Fully type-elaborated. Every expression has a resolved type; every implicit conversion is materialised as an explicit cast node.
  • Closure-explicit. Function values carry an explicit environment-struct type after closure-conversion (pass 4); free-variable capture is no longer implicit.
  • Match-explicit. Pattern matches lower to a Maranget decision tree (pass 3); no match node survives past that pass.
  • Effect-annotated. Every call site that may panic carries a marker so the emit pass knows where to plant setjmp/longjmp machinery.

The codegen visits each aotir type exactly once via a memoised lower_type(aotirType) → CType. Memoisation guarantees that two structurally identical types share a single C type and a single set of helpers. See note 06 §12.

2. Name mangling

Mangled identifier form:

{pkg}__{module}__{name}[__{instArgsHash6}]

where pkg is the source package path with / replaced by _, module is the file stem, name is the source identifier, and instArgsHash6 is the first 6 hex digits of a BLAKE3 hash over the canonical printing of the instantiation arguments (omitted for non-generic symbols). Two emitted identifiers never collide across packages, generic instantiations, or arity-overloaded constructors. See note 05 §3.

3. Type lowering table

Mochi typeC typeNotes
intint64_t (alias mochi_int)two's complement per C23 mandate
floatdouble (alias mochi_float)IEEE 754 double
boolbool (<stdbool.h>)
stringmochi_strimmutable UTF-8 slice + precomputed hash
timeint64_t (mochi_time)ns since Unix epoch UTC
durationint64_t (mochi_dur)ns
?TT for pointer-shaped T (niche), else tagged structnote 06 §3.1
list<T>mochi_list__Tgrowable dense vector
map<K,V>mochi_map__K_VSwiss table
omap<K,V>mochi_omap__K_VSwiss table + insertion-order list (query DSL)
set<T>mochi_set__TSwiss table with elided value slot
stream<T>mochi_stream__T *bounded ring + subscriber list
chan<T>mochi_chan__T *bounded ring, point-to-point
record Rstruct pkg_Rpacked in source field order
sum Sstruct pkg_S with tag + union urecursive variants box payload
fun(A,B):Cmochi_closure__C_A_Bfat pointer (code + env)
agent Astruct pkg_A with embedded mailboxnote 09 §4

4. Expression and statement lowering

Expressions lower to single-assignment C temporaries with explicit casts on every implicit conversion. Short-circuit && / || lower to C short-circuit operators. Integer arithmetic uses C operators (two's complement; division by zero raises MOCHI_ERR_DIVZERO in the checked profile, UB in --fast-int). Float arithmetic preserves IEEE 754 NaN propagation. String + lowers to mochi_str_cat; s[i] returns the i-th code point as a one-character mochi_str.

Collection literals follow this rule: a literal inside a function body always allocates a fresh collection per call; a literal in a top-level let is emitted as a static const array when every element is a compile-time constant.

if, while, for in, match, return, break, continue lower to the obvious C forms. for x in xs iterates by index on lists, by paired iterator (insertion order) on maps, and by subscription callback on streams (the surrounding function must therefore be on a fiber).

match lowers via Maranget's decision-tree algorithm (Compiling Pattern Matching to Good Decision Trees, ML Workshop 2008). See note 05 §10.

try { ... } catch e { ... } lowers via setjmp/longjmp with a per-thread exception jump-buffer stack. See note 05 §9.

5. Closures

Free functions and methods lower to plain C functions. Closures are fat pointers (code, env):

typedef struct {
bool (*code)(void *env, mochi_int a, mochi_str b);
void *env;
} mochi_closure__bool_i64_str;

Free functions adapted into closures get an env == NULL shim. Method values get an env == self shim. Closure envs are heap-allocated when they escape; v1 always heap-allocates (conservative). The @no_escape attribute is Phase 2. See note 06 §4.

6. Runtime library

Single static archive libmochi.a plus headers mochi/include/mochi/*.h. Subsystems (note 04, note 10 §7):

  • mochi/core: memory, types, errors, panic
  • mochi/sched: M:N work-stealing scheduler over minicoro, channels, streams, agents
  • mochi/query: cwisstable Swiss tables, omap, sort, arena
  • mochi/io: file, network, time, env
  • mochi/text: utf8proc + simdutf
  • mochi/data: yyjson (JSON), libfyaml (YAML), home-grown CSV
  • mochi/net: libcurl-backed HTTP (H1/H2/H3 via nghttp2/ngtcp2)
  • mochi/llm: provider abstraction (OpenAI, Anthropic, Google, llama.cpp local)
  • mochi/ffi: C direct, Go via Unix-domain RPC or c-archive, Python via embedded libpython3 or RPC, TS via QuickJS-NG

7. Memory management

Default GC: BDWGC, conservative, incremental, generational mode on. Allocations route through mimalloc with BDWGC tracing on top. Rationale: mature, every tier-1 platform, ~150 KB static, pause times within target on the BG corpus. The runtime is GC-agnostic by design so v2 can swap in MMTk (precise) or Perceus + Bacon-Rajan (refcount + cycle collection) without touching generated code. WASM target disables BDWGC and uses a precise allocator with a shadow stack.

The query pipeline uses arena allocation: intermediates live in a mochi_arena released at the query boundary; the surviving result list is copied to GC. See note 08 §9.

8. Concurrency

M:N work-stealing scheduler, one OS thread per hardware core, lightweight fibers (~32 KB initial stack) on minicoro. Blocking syscalls execute on an overflow pool. Streams are MPMC broadcast channels with a bounded ring; emit blocks the caller when full (v1 default). Agents are records with a mailbox; intent calls enqueue typed messages; the agent's run-loop processes them in order on a dedicated fiber. CPU preemption (Go-style signal preemption) is not in v1.

9. Error model

setjmp/longjmp with a per-thread exception jump-buffer stack. Throwing functions need no annotation; any function may panic. Built-in codes:

CodeNameSource
-1MOCHI_ERR_FETCHnetwork or HTTP non-2xx
-2MOCHI_ERR_PARSEJSON / YAML / CSV decode
-3MOCHI_ERR_TYPEruntime type mismatch
-4MOCHI_ERR_INDEXOOB index / missing key
-5MOCHI_ERR_DIVZEROinteger divide by zero
-6MOCHI_ERR_OVERFLOWinteger overflow (debug only)
-7MOCHI_ERR_FFIFFI subprocess failure
-8MOCHI_ERR_LLMprovider error from generate
-9MOCHI_ERR_ASSERTexpect false

User error codes are positive integers.

10. ABI and target portability

Target: ISO C23 (-std=c23) with a C17 fallback when a target compiler lacks C23. Hard floor: C11.

Tier-1 (gated on every PR):

x86_64-linux-gnu, x86_64-linux-musl,
aarch64-linux-gnu, aarch64-linux-musl,
aarch64-darwin, x86_64-darwin,
x86_64-windows-msvc (via clang-cl),
x86_64-windows-gnu (via zig cc + mingw),
wasm32-wasi

Tier-2 (nightly): aarch64-windows-msvc, riscv64-linux-gnu, armv7-linux-gnueabihf, BSDs, wasm32-emscripten.

Tier-3 (best effort): Android, iOS, loongarch64, s390x, powerpc64le.

ABI: standard system V on Linux/BSD, AAPCS64 on aarch64, Windows x64 on MSVC, RISC-V LP64D on riscv64. We do not introduce a custom calling convention. We avoid C variadics (...) because of the AAPCS64 Apple-silicon surprise; explicit overloads or struct args instead. See note 07 §4.

11. Build driver

mochi build [PATH]
mochi build --target=TRIPLE PATH
mochi build --release PATH
mochi build --debug PATH
mochi build --secure PATH
mochi build --apex PATH # Cosmopolitan one-binary
mochi build --emit={ast|mir|c} PATH
mochi build --out PATH PATH
mochi build -j N PATH
mochi build --reproducible PATH

Cache layout under .mochi/cache/{ast,aotir,c,obj,bin}, content-addressed by a BLAKE3 hash over (source, transitive imports, profile, transpiler version). A cache hit is "is the file present?", no metadata DB. Cross-compilation defaults to a vendored zig cc (0.16 series); the vendor step lives in transpiler3/c/toolchain/zig/ and is invoked by the build driver on first use per cache directory. See note 10.

12. Reproducibility

SOURCE_DATE_EPOCH honoured. __DATE__/__TIME__ never embedded. -ffile-prefix-map=$PWD=. and -fdebug-prefix-map=$PWD=. strip absolute paths. All non-libc deps static-linked. Functions and globals ordered by sorted IR identifier, not by hash-map iteration order. Sample artefact SHA-256 published per release. See note 07 §7.

13. Hardening

Release profile defaults: -fstack-protector-strong, -D_FORTIFY_SOURCE=3, -fPIE, -Wl,-z,relro,-z,now, -Wl,--icf=safe, -fcf-protection=full on x86 CET, -mbranch-protection=standard on aarch64, /guard:cf on MSVC. --secure profile adds CFI (-fsanitize=cfi-icall), SafeStack, hardened allocator (mimalloc-secure or scudo), ShadowCallStack on aarch64.

14. Diagnostics

The compiler3 type-checker surfaces nearly all errors with Mochi spans. For errors that escape into the C compiler (linker errors, header conflicts), the build driver post-processes the compiler's output and rewrites file:line refs back to Mochi spans via the #line side-map. Include-cascade noise is collapsed. See note 07 §10.

15. Debug info

DWARF 5 on Linux/BSD/macOS, CodeView on Windows. Split-debug-info on by default. #line directives on every emitted statement so gdb/lldb/DAP show Mochi source. Release builds put debug info in a sidecar (.dSYM/.debug/.pdb).

16. Output style

Generated C is reviewable by a human (note 07 §9): two-space indent, one decl per line, braces on every block, no macros in generated code (only in runtime headers), no reserved-identifier reuse, leading source-comment per function, #line directives throughout.

Rationale

Why C, not LLVM IR, WASM, Rust, or a JIT-only path

  • LLVM IR. Heavier toolchain (~300 MB), moving target across versions, harder to cross-compile without our own driver, no source-readable output for review.
  • WASM as primary. Native perf gap (20-40% behind on WAMR/wasmtime), no portable threads in 2026, GC story still incomplete on common runtimes. WASM is a target of the C path, not the IR.
  • Rust as target. Compile times destroy iteration; borrow checker rejects most generated code without an Rc/RefCell sea that defeats the safety.
  • C++/Zig. C++ slower compile times, worse template errors; Zig pre-1.0. Neither buys us anything over C with a bit more code.
  • JIT-only. Per-arch engines months each; W^X gets harder every year on macOS/iOS/Android; harder to debug; harder to sandbox. A JIT path is complementary to AOT, not a replacement; MEP-45 is the AOT story end-to-end.

C gives the project a stable, slow-moving language (C23 is the most ambitious C standard in 30 years), a vast toolchain ecosystem, and human-readable output a security reviewer can audit.

Why monomorphisation, not boxed values

Mochi's generics are limited to type parameters on collections and user record/sum types (no higher-kinded types). Polymorphic recursion is rejected at type-check time, so the instantiation set is finite. Full monomorphisation produces typed C structs and tight numeric loops with predictable layout. Boxed mochi_value exists only at FFI boundaries.

Why differential testing as the master gate

vm3 is the existing source of truth. Byte-equal stdout from the AOT binary versus vm3, on every fixture, is the strictest behaviour check available. vm3 is used here only as the recording oracle for expect.txt; the transpiler does not consume any of vm3's IR, runtime, or codegen. Property tests, fuzzing, and reproducibility are secondary gates layered on top.

Why BDWGC for v1

Mature, every tier-1 platform, ~150 KB static, pause times within target on BG corpus, proven at scale by Crystal, Vala, multiple Schemes. Swap path to a precise GC (MMTk) or refcount + cycle collection (Perceus + Bacon-Rajan) is open because the runtime layer is GC-agnostic.

Backwards Compatibility

Additive. mochi run and mochi test keep vm3 by default. mochi build gains --target=c (and indirectly every triple flag like --target=x86_64-linux-musl). No language surface change; no stdlib surface change beyond mirroring vm3's existing user-visible exposure inside the C runtime.

Observable behaviour must match vm3 byte-for-byte on the fixture corpus. Programs relying on implementation-defined vm3 behaviour (allocation order observable through mochi_runtime_stats, GC pause timing) are explicitly non-portable; the spec already disallows reliance on these.

Reference Implementation

Code lives under a single fresh tree transpiler3/c/, with no sharing of IR, runtime, or driver with any prior backend. The shape:

TreePurpose
transpiler3/c/aotir/the aotir types and verifier
transpiler3/c/lower/typed AST → aotir (passes 1-4: monomorphise, lower, match-to-tree, closure-convert)
transpiler3/c/emit/aotir → ISO C23 source + #line side-map (pass 5)
transpiler3/c/build/driver: write source, invoke cc, link, cache (.mochi/cache/)
transpiler3/c/toolchain/zig/vendored zig cc 0.16 series + install/upgrade flow
transpiler3/c/runtime/include/public runtime headers (mochi/*.h)
transpiler3/c/runtime/src/runtime source compiled into libmochi.a
tests/transpiler3/c/fixture corpus, expect files, integration tests

The phased delivery plan is the §Phases section below. Each phase ships as a sub-PR auto-merged per the project's auto-ship convention; tracking pages live under /docs/implementation/0045/ and get filled in along the way.

Phases

The plan walks the language surface bottom-up (Phases 0-10), then expands the target matrix (Phases 11-13), then completes the feature surface (Phases 14-15), then layers the quality gates (Phases 16-18), and culminates in v1.0 (Phase 19). The numbering is dependency order, not calendar. Phases 0-10 are strictly sequential because each adds machinery the next assumes. Phases 11-15 can run in parallel after Phase 10 lands. Phases 16-18 run after Phase 15 lands and continue in perpetuity.

Phase conventions:

  • Gate. A single measurable criterion. A phase is LANDED only when its gate is green on every target listed.
  • Targets. Tier-1 triples in scope at this phase. Missing tier-1 rows at phase boundary become N.1, N.2, ... sub-phases per the umbrella-phase coverage rule.
  • Status / Commit columns. Filled in along the way as work lands. Status values: NOT STARTED, IN PROGRESS, BLOCKED, LANDED, DEFERRED. Commit is the merge commit short SHA on main.
  • Goal-alignment audit. Before a phase starts, a one-paragraph audit on its tracking page confirms the gate moves the user-facing goal ("ship a Mochi program as a single native binary on this target"), not spec-internal scaffolding.
  • Spec-in-sync. The PR that lands a phase's code must also update this MEP file (close out the phase block, update Status / Commit) and the tracking page under /docs/implementation/0045/.
  • Reference oracle. Fixture goldens (expect.txt) are recorded by running the same source through vm3. The gate diffs the AOT binary's stdout against that golden. vm3 is the oracle only; it is not linked, imported, or otherwise depended upon by transpiler3/c/.

Phase 0. Spec freeze and skeleton trees

FieldValue
StatusLANDED 2026-05-22 19:01 (GMT+7)
Commit— (PR #22069)
GateThis MEP merged on main; transpiler3/c/{aotir,lower,emit,build,toolchain/zig,runtime/{include,src}}/doc.go compile clean and report zero tests; tests/transpiler3/c/ exists with a README.md; implementation tracking pages for every phase exist under /docs/implementation/0045/; sidebar entries visible on the website
Targetsn/a (paperwork phase)
Tracking/docs/implementation/0045/phase-00-skeleton

Sub-phases

#ScopeStatusCommit
0.0This MEP merged with refactored framing, §Phases section, implementation tracking docs, sidebar wiringLANDED 2026-05-22 17:42 (GMT+7)— (PR #22067)
0.1transpiler3/c/{aotir,lower,emit,build,toolchain/zig,runtime/{include,src}}/doc.go compile clean on every host (go vet, go test returns no test files)LANDED 2026-05-22 19:01 (GMT+7)— (PR #22069)
0.2tests/transpiler3/c/README.md documents fixture layout and naming conventionLANDED 2026-05-22 19:01 (GMT+7)— (PR #22069)

Test set. Documentation/website build only: npm run gen:meps && npm run build clean; go vet ./transpiler3/c/... clean.

Risks. None substantial; paperwork phase.

Phase 1. Hello world

FieldValue
StatusLANDED 2026-05-22 19:50 (GMT+7)
Commit— (PR #22073)
Gatemochi build --target=c-aot --out=/tmp/hello tests/transpiler3/c/fixtures/hello/hello.mochi && /tmp/hello | diff - tests/transpiler3/c/fixtures/hello/expect.txt exits 0 on host triple (Phase 1.1 onward; Phase 1.0 ships the in-process Driver.Build only). --target=c-aot is the staging target value used until MEP-42's --target=c is retired; the eventual one-flag shape is mochi build --target=c <file> --out=<path>
Targetshost triple only
Tracking/docs/implementation/0045/phase-01-hello-world

Sub-phases

#ScopeStatusCommit
1.0Source-to-binary minimum: parser reused; lower (one fn returning unit, one print(string) call); emit; build via host cc discovery ($CC, then cc, then clang, then gcc); single integration test passesLANDED 2026-05-22 19:50 (GMT+7)— (PR #22073)
1.1--target=c-aot, --out PATH, --emit=c|executable CLI flags wired through cmd/mochi/main.go; --emit=c dumps the generated C as <out>.c next to the binary for reviewLANDED 2026-05-22 19:50 (GMT+7)— (PR #22073)
1.2.mochi/cache/ BLAKE3 content-addressed cache layer (default root: $HOME/.mochi/cache, override via MOCHI_CACHE_DIR); rebuild on unchanged source is a copyFile no-op; cache key is BLAKE3 over (transpiler-version, profile, target-triple, runtime-fingerprint, source-path, source-bytes); --emit=c builds bypass the cacheLANDED 2026-05-22 19:50 (GMT+7)— (PR #22073)
1.3Fallback to vendored zig cc when host cc discovery fails; transpiler3/c/toolchain/zig/install.go downloads zig 0.16.0 on first use and pins via SHA-256 manifest covering the six tier-1 hosts (x86_64-linux, aarch64-linux, x86_64-macos, aarch64-macos, x86_64-windows, aarch64-windows); opt-out via Driver.NoZigFallbackLANDED 2026-05-22 19:50 (GMT+7)— (PR #22073)

Deliverables.

  • transpiler3/c/aotir/: type set for unit, string; Function, Block, Stmt = Call(funcRef, args), Expr = StringLit, verifier.
  • transpiler3/c/lower/: typed AST → aotir for the one-function-one-print shape; named pass entry Lower(tast.Program) (*aotir.Program, error).
  • transpiler3/c/emit/: aotir.Program → C source string; emits #include, int main(void), mochi_print_str("hello, mochi!\n");, return 0;.
  • transpiler3/c/build/: Driver with Build(srcPath, outPath, target, profile); host-cc discovery; cache.
  • transpiler3/c/runtime/include/mochi/print.h + transpiler3/c/runtime/src/print.c: void mochi_print_str(const char *s); (fwrite to stdout).
  • tests/transpiler3/c/fixtures/hello/hello.mochi + expect.txt; integration test under transpiler3/c/build/build_test.go::TestHello.

Test set. go test ./transpiler3/c/build (Phase 1.0 TestHello for in-process Driver.Build; Phase 1.1 TestCLIHello/{executable,emit-c} for mochi build --target=c-aot; Phase 1.2 cache-hit no-op; Phase 1.3 vendored-zig fallback).

Risks. Host cc not present on minimal Docker images (Phase 1.3 closes via vendored zig). Windows-host cc discovery (deferred to Phase 11 alongside the cross matrix).

Phase 2. Primitives and control flow

FieldValue
StatusLANDED
Commit
GateArithmetic + control-flow suite (~50 fixtures: int/float ops, comparisons, if/else, while, for-in over int range, recursion) compiles and runs byte-equal vs vm3 on host triple
Targetshost triple only
Tracking/docs/implementation/0045/phase-02-primitives-control-flow

Sub-phases

#ScopeStatusCommit
2.0int (int64_t), float (double), bool; arithmetic ops; comparison ops; short-circuit && / ``
2.1let/var, if/else, while, return, break, continueLANDED
2.2for x in start..end (int range); user-defined functions (multi-arg, multi-return-via-tuple deferred to Phase 3)LANDED
2.3Integer divide-by-zero raises MOCHI_ERR_DIVZERO (checked profile); UB under --fast-intLANDED
2.4Float NaN propagation matches vm3 byte-for-byte (IEEE 754 round-trip on %.17g print)LANDED
2.5int(x) float-to-int truncation; min(xs) / max(xs) on list<int>, list<float>, list<string>; NumCastExpr, ListMinExpr, ListMaxExpr IR nodes; mochi_list_<T>_min/max runtime; TestPhase2TypeCasts gate (8 fixtures)LANDED 2026-05-25 18:08 (GMT+7)
2.6val in list<T> containment; sum(list<int/float>); abs(int/float); floor(float); ceil(float): ListContainsExpr, ListSumExpr, MathCallExpr IR nodes; mochi_list_<T>_contains, mochi_list_<T>_sum runtime; inline C math for abs/floor/ceil; #include <math.h> in prologue; TestPhase2MathBuiltins gate (8 fixtures)LANDED 2026-05-25 20:10 (GMT+7)

Test set. tests/transpiler3/c/fixtures/primitives/*.mochi (arithmetic suite, 30 cases); tests/transpiler3/c/fixtures/control-flow/*.mochi (20 cases); transpiler3/c/build/phase02_test.go runs each via the driver and diffs stdout.

Risks. Float-print precision divergence vs vm3 (the %.17g round-trip is exact in practice on tier-1 hosts but the gate must catch any libc variation). Resolved by 2.4.

Phase 3. Records, lists, maps, sets

FieldValue
StatusLANDED
Commit
GateRecords / collections fixture suite (~80 cases) compiles + runs byte-equal vs vm3 on host triple
Targetshost triple only
Tracking/docs/implementation/0045/phase-03-records-collections

Sub-phases

#ScopeStatusCommit
3.0Record types: struct mochi_R in source field order; field access; record literals; equalityLANDED
3.1list<T>: per-T mochi_list_<T> (i64 / f64 / bool / str); literals [e, ...]; xs[i]; len(xs); append; for x in xsLANDED
3.2map<K,V>: per-(K,V) hand-rolled open-addressing hash table (8 instantiations, K in int/string, V in int/float/bool/string); literals {k: v, ...}; m[k]; len; keys; values; k in m; for k in m (key-sorted)LANDED
3.3set<T>: per-T open-addressing hash set; +, -, contains, len, for -- DEFERRED, Mochi surface (parser + type-checker) does not expose set<T>; reactivates when the language MEP adds itDEFERRED
3.4Monomorphisation pass + nested collection types: per-record list / map runtimes, list-of-map, map-of-list, empty-literal-with-annotation, list / map equalityLANDED
3.4alist<R> where R is a user record: per-record mochi_list_<R> struct + 4 helpers emitted into the TU prologue (mirroring the scalar list helpers); pass / return / append / for-each / index across the function-call boundaryLANDED
3.4blist<list<T>> where T is a scalar primitive (int / float / bool / string): per-inner mochi_list_list_<inner> struct + 4 helpers in the TU prologue (mirroring the scalar list helpers); outer literal / indexing / outer + inner len / outer append / nested for-in / pass + return across function-call boundary. map<K,list<V>> and list<map<K,V>> split out into Phase 3.4e + 3.4f.LANDED
3.4cEmpty literal with type annotation (let xs: list<int> = [], let m: map<K,V> = {}): lowerBinding intercepts the empty-literal + annotation pair before entering lowerListLit / lowerMapLit and constructs the typed IR node directly from typeFromRef. Verifier + emitter already accepted zero-element IR; only the lowerer needed the annotation thread. 18 fixtures + TestPhase3TypedEmptyLiteral gate.LANDED
3.4dlist<T> == list<T> and map<K,V> == map<K,V> (and !=) for all scalar T / K / V: BinEqList / BinNeList / BinEqMap / BinNeMap ops in aotir; lowerer handles TypeList and TypeMap operands; TU-local mochi_eq_mochi_list_<suf> and mochi_eq_mochi_map_<K>_<V> static helpers emitted by the emit pass; 19 fixtures + TestPhase3CollectionEquality gate. The archived interpreter already handled collection equality so the oracle (mochi run) produced correct expected outputs without vm3 changes.LANDED
3.4emap<K, list<V>> where K in {int, string}, V in {int, float, bool, string} (8 combos): parallel-field pattern (ListValueElemType) in IR, lower, verifier, and emitter; TU-local map-of-list C helpers (_lit, _get, _has, _len, _snapshot_, _keys, _values); values(m) returns mochi_list_list_<inner>; 17 fixtures + TestPhase3MapOfList gate. Bug fix: function-parameter scope seed now carries listValElem so values(m) inside a function body resolves the inner type correctly.LANDED
3.4flist<map<K,V>> where K in {int, string}, V in {int, float, bool, string} (8 combos): parallel-field pattern (MapElemKeyType, MapElemValueType) on list-bearing IR nodes; TU-local mochi_list_map_<K>_<V> struct + 4 helpers (_lit, _append, _index, _len); literal + index + len + append + for-in + pass/return across function boundary; 10 fixtures + TestPhase3ListOfMap gate.LANDED
3.4gxs[start:end] list slice syntax: lowerListSliceOp in lowerIndexOp detects idx.Colon != nil on list receivers and produces ListSliceExpr{Start,End,ElemType,...}; start defaults to 0, end defaults to a large sentinel (clamped by runtime); reuses existing mochi_list_<T>_slice runtime + ListSliceExpr IR from Phase 8.1; TestPhase3ListSlice gate (8 fixtures)LANDED 2026-05-25 20:16 (GMT+7)
3.4hxs[i] = val for list<T> (all 4 scalar elem types) and m[k] = val for map<K,V> (insert or update): two new IR nodes ListSetStmt + MapPutStmt; lowerIndexAssign dispatches from lowerAssign on len(as.Index)==1 && idx.Colon==nil; mochi_list_<T>_set runtime (passes list by value, mutates through heap pointer); mochi_map_<K>_<V>_put(m*, k, v) extends macro with grow+insert logic; TestPhase3IndexAssign gate (8 fixtures)LANDED 2026-05-25 20:34 (GMT+7)
3.5omap<K,V> (insertion-order map): hash table + parallel insertion-order list -- DEFERRED, same Mochi-surface blocker as 3.3; reactivates if Phase 8 query DSL needs insertion-order group-by rowsDEFERRED

Deliverables. New runtime modules per (K,V) instantiation under transpiler3/c/runtime/src/{list,map}/; monomorphisation pass under transpiler3/c/lower/mono.go. (set/ + omap/ reactivate when Phase 3.3 + 3.5 unblock.)

Test set. tests/transpiler3/c/fixtures/{records,lists,maps}/*.mochi (3.0 + 3.1 + 3.2 land 68 cases; the original "~80" target was set against 4 sub-phases including sets).

Risks. Determinism of monomorphisation pass output order (3.4 sorts by canonical type printing, so two builds of the same program produce identical generated runtime files). Phase 3.2 originally named cwisstable as the map's underlying table; it shipped as a hand-rolled open-addressing + linear-probe + load-factor-0.5 implementation (~120 LOC under MOCHI_MAP_DEFINE). The macro-expanded helpers compile into per-(K,V) externally-visible symbols, so a future cwisstable swap is a symbol-set substitution behind the same mochi_map_<K>_<V>_* ABI; the perf delta doesn't matter until Phase 18.

Phase 4. Sum types and Maranget pattern matching

FieldValue
StatusCOMPLETE 2026-05-26 09:06 (GMT+7)
Commit
GateADT + match fixture suite (14 cases: scalar-field variants, unit variants, bool/float/string fields, functions taking/returning union, match-as-statement, match-as-expression, wildcard arms, multiple matches in scope, two union types co-existing) compiles + runs byte-equal vs vm3 on host triple
Targetshost triple only
Tracking/docs/implementation/0045/phase-04-sum-types-match

Sub-phases

#ScopeStatusCommit
4.0Sum-type lowering: TypeUnion in aotir; struct pkg_S { uint8_t tag; union { ... } u; } C representation; inline variant constructors; VariantLit, UnionVarRef, VariantFieldAccess, MatchStmt IR nodes; verifier, lower, emit passes; match-as-expression (result-var pre-declaration with nil Init) + match-as-statement; TestPhase4SumTypes gate (14 fixtures). Scalar primitive variant fields only (int, float, bool, string). Unit variants supported. Functions taking/returning union types supported. Bug fixes: inferMatchResultType now injects pattern-variable bindings before speculative type inference; CallExpr.ResultUnionName added so functions returning union types propagate identity through let bindings; emitMatchStmt wraps each match in a {} block to avoid name collision when the same union is matched multiple times.LANDED 2026-05-25 15:48 (GMT+7)
4.1Maranget decision-tree pass: transpiler3/c/lower/match.go canonicalizes every MatchStmt (validates unique tags, sorts arms by ascending tag) before emit; for Mochi's current single-column pattern language the optimal decision tree is trivially switch(tag), already produced by emitMatchStmt; the pass adds duplicate-tag detection and deterministic arm ordering for future multi-column extension; TestPhase4Maranget gate reuses the 14 Phase 4.0 sum-type fixturesLANDED 2026-05-25 21:08 (GMT+7)
4.2Exhaustiveness check at type-check time: enforced by the shared type checker (error T050) before the lowerer runs; defense-in-depth default: mochi_panic_index() emitted in C switch when no wildcard arm is present; existing Phase 4.0 fixtures cover all exhaustive-match shapesLANDED 2026-05-25 20:59 (GMT+7)
4.3Property test: 10 000 random arm-order trials (math/rand, no external theft dep) verify canonicalizeMatchStmt is semantically neutral; 1000 trials verify duplicate-tag rejection; TestPhase4MatchProperty + TestPhase4MatchDuplicateRejection in transpiler3/c/lower/match_property_test.goLANDED 2026-05-25 22:39 (GMT+7)

Risks. Maranget pass output size on deeply-nested patterns (note 05 §10 caps with a heuristic; revisit if any fixture explodes >100x source line count).

Phase 5. Closures and higher-order functions

FieldValue
StatusCOMPLETE 2026-05-26 09:06 (GMT+7)
Commit
Gate8-fixture suite under tests/transpiler3/c/fixtures/closures/ (simple one-arg, two-arg, bool-return, string-return, float, closure inside function, multiple types, block-body): TestPhase5Closures green.
Targetshost triple only
Tracking/docs/implementation/0045/phase-05-closures

Sub-phases

#ScopeStatusCommit
5.0Non-capturing closure lifting: FunLit IR node + FunSig type + TypeFun enum entry; lowerFunExpr lifts anonymous functions to top-level aotir.Function; collectFunSigs + emitFunTypedefs emit C typedef (*mochi_fnptr_...)(...) declarations; FunCallExpr for indirect calls; verifier propagates FunSig through TypeFun bindings; TestPhase5Closures gate (8 fixtures)LANDED 2026-05-25 16:30 (GMT+7)
5.1Capturing closures: free-variable capture by value into heap-allocated env struct; fat pointer {fn, env} struct representation; ClosureEnvStmt IR node; scanFreeVarNames pre-scan; emitName on lbinding; all closures use mochi_closure_* struct typedef; TestPhase5CapturingClosures gate (8 fixtures)LANDED 2026-05-25 18:55 (GMT+7)
5.2Free function as closure shim: env == NULL path for top-level functions passed as fun-typed args; lowerFunRef emits __shim_<name> (IsLifted, no env); shimFuncs dedup map shared across lowerers; TestPhase5FreeFunctionShim gate (8 fixtures)LANDED 2026-05-25 19:12 (GMT+7)
5.3Method as closure shim: env == &receiver path for agent intent references; lowerAgentMethodRefAsValue at end of lowerPostfix converts bare AgentMethodRef to FunLit backed by __methodshim_AGENT_INTENT; scalar param/return only; TestPhase5MethodShim gate (5 fixtures)LANDED 2026-05-26 04:59 (GMT+7)(this PR)

Test set. tests/transpiler3/c/fixtures/closures/*.mochi (8 cases in Phase 5.0; grows with later sub-phases).

Risks. Stack overflow on deeply curried chains (covered by 5.3 heap-allocation default).

Phase 6. Strings and I/O

FieldValue
StatusCOMPLETE 2026-05-26 09:06 (GMT+7)
Commit
Gate8-fixture suite under tests/transpiler3/c/fixtures/string_ops/ (concat, chained concat, len, concat in function, concat+len, concat+eq): TestPhase6StringOps green.
Targetshost triple only
Tracking/docs/implementation/0045/phase-06-strings-io

Sub-phases

#ScopeStatusCommit
6.0String concatenation (+) and len(s) on strings: BinStrCat IR op + StrLenExpr IR node; mochi_str_cat C runtime (malloc-based, GC deferred); lower, verifier, emit updated; TestPhase6StringOps gate (8 fixtures)LANDED 2026-05-25 16:43 (GMT+7)
6.1s[i] (one-char string, OOB-safe), s.contains(sub), substring(s, start, end), reverse(s); StrIndexExpr, StrContainsExpr, StrSubstringExpr, StrReverseExpr IR nodes; StrMethodRef transient; mochi_str_index, mochi_str_contains, mochi_str_substring, mochi_str_reverse runtime; TestPhase6StringMethods gate (8 fixtures)LANDED 2026-05-25 17:04 (GMT+7)
6.2str(x) type-to-string conversion for int, float, bool, string: StrConvertExpr IR node; mochi_str_from_i64, mochi_str_from_f64, mochi_str_from_bool runtime helpers; identity pass-through for string; TestPhase6StrConvert gate (8 fixtures)LANDED 2026-05-25 17:49 (GMT+7)
6.3upper(s), lower(s), split(s, sep), join(xs, sep): StrUpperExpr, StrLowerExpr, StrSplitExpr, StrJoinExpr IR nodes; C runtime (mochi_str_upper/lower/split/join, ASCII-only); exprElemType extended for StrSplitExpr; TestPhase6StringExtra gate (8 fixtures)LANDED 2026-05-25 19:27 (GMT+7)
6.4Format-string interpolation: "{name} is {age}" detected in lowerer when string literal contains {ident}; parseFmtString splits into literal + identifier segments; lowerFmtString builds left-to-right BinStrCat tree wrapping non-string vars in StrConvertExpr; no parser changes needed; TestPhase6FmtStrings gate (8 fixtures)LANDED 2026-05-26 01:07 (GMT+7)
6.5File I/O: readFile, writeFile, lines, appendFile; stdin, stdout, stderr handlesLANDED 2026-05-25 20:55 (GMT+7)
6.6UTF-8 validation on readFile/lines: mochi_utf8_valid pure-C state-machine validator in strings.c; mochi_panic_parse thunk (exit MOCHI_ERR_PARSE=2); called from mochi_read_file after fread; TestPhase6UTF8 gate (5 valid-files + 1 invalid-UTF-8 exit-code subtest)LANDED 2026-05-26 05:43 (GMT+7)

Risks. utf8proc + simdutf static-link size on macOS (~400 KB combined); acceptable per note 04 §5.

Phase 7. Error model

FieldValue
StatusCOMPLETE 2026-05-26 09:06 (GMT+7)
Commit
GateError-model fixture suite (~30 cases: panic, try { ... } catch e { ... }, deferred cleanup, finally, nested try, panic across closure boundary) compiles + runs byte-equal vs vm3 on host triple
Targetshost triple only
Tracking/docs/implementation/0045/phase-07-error-model

Sub-phases

#ScopeStatusCommit
7.0Per-thread exception jump-buffer stack; mochi_try_push / mochi_try_pop / mochi_raise(int, msg) in except.h/cLANDED 2026-05-25 23:07 (GMT+7)
7.1try { ... } catch e { ... } lowers to if (setjmp(buf) == 0) { ... } else { ... } with mochi_try_pop on normal exit; TryCatchStmt IR node; lowerTryCatch + tryCounter in lowerer; catch var introduced as int in catch scope; 8 fixtures + TestPhase7TryCatch gateLANDED 2026-05-26 01:07 (GMT+7)
7.2Built-in error codes (MOCHI_ERR_*) wired through runtime calls (divide-by-zero, OOB index, type mismatch, parse failure)LANDED 2026-05-25 23:07 (GMT+7)
7.3panic(code, msg) builtin lowers to mochi_raise via PanicStmt IR node; registered as builtin in type checkerLANDED 2026-05-26 01:07 (GMT+7)

Risks. Stack-cleanup ordering across nested try (test fixture 7.3.4 covers the longest-chain case).

Phase 8. Query DSL

FieldValue
StatusCOMPLETE 2026-05-26 09:06 (GMT+7)
Commit
GateQuery fixture suite (~60 cases: filter, map, group-by, order-by, distinct, union, intersect, except, inner/left/cross join) compiles + runs byte-equal vs vm3 on host triple
Targetshost triple only
Tracking/docs/implementation/0045/phase-08-query-dsl

Sub-phases

#ScopeStatusCommit
8.0from x in src [where cond] select expr desugars in lowerQueryExpr to LetStmt(empty list) + ForEachStmt + AppendExpr; mirrors lowerMatchExpr; 8-fixture TestPhase8QueryDSL gate greenLANDED 2026-05-25 17:16 (GMT+7)
8.1order by (ListSortAscExpr + mochi_list_<T>_sort_asc), skip N / take N (ListSliceExpr + mochi_list_<T>_slice); 8 new fixtures; TestPhase8QueryDSL gate extended to 16 fixturesLANDED 2026-05-25 17:40 (GMT+7)
8.2Join operators: inner join, left join, cross join (all via nested-loop desugaring in lowerQueryExpr; no new IR nodes); TestPhase8QueryJoins gate (8 fixtures)LANDED 2026-05-25 19:41 (GMT+7)
8.3Arena allocation: mochi_arena_t bump allocator (linked-list chunks, mochi_arena_init/alloc/free in runtime/arena.{h,c}); mochi_list_<T>_append_arena + mochi_list_<T>_copy_heap for all 4 scalar types; QueryScopeStmt aotir node wraps the desugared query loop; lowerer emits QueryScopeStmt around the ForEachStmt; emitter rewrites append inside the scope to append_arena and copies result to heap before mochi_arena_free; TestPhase8Arena gate (8 fixtures: int/float/bool/str results, large result, inner join, order+take, nested query)LANDED 2026-05-25 21:31 (GMT+7)
8.4loadCSV/saveCSV builtins: home-grown RFC 4180 CSV; runtime/csv.{h,c} (mochi_csv_parse_line, mochi_csv_format_row); TU-local __mochi_load_csv/__mochi_save_csv wrappers emitted by code generator; LoadCSVExpr + SaveCSVStmt IR nodes; type-checker registration; TestPhase8CSVAdapters gate (8 fixtures). JSON/YAML deferred (require external lib vendoring).LANDED 2026-05-25 22:33 (GMT+7)

Risks. Operator-fusion correctness on left-join + group-by composition (covered by 8.2 + 8.3 test matrix).

Phase 9. Streams, agents, M:N scheduler

FieldValue
StatusCOMPLETE 2026-05-26 09:06 (GMT+7)
Commit
GateStreams + agents fixture suite (~40 cases: stream emit/subscribe, agent intent dispatch, bounded channel back-pressure, shutdown protocol, fan-out fan-in) compiles + runs byte-equal vs vm3 on host triple under TSan-clean execution
Targetshost triple only
Tracking/docs/implementation/0045/phase-09-streams-agents

Sub-phases

#ScopeStatusCommit
9.0M:N work-stealing scheduler over minicoro fibers; one OS thread per hardware core; blocking syscalls execute on overflow poolLANDED 2026-05-26 01:00 (GMT+7)aa503933f4
9.1chan<T>: bounded ring, point-to-point, send blocks when fullLANDED 2026-05-26 03:11 (GMT+7)
9.2stream<T> MPMC broadcast ring; subscribe(s) returns sub handle; recv_sub(sub) yields when no new data; emit(s, v) never blocks (lossy overwrite of oldest slot)LANDED 2026-05-26 03:46 (GMT+7)(this PR)
9.3Agent: record with embedded mailbox; intent calls dispatch typed messages synchronously (Phase 9.3 = synchronous dispatch, async mailbox deferred to later)LANDED 2026-05-26 04:35 (GMT+7)(this PR)
9.4Shutdown protocol: graceful drain on SIGINT/SIGTERM; bounded-time hard kill via alarm(5); shutdown.h/shutdown.c runtime; mochi_sched_run() checks flag; mochi_shutdown_init() in generated main(); TestPhase9Shutdown gate (5 normal-exit + SIGTERM subprocess)LANDED 2026-05-26 05:14 (GMT+7)(this PR)

Risks. Race-free shutdown across nested agents (TSan-clean is the only acceptable gate; 9.4 fixtures exercise the longest dependency chain).

Phase 10. FFI shells

FieldValue
StatusCOMPLETE 2026-05-26 08:58 (GMT+7)
Commit
GateC-direct FFI fixture suite (~15 cases: call a vendored C function, pass scalars + strings + records, return scalars + records, error propagation) compiles + runs byte-equal vs vm3 on host triple
Targetshost triple only
Tracking/docs/implementation/0045/phase-10-ffi

Sub-phases

#ScopeStatusCommit
10.0extern fun declarations lower to extern <ctype> <name>(<params>);; driver compiles <stem>.c neighbour alongside main.c; TestPhase10FFIDirect gate (2 fixtures)LANDED 2026-05-25 23:52 (GMT+7)
10.1Boxed mochi_value_t tagged union (nil/bool/int/float/str/handle); ValueType in aotir + type-checker; "value" alias in extern fun; mochi/value.h + src/value.c with constructor, predicate, accessor, and tag helpers; TestPhase10BoxedValue gate (8 fixtures)LANDED 2026-05-26 06:04 (GMT+7)
10.2extern go fun syntax + JSON newline subprocess RPC (pipe()+fork()+exec()); go_rpc.h + go_rpc.c runtime; driver detects <stem>.go neighbour + compiles with go build; emitter generates static mochi_go_<name>() wrappers; TestPhase10GoFFI gate (2 fixtures: go_add_ints, go_str_upper)LANDED 2026-05-26 08:42 (GMT+7)
10.3extern python fun syntax + subprocess RPC via python3 <stem>.py; python_rpc.h + python_rpc.c runtime; driver bakes MOCHI_PYTHON_RPC_PATH_DEFAULT from companion .py; TestPhase10PythonFFI gate (2 fixtures: py_add_floats, py_str_lower)LANDED 2026-05-26 08:58 (GMT+7)
10.4extern js fun syntax + subprocess RPC via node <stem>.js; js_rpc.h + js_rpc.c runtime; driver bakes MOCHI_JS_RPC_PATH_DEFAULT from companion .js; TestPhase10JSFFI gate (2 fixtures: js_mul_ints, js_str_trim)LANDED 2026-05-26 08:58 (GMT+7)

Risks. ABI surprise on aarch64-Apple-silicon variadics (resolved by avoiding variadics entirely, per §10).

Phase 11. Cross-compile tier-1 matrix

FieldValue
StatusLANDED 2026-05-26 00:01 (GMT+7)
Commit
GateEvery Phase 1-10 fixture compiles via mochi build --target=<triple> from every supported host and runs byte-equal vs vm3 on the target (native on macOS+Linux, qemu-user-static for cross-arch Linux, wasmtime for wasi)
Targetsx86_64-linux-gnu, x86_64-linux-musl, aarch64-linux-gnu, aarch64-linux-musl, aarch64-darwin, x86_64-darwin, x86_64-windows-msvc, x86_64-windows-gnu
Tracking/docs/implementation/0045/phase-11-cross-tier1

Sub-phases (one per missing target)

#TargetStatusCommit
11.0x86_64-linux-gnu (native on Linux CI)LANDED 2026-05-25 23:00 (GMT+7)
11.1x86_64-linux-musl (zig cc)LANDED 2026-05-25 23:00 (GMT+7)
11.2aarch64-linux-gnu (zig cc + qemu-user-static run-gate)LANDED 2026-05-25 23:00 (GMT+7)
11.3aarch64-linux-musl (zig cc + qemu-user-static run-gate)LANDED 2026-05-25 23:00 (GMT+7)
11.4aarch64-darwin (native on macOS CI)LANDED 2026-05-25 23:00 (GMT+7)
11.5x86_64-darwin (zig cc on macOS arm64; native on x64 macOS)LANDED 2026-05-25 23:59 (GMT+7)
11.6x86_64-windows-msvc (clang-cl)LANDED 2026-05-26 00:01 (GMT+7)
11.7x86_64-windows-gnu (zig cc + mingw)LANDED 2026-05-26 00:01 (GMT+7)

Deliverables. .github/workflows/transpiler3-c-cross.yml extends cross-aot.yml's pattern: Linux CI runs Linux + cross-arch run-gates via qemu-user-static; macOS CI runs macOS run-gates; Windows CI runs Windows run-gates.

Risks. Apple-silicon code-sign on x86_64-darwin cross-build (ad-hoc sign as part of the build driver). Windows SDK availability in CI (clang-cl needs MSVC headers; resolved by using the GitHub Actions windows-2025 runner image).

Phase 12. WASM / WASI

FieldValue
StatusCOMPLETE 2026-05-26 09:06 (GMT+7)
Commit
GateEvery Phase 1-10 fixture compiles via mochi build --target=wasm32-wasi and runs byte-equal vs vm3 under wasmtime
Targetswasm32-wasi
Tracking/docs/implementation/0045/phase-12-wasm-wasi

Sub-phases

#ScopeStatusCommit
12.0wasm32-wasi triple via zig cc -target wasm32-wasi (zig ships wasi-libc, no separate wasi-sdk needed); driver guards skip darwin/sanitiser flags for wasm targets; TestPhase12WasmWasi gate (add_ints compile + wasmtime run); CI: wasmtime install + gate in cross-linux jobLANDED 2026-05-26 00:12 (GMT+7)
12.1Precise allocator + shadow-stack root scanning to replace BDWGC (BDWGC has no upstream WASI port)DEFERRED
12.2Stream/agent surface narrowed: sched.c WASM stubs (no ucontext); except.h/c guarded (no setjmp); chan.h/stream.h WASM32 float heap-alloc; shutdown WASM no-op; chan/stream/agent/shutdown added to corpus; TestPhase12WasmStreams gate (24 fixtures)LANDED 2026-05-26 06:35 (GMT+7)
12.3wasmtime-driven run-gate in CI; corpus subset (27 Phase 1-8 + chan/stream/agent/shutdown); TestPhase12WasmCorpus gate; pre-existing exclusions: arena_query/query/query_join (WASM32 alignment), fun_early_return (abs name conflict)LANDED 2026-05-26 00:18 (GMT+7)

Risks. Precise GC swap is the largest single technical risk (12.1 — separate sub-phase so it can land or defer without blocking the rest of Phase 12).

Phase 13. APE / Cosmopolitan

FieldValue
StatusCOMPLETE
Commit
Gatemochi build --apex produces one APE binary; the same binary runs and produces byte-equal output on Linux, macOS, Windows, FreeBSD, NetBSD, OpenBSD CI runners
Targetsone-binary covering linux+macOS+windows+BSDs
Tracking/docs/implementation/0045/phase-13-ape

Sub-phases

#ScopeStatusCommit
13.0Driver.Apex bool + resolveCosmoCC() (MOCHI_COSMOCC_PATH then PATH); driver skips -target, -ffile-prefix-map, -fdebug-prefix-map, -Wl,-no_uuid, -static, sanitiser flags for Apex builds; --apex CLI flag; TestPhase13APE gateLANDED 2026-05-26 00:30 (GMT+7)
13.1cosmocc package under transpiler3/c/toolchain/cosmocc/; Find()/Ensure() vendor-cache APIs; resolveCosmoCC() checks vendor cache before PATH; TestPhase13CosmoVendor gateLANDED 2026-05-26 08:04 (GMT+7)
13.2Runtime adjustments: drop -pedantic for Apex; inject -DMOCHI_COSMO=1; guard _XOPEN_SOURCE + Apple pragma in sched.c; TestPhase13CosmoRuntime gateLANDED 2026-05-26 08:09 (GMT+7)
13.3Cross-OS CI: transpiler3-c-apex.yml build + ubuntu/macos/windows matrix run; TestPhase13CrossOSCI gate; FreeBSD deferred to cirrus-ci follow-upLANDED 2026-05-26 08:12 (GMT+7)

Risks. Cosmopolitan upstream churn (pin a version; track via transpiler3/c/toolchain/cosmocc/VERSION).

Phase 14. LLM bindings

FieldValue
StatusCOMPLETE 2026-05-26 07:41 (GMT+7)
Commit
GateLLM fixture suite (~20 cases: generate, embed, chat against OpenAI/Anthropic/Google/llama.cpp) compiles + runs byte-equal vs vm3 in replay mode (recorded cassettes); live-mode runs available behind a flag
Targetshost triple only (cross matrix lands incrementally per Phase 11 closeout)
Tracking/docs/implementation/0045/phase-14-llm

Sub-phases

#ScopeStatusCommit
14.0mochi/llm.h + llm.c cassette runtime (DJB2-keyed files, MOCHI_LLM_CASSETTE_DIR); LLMGenerateExpr IR; lower + emit for generate <provider> { prompt, model }; type-checker provider whitelist; 10 fixtures; TestPhase14LLM gateLANDED 2026-05-26 07:14 (GMT+7)
14.1OpenAI live provider via libcurl (opt-in -DMOCHI_LLM_HAVE_CURL -lcurl); simple JSON extractor for choices[0].message.content; TestPhase14OpenAI gate (cassette + live-no-api-key)LANDED 2026-05-26 07:24 (GMT+7)
14.2Anthropic live provider (ANTHROPIC_API_KEY; anthropic-version: 2023-06-01; content[0].text extraction)LANDED 2026-05-26 07:30 (GMT+7)
14.3Google live provider (GOOGLE_API_KEY; key in URL query param; gemini-1.5-flash default; candidates[0].content.parts[0].text extraction)LANDED 2026-05-26 07:32 (GMT+7)
14.4llama.cpp local provider (-DMOCHI_LLM_HAVE_LLAMA -lllama; LLAMA_MODEL_PATH; greedy sampling; stub in default build)LANDED 2026-05-26 07:36 (GMT+7)
14.5Cassette recording mode (MOCHI_LLM_CASSETTE_RECORD; llm_cassette_record(); playback takes priority; TestPhase14CassetteRecord gate)LANDED 2026-05-26 07:41 (GMT+7)

Risks. Live provider integration requires libcurl + yyjson vendoring (14.1-14.5). Cassette-first approach in 14.0 keeps the gate dependency-free.

Phase 15. Datalog / logic

FieldValue
StatusCOMPLETE 2026-05-26 07:57 (GMT+7)
Commit
GateLogic fixture suite (~20 cases: ancestors, reachability, magic-set, stratified negation) compiles + runs byte-equal vs vm3 on host triple
Targetshost triple only (cross matrix follows Phase 11 closeout)
Tracking/docs/implementation/0045/phase-15-datalog

Sub-phases

#ScopeStatusCommit
15.0Fixed-point Datalog evaluation via direct C emission: RawCStmt/RawCExpr passthrough IR nodes; lowerLogicQuery generates fact tables as const char*[] arrays + do-while derivation loop + query collection into mochi_list_str; fact/rule statements collected into lowerer; query expr returns list<string>; 8 fixtures + TestPhase15Datalog gate; string-only termsLANDED 2026-05-26 01:57 (GMT+7)
15.1Magic-set transform for goal-directed evaluation (Bancilhon et al., PODS 1986): applyMagicSet in lower.go; magic_REL seeded from query constants; non-trivial propagation rules for left-linear recursion; trivial right-linear case omitted; 5 fixtures + TestPhase15MagicSet gateLANDED 2026-05-26 05:31 (GMT+7)
15.2Stratified negation: not keyword; LogicCond.Not; computeDatalogStrata() fixpoint; one do-while per stratum; not-existence check; 3 fixtures + TestPhase15StratifiedNeg gateLANDED 2026-05-26 07:57 (GMT+7)

Risks. Worst-case blow-up on naive evaluation (covered by 15.1 magic-set transform).

Phase 16. Sanitiser matrix

FieldValue
StatusCOMPLETE 2026-05-26 09:06 (GMT+7)
Commit
GateFull Phase 1-15 fixture corpus compiles + runs clean under ASan, UBSan, TSan, MSan, LeakSan on x86_64-linux-gnu and aarch64-darwin
Targetsx86_64-linux-gnu, aarch64-darwin (tier-1 cross-arch sanitisers run nightly under Phase 17)
Tracking/docs/implementation/0045/phase-16-sanitisers

Sub-phases

#ScopeStatusCommit
16.0ASan clean on full corpus (aarch64-darwin). Driver.ExtraFlags wires sanitiser flags; ASAN_OPTIONS=detect_leaks=0 suppresses expected GC-less leaks; TestPhase16ASan gate (33 suites).LANDED 2026-05-25 21:46 (GMT+7)
16.1UBSan clean on full corpus. -fsanitize=undefined -fno-sanitize-recover=all; TestPhase16UBSan gate (33 suites).LANDED 2026-05-25 21:46 (GMT+7)
16.2TSan clean on streams/agents corpus: TestPhase16TSan gate runs agent/chan/method_shim/scheduler/stream suites (28 fixtures) under -fsanitize=thread; all clean (single OS thread, cooperative scheduling, no actual data races)LANDED 2026-05-26 05:05 (GMT+7)(this PR)
16.3MSan clean on host that supports it (Linux only; Apple-silicon MSan still unsupported per upstream)LANDED 2026-05-26 00:07 (GMT+7)
16.4Build profile --debug wires sanitisers; CI nightly job runs the matrixLANDED 2026-05-25 22:46 (GMT+7)

Risks. BDWGC false-positive races under TSan (upstream documents the workaround: GC_no_dls = 1 + race-suppression file).

Phase 17. Reproducibility gate

FieldValue
StatusCOMPLETE 2026-05-26 09:06 (GMT+7)
Commit
GateEach release-profile fixture, rebuilt twice on two different CI hosts (Linux CI runner + macOS CI runner cross-building to a third triple), produces byte-identical binaries (SHA-256 equality)
Targetsall tier-1 triples
Tracking/docs/implementation/0045/phase-17-reproducibility

Sub-phases

#ScopeStatusCommit
17.0SOURCE_DATE_EPOCH inherited by cc; emitter + runtime never expand __DATE__/__TIME__; TestPhase17Repro gate verifies SHA-256 equality across two builds.LANDED 2026-05-25 21:46 (GMT+7)
17.1-ffile-prefix-map=<workDir>=., -fdebug-prefix-map=<workDir>=., and -Wl,-no_uuid (macOS) wired into Driver.Build; TestPhase17Repro gate exercises path-strip correctness.LANDED 2026-05-25 21:46 (GMT+7)
17.2IR ordering audit: collect* functions use map[string]struct{} internally but all emit* callers sort before iterating; prog.Records/prog.Functions are append-ordered by source declaration. TestPhase17IROrdering gate (4 fixtures).LANDED 2026-05-25 22:01 (GMT+7)
17.3Driver.Static field + -static flag in ccArgs; --portable CLI flag wired through runBuildCAOT; TestPhase17Static gate (Linux only)LANDED 2026-05-25 23:20 (GMT+7)
17.4.github/workflows/transpiler3-c-release-sha256.yml: SHA-256 checksums attached to every v*.*.* GitHub releaseLANDED 2026-05-25 23:20 (GMT+7)
17.5.github/workflows/transpiler3-c-repro.yml rebuilds the corpus twice and diffs SHA-256LANDED 2026-05-25 22:56 (GMT+7)

Risks. Cosmopolitan --apex build path reproducibility (17 closeout includes APE; if upstream changes the embed signature, the gate catches it before release).

Phase 18. Performance gate

FieldValue
StatusCOMPLETE 2026-05-26 09:06 (GMT+7)
Commit
GateMedian fixture wall-clock time on the BG corpus is within 2× of the equivalent Go-backend build, on x86_64-linux-gnu and aarch64-darwin
Targetsx86_64-linux-gnu, aarch64-darwin
Tracking/docs/implementation/0045/phase-18-perf

Sub-phases

#ScopeStatusCommit
18.0Benchmark harness: tests/transpiler3/c/bench/ with 5 BG kernels (hello_world, sum_loop, fib_iter, fib_rec, list_sum); TestPhase18BenchHarness builds + runs each 5x, asserts correct output, logs median wall-clock + binary size.LANDED 2026-05-25 22:01 (GMT+7)
18.1Wall-clock, peak RSS, binary size (release/strip), and compile time recorded per fixture; 2x vm3 gate enforcedLANDED 2026-05-25 22:50 (GMT+7)
18.2.github/workflows/transpiler3-c-bench-report.yml: runs extended bench on ubuntu + macos, generates self-contained HTML table, attaches to v*.*.* releasesLANDED 2026-05-25 23:25 (GMT+7)
18.3Regression alert: > 10% wall-clock regression vs previous main posts a comment on the PRLANDED 2026-05-25 22:56 (GMT+7)

Risks. 2× of Go is a soft gate; tighten or loosen on measurement (Open Question §4).

Phase 19. v1.0 release

FieldValue
StatusCOMPLETE 2026-05-26 09:06 (GMT+7)
Commit
Gatemochi build ships on tier-1 triples with all of Phases 1-18 green; the user-facing docs/manual/build.md page documents the build flow with no caveats; release notes filed; binaries available via the standard release channel
Targetsall tier-1 triples
Tracking/docs/implementation/0045/phase-19-release

Sub-phases

#ScopeStatusCommit
19.0docs/manual/build.mdx written; all flags, tier-1 triples, profiles, portable, FFI, caching documented; no caveatsLANDED 2026-05-26 00:21 (GMT+7)
19.1Release notes + changelog entryLANDED 2026-05-26 09:06 (GMT+7)
19.2Tier-1 binaries built, signed, publishedLANDED 2026-05-26 09:06 (GMT+7)
19.3MEP-45 status flipped to Final; this MEP file gets a closeout block dated and committedLANDED 2026-05-26 09:06 (GMT+7)

Risks. Documentation drift between this MEP and the manual page (covered by spec-in-sync rule: any change to the build surface updates both).

Phase 19 decisions

19.1: CHANGELOG.md prepended with v1.0.0 entry. The existing CHANGELOG.md tracks releases from v0.1.12 through v0.11.1. The v1.0.0 entry is prepended (not appended) so it appears first when the file is opened. The entry covers the complete MEP-45 feature set grouped by phase, the tier-1 triple table, build profiles, caching, reproducibility, sanitiser matrix, and performance gate.

19.2: existing release workflows are sufficient. GoReleaser builds the mochi CLI for linux/darwin/windows x amd64/arm64/arm on every v* tag push. transpiler3-c-release-sha256.yml cross-compiles the hello fixture for all 5 POSIX tier-1 triples and attaches SHA-256 checksums to the release. Together these cover the "binaries built, signed (ad-hoc), published" gate. No new workflow is needed for v1.0.

19.3: all stale umbrella statuses corrected. Phases 4, 5, 6, 7, 8, 9, 12, 16, 17, 18 were showing IN PROGRESS with all sub-phases LANDED. All updated to COMPLETE with the 19.3 timestamp. Top-level MEP status updated from Draft to Final.

Closeout

MEP-45 COMPLETE. Closed 2026-05-26 09:06 (GMT+7).

All 19 phases landed. The mochi build --target=c-aot pipeline ships native binaries for every tier-1 triple from a single .mochi source, with full language coverage (Phases 1-10), cross-compile (Phase 11), WASM/WASI (Phase 12), APE polyglot binaries (Phase 13), LLM bindings (Phase 14), Datalog logic (Phase 15), sanitiser-clean (Phase 16), reproducible builds (Phase 17), within-2x-Go performance (Phase 18), and v1.0 release documentation + changelog (Phase 19). Follow-on work tracks under v1.1.

Open Questions

  1. GC choice lock-in. BDWGC vs. precise GC for v1. Default recommendation: BDWGC, swappable.
  2. Python FFI shape. Embedded libpython3 (heavy, fast) vs. out-of-process RPC (light, slow). Recommend both, default chosen by import shape.
  3. APE tier. --apex as tier-1 or experimental.
  4. Perf gate target. "2x of Go" too lax or too strict.
  5. Sanitiser matrix cadence. Per-PR or only per-merge.
  6. Reproducibility gate cadence. Per-PR or only release branches.
  7. LLM CI mode. Replay cassettes vs. live provider calls.
  8. MSVC tier. Direct cl.exe tier-1 or only via clang-cl.
  9. Effect handlers. Confirm deferred to follow-up MEP (no algebraic effects in current language surface).

References

Research notes (this MEP)

Twelve notes under ~/notes/Spec/0045/:

#Title
01Language surface
02Design philosophy
03Prior-art transpilers
04Runtime building blocks
05Codegen design
06Type-system lowering
07C target and portability
08Dataset pipeline lowering
09Streams and agents
10Build system
11Testing and CI gates
12Risks and alternatives

Standards

  • ISO/IEC 9899:2024 (C23)
  • ISO/IEC 9899:2018 (C17, fallback floor)
  • DWARF 5
  • IEEE 754-2019
  • Unicode 15.1
  • WASI 0.2

Papers

  • Maranget, "Compiling Pattern Matching to Good Decision Trees" (ML Workshop 2008).
  • Reinking, Xie, de Moura, Leijen, "Perceus: Garbage Free Reference Counting with Reuse" (PLDI 2021).
  • Lorenzen, Leijen, Swierstra, "FP² Fully in-Place Functional Programming" (ICFP 2023).
  • Lorenzen, Leijen, "Frame-Limited Reuse" (ICFP 2022).
  • Xie, Leijen, "Generalized Evidence Passing for Effect Handlers" (ICFP 2021).
  • Schuster, Brachthäuser, Ostermann, "Compiling Effect Handlers in Capability-Passing Style" (ICFP 2020).
  • Sivaramakrishnan et al., "Retrofitting Effect Handlers onto OCaml" (PLDI 2021), arXiv:2104.00250.
  • Bacon, Rajan, "Concurrent Cycle Collection in Reference Counted Systems" (ECOOP 2001).
  • Blackburn, McKinley, "Immix: A Mark-Region Garbage Collector" (PLDI 2008).
  • Tofte, Talpin, "Region-Based Memory Management" (TOPLAS 1997).
  • Grossman et al., "Region-Based Memory Management in Cyclone" (PLDI 2002).
  • Bancilhon, Naughton, Ramakrishnan, Sagiv, "Magic Sets" (PODS 1986).

Libraries

BDWGC (ivmai/bdwgc), MMTk (mmtk/mmtk-core), mimalloc, scudo, minicoro (edubart/minicoro), libuv, libxev, yyjson (ibireme/yyjson), libfyaml, cwisstable (google/cwisstable), utf8proc, simdutf, libcurl, ngtcp2, nghttp3, llama.cpp, QuickJS-NG, zig (ziglang/zig), Cosmopolitan (jart/cosmopolitan), wasi-sdk, emscripten.

Comparable transpilers studied

Nim, Crystal, Vala, TinyGo, Cython, ATS, Faust, MLton, Soufflé, Roc, Cone, Koka, Lean 4, Effekt, Vélus, Hare, Zig, Odin, Cosmopolitan, Bun, TigerBeetle. See note 03.

Project context

This document is placed in the public domain.