Skip to main content

MEP 74. Mochi and Go package bridge

FieldValue
MEP74
TitleMochi and Go package bridge
AuthorMochi core
StatusDraft
TypeStandards Track
Created2026-05-29 20:25 (GMT+7)
DependsMEP-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:

  1. go/packages + go/types from the Go standard library as the canonical machine-readable Go module surface. The Go toolchain ships go/packages.Load (under golang.org/x/tools/go/packages) which returns a full []*packages.Package tree where each *packages.Package carries a *types.Package exposing every exported identifier with its full types.Type (named types, struct fields with types.Var, methods with types.Func, interfaces with full method sets, signatures with parameter and result tuples, type parameters since Go 1.18). This is the same surface that gopls, staticcheck, and golangci-lint consume; 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 duplicate go/parser + go/types for no gain). Reading pkg.go.dev's HTML documentation is rejected (it is rendered, not normative). Using gomobile bind's .aar / .framework output 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.

  2. Auto-synthesised cgo wrapper Go package (go_wrap/) per imported Go dep, not direct cgo invocation or direct dlopen of 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 //export surface wrapping each translatable public item: scalar return types pass through directly, string becomes *C.char plus an explicit mochi_go_<module>_string_free symbol (the matching C.free on the Go-allocated string requires C.GoString then C.CString round-trip to escape Go's GC), []T becomes a (ptr, len, cap) triple plus a _free symbol on the C side, map[K]V becomes an opaque *C.MochiMap handle with explicit _get / _set / _iter / _free symbols (Go maps cannot be passed across the cgo boundary safely), error becomes a pair of out-pointers plus a mochi_status return code (matching the MEP-73 Result<T, E> shape), chan T becomes a *C.MochiChannel handle with _send / _recv / _close symbols, and func types become *C.MochiFunc handles with a _call symbol that takes a packed argument slice. The wrapper package is built as a c-archive via go 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 the go-module target) 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-module plugin.Open path; Go's plugin build 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's aotir IR 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.

  3. 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 is string or 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 types type 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 types type I interface { M() }↔ extern type I (opaque handle with method-call thunks), error↔ try-catch desugar (matches MEP-73 Result<T, E> shape), chan T↔stream<T> (with explicit _close semantics on the wrapper side), func(A, B) (C, error)↔ fun(A, B) -> Result<C> (signature-mapped), and the ...T variadic suffix as varargs<T>. Items outside the table (generic type parameters beyond the monomorphised concretisations declared in mochi.toml's [go.monomorphise] section, unsafe.Pointer, reflect.Value, reflect.Type, cgo.Handle, channel directions on type-aliases (chan<- T vs <-chan T), unexported types in exported positions, interface{} / any in non-constrained positions, struct fields with tags whose semantics depend on a tag-consuming downstream library, and any item whose visibility crosses an internal/ directory boundary) are skipped with a SkipReport entry that names the item and the reason. The user can override with a hand-written extern fn declaration that takes responsibility for the type at the FFI boundary. Aggressive translation (e.g., synthesising a Mochi wrapper for every interface{} return) is rejected: the bridge promises zero boilerplate, not full generality. See 05-type-mapping §1, 02-design-philosophy §3.

  4. import go "<module-path>@<semver>" as <alias> as the surface, with <semver> resolved through mochi.lock, extending MEP-54 phase 10's existing keyword. The grammar's go <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 plus mochi.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 like v0.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 next mochi pkg lock). The mochi.lock lockfile records the resolved version, BLAKE3-256 + the Go h1: hash (base64-of-sha256-of-zip-content; identical to what sum.golang.org publishes), the go/packages revision 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 go participates in the same module-resolution flow as import rust / import python / import typescript. See 01-language-surface §1, 06-go-module-publish-flow §2.

  5. Mochi → Go library module emission via a new TargetGoLibrary MEP-54 target, parallel to the existing TargetGoModule. MEP-54 today exposes TargetGoBinaryLinuxAmd64, TargetGoBinaryLinuxArm64, TargetGoBinaryDarwinAmd64, TargetGoBinaryDarwinArm64, TargetGoBinaryWindowsAmd64, TargetGoBinaryFreeBSDAmd64, TargetGoModule (emit-the-source-tree only, no build), TargetGoWasmJS, and TargetGoWasiP1. MEP-74 adds TargetGoLibrary. Where TargetGoModule writes a package main with a func main(), the library path emits a package <name> with func, type, const, and var declarations mirroring the Mochi package's public surface; sets module <vanity-import-path> in go.mod; runs the bridge's cbindgen-equivalent (a built-in _cgo_export.h emitter; cgo's standard tooling already does this when -buildmode=c-archive is used, but the library target also emits a plain Go header for non-Go non-cgo consumers); populates the publish-side metadata (the doc.go with the package summary, the README.md at the repo root, the LICENSE file, the // Package foo provides ... first-sentence rule godoc enforces) from the Mochi package's mochi.toml; and emits a go.sum pinning every transitive dependency. The driver's Build function gates the library path on Driver.LibraryMode=true plus target == TargetGoLibrary. See 01-language-surface §3, 06-go-module-publish-flow §1.

  6. 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>.sig tag. 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 user go gets the module, the module proxy at proxy.golang.org fetches the new tag, computes the h1: 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:

    1. Builds the library module via TargetGoLibrary.
    2. Validates go vet ./... and gofmt -l ./... produce zero diagnostics.
    3. Runs go build ./... against the emitted module to confirm it compiles.
    4. Tags the canonical-import-path repo at HEAD with the requested semver tag.
    5. 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).
    6. Pushes the tag (and optionally <tag>.sig) to the canonical-import-path remote.
    7. Optionally pings proxy.golang.org/<module-path>/@v/<tag>.info to 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 .zip to a private GOPROXY-compatible server. There is no GO_PROXY_TOKEN analogue; 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.

  7. Goroutine bridge that exposes Go's first-class concurrency (go statement, 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 the go statement freely. The bridge has no global runtime to construct. The bridge does need to map Mochi's spawn <expr> to the wrapper's go <expr-shim>() and Mochi's stream<T> to the wrapper's chan T; these mappings happen at wrapper-synthesis time, with the wrapper holding a sync.Map of active channels keyed by opaque handle ID. The wrapper exposes per-channel _send, _recv, and _close cgo-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.

  8. sum.golang.org checksum-DB integration as a hard prerequisite of every lock operation; private-module GONOSUMCHECK is opt-in per-module only. The Go ecosystem ships an unusually strong supply-chain story by 2026 standards: the module proxy at proxy.golang.org and the checksum database at sum.golang.org are jointly operated by the Go team, and every go get of 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 the h1: hash sum.golang.org publishes (cross-checked against the public DB at lock time). The cross-check is a hard error on mismatch (the h1: 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 corporate corp.example.com/internal/foo) opts out via [go.private] modules = ["corp.example.com/**"] in mochi.toml, mirroring the GOPRIVATE / GONOSUMCHECK Go-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:

  1. 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.

  2. 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 (the go-module target) writes a source tree but does not produce a publish-ready library module: there is no doc.go package comment, no godoc-formatted public-API surface, no go.sum, no transitive-dep pinning, no LICENSE harvest from mochi.toml. MEP-74 extends MEP-54 to emit user packages as publish-ready library modules with the right metadata.

  3. 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 get validates 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 than go get itself. The cross-check is a one-RTT HTTP GET per dep at lock time; the cost is negligible; the safety gain is substantial.

  4. The "import go" surface has zero learning curve for Mochi users. import go "github.com/spf13/cobra@^v1.8" as cobra is the same shape as import rust "tokio@^1.42" and import python "numpy". Mochi users do not need to know what a go.mod is, what a go.sum is, what replace directives are, what go vet's pure-Go subset rules are, what a build tag is, what a goroutine is, what cgo is, what //export is, what unsafe.Pointer is, what a vanity import path is, what a pseudo-version is, what sum.golang.org's h1: hash format is. They write import go "..." as ... and the bridge does the rest.

  5. go/packages + go/types give 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-options as of MEP-73 spec authoring), the Go ingest path is stable, ships in the standard library extension golang.org/x/tools, and has been API-stable since 2018. The bridge ingest binary is a ~300-LOC Go program that loads the module with packages.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.

  6. MEP-57 + MEP-73 already shipped the prerequisite manifest / lockfile / capability / publish infrastructure. The mochi.toml table layout, the mochi.lock serialisation, 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+...).

  7. 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, the go/packages ingest 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's go build only compiles, never generates code from go/packages). There is no unsafe.Pointer in the synthesised wrapper unless the user opts in via [capabilities] unsafe = true.

  8. 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 Go chan T is exposed as a Mochi stream<T>. Default 1 (unbuffered Go channels have cap == 0 but 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 underlying cgo.Handle is 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 -tags flag.

The [go.publish] table holds Mochi-as-library knobs:

  • canonical-import-path: the module path the published go.mod declares (e.g., github.com/example/my-mochi-lib). REQUIRED when mochi pkg publish --to=go+git+... is invoked.
  • go-version-floor: the minimum Go toolchain the published module supports. Written into go.mod's go <version> directive.
  • license: the SPDX licence expression. Written into the package-root LICENSE file plus the doc.go first line.
  • cgo-export: whether to emit _cgo_export.h for non-Go consumers. Default false.

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 with import "C".
  • unsafe: the dep graph contains files importing unsafe.

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 bypasses proxy.golang.org and reaches the upstream git repo directly).
  • sumdb-skip: glob patterns for which the bridge skips the sum.golang.org cross-check. Defaults to the modules value.

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] plus mochi.lock.
  • <module-path>@<semver-req>: explicit version constraint.
  • <module-path>@<pseudo-version>: pseudo-version v0.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
