12. Risks and alternatives
This note enumerates the risks identified during the MEP-54 design pass and the alternatives that were considered and rejected.
Risk register
R1: macOS LC_UUID non-determinism
Symptom: Two Driver.Build invocations on macOS produce binaries that differ in exactly the LC_UUID load command (16 bytes).
Cause: Apple's ld64 linker generates a random UUID per link. The UUID is used by Crash Reporter to correlate crash logs with debug symbols. There is no rustc-equivalent -no-uuid flag for Go on darwin.
Mitigation: Phase 16 platform-skips the macOS reproducibility gate. The Linux and Windows gates are enforced. Users who need bit-reproducible darwin binaries can run a post-link patcher that zeroes the LC_UUID payload (ldid -S works, but is itself a third-party tool).
R2: Browser wasm has no net/http server
Symptom: A Mochi program that uses httpGet works on Linux but produces a runtime "operation not supported" error on GOOS=js GOARCH=wasm.
Cause: The browser's wasm runtime does not provide a net.Listen or net.Dial capability. Go's net/http client falls back to the browser's fetch API via a shim in wasm_exec.js, but only for HTTPS to same-origin or CORS-allowed hosts.
Mitigation: Phase 17 fixtures targeting wasm-js exclude httpGet. Documentation flags the restriction. A future sub-phase may ship a MOCHI_FETCH_GLUE shim that wires httpGet to the browser's fetch directly, but that requires JS-side cooperation we do not have for the default test harness.
R3: wasip1 has no cgo
Symptom: A Mochi program that uses import "C" fails to compile under GOOS=wasip1 GOARCH=wasm.
Cause: wasip1 has no syscall surface for cgo; the Go toolchain rejects cgo for that target tuple.
Mitigation: Phase 12 fixtures detect cgo usage at the lower stage and skip when the target is wasip1. The skip is at fixture-discovery time, not build-failure time.
R4: LLM cassette drift
Symptom: A fixture using generate openai { prompt: "..." } passes when the cassette was first recorded but fails after a prompt edit because the SHA-256 key changed.
Cause: The cassette is keyed by sha256(provider + ":" + model + ":" + prompt). Any character change in the prompt produces a different key, so the cassette lookup misses and the runtime helper returns the empty string.
Mitigation: Fixtures pin the exact prompt string and commit the cassette file alongside the .mochi source. The Phase 13 documentation flags the brittleness. Re-recording requires MOCHI_RECORD_CASSETTES=1 env var so accidental re-records are prevented.
R5: Generic-method gap (Go 1.21)
Symptom: A runtime helper that we would naturally write as a method on a generic type (e.g., func (s *Stream[T]) Map[U](f func(T) U) *Stream[U]) cannot be expressed because Go 1.21 does not allow generic methods.
Cause: Go's generics design intentionally excludes generic methods to preserve interface compatibility. The Go team has signalled openness to adding them in a future version but it has not landed as of 2026-05.
Mitigation: Every generic helper is a free function (func Map[T, U any](s *Stream[T], f func(T) U) *Stream[U]). This works but produces less idiomatic call sites (Map(stream, f) instead of stream.Map(f)). The lowerer adapts by always emitting the free-function form.
R6: go build hermeticity gap
Symptom: Driver.Build reads $HOME/.netrc (for proxy auth) and $GIT_TERMINAL_PROMPT (for prompt suppression) from the user's environment. A hostile .netrc could exfiltrate the build to a malicious proxy.
Cause: go build invokes git fetch internally to resolve module dependencies; git reads .netrc for HTTP auth.
Mitigation: Vendor mode is on by default; in vendor mode go build does not invoke git fetch and does not read .netrc. Proxy mode is opt-in via Driver.NoVendor=true and the user accepts the auth-config risk in that mode. The driver also clears GOPATH and GOCACHE to per-build directories so a hostile module cache cannot poison the build.
R7: go vet false positives on lowered code
Symptom: A lowered function triggers a go vet warning that is not actually a bug (e.g., "self-assignment" on x = x synthesised by a sum-type variant unpack).
Cause: Go vet does not understand the lowering context; it reasons about the emitted source as if it were hand-written.
Mitigation: The lowerer avoids known-triggering patterns. When a true positive is impossible to avoid (e.g., the synthesised _ = m after an agent message handler unpacks all fields), the harness's MOCHI_GO_VET_ALLOW env var skips specific warnings without disabling vet entirely.
R8: Channel capacity overflow
Symptom: make_chan(N) with a very large N allocates more memory than expected at the runtime helper layer.
Cause: Go's make(chan T, cap) is O(1) in capacity (it just records the cap), but if the capacity is INT_MAX and the channel fills, the buffer holds INT_MAX elements of T, which can exhaust memory.
Mitigation: The lowerer caps channel capacity at 1 << 24 (16M elements); larger requests are rejected at lower time with a clear error. This is a soft limit that can be overridden via MOCHI_MAX_CHAN_CAP env var.
R9: Cross-module name collisions
Symptom: A Mochi program imports two modules that both export a type named Foo; the Go lowering produces two type Foo declarations in the same package.
Cause: Mochi's namespace model allows the imports to live in distinct module namespaces; Go's package model flattens everything into one namespace.
Mitigation: The lowerer mangles colliding names with a module-derived prefix (module1_Foo, module2_Foo). Phase 4 wires this. The mangling is deterministic so re-builds produce the same names.
R10: Cassette directory not set
Symptom: generate openai { ... } returns the empty string with no clear indication why.
Cause: MOCHI_LLM_CASSETTE_DIR env var is unset, so the cassette lookup short-circuits to the empty-string fallback.
Mitigation: The runtime helper writes a one-line stderr diagnostic on the first cassette-miss per process: mochi: LLM cassette dir not set ($MOCHI_LLM_CASSETTE_DIR); returning empty string. The diagnostic is suppressed after the first emit so chatty fixtures do not flood the log.
Alternatives considered
A1: Use go/ast for code generation
Why considered: Stdlib AST, no separate maintenance burden.
Why rejected: go/ast is designed for parsing existing Go, not for synthesising new Go. Every node has a token.Pos field that must be populated for go/printer to produce sensible output. The structural gotree is shaped for synthesis (no position fields; positions are reconstructed by the renderer). See codegen-design and prior-art-transpilers.
A2: Use text/template for code generation
Why considered: Simpler than a structural AST; widely used by protoc-gen-go, sqlc, stringer.
Why rejected: Template-based emitters accumulate whitespace bugs. The "fix one bug, break two others" failure mode is well-documented in the kubebuilder issue tracker. Structural AST avoids the class entirely.
A3: Use sync.Mutex-wrapped queues for channels
Why considered: Direct port of the C target's lowering.
Why rejected: Go's native chan T is exactly the right primitive. Wrapping it would add a synchronisation cost the source language does not require.
A4: Use TinyGo as the default backend
Why considered: TinyGo produces smaller wasm binaries (5-50x smaller than gc) and supports more embedded targets.
Why rejected: TinyGo does not support the full Go stdlib. Notable gaps: reflect (used by mochiruntime.AnyEqual), encoding/json (limited), net/http (limited). TinyGo is a Phase 17.x sub-target for users who need it; not the default.
A5: Inline the runtime per-emission
Why considered: No external dep, single-file builds work without go.mod.
Why rejected: Makes generated code untraceable under go doc. Breaks go install for users who want to consume Mochi-emitted Go programs the standard way. Inflates the per-fixture diff because every emit ships a copy of the runtime.
A6: Skip the published runtime module
Why considered: Simpler distribution (no pkg.go.dev story).
Why rejected: Same reasons as A5. Publication is one git tag per release; the cost is negligible and the discoverability gain is large.
A7: Use a green-thread library for agents
Why considered: Could give finer-grained control over scheduling than goroutines.
Why rejected: Goroutines are already cooperatively scheduled by the Go runtime with M:N to OS threads. A green-thread library would duplicate that work without adding capability. We use errgroup-style supervision internally as an implementation detail in the agent runtime, but the lowering pattern is plain go statements.
A8: Use go/printer directly without re-formatting
Why considered: Skips one parse-print pass.
Why rejected: go/printer is finicky about leading comments, trailing semicolons in single-line composite literals, and tab vs space rendering. go/format.Source (the function gofmt uses) handles all of these by re-parsing and re-printing. The cost is one extra parse; the win is no whitespace bugs.
A9: Lower to native Go generics throughout
Why considered: Smaller emitted code (one generic function instead of one per instantiation).
Why rejected: Monomorphisation runs in clower (shared with C / Rust / etc.); duplicating that pass for Go alone would diverge the cross-target semantics. Generic helpers in the runtime module (mochiruntime.Map[T, U]) are an exception: they are runtime helpers, not lowered code, so they can be generic without affecting the cross-target shape.
A10: Skip the deterministic build flags by default
Why considered: Driver.Deterministic=true is opt-in; default builds produce timestamps and build IDs.
Why rejected: Reproducibility is on a per-call basis: Driver.Build callers who want it set Deterministic=true, callers who want a normal dev build do not. This is the same shape as MEP-53. Always-on determinism would surprise users debugging via go tool objdump (the stripped symbol table makes objdump less useful).
Future candidates
Risks marked deferred-mitigation become candidates for future sub-phases:
- F1: Browser wasm
fetchshim (Phase 17.x). WirehttpGetto the browser'sfetchAPI. - F2: Darwin LC_UUID post-link patcher (Phase 16.x). Bundle an
ldid -S-equivalent in the driver so darwin reproducibility lights up. - F3: TinyGo sub-target for embedded wasm (Phase 17.x). Gate the source surface that does not use reflect or
encoding/json. - F4: Generic method support (depends on upstream Go). When generic methods land in Go, the runtime helpers move to method form for ergonomics.
- F5: Sandboxed
go build(Phase 16.x). Rungo buildinside a Linux user namespace so the hermetic-build gap (R6) is closed structurally rather than by convention.