Memory Management

GX has no garbage collector and no hidden allocations. You decide exactly when memory is allocated and when it’s freed. The language gives you several tools — stack allocation, dynamic arrays, and three allocator types — to match the memory pattern to your workload.

Stack vs Heap

Values declared with var are on the stack. They disappear automatically when the scope ends:

fn main() {
    var x:i32 = 42                 // stack
    var arr:i32[100]               // stack (400 bytes)
    var pos = Vec3{1.0, 2.0, 3.0}  // stack (12 bytes)

    print("x = {x}\n")
    // arr, pos, and x all freed automatically when main ends
}

Stack allocation is free — just bumping a pointer. Use it whenever the size is fixed and known at compile time.

For truly dynamic data (unknown size, grows over time), you need heap allocation via array<T> or an allocator.

Dynamic Arrays

array<T> is a resizable array that allocates from the heap:

fn main() {
    var nums:array<i32>
    nums.init()
    defer { nums.free() }

    nums.push(10)
    nums.push(20)
    nums.push(30)

    print("length: {nums.len()}\n")
    for (var v in nums) {
        print("{v}\n")
    }
}

The defer { nums.free() } guarantees cleanup on any exit path. Without it, you leak memory when the function returns.

The Three Allocators

For more control over allocation patterns, GX provides three allocator types:

AllocatorPatternUse Case
ArenaBump pointer, free all at onceShort-lived batches, per-frame data, parsing
PoolFixed-size slots, free individuallyObject pools, entity systems
FreeListVariable sizes, coalesces on freeGeneral-purpose allocator replacement

Arena Allocator

An arena allocates memory linearly from a big buffer. You can free everything at once, but not individual allocations:

fn main() {
    var arena:Arena
    arena.init(65536)               // 64 KB buffer
    defer { arena.destroy() }

    var a:*i32 = arena.alloc(i32)
    var b:*i32 = arena.alloc(i32)
    var c:*f32 = arena.alloc(f32)

    *a = 10
    *b = 20
    *c = 3.14

    print("{*a} {*b} {*c}\n")
    // arena.destroy() frees all three allocations at once
}

Perfect for per-frame data: allocate anything you need during the frame, call arena.reset() at the end to reclaim all of it instantly.

fn main() {
    var frame_arena:Arena
    frame_arena.init(1048576)    // 1 MB
    defer { frame_arena.destroy() }

    var frame = 0
    while (frame < 60) {
        // Allocate stuff for this frame
        var buf:*i32 = frame_arena.alloc(i32)
        *buf = frame * 100

        // Reset for next frame — no individual frees needed
        frame_arena.reset()
        frame = frame + 1
    }
}

Pool Allocator

A pool pre-allocates a fixed number of slots of the same size. You allocate and free individual slots, and free slots are reused immediately:

struct Particle {
    x:f32
    y:f32
    vx:f32
    vy:f32
    life:f32
}

fn main() {
    var pool:Pool
    pool.init(Particle, 1000)    // 1000 Particle slots
    defer { pool.destroy() }

    // Allocate a particle
    var p:*Particle = pool.alloc(Particle)
    p.x = 100.0
    p.y = 200.0
    p.life = 1.0

    print("spawned at ({p.x}, {p.y})\n")

    // Free it — slot becomes available for reuse
    pool.free(p)
}

Pools are ideal for object pools (enemies, bullets, particles) where you allocate and free often but the max count is known.

FreeList Allocator

A freelist handles variable-sized allocations and coalesces adjacent free blocks. It’s a general-purpose allocator:

fn main() {
    var fl:FreeList
    fl.init(65536)    // 64 KB buffer
    defer { fl.destroy() }

    var a:*i32 = fl.alloc(i32)
    var b:*f64 = fl.alloc(f64)
    var c:*i32 = fl.alloc(i32)

    *a = 100
    *b = 3.14159
    *c = 999

    print("{*a} {*b} {*c}\n")

    // Free takes the type so the allocator knows the size
    fl.free(a, i32)
    fl.free(b, f64)
    fl.free(c, i32)
}

Use a freelist when you need malloc/free-style flexibility but want to control the underlying buffer.

Choosing an Allocator

