The per-function arena model has a boundary violation: when a method mutates its
receiver (s.M[k] = v), the stored value is allocated in the method's function
arena but must survive into the caller's arena. The current mechanism records
every receiver write in a table (emitRecvWriteAppend) and runs the full codec
serialization cascade on each entry. Encoding a *TypeName follows `Pkg ->
TCPackage -> Scope -> map -> ALL TypeNames` -- O(graph) per write. Type system
initialization takes 90+ seconds.
The root cause: the function arena is temporary (pushed at entry, popped at exit), so every mutation to a receiver that outlives the call must be serialized. N writes in a loop produce N independent O(graph) cascades.
Give mutable objects their own persistent arenas. Methods borrow the receiver's arena instead of creating a new function arena. Receiver writes become local allocations within the sovereign arena -- no relocation, no codec, no write table. The receiver write table and all its supporting code are deleted.
The codec retains full recursive serialization for spawn channel wire encoding (portable across processes) and adds a relocate mode for the arena epilogue (fast, skips sovereign pointer recursion within the same address space).
Zero new annotations. Zero pragmas. The compiler infers everything from existing source.
Three arena tiers:
Root Arena (global, never freed)
|
Function Arena (per call, freed at return, Reloc* relocates returns to parent)
|
Sovereign Arena (per object, persists for object lifetime, NO relocation)
Never freed.
Three-pass relocation (RelocInit/RelocMark/RelocForward/RelocCompact/ RelocFixupAll). Released at function return.
the object (ArenaNew in constructor). Methods borrow it via SetCurrentArena
save/restore (same pattern hashmap already uses for owner-arena switching).
Persistent until the owning object's arena is released or the domain exits.
The compiler infers which types need sovereign arenas by scanning method bodies
during SSA construction. No //go:sovereign pragma.
Rule:
For each named struct T in the package:
Scan all methods with receiver *T:
If any method contains a Store instruction to a FieldAddr rooted
at the receiver parameter
AND typeNeedsCopy(T) is true
THEN T is sovereign
isReceiverAddr already exists (compiler.go:1629) and detects writes to
receiver fields. typeNeedsCopy already exists (compiler.go:3908) and detects
pointer-containing types. The inference is a loop over SSA instructions per
type -- O(methods * instructions), negligible.
A method on a sovereign type automatically gets the sovereign prologue (borrow receiver's arena) and epilogue (restore parent arena + relocate-mode codec for return values). A method on a non-sovereign type gets the existing fn arena push/pop. The developer writes normal methods, the compiler handles arena strategy.
_arena Field: Compiler-Injected, Zero-VisibilityThe _arena *runtime.Arena pointer is NOT declared in source. The compiler
injects it during IR construction:
*Arena (pointer)_arena)_arena)s._arena, no field name, no reflection entryThe compiler accesses it via hardcoded CreateStructGEP(type, val, 0, ...)
in sovereign prologues and epilogues. It is invisible to all user code.
Two modes, one codec, one buffer format:
| Mode | Use case | Sovereign pointer encoding | Portable |
|---|---|---|---|
codecModeRelocate | Arena epilogue (fast, same process) | Raw 8-byte pointer copy | No |
codecModeSerialize | Spawn channels (wire encoding) | Full recursive | Yes |
Sovereign pointer fields are written as raw 8-byte values. The decoder writes these back as-is, producing a pointer into the same sovereign arena. Correct because: the sovereign arena outlives the function call; the codec is decoded within the same process; the caller already holds a reference to the sovereign object via the return value.
No relocation passes needed for sovereign pointer fields. The encode/decode is a trivial memcpy of 8 bytes per pointer field.
All fields are fully recursively encoded, producing a self-contained byte stream decodable in a different process with an independent address space. Sovereign pointer fields are recursively encoded just like non-sovereign pointer fields. The decoder allocates new copies in the receiving process's arena.
The mode is a uint8 field on CodecBuf and codecDecodeSeen. The arena
epilogue always uses relocate mode; spawn channel encode always uses serialize
mode.
//go:arena inherit -- Unchanged, OrthogonalExisting //go:arena inherit annotations on constructors (36 occurrences in
the type system) remain as-is. They are a micro-optimization that skips
FnArenaPush/FnArenaPop for functions that allocate directly into the caller's
arena. They are not required for sovereign arena correctness and are
orthogonal to this plan.
metaSovereign type flag`src/runtime/typekind.mx` -- add sovereign detection:
const metaSovereign uint8 = 0x40
func (t *rawType) isSovereign() bool {
return t.meta & metaSovereign != 0
}
`src/runtime/codec.mx` -- add mode constants and fields:
const codecModeRelocate uint8 = 0
const codecModeSerialize uint8 = 1
Add mode uint8 to CodecBuf and codecDecodeSeen structs.
kindStruct`src/runtime/codec.mx` -- in codecEncodeValue, kindStruct case:
When typ.isSovereign() && cb.mode == codecModeRelocate:
codecTagNil for nil, codecTagPtr + raw 8 bytes for non-nil_arena field is invisible to the codec (type descriptor excludes it).It is accessed only via compiler-generated GEP(0) in prologues/epilogues.
When cb.mode == codecModeSerialize or type is non-sovereign:
Matching decode logic in codecDecodeValue, kindStruct case:
sovereign pointer fields, write directly to struct slots
`legacy/compiler/compiler.go` -- add function isSovereignType(t types.Type) bool
that checks the metaSovereign bit on the type descriptor.
`legacy/compiler/interface.go` -- during type descriptor generation for
named struct types, scan all methods with receiver *T for receiver field
writes using the SSA-level check (same logic as isReceiverAddr). If found
and typeNeedsCopy(t) is true, set metaSovereign in the metadata byte.
This pass runs first. Steps 5 and 6 (prologue/epilogue) query
isSovereignType which reads the bit set here.
`legacy/compiler/compiler.go` -- add fields to builder:
arenaBorrowed bool // true if method borrowed receiver's sovereign arena
parentArenaVal llvm.Value // alloca holding parent arena for epilogue restore
`emitArenaPrologue` -- before the FnArenaPush path, check if the receiver
type is sovereign:
if b.hasReceiver() {
recvType := b.fn.Signature.Recv().Type()
if isSovereignType(recvType) {
recvVal := b.getValue(b.fn.Params[0], getPos(b.fn))
recvLLVMType := b.getLLVMType(recvType)
arenaGEP := b.CreateStructGEP(recvLLVMType, recvVal, 0, "sov_arena_gep")
arenaVal := b.CreateLoad(b.dataPtrType, arenaGEP, "sov_arena")
parentArena := b.createRuntimeCall("CurrentArena", nil, "sov_parent")
b.parentArenaVal = parentArena
b.createRuntimeCall("SetCurrentArena", []llvm.Value{arenaVal}, "")
b.arenaActive = true
b.arenaBorrowed = true
return // skip FnArenaPush + write table init
}
}
// existing FnArenaPush + write table init follows (unchanged)
`emitArenaEpilogue` -- short path for sovereign methods:
if b.arenaBorrowed {
if len(instr.Results) == 0 {
b.createRuntimeCall("SetCurrentArena", []llvm.Value{b.parentArenaVal}, "")
b.CreateRetVoid()
return
}
if !needsCodec {
b.createRuntimeCall("SetCurrentArena", []llvm.Value{b.parentArenaVal}, "")
b.CreateRet(retVal)
return
}
// Encode return values with relocate mode. Sovereign pointer fields
// are written as raw 8-byte values. Data lives in sovereign arena.
... encode with cb.mode = codecModeRelocate ...
// Switch to parent arena before decode.
b.createRuntimeCall("SetCurrentArena", []llvm.Value{b.parentArenaVal}, "")
// Decode return values into parent arena. Sovereign pointer fields
// are read as raw 8-byte values, producing valid pointers into the
// sovereign arena.
... decode ...
// NO FnArenaPop, NO ArenaRelease (borrowed, not owned)
b.CreateRet(cur)
return
}
The SetCurrentArena save/restore pattern is the same pattern hashmap
already uses for owner-arena switching (src/runtime/hashmap.mx:323-328).
Arena switching is distinct from arena lifetime management (the Reloc*
domain). See CLAUDE.md line 32-33.
Removed from `legacy/compiler/compiler.go`:
| Lines | What |
|---|---|
| 218-220 | recvWriteTable, recvWriteCount, recvWriteMax fields in builder |
| 1464-1475 | Receiver write table initialization in emitArenaPrologue |
| 1629-1654 | isReceiverAddr -- kept (needed for inference, step 4) |
| 1656-1705 | valueIsMethodAllocated function |
| 1707-1741 | emitRecvWriteAppend function |
| 1831-1838 | Store instruction hook calling emitRecvWriteAppend |
| 4050-4058 | codecEncodeRecvWrites call in epilogue encode |
| 4101-4109 | codecDecodeRecvWrites call in epilogue decode |
Note: isReceiverAddr stays -- it's needed for sovereign type inference.
Removed from `src/runtime/codec.mx`:
| Lines | What |
|---|---|
| 815-827 | codecEncodeRecvWrites function |
| 829-843 | codecDecodeRecvWrites function |
After deleting the write table, non-sovereign types with mutating methods
would silently drop receiver writes. Add compile-time check: if a Store
instruction writes to a receiver field and:
typeNeedsCopy)...emit error: "receiver write on non-sovereign type T with pointer fields; declare T as sovereign or copy to local variable first."
This catches the error at compile time instead of producing silent data corruption. A non-sovereign type can still have methods that mutate primitive fields (no pointer escape, no codec needed). Only pointer-containing types require sovereign arenas for receiver writes.
_arena into sovereign LLVM typesDuring LLVM type construction for a sovereign struct, the compiler prepends
the _arena field:
llvmType := b.ctx.StructType(
[]llvm.Type{b.dataPtrType, userField1, userField2, ...},
false,
)
The type descriptor reports only user-visible fields. The codec enumerates
only user-visible fields. The _arena field at index 0 is accessible only
via compiler-generated GEP(0) in prologues/epilogues.
_arena initializationThe compiler detects constructor patterns and injects _arena field
initialization automatically. No source changes, no intrinsics.
Detection rule: a function returning *T where T is sovereign AND the
function body contains &T{} (or equivalent struct literal allocation).
The compiler emits, immediately after the &T{} allocation:
runtime.ArenaNew(65536)*TThe source code is completely unchanged from today. Example unchanged source:
func NewScope(parent *Scope) (s *Scope) {
s = &Scope{}
s.parent = parent
s.elems = map[string]*TypeName{}
return s
}
The compiler transforms this into the equivalent of:
func NewScope(parent *Scope) (s *Scope) {
s = &Scope{}
// injected by compiler:
// GEP(0, s) = ArenaNew(65536)
s.parent = parent
s.elems = map[string]*TypeName{}
return s
}
The ArenaNew call returns an arena pointer; the store to GEP(0) sets the
injected field. The //go:arena inherit annotation on the constructor
(if present) remains an orthogonal micro-optimization.
| Metric | Before | After |
|---|---|---|
resolveAll for minimal package | 90s+ | <1s |
Codec cascade per Scope.Insert | Full type graph | 0 (no write table) |
| Receiver write table entries | Up to 16 per method | 0 (deleted) |
| Lines of code change | - | -200 (delete) + ~150 (new) |
| Arena lifetime | Per function call | Per object (sovereign), per call (regular) |
| Spawn channel serialization | Not implemented | Full recursive (codecModeSerialize) |
| New pragmas required | - | 0 (compiler-inferred) |
metaSovereign = 0x40, isSovereign() methodcodecModeRelocate/codecModeSerialize constants, mode field to CodecBuf and codecDecodeSeen, sovereign
pointer skip logic in kindStruct encode/decode for relocate mode
named struct methods for receiver field writes, set metaSovereign
arenaBorrowed, parentArenaValfields, sovereign prologue (borrow receiver arena via GEP(0)), sovereign epilogue (restore parent arena, relocate-mode codec)
_arena *runtime.Arena atstruct index 0 in LLVM type generation for sovereign types
writes on non-sovereign pointer-containing types
(6 functions/fields, Store hook, epilogue calls)
_arena field initialization in constructors (detect &T{} where T is sovereign,
emit GEP(0) store of ArenaNew result)
src/runtime/codec_test.mx with ~32 test cases covering:- Relocate mode: sovereign struct encode/decode round-trip - Relocate mode: sovereign struct with nil pointer fields - Relocate mode: non-sovereign struct encode/decode (unchanged) - Serialize mode: sovereign struct encode/decode (full recursive) - Serialize mode: spawn channel boundaries - Write table: verify zero entries for sovereign methods
moxie build on pkg/types, then moxie test src/runtime/Full bootstrap: stage1 build -> stage1 compiles pkg/types in <2s
//go:arena inherit annotationsare kept. They are a micro-optimization (saves one fn arena push/pop per constructor call, ~microseconds). Not required for correctness. Can be eliminated later via compiler inference of the "returns pointer to local allocation" pattern.
For root-persistent objects (type descriptors), the arena lives until domain exit (munmap). For shorter-lived objects, the arena is registered in the creating function's arena cleanup list. If the constructor's return value is stored in a persistent location (detected by escape analysis), the arena is removed from the cleanup list. Otherwise, the arena is released when the fn arena is popped. Stack-like deterministic cleanup, no reference counting.
{} boundaries in method bodies can generate arena save/restore marks, allowing allocations within blocks
to be reclaimed at } unless they escape to receiver fields. This reduces
peak memory in long-lived method loops but is not required for the type
init fix. Deferred to follow-up.
&T{} patterns in functions returning *T where T is sovereign. It emits a GEP(0) store
of ArenaNew(capacity) before the rest of the constructor body. No source
changes needed. No SetStructArena intrinsic needed. The existing &T{}
pattern Just Works, with the arena silently added by the compiler.