allocators/stack

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 stack

When 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) Stack

If 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) rawptr

allocAligned

Allocates memory with a specific alignment.

var aligned = stack.allocAligned(&myStack, 64, 16)

Signature:

func allocAligned(s *Stack, size usize, alignment usize) rawptr

The 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) rawptr

allocType

Allocates memory for a specific type.

struct ParseState { depth i32, flags u32 }
var state = stack.allocType[ParseState](&myStack)

Signature:

func allocType[T](s *Stack) rawptr

Freeing 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 recent

Signature:

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) rawptr

Returns 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) usize

isEmpty

Check if the stack has no allocations.

if stack.isEmpty(&myStack) {
    io.println("Stack is empty")
}

Signature:

func isEmpty(s *Stack) bool

Types

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

ScenarioUse StackUse Arena
Function-scoped temp data with deferYes
Per-frame allocations (bulk reset)Yes
Recursive algorithmsYes
Many allocations, one resetYes
Need to free in reverse orderYes
Don’t need individual freesYes
Minimize per-allocation overheadYes

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.