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:
aner check examples/collections_basic.aner
aner examples/collections_basic.aner
aner examples/collections_nested.anerFor 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
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:
List(10, "hello", 1.5, true, null, "last")
15
6
5The 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.
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.
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:
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;
falsecomes beforetrue. 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.
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:
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.
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:
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.
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:
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 method | Result | Time | Auxiliary / result / new storage |
|---|---|---|---|
List(...values: Any) | List | O(n) for n inputs | O(n) new List |
len() | Int64 | O(1) | O(1) |
is_empty() | Bool | O(1) | O(1) |
append(value: Any) | Unit | Amortized O(1); worst O(n) on growth | O(1) new slot; worst O(n) transient growth buffer |
extend(other: List) | Unit | O(m) amortized; worst O(n + m) on growth | O(m) new slots; O(m) auxiliary snapshot, plus up to O(n + m) transient growth storage |
get(index: Int64) | Any | O(1) | O(1) result; add payload copy bytes |
get_or(index: Int64, default: Any) | Any | O(1) | O(1) result; add payload copy bytes |
set(index: Int64, value: Any) | Unit | O(1) | O(1) replacement; add stored payload bytes |
insert(index: Int64, value: Any) | Unit | O(n) worst; append position amortized O(1) | O(1) new slot; worst O(n) transient growth buffer |
remove_at(index: Int64) | Any | O(n) worst; last position O(1) | O(1) auxiliary; removed owned payload transfers to result; capacity retained |
pop() | Any | O(1) | O(1) auxiliary; removed owned payload transfers to result; capacity retained |
contains(value: Any) | Bool | O(n) worst | O(1) |
sort() | Unit | O(n) validation + O(n log n) stable sorting for n >= 2; O(1) for n <= 1; String comparison adds byte cost | O(n) auxiliary; no new container or deep payload copy |
clear() | Unit | O(n) | O(1) auxiliary; releases List capacity and contents |
copy() | List | O(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 method | Result | Time | Auxiliary / result / new storage |
|---|---|---|---|
Dictionary() | Dictionary | O(1) | O(1) empty container |
len() | Int64 | O(1) | O(1) |
is_empty() | Bool | O(1) | O(1) |
set(key: Any, value: Any) | Unit | Expected amortized O(1); worst O(n), including rehash | O(1) new/replacement entry; worst O(n) transient bucket growth |
get(key: Any) | Any | Expected O(1); worst O(n) | O(1) result; add payload copy bytes |
get_or(key: Any, default: Any) | Any | Expected O(1); worst O(n) | O(1) result; add payload copy bytes |
contains(key: Any) | Bool | Expected O(1); worst O(n) | O(1) |
remove(key: Any) | Unit | Expected O(1); worst O(n) | O(1); releases entry; bucket capacity retained |
keys() | List | O(n) | O(n) new List and copied keys |
values() | List | O(n) | O(n) new shallow List |
items() | List | O(n) | O(n) outer List plus n separate two value Lists |
clear() | Unit | O(n + b) | O(1) auxiliary; releases entries and buckets |
copy() | Dictionary | Expected O(n); worst O(n²) under collisions | O(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 method | Result | Time | Auxiliary / result / new storage |
|---|---|---|---|
Set(...values: Any) | Set | Expected O(n) for n inputs; worst O(n²) under collisions | O(u) new Set for u unique inputs |
len() | Int64 | O(1) | O(1) |
is_empty() | Bool | O(1) | O(1) |
add(value: Any) | Unit | Expected amortized O(1); worst O(n), including rehash | O(1) new entry; worst O(n) transient bucket growth |
contains(value: Any) | Bool | Expected O(1); worst O(n) | O(1) |
remove(value: Any) | Unit | Expected O(1); worst O(n) | O(1); releases entry; bucket capacity retained |
to_list() | List | O(n) | O(n) new List and copied scalar values |
clear() | Unit | O(n + b) | O(1) auxiliary; releases entries and buckets |
copy() | Set | Expected O(n); worst O(n²) under collisions | O(n) new Set |
union(other: Set) | Set | Expected O(n + m); worst O((n + m)²) under collisions | O(r) new Set; r <= n + m |
intersection(other: Set) | Set | Expected O(n); worst O(nm + r² + n) under collisions | O(r) new Set; r <= min(n, m) |
difference(other: Set) | Set | Expected O(n); worst O(nm + r² + n) under collisions | O(r) new Set; r <= n |
symmetric_difference(other: Set) | Set | Expected O(n + m); worst O((n + m)²) under collisions | O(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 session | Accounting |
|---|---|
| 16,384 outstanding heap objects | User objects, Lists, Dictionaries, and Sets combined |
| 262,144 outstanding value slots | Class fields + List elements + Set members + two slots per Dictionary pair |
| 16 MiB directly stored collection String bytes | String 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.