Skip to main content

MEP 54. Mochi-to-Go transpiler

FieldValue
MEP54
TitleMochi-to-Go transpiler
AuthorMochi core
StatusActive
TypeStandards Track
Created2026-05-29 15:50 (GMT+7)
DependsMEP-4 (Type System), MEP-5 (Type Inference), MEP-13 (ADTs and Match), MEP-45 (C transpiler, IR reuse), MEP-46 (BEAM transpiler, agent shape), MEP-47 (JVM transpiler, IR reuse), MEP-48 (.NET transpiler, IR reuse), MEP-53 (Rust transpiler, rtree AST pattern)
Research/docs/research/0054/
Tracking/docs/implementation/0054/

Abstract

Mochi today ships vm3 (mochi run), a C transpiler producing native single-file binaries (MEP-45), a BEAM transpiler producing supervised concurrent runtimes (MEP-46), a JVM transpiler producing Maven-Central-interoperable jars (MEP-47), a .NET transpiler producing NuGet-interoperable assemblies (MEP-48), a Swift transpiler producing Apple-ecosystem binaries (MEP-49), a Kotlin transpiler producing JVM and Android binaries (MEP-50), a Python transpiler (MEP-51), a TypeScript / JavaScript transpiler (MEP-52), a Rust transpiler (MEP-53), a PHP transpiler (MEP-55), and a Ruby transpiler (MEP-56). None target the Go ecosystem directly: over 500,000 Go modules indexed on pkg.go.dev as of 2026-05, the canonical 2014-2026 cloud-native systems language (Docker, Kubernetes, Terraform, Prometheus, etcd, Consul, Istio, containerd, CRI-O, gVisor, OpenTelemetry collectors, the Cilium eBPF datapath), the dominant CLI and serverless target after Node.js, and the language with the shortest "binary out of git checkout" toolchain story of any post-2010 language (go install and a tag). MEP-54 specifies a Mochi-to-Go pipeline that lowers a type-checked Mochi program to Go source emitted via a structural gotree AST, then drives the Go toolchain to produce a statically linked single-file native binary, a publishable Go module, a wasm/js bundle, or a wasip1 module.

The pipeline reuses MEP-45's typed-AST and aotir IR, plus the monomorphisation, match-to-decision-tree, and closure-conversion passes shared with every other transpiler. It forks at the emit stage: instead of emitting ISO C23 (MEP-45), Core Erlang via cerl (MEP-46), Java source via JavaPoet (MEP-47), C# source via Roslyn (MEP-48), Swift source via SwiftSyntax (MEP-49), Kotlin source via KotlinPoet (MEP-50), Python source (MEP-51), TypeScript source (MEP-52), Rust source via the rtree AST (MEP-53), PHP source (MEP-55), or Ruby source via the Ruby rtree (MEP-56), it emits Go source as gotree.SourceFile trees, then renders to disk through go/format (the same formatter the standard gofmt tool uses) so output is canonically formatted by construction.

Nine packaging targets ship together: TargetGoBinaryLinuxAmd64, TargetGoBinaryLinuxArm64, TargetGoBinaryDarwinAmd64, TargetGoBinaryDarwinArm64, TargetGoBinaryWindowsAmd64, and TargetGoBinaryFreeBSDAmd64 use Go's first-class GOOS=... GOARCH=... go build cross-compile path (no extra linker, no zigbuild equivalent needed); TargetGoModule exports the full Go module (go.mod + go.sum + emitted *.go + vendored runtime) into the output directory without invoking go build, used for publish-ready inspection; TargetGoWasmJS produces a .wasm runnable in a browser via wasm_exec.js; TargetGoWasiP1 produces a .wasm runnable under wasmtime via the wasip1 target shipped in Go 1.21+.

The intended master correctness gate is byte-equal stdout from the produced binary versus vm3 on the entire fixture corpus, across Go 1.26.0 and Go 1.26.x latest on ubuntu-24.04 (host), macos-15 (Apple silicon), and windows-2025. As of 2026-05-29 phases 0-7.12 and phase 9.1 are LANDED on the host (ubuntu-24.04 + macos-15); the cross-compile matrix and Windows native-build coverage stays gated under sub-phase 16 (reproducibility) once the full language surface lands. See /docs/implementation/0054/ for the live status. vm3 is the recording oracle for expect.txt; the transpiler does not link against or depend on vm3.