import go "go.uber.org/[email protected]" as zap

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.Commandcobra.command, cobra.NewCommandcobra.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 runs mochi 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, run go-ingest to produce ApiSurface JSON, synthesise the wrapper, and write [[go-package]] entries.
  • mochi pkg lock --check: as today, now extended to verify zip-blake3, zip-h1, sumdb-record-hash, api-surface-sha256, wrapper-sha256, and capabilities-declared for 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 via TargetGoLibrary, validates with go vet + gofmt, runs go build, tags the canonical-import-path repo, optionally cosign-signs the tag, pushes. --dry-run skips 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:

  1. Before invoking go build, the driver invokes package3/go/Bridge.PrepareWorkspace(workdir, mochiLock) which:

    • For each [[go-package]] in mochi.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.mod module manifest with replace directives for every materialised module (matches the existing go-module target's pattern) plus every wrapper package as a replace to its workspace path.
  2. The MEP-54 emit pass treats each import go "<module>" as <alias> (now semver-pinned) as a Mochi import "./go_wrap/<module>/shim.mochi" as <alias> shim, where the shim file is the extern fn corpus the bridge emitted.

  3. The wrapper package's go.mod declares the source module as a require entry with the exact version pinned from mochi.lock.

  4. The user's go build (invoked by Driver.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 for TargetGoLibrary are exported via //export and built with -buildmode=c-archive.

  5. The driver invalidates the cache when any [[go-package]] wrapper-sha256 changes (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:

  1. 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.
  2. 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 the h1: 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

Phasehost stable go 1.23 (darwin-arm64)linux-amd64 / linux-arm64windows-amd64wasm-wasip1 / wasm-jstinygo embedded
0. skeletonLANDEDn/an/an/an/a
1. module-proxy clientLANDEDn/an/an/an/a
2. sum.golang.org clientLANDEDn/an/an/an/a
3. go/packages ingestLANDEDn/an/an/an/a
4. ApiSurface JSONLANDEDn/an/an/an/a
5. type-mapping tableLANDEDrequiredrequiredrequiredrequired
6. cgo wrapper synthesiserLANDED (baseline)requiredrequiredn/a (cgo off on wasm)required (no_cgo subset)
7. extern emitterLANDED (baseline)requiredrequiredrequiredrequired
8. import-go grammar (semver)LANDEDrequiredrequiredrequiredrequired
9. build orchestrationLANDED (baseline)requiredrequiredrequired (no cgo)required
10. mochi.lock integrationLANDED (schema)requiredrequiredrequiredrequired
11. TargetGoLibrary emitLANDED (baseline)requiredrequiredrequiredrequired
12. git-tag publishLANDED (baseline)n/a (publish is host-only)n/an/an/a
13. cosign on tag.sigLANDED (baseline)n/an/an/an/a
14. goroutine bridgeLANDED (baseline)requiredrequiredn/a (no goroutines on wasm-js without scheduler shim)required
15. monomorphisationLANDED (baseline)requiredrequiredrequiredrequired
16. TinyGo embedded subsetLANDED (baseline)n/an/arequired (wasm-js via tinygo)required
17. vanity-import + WASI publishLANDED (baseline)requiredrequiredrequiredn/a

A phase marked n/a for a target is intentional: the bridge does not promise the behaviour on that target.

Alternatives considered

  1. Parse Go source directly with go/parser instead of via go/packages. Rejected: go/parser returns an untyped AST; the bridge needs types.Type to translate accurately. go/packages is exactly the right level (it runs go/parser plus go/types plus dep resolution). There is no win from going one level lower in the Go stdlib.

  2. Use pkg.go.dev's HTML rendering as the API source. Rejected: HTML is rendered for human reading, not normative. The same data lives in go/packages in machine form.

  3. Use gomobile bind's .aar / .framework output as the bridge surface. Rejected: gomobile bind supports 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.

  4. Generate cgo source code with hand-written //export directives (a la gobind). 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.

  5. Use Go's plugin build mode instead of c-archive. Rejected: plugin is linux-only, has hard limitations on cross-package symbol visibility, and has known dlopen-time crashes on Go version drift. c-archive is portable across darwin, linux, windows, freebsd.

  6. 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.

  7. 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.

  8. Treat the Go module proxy's info, mod, zip triple as the bridge surface (skip go/packages). Rejected: the triple is enough for fetch + verify but does not parse the Go source. The bridge still needs go/packages for type-aware translation.

  9. Allow long-lived GOPROXY API tokens for private-module fetch. Acknowledged but not rejected: private modules legitimately need authentication. The bridge supports GOPROXY env-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.

  10. 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-sign as opt-in and re-evaluates the default when the Go team publishes a canonical signing spec.

  11. Use golang.org/x/mod as a dep instead of re-implementing the module-proxy client in package3/go/goproxy. Accepted partially: the bridge uses golang.org/x/mod/module and golang.org/x/mod/semver for 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.

  12. Use cgo-less wrappers via purego (a Go library that calls into shared libraries via dlopen without cgo). Rejected for v1: purego is 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.

  13. 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.

  14. 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

  1. 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 for body) and offers a batched variant that takes a slice on the input side, dispatches once, and returns a slice on the output side. Documented in 09-abi-stability §3.

  2. Go's GC and cgo interact non-trivially. A pointer the Go side passes to the C side via //export is NOT GC-managed on the C side; the C side must release it explicitly via the matching _free symbol or the underlying Go object becomes unreachable from Go GC's perspective and may be moved. Mitigation: the wrapper synthesiser inserts runtime.KeepAlive calls at the end of every //export function, and the wrapper documents the ownership contract per item. See 09-abi-stability §2.

  3. 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.

  4. 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-skip opts out.

  5. 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 get makes 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.

  6. 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.

  7. 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-running mochi pkg lock; the --check mode catches drift.

  8. Vanity import paths require an HTTP redirect. Modules at vanity paths (go.uber.org/zapgithub.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.

  9. Cross-platform _cgo_export.h generation. 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>.h and the build driver picks the right one.

  10. 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 an internal/ boundary is silently skipped (no SkipReport entry; the item is invisible to the bridge by Go's own rules).

  11. Mochi-as-library cgo symbol 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 the mochi_<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.

  12. Go's go.mod replace directive does not survive publish. When a published Go module is consumed by another, the consumer's go.mod does NOT inherit the producer's replace directives. Mitigation: mochi pkg publish rejects publishing a module that has unresolved replace directives in its [go-dependencies]; all replaces must point to a real upstream version before publish.

  13. 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 cosign for phase 13. The CI image is bigger. Mitigation: the standard mochilang/ci-go image bundles the full toolset; standalone users install via go install plus brew 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.