Chasm has two kinds of arrays. The right choice depends on where the array lives and whether its maximum size is known upfront.
len vs cap, The Most Important Distinction
Every array tracks two numbers: len and cap. These are different things and confusing them is the most common source of bugs.
arr = array_new(8) # create an array with capacity 8
print(arr.len) # 0 , no elements stored yet
print(arr.cap) # 8 , room for 8 elements
arr.push(10)
arr.push(20)
print(arr.len) # 2 , two elements stored
print(arr.cap) # 8 , capacity unchangedlen is how many elements are actually in the array right now. It starts at 0 and grows as you call .push.
cap is how much memory has been reserved. The array can hold up to cap elements before it needs to allocate more space (for array_new) or abort (for array_fixed).
Think of it like a box: cap is the size of the box, len is how many items are inside.
Reading out of bounds:
.get(i) is only valid for 0 <= i < len. Accessing at index len or beyond reads uninitialized
memory or aborts. Always loop over 0..arr.len, not 0..arr.cap.
array_new, Heap-backed, Growable
array_new(initialCapacity) creates an empty array that grows automatically as you push elements. Use it for local scratch data inside functions:
def main() do
hits = array_new(4) # starts empty, capacity 4
hits.push(42)
hits.push(7)
hits.push(99)
# Iterate using len, not cap
for i in 0..hits.len do
print(hits.get(i))
end
endWhen the array outgrows its capacity, it doubles: capacity goes from 4 → 8 → 16 → 32. Each doubling copies all existing elements into a larger allocation. This is automatic, you never call realloc yourself.
The initial capacity argument is a hint, not a limit. Passing 4 when you expect 4 elements avoids the first reallocation. Passing 0 works too, the first push triggers the first allocation.
Why the initial capacity matters
Every push that exceeds cap triggers a heap allocation. In a hot loop this can cause unpredictable frame times. If you know the upper bound, pass it up front:
# Bad: may reallocate many times during the loop
bullets = array_new(0)
for i in 0..100 do bullets.push(0) end
# Good: allocates once
bullets = array_new(100)
for i in 0..100 do bullets.push(0) endTyped struct arrays
Pass a struct type name as the first argument to create a typed array. .push, .get, and .set operate on the struct directly, no casting:
defstruct Bullet do
x :: float
y :: float
speed :: float
end
bullets = array_new(Bullet, 64)
bullets.push(Bullet{ x: 0.0, y: 0.0, speed: 5.0 })
b :: Bullet = bullets.get(0)
bullets.set(0, b with { x: b.x + b.speed })
print(bullets.len) # 1
print(bullets.cap) # 64Without the Bullet type argument, .get returns an opaque integer handle. With it, .get returns a real Bullet struct by value.
array_fixed, Arena-backed, Fixed Capacity
array_fixed(cap) allocates from the arena that matches the attribute's declared lifetime. The key differences from array_new:
- No heap allocation, memory comes from the arena
- Capacity is fixed, pushing beyond
capaborts with an error - Automatic reset, when the lifetime resets (e.g. frame ends), the entire arena is wiped; there is no per-element cleanup
@bullets :: script = array_fixed(64) # up to 64 elements, reset on reload
@sparks :: frame = array_fixed(256) # up to 256 elements, reset every tick
@records :: persistent = array_fixed(16) # up to 16 elements, never resetThe @sparks example is the most powerful: 256 slots of scratch space that cost nothing to reset, the frame arena pointer just snaps back to where it started.
cap is the hard limit
Unlike array_new which doubles, array_fixed will abort if you push more than cap elements:
@pool :: script = array_fixed(4)
def on_tick(dt :: float) do
@pool.push(1)
@pool.push(2)
@pool.push(3)
@pool.push(4)
@pool.push(5) # runtime abort: fixed array overflow
endAlways verify arr.len < arr.cap before pushing to a fixed array when the count is dynamic.
Pre-filled slots with a default value
Pass a second argument to pre-fill all cap slots at initialization time. This sets len = cap immediately, every slot is ready to read:
@bullet_x :: script = array_fixed(8, 0.0) # []float, len=8, all 0.0
@active :: script = array_fixed(8, 0) # []int, len=8, all 0Without a default, len = 0 and slots must be filled with .push before they can be read.
# No default: start empty and push
@scores :: script = array_fixed(8)
def on_init() do
@scores.push(0)
@scores.push(0)
# ...
end
# With default: start full, use set/get immediately
@scores :: script = array_fixed(8, 0)
def on_tick(dt :: float) do
@scores.set(0, @scores.get(0) + 1)
endStruct arrays with array_fixed
Pass a struct literal as the default to pre-fill all slots with that value:
defstruct Enemy do
x :: float = 0.0
y :: float = 0.0
hp :: int = 10
active :: int = 0
end
@enemies :: script = array_fixed(32, Enemy{})
def on_tick(dt :: float) do
i = 0
while i < @enemies.len do
e = @enemies.get(i)
if e.active == 1 do
@enemies.set(i, e with { x: e.x + 2.0 * dt })
end
i = i + 1
end
endEnemy{} uses struct field defaults, so every slot starts with x=0, y=0, hp=10, active=0. On hot-reload the script arena resets and all 32 slots reinitialize from scratch, no manual cleanup needed.
Array Methods
| Method | Description |
|---|---|
arr.len | Number of elements currently stored |
arr.cap | Total capacity, how many elements fit before overflow |
arr.push(v) | Append v to the end; increments len |
arr.get(i) | Read element at index i (0-based, must be < len) |
arr.set(i, v) | Write element at index i (must be < len) |
arr.pop() | Remove and return the last element; decrements len |
arr.clear() | Set len to 0; capacity and allocated memory unchanged |
.clear() does not free memory, it just resets the length counter. The slots are still allocated; calling .push after .clear() reuses them from index 0.
Iterating Arrays
Always iterate over 0..arr.len, never 0..arr.cap:
# Correct
for i in 0..arr.len do
print(arr.get(i))
end
# Also correct, array iteration syntax
for item in arr do
print(item)
endChoosing the Right Array
array_new | array_fixed | |
|---|---|---|
| Lifetime | Heap, manual / GC on scope exit | Arena, tied to frame/script/persistent |
| Grows | Yes, doubles on overflow | No, abort on overflow |
| Initial len | 0 (or cap with pre-fill default) | 0 or cap (with default value) |
| Reset cost | Per-element (GC or manual) | Zero, arena pointer reset |
| Use for | Local scratch in functions | Module attributes (@name) |
Rule of thumb:
If it lives at module level as an @attr, use array_fixed. If it lives inside a function as a
local variable, use array_new.