Browse the handbook
Language

List, Dictionary, and Set

Use mixed values, nested collections, stable numeric sorting, ordered keys, and unique scalar members, with complete time and space complexity.

List, Dictionary, and Set are built in Aner types and constructors. They need no import and no <T>. Their contents retain their concrete runtime types: integer, float, String, Bool, null, user object, native value, or another collection. Dictionary values support the same mixed contents; Dictionary keys and Set members follow the scalar key rules below. Unit () is not a payload.

Use a rebuilt native executable with the compatible editor extension. Installing the VSIX alone does not add collection support to an older Aner executable. Try the basic example, nested collections, or the shared state notebook:

sh
aner check examples/collections_basic.aner
aner examples/collections_basic.aner
aner examples/collections_nested.aner

For explicit homogeneous contiguous storage, use Array and DynamicArray. They require a dtype, offer fixed or dynamic length, and support optimized sorting/search and numerical loops. List remains the mixed value container.

Start with mixed values

aner
let values = List(10, "hello", 1.5, true, null)
values.append("last")
print(values)
print(cast<Int64>(values.get(0)) + 5)

let record = Dictionary()
record.set("name", "Aner")
record.set("values", values)
print(cast<List>(record.get("values")).len())

let unique = Set(1, "1", 1.0, true, null, 1)
print(unique.len())

Output:

text
List(10, "hello", 1.5, true, null, "last")
15
6
5

The List and Dictionary value accessors return static type Any. Use type_of(value) to inspect its concrete type or cast<Type>(value) when a typed operation is needed. Casts check the value and do not convert it. For example, integer 1 cannot be cast to Float64; Float64(cast<Int64>(value)) performs the explicit numeric conversion. Invalid casts report R1002.

let values: List = List() is supported, as are Dictionary/Set parameters, fields, results, casts, and concrete arguments to user generic classes. List<Int64> and other typed collection specializations are not supported. A binding inferred as List cannot later hold a Dictionary; use an explicit Any binding when that variation is intended. List, Dictionary, and Set are reserved built in names; an earlier user defined class using one of those names must be renamed.

List and Set constructors accept zero or more positional values, evaluated once in source order. List(value: 1) and Set(value: 1) are invalid. Dictionary() accepts no arguments. Instance methods accept positional or named arguments with the exact labels in the tables below; named arguments change parameter destinations without changing expression evaluation order.

List: ordered and indexed

List uses a growable array. It preserves order and duplicates, provides efficient indexed access, and can serve as a stack with append/pop. Removing from the front or inserting in the middle shifts subsequent values.

aner
let tasks = List("read", "write")
tasks.insert(index: 1, value: "check")
tasks.set(index: 0, value: "open")
print(tasks.remove_at(1))
print(tasks.pop())
print(tasks.get_or(index: 99, default: "no task"))

Output is check, write, then no task, each on its own line. Indices are Int64 and zero based. get, set, and remove_at require 0 <= index < len; insert permits index == len to append. Negative indices do not count from the end. Strict out of range access and pop on an empty List report R2901. get_or returns its fallback for any out of range index, including a negative index.

extend(other) appends the other List's current elements. Extending a List with itself repeats its original contents once. contains uses strict runtime equality: scalar contents with matching types, null, and object/collection reference identity. Different tags compare unequal. Same type native composites such as two Tensors do not have equality; encountering that comparison through List.contains reports R2901 (the Any equality operator retains R1002). Membership does not deeply compare nested Lists.

Sort a List

sort() changes the existing List into ascending order and returns Unit. It is stable: items that compare equal keep their original relative order. All aliases see the new order; call copy() first to sort a separate outer List.

aner
let numbers = List(3, 1.5, 1, -2.0)
numbers.sort()
print(numbers)
let words = List("pear", "apple", "banana")
words.sort()
print(words)
let flags = List(true, false, true)
flags.sort()
print(flags)

Output:

text
List(-2.0, 1, 1.5, 3)
List("apple", "banana", "pear")
List(false, true, true)

Supported ordering is:

  • Numeric values: Int64 and Float64 may be mixed. Comparison uses their numeric values without converting stored values or rounding large Int64 values to Float64. Negative infinity sorts first and positive infinity last; NaN is rejected. Numerically equal integers/floats and positive/negative zero keep their original order and types.
  • Strings: every item must be String. Comparison is case sensitive lexicographic order of encoded bytes, as in existing String comparisons; it does not apply locale rules, natural number ordering, or Unicode normalization.
  • Booleans: every item must be Bool; false comes before true. Bool cannot be mixed with numbers.

