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/*.go file 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 skipped etest entry) in the same PR that discovers it, even if the fix comes later.
  • For tests defined directly in interp/interpreter_test.go (the etest table), set skip: true on 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:

  1. Stringer files -- lang/token_string.go, vm/op_string.go, symbol/kind_string.go. Regenerate after changing the Token, Op, or Kind enums. Requires stringer (go install golang.org/x/tools/cmd/stringer@latest).
  2. Stdlib bindings -- one file per import path under stdlib/core/ and stdlib/ext/, each headed // Code generated by cmd/extract. They are produced by cmd/extract from the //go:generate go run ../cmd/extract directives in stdlib/gen.go, reflecting over the packages in your active Go toolchain ($GOROOT/src/...). The core vs. ext split (link-footprint control) is defined by the Core map in cmd/extract/categories.go. To expose a new stdlib package: add a //go:generate go run ../cmd/extract -stdlib <path> $GOROOT/src/<path> line to stdlib/gen.go (and add it to Core if it is pure-compute and browser-safe), then make generate. See docs/modules/extract.md and docs/modules/stdlib.md.
  3. Platform syscall bindings -- stdlib/ext/syscall_<os>_<arch>.go, one per go tool dist list target. The Makefile generate target loops over all platforms; you do not invoke extract for these by hand.
  4. The embedded std module -- stdlib/src.zip, a Go-module-proxy-format zip of the synthetic github.com/mvm-sh/std module, committed to the repo (it is //go:embed-ed by stdlib/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 its patches/ directory. Native bridges and shadow packages live here in mvm instead (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 generate repacks it from a clone of https://github.com/mvm-sh/std at ../std (override with MVMSTD_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 of gofmt), enforced by golangci-lint. Run make lint after 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

If you change behavior covered by an ADR, please update the ADR (or add a new one) in the same PR.