Skip to content

QBE as a Code-Generation Backend for Mochi

Quentin Carbonneaux's deliberately tiny SSA backend, "70% of LLVM in 10% of the code."

§1 Provenance

§1.5 Self-description (from project home)

“QBE aims to provide 70% of the performance of industrial optimizing compilers in 10% of the code.”

The C codebase is kept “hobby-scale and pleasant to hack on” (https://c9x.me/compile/).

§2 Mechanism

QBE consumes a small text IR called the QBE IL. The IL is SSA, has block labels, basic types (w, l, s, d, plus aggregates), and a tiny set of instructions covering arithmetic, memory, compare-and-branch, and calls with full C ABI compliance. The compiler reads .ssa text, runs a handful of passes (SSA construction, sparse conditional constant propagation, copy elimination, simple GVN, dead-code elimination, register allocation by a graph-coloring allocator from Briggs/Cooper-style literature, peephole), then emits target-specific GAS-style assembly to stdout. The user invokes as and ld (or cc) to finish.

There is no JIT API. QBE is strictly a textual frontend-to-asm pipe. Total source is ~12k lines of C in the official tarball.

§3 Target coverage (May 2026)

  • amd64 on Linux, macOS, OpenBSD, FreeBSD: stable.
  • arm64 on Linux, macOS: stable.
  • riscv64 (RV64GC) on Linux: stable, added 2022-2023.
  • ARM32: in active development under NLnet funding (2025-10 start, https://nlnet.nl/project/QBE-32-bit-ARM-support/), driven by the Hare ecosystem needing it for ARMv7 hardware.
  • No Wasm, no Windows, no PowerPC, no MIPS. No s390x.

Object format: QBE emits text assembly. The system as produces ELF on Linux/BSD and Mach-O on macOS. PE/COFF is not natively supported.

DWARF: basic line tables emitted as .loc directives that GAS handles; no full DI tree.

§4 Production / language adoption status (May 2026)

  • Hare (https://harelang.org): primary production user. Drew DeVault’s systems language uses QBE as its only backend and has driven much of QBE’s recent feature work, including the upcoming ARM32 port.
  • cproc (https://sr.ht/~mcf/cproc): a C11 compiler that targets QBE; useful as a small, hackable C frontend.
  • Lacc (https://github.com/larmel/lacc): another C compiler with a QBE backend option.
  • bf-q, scc: various small experiments.

Maintainership is essentially Quentin Carbonneaux plus a small group of patch contributors via sourcehut. Releases are infrequent but the mob branch is reasonably active.

Performance: per Carbonneaux’s own claim (validated by Hare benchmarks), generated code runs at roughly 70% of LLVM -O2 speed on integer-heavy code; QBE typically loses on tight FP loops and on code that benefits from vectorization (QBE has no auto-vectorizer).

§5 Engineering cost for Mochi

  • Binary footprint: The qbe binary is ~500 KB stripped. No runtime library is required.
  • Build complexity: Trivial. make && make install. No dependencies beyond a C99 compiler. For Mochi, the easiest integration is: shell out to qbe with a textual .ssa input, capture assembly, pipe through as and ld. This is the same model that Hare itself uses.
  • License: MIT.
  • Cross-compilation: Built-in via qbe -t amd64_sysv|amd64_apple|arm64|arm64_apple|rv64. One QBE binary handles all targets.
  • Debugging: Minimal. Adequate for stack traces; not enough for source-level debugging without extra DWARF emission.
  • Runtime startup: None. QBE is invoked at build time only.

§6 Mochi adaptation note

QBE IL is a near-perfect target for compiler3’s existing IR (/Users/apple/github/mochilang/mochi/compiler3/ir). The mapping:

  • Mochi typed Cells (8-byte handles, see /Users/apple/github/mochilang/mochi/runtime/vm3/cell.go) become QBE l (64-bit) values.
  • The three-bank register file in runtime/vm3/vm.go becomes SSA temporaries.
  • Each vm3 op in runtime/vm3/op.go becomes a small QBE snippet, often 2-5 QBE instructions.
  • A new compiler3/emit/qbe package would be ~1000-2000 lines of Go, emitting text and invoking qbe+as+ld as a build step.

This is the cleanest “Phase 1 naive native emitter” candidate: it preserves Mochi’s pure-Go ethos at build time (QBE is a subprocess), it covers x86_64, arm64, riscv64 (matching MEP-42’s three priority targets), and total integration cost is one weekend of work.

§7 Open questions for MEP-42

  • No Wasm target: If MEP-42 wants Wasm as a first-class peer to x86_64/arm64/riscv64, QBE alone is insufficient. Pair with wasmtime compile (see 12_wasmtime_aot.md).
  • No Windows native: Mochi-on-Windows-x86_64 would need a different backend, or a WSL/MinGW workaround.
  • 70% performance ceiling: If MEP-42 Phase 2 targets parity with optimized C/Go output, QBE caps out; a Phase 2 LLVM/MLIR path becomes necessary.
  • Bus factor: QBE is essentially one person. Mochi depending on it long-term means accepting that risk or being prepared to fork.
  • DWARF gap: Native source-level debugging will need extra work (probably emitting our own .debug_line).
  • Will the ARM32 work land on schedule? NLnet started funding October 2025; ARMv7 hosts would unlock Mochi-on-Raspberry-Pi-Zero if it does.