Collections
This page covers Aura's three generic owned collection types:
list[T]for ordered sequencesdict[K, V]for key/value lookupset[T]for uniqueness and membership
str is the immutable owned UTF-8 text type. Collections are move types. A bare parameter or loop grants shared access. mut grants exclusive mutable access, and own transfers the value.
Literals And Constructors
List literals and constructors are homogeneous:
values = [1, 2, 3]
empty: list[int32] = []
other = list[int32]()A dictionary literal evaluates its entries from left to right, each key before its value. A repeated equal key updates the value in the slot where the key was first inserted.
counts = {"ready": 1, "done": 2}
empty: dict[str, int32] = {}
other = dict[str, int32]()A set literal uses braces when the context expects a set[T]. Without that context {} is a dictionary literal, so an empty set takes its type from an annotation, a parameter, a field, or a return type, or from the typed constructor.
seen: set[int32] = {1, 2, 3}
empty: set[int32] = {}
other = set[int32]()Every literal is homogeneous in each type position. The checker rejects mixed list or set element types, mixed dictionary key types, and mixed dictionary value types with AU2002. Integer literals take their type from context. Aura does not convert a value that already has a type, and it does not infer a union for a collection literal.
Equality Requirement
An operation that compares stored values needs equality for the relevant type. These operations compare values:
- list
remove,index, andcount inandnot in- set insertion
- every dictionary-key operation
Callables, random.Rng, opaque FFI handles, and values that contain any of them have no equality. The compiler rejects the operation with AU2008 and names the unmet requirement.
Iteration
Lists support shared, consuming, and mutable place iteration. Sets support shared and consuming iteration, but not mutable iteration.
for value in values:
print(value)
for value in own values:
consume(value)
for value in mut values:
value += 1A bare list or set loop freezes the collection for the duration of the loop. own selects the source once and consumes it.
Each list loop step takes constant time. A bare loop reads the length once, because the frozen list cannot change length. An own loop takes its elements in order without shifting the rest.
Dictionaries are not iterated directly. They expose eager owned snapshots. items() returns key/value tuples in insertion order:
for key, value in counts.items():
print(key + "=" + value.to_string())Comprehensions use the bare shared form and eagerly build fresh owned collections:
squares = [value * value for value in values]
even = {value for value in values if value % 2 == 0}
labels = {value: str(value) for value in values}
Nested clauses run in outer-major order, and filters run from left to right. A dictionary comprehension evaluates its key before its value. The output owns each element, key, and value it produces.
Reductions
sum, min, max, any, and all reduce a collection to one value. They take a list[T], set[T], Span[T], or Range. sum, min, and max also take an Array[T].
def main():
scores: list[int32] = [72, 95, 88]
print(sum(scores)) # 255
print(min(scores)) # 72
print(max(scores)) # 95
print(sum(range(1, 11))) # 55
print(all([score >= 70 for score in scores])) # true
print(any([score > 90 for score in scores])) # true| Call | Element type | Result | Empty collection |
|---|---|---|---|
sum(values) | numeric T | T | zero |
min(values), max(values) | numeric T | T | stops with AU4001 |
any(values) | bool | bool | false |
all(values) | bool | bool | true |
A Range has int64 elements. The collection is read through shared access and stays usable.
sum adds from left to right with the checked + of T, so an overflow stops the program with AU4002. min and max compare the same way as min(left, right) and max(left, right), and the first of equal values wins. any stops at the first true element, and all stops at the first false one. An Array argument uses the Array's own sum(), min(), or max(); see Numeric Arrays.
min and max also take two or more numbers of one exact type, as in min(3, 1, 2). A single number is an error: pass a collection or at least two values.
list[T]
list[T] keeps elements in order. len(values) and values.len() return int64.
view name = values[index] and view mut name = values[index] bind one element in place. You can read or assign a field of a class element through the index, as in values[index].field, without copying the element. See Statements.
| Method | Signature | Contract |
|---|---|---|
append | append(value: own T) -> None | Moves value to the end. |
pop | pop(index: int64 = -1) -> T | Removes the element at the normalized position and moves it out. |
remove | remove(value: T) -> None | Removes the first equal element. |
index | index(value: T) -> int64 | Returns the position of the first equal element. |
count | count(value: T) -> int64 | Counts equal elements. |
insert | insert(index: int64, value: own T) -> None | Moves value in before the clamped position. |
extend | extend(other: own list[T]) -> None | Moves every element of other to the end, in order. |
clear | clear() -> None | Removes all elements. |
reverse | reverse() -> None | Reverses the list in place. |
sort | sort(reverse: bool = false) -> None | Stably sorts an orderable list in place. |
sort | sort[K](key: def(T) -> K, reverse: bool = false) -> None | Stably sorts by keys computed once per element. |
copy | copy() -> list[T] | Returns independent owned storage. Requires clone-safe T. |
get | get(index: int64) -> Lookup[T] | Returns Lookup.Found(value) with a cloned element, or Lookup.Missing. Requires clone-safe T. |
lookup | match [mut] items.lookup(index): | Runs the Found arm with a view of the element, or the Missing arm. Only valid as the subject of a match statement. |
set | set(index: int64, value: own T) -> T | Replaces the element at a position and moves the old element out. |
swap | swap(first: int64, second: int64) -> None | Swaps two positions. |
reserve | reserve(additional: int64) -> None | Ensures room for len() + additional elements. |
with_capacity | list[T].with_capacity(minimum: int64) -> list[T] | Creates an empty list with at least the requested capacity. |
map and filter are eager and leave the source list in place. map owns each callback result. filter clones the accepted elements, so it needs a clone-safe element type.
Positions, Indexing, And Slicing
Direct indexing, get, set, swap, and pop normalize a negative position once as len() + index. The result must be in 0..len().
- An invalid direct index, or an invalid
pop,set, orswapposition, traps withAU4003. getreturnsLookup.Missingfor an invalid position. A present element whose value isNonestays distinct from an absent one.
match values.get(index):
case Lookup.Found(value):
print(value)
case Lookup.Missing:
print("missing")def main():
mut values = [10, 20, 30]
print(values[-1])
print(values.get(-2))Looking Up Without Cloning
get clones the element, so it needs a clone-safe T. To read or change an element in place when the position may be invalid, match on lookup:
class Job:
name: str
runs: int64
def main():
mut jobs = [Job(name="build", runs=1)]
match mut jobs.lookup(0):
case Lookup.Found(job):
job.runs += 1
case Lookup.Missing:
print("no job")
print(jobs[0].runs)lookup never produces a value. It exists only as the subject of a match statement, and its arms follow these rules:
case Lookup.Found(name):binds a view of the element for that arm. A barematchgives a shared view;match mutgives a mutable view whose writes reach the list.Found(_)binds nothing.case Lookup.Missing:orcase _:runs when the position is invalid.- The position is evaluated once. Arms cannot have guards, and
match ownis refused; usepoporsetto take an element out. - While the
Foundview is live, the list cannot change shape (AU3011). TheMissingarm holds no view, so it may append or insert. - For a Copy element such as
int64, a barelookupbinds a copy, so the arm can return or store it.
pop() removes the final element and traps on an empty list. remove(value) and index(value) search from the start and trap with AU4008 when the value is absent. count(value) returns zero when the value is absent.
insert clamps the way Python does:
- A negative index first has the current length added to it.
- A result below zero becomes zero.
- A result above the length becomes the length.
The value goes in before that effective position.
List positions and written slice endpoints are in the int64 index domain. int8, int16, int32, uint8, uint16, and uint32 values widen losslessly in these positions. This rule applies only to positions. It does not convert ordinary assignments or function arguments.
A one-colon slice returns a fresh owned list:
- Endpoints are half-open and may be omitted.
- A negative endpoint is normalized once.
- Both effective endpoints must be in
0..=len(), and the start must not exceed the end. Invalid or reversed bounds trap withAU4003. - The slice copies Copy elements and clones clone-safe non-Copy elements.
str slicing uses the same position rules, counts Unicode scalar values, and returns a fresh valid UTF-8 str. You cannot index a str with an integer.
Stable Sorting
These are the four forms of sort:
def make_key(value: int64) -> int64:
return -value
def main():
mut values = [3, 1, 2]
values.sort()
values.sort(reverse=true)
values.sort(key=make_key)
values.sort(key=make_key, reverse=true)Natural sorting requires T: Ord. Key sorting requires an orderable key type. Equal elements or keys keep their input order in both directions.
The key function runs exactly once per element, from first to last. All keys are stored before the list changes. If an argument, key, ordering, or allocation fails before that point, the list is unchanged.
Span[T]
Span[T] is a view of a contiguous range of a list[T]. It reads and writes the list's elements in place and never copies them.
def total(values: Span[int64]) -> int64:
mut sum = 0
for value in values:
sum += value
return sum
def scale(values: mut Span[int64], factor: int64):
for value in mut values:
value *= factor
def middle(values: list[int64]) -> view Span[int64] from values:
return view values[1:-1]
def main():
mut scores = [4, 9, 2, 7, 5]
print(total(scores[1:4]))
print(total(scores))
view mut tail = scores[2:]
tail.sort()
tail[0] = 1
print(len(tail))
print(tail.to_list())
print(scores)
scale(scores[:2], 10)
print(scores)
view inner = middle(scores)
print(inner.to_list())
print(90 in inner)Creating A Span
view part = values[start:end] lends a range of values. view mut part = values[start:end] lends it for writing and needs a mutable list.
- Endpoints follow the slice rules:
int64endpoints, an omitted endpoint means the start or end, and a negative endpoint counts from the end once. - A span evaluates its base, start, and end once each, in that order. In
rows[i][a:b],iruns beforeaandb. - An invalid or reversed range traps with
AU4003when the span is created. - Slicing a span with
view inner = part[1:]makes a nested span. Its endpoints are positions inpart, and it must fit insidepart. view alias = partreborrows a span.aliascovers the same range aspart. ASpan[T]parameter reborrows the same way.
A parameter of type Span[T] or mut Span[T] accepts a slice such as total(values[1:3]), a whole list such as total(values), or an existing span. A mut Span[T] parameter needs a mutable list or a mutable span.
Span Operations
Positions in a span count from the start of its range. An invalid position traps with AU4003.
| Operation | Form | Contract |
|---|---|---|
| length | len(part), part.len() | Returns the length of the range. |
| indexing | part[i], part[i] = value | Reads or writes one element. Writing needs a mutable span. |
| iteration | for x in part:, for x in mut part: | Visits the range in order. enumerate and zip count from 0. |
| membership | value in part | Tests the range for an equal element. |
| equality | part == other | Compares the range with a list or another span. |
index | index(value: T) -> int64 | Returns the position of the first equal element. Traps with AU4008 when no element is equal. |
count | count(value: T) -> int64 | Counts equal elements in the range. |
reverse | reverse() -> None | Reverses the range in place. Needs a mutable span. |
sort | sort(reverse: bool = false) -> None | Stably sorts the range in place. Needs a mutable span. |
swap | swap(first: int64, second: int64) -> None | Swaps two positions. Needs a mutable span. |
fill | fill(value: T) -> None | Writes value to every position. Needs a mutable span and a Copy T. |
to_list | to_list() -> list[T] | Returns an owned copy of the range. Requires clone-safe T. |
A span has a fixed length, so it has no append, pop, insert, or other member that changes the list's length. Calling one reports AU2001. A value slice of a span, such as copy = part[1:], is an owned list[T].
Returning A Span
A function can return a range of a list parameter. Declare -> view Span[T] from values and return a slice of values, the whole of values, a span of it, or another call that returns one.
def middle(values: list[int64]) -> view Span[int64] from values:
return view values[1:-1]
def main():
scores = [4, 9, 2, 7]
view inner = middle(scores)
print(inner.to_list())Bind the result with view, or view mut for a view mut Span[T] result. Using the call in any other position reports AU3010. A method cannot return a span of its receiver.
The call evaluates its origin argument once. For an element or entry origin, such as middle(rows[i]), the view covers the element that the call selected.
Span Ownership
A span holds a loan of its whole list. While a shared span is live, the list can be read but not changed. While a mutable span is live, the list is reachable only through that span. A conflicting access reports AU3002.
A span is a view, so it cannot be stored or moved:
- It cannot be a field, an owned local, an
ownparameter, an owned result, a type argument, or a tuple element. These reportAU3010. - It cannot be a union member (
AU2010) or cross a task boundary (AU3008). - A function with a
Span[T]parameter or result cannot be used as a function value (AU3010). Call it directly. - A span has no text form.
print(part)reportsAU2002. Printpart.to_list()instead.
dict[K, V]
Dictionaries keep insertion order for iteration and snapshots. A hash index over the keys makes indexing, assignment, get, and membership take expected constant time. remove keeps the order of the remaining entries, so it takes time proportional to the dictionary's size. Indexing, assignment, and membership are the main ways to look up and store entries:
value = table[key]
table[key] = value
present = key in table
| Method | Signature | Contract |
|---|---|---|
get | get(key: K) -> Lookup[V] | Returns Lookup.Found(value) with a cloned value, or Lookup.Missing when the key is absent. Requires clone-safe V. |
remove | remove(key: K) -> Lookup[V] | Removes the entry and moves its value into Lookup.Found(value), or returns Lookup.Missing. |
lookup | match [mut] table.lookup(key): | Runs the Found arm with a view of the value, or the Missing arm. Only valid as the subject of a match statement. |
keys | keys() -> list[K] | Returns cloned keys in insertion order. |
values | values() -> list[V] | Returns cloned values in insertion order. |
items | items() -> list[(K, V)] | Returns cloned key/value tuples in insertion order. |
copy | copy() -> dict[K, V] | Returns independent owned storage. |
update | update(other: own dict[K, V]) -> None | Moves the entries of other into the dictionary in insertion order. |
clear | clear() -> None | Removes all entries. |
reserve | reserve(additional: int64) -> None | Ensures room for len() + additional entries. |
with_capacity | dict[K, V].with_capacity(minimum: int64) -> dict[K, V] | Creates an empty dictionary with at least the requested capacity. |
Clone-safety requirements for the snapshot methods:
keys()andcopy()require clone-safeK.values()requires clone-safeV.items()requires both.
These methods return eager snapshots, not live views. For a live entry, use view name = table[key] for shared access or view mut name = table[key] for mutable access. See Statements.
get takes no default argument. Lookup[V] represents absence: a missing key gives Lookup.Missing. A present value gives Lookup.Found(value), even when V is an optional T | None and the stored value is None.
def bump(counts: mut dict[str, int32], key: own str):
match counts.get(key):
case Lookup.Found(count):
counts[key] = count + 1
case Lookup.Missing:
counts[key] = 1To read or update a value in place without cloning it, match on lookup. It follows the same rules as list lookup, with the key read in place:
def record_visit(pages: mut dict[str, list[str]], user: own str, page: own str):
match mut pages.lookup(user):
case Lookup.Found(visited):
visited.append(page)
case Lookup.Missing:
pages[user] = [page]An indexed read follows the collection ownership rule for V. It traps with AU4003 when the key is absent. Indexed assignment moves its key and value in as needed. It inserts an absent key. For an equal key, it updates the value and keeps the key's insertion position. update follows the same position rule.
set[T]
A set stores one value per equality class and keeps insertion order. Test membership with in and not in. Membership and add take expected constant time. remove and discard keep the order of the remaining values, so they take time proportional to the set's size.
| Method | Signature | Contract |
|---|---|---|
add | add(value: own T) -> None | Moves a value into the set. |
remove | remove(value: T) -> None | Removes an equal value. Traps with AU4008 when the value is absent. |
discard | discard(value: T) -> None | Removes an equal value if one is present. |
copy | copy() -> set[T] | Returns independent owned storage. Requires clone-safe T. |
clear | clear() -> None | Removes all values. |
reserve | reserve(additional: int64) -> None | Ensures room for len() + additional values. |
with_capacity | set[T].with_capacity(minimum: int64) -> set[T] | Creates an empty set with at least the requested capacity. |
add, remove, discard, and membership require equality for T. The mutating methods return None, so use membership to check whether a value is present.
def main():
mut ids = set[int32]()
ids.add(42)
ids.discard(7)A non-empty set prints as {first, second} in its iteration order. An empty set prints as set().
Equality, Copying, And Capacity
Equality consumes neither operand:
- Lists are equal when their elements are equal in order.
- Dictionaries are equal when they hold equal key/value mappings.
- Sets are equal when they have equal members.
copy creates independent owned storage, subject to the clone-safety requirements above. Removing methods move stored values out. Shared lookup and search operations leave the collection and their arguments in place.
A copy of a collection that cannot hold an Array shares its storage with the original until either side changes. The first change copies the storage, so each copy stays independent and copying takes constant time.
reserve(additional) guarantees a capacity of at least len() + additional. with_capacity(minimum) creates an empty collection with a capacity of at least minimum. Neither changes contents or order.
- A negative value traps with
AU4003. - Overflow, a runtime-limit violation, or allocation failure traps with
AU4005. - A failed
reserveleaves the collection unchanged.
This program exercises collection literals, eager algorithms, stable sorting, set deduplication, and comprehension order:
def doubled(value: int32) -> int32:
return value * 2
def is_even(value: int32) -> bool:
return value % 2 == 0
def descending_key(value: int32) -> int32:
return -value
def main():
values: list[int32] = [3, 1, 2, 4]
middle = values[1:3]
mapped = values.map(doubled)
filtered = values.filter(is_even)
mut ascending = values.copy()
ascending.sort()
mut descending = values.copy()
descending.sort(key=descending_key)
squares = [value * value for value in values]
even_squares = [value * value for value in values if value % 2 == 0]
remainders: set[int32] = {value % 3 for value in values}
labels = {value: value * 10 for value in values if value >= 3}
pairs = [
left * 10 + right
for left in values if left < 3
for right in values if right < 3
]
assert 0 in remainders
assert 1 in remainders
assert 2 in remainders
print(middle)
print(mapped)
print(filtered)
print(ascending)
print(descending)
print(values)
print(squares)
print(even_squares)
print(labels)
print(pairs)Grammar
Grammar defines the productions for literals, comprehensions, constructors, indexing, slicing, indexed assignment, method calls, and loop ownership modes. In a non-empty brace literal, the first colon selects dictionary syntax. {} is a dictionary literal.
Typing Rules
Collection specializations are invariant and homogeneous. An empty literal needs an expected type. Mutating methods and indexed assignment need a mutable collection place.
A direct dictionary read follows the ownership rule for V. get gives a cloned Lookup[V] for clone-safe V. lookup views any V in place inside a match. remove moves out any stored V.
List callbacks must have exact shared function types:
| Operation | Callback type |
|---|---|
map | def(T) -> U |
filter | def(T) -> bool |
keyed sort | def(T) -> K with K: Ord |
The callback must be repeatable. It can be a named function, a repeatable closure, or a packed Shared Callable[...] or TaskCallable[...] value with the matching contract. The operation borrows a packed value for the call. It does not clone the value or erase its contract.
- A Mutable or Consuming packed value is rejected with
AU2002. - The operation calls the callback positionally, so a keyword-only element parameter is rejected with
AU2004.
Runtime Semantics
Every collection expression evaluates once, from left to right. Lists and dictionaries keep the order described on this page. Sets collapse equal duplicates. Comprehensions are eager and run as nested loops. If a trap occurs, any partly built collection is cleaned up.
Ownership And Evaluation Order
Collection storage owns its non-Copy elements, keys, and values. Shared lookups and searches leave their inputs in place.
append,insert,extend,add,update, indexed assignment, and comprehension output move owned values in.- Value slices and
copyproduce independent storage. ASpan[T]view lends the list instead. - No collection operation inserts a hidden clone.
Diagnostics
| Code | Cause |
|---|---|
AU2001 | Unknown collection type or member, or a length-changing member on a span. |
AU2002 | Type, arity, homogeneity, or callback mismatch. |
AU3001 | Use after move. |
AU3002 | Conflicting access, including use of a list while a span of it is live. |
AU3010 | A span in an owned position, or an unbound call that returns a span. |
AU3003 | Mutation through an immutable place. |
AU3005, AU3006, AU3007, AU3009 | Ownership or clone-safety violation. |
AU4003 | Invalid position or span range, or a missing key in a direct dictionary read. |
AU4005 | Allocation or capacity failure. |
AU4008 | Missing value for list or span index, list remove, or set remove. |
Backend Support
The MIR and direct backends implement the same collection types, methods, ordering, ownership, evaluation, rendering, and diagnostics. The compiler's analysis and the language server use the same builtin signatures.
Limits And Implementation-Defined Behavior
These are not available:
- mutable set iteration and direct dictionary iteration
- set algebra and set relations
- arbitrary user-defined iterables and generator expressions
- slice steps and slice assignment
- spans over
str,Array, or a receiver field returned from a method - integer indexing of
str
Set order is not part of the API contract. Allocation is limited by host resources and the runtime's size caps.
Status
The collection contract on this page is the Aura 0.6 surface.
Design record: ADR-0044: Canonical collection surface.