Five load-bearing decisions:

  1. Go source via a structural gotree AST, rendered through go/format. The default emit path constructs gotree.SourceFile trees, then calls gotree.Render(sf) which passes the printed tokens through go/format.Source. The structural representation gives free indentation handling, avoids whitespace bugs (a class of issue that plagues string-template-based emitters), and produces output that is byte-equal to what a human Go programmer running gofmt would produce. The gotree.Render step is allowed to fail on syntactically-invalid trees, which catches lowerer bugs as compile errors before go build ever runs.

  2. Native goroutines and channels. Mochi channels lower to native chan T (make(chan T, cap)); send is ch <- v; recv is <-ch. Agents lower to a goroutine wrapping a for msg := range ch receive loop. Streams lower to a struct holding []chan T subscriber slots with bounded capacity for backpressure. No sync.Mutex is required in the runtime: Go's channel semantics directly model Mochi's bounded blocking channel. Goroutines are stack-managed by the runtime so the per-agent cost is ~2KB, not a 1MB OS thread.

  3. Reuse MEP-45's aotir IR. The IR is target-agnostic; monomorphisation, match-to-decision-tree, and closure-conversion passes run once and feed eleven backends. The fork is at the emit pass: transpiler3/go/lower/ lowers aotir to Go-source structural nodes. Go-specific lowerings (e.g., closure environments lifted into a struct via ClosureEnvStmt, discriminated interface + final variant structs for sum types, omap as a small generic helper that pairs a Go map with an insertion-order slice) are localised to the Go lower pass.

  4. Deterministic go build via -trimpath -buildvcs=false + SOURCE_DATE_EPOCH. Identical inputs produce byte-identical outputs across Driver.Build invocations. -trimpath strips the absolute filesystem path from the binary; -buildvcs=false disables git-derived stamping; SOURCE_DATE_EPOCH=0 neutralises any remaining time-derived metadata. On macOS the gate is platform-skipped: the Mach-O LC_UUID load command is randomised per link by Apple's ld64 and cannot be controlled from go-side flags. The Linux and Windows gates are enforced.

  5. Runtime module published to pkg.go.dev under dev.mochilang/runtime/go. Apache-2.0, ~1200 LOC of Go across ~10 packages (agent, collections, datalog, llm, option, query, result, stream, stringz, timez), zero third-party deps in the default build. Vendored into every emitted module so the produced binary builds offline. Importable directly from any Go program via go get dev.mochilang/runtime/go/collections for users who want the typed collections without the transpiler.

The gate for each delivery phase is empirical: every Mochi source file in tests/transpiler3/go/fixtures/ must compile via the Go pipeline and produce stdout that diffs clean against the expect.txt recorded by vm3. go build ./... clean on emitted code is the secondary gate. go vet ./... clean is the tertiary gate. GOOS=linux GOARCH=arm64 go build clean (for the cross-build targets), GOOS=js GOARCH=wasm go build clean (for the wasm/JS target), GOOS=wasip1 GOARCH=wasm go build clean (for the wasip1 target), SHA-256-byte-equal output across two builds with Deterministic=true (for the reproducibility gate), and go mod tidy clean against the published module (for the publish gate) are the quaternary gates.

Motivation