Need dynamic sizing, simple API    → array<T>
All allocations freed together     → Arena
Many uniform objects, allocate/free often  → Pool
Mixed sizes, general purpose       → FreeList
Fixed size, known at compile time  → Stack array T[N]

Common Patterns

Per-frame temporary allocations:

var tmp:Arena
tmp.init(65536)
defer { tmp.destroy() }

// ... use tmp during the frame ...
tmp.reset()    // clear everything, ready for next frame

Reusable object pool:

var bullets:Pool
bullets.init(Bullet, 500)
defer { bullets.destroy() }

fn fire() {
    var b:*Bullet = bullets.alloc(Bullet)
    // ... initialize bullet ...
}

fn destroy(b:*Bullet) {
    bullets.free(b)
}

General dynamic memory:

var heap:FreeList
heap.init(16 * 1024 * 1024)    // 16 MB
defer { heap.destroy() }

var buffer:*u8 = heap.alloc(u8)
// ... use ...
heap.free(buffer, u8)

Practical Example: Simple Task Queue

struct Task {
    id:i32
    priority:i32
}

fn main() {
    // Use a pool for task objects since we create/destroy them often
    var task_pool:Pool
    task_pool.init(Task, 100)
    defer { task_pool.destroy() }

    // Track active tasks in a dynamic array
    var tasks:array<*Task>
    tasks.init()
    defer { tasks.free() }

    // Create some tasks
    for (i = 1:5) {
        var t:*Task = task_pool.alloc(Task)
        t.id = i
        t.priority = (6 - i) * 10
        tasks.push(t)
    }

    // Process them
    print("Tasks:\n")
    for (var t in tasks) {
        print("  task {t.id} priority {t.priority}\n")
    }

    // Free individual tasks back to the pool
    for (var t in tasks) {
        task_pool.free(t)
    }
}

Try it — Create an arena, allocate 5 integers, print their values, then reset the arena and allocate 5 more. Notice how the second batch reuses the same memory in the Playground.


Expert Corner

Why no GC: Garbage collection trades programmer effort for runtime overhead — background scans, pause spikes, unpredictable cleanup timing. For games, systems code, and embedded, those costs are unacceptable. GX shifts the effort back to the programmer, explicitly and visibly. You pay for memory management in source code, not CPU cycles.

defer is your memory safety net: Every allocator has a destroy() or free() method. Pair every init() with a defer { ... } on the next line. This makes leaks nearly impossible — the cleanup is always right next to the allocation, and it runs on every exit path.

Arena is the secret weapon: For workloads with clear lifetime boundaries (per-frame, per-request, per-parsing-pass), arenas are dramatically simpler and faster than malloc/free. Allocation is just a pointer increment. “Freeing” is just resetting the pointer. No metadata, no fragmentation, no individual tracking. Modern C game engines use arena allocators everywhere for this reason.

Pool vs FreeList: Pools require fixed-size slots but support O(1) alloc and free with perfect locality — allocated slots are contiguous in the pool’s buffer. FreeLists handle variable sizes but pay for it with more bookkeeping per allocation and potential fragmentation. If your objects are all the same type, use a pool.

Stack allocation limits: The stack is fast but small — typically 1-8 MB per thread. Don’t put huge arrays on the stack (u8[10485760] = 10 MB on the stack will crash). Rule of thumb: stack allocations up to a few KB are fine, beyond that use an allocator.

No RAII, explicit cleanup: GX doesn’t have destructors. You write defer { x.free() } manually. The tradeoff: more ceremony than C++ RAII, but zero hidden behavior. You always know what’s being freed and when.

array<T> is a FreeList-backed dynamic array: Under the hood, array<T> uses the default FreeList allocator and grows by doubling capacity when full. If you need more control (pre-allocated capacity, custom allocator), write your own with a raw buffer and a length counter.

When to reach for each:

  • var x:T — you know the size, it’s small, lifetime matches a scope. 95% of code.
  • var arr:T[N] — fixed-size buffer. 4% of code.
  • array<T> — dynamic list, easy API. Most remaining cases.
  • Arena — per-frame, per-request, per-parse. Batch lifetimes.
  • Pool — entity systems, object pools, uniform sizes.
  • FreeList — general-purpose when you need explicit control over the buffer.