An empty List succeeds. An unsupported item, even in a one item List, or incompatible types such as List(1, "two") report R2901. Null, NaN, objects, nested collections, and native composite values have no default sort order. Sorting validates the whole List before changing it; validation or allocation failure leaves its contents and order unchanged. In a persistent notebook, the usual runtime error rule still clears the entire Session. There is no custom comparator or descending order parameter yet.

The implementation validates in O(n), stably merge sorts an index permutation, then moves Values into the resulting order. Numeric/Bool sorting takes O(n log n) time and O(n) auxiliary space for n >= 2; zero/one item validation is O(1). It makes O(n log n) String comparisons, each of which may inspect a shared prefix. If String length is bounded by k, the worst case bound is O(n log n · (1 + k)), with O(n) auxiliary storage. The sort itself does not deep copy owned String payloads or traverse nested graphs.

Dictionary: keys and mixed values

Dictionary maps a scalar key to one Any value. It uses a hash index and preserves insertion order. Updating an existing key replaces its value and keeps its position. Removing a key and inserting it again moves it to the end. Removing an absent key is an operation with no effect.

aner
let record = Dictionary()
record.set("name", "Aner")
record.set("optional", null)
print(record.contains("optional"))
print(record.get("optional"))
print(record.get_or("missing", "fallback"))
print(record.keys())

Output:

text
true
null
fallback
List("name", "optional")

get reports R2901 when a key is absent. contains distinguishes an absent key from a key whose stored value is null. get_or uses the fallback only when the key is absent, so it returns stored null unchanged. All arguments, including the fallback expression, are evaluated before the method runs; fallback evaluation is not lazy.

keys() and values() create separate Lists in Dictionary insertion order. items() creates a List of two element Lists, each containing the key and its value in that order. These are snapshots of the outer storage, not live views. Replacing a Dictionary entry does not replace a previously returned snapshot's entry; nested object/collection references remain shared.

Set: unique scalar values

Set uses a hash index, suppresses duplicates, and preserves insertion order. add of an existing member keeps its position. Removing an absent member is an operation with no effect; removing and re adding a member puts it at the end. to_list() returns an insertion order snapshot.

aner
let left = Set(3, 1, 2)
let right = Set(2, 4, 1, 5)
print(left.union(right).to_list())
print(left.intersection(right).to_list())
print(left.difference(right).to_list())
print(left.symmetric_difference(right).to_list())

Output:

text
List(3, 1, 2, 4, 5)
List(1, 2)
List(3)
List(3, 4, 5)

Algebra operations allocate a new Set and leave both inputs unchanged. Intersection and difference follow the left operand's order. Union follows left order, then right only order. Symmetric difference follows left only order, then right only order.

Keys, uniqueness, and equality

Dictionary keys and Set members may be String, Int64, Bool, Float64 except NaN, or null. Their type is part of identity: integer 1, float 1.0, Boolean true, and String "1" are four different keys/members. Float -0.0 and 0.0 are equal and hash the same. Positive/negative infinity produced by arithmetic can be keys; NaN cannot because it does not have reflexive numeric equality.

Unsupported keys/members, including Lists, Dictionaries, Sets, user objects, and native composite values, report R2901. This validation applies to membership, lookup, removal, and fallback lookup as well as insertion; an empty container does not waive it. Dictionary values and List elements may still contain all these composite values.

Collections themselves compare by reference identity: a container equals an alias of itself, not a separately constructed container with equal contents. A copy() therefore has a different identity. Equality between differently typed collections requires one operand to be statically Any; their concrete tags then compare unequal. There is no deep equality, custom hash callback, or automatic key conversion. Numeric sorting permits Int64/Float64 comparisons without changing the strict equality and hashing rules above.

Aliases, copies, and traversal

Assigning a collection shares its reference. let prevents rebinding the name and permits mutating its contents. copy() creates a new outer container. Scalars such as Strings are owned values; nested collections and user objects keep their original shared references. Native values retain their existing native copy/alias behavior, including Tensor gradient behavior. This is a shallow copy, not recursive cloning.

aner
let nested = List(1)
let original = List(nested)
let copied = original.copy()
cast<List>(copied.get(0)).append("shared")
copied.append("copy only")
print(nested.len())
print(original.len())
print(copied.len())

The lengths are 2, 1, and 2. Cycles and references shared between classes and collections are supported by the same tracing heap.

Use a while loop and explicit methods for traversal:

aner
let record = Dictionary()
record.set("name", "Aner")
record.set("count", 2)
let keys = record.keys()
var index = 0
while index < keys.len() {
    let key = keys.get(index)
    print(key)
    print(record.get(key))
    index = index + 1
}

