Collections
For primitive-field class records scanned by field, see columnar lists. Their packed columns and borrowed scalar pipelines have a deliberately smaller interface than ordinary lists.
list[T], dict[K, V], and set[T] have concrete element types, including
nested collections. Dictionary keys and set members are integers, bool, str,
bytes, or tuples, classes, enums and anonymous sums recursively containing those types.
Every class field and every payload of every variant must be a supported key
type. Hashing includes class fields, variant tags and payload contents,
independently of where the value is stored. Ordinary classes move into a
collection; @copy classes keep their usual copy behavior. Stored keys are
exposed only through shared borrows, so their identity cannot change in place.
Floats, collections, boxes and recursive types are not supported keys.
Keys with custom __del__ cleanup are supported, including nested cleanup,
but cannot be duplicated: copying collections, non-mutating set algebra and
key-view to_set() reject these keys. Borrowing and ownership transfers work normally. User-defined hash/equality hooks remain unsupported. See the
heterogeneous collections tutorial.
Unit elements are rejected. Sets of supported finite enums or tuples with at most 256 possible values
use bitsets instead; they do not hash or request entropy.
Dictionary and hash-backed set bucket selection uses SipHash-1-3 over the complete key,
the schedule Rust’s HashMap, CPython and Swift use. A str, bytes,
integer or bool key is hashed as its contents alone; keys nested in tuples, classes and
enums are framed by field count, variant tag and string length. One secret 128-bit key is
obtained from OS entropy on the process’s first hash operation and shared across
worker threads. Initialization and hashing allocate no runtime storage.
Constructing or reserving an empty collection does not need entropy. The first
hash may wait for the OS entropy source to initialize. If the OS cannot
provide the key, the operation traps with collection hashing requires OS entropy; there is no predictable fallback. Interrupted reads retry.
Randomized bucket placement makes precomputed hash-flooding inputs harder to construct; it does not guarantee worst-case constant-time lookup. Hash values are private implementation details, not persistent identifiers or cryptographic digests. No test-mode fixed seed is needed: dictionary insertion order is independent of buckets. Set iteration order remains unspecified.
Lists preserve order and duplicates. Dictionaries preserve first insertion order: replacing a value keeps its key’s position. Sets remove duplicates and promise no iteration order. Equality is structural: list order matters; dictionary insertion order and set order do not. Structural comparisons allocate nothing. Collections have no identity test.
len(collection) -> i64, in, and not in observe collections. Dictionary
membership tests keys. Indexed reads use i64 list indices (including negative
indices) or the dictionary’s key type; missing keys and out-of-bounds indices
trap with exit status
one, reporting the source position. An index is one expression evaluated once; multiple
comma-separated indices are rejected. std.bytes.Bytes
uses the list index rules for buffer[i] reads and writes.
len(value) also calls an accessible ordinary def len(self) -> i64 method
on a class or enum, including standard-library containers such as
Deque and Bytes.
It borrows the receiver, including a temporary, and evaluates it once. A mutable
or owned receiver, extra parameters, method type parameters, or another return
type does not meet this contract; call such methods directly instead. The method
body determines its cost and whether it allocates. Built-in collections retain
their intrinsic length operations. Shared arguments follow the ordinary
call borrowing rules:
len(&"ada"), len(&b"abc"), and len(&make_collection()) are valid.
Borrowing itself does not allocate; producing the argument may allocate.
in and not in do not dispatch to methods on classes or enums, even if they
have an ordinary contains method. Use the container’s explicit API; for
example, Bytes.find searches a byte range.
A type being iterable or indexable does not imply that it supports membership.
Collections move on assignment, owned calls, and returns. Independent duplication uses copy. Indexed reads do not extract affine owners: use an explicit element reference, an owned removal operation, or copying. Element loans protect the whole collection unless they use distinct nonnegative integer literal list indices. See borrowing and iteration.
Flat unpacking also consumes a list: a, b = values requires exactly two
elements and transfers their owners; _ drops an ignored element. A length
mismatch traps at the unpacking expression before any target is written.
Unpacking itself allocates nothing. Borrowed lists, nested targets, and starred
targets are unsupported; explicitly copy a list first to preserve it.
replace(collection[index], value) transfers the previous element out while
writing its replacement. It requires exclusive access, evaluates the replacement
before the target index, and adds no allocation. List indices must exist.
Dictionaries reject it, as they reject all
indexed writes; use replace(*value, replacement) on a
reference from get_mut.
Shared mutation rules
Mutation requires a named mut binding, mutable class field, or exclusive
reference. Arguments and indices evaluate before the target is exclusively
borrowed, so items.append(len(items)) is valid. Active conflicting loans
still reject mutation; temporary mutation receivers are unsupported.
Copying and borrowed lookup
Optional lookups named get, and peek for containers with an order such as
caches and heaps, return a copy: dictionary.get(key) -> Option[V] and
list.get(index) -> Option[T]. Indexing traps on a missing key or index
instead.
A copying get has no hidden cost. It accepts only
copyable values:
scalars, immutable strings, ranges, function values, instances of @copy
classes, and tuples or enums of those. Copying one is a fixed-size copy; a
string shares its immutable storage. Neither allocates nor duplicates a buffer.
Values that own storage, including every collection and every class instance
not declared @copy, are affine, and get rejects them
at compile time (“get cannot return owned dictionary values”), so owned data is
never copied by accident.
Copying is the default because the common case reads a small value and uses it
directly, as in counts.get(key, 0) + 1. A borrowing get would need a
dereference at each such use, and its get(key, default) form could not exist:
returning either the stored value or the default would choose between two
borrows, which returned references
do not allow.
Borrowed forms take a suffix: get_ref and peek_ref for shared references,
get_mut and peek_mut for exclusive ones. Builtin and standard library
containers follow the same rule, so the method name tells the reader whether
they hold a copy or a borrow. User functions can return
Option[&V]. Of the
builtin containers, only dictionaries provide an optional borrowed lookup,
get_mut; get_ref is not provided yet.
collection.clear() -> () removes all entries while retaining capacity and
allocating no runtime storage. Removed owners drop before return: lists in
element order, dictionaries in insertion order with keys before values, sets in
unspecified order. Independent immutable owners survive. Empty clear is harmless.
See construction and copying for constructors, growth, reservation, and failures, then lists, dictionaries, and sets for their operations.