List<T>¶
Generic growable array with automatic memory management.
Import¶
# List@(T) is built-in - no import required
Overview¶
List@(T) is a dynamically-sized array that grows automatically as elements are added. It provides:
- Zero-capacity start: Lazy allocation until first push
- Exponential growth: Doubles capacity for amortized O(1) push
- Type-safe access: .get() returns Maybe@(T) for safe bounds checking
- Iterator support: Works with foreach loops
- RAII cleanup: Automatic recursive element destruction
List@(T) is an owning type: assigning it, or handing it to a nom parameter, moves
it (the source binding can no longer be used; the destination now owns and frees it). An
ordinary parameter borrows, so f(list) leaves the list yours; peek List@(T) /
poke List@(T) borrow by pointer, which is what lets a callee mutate it in place. There is
no direct list[i] indexing operator (unlike T[] arrays) — use .get(i) for safe access.
Construction¶
List.new() -> List@(T)¶
Create empty list (zero capacity, lazy allocation).
let List@(i32) nums = List.new()
List.with_capacity(i32 n) -> List@(T)¶
Create list with pre-allocated capacity.
let List@(string) names = List.with_capacity(100)
Query Methods¶
.len() -> i32¶
Get number of elements.
println("Size: {list.len()}")
.capacity() -> i32¶
Get allocated capacity.
println("Capacity: {list.capacity()}")
.is_empty() -> bool¶
Check if list is empty.
if (list.is_empty()):
println("Empty list")
Access Methods¶
.get(i32 index) -> Maybe@(T)¶
Get element at index (bounds-checked). The list keeps the element — .get() does not remove
it. If T is an owning type (e.g. string, a struct/enum holding heap data), the returned
value is a deep copy, so the list and the returned Maybe each own independent memory.
match list.get(0):
Maybe.Some(value) ->
println("First: {value}")
Maybe.None() ->
println("Index out of bounds")
.pop() -> Maybe@(T)¶
Remove and return last element. Unlike .get(), this moves the element out — the list no
longer owns it.
match list.pop():
Maybe.Some(value) ->
println("Popped: {value}")
Maybe.None() ->
println("Empty list")
Modification Methods¶
.push(T element) -> ~¶
Append element (auto-grows capacity).
list.push(42)
list.push(100)
.insert(i32 index, T element) -> Result@(~)¶
Insert element at index (shifts elements right). Returns Result.Err if index is out of
bounds — unlike .push()/.get()/.pop()/.remove(), this is the one List@(T) method that
can fail, so it returns a Result instead of ~ or Maybe@(T).
let List@(i32) nums = List.new()
nums.push(2)
nums.push(3)
nums.push(4)
# Insert at beginning
match nums.insert(0, 1):
Result.Ok(_) -> println("inserted")
Result.Err(_) -> println("index out of bounds")
# Insert in middle
nums.insert(2, 99)
# Insert at end (equivalent to push)
nums.insert(nums.len(), 100)
Bounds: 0 <= index <= len
.remove(i32 index) -> Maybe@(T)¶
Remove and return element at index (shifts elements left).
match list.remove(2):
Maybe.Some(value) ->
println("Removed: {value}")
Maybe.None() ->
println("Index out of bounds")
Bounds: 0 <= index < len
.clear() -> ~¶
Remove all elements (keeps capacity).
list.clear()
println("Length: {list.len()}") # 0
println("Capacity: {list.capacity()}") # Unchanged
Capacity Management¶
.reserve(i32 additional) -> ~¶
Ensure capacity is at least len() + additional — i.e. reserve room for additional more
elements on top of what the list already holds. Only grows, never shrinks; a no-op if the
current capacity already covers len() + additional.
list.reserve(100) # Ensure space for 100 more elements beyond the current length
.shrink_to_fit() -> ~¶
Reduce capacity to match length.
list.shrink_to_fit() # Capacity = len
Iteration¶
.iter() -> Iterator@(T)¶
Create iterator for foreach loops.
foreach(value in list.iter()):
println(value)
Hashing¶
.hash() -> u64¶
The hash of what the list HOLDS: each element in turn, then the length. It is derived automatically, and it exists only when the element type has a hash of its own.
let List@(i32) a = List.new()
a.push(1)
a.push(2)
let List@(i32) b = List.new()
b.push(1)
b.push(2)
println("{a.hash() == b.hash()}") # true -- two lists, the same elements
Two lists that hold equal elements answer one hash, although their buffers are two. The length is part of the hash, so a list of one zero and a list of two zeros do not collide.
A list is not a HashMap@(K, V) key: a key also needs an equality test, and a list has
none.
Memory Management¶
.free() -> ~¶
Free memory and reset to empty (still usable).
list.free()
list.push(1) # OK: Can still use
.destroy() -> ~¶
Free memory and invalidate (unusable).
list.destroy()
# list.len() # ERROR CE2406: use of destroyed variable
Debugging¶
.debug() -> ~¶
Print internal state (length, capacity, elements).
list.debug()
Output (one element per line):
List@(i32) {
len: 3, capacity: 4
[0] 1
[1] 2
[2] 3
}
An element of a primitive type or a string prints as println writes it, and a
string is quoted. Any other element -- a struct, an enum, a nested container --
prints as <value>.
Performance¶
push(): Amortized O(1)pop(): O(1)get(): O(1)insert(): O(n)remove(): O(n)clear(): O(n)
Implementation Details¶
- Uses
llvm.memmovefor safe overlapping memory operations - Exponential growth strategy: doubles capacity on each reallocation
- Recursive element destruction for nested structures
- Iterator support for foreach loops via
.iter()
Best Practices¶
- Use
.with_capacity()when final size is known to avoid reallocations - Use
.get()for safe access, returnsMaybe@(T)instead of panicking - Call
.free()to reclaim memory early if list is no longer needed - Use
.shrink_to_fit()after batch operations to reduce memory footprint - Prefer
.pop()over.remove(len-1)for last element