12. Memory
12.1 No hidden allocation#
No Base operation allocates. There are no built-in collections; a list, map, or string builder is a library type that takes an Allocator. Every allocation is a call the programmer wrote.
12.2 The Allocator interface#
pub interface Allocator:
mutating func allocate(self, size: usize, alignment: usize) -> u8[]?
mutating func resize(self, block: u8[], new_size: usize, alignment: usize) -> bool
mutating func free(self, block: u8[], alignment: usize)
The requirements are mutating because every real allocator has state. allocate returns uninitialised memory or none. Typed helpers in the standard memory module zero what they hand out:
let node = try memory.create[Node](allocator) # Node*!, zeroed
let items = try memory.array[i32](allocator, count) # i32[]!, zeroed
memory.destroy(allocator, node)
memory.free_array(allocator, items)
memory.out_of_memory is the error when allocate answers none.
Why explicit allocators. Zig's explicit Allocator parameter and Odin's implicit context are the two designs. The explicit parameter is right for a language that must be callable from C and from full Luce with a plain calling convention: a hidden context pointer needs a custom convention and breaks C transparency. It also makes every allocation greppable, which is the property that lets a freestanding program prove it allocates nothing.
12.3 Standard allocators#
PageAllocator maps host pages. CAllocator wraps malloc and free. FixedBuffer allocates from a caller's u8[]. Arena bump-allocates from any parent and resets wholesale. Inside a full Luce program the runtime's heap is available as runtime.heap() (§18.9). In a diagnostic build each standard allocator quarantines freed blocks, fills them with a pattern, and records allocation sites.
12.4 Ownership conventions#
There is no ownership syntax. A pointer parameter is borrowed for the call unless the documentation says the callee takes it; a pointer result is owned by the caller unless the documentation says it is borrowed. defer and errdefer are the tools. The linter has a rule for memory.create without a matching destroy or errdefer in scope.
12.5 The safety statement#
Base is memory-unsafe the way Zig is, not the way C is. These two lists are exhaustive for the language. A library may add checks; nothing removes one.
Defined and checked in every build: integer overflow in +, -, *, //, % (trap); shift by the operand width or more (trap); division by zero and minimum_signed // -1 (trap); indexing and slicing of arrays, spans, and str (trap); unwrapping every optional (trap through else trap, or a compile error without it); dereference of a bare pointer (cannot be null by type, with the one boundary check of §17.1); reading an uninitialised local (compile error, except after ---); non-exhaustive match (compile error); converting an invalid integer to an integer-backed enum (none); checked conversions T(x) (trap); float-to-integer casts (saturate); reads and writes through volatile (never elided or merged); every atomic operation; aliasing of compatible objects (no type-based aliasing rule); signed right shift (arithmetic).
Undefined, exactly as in C, and the programmer's responsibility: use after free and double free; dereferencing a dangling pointer, including one the escape rule of §6.6 could not see; pointer arithmetic that leaves the object, including p[i] out of range; misaligned access after a (T*) cast; reading storage declared with ---, or returned by Allocator.allocate, before writing it; modifying a let or const object through a cast; a data race on memory that is not @T; a longjmp through a Base frame; an asm block that violates its declared clobbers; a C callee that retains a lent pointer past the call; a C caller that violates a contract stated in an extern declaration.
Why two lists. "Everything else is defined" is the sentence every safer-C proposal writes and none can keep. Two exhaustive lists can be checked against the C standard's own catalogue of undefined behaviour, and every future rule has to add itself to one of them.