Native language tradeoffs (exception candidates)

Companion to exception-to-go. Go is deliberately absent from the table below: Go is the baseline every kernel is measured against, not a candidate. This page exists so a proposal for "a different language for one specific kernel" — required-justification item 5 in exception-to-go — starts from evidence instead of rediscovering it under time pressure.

Scope is fixed: one small kernel behind a C ABI, caller-owned buffers, a pure-Go fallback shipping from day one. Each language is judged on the same five axes:

The table

LanguageBiggest riskWhy it hurts this kernel
Zig (named default)Manual lifetime across the boundaryA missed free in a long-running process is silent OOM
RustBuild cost, and safety stops at the FFI linetarget/ bloat + slow CI break the unified build; unsafe + panic rules reintroduce the exact bug class Rust was meant to remove
NimHidden runtime + small bus factorNimMain + per-thread GC init is invisible global state behind the boundary; the community is a fraction of Zig's and Nim 3 announces breaking changes
OdinPre-1.0 churn + thin ecosystemMonthly releases carry BREAKING changes; no package manager by design; LLM and hiring knowledge near zero
MojoVendor roadmap + churn historyOne company sets the pace; the C-ABI↔Go path is untraveled; the language is GPU-first, still missing general-purpose pieces

Zig — the default, with an admitted cost

Biggest risk is manual memory: every alloc needs its paired free, and the failure mode is a leak, not a compiler error. The counterweight is that the failure is detectable and deterministic: std.testing.allocator fails tests on leak, DebugAllocator.deinit() returns .leak with a stack trace, and the lifetime/leak gate makes those checks mandatory, including a loop test that catches accumulation a single call hides.

Zig stays the default for checkable properties, all verified in this repo's context: zig cc cross-compiles without a second toolchain, the stdlib is deep enough that a kernel needs no package ecosystem (std.net, std.crypto, std.compress, threads, SIMD), the static-lib-to-Go path is demonstrated, and explicit allocators are auditable in review. The admitted cost is pre-1.0 churn (0.14→0.15 rewrote Reader/Writer, ArrayList conventions, and removed usingnamespace and async/await), which is why the toolchain must be pinned and every kernel carries a pure-Go fallback.

Rust — the build is the risk, not the borrow checker

Inside Rust, the safety story holds. Across a C ABI into Go, almost none of it transfers, and the price is paid in build complexity:

None of this disqualifies Rust for a specific kernel — but a proposal must rebut every line above, not just cite "memory safety".

Nim — pleasant to write, wrong-shaped for the boundary

Nim's risk is what the Go side cannot see. Calling Nim from C (and therefore from Go via cgo) requires initializing the runtime with NimMain() and, for any GC-managed type, initializing the collector per calling thread (setupForeignThreadGc + nimGC_setStackBottom) — on Go's M:N scheduler that means initializing in every exported function or pinning threads. Shared references need manual GC_ref/GC_unref. The memory mode (--mm:arc, orc, atomicArc, yrc, refc, …) becomes part of the build contract, and the Go-interop mode (--mm:go) is documented as having "seen little real world use — use at your own risk".

Two further discounts: the community is roughly half of Zig's by stars with slower growth and no presence in the Stack Overflow admired-languages list, and the Nim 3 roadmap announces real breaking changes (reworked async, macros replaced by compiler plugins, explicit nil references, removal of multi-methods). Pinning handles the second; nothing handles the hidden runtime, which directly violates the stateless-boundary rule. Nim is a fine choice for stand-alone tooling — it is the wrong shape for an invisible kernel behind a Go API.

Odin — good language, premature dependency

Odin's trajectory is genuinely healthy: a decade of work, real shipped products (JangaFX), and a 1.0 ("Odin 2027") targeting RC by Christmas 2026 with a full language specification and backward-compatibility commitment. The risk is timing and thickness:

Revisit after the 1.0 specification ships and holds for a few quarters. Until then, a proposal must explain why Odin beats Zig for this specific kernel despite the above — "cleaner syntax" is not that explanation.

Mojo — best trajectory, worst timing for this use

Mojo 1.0 (Aug 2026) is real progress: core language features declared stable, stdlib APIs individually marked stable (a compiler-enforced contract, stronger than a blog promise), and nearly every breaking change shipping with a deprecated alias plus a compiler fix-it. But three facts dominate for a Go-side kernel:

Revisit when 1.x has a year of additive-only history and someone has shipped the Go interop path. For GPU kernels the calculus may already differ; for a CPU kernel behind a Go API it does not — yet.

How to use this page

A justification item 5 that proposes a non-Zig language must cite the corresponding row above and rebut it point by point, with the same benchmark-and-profile evidence the Zig path requires. "Ergonomics" or "syntax" alone rebuts nothing.

Revisit this page yearly, or when any of these land: Odin 1.0 spec holding for two quarters, Mojo 1.x with an additive-only year, Nim 3 stabilizing, or a demonstrated Go interop path for any of them.