The lowest-cognitive-load path: emit C, let GCC/Clang do the rest.
§1 Provenance
- Nim’s C backend: https://nim-lang.org/docs/backends.html
- Nim compiler user guide: https://nim-lang.org/docs/nimc.html
- Vlang (V): https://vlang.io/ ; the V compiler emits C by default.
- Hare uses QBE rather than C, so it is not an example here. Hare’s c-cproc relative
cprocis a C-to-QBE compiler. - Cython: https://cython.org/
- MicroPython’s mpy-cross with C emission: https://github.com/micropython/micropython
- Classic reference: Cooper and Torczon, “Engineering a Compiler” (EaC), Chapters 7 and 8 cover C as a target.
- Earlier well-known C backends: Eiffel (SmartEiffel), Sather, Mercury, Vala, Genie, Haskell’s old
-fvia-Cmode (deprecated post-GHC 7.x in favor of LLVM/NCG).
§2 Mechanism
The compiler emits a C source file (or many) representing the program’s semantics. The host system C compiler (GCC, Clang, MSVC, TCC) handles preprocessing, optimization, instruction selection, register allocation, ABI, debug info, and linking.
Typical strategies:
- One C function per source function. Direct mapping; readable output if you want it.
- Trampolined control flow: turn each basic block into a labeled statement; use
gotofor arbitrary control flow. This is the Nim and Vala approach. - Computed goto for interpreters: a fast switch-dispatch idiom (
&&label, GCC-specific). - setjmp/longjmp for exceptions: portable but slow.
- Native exceptions via
__builtin_setjmpor libunwind: better but per-compiler.
For garbage-collected languages, a C backend usually carries a runtime library (its own GC, scheduler, etc.) and the emitted C calls into that runtime.
§3 Target coverage (May 2026)
Every target that GCC, Clang, MSVC, or TCC supports. That is the appeal. Concretely for Mochi’s MEP-42 priority list:
- x86_64 Linux/macOS/Windows: all four C compilers.
- AArch64 Linux/macOS/Windows: GCC, Clang, MSVC.
- RISC-V RV64GC Linux: GCC, Clang.
- Wasm: Emscripten (Clang-based), wasi-sdk (Clang-based). Both produce wasm32 modules.
- Plus: every embedded target with a C compiler (AVR, MSP430, PIC, ARM Cortex-M, RP2040, ESP32, etc.). This is unmatched by any other backend on this list.
Object formats: whatever the host toolchain produces.
DWARF: full, from GCC/Clang. PDB on MSVC.
§4 Production / language adoption status (May 2026)
- Nim: ships C, C++, Objective-C, and JavaScript backends. C is the default. Used in production by Status (cryptocurrency), Galois, NimSkull, various game and systems shops.
- V (Vlang): C-only backend. Active community, controversial design but real adoption.
- Cython: emits C for Python; ubiquitous in scientific Python.
- Crystal: was C-backed in early versions; switched to LLVM. Still illustrative.
- GHC: had a C backend (
-fvia-C); removed because LLVM and the native code generator surpassed it in code quality. - Vala / Genie (GObject ecosystem): emit C.
- Haxe: ships a C++ backend (
hxcpp), morally similar. - MicroPython mpy-cross: optional C emit path for AOT.
- Eiffel / SmartEiffel: classic; the “Liberty Eiffel” community continues this tradition.
- TXR Lisp and other small languages.
Maintainership of “C as a target” as a technique is not a single project; it is a folklore pattern. Every C compiler the user has counts as part of the ecosystem.
Performance: with GCC -O3 or Clang -O3, generated code is essentially native-compiler quality. The penalty is purely compile-time: the C compiler must reparse, retypecheck, and re-optimize what the language frontend already understood.
License: unencumbered (the technique). Mochi’s emitted C is whatever Mochi chooses to license it as; the host C compiler’s license does not infect emitted code (Runtime Library Exception).
§5 Engineering cost for Mochi
- Binary footprint: Mochi adds zero new dependencies; the C compiler is the user’s responsibility.
- Build complexity: Mochi emits
.cfiles and shells out tocc. No cgo. No external libraries to link against at Mochi build time. - License: irrelevant; C-as-target is a technique.
- Cross-compilation: cross-compile by setting
CC=aarch64-linux-gnu-gcc(orzig cc -target ...for the zero-install hack). Mochi can ship a recommended toolchain matrix. - Debugging: full DWARF/PDB via the host compiler. Source mapping back to Mochi source requires
#linedirectives in the emitted C (standard technique used by Cython, Nim, Bison, etc.). - Runtime startup: pure AOT; nothing at runtime.
For a Go-hosted Mochi, this is the lowest engineering cost of every backend on the list. No FFI. No cgo. No new dependency. The downside is per-target compile speed and the awkwardness of shipping a C compiler with Mochi.
§6 Mochi adaptation note
Mochi already has runtime/tcc/Makefile (/Users/apple/github/mochilang/mochi/runtime/tcc/Makefile), suggesting prior thought about TCC integration. A compiler3/emit/c package would:
- Walk compiler3 IR (
/Users/apple/github/mochilang/mochi/compiler3/ir). - Emit one C function per Mochi function. Each vm3 op in
/Users/apple/github/mochilang/mochi/runtime/vm3/op.gobecomes a small inline C function or macro. - Emit the vm3 typed Cell (
/Users/apple/github/mochilang/mochi/runtime/vm3/cell.go) as a Cuint64_taggregate with helper macros. - Emit
#linedirectives to preserve Mochi source locations. - Shell out to
ccorzig cc.
The vm3 interpreter itself is essentially a C program written in Go; transliterating to C is straightforward. Memory management hooks would call into a small libmochi runtime (also written in C, or compiled from a Mochi runtime subset).
§7 Open questions for MEP-42
- Compile speed: C compilers are slow. A 10k-line Mochi program emits ~50k-100k lines of C; GCC
-O2may take seconds. Mitigation: ship-O0for debug builds and-O2for release. - Which C compiler? TCC is fast but produces poor code; GCC/Clang is slow but excellent. Recommendation: detect at install time, default to whichever is present, document Clang and TCC tradeoffs.
- Zig as portable cc:
zig cc(https://andrewkelley.me/post/zig-cc-powerful-drop-in-replacement-gcc-clang.html) bundles Clang with full cross-compilation toolchains for every target out of the box. A single Zig install gives Mochi cross-compilation to every triple. Strongly worth considering. - No JIT story: C-as-target is AOT-only. JIT would use a separate backend (golang-asm for now, copy-and-patch later).
- Runtime size: Mochi’s runtime (GC, scheduler, stdlib) must be available as either a static C library or as emitted C alongside user code.
- Verdict: C-as-target is the default Phase 1 path for AOT unless we have a strong reason to deviate. It maximizes target coverage and minimizes Mochi-side complexity.