MEP 54. Mochi-to-Go transpiler
| Field | Value |
|---|---|
| MEP | 54 |
| Title | Mochi-to-Go transpiler |
| Author | Mochi core |
| Status | Active |
| Type | Standards Track |
| Created | 2026-05-29 15:50 (GMT+7) |
| Depends | MEP-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:
-
Go source via a structural
gotreeAST, rendered throughgo/format. The default emit path constructsgotree.SourceFiletrees, then callsgotree.Render(sf)which passes the printed tokens throughgo/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 runninggofmtwould produce. Thegotree.Renderstep is allowed to fail on syntactically-invalid trees, which catches lowerer bugs as compile errors beforego buildever runs. -
Native goroutines and channels. Mochi channels lower to native
chan T(make(chan T, cap));sendisch <- v;recvis<-ch. Agents lower to a goroutine wrapping afor msg := range chreceive loop. Streams lower to a struct holding[]chan Tsubscriber slots with bounded capacity for backpressure. Nosync.Mutexis 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. -
Reuse MEP-45's
aotirIR. 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/lowersaotirto Go-source structural nodes. Go-specific lowerings (e.g., closure environments lifted into a struct viaClosureEnvStmt, 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. -
Deterministic
go buildvia-trimpath -buildvcs=false+SOURCE_DATE_EPOCH. Identical inputs produce byte-identical outputs acrossDriver.Buildinvocations.-trimpathstrips the absolute filesystem path from the binary;-buildvcs=falsedisablesgit-derived stamping;SOURCE_DATE_EPOCH=0neutralises any remaining time-derived metadata. On macOS the gate is platform-skipped: the Mach-OLC_UUIDload command is randomised per link by Apple'sld64and cannot be controlled from go-side flags. The Linux and Windows gates are enforced. -
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 viago get dev.mochilang/runtime/go/collectionsfor 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:
-
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
DaemonSetsidecar, akubectlplugin, a Terraform provider, or an OTel receiver compiles into the same binary shape as every other tool in that ecosystem. -
First-class cross-compile.
GOOS=linux GOARCH=arm64 go buildruns 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 needscargo-zigbuildor 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). -
go installand pkg.go.dev.go install dev.mochilang/runtime/go/cmd/...@latestis 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 atproxy.golang.org). Versioning is tag-driven (v1.2.3). Publication isgit tag+git push; the proxy fetches the module the next time anyone references it. -
WebAssembly: both browser and wasip1. Go has shipped first-class
GOOS=js GOARCH=wasmsince 1.11 (Aug 2018) andGOOS=wasip1 GOARCH=wasmsince 1.21 (Aug 2023). The browser target produces a.wasmthat runs under any modern browser via the bundledwasm_exec.jsglue; the wasip1 target produces a.wasmthat runs under wasmtime, wasmer, or WasmEdge with no extra glue. Mochi-on-Go reaches both deployment surfaces from the same source. -
Goroutines and channels: the agent / stream story is native. Mochi agents and channels map onto Go's
gostatement andchan Twith zero translation tax. No userland scheduler, noRc<RefCell<...>>single-thread compromise (MEP-53), nosync.Mutex-wrapped queues, noasyncio.Queue(MEP-51), noPromise-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. -
Reproducible builds out of the box.
go build -trimpath -buildvcs=falseplusSOURCE_DATE_EPOCH=0produces 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. -
The shortest binary-out-of-checkout story.
git clone && go buildproduces a single statically linked binary with no extra steps on every platform Go supports. No./configure, nocmake, nomeson, nocargo 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. -
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 singlecgo-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:
$MOCHI_GO(env override)$GOROOT/bin/go(ifGOROOTis set)~/sdk/go1.26.0/bin/go(thego install golang.org/dl/go1.26.0@latest && go1.26.0 downloadconvention)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 construct | Go lowering |
|---|---|
let x = e | x := e |
var x = e | var x T = e (typed when the lowerer knows T) |
if/elif/else | if 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 => body | f := 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 | B | type 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 expr | expr (immediate evaluation; the colour pass decides where async boundaries become go statements) |
await fut | fut (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 / continue | break / continue |
from x in xs where p select e | desugared 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 t | sort.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 divide | mochiruntime.DivI64(a, b) (panics with code 5 on zero) |
a % b integer modulo | mochiruntime.ModI64(a, b) |
xs[i] indexing | bounds-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, genericOMap[K, V])query— query-DSL helpers (joins, group-by, top-K via heap-backedcontainer/heap,arena_queryarena-allocated result slice)agent— agent supervisor (Spawn,Stop, supervised goroutine restart)stream— bounded broadcast stream with per-subscriberchan Tslots and explicit backpressuredatalog— semi-naive fixpoint helpers (most Datalog evaluation is compile-time; this package handles the runtime fallback)option/result— sum-type helpers forOption<T>andResult<T, E>loweringstringz— UTF-8-aware string helpers (Substring,ReverseString,RuneAt)timez— deterministic time helpers (Nowis overridable viaMOCHI_NOW_NS)llm— cassette replay; provider plugins underdev.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
| Target | Switch case | Output layout |
|---|---|---|
TargetGoBinaryLinuxAmd64 | go-linux-amd64 | <out>/<name> (GOOS=linux GOARCH=amd64) |
TargetGoBinaryLinuxArm64 | go-linux-arm64 | <out>/<name> (GOOS=linux GOARCH=arm64) |
TargetGoBinaryDarwinAmd64 | go-darwin-amd64 | <out>/<name> (GOOS=darwin GOARCH=amd64) |
TargetGoBinaryDarwinArm64 | go-darwin-arm64 | <out>/<name> (GOOS=darwin GOARCH=arm64) |
TargetGoBinaryWindowsAmd64 | go-windows-amd64 | <out>/<name>.exe (GOOS=windows GOARCH=amd64) |
TargetGoBinaryFreeBSDAmd64 | go-freebsd-amd64 | <out>/<name> (GOOS=freebsd GOARCH=amd64) |
TargetGoModule | go-module | <out>/go.mod + <out>/main.go + <out>/vendor/... (no go build invocation; for publish-ready inspection) |
TargetGoWasmJS | go-wasm-js | <out>/<name>.wasm + <out>/wasm_exec.js (GOOS=js GOARCH=wasm) |
TargetGoWasiP1 | go-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 scope | Go 1.26 ubuntu-24.04 | Go 1.26 macos-15 | Go 1.26 windows-2025 | wasm/js (browser) | wasip1 (wasmtime) |
|---|---|---|---|---|---|
| Scalars / arithmetic | LANDED | LANDED | required (16.x) | required (17) | required (17) |
| Lists / maps / sets | LANDED | LANDED | required | required | required |
| Records / sums | LANDED | LANDED | required | required | required |
| Closures | LANDED | LANDED | required | required | required |
| Queries (filter, map, order, joins) | LANDED | LANDED | required | required | required |
| Datalog | NOT STARTED (phase 8) | NOT STARTED | NOT STARTED | NOT STARTED | NOT STARTED |
Channels (chan T) | LANDED (9.1) | LANDED | required | required | required |
| Streams | NOT STARTED (phase 9.2) | NOT STARTED | NOT STARTED | NOT STARTED | NOT STARTED |
| Agents (goroutine + chan) | NOT STARTED (phase 10) | NOT STARTED | NOT STARTED | NOT STARTED | NOT STARTED |
| Async / await | NOT STARTED (phase 11) | NOT STARTED | NOT STARTED | NOT STARTED | NOT STARTED |
| FFI (cgo) | NOT STARTED (phase 12) | NOT STARTED | NOT STARTED | n/a (no cgo on wasm) | n/a |
| LLM (cassette) | NOT STARTED (phase 13) | NOT STARTED | NOT STARTED | NOT STARTED | NOT STARTED |
| Fetch + JSON | NOT STARTED (phase 14) | NOT STARTED | NOT STARTED | n/a (no net/http on browser wasm without fetch shim) | required |
| Try / catch / panic | LANDED (7.10) | LANDED | required | required | required |
| go-module packaging | NOT STARTED (phase 15) | NOT STARTED | NOT STARTED | NOT STARTED | NOT STARTED |
| Reproducibility | NOT STARTED (phase 16, Linux + Windows gated; darwin LC_UUID skipped) | NOT STARTED | NOT STARTED | NOT STARTED | NOT STARTED |
| wasm/js + wasip1 | NOT STARTED (phase 17) | NOT STARTED | n/a | NOT STARTED | NOT STARTED |
| Publish | NOT STARTED (phase 18, pkg.go.dev metadata + signed tag) | NOT STARTED | NOT STARTED | NOT STARTED | NOT 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
- Emit via
go/ast(the standard-library AST) instead of an in-treegotree. Rejected:go/astis the AST thatgo/parserproduces, not an AST designed for ergonomic synthesis from a non-Go program. Constructing a validgo/ast.Filefrom scratch requires populating dozens of position fields (token.Pos) for every node sogo/printerproduces sensible output. The structuralgotreeis shaped for synthesis (notoken.Posfield on any node; positions are reconstructed by the renderer) and is shared in concept with the Rustrtree(MEP-53) and Rubyrtree(MEP-56). - Use
sync.Mutex+ condvars for channels. Rejected: Go's nativechan Talready 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. - 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. - Use
encoding/jsonforjson_decode. Accepted with a wrapper:encoding/jsonis stdlib and handles all JSON shapes correctly. The wrapper inmochiruntime.JsonDecodereturnsmap[string]anyrather thaninterface{}to match vm3's any-typed shape, and panics with code 97 on malformed input. - Use
net/httpforhttpGet. Accepted with a wrapper:net/httpis stdlib, supports TLS (unlike the Rust target'smochi_runtime::fetch::getwhich is plain TCP only), and supportsTransfer-Encoding: chunkedtransparently. The wrapper inmochiruntime.HttpGetcollapses the request/response/body lifecycle into a single call. - Use
crypto/sha256for the LLM cassette key. Accepted: stdlib, well-audited, fast enough that no inlined hash is needed. - Bundle the runtime as a single inlined file per emission instead of an external module. Rejected: makes generated code untraceable under
go docand breaksgo install. Shippingdev.mochilang/runtime/goas a real Go module lets usersgo get dev.mochilang/runtime/go/collectionsfrom any Go program and lets the LLM cassette / fetch / panic packages be tested in isolation. - 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. TheClosureEnvStmtpattern explicitly snapshots the captured variables into a per-closure struct at the point of closure creation, matching Mochi's by-value capture semantics.
Risks
- macOS LC_UUID non-determinism. The Mach-O
LC_UUIDload command is randomised per link by Apple'sld64. 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. - Browser wasm has no
net/httpserver.GOOS=js GOARCH=wasmcannot run an HTTP server (nonet.Listen); the client side works via the browser'sfetchAPI but requires thewasm_exec.jsglue'sXHRshim. Phase 17 fixtures targetingwasm-jsexcludehttpGet; fetch coverage on browser wasm waits until aMOCHI_FETCH_GLUEshim ships. - wasip1 cgo gap.
GOOS=wasip1 GOARCH=wasmdoes not support cgo. FFI fixtures (phase 12) are not exercised under wasip1. Mitigation: the phase-17 wasip1 gate skips any fixture that importsimport "C". - 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.mochisource. - 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) []Uis a free function for this reason, not a method on a generic container. Phase 6 and 7.12 use this pattern. go buildis not hermetic by default. Ago buildinvocation reads$GOPATH,$GOCACHE, and the user's~/.netrc.Driver.Buildclears these viaenv := append(os.Environ(), "GOPATH=...", "GOCACHE=...")to a per-build cache so a hostile.netrcdoes not poison the build. The-trimpathflag handles the rest.go vetis best-effort.go vetcan produce false positives on lowered code (e.g., when the lowerer synthesises afor _ = range xloop 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.