There are no general collection literals, items[index] operators, for, tuple destructuring, or iterator APIs yet. Existing [[1.0, 2.0]] syntax remains a numeric Tensor literal when its module is imported. Mutators return Unit unless explicitly documented below; do not chain append, set, add, remove, clear, extend, insert, or sort.

Time and space complexity

The following Big O bounds describe container operations, not interpreter parsing, argument evaluation, heap reference registration/validation bookkeeping, automatic tracing, or display. The heap identity registry has its own expected constant time hash access and growth costs; its collision worst case depends on the total heap size, not only this container. They assume bounded cost Values and fixed length scalar keys. Add the bytes processed when hashing/comparing/copying Strings or copying native payloads: hashing a String of length k costs O(k), and returning/storing an owned String of length p can cost O(p) time and additional O(p) storage. Nested collection/user object references are shallow handles, so a nested graph is not recursively copied. String comparison in a collision chain can multiply the per comparison byte cost by the chain length.

n is the receiver's live length, m is the other List/Set's length, r is the result's length, c is allocated List capacity, and b is allocated hash bucket count. u is the number of unique constructor inputs. Space columns describe additional auxiliary and result storage, beyond existing containers and already evaluated arguments. Persistent new entries are identified explicitly. A hash lookup's expected O(1) bound relies on well distributed hashes; worst O(n) collision behavior is not a guaranteed constant time lookup.

List operations

Constructor or methodResultTimeAuxiliary / result / new storage
List(...values: Any)ListO(n) for n inputsO(n) new List
len()Int64O(1)O(1)
is_empty()BoolO(1)O(1)
append(value: Any)UnitAmortized O(1); worst O(n) on growthO(1) new slot; worst O(n) transient growth buffer
extend(other: List)UnitO(m) amortized; worst O(n + m) on growthO(m) new slots; O(m) auxiliary snapshot, plus up to O(n + m) transient growth storage
get(index: Int64)AnyO(1)O(1) result; add payload copy bytes
get_or(index: Int64, default: Any)AnyO(1)O(1) result; add payload copy bytes
set(index: Int64, value: Any)UnitO(1)O(1) replacement; add stored payload bytes
insert(index: Int64, value: Any)UnitO(n) worst; append position amortized O(1)O(1) new slot; worst O(n) transient growth buffer
remove_at(index: Int64)AnyO(n) worst; last position O(1)O(1) auxiliary; removed owned payload transfers to result; capacity retained
pop()AnyO(1)O(1) auxiliary; removed owned payload transfers to result; capacity retained
contains(value: Any)BoolO(n) worstO(1)
sort()UnitO(n) validation + O(n log n) stable sorting for n >= 2; O(1) for n <= 1; String comparison adds byte costO(n) auxiliary; no new container or deep payload copy
clear()UnitO(n)O(1) auxiliary; releases List capacity and contents
copy()ListO(n)O(n) new shallow List

A List retains O(c) slots, plus owned payload bytes, even after removals. Geometric growth gives amortized append bounds; any one growth step may copy/move all n current entries. clear releases its allocation.

Dictionary operations

Constructor or methodResultTimeAuxiliary / result / new storage
Dictionary()DictionaryO(1)O(1) empty container
len()Int64O(1)O(1)
is_empty()BoolO(1)O(1)
set(key: Any, value: Any)UnitExpected amortized O(1); worst O(n), including rehashO(1) new/replacement entry; worst O(n) transient bucket growth
get(key: Any)AnyExpected O(1); worst O(n)O(1) result; add payload copy bytes
get_or(key: Any, default: Any)AnyExpected O(1); worst O(n)O(1) result; add payload copy bytes
contains(key: Any)BoolExpected O(1); worst O(n)O(1)
remove(key: Any)UnitExpected O(1); worst O(n)O(1); releases entry; bucket capacity retained
keys()ListO(n)O(n) new List and copied keys
values()ListO(n)O(n) new shallow List
items()ListO(n)O(n) outer List plus n separate two value Lists
clear()UnitO(n + b)O(1) auxiliary; releases entries and buckets
copy()DictionaryExpected O(n); worst O(n²) under collisionsO(n) new shallow Dictionary

Dictionary total container storage is O(n + b), plus owned key/value payload bytes. Every live pair has two value slots. Hash entries and insertion order links share each stored key; no duplicate key value is stored just to maintain order. Deletion releases entries immediately, while buckets can retain capacity from earlier growth. There are no accumulating tombstone entries; clear releases the buckets too.

Set operations

