MEP 74. Mochi and Go package bridge
| Field | Value |
|---|---|
| MEP | 74 |
| Title | Mochi and Go package bridge |
| Author | Mochi core |
| Status | Draft |
| Type | Standards Track |
| Created | 2026-05-29 20:25 (GMT+7) |
| Depends | MEP-1 (Grammar, for the import go extension), MEP-2 (AST, for the import node), MEP-4 (Type System), MEP-13 (ADTs and Match, for error → Result desugar), MEP-45 (C transpiler, for the FFI sidecar pattern), MEP-54 (Go transpiler, for the lowering pipeline, the runtime module, the existing TargetGoModule target, and the existing import go "..." FFI surface), MEP-57 (Mochi module and package system, for mochi.toml, mochi.lock, content-addressed object store, capability declaration model, and Sigstore-keyless trusted publishing) |
| Research | /docs/research/0074/ |
| Tracking | /docs/implementation/0074/ |
Abstract
Mochi today (May 2026, immediately after MEP-54's Go target landed at 18 phases and MEP-57's source-level package system reached Draft) has nine code-emitting targets and the beginnings of a source-level package management story. MEP-54 phase 10 wired a minimal import go "<package>" FFI surface that the Go target understands (the import becomes a Go import statement with FFI mangling stripped, then linked through the host Go toolchain), but it does not version-pin the imported module, does not record a checksum, does not gate on a capability declaration, and does not run on any target other than Go. The Mochi-to-Go path emits a package main executable that ships the runtime module dev.mochilang/runtime/go as its only Go dependency at module pin time; user-written Mochi programs cannot pull in arbitrary modules from pkg.go.dev with a version constraint, and a Mochi package author cannot publish their work as a Go module that downstream Go users go get. The 1.5-million-plus modules indexed at pkg.go.dev (April 2026 snapshot from the pkg.go.dev public stats endpoint) remain semi-off-limits to Mochi authors, and the Go ecosystem cannot consume Mochi packages directly.
MEP-74 specifies the bidirectional Go module bridge: Mochi packages can consume any well-formed Go module via import go "<module>@<semver>" as <alias> with no user-written FFI boilerplate beyond what MEP-54 phase 10 already requires, with sum.golang.org checksum-DB verification on the consume path, and with a per-module capability declaration. Mochi packages can publish to the Go module ecosystem via mochi pkg publish --to=go+git+<repo-url>@<tag> (the Go ecosystem has no central registry; publication is a git-tag operation that the module proxy at proxy.golang.org picks up on next fetch, optionally accompanied by a Sigstore cosign signature on a sibling <tag>.sig tag per the experimental gosum-cosign workflow draft of 2026-Q1). The bridge is the fifth source-language interop story Mochi ships (after the Go FFI of import go "..." at MEP-54 phase 10, the Python FFI of import python "...", the TypeScript / Deno FFI of import typescript "...", and the MEP-73 Rust bridge), the second one that targets a typed semver-versioned package ecosystem as both a consumer and a producer (after MEP-73 Rust), and the first one where the source-language toolchain itself ships a stable, machine-readable, in-stdlib API surface (go/packages plus go/types) so the ingest pipeline does not require a nightly toolchain.
The proposal builds on MEP-57's manifest / lockfile / capability infrastructure, MEP-54's emit pipeline, and MEP-73's bridge philosophy, and adds a new self-contained component under package3/go/. The Mochi grammar gains semver-pinned forms for the existing go <lang> token in the FFI-import production (no new keywords). The Mochi build pipeline gains one new target (TargetGoLibrary in MEP-54's build driver, parallel to the existing TargetGoModule which only writes the source tree). The lockfile gains one new repeated table ([[go-package]]). No existing transpiler MEP needs structural change; MEP-54 phase 10's existing FFI mangling pass is extended (not replaced) to honour the new lockfile pin.
The system is anchored on eight load-bearing decisions, each justified in §Rationale and surveyed in the companion research notes:
-
go/packages+go/typesfrom the Go standard library as the canonical machine-readable Go module surface. The Go toolchain shipsgo/packages.Load(undergolang.org/x/tools/go/packages) which returns a full[]*packages.Packagetree where each*packages.Packagecarries a*types.Packageexposing every exported identifier with its fulltypes.Type(named types, struct fields withtypes.Var, methods withtypes.Func, interfaces with full method sets, signatures with parameter and result tuples, type parameters since Go 1.18). This is the same surface thatgopls,staticcheck, andgolangci-lintconsume; it has been stable since Go 1.11 (August 2018) with a documented compatibility promise. The bridge ingests this surface via a small Go-side helper binary (package3/go/cmd/govet/go-ingest) that emits a JSON document describing every public item, which the main Mochi binary (written in Go itself) parses. Parsing Go source directly is rejected (would duplicatego/parser+go/typesfor no gain). Readingpkg.go.dev's HTML documentation is rejected (it is rendered, not normative). Usinggomobile bind's.aar/.frameworkoutput is rejected (it only supports a tiny subset of Go and targets mobile-only). See 01-language-surface §2, 04-go-doc-ast-ingest §1, 02-design-philosophy §1. -
Auto-synthesised cgo wrapper Go package (
go_wrap/) per imported Go dep, not direct cgo invocation or directdlopenof pre-built shared libraries. For each Go module the user imports, the bridge generates a sibling Go package (<workdir>/go_wrap/<module-flat-name>/) that imports the source module and exposes a flat//exportsurface wrapping each translatable public item: scalar return types pass through directly,stringbecomes*C.charplus an explicitmochi_go_<module>_string_freesymbol (the matchingC.freeon the Go-allocated string requiresC.GoStringthenC.CStringround-trip to escape Go's GC),[]Tbecomes a(ptr, len, cap)triple plus a_freesymbol on the C side,map[K]Vbecomes an opaque*C.MochiMaphandle with explicit_get/_set/_iter/_freesymbols (Go maps cannot be passed across the cgo boundary safely),errorbecomes a pair of out-pointers plus amochi_statusreturn code (matching the MEP-73Result<T, E>shape),chan Tbecomes a*C.MochiChannelhandle with_send/_recv/_closesymbols, andfunctypes become*C.MochiFunchandles with a_callsymbol that takes a packed argument slice. The wrapper package is built as ac-archiveviago build -buildmode=c-archive -o libwrap.a(which has been a first-class Go build mode since Go 1.5 in 2015 and which the MEP-54 Phase 15 work already exercised for thego-moduletarget) and linked into the final binary by the MEP-54 driver's existing link-step path. This indirection accepts the wrapper-codegen cost in exchange for a clean Mochi-side ABI (no need to teach Mochi's lowering pass about Go's GC, its method-set rules, or its interface satisfaction), a single static link surface for the whole dependency graph (no per-moduleplugin.Openpath; Go'spluginbuild mode is linux-only and brittle), and a per-wrapper SHA-256 lockfile pin that catches silent go/packages drift. The alternative of having Mochi emit Go code directly that calls the source module without an intermediate wrapper is rejected because Mochi'saotirIR is target-agnostic by design; teaching it about Go's interface satisfaction would break the multi-backend invariant. See 02-design-philosophy §2, 03-prior-art-bridges §3, 09-abi-stability §1. -
Closed Go-to-Mochi type translation table, with explicit refusal on out-of-table cases. The translation covers
int↔int,int32↔int(with overflow guard at the FFI boundary),int64↔int,uint↔int(with positive-range guard),float32↔float,float64↔float,bool↔bool,string↔string,[]byte↔bytes,[]T↔list<T>(when T is in-table),map[K]V↔map<K, V>(when K isstringor integer and V is in-table; map iteration order is non-deterministic on the Go side and the wrapper documents this),*T↔T?(a nullable handle for pointer types where T is a named struct or named primitive), named struct typestype S struct { A int; B string }↔record S { A: int, B: string }(only fully-exported fields; mixed-export structs project the exported subset and skip the rest with a SkipReport entry), named interface typestype I interface { M() }↔ extern type I(opaque handle with method-call thunks),error↔ try-catch desugar(matches MEP-73Result<T, E>shape),chan T↔stream<T>(with explicit_closesemantics on the wrapper side),func(A, B) (C, error)↔ fun(A, B) -> Result<C>(signature-mapped), and the...Tvariadic suffix asvarargs<T>. Items outside the table (generic type parameters beyond the monomorphised concretisations declared inmochi.toml's[go.monomorphise]section,unsafe.Pointer,reflect.Value,reflect.Type,cgo.Handle, channel directions on type-aliases (chan<- Tvs<-chan T), unexported types in exported positions,interface{}/anyin non-constrained positions, struct fields withtagswhose semantics depend on a tag-consuming downstream library, and any item whose visibility crosses aninternal/directory boundary) are skipped with aSkipReportentry that names the item and the reason. The user can override with a hand-writtenextern fndeclaration that takes responsibility for the type at the FFI boundary. Aggressive translation (e.g., synthesising a Mochi wrapper for everyinterface{}return) is rejected: the bridge promises zero boilerplate, not full generality. See 05-type-mapping §1, 02-design-philosophy §3. -
import go "<module-path>@<semver>" as <alias>as the surface, with<semver>resolved throughmochi.lock, extending MEP-54 phase 10's existing keyword. The grammar'sgo<lang>token already exists from MEP-54 phase 10. MEP-74 extends the existing<spec>production to admit:<module-path>: bare module path (e.g.,github.com/spf13/cobra), resolves through the manifest constraint plusmochi.lock.<module-path>@<semver-req>: explicit version constraint (v1.8.0,^v1.8,~v1.8.0,>=v1.0.0 <v2.0.0,latest).<module-path>@<pseudo-version>: Go pseudo-version likev0.0.0-20240101120000-abcdef123456(UTC YYYYMMDDhhmmss + git-rev short hash; the Go module proxy mints these for any git commit without a semver tag).<module-path>@git+<url>#<rev>: git source override (escape hatch for forks).<module-path>@path+<relative-path>: path source for replace-directive-like local development.
Bare names resolve to the highest version on the module proxy that satisfies the manifest's
[go-dependencies]constraint (the bridge auto-rewrites the import to match the resolved version on nextmochi pkg lock). Themochi.locklockfile records the resolved version, BLAKE3-256 + the Goh1:hash (base64-of-sha256-of-zip-content; identical to what sum.golang.org publishes), thego/packagesrevision hash, the SHA-256 of the generated wrapper package's source, and the capability surface declared for the module. No other Mochi syntax changes are introduced;import goparticipates in the same module-resolution flow asimport rust/import python/import typescript. See 01-language-surface §1, 06-go-module-publish-flow §2. -
Mochi → Go library module emission via a new
TargetGoLibraryMEP-54 target, parallel to the existingTargetGoModule. MEP-54 today exposesTargetGoBinaryLinuxAmd64,TargetGoBinaryLinuxArm64,TargetGoBinaryDarwinAmd64,TargetGoBinaryDarwinArm64,TargetGoBinaryWindowsAmd64,TargetGoBinaryFreeBSDAmd64,TargetGoModule(emit-the-source-tree only, no build),TargetGoWasmJS, andTargetGoWasiP1. MEP-74 addsTargetGoLibrary. WhereTargetGoModulewrites apackage mainwith afunc main(), the library path emits apackage <name>withfunc,type,const, andvardeclarations mirroring the Mochi package's public surface; setsmodule <vanity-import-path>ingo.mod; runs the bridge's cbindgen-equivalent (a built-in_cgo_export.hemitter; cgo's standard tooling already does this when-buildmode=c-archiveis used, but the library target also emits a plain Go header for non-Go non-cgo consumers); populates the publish-side metadata (thedoc.gowith the package summary, theREADME.mdat the repo root, theLICENSEfile, the// Package foo provides ...first-sentence rule godoc enforces) from the Mochi package'smochi.toml; and emits ago.sumpinning every transitive dependency. The driver'sBuildfunction gates the library path onDriver.LibraryMode=trueplustarget == TargetGoLibrary. See 01-language-surface §3, 06-go-module-publish-flow §1. -
Git-tag-based publish to a user-owned git repo, with the module proxy and sum.golang.org picking it up automatically, plus an optional Sigstore cosign signature on a sibling
<tag>.sigtag. Go has no central package registry analogous to crates.io, npm, or PyPI. Module publication is a git operation: tag a commit with a semver-compatible tag (e.g.,v1.2.3) and push the tag to the canonical-import-path repo. The next time a Go usergo gets the module, the module proxy atproxy.golang.orgfetches the new tag, computes theh1:hash, submits it to the Go checksum database (sum.golang.org, a Merkle-tree transparency log operated by the Go team), and serves the result. There is no upload endpoint. MEP-74's publish flow:- Builds the library module via
TargetGoLibrary. - Validates
go vet ./...andgofmt -l ./...produce zero diagnostics. - Runs
go build ./...against the emitted module to confirm it compiles. - Tags the canonical-import-path repo at HEAD with the requested semver tag.
- Optionally constructs a Sigstore cosign signature over the git tag's commit SHA via the OIDC token from the CI environment, attaches the signature blob as a sibling git tag
<tag>.sig(the gosum-cosign workflow draft of 2026-Q1; the Go team has not committed to a canonical signing format yet, but cosign-over-git-tag is the front-runner). - Pushes the tag (and optionally
<tag>.sig) to the canonical-import-path remote. - Optionally pings
proxy.golang.org/<module-path>/@v/<tag>.infoto warm the module proxy's cache.
The
mochi pkg publish --to=go+git+<repo-url>@<tag>CLI orchestrates this. The legacy upload-to-a-private-registry path (e.g., Athens, JFrog Go) is supported via--to=go+goproxy+<url>which uploads a.zipto a private GOPROXY-compatible server. There is noGO_PROXY_TOKENanalogue; authentication to the publish endpoint (git over SSH, or HTTPS-with-PAT) is the user's existing git authentication. See 06-go-module-publish-flow §1, 07-sigstore-go-checksumdb §1. - Builds the library module via
-
Goroutine bridge that exposes Go's first-class concurrency (
gostatement,chan,select) to Mochi without a runtime singleton. Unlike the Rust bridge's tokio singleton problem, Go's runtime ships with the goroutine scheduler built in: a Go function called from Mochi via cgo runs on a Go runtime thread (cgo callbacks acquire a per-call goroutine), and the function can spawn its own goroutines with thegostatement freely. The bridge has no global runtime to construct. The bridge does need to map Mochi'sspawn <expr>to the wrapper'sgo <expr-shim>()and Mochi'sstream<T>to the wrapper'schan T; these mappings happen at wrapper-synthesis time, with the wrapper holding async.Mapof active channels keyed by opaque handle ID. The wrapper exposes per-channel_send,_recv, and_closecgo-exported functions. The runtime cost of cgo-calling into Go is ~200ns per call (measured on darwin-arm64, Go 1.23, May 2026) plus the goroutine context-switch cost if the called function blocks. This is materially higher than Mochi-native function calls; programs that hot-loop across the cgo boundary pay this cost per iteration. See 08-goroutine-bridge §1, 02-design-philosophy §4. -
sum.golang.org checksum-DB integration as a hard prerequisite of every lock operation; private-module
GONOSUMCHECKis opt-in per-module only. The Go ecosystem ships an unusually strong supply-chain story by 2026 standards: the module proxy atproxy.golang.organd the checksum database atsum.golang.orgare jointly operated by the Go team, and everygo getof a public module since Go 1.13 (September 2019) is verified against the checksum DB by default. The checksum DB is a Merkle-tree transparency log: every published module version is appended, and the log's signed tree head is anchored in the public timeline. MEP-74 requires that every[[go-package]]lock entry record both the BLAKE3-256 of the .zip module artefact (the bridge's authoritative hash) AND theh1:hash sum.golang.org publishes (cross-checked against the public DB at lock time). The cross-check is a hard error on mismatch (theh1:differs from the artefact the bridge downloaded → the proxy is lying or the upstream module was rewritten → the lock fails until the user explicitly accepts the new hash). The Go-private-module workflow (where the module path is not publicly resolvable; e.g., a corporatecorp.example.com/internal/foo) opts out via[go.private] modules = ["corp.example.com/**"]inmochi.toml, mirroring theGOPRIVATE/GONOSUMCHECKGo-side environment variables. See 07-sigstore-go-checksumdb §1, 02-design-philosophy §5.
The gate for each delivery phase is empirical: the bridge must successfully ingest a curated 24-module fixture corpus (drawn from the April 2026 top-25-most-imported-on-pkg.go.dev snapshot: github.com/stretchr/testify, github.com/spf13/cobra, github.com/spf13/viper, github.com/sirupsen/logrus, github.com/google/uuid, github.com/gorilla/mux, github.com/pkg/errors, github.com/google/go-cmp, github.com/golang/protobuf, github.com/json-iterator/go, github.com/davecgh/go-spew, github.com/golang/mock, github.com/cespare/xxhash/v2, github.com/mattn/go-isatty, github.com/klauspost/compress, github.com/prometheus/client_golang, go.uber.org/zap, gopkg.in/yaml.v3, github.com/spf13/pflag, github.com/gin-gonic/gin, github.com/labstack/echo/v4, github.com/valyala/fasthttp, github.com/jmoiron/sqlx, github.com/fatih/color); generate a translatable surface for every public item the closed type-table covers; emit a SkipReport for every item out of table; produce a Mochi extern fn corpus that parses cleanly; and build the resulting wrapper-plus-Mochi binary via MEP-54's existing go build path with zero additional flags. A separate publish gate exercises the TargetGoLibrary path against an in-tree mock git remote and a mock GOPROXY (the same goproxy-mock harness that MEP-57 phase 11 introduced), asserts that the published module go gets cleanly into a synthetic downstream consumer, and asserts that the optional cosign signature verifies against the Sigstore root of trust.
Motivation
Mochi today (May 2026) integrates with foreign ecosystems through four FFI surfaces: Go (via MEP-54 phase 10's import go "<package>" flow, name-mangled and linked through the host Go toolchain, but with no version pinning), Python (via import python "<module>"), TypeScript / Deno (via import typescript "<spec>"), and Rust (via MEP-73's import rust "<crate>@<semver>", with full version-pinning + capability + sumdb-equivalent). The Go surface is the oldest and the least developed of the four despite Go being Mochi's primary host-language target: MEP-54 phase 10 admits any import go "<package>" and trusts whatever is in the user's GOPATH or GOROOT. There is no manifest entry, no lockfile entry, no version constraint, no checksum verification, no capability declaration, no publish path. The MEP-74 motivation is to close that gap and bring the Go bridge to parity with MEP-73 Rust:
-
The Go module ecosystem is the canonical 2024-2026 destination for backend services, cloud infrastructure tooling, CLI applications, CI tooling, observability stacks, and the broad Kubernetes / container-orchestration / cloud-native world. pkg.go.dev indexes 1.5M+ modules (April 2026 snapshot); the top-1000 most-imported modules account for 90%+ of the Go imports observed across the public pkg.go.dev backend (Q1 2026 internal stats). A Mochi program needing JSON Schema validation (
xeipuuv/gojsonschema), modern HTTP routing (gin-gonic/gin,labstack/echo,valyala/fasthttp), zero-allocation logging (uber-go/zap), cobra-based CLI scaffolding (spf13/cobra), or proto3 serialisation (golang/protobuf,protocolbuffers/protobuf-go) has only a name-resolution-best-effort path today. -
The Go ecosystem expects publishable Go modules as the unit of distribution; Mochi must learn to emit one. When a Mochi package author writes a useful library, the natural distribution channel for a Go-using audience is a tagged commit on a public git repo with a proper
go.mod. MEP-54 phase 15 (thego-moduletarget) writes a source tree but does not produce a publish-ready library module: there is nodoc.gopackage comment, no godoc-formatted public-API surface, nogo.sum, no transitive-dep pinning, no LICENSE harvest frommochi.toml. MEP-74 extends MEP-54 to emit user packages as publish-ready library modules with the right metadata. -
Go's checksum-DB-as-transparency-log is the closest thing the Go ecosystem has to crates.io's RFC #3724 trusted publishing, and Mochi's lockfile must integrate with it. sum.golang.org has operated since September 2019 (Go 1.13) as a Merkle-tree transparency log of every public Go module version. Every Go user's
go getvalidates against the DB by default. A 2026 Mochi-to-Go bridge that does not cross-check[[go-package]]lockfile hashes against sum.golang.org is shipping a weaker supply-chain story thango getitself. The cross-check is a one-RTT HTTP GET per dep at lock time; the cost is negligible; the safety gain is substantial. -
The "import go" surface has zero learning curve for Mochi users.
import go "github.com/spf13/cobra@^v1.8" as cobrais the same shape asimport rust "tokio@^1.42"andimport python "numpy". Mochi users do not need to know what ago.modis, what ago.sumis, whatreplacedirectives are, whatgo vet's pure-Go subset rules are, what a build tag is, what a goroutine is, whatcgois, what//exportis, whatunsafe.Pointeris, what a vanity import path is, what a pseudo-version is, what sum.golang.org'sh1:hash format is. They writeimport go "..." as ...and the bridge does the rest. -
go/packages+go/typesgive Mochi a turn-key stable, machine-readable, in-stdlib API surface for every Go module. Unlike rustdoc-JSON (which is nightly-only and behind-Z unstable-optionsas of MEP-73 spec authoring), the Go ingest path is stable, ships in the standard library extensiongolang.org/x/tools, and has been API-stable since 2018. The bridge ingest binary is a ~300-LOC Go program that loads the module withpackages.Load(packages.NeedTypes | packages.NeedDeps | packages.NeedTypesInfo), walks every*types.Package, and emits a JSON document the bridge consumes. No nightly toolchain. No experimental flag. No schema-version pinning needed. This is materially less risky than MEP-73's rustdoc-JSON ingest. -
MEP-57 + MEP-73 already shipped the prerequisite manifest / lockfile / capability / publish infrastructure. The
mochi.tomltable layout, themochi.lockserialisation, the BLAKE3-256 + secondary-hash dual-hashing, the trusted-publishing OIDC token exchange model, the content-addressed object store, and the capability declaration scheme all transfer to the Go bridge with no architectural duplication. MEP-74 is additive on top of MEP-57 + MEP-73: one new manifest section ([go-dependencies]), one new lockfile repeated table ([[go-package]]), one new CLI surface (mochi pkg publish --to=go+git+...). -
The bridge stays small and audit-friendly. The reference implementation under
package3/go/is targeted at ~5,500 LOC of Go across the module-proxy client, the sum.golang.org cross-check, thego/packagesingest binary, the type-mapping pass, the cgo wrapper synthesiser, the Mochi extern emitter, the lockfile integrator, the git-tag publish flow, and the goroutine-bridge runtime hook. There is no Go-side dynamic codegen at user-machine time (the wrapper is fully synthesised by the Go-side bridge binary; the user'sgo buildonly compiles, never generates code fromgo/packages). There is nounsafe.Pointerin the synthesised wrapper unless the user opts in via[capabilities] unsafe = true. -
The bridge closes a competitive gap with the Rust bridge. MEP-73 shipped a full bidirectional Rust bridge with semver pinning, capability declaration, lockfile cross-check, and publish-to-crates.io. The Go bridge under MEP-54 phase 10 lags on every one of those axes. Closing the gap is a one-MEP investment; deferring it would push the Mochi-Go interop story behind the Mochi-Rust interop story, which is the wrong relative ordering given that Go is Mochi's host language and Go is the more widely deployed of the two ecosystems by deployed-instance count.
Specification
This section is normative. Sub-notes under /docs/research/0074/ are informative.
1. Pipeline overview
MEP-74 introduces a per-import Go dependency resolution layer that sits between the Mochi parser (after MEP-57 has resolved mochi.toml) and the MEP-54 build driver:
mochi.toml [go-dependencies]
| pkgmanifest.Parse + pkgsolver.Solve (MEP-57)
v
resolved Go dep tree (module path + version + source URL)
| package3/go/goproxy.Fetch (proxy.golang.org sparse-index protocol)
v
.zip module artefacts in ~/.cache/mochi/go-deps/<blake3-hex>/
| package3/go/sumdb.Verify (sum.golang.org h1: cross-check)
v
hash-verified module sources
| package3/go/cmd/go-ingest (go/packages.Load + types walk, JSON emit)
v
ApiSurface JSON document per module
| package3/go/typemap.Translate (closed Go-to-Mochi table)
v
TranslatedSurface + SkipReport per module
| package3/go/wrapsyn.Emit (synthesised cgo wrapper package)
v
go_wrap/<module>/ Go package source tree
| package3/go/externemit.Emit (Mochi extern fn / extern type)
v
synthesised .mochi shim file per module, imported by the user's source
| MEP-54 Driver.Build (TargetGoBinary* / TargetGoLibrary)
v
binary or library module
The bridge does not invoke go build at ingest time. The only Go toolchain invocation is the go-ingest helper binary, which calls go/packages.Load on the cached module sources to produce the ApiSurface JSON. The wrapper package is emitted as source by the Go-side bridge binary and built by the user's normal go build (orchestrated by the MEP-54 driver) alongside the user's program.
2. Manifest extension: [go-dependencies] and [go]
The MEP-57 mochi.toml gains two new optional top-level tables:
[go-dependencies]
"github.com/spf13/cobra" = "^v1.8.0"
"github.com/sirupsen/logrus" = { version = "^v1.9", build-tags = ["json_logging"] }
"go.uber.org/zap" = "^v1.27"
"gopkg.in/yaml.v3" = "^v3.0.1"
"my-local-module" = { path = "../my-module" }
"my-git-fork" = { git = "https://github.com/example/my-fork", rev = "abc123def4" }
[go]
go-version = "1.23"
goroutine-bridge = { default-buffer = 1, max-handles = 4096 }
monomorphise = [
{ item = "encoding/json.Unmarshal", T = "MyStruct" },
{ item = "sync.Pool", T = "MyStruct" },
]
build-tags = ["json_logging", "go_purego"]
[go.publish]
canonical-import-path = "github.com/example/my-mochi-lib"
go-version-floor = "1.21"
license = "Apache-2.0 OR MIT"
cgo-export = true
[go.capabilities]
net = true
fs = false
proc = false
cgo = false
unsafe = false
[go.private]
modules = ["corp.example.com/**"]
sumdb-skip = ["corp.example.com/**"]
[go-dependencies] follows Go's go.mod require grammar verbatim: simple string for version, table for inline version + build-tags + path / git / branch / rev / tag. This is intentional: Mochi users with Go background can copy from existing go.mod files without translation; the bridge passes the table through to the synthesised wrapper module's go.mod with no edits.
The [go] table holds Mochi-specific knobs:
go-version: the Go toolchain minimum the wrapper module declares (matches MEP-54's floor). Default"1.21".goroutine-bridge.default-buffer: default channel buffer size when a Gochan Tis exposed as a Mochistream<T>. Default1(unbuffered Go channels havecap == 0but the wrapper allocates a 1-buffered channel to avoid lock-step semantics).goroutine-bridge.max-handles: maximum live cgo handles before the wrapper rejects new ones (a defensive bound; the underlyingcgo.Handleis uint64 so the hard limit is 2^64, but a soft limit prevents leak amplification).monomorphise: a list of explicit generic instantiations. For each entry, the bridge emits the wrapper for<item><T>rather than refusing the import (the default behaviour for generic items). Required for Go 1.18+ generic items.build-tags: build tags the wrapper compilation respects. Mirrors the Go-tagsflag.
The [go.publish] table holds Mochi-as-library knobs:
canonical-import-path: the module path the publishedgo.moddeclares (e.g.,github.com/example/my-mochi-lib). REQUIRED whenmochi pkg publish --to=go+git+...is invoked.go-version-floor: the minimum Go toolchain the published module supports. Written intogo.mod'sgo <version>directive.license: the SPDX licence expression. Written into the package-root LICENSE file plus thedoc.gofirst line.cgo-export: whether to emit_cgo_export.hfor non-Go consumers. Defaultfalse.
The [go.capabilities] table holds Go-bridge-specific capability flags (a strict refinement of MEP-57's [capabilities] table):
net: the Go dep graph contains modules that open network sockets (net.Dial,net/http, gRPC).fs: the dep graph reads or writes files (os.Open,io/fs).proc: the dep graph spawns processes (os/exec.Command).cgo: the dep graph contains files withimport "C".unsafe: the dep graph contains files importingunsafe.
The [go.private] table opts modules out of the sum.golang.org cross-check:
modules: glob patterns of canonical-import-paths the proxy should not be queried for (the bridge bypassesproxy.golang.organd reaches the upstream git repo directly).sumdb-skip: glob patterns for which the bridge skips thesum.golang.orgcross-check. Defaults to themodulesvalue.
3. Lockfile extension: [[go-package]]
The MEP-57 mochi.lock gains one new repeated table:
[[go-package]]
module = "github.com/spf13/cobra"
version = "v1.8.0"
source = { kind = "module-proxy", proxy = "https://proxy.golang.org" }
zip-blake3 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
zip-h1 = "h1:fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
sumdb-verified = true
sumdb-tree-size = 12345678
sumdb-record-hash = "abc...def"
api-surface-sha256 = "..."
wrapper-sha256 = "..."
capabilities-declared = ["fs"]
dependencies = ["github.com/spf13/[email protected]", "github.com/inconshreveable/[email protected]"]
build-tags = []
zip-blake3 is the BLAKE3-256 of the module .zip artefact (the bridge's primary verification hash, matches MEP-57's broader BLAKE3 + secondary-hash rule).
zip-h1 is the Go ecosystem's h1: hash: "h1:" + base64.StdEncoding.EncodeToString(sha256(zip-content)). This is what sum.golang.org publishes. The bridge cross-checks zip-h1 against the public DB at lock time.
sumdb-verified records whether the cross-check succeeded. true is the only acceptable value for public modules; [go.private] modules record false plus a comment naming the override.
sumdb-tree-size records the global checksum-DB tree size at lock time (the Merkle tree's leaf count when this module was looked up). The tree size is monotonic; a future --check can request a consistency proof from sum.golang.org showing that the lock-time leaf is still in the current tree.
sumdb-record-hash records the SHA-256 of the specific log record for this module version (the canonical record body is <module> <version> <h1:hash> plus a \n-separator).
api-surface-sha256 records the SHA-256 of the JSON ApiSurface the bridge ingested. A drift here at --check time is a hard error (the upstream module silently changed its public surface).
wrapper-sha256 records the SHA-256 of the synthesised wrapper package's source tree (concatenated file-by-file). A drift here at --check time is a hard error.
capabilities-declared is the capability set the manifest declared at lock time.
dependencies is the resolved transitive dep graph.
build-tags is the build-tag set the lock was taken under (different tag sets produce different ApiSurface trees; the lockfile pins the tag set).
4. Surface syntax: import go "..." as <alias> (extended)
The Mochi grammar's existing FFI-import production (already shipping for MEP-54 phase 10):
ImportStmt := "import" Lang? StringLit "as" Ident ("auto")?
Lang := "go" | "python" | "typescript" | "rust"
The go <lang> token was wired by MEP-54 phase 10 for bare-name imports (import go "fmt"). MEP-74 extends the <spec> form to admit:
<module-path>: bare module path. Resolves through[go-dependencies]plusmochi.lock.<module-path>@<semver-req>: explicit version constraint.<module-path>@<pseudo-version>: pseudo-versionv0.0.0-YYYYMMDDhhmmss-shortrev.<module-path>@git+<url>or<module-path>@git+<url>#<rev>: git source.<module-path>@path+<relative>: path source.
Example surface:
import go "github.com/spf13/cobra@^v1.8" as cobra
import go "github.com/sirupsen/logrus" as log
fn main() {
let cmd = cobra.new_command()
cmd.set_use("hello")
cmd.set_short("say hello")
cmd.set_run(fun(args: list<string>) {
log.info("hello, world")
})
cmd.execute()
}
The <alias> introduces a Mochi namespace. Symbol resolution looks up <alias>.<item> and binds against the synthesised extern fn declaration the bridge generated for <module-path>.<Item>. Item names: Go's exported items are PascalCase; the bridge auto-lowercases the first letter to match Mochi's snake_case / camelCase convention (cobra.Command → cobra.command, cobra.NewCommand → cobra.new_command). This rename pass is reversible (each emitted extern fn carries a from go "<module>.<OrigName>" clause).
The auto modifier (already accepted by MEP-54 phase 10 for import go ... auto) carries forward unchanged.
5. CLI surface
The Mochi CLI gains the following additions, all under the existing mochi pkg subcommand:
mochi pkg add go <module>[@<semver>]: adds an entry to[go-dependencies]and runsmochi pkg lock.mochi pkg lock: as today (MEP-57), now extended to walk[go-dependencies], query the Go module proxy for resolution, fetch each module .zip into the content-addressed cache, cross-check against sum.golang.org, rungo-ingestto produce ApiSurface JSON, synthesise the wrapper, and write[[go-package]]entries.mochi pkg lock --check: as today, now extended to verifyzip-blake3,zip-h1,sumdb-record-hash,api-surface-sha256,wrapper-sha256, andcapabilities-declaredfor every[[go-package]]entry.mochi pkg lock --sumdb-consistency: optionally fetches a Merkle consistency proof from sum.golang.org showing the lock-time tree leaf is still in the current tree (cost: ~2 RTTs per dep; the bridge runs this in parallel).mochi pkg publish --to=go+git+<repo-url>@<tag> [--dry-run] [--cosign-sign]: builds the package as a Go library module viaTargetGoLibrary, validates withgo vet+gofmt, runsgo build, tags the canonical-import-path repo, optionally cosign-signs the tag, pushes.--dry-runskips the push.mochi pkg publish --to=go+goproxy+<url>(alternative): uploads the module .zip to a private GOPROXY-compatible endpoint instead of git-tagging.mochi pkg sync go: regenerates the wrapper package from scratch (without changing the lockfile). Used after manual edits to the synthesised shim file.
6. Build orchestration
When a Mochi program contains one or more semver-pinned import go "..." declarations, the MEP-54 build driver gains the following extensions:
-
Before invoking
go build, the driver invokespackage3/go/Bridge.PrepareWorkspace(workdir, mochiLock)which:- For each
[[go-package]]inmochi.lock, materialises the module source from the content-addressed cache into<workdir>/go_deps/<module>-<version>/. - Materialises the synthesised wrapper package into
<workdir>/go_wrap/<module>/. - Writes a
<workdir>/go.modmodule manifest withreplacedirectives for every materialised module (matches the existinggo-moduletarget's pattern) plus every wrapper package as areplaceto its workspace path.
- For each
-
The MEP-54 emit pass treats each
import go "<module>" as <alias>(now semver-pinned) as a Mochiimport "./go_wrap/<module>/shim.mochi" as <alias>shim, where the shim file is theextern fncorpus the bridge emitted. -
The wrapper package's
go.moddeclares the source module as arequireentry with the exact version pinned frommochi.lock. -
The user's
go build(invoked byDriver.Build) compiles the wrapper, the main package, and the source modules together. Wrapper packages compiled for a binary target are linked statically; wrapper packages compiled forTargetGoLibraryare exported via//exportand built with-buildmode=c-archive. -
The driver invalidates the cache when any
[[go-package]]wrapper-sha256changes (per MEP-54 phase 0's cache-key construction).
7. Goroutine bridge runtime hook
The wrapper package for any source module that exposes chan T or func(...) callback parameters includes a generated mochi_rt.go file:
//go:build mochi_wrap
package gowrap_<module>
import (
"runtime/cgo"
"sync"
)
var (
handlesMu sync.Mutex
handles = map[uint64]cgo.Handle{}
nextID uint64
)
func acquireHandle(v any) uint64 {
handlesMu.Lock()
defer handlesMu.Unlock()
nextID++
h := cgo.NewHandle(v)
handles[nextID] = h
return nextID
}
func releaseHandle(id uint64) {
handlesMu.Lock()
h, ok := handles[id]
if ok {
delete(handles, id)
}
handlesMu.Unlock()
if ok {
h.Delete()
}
}
func resolveHandle(id uint64) any {
handlesMu.Lock()
defer handlesMu.Unlock()
return handles[id].Value()
}
Channel and callback bindings use acquireHandle / releaseHandle to bridge the cgo boundary safely. Pure-sync modules (no chan, no callback params, no exported method that returns one) skip the runtime file entirely and pay zero cgo-handle cost.
8. sum.golang.org cross-check
At lock time, for each public (non-[go.private]) module, the bridge issues two HTTP GETs to https://sum.golang.org:
GET /lookup/<module>@<version>returns the canonical record body (<module> <version> h1:<base64-sha256>\n) plus the signed tree head at the time of the lookup.GET /tile/8/0/<...>fetches the Merkle tile path proving the record is in the tree.
The bridge verifies:
- The record's
h1:field equals theh1:the bridge computed from the downloaded .zip. - The record is included in the tree via the tile path.
- The tree head signature is valid against the Go team's sum.golang.org public key (hard-coded in the bridge).
A mismatch on any of these three is a hard error at mochi pkg lock time. The user can override by adding the module to [go.private] sumdb-skip = [...], which records sumdb-verified = false in the lockfile.
Phases
See /docs/implementation/0074/ for the per-phase tracking matrix. Eighteen phases cover skeleton (0), module-proxy client (1), sum.golang.org client (2), go/packages ingest helper (3), ApiSurface JSON schema (4), type-mapping table (5), cgo wrapper synthesiser (6), Mochi extern emitter (7), import-go grammar extension (8), build orchestration (9), mochi.lock integration (10), TargetGoLibrary emit (11), git-tag publish flow (12), cosign signing on <tag>.sig (13), goroutine bridge (14), monomorphisation of generics (15), TinyGo / embedded subset (16), and vanity-import-path resolver + WASI publish gate (17).
A phase is LANDED only when its gate is green for every target in the matrix below.
Target matrix
| Phase | host stable go 1.23 (darwin-arm64) | linux-amd64 / linux-arm64 | windows-amd64 | wasm-wasip1 / wasm-js | tinygo embedded |
|---|---|---|---|---|---|
| 0. skeleton | LANDED | n/a | n/a | n/a | n/a |
| 1. module-proxy client | LANDED | n/a | n/a | n/a | n/a |
| 2. sum.golang.org client | LANDED | n/a | n/a | n/a | n/a |
| 3. go/packages ingest | LANDED | n/a | n/a | n/a | n/a |
| 4. ApiSurface JSON | LANDED | n/a | n/a | n/a | n/a |
| 5. type-mapping table | LANDED | required | required | required | required |
| 6. cgo wrapper synthesiser | LANDED (baseline) | required | required | n/a (cgo off on wasm) | required (no_cgo subset) |
| 7. extern emitter | LANDED (baseline) | required | required | required | required |
| 8. import-go grammar (semver) | LANDED | required | required | required | required |
| 9. build orchestration | LANDED (baseline) | required | required | required (no cgo) | required |
| 10. mochi.lock integration | LANDED (schema) | required | required | required | required |
| 11. TargetGoLibrary emit | LANDED (baseline) | required | required | required | required |
| 12. git-tag publish | LANDED (baseline) | n/a (publish is host-only) | n/a | n/a | n/a |
| 13. cosign on tag.sig | LANDED (baseline) | n/a | n/a | n/a | n/a |
| 14. goroutine bridge | LANDED (baseline) | required | required | n/a (no goroutines on wasm-js without scheduler shim) | required |
| 15. monomorphisation | LANDED (baseline) | required | required | required | required |
| 16. TinyGo embedded subset | LANDED (baseline) | n/a | n/a | required (wasm-js via tinygo) | required |
| 17. vanity-import + WASI publish | LANDED (baseline) | required | required | required | n/a |
A phase marked n/a for a target is intentional: the bridge does not promise the behaviour on that target.
Alternatives considered
-
Parse Go source directly with
go/parserinstead of viago/packages. Rejected:go/parserreturns an untyped AST; the bridge needstypes.Typeto translate accurately.go/packagesis exactly the right level (it runsgo/parserplusgo/typesplus dep resolution). There is no win from going one level lower in the Go stdlib. -
Use
pkg.go.dev's HTML rendering as the API source. Rejected: HTML is rendered for human reading, not normative. The same data lives ingo/packagesin machine form. -
Use
gomobile bind's.aar/.frameworkoutput as the bridge surface. Rejected:gomobile bindsupports a tiny subset of Go (no generics, no channels in exported positions, no interfaces with non-builtin types in method signatures). MEP-74 needs the full surface. -
Generate cgo source code with hand-written
//exportdirectives (a lagobind). Rejected: this IS what MEP-74 does, just with a closed type table and the type-mapping decisions made by the bridge rather than by the user.gobind's approach (the user writes the bridge interface) is the boilerplate violation MEP-74 was designed to avoid. -
Use Go's
pluginbuild mode instead ofc-archive. Rejected:pluginis linux-only, has hard limitations on cross-package symbol visibility, and has known dlopen-time crashes on Go version drift.c-archiveis portable across darwin, linux, windows, freebsd. -
Use protobuf or flatbuffers as the cross-boundary serialisation instead of cgo. Rejected: introduces a runtime dependency the user did not opt into, adds 100ns+ per call for the marshal step, and would still need cgo for the actual call dispatch. The cgo path is the shortest.
-
Translate Go's goroutine scheduler into Mochi's host runtime. Rejected: Go's goroutine scheduler is deeply tied to the Go runtime (GC interaction, preemption via segmented stacks, channel multiplexing in
runtime/chan.go). Reimplementing it in Mochi would be a 10,000+ LOC project and would lag the Go team's optimisations. The bridge sidesteps the impedance by letting the Go runtime's scheduler run inside the cgo c-archive; Mochi just calls cgo functions and waits for them to return. -
Treat the Go module proxy's
info,mod,ziptriple as the bridge surface (skipgo/packages). Rejected: the triple is enough for fetch + verify but does not parse the Go source. The bridge still needsgo/packagesfor type-aware translation. -
Allow long-lived
GOPROXYAPI tokens for private-module fetch. Acknowledged but not rejected: private modules legitimately need authentication. The bridge supportsGOPROXYenv-var passthrough and.netrc-style git-credential helpers for private remotes. There is no long-lived token to "publish" because publishing is a git push to a user-controlled remote. -
Sigstore-keyless mandatory for publish (mirror MEP-73's stance). Rejected for v1: the Go ecosystem has not converged on a canonical signing format. The gosum-cosign workflow draft of 2026-Q1 is the front-runner but has not landed in any production Go tooling. MEP-74 ships
--cosign-signas opt-in and re-evaluates the default when the Go team publishes a canonical signing spec. -
Use
golang.org/x/modas a dep instead of re-implementing the module-proxy client inpackage3/go/goproxy. Accepted partially: the bridge usesgolang.org/x/mod/moduleandgolang.org/x/mod/semverfor module path validation and semver comparison (these are pure functions with no I/O). The bridge re-implements the HTTP client for the proxy because the bridge needs to interleave the BLAKE3-256 hash computation with the .zip download, which is awkward with the upstream client. -
Use
cgo-less wrappers viapurego(a Go library that calls into shared libraries via dlopen without cgo). Rejected for v1:puregois a recent project (first commit 2022, GA 2024) that requires the source module ship a.so/.dylib. Most pkg.go.dev modules are pure Go; they do not ship binaries. The cgo path covers more modules. -
Hand-author the type-mapping table per-module rather than have one shared closed table. Rejected: this is the MEP-73 cxx-style violation. The closed table is a one-time investment that covers every module.
-
Use the wazero runtime for wasm-target Go interop rather than the cgo path. Acknowledged: wazero is the right tool when the source module compiles to wasm. The bridge's wasm-wasip1 target uses wazero internally for the wasm execution; the wrapper synthesis is the same.
Risks
-
The cgo cost per call is real. A Mochi loop calling into a Go function 1M times pays ~200ms cumulative cgo overhead on darwin-arm64 (200ns × 1M). Hot loops should batch. Mitigation: the wrapper synthesiser detects hot-path candidates (functions whose Mochi caller is in a
forbody) and offers abatchedvariant that takes a slice on the input side, dispatches once, and returns a slice on the output side. Documented in 09-abi-stability §3. -
Go's GC and cgo interact non-trivially. A pointer the Go side passes to the C side via
//exportis NOT GC-managed on the C side; the C side must release it explicitly via the matching_freesymbol or the underlying Go object becomes unreachable from Go GC's perspective and may be moved. Mitigation: the wrapper synthesiser insertsruntime.KeepAlivecalls at the end of every//exportfunction, and the wrapper documents the ownership contract per item. See 09-abi-stability §2. -
Generic monomorphisation explosion. A Go module that exposes a generic function over N types and the user lists M monomorphisations produces a wrapper with M cgo exports. The combinatorial explosion is real. Mitigation: the
[go.monomorphise]table is required to enumerate; the bridge does not auto-monomorphise. -
The Go module proxy can serve a different .zip than the upstream git repo if proxy.golang.org is compromised. The sum.golang.org cross-check is the mitigation; the bridge always cross-checks unless
[go.private] sumdb-skipopts out. -
sum.golang.org operates a single signing key. The transparency log's tree-head signature is signed by one Go-team-controlled key. If that key is compromised, the cross-check provides no protection. Mitigation: this is the same trust assumption every
go getmakes by default. Mochi adopts the same trust model. A future sub-phase could integrate with a second log (e.g., Sigstore Rekor) for defence-in-depth. -
TinyGo embedded subset is small. TinyGo (the alternative Go compiler for microcontrollers, gaining traction since 2018) supports a subset of the Go stdlib and a subset of pkg.go.dev modules (estimated 8-15% of the top 1,000 are TinyGo-compatible). Modules requiring reflect, cgo, or runtime features (channels under contention, goroutine spawn-and-wait at scale) fail to compile under TinyGo. Mitigation: the embedded gate (phase 16) checks TinyGo compatibility at lock time; non-compatible modules are rejected with a clear diagnostic.
-
Build-tag-conditional code paths. A module that conditions important code on a build tag (
//go:build json_logging) produces a different ApiSurface under different tag sets. The lockfile pins the tag set at lock time. Mitigation: changing[go.build-tags]requires re-runningmochi pkg lock; the--checkmode catches drift. -
Vanity import paths require an HTTP redirect. Modules at vanity paths (
go.uber.org/zap→github.com/uber-go/zap) require the bridge to honour the<meta name="go-import">redirect in the canonical URL's HTML response. Mitigation: the phase 17 vanity-import resolver implements the redirect logic per the Go modules spec. -
Cross-platform
_cgo_export.hgeneration. The cgo-generated header depends on the host C compiler's pointer width and integer sizes. A wrapper built on darwin-arm64 may produce a slightly different header than the same wrapper on linux-amd64. Mitigation: the bridge writes the header to<workdir>/go_wrap/<module>/<goarch>-<goos>.hand the build driver picks the right one. -
Go's
internal/package visibility rule. Imports of<module>/internal/*from outside the module tree are compile errors. The bridge respects this rule: an item whose qualified name traverses aninternal/boundary is silently skipped (no SkipReport entry; the item is invisible to the bridge by Go's own rules). -
Mochi-as-library
cgosymbol collision when published as a c-archive consumed by non-Go. Multiple Mochi libraries compiled to c-archives loaded into the same process collide on themochi_<module>_<fn>symbol prefix. Mitigation: the bridge prefixes every exported symbol with the publishing module's path-hash plus its version major; collisions only arise across compatible majors of the same library. -
Go's
go.modreplacedirective does not survive publish. When a published Go module is consumed by another, the consumer'sgo.moddoes NOT inherit the producer'sreplacedirectives. Mitigation:mochi pkg publishrejects publishing a module that has unresolvedreplacedirectives in its[go-dependencies]; all replaces must point to a real upstream version before publish. -
CI image dependency on the Go toolchain plus optionally TinyGo. The MEP-54 gates already require the Go toolchain; MEP-74 adds optional TinyGo for phase 16 and
cosignfor phase 13. The CI image is bigger. Mitigation: the standardmochilang/ci-goimage bundles the full toolset; standalone users install viago installplusbrew install cosign tinygo.
Acknowledgements
This MEP builds on MEP-54 (Go transpiler) for the gotree IR, the build driver, the runtime module, the cgo build flow, the go-module target's source-tree emit, and the existing import go "..." FFI surface; on MEP-57 (Mochi module and package system) for the mochi.toml manifest, the mochi.lock lockfile, the BLAKE3 + secondary-hash dual-hashing, the content-addressed object store, and the capability declaration scheme; on MEP-45 (C transpiler) for the FFI sidecar pattern; on MEP-73 (Mochi+Rust package bridge) for the bidirectional-bridge spec template, the closed-type-table philosophy, the synthesised-wrapper-crate analogue, and the capability-monotonicity rule; on the golang.org/x/tools/go/packages package maintained by the Go team for the canonical ApiSurface; on the golang.org/x/mod module for the module-path and semver utilities; on the Go team's sum.golang.org and proxy.golang.org infrastructure for the supply-chain backbone; on the gosum-cosign workflow draft of 2026-Q1 for the publish-side signing direction; on the TinyGo project for the embedded-Go subset; on cgo, runtime/cgo, and the //export directive for the C-ABI bridge; on gomobile bind and gopy for prior art on Go-to-other-language bridging; on the wazero project for the wasm-side Go interop story; on the Athens project and JFrog Go for the alternative-GOPROXY publish surface; and on Russ Cox's modules design (the canonical golang.org/x/mod and golang.org/x/tools/go/packages design) for the entire structure the bridge plugs into.