Skip to content

List<T>

← Back to Standard Library

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.memmove for 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, returns Maybe@(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