English · 한국어
Run dynamic logic inside Go with explicit resource limits, typed host calls, and a threaded semantic baseline.
- Bounded execution — stack, heap, frame depth, fuel, hooks, context.
- Go interop — Go values and funcs convert both ways; a typed
HostFunctionpath for hot calls. - Native tier — ARM64 execution, on by default, with threaded fallback.
go get github.com/siyul-park/minivmRequires Go 1.26.2+. The VM core uses only the Go standard library.
Build and run a bytecode program that calculates 6 × 7:
prog, err := program.NewBuilder().
Emit(instr.I32_CONST, 6).
Emit(instr.I32_CONST, 7).
Emit(instr.I32_MUL).
Build()
if err != nil {
log.Fatal(err)
}
vm, err := interp.New(prog) // verifies prog before it can run
if err != nil {
log.Fatal(err)
}
defer vm.Close()
if err := vm.Run(context.Background()); err != nil {
log.Fatal(err)
}
result, err := vm.Pop()
if err != nil {
log.Fatal(err)
}
fmt.Println(result) // 42minivm keeps the execution model explicit: bytecode in, verified runtime, typed value out. Fuel and heap are unlimited by default; set interp.WithFuel and interp.WithHeapLimit (plus a cancellable context) for untrusted bytecode. Runnable versions of every example here are in interp/example_test.go.
| Capability | What it gives you |
|---|---|
| Embeddable runtime | First-class functions, locals, globals, closures, refs, strings, arrays, structs, maps, coroutines, and structured errors |
| Host integration | Typed HostFunction calls plus Marshal and Unmarshal for ordinary Go values |
| Resource control | Stack, heap, frame, fuel, context, hook, and debugger controls |
| Fast baseline | Closure-threaded dispatch with low steady-state allocation |
| Semantic baseline | Threaded interpreter with explicit resource controls |
| Safe admission | Static bytecode verification before execution |
- Scripting engines that execute user-defined behavior under host policy
- Rule engines that change runtime decisions without redeployment
- DSL runtimes that need a compact execution layer
- Plugin systems that isolate extension logic from the host application
Marshal turns an ordinary Go func into a VM function. A leading context.Context receives the running context; a trailing error becomes a VM error.
add := func(ctx context.Context, a, b int64) (int64, error) { return a + b, nil }
typ := &types.FunctionType{
Params: []types.Type{types.TypeI64(), types.TypeI64()},
Returns: []types.Type{types.TypeI64()},
}
prog, err := program.NewBuilder().
Globals(typ).
Emit(instr.I64_CONST, 2).
Emit(instr.I64_CONST, 3).
Emit(instr.GLOBAL_GET, 0).
Emit(instr.CALL).
Build()
if err != nil {
log.Fatal(err)
}
vm, err := interp.New(prog)
if err != nil {
log.Fatal(err)
}
defer vm.Close()
fn, err := vm.Marshal(add)
if err != nil {
log.Fatal(err)
}
addr, err := vm.Alloc(fn) // the caller owns this reference
if err != nil {
log.Fatal(err)
}
if err := vm.SetGlobal(0, types.BoxRef(addr)); err != nil { // the global takes it
log.Fatal(err)
}
if err := vm.Run(context.Background()); err != nil {
log.Fatal(err)
}
result, err := vm.Pop()
if err != nil {
log.Fatal(err)
}
fmt.Println(result) // 5For hot calls, interp.NewHostFunction takes and returns []types.Boxed directly, with no reflection.
Unmarshal turns a VM function into a typed Go func:
ctx := context.Background()
// addr holds a VM function; see ExampleRegistry_Unmarshal in interp/example_test.go for the full setup.
var inc func(context.Context, int64) (int64, error)
if err := vm.Unmarshal(types.BoxRef(addr), &inc); err != nil {
log.Fatal(err)
}
n, err := inc(ctx, 41)
if err != nil {
log.Fatal(err)
}
fmt.Println(n) // 42Declare a trailing error on the func type: without one, a failed call panics. A host function may call it: the call runs nested on the same interpreter. Calling it while vm.Run is active outside a host function returns interp.ErrInterpreterBusy.
See Host Integration for the conversion table, ownership of each API, and the low-level HostFunction path.
Threaded execution is the semantic baseline. ARM64 native execution is on by default and tuned or disabled with interp.WithThreshold; measurements and reproduction live in Benchmarks.
interp.New and interp.NewPool verify a snapshot of the program and run only that snapshot, so later changes to prog cannot bypass verification. The snapshot covers code and constants; type objects (for example function signatures) are shared with prog and MUST NOT be mutated after construction (see Verification). To check bytecode without building an interpreter:
if err := program.Verify(prog); err != nil {
log.Fatal(err)
}The verifier rejects malformed control flow, invalid stack behavior, and type mismatches.
prog, err := optimize.New(optimize.O2).Optimize(prog)
if err != nil {
log.Fatal(err)
}The bytecode optimizer rewrites a program before it runs. It does not verify and may panic on malformed input, so run program.Verify first. Levels range from constant folding and dead-code elimination to common-subexpression elimination, promotion, and loop-invariant hoisting.
vm, err := interp.New(prog,
interp.WithStack(4096),
interp.WithHeap(512),
interp.WithFrame(256),
interp.WithFuel(10_000),
interp.WithTick(128),
)
if err != nil {
log.Fatal(err)
}
defer vm.Close()Use hooks for policy checks and debug.NewDebugger with WithHook(dbg.Hook) and WithTick(1) for instruction-accurate breakpoints and stepping (Debugging).
Program → Verify → optimize? → interp.New (verify) → threaded ⇄ native (ARM64)
The threaded interpreter is the semantic baseline and complete execution engine. On ARM64 the JIT adds native execution for hot functions once a program has run long enough to repay a compile (WithThreshold); unsupported paths return to threaded execution.
The instruction set is WebAssembly-inspired but intentionally custom. It uses one-byte opcodes with fixed-width or length-prefixed operands.
| Feature | Status |
|---|---|
| Threaded interpreter | ✅ Available |
| Static bytecode verifier | ✅ Available |
Bytecode optimizer (optimize.O1-O3) |
✅ Available |
ARM64 native tier (on by default, interp.WithThreshold) |
✅ Available |
| Debugger and profiler | ✅ Available |
| x86-64 native backend | 🔲 Not implemented |
The x86-64 assembler backend is not present. See the Roadmap for current priorities.
| Guide | Use it for |
|---|---|
| Documentation Index | Browse all project documentation |
| Compatibility | Check Go, platform, CGO, and build-tag support |
| Host Integration | Connect bytecode with Go values and functions |
| Verification | Understand static admission checks and limits |
| Debugging | Use breakpoints, stepping, and inspection |
| Testing | Understand executable specifications and gates |
| Benchmarks | Reproduce performance and allocation measurements |