The stack allocator extends the arena concept by adding support for individual deallocation in LIFO (Last-In-First-Out) order. Each allocation stores a small header that tracks the previous offset, allowing you to “pop” the most recent allocation.
import "conjure/allocators/stack" as stackWhen to Use Stack Allocators
Stack allocators are perfect when:
- You need LIFO deallocation (free in reverse order of allocation)
- Allocations follow function call patterns (allocate on enter, free on exit)
- You want scoped memory with
defer(RAII-style patterns) - Recursive algorithms that allocate at each level
Common use cases:
- Function-scoped temporary buffers
- Recursive tree/graph traversal scratch data
- Parser state during nested parsing
- Undo/redo stacks
Creating a Stack
new
Creates a new stack allocator with the specified capacity.
// Automatic heap allocation (most common)
var myStack = stack.new(256 * 1024) // 256 KB
defer stack.destroy(&myStack)
// With provided buffer (you manage the memory)
var buffer = memory.alloc(256 * 1024)
var myStack = stack.new(256 * 1024, buffer)
defer memory.free(buffer)Signature:
func new(capacity usize, buffer rawptr = null) StackIf buffer is null (the default), heap memory is allocated automatically.
destroy
Frees the backing memory of a stack created without a provided buffer.
stack.destroy(&myStack)Signature:
func destroy(s *Stack)Allocating Memory
alloc
Allocates a block of memory with default alignment (8 bytes).
var data = stack.alloc(&myStack, 1024)
if data == null {
io.println("Stack exhausted!")
}Signature:
func alloc(s *Stack, size usize) rawptrallocAligned
Allocates memory with a specific alignment.
var aligned = stack.allocAligned(&myStack, 64, 16)Signature:
func allocAligned(s *Stack, size usize, alignment usize) rawptrThe alignment must be a power of 2 and <= 128 bytes.
allocZeroed
Allocates memory and sets all bytes to zero.
var zeroed = stack.allocZeroed(&myStack, 512)Signature:
func allocZeroed(s *Stack, size usize) rawptrallocType
Allocates memory for a specific type.
struct ParseState { depth i32, flags u32 }
var state = stack.allocType[ParseState](&myStack)Signature:
func allocType[T](s *Stack) rawptrFreeing Memory
free
Frees the most recently allocated block (LIFO order).
var a = stack.alloc(&myStack, 100)
var b = stack.alloc(&myStack, 200)
var c = stack.alloc(&myStack, 300)
stack.free(&myStack, c) // OK - c is most recent
stack.free(&myStack, b) // OK - b is now most recent
stack.free(&myStack, a) // OK - a is now most recentSignature:
func free(s *Stack, ptr rawptr)reset
Resets the stack, freeing all allocations at once.
stack.reset(&myStack)Signature:
func reset(s *Stack)resetZeroed
Resets and zeros all memory.
stack.resetZeroed(&myStack)Signature:
func resetZeroed(s *Stack)Resizing Allocations
resize
Attempts to resize the most recent allocation.
var data = stack.alloc(&myStack, 100)
data = stack.resize(&myStack, data, 100, 200)Signature:
func resize(s *Stack, ptr rawptr, oldSize usize, newSize usize) rawptrReturns null if there isn’t enough space or if the pointer isn’t the most recent allocation.
Introspection
used / remaining / capacity
Query stack state.
var bytesUsed = stack.used(&myStack)
var bytesLeft = stack.remaining(&myStack)
var total = stack.capacity(&myStack)Signatures:
func used(s *Stack) usize
func remaining(s *Stack) usize
func capacity(s *Stack) usizeisEmpty
Check if the stack has no allocations.
if stack.isEmpty(&myStack) {
io.println("Stack is empty")
}Signature:
func isEmpty(s *Stack) boolTypes
Stack
The main stack allocator structure.
struct Stack {
buffer rawptr // Backing memory
capacity usize // Total size
offset usize // Current position
}StackHeader
Internal header stored before each allocation (1 byte).
struct StackHeader {
padding u8 // Padding bytes before header
}Example: Scoped Allocations with Defer
The stack allocator works beautifully with defer for automatic cleanup:
import "conjure/allocators/stack" as stack
var tempStack = stack.create(64 * 1024)
func processFile(path string) {
var buffer = stack.alloc(&tempStack, 4096)
defer stack.free(&tempStack, buffer)
readFileInto(path, buffer)
// ... process buffer ...
// buffer is automatically freed when function returns
}
func processMultipleFiles(paths []string) {
for path in paths {
// Each call allocates and frees its own buffer
processFile(path)
}
// tempStack is back to original state
}Example: Recursive Algorithm
import "conjure/allocators/stack" as stack
var recursionStack = stack.create(1024 * 1024)
struct TreeNode { value i32, left *TreeNode, right *TreeNode }
func sumTree(node *TreeNode) i32 {
if node == null {
return 0
}
// Allocate scratch space for this recursion level
var scratch = stack.alloc(&recursionStack, 64)
defer stack.free(&recursionStack, scratch)
// Process children (they'll use their own scratch space)
var leftSum = sumTree(node.left)
var rightSum = sumTree(node.right)
// scratch is automatically freed on return
return node.value + leftSum + rightSum
}Example: Parser with Nested State
import "conjure/allocators/stack" as stack
var parseStack = stack.create(256 * 1024)
struct ParseContext {
depth i32
parentType u8
localVars [32]string
varCount i32
}
func parseBlock(tokens []Token) *AstNode {
// Push new parse context
var ctx = *ParseContext(stack.allocType[ParseContext](&parseStack))
defer stack.free(&parseStack, ctx)
ctx.depth = currentDepth
ctx.parentType = currentType
ctx.varCount = 0
while hasTokens(tokens) {
var token = nextToken(tokens)
if isBlockStart(token) {
// Recursive parse - will push its own context
var child = parseBlock(tokens)
// child's context is already freed
}
// ... more parsing ...
}
return buildNode(ctx)
// ctx is freed when function returns
}Example: Undo Stack
import "conjure/allocators/stack" as stack
var undoStack = stack.create(1024 * 1024)
struct UndoEntry {
type u8
dataSize usize
// data follows immediately after
}
func pushUndo(type u8, data rawptr, size usize) {
var entrySize = UndoEntry.size + size
var entry = *UndoEntry(stack.alloc(&undoStack, entrySize))
entry.type = type
entry.dataSize = size
memory.copy(entry[UndoEntry.size], data, size)
}
func popUndo() {
// Get the most recent entry
// (In practice you'd track the current entry pointer)
// Then free it
// stack.free(&undoStack, lastEntry)
}Stack vs Arena: When to Choose
| Scenario | Use Stack | Use Arena |
|---|---|---|
Function-scoped temp data with defer | Yes | |
| Per-frame allocations (bulk reset) | Yes | |
| Recursive algorithms | Yes | |
| Many allocations, one reset | Yes | |
| Need to free in reverse order | Yes | |
| Don’t need individual frees | Yes | |
| Minimize per-allocation overhead | Yes |
The stack allocator adds ~1 byte overhead per allocation for the header. If you’re making thousands of small allocations and never need individual frees, prefer an arena.