Skip to content

Design philosophy

The five guiding principles behind the Mochi-to-C transpiler (spec-first, boring C, no ABI surprises, portability over performance, verifiable output), plus the runtime shape and a sample C output.

MEP-45 research note 02, Design philosophy

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

This note records the principles the C target should be designed against, written before any code is touched, so that the implementation can be audited against them later. The philosophy is distilled from three places: the language surface (note 01), the Mochi project’s own existing priorities (README.md “small, statically typed, … zero-dependency single binary, … built for clarity, safety, and expressiveness”), and the modern state of the art that the background research agents are gathering.

1. Why a C target at all

There are five concrete reasons a Mochi → C transpiler is worth building in 2026, even though Mochi already has a fast bytecode VM and a JIT:

  1. AOT distribution. A C output gives Mochi programs an unencumbered path to a single, statically-linked native binary on every triple the user’s C compiler supports, Linux amd64/arm64, macOS arm64, Windows x64, FreeBSD, wasm32-wasi, and embedded targets that the Go-based vm3 cannot reach (microcontrollers, kernels, hypervisors, restricted environments where a Go runtime is unwelcome).
  2. Smaller artifacts. A linked native binary that pulls in only the runtime objects it actually uses can be ~100 KB for a hello-world; a Go-runtime binary cannot. Important for CDN-deployed scripts and for future “Mochi on your fridge” stories.
  3. Cold-start latency. Native C, with statically-linked initial heap, starts in <1 ms. The Mochi VM cold-start is dominated by Go runtime init. This matters for serverless and CLI usage.
  4. Interop with native C libraries. A C-emitting backend can #include and call libcurl, libsodium, libsqlite, BLAS, etc. without the marshalling cost of import go. This unblocks systems-programming use cases the VM target cannot reach.
  5. An alternate verifiable lowering. The threat-model document names the verifier as the single point of policy. Having a second lowering path (vm3 bytecode and C source) is a structural check against verifier blind spots: bugs that survive both paths are real semantic bugs, not lowering bugs.

These reasons do not override the VM. They make C an additional output target that the user selects at build time (mochi build --target=c hello.mochi), the same way Go is built or interpreted as the user prefers.

2. Non-goals

What the C target deliberately does not try to do:

  • Replace the VM as the canonical runtime. The VM keeps the hot-iterate, REPL, agent-sandbox, and verified-bytecode positions. The C target is for AOT shipping.
  • Match the VM’s startup cost on multi-megabyte programs. Optimised C link times will be slower than mochi run.
  • Expose C-level UB to user code. A Mochi program that compiles to C must remain memory-safe at the source language level: every dereference is bounds-checked, every union access is tag-checked, every int overflow traps in debug builds.
  • Be a “Mochi as a C library” embedding API. That is a different MEP. The C target emits C, it is not an API for C callers to invoke Mochi ad hoc.
  • Encode every language feature in the first release. The phasing plan in the MEP body explicitly defers generate, full agents, full Datalog, and the import go|python|typescript FFI to later phases. The first ship is a Mochi-Core subset that covers ~80 % of the SPOJ corpus.

3. Five guiding principles

3.1 Spec-first

Every code-generation rule is documented in the MEP body before it is implemented. The MEP is the unit of review; the code lands in PRs that cite the MEP section they implement. This is the “spec-in-sync” rule already imposed on the Mochi MEP corpus (memory feedback_spec_in_sync) and it applies to MEP-45 from day zero.

3.2 Boring C

