1604. gopy arena spec
The arena is a bump allocator with a linked list of fixed-size blocks.
The compiler pipeline (parser, AST, symbol table, codegen) allocates
many small short-lived nodes that share a lifetime: they are all freed
together when compilation of a module finishes. CPython implements this
with pyarena.c. gopy ports the same shape into pure Go.
C reference
Source: cpython/Python/pyarena.c (207 lines). Key constants:
DEFAULT_BLOCK_SIZE = 8192
ALIGNMENT = 8
Layout (one C struct per type):
block:
ab_size total bytes the block can hand out
ab_offset bytes already handed out
ab_next linked list pointer
ab_mem pointer to the first allocatable byte
PyArena:
a_head first block, used only on free
a_cur current block; new blocks get appended past it
a_objects PyList of PyObject* to DECREF on free
Public C API (the only entry points the rest of CPython calls):
_PyArena_New create
_PyArena_Free free everything; DECREF tracked objects
_PyArena_Malloc bump-allocate `size` bytes, aligned to 8
_PyArena_AddPyObject register an object that will be DECREFed on Free
Go API
Package arena. The mapping follows the naming rules in 1601: drop the
_Py prefix, use exported Go identifiers, keep the call shape.
package arena
type Arena struct { /* unexported */ }
func New() *Arena
func (a *Arena) Malloc(n int) []byte
func (a *Arena) AddObject(obj any) error
func (a *Arena) Free()
Notes on the translation:
Mallocreturns[]byte, not a rawunsafe.Pointer. Callers that need a typed allocation will use a generic helper added in v0.5 (func NewObj[T any](a *Arena) *T). v0.1 only exposes the byte-slice form; the AST does not exist yet.AddObjecttakesanybecause gopy has noPyObjectin v0.1. The roadmap in 1603 reaches the object model in v0.2 (handover from the Objects spec series). Until then,AddObjectsimply retains a reference so the value survives untilFree.Freedrops all block buffers and the object list. Go's GC then reclaims them. There is no equivalent toPyMem_Free; clearing the pointers is enough.- Errors: the C version surfaces
PyErr_NoMemory()on allocation failure. Go'smakepanics on out-of-memory, soMallocandNewnever return nil.AddObjectreturnserroronly to mirror the C call shape (int r = PyList_Append(...)); in v0.1 it always returns nil.
Block strategy
A block is a struct { mem []byte; off int; next *block }. mem has
length cap = max(DEFAULT_BLOCK_SIZE, requestedSize). The first block
is created at New() time so a.cur is never nil.
Allocation (Malloc(n)):
- Round
nup to 8. - If
cur.off + n > len(cur.mem), allocate a new block of sizemax(DEFAULT_BLOCK_SIZE, n)and link it aftercur. - Hand out
cur.mem[off : off+n : off+n]. The capped slice prevents the caller from accidentally appending into the next allocation. - Advance
cur.off += n. - If a new block was just linked, advance
curto it.
Step 5 mirrors the C code's "Reset cur if we allocated a new block" trailer.
Alignment
ALIGNMENT = 8 matches CPython on every supported platform. The block
header in C uses _Py_ALIGN_UP(b->ab_mem, ALIGNMENT). In Go, make([]byte, n)
returns memory aligned to at least unsafe.Alignof(maxAlign_t) which is
8 on amd64/arm64, so we do not need a runtime alignment trim. We round
allocation sizes to 8 anyway for parity with CPython.
Tests
arena/arena_test.go covers:
Malloc(0)returns an empty slice, does not advanceoff.Malloc(n)for several sizes, checks alignment and non-overlap.- A request larger than
DEFAULT_BLOCK_SIZElands in its own block. - Many small allocations grow the block list.
AddObjectretains the object untilFree(verified with a finalizer that records when the value becomes unreachable).Freereleases all blocks (verified by exhausting the per-block bump pointer then re-freeing).- Concurrent
Mallocis not safe: this matches CPython, which has no internal locking on_PyArena_Malloc. The doc string says so.
Non-goals
- Per-allocation free. The arena frees in bulk; that is the point.
- Resizing an allocation. CPython has no
_PyArena_Reallocand we do not introduce one. - Concurrent access. Callers serialize externally.