Skip to content

Collections ​

This page covers Aura's three generic owned collection types:

  • list[T] for ordered sequences
  • dict[K, V] for key/value lookup
  • set[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:

Aura
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.

Aura
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.

Aura
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, and count
  • in and not 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.

Aura
for value in values:
    print(value)

for value in own values:
    consume(value)

for value in mut values:
    value += 1

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

Aura
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.

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.

MethodSignatureContract
appendappend(value: own T) -> NoneMoves value to the end.
poppop(index: int64 = -1) -> TRemoves the element at the normalized position and moves it out.
removeremove(value: T) -> NoneRemoves the first equal element.
indexindex(value: T) -> int64Returns the position of the first equal element.
countcount(value: T) -> int64Counts equal elements.
insertinsert(index: int64, value: own T) -> NoneMoves value in before the clamped position.
extendextend(other: own list[T]) -> NoneMoves every element of other to the end, in order.
clearclear() -> NoneRemoves all elements.
reversereverse() -> NoneReverses the list in place.
sortsort(reverse: bool = false) -> NoneStably sorts an orderable list in place.
sortsort[K](key: def(T) -> K, reverse: bool = false) -> NoneStably sorts by keys computed once per element.
copycopy() -> list[T]Returns independent owned storage. Requires clone-safe T.
getget(index: int64) -> Lookup[T]Returns Lookup.Found(value) with a cloned element, or Lookup.Missing. Requires clone-safe T.
lookupmatch [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.
setset(index: int64, value: own T) -> TReplaces the element at a position and moves the old element out.
swapswap(first: int64, second: int64) -> NoneSwaps two positions.
reservereserve(additional: int64) -> NoneEnsures room for len() + additional elements.
with_capacitylist[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, or swap position, traps with AU4003.
  • get returns Lookup.Missing for an invalid position. A present element whose value is None stays distinct from an absent one.
Aura
match values.get(index):
    case Lookup.Found(value):
        print(value)
    case Lookup.Missing:
        print("missing")
Aura
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:

Aura
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 bare match gives a shared view; match mut gives a mutable view whose writes reach the list. Found(_) binds nothing.
  • case Lookup.Missing: or case _: runs when the position is invalid.
  • The position is evaluated once. Arms cannot have guards, and match own is refused; use pop or set to take an element out.
  • While the Found view is live, the list cannot change shape (AU3011). The Missing arm holds no view, so it may append or insert.
  • For a Copy element such as int64, a bare lookup binds 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:

  1. A negative index first has the current length added to it.
  2. A result below zero becomes zero.
  3. 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 with AU4003.
  • 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:

Aura
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.

Aura
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: int64 endpoints, 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], i runs before a and b.
  • An invalid or reversed range traps with AU4003 when the span is created.
  • Slicing a span with view inner = part[1:] makes a nested span. Its endpoints are positions in part, and it must fit inside part.
  • view alias = part reborrows a span. alias covers the same range as part. A Span[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.

OperationFormContract
lengthlen(part), part.len()Returns the length of the range.
indexingpart[i], part[i] = valueReads or writes one element. Writing needs a mutable span.
iterationfor x in part:, for x in mut part:Visits the range in order. enumerate and zip count from 0.
membershipvalue in partTests the range for an equal element.
equalitypart == otherCompares the range with a list or another span.
indexindex(value: T) -> int64Returns the position of the first equal element. Traps with AU4008 when no element is equal.
countcount(value: T) -> int64Counts equal elements in the range.
reversereverse() -> NoneReverses the range in place. Needs a mutable span.
sortsort(reverse: bool = false) -> NoneStably sorts the range in place. Needs a mutable span.
swapswap(first: int64, second: int64) -> NoneSwaps two positions. Needs a mutable span.
fillfill(value: T) -> NoneWrites value to every position. Needs a mutable span and a Copy T.
to_listto_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.

Aura
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 own parameter, an owned result, a type argument, or a tuple element. These report AU3010.
  • 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) reports AU2002. Print part.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
MethodSignatureContract
getget(key: K) -> Lookup[V]Returns Lookup.Found(value) with a cloned value, or Lookup.Missing when the key is absent. Requires clone-safe V.
removeremove(key: K) -> Lookup[V]Removes the entry and moves its value into Lookup.Found(value), or returns Lookup.Missing.
lookupmatch [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.
keyskeys() -> list[K]Returns cloned keys in insertion order.
valuesvalues() -> list[V]Returns cloned values in insertion order.
itemsitems() -> list[(K, V)]Returns cloned key/value tuples in insertion order.
copycopy() -> dict[K, V]Returns independent owned storage.
updateupdate(other: own dict[K, V]) -> NoneMoves the entries of other into the dictionary in insertion order.
clearclear() -> NoneRemoves all entries.
reservereserve(additional: int64) -> NoneEnsures room for len() + additional entries.
with_capacitydict[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() and copy() require clone-safe K.
  • values() requires clone-safe V.
  • 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.

Aura
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] = 1

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

Aura
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.

MethodSignatureContract
addadd(value: own T) -> NoneMoves a value into the set.
removeremove(value: T) -> NoneRemoves an equal value. Traps with AU4008 when the value is absent.
discarddiscard(value: T) -> NoneRemoves an equal value if one is present.
copycopy() -> set[T]Returns independent owned storage. Requires clone-safe T.
clearclear() -> NoneRemoves all values.
reservereserve(additional: int64) -> NoneEnsures room for len() + additional values.
with_capacityset[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.

Aura
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 reserve leaves the collection unchanged.

This program exercises collection literals, eager algorithms, stable sorting, set deduplication, and comprehension order:

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

OperationCallback type
mapdef(T) -> U
filterdef(T) -> bool
keyed sortdef(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 copy produce independent storage. A Span[T] view lends the list instead.
  • No collection operation inserts a hidden clone.

Diagnostics ​

CodeCause
AU2001Unknown collection type or member, or a length-changing member on a span.
AU2002Type, arity, homogeneity, or callback mismatch.
AU3001Use after move.
AU3002Conflicting access, including use of a list while a span of it is live.
AU3010A span in an owned position, or an unbound call that returns a span.
AU3003Mutation through an immutable place.
AU3005, AU3006, AU3007, AU3009Ownership or clone-safety violation.
AU4003Invalid position or span range, or a missing key in a direct dictionary read.
AU4005Allocation or capacity failure.
AU4008Missing 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.

Aura 0.6.0 technical preview. Implementation baseline: 4d7e076de9ff6e2e056cdec10881d4bf421358de.