Skip to main content

1638. gopy stackref

What we are porting

Python/stackrefs.c (~250 lines) plus the inline declarations in Include/internal/pycore_stackref.h. The stackref system is CPython's compact representation for stack values that distinguishes "strong reference" from "borrowed reference" without spending a refcount op per push. It is the foundation the eval loop's stack operations are written against.

A stackref is a tagged pointer: the low bit (or a deferred-refcount bit on free-threading) marks whether the eval loop owns a reference or borrowed it. Tier-1 and Tier-2 dispatch arms are written entirely in stackref terms; the conversion to PyObject* happens at API boundaries (CALL into builtin, raise an exception, etc.).

Why this matters for v0.6

Even though gopy uses Go's GC and refcount ops are largely no-ops, the stackref shape stays. Two reasons:

  1. The bytecodes DSL refers to PyStackRef_* macros directly (PyStackRef_AsPyObjectBorrow, PyStackRef_DUP, ...). The generator (1621) translates those calls to Go, and they need to land somewhere typed.
  2. The free-threaded build (v0.14) reuses the same stackref bits for biased refcounting. Keeping the wrapper now means v0.14 adds bits, not concept.

In v0.6, stackref is structurally a wrapper around object.Object with two helpers (AsObject, New) and four no-op refcount ops. The bit manipulation lands in v0.14.

Go shape

// Ref is a tagged stack value. Mirrors _PyStackRef from
// Include/internal/pycore_stackref.h.
//
// In the GIL build, Ref is structurally a pointer with no tag
// bits. The wrapper exists so the eval loop matches CPython's
// stackref vocabulary, and so v0.14 can swap in the biased-refcount
// representation without touching the dispatch arms.
type Ref struct {
bits uintptr
}

// FromObject wraps a strong reference. Mirrors
// PyStackRef_FromPyObjectSteal.
func FromObject(o object.Object) Ref

// FromObjectNew wraps a new strong reference (incrementing refcount).
// Mirrors PyStackRef_FromPyObjectNew.
func FromObjectNew(o object.Object) Ref

// AsObject extracts the object pointer. Mirrors
// PyStackRef_AsPyObjectBorrow. The caller must not retain past
// the lifetime of the stackref.
func (r Ref) AsObject() object.Object

// AsObjectSteal extracts the object pointer and consumes the
// stackref. Mirrors PyStackRef_AsPyObjectSteal.
func (r Ref) AsObjectSteal() object.Object

// Dup returns a duplicate strong reference. Mirrors
// PyStackRef_DUP.
func (r Ref) Dup() Ref

// IsNull reports whether the ref is the sentinel null. Mirrors
// PyStackRef_IsNull.
func (r Ref) IsNull() bool

// Null is the sentinel for absent values (uninitialized fast
// locals, popped stack slots after clear). Mirrors
// PyStackRef_NULL.
var Null = Ref{}

Sentinels

CPython defines a small set of sentinel stackrefs:

  • PyStackRef_NULL: absent value. Used for unbound fast locals and cleared stack slots.
  • PyStackRef_None, PyStackRef_True, PyStackRef_False: pre-allocated singletons. The eval loop uses these for LOAD_CONST None / True / False to skip a refcount bump.
var (
None Ref // wraps object.None
True Ref // wraps object.True
False Ref // wraps object.False
)

These are populated at runtime init from the global object singletons (Objects spec 1675).

Conversion at API boundaries

CALL, IMPORT_NAME, RAISE_VARARGS, and a handful of other opcodes hand a stackref off to a function written against object.Object. The conversion shim is one of:

  • r.AsObject() for borrowed: caller does not consume.
  • r.AsObjectSteal() for steal: caller consumes the ref.
  • FromObject(o) for return values that come back as object.Object.

The eval loop balances these: every steal at the top is matched by either a FromObject of the return value or a frame teardown that clears the popped slots.

File mapping

C sourceGo target
Python/stackrefs.cvm/stackref/stackref.go
Include/internal/pycore_stackref.h (struct, macros)vm/stackref/stackref.go
Python/stackrefs.c (deferred refcount)vm/stackref/deferred.go (v0.14)

Checklist

Status legend: [x] shipped, [ ] pending, [~] partial / scaffold, [n] deferred / not in scope this phase.

Files

  • stackref/stackref.go: Ref value, FromObject, FromObjectNew, FromObjectImmortal, AsObject, AsObjectSteal, Dup, Close, IsNull, Null. (Path: flat layout per project convention; spec originally said vm/stackref/.)
  • stackref/sentinel.go: None, True, False pre-allocated stackrefs, populated at init from objects.None() / True() / False().
  • stackref/stackref_test.go: round-trip panel (FromObject then AsObject), null sentinel, dup semantics.

Surface guarantees

  • Ref does not grow beyond one Go interface header (typeptr + dataptr; 16 bytes on 64-bit). CPython's _PyStackRef is one tagged uintptr; gopy stores objects.Object directly so the equivalent floor is one interface header. Pinned by TestRefSize. v0.14 can swap in a packed representation without touching dispatch arms.
  • FromObject(o).AsObject() == o for every non-nil object. Pinned by TestFromObjectRoundTrip plus TestFromObjectNewAndImmortal.
  • Null.IsNull() == true; every other sentinel returns false. Pinned by TestNullSentinel and TestSentinels.
  • None.AsObject() == object.None and the same for True, False. Pinned by TestSentinels.
  • Dup produces a stackref the eval loop can drop without affecting the original. Pinned by TestDupIndependent.

Out of scope for v0.6

  • Tagged-bit deferred refcount (free-threaded build). Lives in v0.14.
  • _PyStackRef_FromPyObjectImmortal: the immortal-object path is no-op in gopy (Go's GC handles immortality differently). The helper exists as an alias for FromObject in v0.6.

Cross-references

  • Eval loop that reads and writes stackrefs: 1636.
  • Frame storage backed by []Ref: 1637.
  • Object singletons (None / True / False): 1675.
  • Free-threaded refcounting: 1614 (brc) and v0.14 free-threading.