The C we emit should look like something a competent human would write, not like the inscrutable output of a bytecode-flattener. Concretely:

  • Named locals, not t1, t2, t3.
  • Comments that point back to the source line (// from main.mochi:42).
  • One C function per Mochi function, not a giant trampoline.
  • C control flow (if, for, while) for Mochi control flow.
  • No goto except for break/continue to labelled enclosing loops, and for exception unwinding where setjmp/longjmp is in play.

This is non-negotiable: the C target is also a teaching tool, a debugging aid, and an emergency-egress route for users who need to read the output. Nim’s nimcache output and Vala’s C output both demonstrate that this is achievable.

3.3 No surprises in the C ABI

The runtime is one library (libmochi.a / libmochi.so) with a stable header (mochi.h). The emitted user code links against it. The header is the contract; semantic versioning applies and the MEP body fixes the v1 surface.

3.4 Portability over performance, where they conflict

The first release targets correctness on every triple zig cc supports. Performance comes from a second pass once the targets are green. This ordering matters because portability bugs are caught by the CI matrix, and adding portability later requires rewrites that performance work makes harder. (Memory feedback_umbrella_phase_targets: an umbrella phase only lands when every target in the matrix is green.)

3.5 The C output is verifiable

Every C output should compile under:

  • -std=c23 -Wall -Wextra -Wpedantic -Werror
  • -fsanitize=address,undefined
  • -D_FORTIFY_SOURCE=3
  • -fstack-protector-strong

with no warnings, on the matrix of (gcc-15, clang-19, zig cc 0.16). Any warning surfaces a bug in the codegen, not a feature of the user code. The CI gate enforces this on the BG corpus.

4. The shape of the runtime

The runtime split is the design’s main lever. The current plan, before the background-research results land, is:

  • mochi-core: GC, value-tagging, list, map, set, string, time, duration, error, panic, longjmp-based try/catch, print, format.
  • mochi-query: dataset, group, sort, join, set ops; loaders (CSV, JSON, JSONL, YAML, Parquet stub).
  • mochi-net: fetch (libcurl), generate (provider shims), webhook helpers.
  • mochi-stream: stream/agent scheduler, fiber pool, channel.
  • mochi-logic: facts, rules, query engine (semi-naive eval).
  • mochi-ffi-go, mochi-ffi-python, mochi-ffi-ts: optional sidecar processes (one binary per host language) speaking the same JSON-over-pipe protocol the existing Go runtime uses.
  • mochi-ffi-c: header-only macros for direct #include + extern decl.

Each layer is a separate static archive so that a hello-world program links only mochi-core (target: <100 KB). A program that uses generate brings in mochi-net + the LLM provider’s TLS/HTTP code (~500 KB). A program that uses streams brings in mochi-stream (~80 KB). This linker slicing matters because the C target’s distribution story depends on keeping the small case small.

5. The taxonomy of what “compiling to C” actually means

A literature review (see notes 03 and 04) shows three distinct flavours of “language → C” transpilation, and we have to pick:

  1. Fully type-erased: emit void * everywhere, dispatch through a uniform value representation. This is what classical Scheme-to-C compilers (chicken, gambit) do. Lowest implementation cost, worst cache behaviour, hardest to debug.
  2. Boxed-by-default, unboxed-by-analysis: every value is a tagged union by default; escape analysis unboxes when safe. This is the Nim / Crystal / OCaml-mlton style.
  3. Monomorphised: every type instantiation produces its own C type and its own C function copy. Generics are specialised. This is the Rust / Cone / Roc / TigerBeetle style; closest to “human-readable C”.

MEP-45 picks #3 with selective boxing for sum types and any-typed ports. Concretely:

  • list<int>, list<string>, list<Point> are three separate C types.
  • A method area on Circle and on Square becomes two separate C functions.
  • A sum-type variant payload is boxed when the variant carries a non-trivial payload, inline when nullary.
  • A closure carrying an int captures it by value; carrying a list<T> captures by handle.

Rationale: monomorphisation gives us the small clean C output we need for principle 3.2, eliminates indirect calls in the hot loops the BG corpus measures, and aligns with how Roc and TigerBeetle structure their C-adjacent output. The cost is code-size growth from type-parametric helpers; the mitigation is amalgamation + LTO, which collapses identical instantiations.

6. Memory management

The principle: GC by default, opt-in regions for systems users.

Default heap: a conservative or precise tracing GC. The default option in note 04 is BDW-GC because it works on every target zig cc supports, ships in every distro, integrates with C trivially, and Crystal / Nim-refc-fallback / many Schemes all rely on it for the same reasons.

A second option, considered seriously in note 04, is Perceus-style reference counting in the Koka tradition: the type-checker tracks unique vs shared values, RC operations are elided where the analysis proves uniqueness, and a Bacon-Rajan cycle collector cleans up. This generates predictable-latency code and gives systems users the “deterministic free” they care about.

The MEP body proposes: ship v1 with BDW-GC behind a single mochi_gc.h abstraction, and reserve mochi_gc_perceus as a Phase-3 alternative that flips at link time. Same C output, different runtime.

7. Concurrency

Two layers:

  • Stream/agent dispatch maps to lightweight tasks. The plan: an M:N scheduler over OS threads with assembly-stack-switched fibers (minicoro or libco; both BSD-licensed, both portable to every triple including wasm via Asyncify). Per-agent serial execution; cross-agent parallel.
  • Test replay uses the same scheduler with MOCHI_SCHED=det forced; tasks are popped FIFO and stepped one at a time so a test block sees a deterministic interleaving.

C11 atomics + pthreads underlie the scheduler. Optional io_uring / kqueue / IOCP / wasi-poll under the libuv-like abstraction.

8. Error model

try/catch lowers to setjmp/longjmp around a per-fiber exception buffer. Caught values carry a mochi_error struct. This is the same mechanism Lua, OCaml-bytecode, Nim, and Crystal use; the cost is one setjmp per try (typically <10 ns on M-class hardware) and one frame of buffer overhead per active try.

The alternative, full algebraic effect handlers compiled in capability-passing style, is the more elegant lowering and the background research is gathering papers on it. The MEP body proposes shipping setjmp/longjmp for v1 and re-evaluating once Phase 2 has landed; the lowering is sufficiently localised that swapping in effect handlers later is a Phase-N change rather than an architectural one.

9. Build system

One command (mochi build --target=c -o app foo.mochi) is the entire user-facing surface. Under the hood:

  1. The transpiler emits one or more .c and .h files into .mochi-build/.
  2. A bundled cc (default: zig cc, fallback: the system compiler) compiles them.
  3. The runtime archives (libmochi-core.a, libmochi-query.a, …) are shipped inside the mochi binary as embedded blobs (Phase 2 uses #embed from C23).
  4. The resulting binary is the user’s artifact.

Reproducibility: set SOURCE_DATE_EPOCH, pin the zig version, emit a .mochi-build-manifest.json recording every input hash, compiler flag, and linker order.

10. The “what does the C look like?” answer

A sample to anchor the rest of the design. Given:

fun double(x: int): int { return x * 2 }
let xs = [1, 2, 3]
for n in xs { print(double(n)) }

The emitted C should look approximately like:

// hello.mochi.c, generated by mochi 0.x.y on 2026-05-22T17:00:00Z
#include "mochi/core.h"

static mochi_int hello_double(mochi_int x) {
    return mochi_imul(x, 2);
}

int main(int argc, char **argv) {
    mochi_init(argc, argv);
    mochi_list_int xs = mochi_list_int_of3(1, 2, 3);
    mochi_for_int(n, xs, {
        mochi_print_int(hello_double(n));
        mochi_print_newline();
    });
    mochi_list_int_drop(xs);
    return mochi_shutdown();
}

Notes embedded in this sketch that the MEP body must fix:

  • mochi_int is int64_t. Always.
  • mochi_imul is a checked-multiply macro that traps in debug, plain * in release (using <stdckdint.h> from C23 where present).
  • mochi_list_int is the monomorphised list type. The _of3 constructor is a varargs-free fixed-arity helper; longer literals call mochi_list_int_from_array(arr, n).
  • mochi_for_int is a macro that wraps a for loop with the right cursor type; no hidden allocation.
  • mochi_list_int_drop is RC-aware in the Perceus variant, no-op under GC.

This sample passes principle 3.2 (boring C), 3.4 (portable, no extensions), and 3.5 (compiles under sanitisers).

11. Closing, the philosophy in one sentence

A Mochi-to-C transpiler should emit C that a Mochi-fluent C programmer would have written by hand, that runs on every target zig cc supports, and that preserves every safety guarantee Mochi makes at the source level.

Everything else in MEP-45 is downstream of that statement.