Contributing
May 25, 2026 ยท View on GitHub
Mvm is a free and open-source project, and your feedback and contributions are needed and always welcome.
Issues and pull requests are opened at https://github.com/mvm-sh/mvm.
If you are not sure something is a bug, or want to discuss a possible new feature before filing an issue, start a thread in GitHub Discussions. That is the preferred venue for mvm-related questions and for shaping a change before any code is written.
To try the interpreter without a local checkout, the mvm playground runs it in the browser.
Building and testing
make test # tests with race detector and coverage
make lint # golangci-lint (gofumpt + the linters in .golangci.yaml)
Single-package and single-test forms used while iterating:
go test -v ./interp
go test -run TestExpr ./interp
go test -run "TestExpr/#05" ./interp
go test -bench Fib ./interp
Running the interpreter directly while debugging a change:
go run . run -e "fmt.Println(1+2)" # evaluate one expression
go run . _samples/fib.go # run a sample program
go run . test ./pkg # run Test* funcs in a local package dir
go run . test github.com/google/uuid # fetch a remote module and run its tests
When a program misbehaves, the -x flag (and the MVM_TRACE
environment variable) turn on a per-instruction trace: bare -x /
MVM_TRACE=line for source-line tracing, -x=op / MVM_TRACE=op for
bytecode tracing, -x=all for both. MVM_DEBUG=1 dumps the compiled
data and code segments. Dropping a trap() call into the program (or
into a _samples/*.go repro) opens an interactive debug> prompt where
you can dump the call stack and memory. See
docs/usage.md for the full surface, and
docs/architecture.md for how tracing and the
debugger are wired.
Adding a regression test
Most language- and stdlib-level bugs are reproduced by adding a small
program under _samples/ and asserting its output against go run's.
The interpreter test in interp/interpreter_test.go walks _samples/
and runs each file; it also has an etest table for cases that are
awkward as standalone files.
- A
_samples/*.gofile is expected to compile and run, and to print exactly its trailing// Output:block. - To skip a known-broken sample without removing it, add a top-level
comment
// skip: reason here. The runner leaves it in place but does not execute it. Do not delete a failing sample. Keeping it visible is how we track outstanding bugs -- a newly found bug should land as a skipped sample (or a skippedetestentry) in the same PR that discovers it, even if the fix comes later. - For tests defined directly in
interp/interpreter_test.go(theetesttable), setskip: trueon the entry rather than commenting it out.
Updating generated files and bundled components
Several parts of the tree are generated; never hand-edit them. They
fall into four groups, all refreshed by make generate:
- Stringer files --
lang/token_string.go,vm/op_string.go,symbol/kind_string.go. Regenerate after changing theToken,Op, orKindenums. Requiresstringer(go install golang.org/x/tools/cmd/stringer@latest). - Stdlib bindings -- one file per import path under
stdlib/core/andstdlib/ext/, each headed// Code generated by cmd/extract. They are produced bycmd/extractfrom the//go:generate go run ../cmd/extractdirectives instdlib/gen.go, reflecting over the packages in your active Go toolchain ($GOROOT/src/...). Thecorevs.extsplit (link-footprint control) is defined by theCoremap incmd/extract/categories.go. To expose a new stdlib package: add a//go:generate go run ../cmd/extract -stdlib <path> $GOROOT/src/<path>line tostdlib/gen.go(and add it toCoreif it is pure-compute and browser-safe), thenmake generate. See docs/modules/extract.md and docs/modules/stdlib.md. - Platform syscall bindings --
stdlib/ext/syscall_<os>_<arch>.go, one pergo tool dist listtarget. TheMakefilegeneratetarget loops over all platforms; you do not invokeextractfor these by hand. - The embedded std module --
stdlib/src.zip, a Go-module-proxy-format zip of the syntheticgithub.com/mvm-sh/stdmodule, committed to the repo (it is//go:embed-ed bystdlib/srcfs.go, so the tree builds without a regeneration step). That module holds the interpreted-Go sources for packages mvm runs as source rather than native bridges, plus stdlib patches under itspatches/directory. Native bridges and shadow packages live here inmvminstead (stdlib/bridges.go,stdlib/patcher.go,stdlib/jsonx,stdlib/errorsx); see ADR-012, ADR-013, ADR-017. Rebuild it (and commit the result) only when editing the std module:make generaterepacks it from a clone of https://github.com/mvm-sh/std at../std(override withMVMSTD_SRC=/path/to/clone).
So a fresh clone builds and tests as-is.
Running make generate (only needed after a Go-toolchain bump or a std-module edit)
additionally requires stringer installed, a working Go toolchain (for $GOROOT),
and for the embedded zip, a sibling mvm-sh/std clone.
The verify-generated CI workflow rebuilds everything and fails on any
diff, so commit the regenerated files in the same PR. Its weekly cron
also catches drift introduced by new Go toolchain or mvm-sh/std
releases.
Style
- Code is formatted with
gofumpt(a stricter superset ofgofmt), enforced by golangci-lint. Runmake lintafter every change. - Keep source files ASCII-only unless a non-ASCII character is truly unavoidable.
- Keep comments concise, or omit them.
The code should be self-explanatory and the design lives in
docs/. Non-exported funcs usually need no doc comment; reserve inline comments for non-obvious why (a subtle invariant, a workaround, a footgun). - In Markdown sources (
*.md-- docs, ADRs, this file) and in code comments (block, doc, and multi-line//runs), start each sentence on its own line (wrapping long ones as needed). A single-sentence comment stays one line. It keeps diffs sentence-scoped and lets you edit a sentence without reflowing its neighbours. Older docs and comments still use fixed-column wrapping; convert one to this style when you are substantially editing it, not as a standalone reformat. - Generated files (see above) are produced by
make generate-- don't edit them by hand.
Documentation
- docs/index.md: documentation entry point
- docs/usage.md: CLI reference, tracing, the trap() debugger, remote imports
- docs/architecture.md: pipeline, memory model, key design decisions
- docs/modules/: per-package reference
- docs/decisions/: architecture decision records (ADRs)
If you change behavior covered by an ADR, please update the ADR (or add a new one) in the same PR.