Standard Library
Collections & Iteration
Array, Map, and Set are pure values —
every "changing" method hands back a new collection. Pair, Range, the
iterator protocol, and the lazy Seq pipeline round out how
Leviathan moves data through a loop.
Pure value semantics
Array<T>, Map<K, V>, and
Set<T> are pure values, not mutable
containers. No method mutates the receiver in place — every method that
looks like it "changes" the collection instead returns a new
one, and you rebind the variable to keep the result.
Array<int> a = [1, 2, 3];
a = a.add(4); // a is now [1, 2, 3, 4] — the old array is untouched
a[0] = 99; // rebind-sugar: exactly a = a.with(0, 99)
arr[i] = v and m[k] = v are rebind-sugar,
not mutation — they desugar to arr = arr.with(i, v) and
m = m.with(k, v) respectively. If you hold onto a reference to
the old array or map, it still shows the old contents. Efficiency
for the common single-owner case is a planned copy-on-write optimization
(in-place update when the refcount shows unique ownership) — the
semantics stay pure regardless of how it's implemented under the hood.
Array<T>
Construct with a literal, empty, or sized-and-filled. A bare declaration with no initializer defaults to empty.
Array<int> lit = [1, 2, 3];
Array<int> empty = Array();
Array<int> filled = Array(5, 0); // [0, 0, 0, 0, 0] — T inferred from the fill
Array<int> bare; // []
The result element type is inferred from the transform lambda's own
return type, not forced to stay T:
Array<string> r = a.map((n) => n.toString()); // int -> string, and it type-checksThis holds through chained maps as well.
Basics & queries
| category | members |
|---|---|
| core (native) | length(), at(int), add(T) |
| indexer | get ([])(int i) |
| basics | isEmpty(), first(), last(), firstOrNone() → T?, lastOrNone() → T? |
| queries | where(pred) / filter(pred), any(pred), all(pred), count(pred), contains(T), indexOf(T), indexWhere(pred) (-1 on miss), find(pred) → T? |
Transforms
| category | members |
|---|---|
| transforms | map<U>(fn) / select<U>(fn), reduce<A>(seed, fn), flatMap<U>(fn), forEach(fn), reverse(), take(int), skip(int), takeWhile(pred), skipWhile(pred), concat(Array<T>), unique() (dedup by ==), withIndex() → Array<Pair<int,T>>, groupBy<K>(fn) → Map<K, Array<T>> |
unique() — not distinct, which is a reserved
member-modifier keyword, unrelated to array dedup.
Pure updates & sorting
| category | members |
|---|---|
| pure updates | insertAt(int, T), removeAt(int), with(int i, T v) (index-set — a distinct overload from Map.with's key-set; same vocabulary, no clash), slice(int from, int len) (throws on out-of-bounds, unlike string.subStr's clamp) — all bounds errors throw RuntimeException |
| sorting | sort((T,T)=>int cmp) (stable merge sort — equal-key relative order is preserved), sortBy<K>(fn) (duck-typed < on K; a K without < is an instantiation-time error), minBy<K>(fn) → T?, maxBy<K>(fn) → T? |
Relational & iteration
| category | members |
|---|---|
| relational | join<U>(Array<U>, (T,U)=>bool) → Array<Pair<T,U>>; groupJoin<U>(...) → Array<Pair<T,Array<U>>>; zip<U>(Array<U>) → Array<Pair<T,U>> (length = shorter of the two) |
| strings | joinToString(string sep), concatAll() (native, O(total)) — declared generically but only meaningful on Array<string>; it's the engine behind StringBuilder.toString() |
| iteration / lazy | iterator() → IIterator<T> (protocol uniformity; for..in over an array keeps its fast path), asSeq() → Seq<T> (bridge into the lazy pipeline, see below) |
Aggregates — sum, min, max,
average — are free functions in std,
not methods: Array<T> has no specialization mechanism, so
std::sum overloads by argument type (int vs
float) instead.
int total = std::sum([1, 2, 3]);
float avg = std::average([1.0, 2.0, 3.0]); // std::average always returns float
int? biggest = std::max([]); // None on emptyMap<K, V>
Insertion-ordered, pure, like Array: "changing" methods return
a new map; m[k] = v is rebind-sugar. Since
get/set are keywords, the vocabulary is
at / with / without.
Map<string, int> m = Map();
m = m.with("a", 1);
m["b"] = 2; // sugar for m = m.with("b", 2)
int a = m.at("a");
int? z = m.atOrNone("z"); // None| category | members |
|---|---|
| core (native) | length(), at(K), with(K, V) → Map<K,V>, without(K) → Map<K,V>, has(K), keys() → Array<K>, values() → Array<V> |
| indexer | get ([])(K key) => at(key); m[k] = v rebinds |
| basics | isEmpty(), atOrNone(K) → V?, atOr(K, V dflt) → V |
| bulk | entries() → Array<Pair<K,V>>, withAll(Map<K,V>) (fold via bracket-sugar accumulate), mapValues<U>((V)=>U) → Map<K,U>, whereEntries((K,V)=>bool) → Map<K,V> |
| iteration | for (Pair e in m) — entries as Pair<K, V> (e.first, e.second); also iterator() → IIterator<Pair<K,V>> for protocol uniformity, yielding the identical sequence in insertion order |
console.writeln(m) prints {a: 1, b: 2}.
Key equality
Key comparison follows one contract across every collection built on it (Map and Set):
- primitives
- compare by value
- struct keys
- compare field-wise, recursively — a struct is its fields
- class keys
- compare by identity
This is landed in the tree-walk oracle, bytecode interpreter, emit-C++, and
LLVM backends. The frozen ELF backend (--emit-elf) keeps the
pre-contract identity-only comparison for struct/class keys permanently, by
design — primitive keys are identical on every backend including ELF.
.with()/.without() are missing from the LLVM
and ELF backends' named-method dispatch entirely
engines — use the
m[k] = v bracket-sugar form there instead of calling them by
name. The oracle, IR interpreter, and emit-C++ backends are unaffected.
Set<T>
Implemented in-language over Map<T, bool> — zero natives.
Insertion-ordered, and the same purity model as Array/Map: every "changing"
method returns a new Set.
Set<int> s = Set([1, 2, 3]);
s = s.with(4).without(1); // {2, 3, 4}
Set<int> t = Set([3, 4, 5]);
Set<int> both = s.intersect(t); // {3, 4}| category | members |
|---|---|
| basics | length(), isEmpty(), has(T) |
| pure updates | with(T) → Set<T>, without(T) → Set<T> |
| set algebra | union(Set<T>), intersect(Set<T>), except(Set<T>) — all pure, all Set<T> |
| conversion | toArray() → Array<T> (insertion order), toString() → "{a, b, c}" |
Key equality for Set<T> follows the same rule as
Map — it's built over one. Set's own
with/without rebuild via bracket-sugar internally,
so they are unaffected by the Map engine caveat above and match on all five
backends, including --emit-elf.
Pair & Range
Pair<A, B>
Fields first, second; construct with
Pair::Of(a, b). This is the element type of relational
array joins and of Map
iteration.
Pair<string, int> p = Pair::Of("age", 30);
console.writeln(p.first); // "age"
console.writeln(p.second); // 30Range
a..b — an inclusive integer range, with fields start
and end. Iterable in for..in as a counted loop
(see dispatch order), and also
IIterable<int> with an iterator() for protocol
uniformity. Printable, and it spreads inside array literals.
Range r = 1..5;
console.writeln(r.start); // 1
console.writeln(r.end); // 5
Array<int> spread = [1..3, 7]; // [1, 2, 3, 7]The iterator protocol
Two prelude interfaces make any type iterable by for..in:
interface IIterator<T> { bool hasNext(); T next(); }
interface IIterable<T> { IIterator<T> iterator(); }
for (T x in e), when e's static type implements
IIterable<T>, desugars to:
var __it = e.iterator();
while (__it.hasNext()) { T x = __it.next(); /* body */ }
for (var x in e) infers the loop variable from the
IIterable<T> instantiation. break/continue
behave exactly as in the hand-written while (continue
re-checks hasNext()). A type that implements neither a
built-in collection nor IIterable<T> is a compile error
naming the protocol.
Dispatch order
The checker picks the loop's path statically — there is no runtime probing:
- a
Rangeliterala..b→ a counted loop (no object); - an
Array/Map/Rangevalue → theIterLen/IterAtfast path; - otherwise, the protocol (
iterator()/hasNext()/next()via ordinary dynamic dispatch — no new IR op, zero backend work).
Built-ins never reroute through the protocol — the fast paths are why
arrays are fast. Array<T>, Map<K,V>, and
Range also implement IIterable (with
ArrayIterator/MapIterator/RangeIterator)
purely for uniformity — so one can be passed where an
IIterable<T> is wanted — but a for..in over
one still takes its fast path.
Past the end is unspecified by the protocol itself —
hasNext() is what drives the loop — but the stdlib
iterators throw RuntimeException (loud) if next()
is called past the end anyway. Invalidation: stdlib
iterators are over pure values (an Array snapshot) and can
never be invalidated; a user iterator over a mutable collection is
caveat-emptor, the same as any user code. Strings are
not iterable in v1 — s.chars() returns an
Array instead, to keep bytes vs. scalars explicit (see
Strings, Chars & Regex).
Seq<T>: lazy pipelines
A pure library over the iterator protocol. Arrays are eager;
Seq is the opt-in lazy form. array.asSeq()
is the bridge in.
| category | members |
|---|---|
| combinators (lazy) | map<U>((T)=>U) → Seq<U>, where((T)=>bool) → Seq<T>, take(int) → Seq<T>, takeWhile((T)=>bool) → Seq<T>, skip(int) → Seq<T> |
| terminals (pull) | toArray() → Array<T>, firstOrNone() → T? (short-circuits), count() → int, forEach((T)=>void), reduce<A>(A seed, (A,T)=>A) → A |
The laziness contract
Nothing runs until a terminal (toArray/firstOrNone/
count/forEach/reduce) pulls;
map/where functions run at most once per
pulled element; firstOrNone pulls exactly one matching
element — the lazy payoff. Terminal operations require a
finite source: take(n) is the bound for an
unbounded one (there is no runtime guard, the same stance as
while(true)).
Array<int> r = nums.asSeq()
.map((x) => x * x)
.where((x) => x % 2 == 1)
.take(3)
.toArray(); // squares evaluated only for the 3 kept elements
Each combinator is an iterator-composition class, not a
closure-holding-closure (MapSeq<T,U> : Seq<U>
wraps the source plus the function; its iterator wraps the source's).
Closures capture by snapshot, which is wrong for a stateful
cursor, so the wrappers hold honest mutable cursor fields instead. Each
Seq subclass pulls its source through the
interface type (IIterable/IIterator),
never the concrete class, so the source's own
iterator()/next() override runs correctly.