Module collections/array_list

collections/array_list
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. — stable modules only change additively; this one may still change.

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

ArrayList type-function
fn(T : Type) -> Type

Generic growable array similar to Rust's Vec<T>. Heap-allocated with automatic resizing and reference-counted cleanup.

Type Parameters

NameTypeNotes
TTypecomptime

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 : T

The 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

NameTypeNotes
idxusize

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

NameTypeNotes
tracerGcTracer

Returns: unit

impl(generic(T : Type), ArrayList(T), IntoIterator(...))
Item : T
IntoIter : 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 : T
from_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

NameTypeNotes
accSelf
itemT

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), ...)
ArrayListError

Error variants for ArrayList operations.

Variants

VariantFieldsDescription
AllocErrorerror: AllocError

Memory allocation failed.

IndexOutOfBoundsindex: usize, length: usize

Index is out of bounds for the current length.

EmptyList

Attempted to access an element from an empty list.

ArrayListIter type-function
fn(T : Type) -> Type

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

NameTypeNotes
TTypecomptime

Trait Implementations

impl(generic(T : Type), ArrayListIter(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), ArrayListIter(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)

ArrayListIterPtr type-function
fn(T : Type) -> Type

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

NameTypeNotes
TTypecomptime

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))

ArrayListChunks type-function
fn(T : Type) -> Type

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

NameTypeNotes
TTypecomptime

Trait Implementations

impl(generic(T : Type), ArrayListChunks(T), Iterator(...))
Item : ArrayList(T)
next : (fn(inout(self) : Self) -> Option(ArrayList(T)))

Advance the iterator and return the next value, or None when exhausted.

Returns: Option(ArrayList(T))

ArrayListWindows type-function
fn(T : Type) -> Type

Iterator over overlapping WINDOWS of a list — see ArrayListChunks for why each window is a copy.

Type Parameters

NameTypeNotes
TTypecomptime

Trait Implementations

impl(generic(T : Type), ArrayListWindows(T), Iterator(...))
Item : ArrayList(T)
next : (fn(inout(self) : Self) -> Option(ArrayList(T)))

Advance the iterator and return the next value, or None when exhausted.

Returns: Option(ArrayList(T))

Functions

array_list function
fn(...(quote(elems))) -> unquote(Expr)

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)