Skip to main content

Include/internal/pycore_frame.h

cpython 3.14 @ ab2d84fe1023/Include/internal/pycore_frame.h

Declares _PyInterpreterFrame, the internal execution frame that the eval loop actually manipulates. The Python-visible PyFrameObject is a lazily-allocated wrapper around this struct; most of the interesting state lives here, not in PyFrameObject.

The companion file Include/internal/pycore_interpframe.h (406 lines) holds the bulk of the frame inline functions (_PyFrame_GetLocals, _PyFrame_FastToLocals, etc.). This shorter header carries only the struct definition, the owner enum, and the three most performance- critical inline helpers that the eval loop calls on every instruction.

Map

LinesSymbolRolegopy
1-20_PyFrameOwner enumFRAME_OWNED_BY_CSTACK, FRAME_OWNED_BY_THREAD, FRAME_OWNED_BY_GENERATOR, FRAME_OWNED_BY_FRAME_OBJECT.objects/frame.go
21-45_PyInterpreterFrame structAll execution frame fields: executable, globals, builtins, locals, frame_obj, prev_instr, localsplus.objects/frame.go
46-52_PyFrame_IsIncompleteTrue while RESUME has not yet run — used to detect partially-initialized frames.objects/frame.go
53-57_PyFrame_GetCodeCasts f_executable to PyCodeObject * for the common case.objects/frame.go
58-61_PyFrame_SetStackPointer / _PyFrame_GetStackPointerRead/write the stack pointer stored as an offset from localsplus.objects/frame.go

Reading

_PyInterpreterFrame fields vs PyFrameObject (lines 21 to 45)

cpython 3.14 @ ab2d84fe1023/Include/internal/pycore_frame.h#L21-45

struct _PyInterpreterFrame {
/* f_executable is the code object (common case) or a function object
when the frame is a generator/coroutine still at the first RESUME. */
PyObject *f_executable;

struct _PyInterpreterFrame *previous;
PyObject *f_globals;
PyObject *f_builtins;
PyObject *f_locals; /* may be NULL for optimised frames */
PyFrameObject *frame_obj; /* lazily created PyFrameObject wrapper */

_Py_CODEUNIT *prev_instr; /* pointer to last-executed instruction word */
int f_lasti; /* last instruction index (for tracing) */
bool f_trace_lines;
bool f_trace_opcodes;
uint8_t owner; /* one of _PyFrameOwner */

/* The eval stack and fast locals share this array. Layout:
[0 .. nlocals) fast locals
[nlocals .. ncellvars) cell vars
[ncellvars .. nfreevars) free vars
[nfreevars .. ) eval stack */
PyObject *localsplus[1];
};

The key design decision is the localsplus flexible array. Fast locals, cell variables, free variables, and the evaluation stack all live in one contiguous slab. The interpreter only needs to advance a single pointer (sp) through that slab; no separate stack allocation is required. PyCodeObject.co_stacksize tells the eval loop how many extra slots to reserve beyond the variable section.

PyFrameObject is a separate heap allocation created on demand (when Python code calls sys._getframe() or a debugger inspects the frame). It holds a borrowed reference back to the _PyInterpreterFrame. Because of this lazy creation, always test frame->frame_obj != NULL before touching PyFrameObject fields.

In gopy, objects/frame.go mirrors _PyInterpreterFrame directly. PyFrameObject is represented as a distinct FrameObject struct that wraps *Frame; it is only allocated when frame_obj is requested.

owner enum (lines 1 to 20)

cpython 3.14 @ ab2d84fe1023/Include/internal/pycore_frame.h#L1-20

typedef enum _PyFrameOwner {
FRAME_OWNED_BY_CSTACK = 0, /* allocated in C stack frame (inlined call) */
FRAME_OWNED_BY_THREAD = 1, /* heap-allocated, owned by thread state */
FRAME_OWNED_BY_GENERATOR = 2, /* owned by a generator/coroutine object */
FRAME_OWNED_BY_FRAME_OBJECT = 3, /* kept alive only by PyFrameObject */
} _PyFrameOwner;

The owner field drives deallocation. When the eval loop returns from a frame it checks owner to decide whether to free the frame immediately (FRAME_OWNED_BY_THREAD), leave it on the C stack (FRAME_OWNED_BY_CSTACK), or hand it back to the generator object (FRAME_OWNED_BY_GENERATOR). FRAME_OWNED_BY_FRAME_OBJECT means the PyFrameObject wrapper is the last reference holder; the frame is freed when the wrapper is GC'd.

In gopy, frame lifetime follows Go's GC so the owner enum is informational only. The eval loop in vm/eval_gen.go stores it for parity with CPython's debug assertions.

Inline stack-pointer helpers (lines 58 to 61)

cpython 3.14 @ ab2d84fe1023/Include/internal/pycore_frame.h#L58-61

static inline void
_PyFrame_SetStackPointer(struct _PyInterpreterFrame *frame, PyObject **sp)
{
frame->prev_instr = (_Py_CODEUNIT *)(sp); /* overloaded during suspension */
}

static inline PyObject **
_PyFrame_GetStackPointer(struct _PyInterpreterFrame *frame)
{
return (PyObject **)frame->prev_instr;
}

When a generator or coroutine suspends, CPython stores the stack pointer in prev_instr via a pointer cast. On resume, the stack pointer is recovered with _PyFrame_GetStackPointer before the eval loop overwrites prev_instr with the actual instruction pointer. This dual use of prev_instr is the reason the field is typed as _Py_CODEUNIT * even though it temporarily holds a PyObject **.

In gopy's objects/frame.go, the two helpers are Go methods on *Frame that manipulate a stackPointer int index into Localsplus.