Mochi today targets vm3, C (MEP-45), BEAM (MEP-46), the JVM (MEP-47), .NET (MEP-48), Swift (MEP-49), Kotlin (MEP-50), Python (MEP-51), TypeScript (MEP-52), Rust (MEP-53), PHP (MEP-55), and Ruby (MEP-56). None deliver what Go uniquely provides:

  1. The cloud-native ecosystem. Kubernetes, Docker, Terraform, Consul, Vault, Nomad, Prometheus, Loki, Tempo, Grafana, etcd, CockroachDB (a substantial part), TiDB, Vitess, gVisor, containerd, CRI-O, Cilium's userspace, Istio's control plane, Envoy's xDS gRPC servers (some), OpenTelemetry collectors, Datadog Agent, the entire HashiCorp toolchain, and the Cloud Native Computing Foundation's graduated-project list are dominantly Go. A Mochi program that needs to ship as a DaemonSet sidecar, a kubectl plugin, a Terraform provider, or an OTel receiver compiles into the same binary shape as every other tool in that ecosystem.

  2. First-class cross-compile. GOOS=linux GOARCH=arm64 go build runs on a macOS x86_64 developer machine and produces a statically linked Linux arm64 binary in seconds, without any cross-toolchain install. The matrix scales to every (linux, darwin, windows, freebsd, openbsd, netbsd) x (amd64, arm64, arm, riscv64, s390x, ppc64le) combination that Go supports. No other modern language has this story without setup pain (Rust needs cargo-zigbuild or per-target linker installs; C needs per-target toolchains; the JVM is bytecode but needs platform-specific JRE; .NET NativeAOT needs per-platform SDKs; Swift cross-compile is darwin-host-only in practice).

  3. go install and pkg.go.dev. go install dev.mochilang/runtime/go/cmd/...@latest is the entire dependency story for distributing a Mochi-emitted Go program to another Go developer. pkg.go.dev auto-indexes every published module (it follows the module mirror at proxy.golang.org). Versioning is tag-driven (v1.2.3). Publication is git tag + git push; the proxy fetches the module the next time anyone references it.

  4. WebAssembly: both browser and wasip1. Go has shipped first-class GOOS=js GOARCH=wasm since 1.11 (Aug 2018) and GOOS=wasip1 GOARCH=wasm since 1.21 (Aug 2023). The browser target produces a .wasm that runs under any modern browser via the bundled wasm_exec.js glue; the wasip1 target produces a .wasm that runs under wasmtime, wasmer, or WasmEdge with no extra glue. Mochi-on-Go reaches both deployment surfaces from the same source.

  5. Goroutines and channels: the agent / stream story is native. Mochi agents and channels map onto Go's go statement and chan T with zero translation tax. No userland scheduler, no Rc<RefCell<...>> single-thread compromise (MEP-53), no sync.Mutex-wrapped queues, no asyncio.Queue (MEP-51), no Promise-based fake-concurrency (MEP-52). A Mochi program spawning 10,000 agents costs ~20MB of total Go runtime stack, scheduled cooperatively across the available OS threads via the Go runtime's M:N work-stealing scheduler.

  6. Reproducible builds out of the box. go build -trimpath -buildvcs=false plus SOURCE_DATE_EPOCH=0 produces bit-identical binaries across machines and time on Linux and Windows (the macOS LC_UUID risk is documented in §Risks). Reproducible-builds.org's 2025 status report lists Go 1.22+ as one of the leading reproducible-by-default toolchains.

  7. The shortest binary-out-of-checkout story. git clone && go build produces a single statically linked binary with no extra steps on every platform Go supports. No ./configure, no cmake, no meson, no cargo build --release --target ..., no Maven, no NuGet restore. For a Mochi program intended to ship as a sidecar binary, this matches the C target's distribution shape (MEP-45) but without manually picking a libc.

  8. First-class FFI to C and SQLite, no glue language needed. import "C" is a stable Go feature since 1.0 (Mar 2012). A Mochi program needing to call into libsqlite3, libpcap, libxml2, or any C library does so through a single cgo-annotated declaration block. The runtime cost is one syscall-boundary cross per call (no marshalling layer like JNI for the JVM).

The C target remains the right choice for minimal binary size and bare-metal targets without garbage collection. The BEAM target remains the right choice for hot-reload services and OTP supervision. The JVM target remains the right choice for Maven Central interop. The .NET target remains the right choice for NuGet and Windows enterprise. The Swift target remains the right choice for the Apple ecosystem. The Kotlin target remains the right choice for Android. The Python target remains the right choice for PyPI and data-science workflows. The TypeScript target remains the right choice for npm and web. The Rust target remains the right choice for crates.io interop, no_std embedded, and the strict-borrow story. The PHP target remains the right choice for WordPress and Packagist. The Ruby target remains the right choice for RubyGems and Rails. The Go target is the right choice for cloud-native deployment, cross-platform CLI distribution, goroutine-native concurrency, pkg.go.dev publication, browser wasm, and wasip1 sidecar binaries.

