1636. gopy eval loop
What we are porting
Two files, ~10k lines total:
Python/ceval.c(~9000 lines): the Tier-1 eval loop. Entry, dispatch, exception unwind, frame push/pop on call and return, generator resume, eval breaker poll points, and the small set of hand-written opcodes that do not live inbytecodes.c(RESUME, ENTER_EXECUTOR, INSTRUMENTED_*).Python/ceval_macros.h(~700 lines): macros the generated dispatch arms expand against. STACK_GROW, GETLOCAL, SETLOCAL, TARGET, DISPATCH, JUMPBY, INSTRUCTION_SIZE, the inline-cache walk macros, deopt machinery.
The generated dispatch arms themselves live in vm/opcodes_gen.go
(spec 1621). This spec covers everything around them: the loop
that calls them, the helpers they expand against, and the unwind
logic when one of them returns an error.
Go shape
Top-level entry in vm/eval.go:
// Eval runs frame f to completion under thread state ts and
// returns the value the frame produced (RETURN_VALUE) or the
// error that escaped (uncaught exception).
//
// Mirrors _PyEval_EvalFrameDefault from Python/ceval.c.
func Eval(ts *state.Thread, f *frame.Frame) (object.Object, error)
// EvalCode is the convenience wrapper that builds a frame from a
// code object plus globals/locals and calls Eval. Mirrors
// PyEval_EvalCode.
func EvalCode(ts *state.Thread, co *object.Code, globals, locals object.Object) (object.Object, error)
The eval state is a goroutine-local struct that survives the whole call:
// evalState is the per-call state the dispatch arms read and write.
// Mirrors the locals at the top of _PyEval_EvalFrameDefault.
type evalState struct {
ts *state.Thread
f *frame.Frame // current frame
pc int // instruction offset within f.code
stack []stackref.Ref
sp int // stack pointer
fastlocals []stackref.Ref // f.localsplus alias
err *errors.Exception
breaker uint32 // eval-breaker shadow read each DISPATCH
}
Dispatch shape
CPython uses computed gotos when the compiler supports them and falls back to a switch otherwise. Go has no computed goto; we use a switch with a function-call indirection only for the rare adaptive paths. The hot loop is:
for {
inst := e.f.code.Instructions[e.pc]
op := opcode.Op(inst >> 8)
oparg := uint32(inst & 0xff)
if e.breaker != 0 {
if err := e.handleEvalBreaker(); err != nil {
return e.unwind(err)
}
}
next, err := e.dispatch(op, oparg)
if err != nil {
if v, handled := e.handleException(err); handled {
continue
} else {
return v, err
}
}
e.pc = next
}
dispatch is the generated function from 1621.
Exception unwind
Python/ceval.c walks the code object's exception table to find a
handler when an opcode raises. The walk format is PEP 657 and was
already ported in 1628 (assemble) for the writer side. The reader
side ports here:
// handleException walks the exception table starting at e.pc to
// find a handler. Returns (recovered_pc, true) on hit and
// (zero, false) on miss. Mirrors _PyFrame_GetExceptionTableHandler.
func (e *evalState) handleException(err error) (object.Object, bool)
The exception table walk is a varint decode against
co.ExceptionTable. Same algorithm CPython uses; the byte format
is what 1628 emits.
Eval breaker
CPython's eval breaker is a bitmask of pending things the eval loop must react to: signals, GIL drop requests, async exceptions, GC requests, profiler attach, monitoring tool installs.
Detail lives in 1639. The eval-loop side just polls the bit and
calls into vm/gil.go when it is set.
Special opcodes (hand-written)
A small set of opcodes are not in bytecodes.c. They live in
vm/eval.go directly:
RESUME: re-enter a frame after a yield. Readsf.PrevInstr, advances pastRESUME, polls eval breaker.INSTRUMENTED_RESUME,INSTRUMENTED_*: monitoring hooks (PEP 669). For v0.6 these reduce to their non-instrumented base case.ENTER_EXECUTOR: Tier-2 entry. For v0.6 this is an unconditional fall-through (the executor table is empty).
File mapping
| C source | Go target |
|---|---|
Python/ceval.c | vm/eval.go |
vm/eval_unwind.go (exception table walk) | |
vm/eval_call.go (CALL trampoline, frame push) | |
vm/eval_resume.go (RESUME, generator re-entry) | |
Python/ceval_macros.h | vm/dispatch.go (DISPATCH, JUMPBY, GETLOCAL...) |
Python/ceval.c (helpers) | vm/eval_helpers.go (deoptHere, stack push/pop) |
Checklist
Status legend: [x] shipped, [ ] pending, [~] partial / scaffold,
[n] deferred / not in scope this phase.
Files
-
vm/eval.go:Eval,EvalCode, the dispatch loop driver,evalStatestruct, fetch with EXTENDED_ARG accumulation, push/pop/peek shortcuts, advance/jumpBy, local helpers. -
vm/eval_unwind.go: PEP 657 exception-table walk inhandleException(drives stack truncation + handler jump).unwind/handleEvalBreakerstill placeholder for #161. -
vm/exctable.go:readExcVarint+findExcHandler, round-tripped againstcompile/assemble_exceptions.goinvm/exctable_test.go. End-to-end parity panel invm/exception_table_parity_test.goruns curated handler-range fixtures throughcompile.AssembleExceptionTable, then walks every byte offset to assert the reader reproduces the writer's (start, end, target, depth, lasti) tuples. - [~]
vm/eval_call.go: CALL trampoline, frame push on call, frame pop on return. Wiresobjects.FunctionType.Callto push a frame, bind positional+keyword args, fill defaults, and re-enter Eval. Vectorcall plumbing waits on the abstract layer (#161). - [~]
vm/eval_resume.go: RESUME with eval-breaker poll. Generator re-entry / RETURN_GENERATOR blocked on objects.Generator (1687). -
vm/dispatch.go: dispatch driver returning typed notImplemented{op} until B2-B5 generates the real arms. -
vm/threadstate.go: per-Thread VM state (breaker, frame stack, pending-call queue) keyed by *state.Thread. -
vm/eval_helpers.go:incref,decref,newref,decrefInputs(Go-GC no-ops), plusiterToSlice, slice helpers, intrinsics dispatch tables. (B6) - [~]
vm/eval_test.go: surface tests for ErrNotImplemented wrap, opcode-name in error message, EXTENDED_ARG fetch, lazy threadstate init, plus per-arm smoke tests (CALL builtin, MAKE_FUNCTION + Call, UNPACK_SEQUENCE, LIST_APPEND, CALL_INTRINSIC_1 list-to-tuple, CALL_KW, BINARY_SLICE). Wider coverage comes after B2-B5 generates arms.
Opcode panel (Tier-1, unspecialized)
Each row flips to [x] once its switch arm in vm/opcodes_gen.go
runs without panicking and vm/eval_test.go covers the happy
path. Adaptive variants (*_INT, *_STR, ...) inherit their
parent row.
- Stack ops:
NOP,RESUME,POP_TOP,PUSH_NULL,COPY,SWAP,POP_BLOCK,POP_ITER,NOT_TAKEN,TO_BOOL. - Constant / fast load:
LOAD_CONST,LOAD_SMALL_INT,LOAD_FAST,LOAD_FAST_BORROW,LOAD_FAST_CHECK,LOAD_FAST_AND_CLEAR,LOAD_FAST_LOAD_FAST,LOAD_FAST_BORROW_LOAD_FAST_BORROW,STORE_FAST,STORE_FAST_LOAD_FAST,STORE_FAST_STORE_FAST,DELETE_FAST. (RETURN_VALUElives here too for the v0.6 starter.) - Global / name:
LOAD_GLOBAL,LOAD_NAME,STORE_GLOBAL,STORE_NAME,DELETE_GLOBAL,DELETE_NAME. - Closure / cell:
LOAD_DEREF,STORE_DEREF,DELETE_DEREF,LOAD_FROM_DICT_OR_DEREF,MAKE_CELL,COPY_FREE_VARS. - [~] Attribute / subscript:
STORE_SUBSCR,DELETE_SUBSCRshipped;BINARY_OP NB_SUBSCRroutes throughgetItemagainstMappingMethods.GetItem/SequenceMethods.GetItem.LOAD_ATTR,STORE_ATTR,DELETE_ATTRroute throughobjects.GetAttr/SetAttr/DelAttr(which dispatch throughtp_getattro/tp_setattro).LOAD_SUPER_ATTR,LOAD_SPECIALstill wait on the descriptor / method-resolution work in 1685. - [~] Arithmetic:
BINARY_OPcovers Add, Sub, Mul, TrueDivide, FloorDivide, Remainder, Power, And, Or, Xor, Lshift, Rshift, and the Subscr sub-op; the matching inplace forms (NB_INPLACE_*) reuse the non-inplace slot since Int is immutable. MatMul still returns TypeError pending its own slot.UNARY_NEGATIVE,UNARY_NOT,UNARY_INVERT,COMPARE_OP,IS_OP,CONTAINS_OP(sequence-aware, falls back to iterator walk). Floor and modulo use Python sign-of-divisor semantics; true divide always returns float;numericForwardhonors theNotImplementedsentinel so an int + float pair falls through to the float slot. - [~] Iteration:
GET_ITER,FOR_ITER,END_FORshipped.GET_AITER,GET_ANEXTdefer to async (#165). - [~] Containers:
BUILD_LIST,BUILD_TUPLE,BUILD_MAP,BUILD_STRING,LIST_APPEND,LIST_EXTEND,MAP_ADD,DICT_UPDATE,DICT_MERGE.BUILD_SETreturns a TypeError pending the set port (1681).LIST_TO_TUPLElands via the intrinsic dispatch (CALL_INTRINSIC_1). - Unpacking:
UNPACK_SEQUENCE,UNPACK_EX. - Slicing:
BINARY_SLICE,STORE_SLICE,BUILD_SLICE. - Control flow:
JUMP_FORWARD,JUMP_BACKWARD,JUMP,JUMP_NO_INTERRUPT,POP_JUMP_IF_TRUE,POP_JUMP_IF_FALSE,POP_JUMP_IF_NONE,POP_JUMP_IF_NOT_NONE,JUMP_BACKWARD_NO_INTERRUPT,RETURN_VALUE. (RETURN_GENERATORis held back for #161 alongside the CALL trampoline;RETURN_CONSTwas removed in 3.14.) - Calls:
CALL,CALL_KW,CALL_FUNCTION_EX,MAKE_FUNCTION,SET_FUNCTION_ATTRIBUTE.KW_NAMESis folded intoCALL_KWin 3.14 (the kwnames tuple ships on the stack). - [~] Generator / coroutine:
END_SENDshipped (drops the receiver and forwards the value, matching theyield fromcleanup contract).YIELD_VALUE,SEND,GET_AWAITABLE,GET_YIELD_FROM_ITER,RETURN_GENERATORblock on objects.Generator (#165). - Async:
BEFORE_ASYNC_WITH,BEFORE_WITH,WITH_EXCEPT_START,CLEANUP_THROW,LOAD_SPECIAL. (#165) - [~] Exception handling:
RAISE_VARARGS,RERAISE,PUSH_EXC_INFO,POP_EXCEPT,CHECK_EXC_MATCHshipped.CHECK_EG_MATCHandLOAD_ASSERTION_ERRORwait on the exception module port (1686). The unwind uses Go errors as exception values until the exception class hierarchy lands. - [~] Class / type:
GET_LEN,EXIT_INIT_CHECKshipped (the latter raises TypeError when__init__returns non-None, matching CPython).LOAD_BUILD_CLASS,MATCH_CLASS,MATCH_MAPPING,MATCH_SEQUENCE,MATCH_KEYSdefer to the class / pattern-matching ports. - f-strings:
FORMAT_SIMPLE,FORMAT_WITH_SPEC,CONVERT_VALUE,BUILD_STRING. Empty spec routes throughStr; non-empty spec waits onPyObject_Format.FORMAT_VALUEwas retired in 3.14. - Imports:
IMPORT_NAME,IMPORT_FROM,IMPORT_STAR. Defer to the import system port (1683). - [~] Type alias / TypeVar / PEP 695:
INTERPRETER_EXIT,LOAD_CLOSURE,LOAD_LOCALS,LOAD_FROM_DICT_OR_GLOBALS,LOAD_FROM_DICT_OR_DEREF,LOAD_COMMON_CONSTANTshipped.TYPE_ALIAS,INSTRUMENTED_LOAD_SUPER_ATTRreach through the intrinsics table; bodies ship in 1689. - Intrinsic dispatch:
CALL_INTRINSIC_1,CALL_INTRINSIC_2routed throughintrinsics.UnaryTable/BinaryTable. Most helpers stay stubs until their owning blocks land;UnaryListToTupleis wired.
Surface guarantees
-
EvalCode(ts, co, globals, nil)runs hand-built bytecode end-to-end through the v0.6 release gate (gopy -c "print(1+2)"prints3). Pinned byvmtest/smoke_test.go::TestSmokeReleaseV06and thevmtest/gate_test.gopanel (Constant, Arithmetic, IfElse, ListBuild, FString). The v0.5 disassembly golden corpus comparison waits on parser rule-body emission (1640). - [~] Exception table walk: round-trip parity with the writer side
pinned by
vm/exctable_test.go. The CPython golden corpus comparison still needsvm/exception_table_test.go. - [n] Generator state machine:
g = gen(); next(g); next(g); ...visits the same bytecode offsets in the same order as CPython. Defers to objects.Generator (1687) plus the pending YIELD_VALUE / SEND / RETURN_GENERATOR handlers tracked in #193. - [n] Async coroutine state machine: same shape as generator, same defer.
- Eval breaker fires at every
RESUMEand at every backwardJUMP_BACKWARD, plus the unconditional top-of-loop poll;JUMP_BACKWARD_NO_INTERRUPTskips the per-arm poll. Pinned byvm/eval_breaker_test.go(top-of-loop, JUMP_BACKWARD, RESUME, no-interrupt skip, no-bit no-drain). - Line table walk: round-trip parity with the writer pinned by
vm/positions_test.go, which builds curated*compile.Sequencefixtures, runs them throughcompile.AssembleLineTable, and walks every byte offset to assert the reader reproduces the writer's (line, endLine, column, endColumn) tuples across all five PEP 626 record formats (short, oneline, no-column, long, none) plus multi-record split spans. -
EvalCodeis goroutine-safe for distinct calls; oneevalStateis not safe for concurrent use. Pinned byTestEvalCodeGoroutineSafety(32 goroutines × shared*Code).
Out of scope for v0.6
INSTRUMENTED_*opcodes execute their non-instrumented base case. Real monitoring lands in 1634 at v0.9.ENTER_EXECUTOR: falls through. Tier-2 lands in 1632 at v0.12.- Adaptive specialization: adaptive variants reduce to base case. Lands in 1631 at v0.11.
Cross-references
- Generated dispatch table: 1621.
- Frame storage: 1637.
- Stack reference values: 1638.
- GIL and eval breaker: 1639.
- Intrinsic dispatch: 1635.
- Vectorcall protocol: 1684 (Objects block).
- Code / Frame / Generator / Cell objects: 1687 (Objects block).