Dynamic Arrays

Fixed arrays are great when you know the size upfront. When you don’t, array<T> grows as needed.

Creating a Dynamic Array

fn main() {
    var list:array<i32>
    list.init()

    list.push(10)
    list.push(20)
    list.push(30)

    print("Length: {list.len()}\n")  // 3

    for (i = 0:list.len() - 1) {
        print("list[{i}] = {list.at(i)}\n")
    }

    list.free()
}

Three steps: init() to set up, use it, free() when done. No garbage collector — you own the memory.

Push and Access

fn main() {
    var names:array<str>
    names.init()

    names.push("Alice")
    names.push("Bob")
    names.push("Charlie")
    names.push("Diana")

    print("First: {names.at(0)}\n")
    print("Last: {names.at(names.len() - 1)}\n")
    print("Count: {names.len()}\n")

    names.free()
}

Use .at(index) to read elements and .len() for the count.

Iterating

For-each works with dynamic arrays:

fn main() {
    var scores:array<i32>
    scores.init()

    scores.push(95)
    scores.push(87)
    scores.push(92)
    scores.push(78)

    var total = 0
    for (var s in scores) {
        total += s
    }

    print("Average: {total / scores.len()}\n")

    scores.free()
}

Sorting

Built-in sort for numeric arrays:

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

    nums.push(42)
    nums.push(17)
    nums.push(93)
    nums.push(5)
    nums.push(68)

    print("Before: ")
    for (var n in nums) {
        print("{n} ")
    }
    print("\n")

    nums.sort()

    print("After:  ")
    for (var n in nums) {
        print("{n} ")
    }
    print("\n")

    nums.free()
}

Building with Collect

Remember collect from the loops tutorial? It returns a dynamic array:

fn main() {
    var data:i32[8] = {1, 2, 3, 4, 5, 6, 7, 8}

    var evens = for (n in data) where (n % 2 == 0) collect n

    print("Evens: ")
    for (var e in evens) {
        print("{e} ")
    }
    print("\n")

    evens.free()  // don't forget!
}

Fixed vs Dynamic: When to Use Which

Use caseChoose
Size known at compile timei32[10] (fixed)
Size known at startuparray<T>
Growing collectionarray<T>
Stack-allocated, fasti32[N] (fixed)
Return from functionarray<T>
fn main() {
    // Fixed: perfect when you know the size
    var rgb:f32[3] = {0.2, 0.5, 0.8}

    // Dynamic: when size varies
    var results:array<i32>
    results.init()
    for (i = 1:100) {
        if (i % 7 == 0) {
            results.push(i)
        }
    }
    print("Multiples of 7 under 100: {results.len()}\n")
    results.free()
}

Try it — Build a dynamic array with collect in the Playground.


Expert Corner

array<T> is a thin wrapper over a heap-allocated buffer. Internally it’s { T* data; int64_t len; int64_t cap; } — pointer, current length, and capacity. When you push and the buffer is full, it reallocates with double the capacity. This gives amortized O(1) push performance.

Why explicit .init() and .free()? GX doesn’t run constructors or destructors behind your back. If you declare var list:array<i32>, the memory is uninitialized until you call .init(). This makes it crystal clear where allocation and deallocation happen. No RAII surprises, no hidden mallocs, no destructor chains — you see every allocation in the code.

collect generates .init() and .push() calls — it’s syntactic sugar, not magic. The compiler transforms var evens = for (n in data) where (...) collect n into an init, a loop with a push, and assigns the result. You still need to .free() the result.

Memory tip: If you know approximately how many elements you’ll add, you can avoid repeated reallocations. The current implementation doubles capacity on each grow, starting small. For performance-critical code, consider pre-sizing or using a fixed array when possible.