Specification

This section is normative.

1. Pipeline and IR reuse

The Go pipeline reuses MEP-45's aotir IR. The emit stage forks: transpiler3/go/lower/Lower(prog, fileBase, packageName) consumes *aotir.Program and returns *gotree.SourceFile. transpiler3/go/emit/Emit(sf, workDir) writes the rendered source to disk via go/format.Source. The driver at transpiler3/go/build/Driver.Build(src, out, target) glues parse to typecheck to clower.Lower to go/lower.Lower to emit to go build.

2. Toolchain detection

build.resolveGo() resolves go in order:

  1. $MOCHI_GO (env override)
  2. $GOROOT/bin/go (if GOROOT is set)
  3. ~/sdk/go1.26.0/bin/go (the go install golang.org/dl/go1.26.0@latest && go1.26.0 download convention)
  4. exec.LookPath("go")

The minimum supported Go version is 1.21 (the first version with stable GOOS=wasip1); the production gate runs on Go 1.26.0 and Go 1.26.x latest (the active stable as of 2026-05).

3. Surface-syntax lowering

Mochi constructGo lowering
let x = ex := e
var x = evar x T = e (typed when the lowerer knows T)
if/elif/elseif cond { ... } else if cond { ... } else { ... }
while c { ... }for c { ... }
for i in lo..hi { ... }for i := lo; i < hi; i++ { ... }
for x in xs { ... }for _, x := range xs { ... }
fun f(a: T): U { ... }func f(a T) U { ... } (free function, package-level)
let f = fun(a: T): U => bodyf := func(a T) U { body } with captures lifted via ClosureEnvStmt into a heap-allocated env struct when crossing a goroutine boundary
match e { Variant(x) => arm }switch v := e.(type) { case *VariantStruct: x := v.Field; arm; ... } (sum-type dispatch via type switch on the discriminated interface)
type T = A | Btype T interface { isT() } marker + type A struct {...}; func (*A) isT() {}; type B struct {...}; func (*B) isT() {} per variant
record User { id: int }type User struct { Id int64 } (exported field; comparable by == when all fields are comparable)
type Pair = { a: int, b: int } (anonymous record)identical to record form, both lower through lowerStruct
Stream s = make_stream(N)s := mochiruntime.StreamMake[T](N) (subscriber list + per-sub bounded channel)
subscribe(s)sub := s.Subscribe() (returns chan T)
subscribe_limit(s, N)sub := s.SubscribeLimit(N)
emit(s, v)s.Emit(v)
recv_sub(sub)<-sub
Channel make_chan(N)make(chan T, N)
send(ch, v)ch <- v
recv(ch)<-ch
agent A { state ... on Msg ... }type AAgent struct { in chan AMsg; state ... }; func NewA() *AAgent { a := &AAgent{...}; go a.run(); return a }; func (a *AAgent) run() { for m := range a.in { switch m := m.(type) { ... } } }
spawn AgentType()NewAgentType() (returns the agent value; goroutine spawned inside New)
a.intent(arg)a.in <- &AgentTypeMsgIntent{Arg: arg}
async exprexpr (immediate evaluation; the colour pass decides where async boundaries become go statements)
await futfut (identity; the colour pass elides the await when fut is already an immediate value)
try { ... } catch e { ... }func() { defer func() { if r := recover(); r != nil { e := r.(int64); ... } }(); ... }()
panic(code)panic(int64(code))
break / continuebreak / continue
from x in xs where p select edesugared by clower into an aotir.QueryExpr; the Go lowerer emits a loop with append into a typed slice: result := []ET{}; for _, x := range xs { if p { result = append(result, e) } }
... order by k skip s take tsort.SliceStable(result, func(i, j int) bool { return ki < kj }); result = result[s:min(len(result), s+t)]
Datalog query parent(_, Y)evaluated at compile-time via semi-naive fixpoint in transpiler3/go/lower/datalog.go; emitted as a frozen slice literal of pre-computed result tuples
min(xs) / max(xs) / sum(xs)slices.Min(xs) / slices.Max(xs) / sumHelper(xs) (the runtime helper handles int64 / float64 dispatch)
in(x, xs)slices.Contains(xs, x)
map(xs, f) / filter(xs, p) / reduce(xs, acc, f)mochiruntime.Map(xs, f) / mochiruntime.Filter(xs, p) / mochiruntime.Reduce(xs, acc, f)
len(xs) / len(s) / len(m) / len(set)int64(len(xs)) / int64(utf8.RuneCountInString(s)) / int64(len(m)) / int64(len(set))
keys(m) / values(m)mochiruntime.MapKeys(m) / mochiruntime.MapValues(m)
append(xs, v)append(xs, v)
slice(xs, lo, hi) / xs[lo:hi]xs[lo:hi]
sort(xs)mochiruntime.Sort(xs) (returns a sorted copy; the input slice is not mutated)
abs(n) / floor(f) / ceil(f)mochiruntime.AbsI64(n) / math.Floor(f) / math.Ceil(f)
str(v)mochiruntime.Str(v) (dispatches on type at lowering time, emits strconv.FormatInt or strconv.FormatFloat or identity for string)
upper(s) / lower(s)strings.ToUpper(s) / strings.ToLower(s)
index(s, sub) / contains(s, sub)int64(strings.Index(s, sub)) / strings.Contains(s, sub)
substring(s, lo, hi)mochiruntime.Substring(s, lo, hi) (rune-aware; lo and hi are rune offsets)
reverse(s) / split(s, sep) / join(xs, sep)mochiruntime.ReverseString(s) / strings.Split(s, sep) / strings.Join(xs, sep)
Set literal set{1, 2} / add(s, x) / has(s, x)map[int64]struct{}{1: {}, 2: {}} / s[x] = struct{}{} / _, ok := s[x]; ok
Map literal / m[k] / has(m, k)map[K]V{k: v, ...} / m[k] / _, ok := m[k]; ok
readFile(p)mochiruntime.ReadFile(p) (wraps os.ReadFile; returns empty string on error matching vm3 semantics)
lines(p)mochiruntime.Lines(p) (returns []string)
writeFile(p, s) / appendFile(p, s)os.WriteFile(p, []byte(s), 0644) / mochiruntime.AppendFile(p, s)
loadCSV(p) / saveCSV(p, xs)mochiruntime.LoadCSV(p) / mochiruntime.SaveCSV(p, xs) (encoding/csv based)
json_decode(s)mochiruntime.JsonDecode(s) (returns map[string]any matching vm3's any-typed JSON shape)
fetch <url> / httpGet(url)mochiruntime.HttpGet(url) (wraps net/http.Get with no TLS verification toggle)
generate <provider> { prompt: p, model: m }mochiruntime.LlmGenerate(provider, model, p) (cassette replay over $MOCHI_LLM_CASSETTE_DIR/<sha256(provider:model:prompt)>.txt)
(int)f / int(x)int64(f) (Go conversion truncates toward zero, matching the C target)
a / b integer dividemochiruntime.DivI64(a, b) (panics with code 5 on zero)
a % b integer modulomochiruntime.ModI64(a, b)
xs[i] indexingbounds-checked at the Go runtime layer; out-of-range panic is rewrapped to code 4 by the try/catch lowering when present
!b boolean negation!b
Bareword identifier collisions (func, var, type, range, chan, ...)suffix with _ (e.g., func_) to avoid Go keyword clash

4. Runtime module

dev.mochilang/runtime/go (Apache-2.0, ~1200 LOC of Go across ~10 packages, zero third-party deps in the default build) exports:

  • collections — typed list / map / set / omap helpers (Map, Filter, Reduce, Sort, MapKeys, MapValues, generic OMap[K, V])
  • query — query-DSL helpers (joins, group-by, top-K via heap-backed container/heap, arena_query arena-allocated result slice)
  • agent — agent supervisor (Spawn, Stop, supervised goroutine restart)
  • stream — bounded broadcast stream with per-subscriber chan T slots and explicit backpressure
  • datalog — semi-naive fixpoint helpers (most Datalog evaluation is compile-time; this package handles the runtime fallback)
  • option / result — sum-type helpers for Option<T> and Result<T, E> lowering
  • stringz — UTF-8-aware string helpers (Substring, ReverseString, RuneAt)
  • timez — deterministic time helpers (Now is overridable via MOCHI_NOW_NS)
  • llm — cassette replay; provider plugins under dev.mochilang/runtime/go/llm/<provider> (openai, anthropic, etc.) but the base package only knows how to read cassettes
  • The top-level package exports the panic / recover helpers, Str, AbsI64, DivI64, ModI64, Lines, ReadFile, AppendFile, LoadCSV, SaveCSV, JsonDecode, HttpGet, LlmGenerate

The runtime is vendored into every emitted module under vendor/dev.mochilang/runtime/go/ so the produced binary builds offline without hitting proxy.golang.org.

5. Build targets

TargetSwitch caseOutput layout
TargetGoBinaryLinuxAmd64go-linux-amd64<out>/<name> (GOOS=linux GOARCH=amd64)
TargetGoBinaryLinuxArm64go-linux-arm64<out>/<name> (GOOS=linux GOARCH=arm64)
TargetGoBinaryDarwinAmd64go-darwin-amd64<out>/<name> (GOOS=darwin GOARCH=amd64)
TargetGoBinaryDarwinArm64go-darwin-arm64<out>/<name> (GOOS=darwin GOARCH=arm64)
TargetGoBinaryWindowsAmd64go-windows-amd64<out>/<name>.exe (GOOS=windows GOARCH=amd64)
TargetGoBinaryFreeBSDAmd64go-freebsd-amd64<out>/<name> (GOOS=freebsd GOARCH=amd64)
TargetGoModulego-module<out>/go.mod + <out>/main.go + <out>/vendor/... (no go build invocation; for publish-ready inspection)
TargetGoWasmJSgo-wasm-js<out>/<name>.wasm + <out>/wasm_exec.js (GOOS=js GOARCH=wasm)
TargetGoWasiP1go-wasip1<out>/<name>.wasm (GOOS=wasip1 GOARCH=wasm)

Build flags include -trimpath (strips absolute filesystem paths) and -buildvcs=false (disables git-derived stamping) by default. When Driver.Deterministic=true, the additional flags -ldflags=-buildid= -ldflags=-s -ldflags=-w strip the build ID and symbol table; SOURCE_DATE_EPOCH=0 is set in the build env. Cache directory defaults to ~/.cache/mochi/go/ and is content-addressed by SHA-256 of the workspace path; Driver.NoCache=true skips the cache and forces -a (rebuild everything).

Phases

See /docs/implementation/0054/ for the per-phase tracking matrix. Nineteen phases cover skeleton (0), language surface (1-7.12), concurrency (8-12), data integrations (13-14), and packaging / reproducibility / cross-platform (15-18). Sub-phases (e.g., 7.1, 7.2, ..., 9.1) split umbrella phases into shippable units once the gate matrix reveals a target tuple is not yet green.

A phase is LANDED only when its gate is green on every Go runtime listed for it in §6 below.

6. Target matrix

The matrix below is the intended correctness gate, not the current CI matrix. As of 2026-05-29, phases 0 through 7.12 and 9.1 are LANDED on Go 1.26.0 + Go 1.26.x latest on ubuntu-24.04 + macos-15. The Windows native build target is exercised but not yet gated per phase; full Windows phase coverage lands under sub-phase 16.x. The wasm/JS and wasip1 targets are gated by phase 17. See /docs/implementation/0054/ for the live runtime-matrix status.

Phase scopeGo 1.26 ubuntu-24.04Go 1.26 macos-15Go 1.26 windows-2025wasm/js (browser)wasip1 (wasmtime)
Scalars / arithmeticLANDEDLANDEDrequired (16.x)required (17)required (17)
Lists / maps / setsLANDEDLANDEDrequiredrequiredrequired
Records / sumsLANDEDLANDEDrequiredrequiredrequired
ClosuresLANDEDLANDEDrequiredrequiredrequired
Queries (filter, map, order, joins)LANDEDLANDEDrequiredrequiredrequired
DatalogNOT STARTED (phase 8)NOT STARTEDNOT STARTEDNOT STARTEDNOT STARTED
Channels (chan T)LANDED (9.1)LANDEDrequiredrequiredrequired
StreamsNOT STARTED (phase 9.2)NOT STARTEDNOT STARTEDNOT STARTEDNOT STARTED
Agents (goroutine + chan)NOT STARTED (phase 10)NOT STARTEDNOT STARTEDNOT STARTEDNOT STARTED
Async / awaitNOT STARTED (phase 11)NOT STARTEDNOT STARTEDNOT STARTEDNOT STARTED
FFI (cgo)NOT STARTED (phase 12)NOT STARTEDNOT STARTEDn/a (no cgo on wasm)n/a
LLM (cassette)NOT STARTED (phase 13)NOT STARTEDNOT STARTEDNOT STARTEDNOT STARTED
Fetch + JSONNOT STARTED (phase 14)NOT STARTEDNOT STARTEDn/a (no net/http on browser wasm without fetch shim)required
Try / catch / panicLANDED (7.10)LANDEDrequiredrequiredrequired
go-module packagingNOT STARTED (phase 15)NOT STARTEDNOT STARTEDNOT STARTEDNOT STARTED
ReproducibilityNOT STARTED (phase 16, Linux + Windows gated; darwin LC_UUID skipped)NOT STARTEDNOT STARTEDNOT STARTEDNOT STARTED
wasm/js + wasip1NOT STARTED (phase 17)NOT STARTEDn/aNOT STARTEDNOT STARTED
PublishNOT STARTED (phase 18, pkg.go.dev metadata + signed tag)NOT STARTEDNOT STARTEDNOT STARTEDNOT STARTED

The cross-build targets (go-linux-arm64, go-darwin-amd64, go-freebsd-amd64) use Go's GOOS=... GOARCH=... go build cross-compile path; no extra linker or toolchain install is required because Go ships a complete cross-toolchain in the base SDK. wasm/js requires the wasm_exec.js glue from $(go env GOROOT)/lib/wasm/; wasip1 requires wasmtime on PATH for the gate to run (skipped if absent).

Toolchain handling currently auto-detects go via MOCHI_GO, GOROOT/bin/go, then PATH. The go install golang.org/dl/go1.26.0@latest SDK convention is detected via ~/sdk/go1.26.0/bin/go for the secondary toolchain in the matrix.

Alternatives considered

  1. Emit via go/ast (the standard-library AST) instead of an in-tree gotree. Rejected: go/ast is the AST that go/parser produces, not an AST designed for ergonomic synthesis from a non-Go program. Constructing a valid go/ast.File from scratch requires populating dozens of position fields (token.Pos) for every node so go/printer produces sensible output. The structural gotree is shaped for synthesis (no token.Pos field on any node; positions are reconstructed by the renderer) and is shared in concept with the Rust rtree (MEP-53) and Ruby rtree (MEP-56).
  2. Use sync.Mutex + condvars for channels. Rejected: Go's native chan T already provides bounded blocking-queue semantics with goroutine scheduling that exactly matches Mochi's bounded blocking channel. Wrapping the chan in a Mutex would add a synchronisation cost the source language does not require.
  3. Use a green-thread library (e.g., tinygo.org/x/coroutines) for agents. Rejected: goroutines are already cooperatively scheduled by the Go runtime; an external coroutine library would add overhead without changing semantics. Goroutine stacks start at 2KB and grow as needed, so 10,000 agents fit in ~20MB.
  4. Use encoding/json for json_decode. Accepted with a wrapper: encoding/json is stdlib and handles all JSON shapes correctly. The wrapper in mochiruntime.JsonDecode returns map[string]any rather than interface{} to match vm3's any-typed shape, and panics with code 97 on malformed input.
  5. Use net/http for httpGet. Accepted with a wrapper: net/http is stdlib, supports TLS (unlike the Rust target's mochi_runtime::fetch::get which is plain TCP only), and supports Transfer-Encoding: chunked transparently. The wrapper in mochiruntime.HttpGet collapses the request/response/body lifecycle into a single call.
  6. Use crypto/sha256 for the LLM cassette key. Accepted: stdlib, well-audited, fast enough that no inlined hash is needed.
  7. Bundle the runtime as a single inlined file per emission instead of an external module. Rejected: makes generated code untraceable under go doc and breaks go install. Shipping dev.mochilang/runtime/go as a real Go module lets users go get dev.mochilang/runtime/go/collections from any Go program and lets the LLM cassette / fetch / panic packages be tested in isolation.
  8. Skip the closure-environment lifting (ClosureEnvStmt) and rely on Go's native closure capture. Rejected: Go's native closure capture is by-reference, which races when the same closure is invoked from multiple goroutines reading the captured variables. The ClosureEnvStmt pattern explicitly snapshots the captured variables into a per-closure struct at the point of closure creation, matching Mochi's by-value capture semantics.

Risks

  1. macOS LC_UUID non-determinism. The Mach-O LC_UUID load command is randomised per link by Apple's ld64. Cannot be controlled from go-side flags. Phase 16 platform-skips the macOS reproducibility gate and asserts only the Linux and Windows equivalents. Mitigation: document the skip prominently; users who need bit-reproducible darwin binaries are directed to a post-link patcher.
  2. Browser wasm has no net/http server. GOOS=js GOARCH=wasm cannot run an HTTP server (no net.Listen); the client side works via the browser's fetch API but requires the wasm_exec.js glue's XHR shim. Phase 17 fixtures targeting wasm-js exclude httpGet; fetch coverage on browser wasm waits until a MOCHI_FETCH_GLUE shim ships.
  3. wasip1 cgo gap. GOOS=wasip1 GOARCH=wasm does not support cgo. FFI fixtures (phase 12) are not exercised under wasip1. Mitigation: the phase-17 wasip1 gate skips any fixture that imports import "C".
  4. Cassette drift. LLM cassettes (phase 13) are keyed by SHA-256 of provider:model:prompt; any change to the prompt invalidates the cassette. Mitigation: fixtures pin the exact prompt string and commit the cassette file alongside the .mochi source.
  5. Generic closures and the Go 1.18+ generic restriction. Go generics do not currently allow type parameters on methods. mochiruntime.Map[T, U](xs []T, f func(T) U) []U is a free function for this reason, not a method on a generic container. Phase 6 and 7.12 use this pattern.
  6. go build is not hermetic by default. A go build invocation reads $GOPATH, $GOCACHE, and the user's ~/.netrc. Driver.Build clears these via env := append(os.Environ(), "GOPATH=...", "GOCACHE=...") to a per-build cache so a hostile .netrc does not poison the build. The -trimpath flag handles the rest.
  7. go vet is best-effort. go vet can produce false positives on lowered code (e.g., when the lowerer synthesises a for _ = range x loop the vet "for x = range x" warning fires). Phase 0 ships a vet allowlist (MOCHI_GO_VET_ALLOW=...) so the gate can stay green while real bugs are still caught.

Acknowledgements

This MEP builds on MEP-45 (C transpiler) for the aotir IR and clower pipeline, on MEP-46 / MEP-47 / MEP-48 / MEP-49 / MEP-50 / MEP-51 / MEP-52 / MEP-53 / MEP-55 / MEP-56 for the multi-backend lowering pattern, on Go's stable GOOS=wasip1 target shipped in Go 1.21 (Aug 2023), on Go 1.22's reproducible-build defaults, on the Go module proxy and pkg.go.dev for the zero-friction publish story, on go/format for canonical formatting, on Go's M:N goroutine scheduler for native concurrency, and on the cgo machinery for first-class FFI without a glue language.