ADR 0004 — M4 regions: built-in region types, scoped results, LIFO scratch frames
- Status: Accepted (2026-10-22, by the agent under the owner's instruction "start M4", following the pattern the owner chose in ADR 0003; open to revision by the owner)
- Scope: how M4 provides
Arena,FixedBufferAllocator,ScratchandPoolbefore generics, interfaces andunsafeexist (M6, M8).
Context
memory-model.md §3.3–3.5 and ownership.md R1–R4 describe region allocators whose allocations are loans of the region. The examples (language-spec.md Ex15, Ex16) use generic methods (arena.allocArray<u32>(256)), named arguments and allocations returned inside Result. v0 has none of: generic methods, named arguments, references inside Result (§17.1 forbids them).
Decision
- Built-in region types, as ADR 0003 did for
Box.Arena,FixedBufferAllocator,ScratchFrame,Pool<T>andPoolBox<T>are compiler-known, and their operations are built-in methods. M6 may turn them into library types with the same surface. - Scoped results. A
Result<T, E>whoseTorEis scoped (a reference, a slice, or another scoped value) is itself scoped. It follows the reference rules (§13.1): only inletlocals, parameters and return types. The borrow checker treats every local of a scoped type as holding loans, not only reference locals (lifetimes.md§3). - R1 as specified. Allocation methods take
borrow thisand return values that carry a shared loan of the region. An allocation may still be aborrow mut T: its memory is fresh and disjoint from every other allocation, so exclusive access to it does not conflict with the shared loan of the region. - No generic methods.
allocArray(n, fill)takes the fill value (like[v; N]), andTcomes fromfill.Pool<T>'sTcomes from the expected type, as forOk/Err. Arena.createcannot fail. Chunks are allocated on demand, so it returns anArena, not aResult.- Scratch frames are LIFO by borrowing (corrects
memory-model.md§3.4).- The claim that
usingalone keeps frames LIFO is wrong. With framesa, thenb, an allocation fromawhilebis live lands aboveb's watermark, andb's release then frees memory thata's allocation still uses. - Fixed by: a nested frame is made with
parent.enter(), which holds a mutable loan of the parent. The parent cannot allocate while the child lives (B1). - A root frame is made with
Scratch.enter(), and only one root frame per thread may be live: entering a second one panics ("a scratch frame is already active"). - Scratch memory is a runtime-owned stack of heap chunks, not the native stack.
- The claim that
Consequences
- R1 needs no special case in the borrow checker beyond decision 2: loans flow from the region into the allocation through the method's receiver, like any
borrow(this)result. - Ex15 and Ex16 run in adapted forms (
milestone-4.md). memory-model.md§3.4 is amended to point here.