ADR 0004 — M4 regions: built-in region types, scoped results, LIFO scratch frames
Inherited. ADR 0004 was accepted before CeeSafe's fork (Decision 0006), when the language had its earlier name. In this record, "the reference" is that language and project and "the reference compiler" its compiler: read them as CeeSafe and
ceesafec. Links to documents CeeSafe did not keep are given as text.
- 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.