Module collections/array_list
Dynamic array (vector) with amortized O(1) push and O(1) indexed access.
Stability
stable — every breaking flip this type owed Rust shipped in v0.2.28:
push is infallible with try_push beside it (D9), iter() yields
POINTERS while into_iter() yields values (D14), and sort is the stable
merge with sort_unstable as the heapsort (D17). The P1 battery is
complete and test-covered, and plans/STD_API_STABILIZATION.md §4 lists
nothing still open against this module, so further work here is additive.
Types
Generic growable array similar to Rust's Vec<T>.
Heap-allocated with automatic resizing and reference-counted cleanup.
Type Parameters
| Name | Type | Notes |
|---|---|---|
T | Type | comptime |
Trait Implementations
impl(generic(T : Type), ArrayList(T), ...)
len : (fn(self : Self) -> usize)Number of elements. O(1) — the length is stored, never counted.
Returns: usize
is_empty : (fn(self : Self) -> bool)True when the list holds no elements — len() == 0.
Returns: bool
capacity : (fn(self : Self) -> usize)Elements the current buffer can hold before push has to reallocate.
SIZE_MAX for a zero-sized element type, which never grows — see
_zst_anchor. Rust's Vec::capacity reports usize::MAX for the same
reason.
Returns: usize
ptr : (fn(self : Self) -> ?(*(T)))Raw pointer to the element buffer, or .None when nothing is allocated
yet — Rust's Vec::as_ptr, minus the dangling-pointer guarantee.
The pointer is only valid until the next reallocation (push past
capacity, reserve, ensure_total_capacity, shrink_to_fit), and it
borrows: it does not keep the list alive.
Returns: ?(*(T))
set_len : (fn(self : Self, new_len : usize) -> unit)Declare that the first new_len slots of the buffer are live — Rust's
Vec::set_len, and just as unsafe.
Nothing is initialized and nothing is dropped: growing the length exposes
whatever bytes the buffer already held, and shrinking it leaks the
elements past the new end (dispose and clear only walk [0, len)).
Reach for it only after filling the buffer through ptr() or
extend_from_ptr. PANICS when new_len exceeds the capacity.
Returns: unit
new : (fn() -> Self)Empty list. Allocates nothing — the first push buys the buffer (four
elements), as Rust's Vec::new does.
Returns: Self
slice_copy : (fn(self : Self, r : Range(usize)) -> Self)Owned copy of [r.start, r.end) of a fixed-size array. The end is
clamped to N, so an over-long range yields a short list.
Returns: Self
slice_copy_inclusive : (fn(self : Self, r : RangeInclusive(usize)) -> Self)Inclusive-range companion of slice_copy (arr(a..=b)).
Returns: Self
from_array : (fn(generic(N : usize), arr : Array(T, N)) -> Self)Owned ArrayList copy of a fixed-size array's elements — the companion
to array range-indexing (plans/archive/SLICE_REWORK.md).
N comes from the array's type, so the exact capacity is reserved up
front and no growth happens.
Returns: Self
with_capacity : (fn(cap : usize) -> Self)Empty list whose buffer already holds cap elements — Rust's
Vec::with_capacity.
PANICS on "capacity overflow" when sizeof(T) * cap would wrap (Rust
panics with the same message), and on allocation failure. A zero-sized
element type takes the one-byte anchor buffer instead of allocating
cap of nothing.
Returns: Self
_zst_anchor : (fn() -> Self)The one buffer an ArrayList of a ZERO-SIZED element type ever has.
sizeof(T) == 0 (unit, an all-unit aggregate, Array(unit, N)) means
the elements occupy no bytes: a store writes nothing, a read reads nothing,
and the byte size of every buffer is 0. Rust's Vec<()> never allocates
and reports capacity() == usize::MAX. Yo cannot conjure a non-null
dangling pointer, so the stand-in is ONE one-byte block, allocated once and
never resized: the capacity is SIZE_MAX, so push never reaches the grow
path, and shrink_to_fit is a no-op. Going through the allocator with
sizeof(T) * cap == 0 instead would malloc(0) and then realloc(p, 0) —
which frees p and returns NULL on glibc and the Windows CRT — and the
list would report "allocation failed" on its second growth.
Returns: Self
try_push : (fn(self : Self, value : T) -> Result(unit, ArrayListError))Append an element, reporting allocation failure as
.Err(.AllocError(.OutOfMemory)) instead of panicking.
The allocator-aware form (D9); push is the one to reach for. Amortized
O(1) — the buffer doubles (4 elements first), so a growth INVALIDATES
every pointer previously handed out by ptr(), iter() or the Index
impl.
Returns: Result(unit, ArrayListError)
push : (fn(self : Self, value : T) -> unit)Append an element — amortized O(1).
PANICS on allocation failure: Rust's Vec::push aborts, and so do this
list's own ensure_total_capacity / with_capacity. Before D9 this
returned a Result that every one of its callers discarded, usually via
a throwaway _p := binding — a Result nobody checks is strictly worse
than a panic, because it reads as if the failure were handled. Use
try_push when the caller genuinely handles OOM.
A growth reallocates, so any pointer into the buffer (ptr(), iter(),
self(i)) is invalid afterwards.
Returns: unit
pop : (fn(self : Self) -> Option(T))Remove and return the last element, or .None when empty — O(1), and
the capacity is kept.
Returns: Option(T)
get : (fn(self : Self, index : usize) -> Option(T))Copy of the element at index, or .None when index >= len().
The bounds-checked reader. self(index) (the Index impl) hands back a
POINTER into the buffer and panics out of range instead; iter() is the
way to walk without copying a refcounted T.
Returns: Option(T)
shrink_to_fit : (fn(self : Self) -> Result(unit, ArrayListError))Give the unused tail of the buffer back to the allocator so that
capacity() == len() — Rust's Vec::shrink_to_fit.
Reallocates (an empty list frees its buffer outright), so every pointer
into the buffer is invalid afterwards and the next push grows again.
.Err(.AllocError(.OutOfMemory)) when the shrinking realloc fails —
the list is left untouched and usable. A zero-sized element type is a
no-op: its anchor buffer cannot shrink.
Returns: Result(unit, ArrayListError)
_free_elements : (fn(self : Self) -> unit)Returns: unit
drain : (fn(self : Self, r : Range(usize)) -> Self)Remove the elements in [r.start, r.end) and return them, in order, as
a fresh list — Rust's Vec::drain, collected eagerly rather than lazily
(Yo has no borrowing iterator to hold the source open).
O(n): the tail shifts left to close the gap. PANICS on an inverted range
or an end past the length — the old remove(start, count) silently
clamped the count, which hid caller bugs.
Returns: Self
insert : (fn(self : Self, idx : usize, value : T) -> unit)Insert value at idx, shifting the tail right — O(n). idx == len
appends, as in Rust.
Returns: unit
append : (fn(self : Self, other : Self) -> unit)Move every element of other onto the end of self, leaving other
EMPTY — Rust's append. extend copies instead.
Returns: unit
first : (fn(self : Self) -> Option(T))First element, or .None when empty.
Returns: Option(T)
last : (fn(self : Self) -> Option(T))Last element, or .None when empty.
Returns: Option(T)
reserve : (fn(self : Self, additional : usize) -> unit)Reserve room for additional MORE elements.
Rust's spelling. ensure_total_capacity takes a TOTAL, which is the same
call with a different meaning and an easy one to get wrong at a call site.
Returns: unit
swap : (fn(self : Self, i : usize, j : usize) -> unit)Exchange the elements at i and j.
Moves the raw slots, like reverse — no dup/drop, so this is RC-neutral
however the element type is refcounted.
Returns: unit
swap_remove : (fn(self : Self, idx : usize) -> T)Remove the element at idx and return it, moving the LAST element into
the hole — O(1), and it does not preserve order. Rust's swap_remove.
Returns: T
truncate : (fn(self : Self, new_len : usize) -> unit)Shorten to new_len, dropping the elements past it. Longer new_len is
a no-op, as in Rust.
Returns: unit
resize : (fn(self : Self, new_len : usize, value : T) -> unit)Resize to exactly new_len, dropping the tail when shrinking and
appending copies of value when growing. Rust's resize.
Distinct from resize_with_byte, which memsets raw storage and is only
meaningful for a plain-old-data element type.
Returns: unit
fill : (fn(self : Self, value : T) -> unit)Overwrite every element with value, keeping the length. Rust's fill.
Returns: unit
split_off : (fn(self : Self, at : usize) -> Self)Split off [at, len) into a new list, leaving [0, at) here.
Returns: Self
remove : (fn(self : Self, idx : usize) -> T)Remove the element at idx and return it, shifting the tail left —
O(n), order preserved. Rust's Vec::remove.
PANICS when idx >= len(). swap_remove is the O(1) alternative when
order does not matter, and LinkedList.remove returns a Result
instead — a deliberate difference (D1): the list that has to walk to
the index reports the caller's mistake as a value.
Returns: T
slice : (fn(self : Self, start : usize, end : usize) -> Result(Self, ArrayListError))Owned copy of [start, end), or an .IndexOutOfBounds error — the
CHECKED counterpart of slice_copy, which clamps instead.
An inverted range reports .IndexOutOfBounds(index : start, length : end). O(end - start), one allocation of the exact size.
Returns: Result(Self, ArrayListError)
ensure_total_capacity : (fn(self : Self, min_cap : usize) -> unit)Grow the buffer so it holds at least min_cap elements in TOTAL,
doubling as it goes; a request under the current capacity does nothing.
reserve(additional) is the Rust spelling and takes a COUNT TO ADD —
the same call with a different meaning, and the easy one to get right at
a call site. PANICS on capacity overflow and on allocation failure
(try_push is the only allocator-aware entry point). Reallocates, so
pointers into the buffer do not survive it.
Returns: unit
extend_from_ptr : (fn(self : Self, src : *(T), count : usize) -> unit)Append count elements from a raw buffer with one memcpy.
The caller guarantees src holds count initialized, correctly aligned
Ts that do not alias this list's buffer. The bytes are copied WITHOUT
per-element dup, so for a refcounted T this MOVES the source elements:
the caller must not also release them. Reaching for it on a plain-data
element type is the point — that is where it beats a push loop.
Returns: unit
clear : (fn(self : Self) -> unit)Drop every element and set the length to 0, KEEPING the capacity —
Rust's Vec::clear. Use shrink_to_fit afterwards to release the
buffer too.
Returns: unit
fill_with_byte : (fn(self : Self, byte_val : int) -> unit)Overwrite the live len() elements with a repeated byte via memset —
the fast way to zero a bool / integer list.
Writes over the elements without dropping them, so it is only sound for
a plain-data T: on a refcounted element type it strands every handle
it overwrites. fill(value) is the type-safe form.
Returns: unit
resize_with_byte : (fn(self : Self, new_len : usize, byte_val : int) -> unit)Set the length to exactly new_len, memsetting any newly exposed
slots to a repeated byte.
Shrinking does NOT run destructors and growing does not construct
anything, so — like fill_with_byte — this is for a plain-data T
only; resize(new_len, value) is the type-safe form. Growing may
reallocate and can panic on allocation failure.
Returns: unit
impl(generic(T : Type), ArrayList(T), Index(usize)(...))
Output : TThe output type is the element type T.
index : (fn(inout(self) : Self, idx : usize) -> *(Self.Output))Returns a pointer to the element at the given index. Panics if the index is out of bounds.
Parameters
| Name | Type | Notes |
|---|---|---|
idx | usize |
Returns: *(Self.Output)
impl(generic(T : Type), ArrayList(T), Dispose(...))
dispose : (fn(self : Self) -> unit)Drops every live element and frees the buffer. Runs when the last reference to the list goes away, not at the end of the binding's scope.
Returns: unit
impl(generic(T : Type), ArrayList(T), Trace(...))
trace : (fn(self : Self, tracer : GcTracer) -> unit)Cycle-GC tracing. The elements live in a malloc'd buffer the
compiler's auto-derived field walk cannot reach, so trace each
element's buffer slot. tracer.visit takes the slot POINTER and reads
it WITHOUT touching the element's reference count — a by-value managed
handle would be dup'd then dropped, freeing a live element
mid-collection.
Parameters
| Name | Type | Notes |
|---|---|---|
tracer | GcTracer |
Returns: unit
impl(generic(T : Type), ArrayList(T), IntoIterator(...))
Item : TIntoIter : ArrayListIter(T)into_iter : (fn(self : Self) -> ArrayListIter(T))Returns: ArrayListIter(T)
impl(generic(T : Type), ArrayList(T), ...)
iter : (fn(self : Self) -> ArrayListIterPtr(T))Borrowing walk over a shared handle: yields a *(T) into the list's
buffer and leaves the list intact — for(list.iter(), ptr => ...),
where into_iter would move the list and yield values (D14; the
iter / into_iter split every other std collection already had).
The receiver arrives RC-dup'd at the method boundary, so moving it into the iterator is balanced and the buffer outlives the walk. Do not grow the list mid-walk: a reallocation invalidates the pointers already yielded.
Returns: ArrayListIterPtr(T)
impl(generic(T : Type), where(T <: Eq(T)), ArrayList(T), ...)
dedup : (fn(self : Self) -> unit)Remove CONSECUTIVE duplicates, keeping the first of each run — Rust's
dedup. Only adjacent equals collapse, so [1,2,1] is unchanged; sort
first if you want set semantics.
Returns: unit
starts_with : (fn(self : Self, prefix : Self) -> bool)Whether self begins with prefix — Rust's starts_with.
Returns: bool
ends_with : (fn(self : Self, suffix : Self) -> bool)Whether self ends with suffix — Rust's ends_with.
Returns: bool
contains : (fn(self : Self, value : T) -> bool)Whether any element equals value — a linear scan, O(n), stopping at
the first match. Rust's Vec::contains.
Returns: bool
index_of : (fn(self : Self, value : T) -> Option(usize))Index of the FIRST element equal to value, or .None — a linear
scan, O(n). Rust spells this iter().position(...); on a sorted list
reach for binary_search instead.
Returns: Option(usize)
impl(generic(T : Type), ArrayList(T), ...)
reverse : (fn(self : Self) -> unit)Reverse the elements in place, O(n).
Exchanges raw slots, like swap, so no element is dup'd or dropped —
RC-neutral however T is refcounted.
Returns: unit
impl(generic(T : Type), where(T <: Ord(T)), ArrayList(T), ...)
sort : (fn(self : Self) -> unit)Sort ascending by Ord. STABLE (D17): equal elements keep their input
order, as sort does in Rust, Python, Java and Go.
O(n log n) with one usize index array plus scratch allocated for the
merge — sort_unstable is the allocation-free heapsort for when order
among equals does not matter.
Returns: unit
sort_unstable : (fn(self : Self) -> unit)Sort ascending by Ord WITHOUT preserving the order of equal elements
— Rust's sort_unstable. Heapsort: O(n log n) worst case, in place, no
allocation, no recursion (see _heapsort_by).
Returns: unit
impl(generic(T : Type), where(T <: Eq(T)), ArrayList(T), Eq(ArrayList(T))(...))
impl(generic(T : Type), ArrayList(T), Default(...))
default : (fn() -> Self)The default value of the type.
Returns: Self
impl(generic(T : Type), where(T <: Clone), ArrayList(T), Clone(...))
clone : (fn(inout(self) : Self) -> Self)Create an independent clone of self.
Returns: Self
impl(generic(T : Type), ArrayList(T), FromIterator(...))
Elem : Tfrom_iter_new : (fn() -> Self)The empty collection collect starts from.
Returns: Self
from_iter_add : (fn(acc : Self, item : T) -> Self)Add one element to a partially-built collection and return it.
Parameters
| Name | Type | Notes |
|---|---|---|
acc | Self | |
item | T |
Returns: Self
impl(generic(T : Type), ArrayList(T), ...)
sort_by : (fn(self : Self, less : Impl(Fn(a : T, b : T) -> bool)) -> unit)Sort by a caller-supplied strict "less" — less(a, b) is true when a
must come before b. STABLE: equal elements keep their input order,
matching Rust's sort_by. Use sort_unstable_by for the
allocation-free heapsort.
Returns: unit
sort_by_key : (
fn(
generic(K : Type),
self : Self,
key : Impl(Fn(v : T) -> K),
where(K <: Ord(K))
) -> unit
)Sort ascending by a KEY extracted from each element — Rust's
sort_by_key. Stable, like sort_by, which it delegates to.
The key type is a method-level generic(K : Type), which the clause
order requires to come BEFORE self; it is inferred from the closure at
the call site, so xs.sort_by_key((p) => p.age) needs no annotation.
Returns: unit
binary_search_by : (fn(self : Self, cmp : Impl(Fn(v : T) -> Ordering)) -> Result(usize, usize))Binary search over a list ordered by cmp — Rust's binary_search_by.
cmp reports how each ELEMENT orders against the target (.Less when
the element comes first), so searching does not require constructing a
T to compare against — which is the whole reason to reach for this
over binary_search. .Ok(index) of a match (any one of an equal run),
or .Err(insertion_index).
Returns: Result(usize, usize)
chunks : (fn(self : Self, size : usize) -> ArrayListChunks(T))Non-overlapping chunks of size, in order — Rust's chunks. The final
chunk is SHORT when the length is not a multiple of size.
PANICS on size == 0, as Rust does.
Returns: ArrayListChunks(T)
windows : (fn(self : Self, size : usize) -> ArrayListWindows(T))Overlapping windows of EXACTLY size, advancing one element at a time —
Rust's windows. Yields nothing when the list is shorter than size.
PANICS on size == 0, as Rust does.
Returns: ArrayListWindows(T)
sort_unstable_by : (fn(self : Self, less : Impl(Fn(a : T, b : T) -> bool)) -> unit)Sort by a caller-supplied strict "less" WITHOUT preserving the order of
equal elements (heapsort). Rust's sort_unstable_by.
Returns: unit
retain : (fn(self : Self, pred : Impl(Fn(v : T) -> bool)) -> unit)Keep only the elements pred accepts, in place and in order; the
rejected elements are dropped. One pass: the survivors are collected
into a fresh list, clear releases every original, extend moves the
survivors back — O(n) with a single allocation, and every RC step is one
of the module's proven public operations (push / clear / extend). The
first cut compacted through raw pointers and double-released RC elements
(the Linux-ASan heap-use-after-free on PR #313's first run; local ASan is
non-functional, so CI was the first to see it); the second walked
backwards over drain, which was O(n²) with an allocation per rejection
(issues/fixed/array-list-retain-is-quadratic.md).
Returns: unit
extend : (fn(self : Self, other : ArrayList(T)) -> unit)Append a copy of every element of other.
Returns: unit
impl(generic(T : Type), where(T <: Ord(T)), ArrayList(T), ...)
binary_search : (fn(self : Self, value : T) -> Result(usize, usize))Binary search over a SORTED list (ascending, sort's order):
.Ok(index) of a matching element (any one of an equal run), or
.Err(insertion_index) where the value would keep the order.
Returns: Result(usize, usize)
Error variants for ArrayList operations.
Variants
| Variant | Fields | Description |
|---|---|---|
AllocError | error: AllocError | Memory allocation failed. |
IndexOutOfBounds | index: usize, length: usize | Index is out of bounds for the current length. |
EmptyList | Attempted to access an element from an empty list. |
Value iterator over an ArrayList — yields each element BY VALUE, which
dups a refcounted T. This is what into_iter() (and therefore
for(list, ...)) returns; iter() yields pointers instead (D14).
It holds the list by handle and re-reads _length on every next, so a
push during a forward walk extends the walk. Double-ended: next_back
takes from the far end.
Type Parameters
| Name | Type | Notes |
|---|---|---|
T | Type | comptime |
Trait Implementations
impl(generic(T : Type), ArrayListIter(T), Iterator(...))
Item : Tnext : (fn(inout(self) : Self) -> Option(T))Advance the iterator and return the next value, or None when exhausted.
Returns: Option(T)
impl(generic(T : Type), ArrayListIter(T), DoubleEndedIterator(...))
Item : Tnext_back : (fn(inout(self) : Self) -> Option(T))Advance the iterator from the back and return the previous value, or
None when the two ends have met.
Returns: Option(T)
Pointer iterator over an ArrayList — yields *(T) INTO the list's own
buffer (D14). This is what iter() returns; into_iter() keeps yielding
values.
The elements are borrowed, not dup'd, so iterating a list of RC values
costs no refcount traffic and writing through the pointer mutates the list
in place. HashMapIterPtr established the shape.
The pointers alias the buffer, so any operation that REALLOCATES it
(push past capacity, reserve, shrink_to_fit) invalidates every
pointer already yielded. Live-length semantics are otherwise the same as
ArrayListIter's: _length is re-read on each next.
Type Parameters
| Name | Type | Notes |
|---|---|---|
T | Type | comptime |
Trait Implementations
impl(generic(T : Type), ArrayListIterPtr(T), Iterator(...))
Item : *(T)next : (fn(inout(self) : Self) -> Option(*(T)))Advance the iterator and return the next value, or None when exhausted.
Returns: Option(*(T))
impl(generic(T : Type), ArrayListIterPtr(T), DoubleEndedIterator(...))
Item : *(T)next_back : (fn(inout(self) : Self) -> Option(*(T)))Advance the iterator from the back and return the previous value, or
None when the two ends have met.
Returns: Option(*(T))
Iterator over non-overlapping CHUNKS of a list.
Yo has no slice type, so each chunk is a freshly allocated ArrayList(T)
COPY rather than a borrowed view — the honest rendering of Rust's chunks,
whose item is a &[T]. Mutating a yielded chunk does not touch the source.
Type Parameters
| Name | Type | Notes |
|---|---|---|
T | Type | comptime |
Trait Implementations
Iterator over overlapping WINDOWS of a list — see ArrayListChunks for why
each window is a copy.
Type Parameters
| Name | Type | Notes |
|---|---|---|
T | Type | comptime |
Trait Implementations
Functions
array_list macro — construct an ArrayList(T) literal.
The element type T is inferred from the first element via typeof, so
at least one element is required.
Example
xs := array_list(i32(1), i32(2), i32(3));
names := array_list(`alice`, `bob`);
Expands to:
{
__first := <first elem>;
__tmp := ArrayList(typeof(__first)).new();
__tmp.push(__first);
__tmp.push(<elem 2>);
...
__tmp
}
Returns: unquote(Expr)