ADR-013: Split stdlib bindings into core and ext
May 28, 2026 ยท View on GitHub
Status: accepted Date: 2026-04-27
Context
Mvm's pitch is "single static binary, full Go stdlib bundled in." That
bundling has a cost: every package wired into stdlib.Values
contributes its transitive Go dependencies to the final binary, even
if the embedding host never imports it from interpreted code.
Early on, all generated bindings lived in a single stdlib/ package.
A blank-import (_ "github.com/mvm-sh/mvm/stdlib") pulled in
everything: net/http, crypto/tls, image/jpeg, runtime/pprof,
the full syscall matrix. For embedders who want to run untrusted
interpreted Go in a tight binary, or in a js/wasm build, or in
a sandbox where syscalls are off-limits. That "all or nothing" choice
was wrong.
Decision
Split generated bindings into two sub-packages plus a convenience aggregator:
stdlib/core/: pure-compute, browser-safe packages with modest transitive footprint:fmt,bytes,strings,strconv,encoding/json,regexp,time,math/*, the container types,sync,sort, etc. ~40 packages.stdlib/ext/: host-coupled or transitively heavy packages:net/*,os/*,crypto/*(except hashes),image/*,runtime/*,database/sql,syscall/*(per platform),testing,text/template, etc. ~170 files (counting per-platform syscall variants).stdlib/all/: blank-importscoreandext. The full bundle, kept as one line for embedders that want everything.
The split is data-driven: cmd/extract/categories.go carries a Core
map listing the import paths that route to core/. Anything not
listed goes to ext/. Adding a package to core is a one-line
change followed by make generate.
The criterion for core membership is roughly: would this package
work in a js/wasm build with cgo disabled, with no system access,
and without dragging in net/crypto stack? If yes, it belongs in
core. The list is reviewed by hand, not automated.
Consequences
Easier:
- An embedder who needs only computational packages imports
stdlib/coreand gets a much smaller binary (no net/crypto/image link cost). js/wasmand similar restricted targets become viable without build-tag gymnastics on individual stdlib packages.- The
coreset is small enough that contributors can scan it; new packages get a deliberate routing decision rather than ending up in one undifferentiated bucket.
Harder:
- Two import paths to register instead of one. Embedders who want
everything must import
stdlib/all, not juststdlib. The mvm CLI itself does this inmain.go. - The split criterion is a judgement call. There is no automated
"transitive footprint" analysis; a package on the
corelist could silently grow heavy if upstream Go adds dependencies. Periodic manual review is implied. - The mvm-native shadow sub-packages (
stdlib/jsonx,xmlx,gobx) that once sat alongsidecore/extwere removed when synthesized rtypes (ADR-021) made them unnecessary;stdlib/all/now pulls onlycoreandext.