vm
June 30, 2026 ยท View on GitHub
Stack-based bytecode virtual machine.
Overview
The vm package executes compiled bytecode. Machine.Run() interprets a
Code (slice of Instruction) over two separate slices: globals []Value
(module-level vars and function addresses, shared across goroutines) and
mem []Value (the per-goroutine call stack). The package also defines
Value (the runtime value representation) and Type (runtime type
metadata).
Key types and functions
Execution
Machine-- VM state:code,globals []Value(shared with child goroutines),mem []Value(per-goroutine call stack),ip,fp, closureheap, aheapFrames [][]*Valuestack (saved caller closure heaps, pushed only for closure calls whereheap != nil), panic state (panicking,panicVal), goroutine state (wg *sync.WaitGroup,isGoroutine bool), a parent-ownedfuncFields *funcFieldsTablekeyed by the closure's function pointer read live from the field's bytes (resolves the original mvm Value even after a struct-copy rewrites the field), and debug state.Run() error-- main execution loop. Dispatches onOpvia a switch statement.Push(vals ...Value)-- append values toglobals(used beforeRunto load the data segment). Returns the start index.PushCode(instrs ...Instruction)-- append instructions (for incremental evaluation).
Values
Value-- hybrid runtime value:num uint64-- inline storage for numeric types (bool, int*, uint*, float*). Holds raw bits.ref reflect.Value-- composite data (string, slice, map, struct, func) or type metadata for numerics.- Variable slots (from
NewValue):refis addressable. - Temporaries (from arithmetic):
refisreflect.Zero(typ), non-addressable;numis canonical.
NewValue(reflect.Type) Value-- allocate a zero value (addressable).ValueOf(any) Value-- wrap a Go value.
Types
Type-- runtime type:Rtype reflect.Type,Name,PkgPath,Methods []Method,IfaceMethods []IfaceMethod,Embedded []EmbeddedField,Params []*Type,Returns []*Type,Fields []*Type,ElemType *Type.ElemTypepreserves the mvm-level element type for map/slice/array/pointer/chan composites, propagated byPointerTo,ArrayOf,SliceOf,MapOf, andChanOf.Elem()returnsElemTypewhen set, falling back to a barereflect.Typewrapper.(*Type).SameAs(u *Type) bool-- reports whether two types represent the same concrete type (sameRtypeandName). Used byTypeBranchfor type-switch comparison and byTypeAssert.(*Type).ReturnType(i int) *Type-- returns the mvm-level i'th return type fromReturnsif available, else falls back toOut(i)(reflect-level). Preserves interface method metadata for multi-return functions.Iface{Typ *Type, Val Value}-- boxed interface value.Closure{Code int, Heap []*Value}-- captured function.SelectCaseInfo-- describes one case of aselectstatement:Dir reflect.SelectDir(Send/Recv/Default),Slot/OkSlot(memory indices for received value and ok bool, -1 if unused),Local bool(true if slots are frame-relative rather than global).SelectMeta-- compile-time metadata for aselectblock, stored in the data segment at a known index. HoldsCases []SelectCaseInfoandTotalPop int(total stack slots consumed by channel/value entries, precomputed soSelectExeccan find the base without scanning).MvmFunc{Val Value, GF reflect.Value}-- wraps a mvm function value alongside itsreflect.MakeFunc-generated Go wrapper. Stored when a mvm func is assigned to a struct field of func type so that native Go callbacks (e.g. HTTP handlers) can call back into the VM.WrapFunccreates it; the inner VM always dispatches viaVal.
Opcodes
Op(int enum) -- 230+ opcodes organized in groups:- Stack/control:
Nop,Pop,Push,Swap,Exit,Jump,JumpTrue,JumpFalse,JumpSetTrue,JumpSetFalse,Call,CallImm,Return. - Memory:
Get,GetLocal,GetGlobal,SetLocal,SetGlobal,SetS,Addr,Deref,DerefSet,Grow.SetLocal/SetGlobalare specialized variants that write directly to frame-relative or global indices.SetSis used for addressable slots (callsreflect.Value.Setto propagate writes into the ref without touchingnum). PlainSetno longer exists as an opcode. - Arithmetic -- no generic numeric opcodes remain; every arithmetic op
is statically typed:
Equal,EqualSet(type-agnostic comparison viaValue.Equal).AddStr-- string concatenation (s1 + s2); the only non-numeric binary add op.- Per-type variants (12 types each, selected at compile time via
NumKindOffset):AddInt...AddFloat64,SubInt...SubFloat64,MulInt...MulFloat64,DivInt...DivFloat64,RemInt...RemFloat64,NegInt...NegFloat64,GreaterInt...GreaterFloat64,LowerInt...LowerFloat64.
- Immediate:
AddIntImm,SubIntImm,MulIntImm,GreaterIntImm,GreaterUintImm,LowerIntImm,LowerUintImm. - Bitwise:
BitAnd,BitOr,BitXor,BitAndNot,BitShl,BitShr,BitComp. - Bit manipulation:
Clz32,Clz64,Ctz32,Ctz64,Popcnt32,Popcnt64,Rotl32,Rotl64,Rotr32,Rotr64. Unary ops (clz/ctz/popcnt) setref = zint; binary ops (rotl/rotr) preserve the input type. Implemented viamath/bits. - Float math (unary):
AbsFloat32,AbsFloat64,SqrtFloat32,SqrtFloat64,CeilFloat32,CeilFloat64,FloorFloat32,FloorFloat64,TruncFloat32,TruncFloat64,NearestFloat32,NearestFloat64. Implemented viamath. - Float math (binary):
MinFloat32,MinFloat64,MaxFloat32,MaxFloat64,CopysignFloat32,CopysignFloat64. - Collections:
Index,IndexAddr,IndexSet,MapIndex,MapIndexOk,MapSet,Slice,Slice3,Field,FieldSet,FieldFset.IndexAddrtakes an array/slice and index and pushes a pointer to the element (used for&a[i]).MapIndexOkpushes both value and ok (thev, ok := m[k]form). - Types:
Convert,TypeAssert,TypeBranch,Fnew,FnewE,New,MkSlice,MkMap. - Builtins:
Append,AppendSlice,CopySlice,DeleteMap,Cap,Len,PtrNew.AppendSlicepacks N values into a[]Tand callsreflect.AppendSlice; used whenappendreceives multiple elements to avoid intermediate heap allocation. - Output:
Print,Println-- dedicated opcodes forprint(v...)andprintln(v...); write tom.outdirectly withoutreflect.Value.Call. - Closures:
HeapAlloc,HeapGet,HeapSet,HeapPtr,MkClosure. - Interfaces:
IfaceWrap,IfaceCall. - Native interop:
WrapFunc-- wraps a mvm function value in areflect.MakeFuncadapter so it can be called by native Go code. Produces aMvmFuncon the stack. - Goroutines and channels:
GoCall-- pop function + N args, spawn a childMachineviagoand return immediately;A= narg.GoCallImm-- likeGoCallbut for a known non-closure function (avoids loading the function value from the stack);A= globals index of the function,B= narg.MkChan-- create a channel;A= globals index of elem type,B= buffer size (negative means read size from stack).ChanSend-- pop channel and value, send synchronously viareflect.Value.Send.ChanRecv-- pop channel, push received value; ifA== 1 also push the ok bool (two-result formv, ok := <-ch).ChanClose-- pop channel, callreflect.Value.Close.SelectExec-- execute aselectstatement.A= globals index ofSelectMeta;B= number of cases. Pops channel/value entries off the stack (count frommeta.TotalPop), callsreflect.Select, then writes the received value and ok bool into the slots named inSelectMeta.Cases. Pushes the chosen case index.
- Range:
Next,Next0,Next2,NextLocal,Next2Local,Pull,Pull2,Stop,Stop0.Next0/Stop0are used when the range variable is blank or absent.NextLocal/Next2Localare fast paths for local-scope iterators (single and double variable forms). - Super instructions (fused multi-op sequences):
GetLocal2-- push two locals in one dispatch.GetLocalAddIntImm,GetLocalSubIntImm,GetLocalMulIntImm-- load local + arithmetic with immediate.GetLocalLowerIntImm,GetLocalLowerUintImm,GetLocalGreaterIntImm,GetLocalGreaterUintImm-- load local + compare with immediate.GetLocalReturn-- load local + return.LowerIntImmJumpFalse,LowerIntImmJumpTrue-- compare with immediate + conditional jump (no boolean materialized on stack).GetLocalLowerIntImmJumpFalse,GetLocalLowerIntImmJumpTrue-- triple fusion: load local + compare + jump.BpackslocalOff<<16 | imm&0xFFFF.
- Exceptions:
Panic,Recover,DeferPush,DeferRet. - Debug:
Trap.
- Stack/control:
Internal design
Memory layout
globals[0 .. dataLen-1] global vars, func code addresses, string literals
(shared backing array across all goroutines)
mem[0 ..] per-goroutine call stack (frame-relative indices only)
The two slices were previously unified in a single mem. They were split so
that goroutines can share globals while running independent stacks.
Push() appends to globals; GetGlobal/Set (global scope) index into
globals; GetLocal/Set (local scope) index into mem relative to fp.
Call frame
[ ... args | deferHead | retIP | prevFP | locals ... ]
^
fp (frameOverhead = 3 slots)
Call pushes deferHead, retIP, and prevFP onto the stack, then sets
fp past all three. If the caller has a non-nil closure heap, it is
saved to Machine.heapFrames and the high bit of prevFP is set
(heapSavedFlag). Return inspects deferHead (at mem[fp-3]) for
pending deferred calls, unpacks nret/frameBase from retIP, and
restores heap from heapFrames if the heap flag is set.
mem[fp-3]--deferHead: index of the topmost deferred-call record (0 = none). Updated byDeferPush.mem[fp-2]-- packedretIP:[frameBase:16 | nret:16 | retIP:32]. Encodes the return address, number of return values, and frame size (distance from fp to bottom of frame) in a singleuint64.packRetIP(retIP, nret, frameBase)constructs the value.mem[fp-1]--prevFP: the caller's frame pointer. High bit (heapSavedFlag = 1<<63) indicates a closure heap was saved toheapFrames.
CallImm is a specialized variant for direct calls to known functions.
It avoids loading the function value from memory and skips runtime type
dispatch. A holds the data index of the function; B packs
narg<<16 | nret.
Per-type numeric ops
All arithmetic opcodes are statically typed -- there are no generic
Add/Sub/Mul/Neg/Greater/Lower opcodes. The compiler always
knows the operand kind at compile time and selects the exact opcode from
the 12-variant block using NumKindOffset:
opcode = baseOp + Op(NumKindOffset[reflect.Kind])
NumKindOffset is a fixed array (indexed by reflect.Kind) that maps
each numeric kind to a 0-based slot: 0=int, 1=int8, ..., 9=uint64,
10=float32, 11=float64. Non-numeric kinds return -1 (the compiler panics
before reaching that state).
String concatenation (s1 + s2) is handled by the dedicated AddStr
opcode rather than a typed numeric block.
The helper functions add[T], sub[T], etc. in numops.go use Go
generics internally; each typed opcode dispatches to exactly one
instantiation with zero runtime branching.
Float32 values are stored as math.Float64bits(float64(f32value)) --
the float32 is promoted to float64 before encoding into uint64. The
helpers getf32(n uint64) float32 and putf32(f float32) uint64 in
numops.go encapsulate this encoding for the float math opcodes.
Instruction encoding
Instruction is a fixed-size 16-byte struct:
type Instruction struct {
Op Op // opcode
A, B int32 // up to 2 immediate operands (0 when unused)
Pos Pos // source position
}
Earlier versions used {Op Op; Arg []int}, which heap-allocated an arg
slice per instruction. The flat layout avoids allocation and improves
cache locality in the dispatch loop. Super instructions that need more
than two operands pack them into A and B (e.g. GetLocalLowerIntImmJumpFalse
packs localOff<<16 | imm&0xFFFF into B).
Get specialization
Get dispatches on a scope flag (Global vs Local) at runtime.
Two specialized opcodes avoid this branch:
GetLocal-- readsmem[A + fp - 1]directly.GetGlobal-- readsmem[A]and syncsnumfromreffor addressable slots (needed whenSetSupdatedrefwithout touchingnum).
The compiler emits GetLocal/GetGlobal whenever the scope is known
statically. Get remains for cases requiring runtime scope resolution.
Super instructions
The compiler fuses common multi-instruction sequences into single opcodes that perform multiple operations in one dispatch cycle. Three levels of fusion exist:
Level 1 -- GetLocal + operation:
| Opcode | Equivalent sequence |
|---|---|
GetLocal2 | GetLocal A; GetLocal B |
GetLocalAddIntImm | GetLocal A; Push B; AddInt |
GetLocalSubIntImm | GetLocal A; Push B; SubInt |
GetLocalMulIntImm | GetLocal A; Push B; MulInt |
GetLocalLowerIntImm | GetLocal A; Push B; LowerInt |
GetLocalGreaterIntImm | GetLocal A; Push B; GreaterInt |
GetLocalReturn | GetLocal A; Return |
Level 2 -- compare + jump:
| Opcode | Effect |
|---|---|
LowerIntImmJumpFalse | if n >= B { ip += A }; sp-- |
LowerIntImmJumpTrue | if n < B { ip += A }; sp-- |
These avoid materializing a boolean on the stack. The compiler rewrites
Greater comparisons using the identity a > imm = !(a < imm+1).
Level 3 -- triple fusion (GetLocal + compare + jump):
| Opcode | Effect |
|---|---|
GetLocalLowerIntImmJumpFalse | if local >= imm { ip += A } |
GetLocalLowerIntImmJumpTrue | if local < imm { ip += A } |
B packs localOff<<16 | imm&0xFFFF. No stack operations at all --
the local is read, compared, and the branch is taken in a single
dispatch.
Stack growth pre-computation
The compiler tracks the maximum expression depth per function and stores
it in the Grow instruction's B field. At function entry, Grow
pre-allocates A + B slots (locals + max expression depth). This
guarantees that GetLocal and the fused super instructions can access
stack slots without bounds checks within the function body.
Closure dispatch
When a closure is called, Machine swaps in its Heap (saved heap cells)
and restores the caller's heap on return. HeapGet/HeapSet read/write through
heap[i] pointers.
Goroutines and channels
GoCall and GoCallImm both call newGoroutine(fval, args), which creates
a child Machine with:
globalspointing to the parent'sglobalsslice (same backing array -- writes in either direction are immediately visible to the other).- A fresh
memslice containing the function value and argument copies. - A private copy of
codewith aCall + Exitepilogue appended atbaseCodeLen, so the goroutine's entry point is a normal call sequence. wgpointing to the parent's*sync.WaitGroup.
The goroutine runs via go func() { child.Run() }(). The parent does not
wait for it; instead, Run waits for all spawned goroutines at exit via
m.wg.Wait() when !m.isGoroutine (the top-level machine only).
Channel operations delegate entirely to reflect: reflect.MakeChan,
reflect.Value.Send, reflect.Value.Recv, and reflect.Value.Close.
The channel value is stored as a Value{ref: reflect.Value} on the stack
and in variable slots like any other composite type.
GoCallImm applies the same optimization as CallImm for regular calls:
when the target is a named non-closure function, the compiler removes the
preceding GetGlobal and encodes the globals index directly in the
instruction, avoiding one stack read.
Panics, defer, recover, and diagnostics
DeferPushsaves a sentinel frame pointing to a deferred function.Panicsetspanicking = trueand unwinds, calling deferred functions.Recoverclears the panic state if called inside a deferred function.DeferRetis emitted at function exit to run deferred functions in LIFO order.
Diagnostics: PanicError. When a raw Go panic (nil deref, divide by
zero, a reflect.Convert mismatch, an explicit interpreted panic(...))
escapes the VM, recoverPanic wraps it in a *PanicError (vm/debug.go)
that carries the panic value, the source Pos and bytecode IP of the
faulting instruction, the captured call frames, and the DebugInfo
snapshot. capturePanic builds it before the stack unwinds, so
PanicError.Error() can render a header, a source snippet with a caret,
and an mvm stack: trace that interleaves interpreted frames (with
file:line:col and function name) and -- via ... [native] -- markers
for reentrant native calls. Symbolization reuses DebugInfo.FuncAt and
Sources.Snippet, so a program that never panics pays nothing.
Clean-exit passthrough. recoverPanic does a shape check rather
than a type check: any recovered value that is an error but not a
runtime.Error is propagated unwrapped, and anything implementing the
CleanExit marker interface bypasses interpreted recover() entirely
and reaches the top-level Run. This is how *interp.ExitError (from
the virtualized os.Exit / log.Fatal* bindings) surfaces as a clean
signal instead of a crash, while keeping vm free of any interp
dependency. Genuine runtime.Error crashes keep the full capturePanic
diagnostics. See
ADR-018 and
interp.md.
Nil-func guard. Function code never lives at code index 0 (that slot
holds the program-entry Jump), so a resolved call target of
nilFuncAddr = 0 means the func value was nil or a global slot was
corrupt. Call/CallImm panic with a Go nil-func deref in that case
rather than jumping to 0, which would re-run the program (or _testmain)
and recurse without bound.
Native Go interop (WrapFunc / CallFunc)
Mvm functions are integers (code addresses) or Closure values at
runtime. Neither can be stored directly in a typed Go func field via
reflect.Set. Two mechanisms bridge this gap:
-
funcFieldsside-table. AfuncFieldsTable(amap[uintptr]Valuebehind async.RWMutex) holds mvm funcs assigned to native struct func fields. Lookup is keyed by the closure's function pointer read live from the field's bytes (funcValuePtr), so areflect.Setthat overwrites the field still resolves to the right closure on the nextCall. The table is parent-owned and pointer-shared with runner Machines and spawned goroutines, so a callback's write is observable to siblings and the parent. When the compiler detects assignment of a mvm func to a native struct field, it emits an instruction that stores the mvm func into the table while writing a zero/stub into the actual field. -
WrapFuncopcode. Converts the mvm func on the stack into aMvmFuncby callingreflect.MakeFuncwith a trampoline that re-enters the VM viaMachine.CallFunc. The resultingGFreflect.Valueis assignable to any Go func field of the matching type.
Re-entrant execution (CallFunc)
CallFunc provides re-entrant VM execution for native Go callbacks. It
saves all volatile state (mem, ip, fp, heap, heapFrames, panic
state, code length), resets per-call state, copies globals to a fresh
stack, pushes the function value and arguments, appends a temporary
Call + Exit sequence, and runs the inner loop. On return (including
via defer), all saved state is restored.
Native callbacks dispatched from multiple goroutines (e.g. sort.Slice
on a wrapped mvm comparator, parallel strings.Map, fmt verb callbacks)
are safe: each dispatch acquires its own runner Machine from the
parent's sync.Pool and the user-invisible internal tables
(funcFields, typesByRtype) are parent-owned and synchronised so a
runner's mutation is visible to siblings and the parent.
Direct invocation of m.CallFunc on the parent Machine itself remains
single-goroutine entry (it mutates m.mem/m.ip/m.fp directly); the
spec-safe public concurrent entry is WrapFunc -> trampoline -> pooled
runner.
Note that CallFunc does NOT isolate globals: a callback's package-var
write is visible to the outer Run, matching Go callback semantics
(a native callback runs in the same address space as the surrounding
program) and the goroutine model documented in ADR-008. Only per-frame
state (mem, ip, fp, heap, heapFrames, panicking, panicVal,
appended code) is saved and restored. Concurrent user-code writes to
package-level vars follow the Go memory model exactly as in native Go;
mvm does not layer extra locking over globals.
makeCallFunc (the trampoline returned by WrapFunc) acquires a
pooled runner Machine via runnerState.acquireRunner, runs the
function with callPooled, and releases the runner back to the pool on
return. The runnerState captured at WrapFunc time carries
pointer-shared references to the parent's funcFields, typesByRtype,
code / globals headers, the sync.Pool itself, and debugInfoFn --
so bridges that introspect the callback runner (e.g. the
runtime.Callers replacement, see Runtime virtualization
bridges) can still resolve symbols
through m.DebugInfo().
Trap and interactive debug mode
The Trap opcode pauses VM execution and enters an interactive debug
session. It is emitted by the compiler for calls to the trap() builtin.
Sentinel opcodes. Out-of-band control flow (defer return, panic unwind,
debug trap) is handled via sentinel opcodes (DeferRet, PanicUnwind)
appended to the code array at Run entry. The Trap opcode handles debug
entry inline: it saves the resume address in trapOrig, syncs Machine
state, and calls enterDebug(). When the user types cont, enterDebug
returns and execution resumes from Machine.ip.
Debug fields on Machine:
debugInfoFn func() *DebugInfo-- builder registered by the interpreter. Called on demand insideenterDebugto produce symbolic info (labels, globals, locals, source registry). Not cached on Machine.debugIn/debugOut-- I/O overrides for the debug REPL (default: stdin/stderr). Tests inject buffers here.trapOrig int-- the ip to resume after the debug session ends.
Debug REPL commands:
| Command | Action |
|---|---|
bt, stack | Dump the full call stack with frame layout and symbol names |
c, cont | Continue execution |
h, help | Show available commands |
DebugInfo (vm/debug.go) holds symbolic metadata populated by
comp.Compiler.BuildDebugInfo(): a scan.Sources registry for
multi-file/REPL position resolution, label-to-name mappings,
global-index-to-name mappings, and per-function local variable lists.
DumpFrame and DumpCallStack use this information to annotate memory
slots with human-readable names and source positions.
Machine.DebugInfo() returns the result of debugInfoFn (or nil) and
is the entry point used by native bridges that need symbolic info.
DumpCallStack walks the frame-pointer chain directly today; the same
walk has been factored into the public WalkCallStack iterator (see
Call-stack walking) for reuse by bridges and the
planned profiler. A future cleanup can fold DumpCallStack onto that
iterator once StackFrame exposes the per-slot annotation state
(narg, nret, funcAddr).
Call-stack walking
Machine.WalkCallStack(yield) (vm/debug.go) is the public iterator
over the live call stack. It yields StackFrame{IP, Pos} from
innermost (the currently running function) to outermost; the first
frame's IP is m.ip-1 (the just-executed instruction), each
subsequent frame's IP is retIP-1 taken from the inner frame's
mem[fp-2] slot -- the call instruction in the caller, whose Pos
points at the source line of the call. yield returns false to stop
early.
Symbolization is not the iterator's job; callers compose with:
DebugInfo.FuncAt(ip)-- linear scan ofLabelsreturning the function whose code address is the largest<= ip.DebugInfo.Sources.Resolve(int(Pos))-- (file, line, col) for a byte offset.
Splitting walk from symbolize matters for the planned sampling
profiler, which must produce IPs cheaply on a tick and resolve only at
profile-export time. Today's consumers are the runtime.Callers
bridge (see Runtime virtualization bridges)
and (partially) DumpCallStack.
Native method dispatch via synthesized rtypes
Go's reflect.StructOf cannot attach methods to dynamically-created types, so
an interpreted struct (or named type) passed to a native function as an
interface{} would not satisfy fmt.Stringer, error, json.Marshaler, etc.
The VM closes this gap by attaching the interpreted methods to a synthesized
rtype that native itab/reflect dispatch reads directly -- no per-call wrapper.
Machine.AttachSynthMethods(t) (vm/synth_bridge.go) runs for every compiled
type. For each method it:
- matches the signature to a shape (
detectShape->stubs.ShapeS1..S38, including the stdlib-specific shapes registered viavm.RegisterExtendedShapes), falling back to a word-class shape (detectWordShape, ADR-022) when no typed shape fits; - builds a handler closure (the generic
makeHandlerS*, an extended handler fromstdlib/synth_method_shapes.go, ormakeWordCorefor the word path) that re-enters the interpreter viaCallFuncwhen the stub fires; - delegates to
stdlib/stubs, which resolves each method to a stub-pool slot PC and fills them into the type's reserved synth rtype in place.
The type's synth identity was reserved at materialize (maybeReserve /
maybeReserveStruct in derive/derive.go), so the fill is in place and no rtype swap
or cascade is needed -- a composite that captured the reserved rtype sees the
methods afterward. See runtype, stubs, and
ADR-021. This replaced the former
per-call bridge registries and argument proxies -- vm/bridge.go no longer holds
Bridges/DisplayBridges/CompositeBridges/InterfaceBridges.
IfaceWrap / bridgeArgs. The compiler still emits IfaceWrap for
interface-typed arguments to native calls, boxing the value as Iface{Typ, Val}
so the mvm *Type identity crosses the boundary (essential for non-struct named
types whose reflect.Type is shared with the underlying type). bridgeArgs(in, funcType, fn) then, per argument: retypes a pointer-to-interpreted-interface to
the synthesized interface rtype (bridgePtrToIface, a gated allowlist, for
errors.As), otherwise unwraps the Iface to its concrete (synthesized) value
via bridgeIface.
Pointer-receiver method dispatch. IfaceCall resolves the method set via
Type.ResolveMethodType, which walks to ElemType.Methods for pointer types,
so methods on T are visible when the concrete value is *T;
runtype.ReservePtrMethods mirrors this on the runtype side by wiring *T's
PtrToThis.
MethodNames []string on Machine maps global method IDs back to names
(populated by the compiler after each Compile); the dispatch handlers use it
to call methods promoted from embedded interfaces.
Native-receiver hook. RegisterNativeMethodHook(recvInstance, name, hook)
(vm/bridge.go) substitutes the result of a named method on a native
receiver. The runtime virtualization bridge uses it for
(*runtime.Func).Name/FileLine -- see Runtime virtualization
bridges.
Runtime virtualization bridges
Some host APIs round-trip opaque pointers (PCs, handles) through user
code and back again. runtime.Callers / runtime.FuncForPC is the
canonical case: pkg/errors stores PCs in Frame uintptr and later
calls (*runtime.Func).Name() / FileLine() to format them. A
plain reflect passthrough makes those calls return host frames inside
vm.(*Machine).Run rather than the interpreted call stack.
The bridge lives in stdlib/runtime_virt.go (registered via
stdlib.RegisterPackagePatcher("runtime", ...)) plus stdlib/runtime_func.go
(the *runtime.Func sentinel registry + method shim). The vm side is the
active-machine slot (vm/active_machine.go, core) and the
RegisterMethodValueShim seam (vm/bridge.go) the shim plugs into.
1. Active-machine slot. Bridges run as Go functions called via
reflect.Call; they need to find the live Machine. Run threads the
current value through:
prev := SetActiveMachine(m)
defer SetActiveMachine(prev)
activeMachine is an atomic.Pointer[Machine], single-machine-at-a-
time semantics. Concurrent goroutines racing on this slot is no worse
than under a mutex+stack alternative because the slot is a single
pointer; per-goroutine GLS would require unsafe-G inspection or
runtime/pprof labels.
2. Synthetic PC sentinels. An mvm-virtual PC is the address of a
freshly allocated *runtime.Func. Two non-obvious bits:
runtime.Funcis a zero-sized struct; Go reuses one canonical address for all zero-size allocations.runtimeFuncSentinelwraps it with a padding byte so eachNewRuntimeFuncSentinel()returns a unique pointer.RegisterRuntimeFunc(rf, name, file, line)storesRuntimeFuncInfo{...}in a sidesync.Map;LookupRuntimeFunc(rf)returns nil for non-mvm pointers so a host PC can fall through.
3. Method intercept. nativeMethodLookup (top of the
MethodByName path) calls tryMethodValueShim(rv, name) first, which
dispatches by receiver rtype to a shim registered via
RegisterMethodValueShim -- stdlib registers one for *runtime.Func.
On a hit (pointer is in the side table, name is "Name" or "FileLine")
the shim returns a reflect.MakeFunc closure with the matching signature;
the host method never runs. On a miss it returns the zero reflect.Value
and the regular lookup proceeds. Cost on the miss path is one map lookup.
The VM also synchronizes its execution locals into the Machine
struct just before the native rv.Call(in):
m.mem, m.fp, m.ip = mem, fp, ip+1
This is a one-way push; the local copies remain authoritative. Bridges
that introspect via m.WalkCallStack, m.fp, m.ip, etc. need this
because Run keeps those fields stale until exit otherwise.
sequenceDiagram
participant User as Interpreted code
participant VM
participant Bridge as stdlib/runtime_virt
participant Walk as WalkCallStack
participant Side as Side table
User->>VM: runtime.Callers(skip, pcs)
VM->>Bridge: rv.Call (after sync mem/fp/ip)
Bridge->>Walk: m.WalkCallStack(yield)
Walk-->>Bridge: StackFrame{IP, Pos} per frame
Bridge->>Side: NewRuntimeFuncSentinel + Register
Bridge-->>User: pcs[i] = uintptr(sentinel) + 1
User->>VM: fn := runtime.FuncForPC(pc)
VM->>Bridge: rv.Call (mvmFuncForPC)
Bridge->>Side: LookupRuntimeFunc(pc-1)
Bridge-->>User: *runtime.Func sentinel
User->>VM: fn.Name() / fn.FileLine(pc)
VM->>VM: nativeMethodLookup -> runtimeFuncShim
VM-->>User: shim returns metadata from side table
Limitations:
- Sentinels are interned by
(IP, Pos)instdlib/runtime_virt.go'ssentinelByFramemap, soruntimeFuncMetasize is bounded by the number of distinct interpreted call sites (typically thousands) rather than the number of stack captures. Long-running embedders that recompile the program many times still grow the map across recompiles -- nothing sweeps the cache automatically. A publicClearwould be straightforward to add when needed. - Single-machine-at-a-time semantics leaks under truly concurrent goroutine execution.
See ADR-016.
Dependencies
scan-- forscan.Sources(source position registry used byDebugInfo).runtype-- rtype synthesis + derive helpers used byderive/andvm/synth_bridge.go(see runtype).stdlib/stubs-- the method-shape catalog andAttach*wrappers used byvm/synth_bridge.go(see stubs).stubsis a leaf package (it imports onlyruntype), so this introduces no cycle.