Constructor or methodResultTimeAuxiliary / result / new storage
Set(...values: Any)SetExpected O(n) for n inputs; worst O(n²) under collisionsO(u) new Set for u unique inputs
len()Int64O(1)O(1)
is_empty()BoolO(1)O(1)
add(value: Any)UnitExpected amortized O(1); worst O(n), including rehashO(1) new entry; worst O(n) transient bucket growth
contains(value: Any)BoolExpected O(1); worst O(n)O(1)
remove(value: Any)UnitExpected O(1); worst O(n)O(1); releases entry; bucket capacity retained
to_list()ListO(n)O(n) new List and copied scalar values
clear()UnitO(n + b)O(1) auxiliary; releases entries and buckets
copy()SetExpected O(n); worst O(n²) under collisionsO(n) new Set
union(other: Set)SetExpected O(n + m); worst O((n + m)²) under collisionsO(r) new Set; r <= n + m
intersection(other: Set)SetExpected O(n); worst O(nm + r² + n) under collisionsO(r) new Set; r <= min(n, m)
difference(other: Set)SetExpected O(n); worst O(nm + r² + n) under collisionsO(r) new Set; r <= n
symmetric_difference(other: Set)SetExpected O(n + m); worst O((n + m)²) under collisionsO(r) new Set; r <= n + m

Set total container storage is O(n + b), plus owned member bytes. It uses the same ordered hash storage and deletion/capacity rules as Dictionary. Algebra is built from hash membership/insertion, rather than nested linear scans in the normal case. Worst case bounds include collisions during both membership checks and result construction.

Display, memory, and errors

print and notebook expression output render bounded collection contents, including collections stored in Any. Strings inside containers are quoted and escaped; a standalone String keeps ordinary print behavior. Examples include List(1, "text", null), Dictionary("name": "Aner"), and Set(1, "one"). User objects and native composites retain labels such as <Record> or <Tensor> instead of dumping their internals.

Display is cycle safe, has a nesting depth limit of eight collection levels, shows at most 50 entries per collection, and caps one rendering at 8,192 bytes. Cycle/depth markers include the type, for example <cycle:List> and <depth-limit:List>; omitted contents use an ellipsis. These are previews, not serialization or a complete data export. Work and output are bounded by the traversal limits, with O(depth) active path state and O(output bytes) output storage.

Classes and collections share one native tracing heap:

Limit per execution or live notebook sessionAccounting
16,384 outstanding heap objectsUser objects, Lists, Dictionaries, and Sets combined
262,144 outstanding value slotsClass fields + List elements + Set members + two slots per Dictionary pair
16 MiB directly stored collection String bytesString elements, keys, and values; each owned copy counts

A nested reference occupies one slot; its referenced collection/object has its own allocation and slot accounting. The String ceiling excludes user class String fields, native payload buffers, heap metadata, reference type name Strings, retained source, and process/allocator overhead. These are allocation ceilings, not a whole process RAM quota. Native values such as Tensor and DataTable keep their own operation limits.

Unreachable objects/collections, including cycles, are traced and reclaimed after a successful shared session cell. There is no mid cell collection: many short lived containers in one cell/program can exhaust the outstanding allocation limits before completion. Snapshot/copy/algebra methods allocate new containers and must fit the same limits. clear and removal release the collection contents and their direct String/slot accounting; unreachable nested containers are reclaimed by the next successful session collection. Iterative tracing avoids recursive graph destruction. With h outstanding heap objects, s traced value slots, and g supplied roots, tracing has expected O(h + s + g) time and O(h) auxiliary space; hash collisions can increase its time. Native buffers are not traversed, and root preparation can add existing Value copy costs.

Invalid bounds, missing strict keys, unsupported keys/members, NaN keys/members, unsupported/mixed sort contents, and collection resource failures use R2901. Static wrong types/arity and unsupported typed collection specializations use E1002. Cast and unsupported Any equality operator failures retain R1002; unsupported native equality encountered by List membership reports R2901. A runtime failure clears the entire native notebook session; a parse/type check failure preserves previous state. Saved notebook outputs do not restore live collections after restart.

VS Code snippets and signature help

Editor extension 0.1.24 adds list, dictionary, set, and collections snippets with valid Aner examples. Use Insert Snippet, or type the prefix and invoke Trigger Suggest. The listsort snippet demonstrates mixed numeric sorting and String sorting. Inline AI previews can remain enabled; snippets are checked against the native interpreter.

Signature help shows constructor/method parameters, return types, time Big O, auxiliary/result space, and relevant bounds or key rules. It opens while entering (, ,, or : and can be requested with Trigger Parameter Hints (usually Ctrl+Shift+Space, or Cmd+Shift+Space on macOS). Receiver recognition covers constructor bindings, explicit collection annotations, parameters, checked collection casts, copies, and supported method chains. This is editor signature assistance, not a language server or proof that unfinished code type checks. User defined same named methods do not inherit native collection signatures. See Aner in VS Code.

Aner handbook · Guides and API reference