Collections
Arrays, lists, queues, sets and dictionaries, with their iterators.
Generated by
bin/build_library_doc.pyfrom the kernel sources. Do not edit by hand: change the generator, or the doc comments inkernel/src/, and re-run it.
Sequences
CLASS Array
IMPLEMENTS Cloneable, Collection
Opaque-size, ordered, indexable collection.
The foundation collection class. Stack, Queue, Deque, LinkedList, Set, and SortedList all compose on top of Array's storage; the hash-based collections (Dictionary) keep their own backing.
Element access is by integer index in 0..length-1. Out-of-bounds reads throw IndexOutOfBoundsError; out-of-bounds writes throw the same. Mutation methods come in move and copy pairs:
- append(value) — move; consumes value
- appendCopy(value) — DEEP COPY; value stays valid
- setAt(i, value) — move
- setAtCopy(i, v) — DEEP COPY
- insertAt(i, v) — move
- insertAtCopy(i,v) — DEEP COPY
For class-typed T, COPY routes through Cloneable.clone(); T must implement Cloneable for any *Copy method to be reachable.
Removal:
- popFirst() / popLast() — return the removed element as T; EMPTY if empty; ownership transfers to the caller.
- remove(i) — destroys the element in place.
- clear() — destroys all elements.
Inspection without removal:
- peekFirst() / peekLast() — return REFERENCE T handles into the underlying storage along with a STATUS. The handle is valid only while no mutation occurs.
Iteration:
- iterator() returns a BidirectionalIterator OF T at position 0. Multiple iterators on the same Array are independent.
Sorting and search:
- sort() — in-place; requires T IMPLEMENTS Comparable[T].
- contains(v) — linear scan; requires T IMPLEMENTS Equatable[T].
Functional transforms (map / filter / reduce) are pure-Envzn loops over the element domain and return new Arrays.
The storage shorthand T[] is the same physical layout as
Array[T]; the difference is API surface. T[] exposes only the
minimal value-array surface (subscript read + HWM write proxy,
.length, .capacity, clear(), remove(i), iterator());
Array[T] does the richer work (append / insertAt / count / sort …)
on top of that surface and maintains a public length field. The
sole developer-visible difference between int32[] and an object
T[] is that object elements must relocate with := (a move),
never a plain =.
kernel/src/Array.ev:71
Constructors
INIT()
Default constructor
INIT(int64 presized)
Presized constructor to reduce the malloc activity
Methods
METHOD iterator() RETURNS ArrayIterator[T]
Returns a fresh ArrayIterator[T] at position 0 — the FINAL
concrete iterator class (covariant against the inherited
Iterable.iterator() shape). Each call returns an independent
iterator; multiple may exist concurrently on the same Array.
The concrete return type lets the emitter inline hasNext() /
next() on FOR/IN loops over Array; callers binding to
BidirectionalIterator OF T still typecheck via class-to-
interface upcast.
METHOD size() RETURNS int64
Number of elements currently stored. Read-only, O(1).
METHOD isEmpty() RETURNS boolean
Countable — the other half of size(). Array carries the whole
Collection contract now (size, isEmpty, iterator), and none of
the three asks anything of T: that is what lets Array join Collection
while keeping its qualifier at Cloneable-or-primitive.
METHOD clone() RETURNS Array[T]
Deep copy. Each element is cloned via T->clone(); T must
implement Cloneable when T is a class type.
For T IMPLEMENTS Inoperative (opaque only today): returns
an empty Array — Inoperative values are opaque so we can't
preserve them on clone.
MODIFY METHOD append(T value) RETURNS STATUS
Append value to the end. Per §10.2 the bare-named form
consumes the source — value is moved into the array and
the caller's source is poisoned (post-call use is a compile
error). For a deep-copy variant that leaves the caller's source
valid, use appendCopy. O(1) amortised.
EMPTY-reject (class-typed T only): an EMPTY class-handle
value is refused with FAILURE before any state change — the
collection only ever holds real owned objects, so every
subsequent peek/pop/at sees a populated slot. Per §9.3 IS VALID
is meaningful only on class handles; the check is gated via
WHEN T IMPLEMENTS Cloneable (the kernel's proxy for
"class-shaped T") and elided for primitive / struct T.
MODIFY METHOD appendCopy(REFERENCE T value) RETURNS STATUS
Append a deep copy of value to the end. value stays fully
valid for the caller after the call. For class-typed T, the
copy goes through T->clone(); T must implement Cloneable. For
primitive/struct T, value-copy. O(1) amortised. Returns SUCCESS,
or FAILURE if backing storage rejected the insert.
METHOD __op_index__(int64 index) RETURNS MUTABLE REFERENCE T
Reference to element at index i. Throws IndexOutOfBoundsError if i is out
of range — bounds check lives in _EvArray<char32_t>::operator[]. A
borrow-propagating ("mirror") accessor: the result mirrors the receiver's
mutability (mutable array -> MUTABLE REFERENCE, const -> read-only REFERENCE).
METHOD subscript(int64 index) RETURNS | MUTABLE REFERENCE T
Bounds-checked reference into the element at index — the lowering
target for arr[idx] syntax on Array[T] receivers, and a borrow-
propagating ("mirror") accessor: SUCCESS populates result with a
reference that mirrors the receiver's mutability (a mutable array
yields a MUTABLE REFERENCE for in-place modification, a const array a
read-only REFERENCE); out-of-bounds populates s with FAILURE. Caller
pattern: IF arr[i] THEN { use($RETURNED) } ELSE { log($!) }. The
reference is valid only while the Array is not mutated (mutation lock).
Replaces the former at / subscript / mutableAt accessor set.
MODIFY METHOD setAt(int64 index, T value) RETURNS STATUS
Replace the element at index. Per §10.2 the bare-named form
consumes the source — value is moved into the array and
the caller's source is poisoned. The previous element is
destroyed. Returns FAILURE on out-of-bounds.
MODIFY METHOD setAtCopy(int64 index, REFERENCE T value) RETURNS STATUS
Replace the element at index with a deep copy of value.
value stays valid for the caller. Throws IndexOutOfBoundsError
on bad index.
MODIFY METHOD insertAt(int64 index, T value) RETURNS STATUS
Insert value at index, shifting subsequent elements right
by one. Per §10.2 the bare-named form consumes the source —
value is moved into the array and the caller's source is
poisoned. Valid index is 0..length inclusive (index == length
appends). Returns FAILURE on out-of-bounds. O(n) — shifts
everything to the right of the insertion point.
MODIFY METHOD insertAtCopy(int64 index, REFERENCE T value) RETURNS STATUS
Insert a deep copy of value at index. value stays valid.
Same index rules and complexity as insertAt.
MODIFY METHOD swap(int64 i, int64 j) RETURNS STATUS
Exchange the elements at i and j in place. Nothing is copied and no
slot is ever empty — this is how an element moves WITHIN an array, since
arr[k] := arr[j] is E6040 (a subscript is a reference to a slot the
array keeps owning). Returns FAILURE if either index is out of bounds.
MODIFY METHOD replace(int64 index, T value) RETURNS | T
Put value at index and hand back the element it displaced. Per
§10.2 value is consumed. Unlike setAt, which destroys the previous
element, this returns it — the owned counterpart of reading arr[i].
Pipe-XOR: FAILURE on out-of-bounds. O(1): the new element is parked at
the tail, swapped into place, and the old one is removed from the tail,
where removal shifts nothing.
MODIFY METHOD remove(int64 index) RETURNS | T
Remove the element at index and return it. Pipe-XOR:
SUCCESS populates removed with the value moved out of the
array; FAILURE populates status with the bounds violation.
Subsequent elements shift down. O(n). index is int64.
MODIFY METHOD clear() RETURNS void
Remove all elements; the array becomes empty.
METHOD map(LAMBDA func) RETURNS Array[T]
Apply func to every element; return a new Array with the
transformed values, in the same order. (Bug #59 / I.J.iii — unblocked
2026-06-26 when LAMBDA-param invocation landed.)
METHOD filter(LAMBDA pred) RETURNS Array[T]
Return a new Array containing only the elements for which
pred(elem) is TRUE, in their original order.
MODIFY METHOD reduce(REFERENCE T initial, LAMBDA acc) RETURNS T
Fold left: starting with initial, repeatedly apply
acc(accumulator, elem) for each element in order. Returns
the final accumulator value.
MODIFY METHOD shuffle(MUTABLE REFERENCE Shuffler rng) RETURNS void
Shuffleable OF T — in-place Fisher-Yates.
Lives on Array, not OrderedArray, so every subclass inherits it. It
costs Array nothing: the storage's swap works for every T Array's
qualifier admits, so no interface requirement is added by its being here. It
was also the one method whose presence on OrderedArray contradicted
that class: shuffling destroys exactly the ordering the name promises. Walks from the last index
down, swapping each element with a uniformly-chosen earlier-or-equal
slot drawn from rng. The exchange is the storage's own swap: no
clones, one form for every T. (It used to clone both elements of a
class T and move the clones back in — two deep copies per step.)
CLASS ArrayIterator
IMPLEMENTS BidirectionalIterator
Concrete BidirectionalIterator OF T for Array[T].
An Array[T]'s ->iterator() method yields a fresh ArrayIterator[T]
at position 0. Multiple iterators on the same Array are independent
(each has its own cursor); the source Array is locked against
mutation while any iterator is held (per §Iterator's mutation-lock
rule).
Notable:
The iterator holds a REFERENCE T[] source directly into
the source Array's data storage (collection storage shorthand) — validated under §REFERENCE structural condition 2
(mutation-locked at INIT). All element access goes through the
T[] implicit methods (->size(), [i]).
kernel/src/ArrayIterator.ev:35
Constructors
INIT(REFERENCE T[] source)
constructor that requires a REFERENCE
The iterator never owns the vector — .source is a
REFERENCE field, not
an owning copy — so a const reference is the correct shape
and accepts both const-source (Array's iterator() is non-
mutating) and non-const-source call sites.
Methods
METHOD hasNext() RETURNS boolean
returns TRUE if we've not reached the end
MODIFY METHOD next() RETURNS | REFERENCE T
Pipe-XOR: SUCCESS populates the value slot with a non-owning
reference; end-of-iteration populates STATUS with FAILURE.
a REFERENCE is non-owning, so referencing it is
harmless; only the by-value lift (popFirst/popLast) rejects opaque.
This matches Array.at, which has never guarded opaque.
METHOD peek() RETURNS | REFERENCE T
peek at the next item without advancing the iterator
MODIFY METHOD skip(uint64 n) RETURNS | REFERENCE T
skip(n) — moves cursor forward by n positions. Parameter is unsigned per spec §Iterator (forward-only contract). Pipe-XOR: SUCCESS with the element at the new position, or FAILURE if out of bounds. skip(0) is equivalent to peek().
METHOD hasPrevious() RETURNS boolean
returns TRUE if the iterator is not at the first element
MODIFY METHOD previous() RETURNS | REFERENCE T
mirror of next: on SUCCESS, it returns a REFERENCE to the value and decrements the iterator's cursor; on FAILURE it returns only the status message
MODIFY METHOD skipBack(uint64 n) RETURNS | REFERENCE T
skipBack(n) — moves cursor backward by n positions. Symmetric counterpart to skip(n) on BidirectionalIterator. Pipe-XOR: SUCCESS with the element at the new position, or FAILURE if start is reached. skipBack(0) is equivalent to peek().
CLASS OrderedArray
EXTENDS Array
Opaque-size, ordered, indexable collection.
The foundation collection class. Stack, Queue, Deque, LinkedList, Set, and SortedList all compose on top of Array's storage; the hash-based collections (Dictionary) keep their own backing.
Element access is by integer index in 0..length-1. Out-of-bounds reads throw IndexOutOfBoundsError; out-of-bounds writes throw the same. Mutation methods come in move and copy pairs:
- append(value) — move; consumes value
- appendCopy(value) — DEEP COPY; value stays valid
- setAt(i, value) — move
- setAtCopy(i, v) — DEEP COPY
- insertAt(i, v) — move
- insertAtCopy(i,v) — DEEP COPY
For class-typed T, COPY routes through Cloneable.clone(); T must implement Cloneable for any *Copy method to be reachable.
Removal:
- popFirst() / popLast() — return the removed element as T; EMPTY if empty; ownership transfers to the caller.
- remove(i) — destroys the element in place.
- clear() — destroys all elements.
Inspection without removal:
- peekFirst() / peekLast() — return REFERENCE T handles into the underlying storage along with a STATUS. The handle is valid only while no mutation occurs.
Iteration:
- iterator() returns a BidirectionalIterator OF T at position 0. Multiple iterators on the same Array are independent.
Sorting and search:
- sort() — in-place; requires T IMPLEMENTS Comparable[T].
- contains(v) — linear scan; requires T IMPLEMENTS Equatable[T].
Functional transforms (map / filter / reduce) are pure-Envzn loops over the element domain and return new Arrays.
The storage shorthand T[] is the same physical layout as
Array[T]; the difference is API surface. T[] exposes only the
minimal value-array surface (subscript read + HWM write proxy,
.length, .capacity, clear(), remove(i), iterator());
Array[T] does the richer work (append / insertAt / count / sort …)
on top of that surface and maintains a public length field. The
sole developer-visible difference between int32[] and an object
T[] is that object elements must relocate with := (a move),
never a plain =.
kernel/src/OrderedArray.ev:71
Constructors
INIT()
Default constructor
INIT(int64 presized)
for presized Stack allocation
Methods
OVERRIDE METHOD clone() RETURNS OrderedArray[T]
Deep copy. Each element is cloned via T->clone(); T must
implement Cloneable when T is a class type.
For T IMPLEMENTS Inoperative (opaque only today): returns
an empty Array — Inoperative values are opaque so we can't
preserve them on clone.
MODIFY METHOD sort() RETURNS void
In-place insertion sort using T->isLessThan(other). T must
implement Comparable[T] — the compiler emits a clear "T does
not implement Comparable" diagnostic at instantiation otherwise.
Each element is walked down by swapping it with its left neighbour
while it is strictly smaller, so equal elements keep their order
(stable). No element is copied and no slot is ever empty: the old
form lifted the element out with T key := .data[i], which is a move
from a reference (E6040) and silently deep-cloned every key.
O(n^2) worst case; acceptable for the small-N collections this
V1 kernel typically holds. A heap-or-merge-sort upgrade is a
V2 candidate if profiling demands it.
METHOD contains(REFERENCE T value) RETURNS boolean
Linear scan for an element equal to value. T must implement
Equatable[T] for class T. O(n).
METHOD equals(REFERENCE OrderedArray[T] other) RETURNS boolean
Element-wise equality. Two arrays are equal when they are the same
length and every element at the same index is equal — which makes two
EMPTY arrays equal, the case the previous form got wrong: it seeded
result = FALSE and only ever set it TRUE from inside the element
loop, so a pair of empty arrays passed the length test, never entered
the loop, and were reported UNEQUAL.
METHOD isLessThan(REFERENCE OrderedArray[T] other) RETURNS boolean
LEXICOGRAPHIC, the same order String.isLessThan settled on and for the
same reason. The first differing element decides; if neither array
differs through the shorter one's length, the shorter is less — so a
prefix sorts before what extends it.
This returned .length < other.length. Length-first is a different
total order, not a cheaper route to this one: it ranks [9] below
[1,1] and calls every pair of same-length arrays equal-or-greater
regardless of content, which silently reorders any OrderedArray-keyed
RedBlackTreeDictionary and makes sort() over arrays-of-arrays wrong.
CLASS Deque
IMPLEMENTS Cloneable, Collection
Defines the Deque[T] class per ENVZN_CONSTITUTION §Deque (L659–680).
Deque[T] is a double-ended queue: O(1) push and pop at either end, no random-access indexing, no mid-list operations. The contract is intentionally narrower than LinkedList[T] — Deque is a LinkedList with a constrained API surface that names the ends as "front" and "back" rather than "first" and "last."
Composes on LinkedList[T], which provides exactly Deque's contract:
- O(1) at both ends (LinkedList.prepend / append / popFirst /
popLast / peekFirst / peekLast).
- BidirectionalIterator OF T.
- Cascading clone() via Cloneable.
So Deque is pure delegation — no new storage, no new iterator,
just a renamed-and-narrowed view over LinkedList.
kernel/src/Deque.ev:29
Constructors
INIT()
Methods
METHOD clone() RETURNS Deque[T]
Hand-written clone() — AUTO refused for the same cascade reason as Queue / Stack / LinkedList: the synthesis walker can't see T's Cloneable status from this instantiation site through the LinkedList[T] composition. Walk the underlying LinkedList's iterator and pushBack a deep clone of each element — ordering preserved (front-to-back).
METHOD iterator() RETURNS LinkedListIterator[T]
METHOD isEmpty() RETURNS boolean
METHOD size() RETURNS int64
Number of elements currently stored. Read-only, O(1).
MODIFY METHOD clear() RETURNS void
MODIFY METHOD pushFront(T value) RETURNS STATUS
FRONT-SIDE OPERATIONS — pushFront / popFront / peekFront
MODIFY METHOD pushFrontCopy(REFERENCE T value) RETURNS STATUS
MODIFY METHOD popFront() RETURNS | T
METHOD peekFront() RETURNS | REFERENCE T
MODIFY METHOD pushBack(T value) RETURNS STATUS
BACK-SIDE OPERATIONS — pushBack / popBack / peekBack
MODIFY METHOD pushBackCopy(REFERENCE T value) RETURNS STATUS
MODIFY METHOD popBack() RETURNS | T
METHOD peekBack() RETURNS | REFERENCE T
CLASS Queue
EXTENDS Stack
Defines the Queue[T] class per ENVZN_CONSTITUTION §Queue.
Queue[T] is a FIFO collection implemented as a thin layer over
Array[T] (a.k.a. T[]). All storage and per-element ownership
semantics live in Array[T]; Queue contributes the FIFO discipline
and the public count field. Front is index 0, back is the last
element — enqueue appends, dequeue removes from index 0.
Performance note (Path B-pragmatic tradeoff):
- enqueue / enqueueCopy are O(1) amortised — same as Stack push
- dequeue is O(n) — removes from index 0; Array shifts elements down
- peek is O(1)
Callers who need O(1) dequeue should use Deque[T], which gets its
own std::deque backing (per Path B-pragmatic split — see
project_kernel_demagic_stage22.md memory). Queue-on-Array eats
the dequeue cost in exchange for a single backing storage shape
shared with Stack and SortedList.
kernel/src/Queue.ev:31
Constructors
INIT()
default constructor
INIT(int64 presized)
for presized Stack allocation
Methods
OVERRIDE METHOD clone() RETURNS Queue[T]
Deep copy. Each element is cloned via T->clone(); T must
implement Cloneable when T is a class type.
For T IMPLEMENTS Inoperative (opaque only today): returns
an empty Array — Inoperative values are opaque so we can't
preserve them on clone.
MODIFY METHOD enqueue(T value) RETURNS STATUS
Add value at the back of the queue. Per §10.2 the bare-named
form consumes the source — value is moved through Array's
owning append into the queue's backing.
MODIFY METHOD enqueueCopy(REFERENCE T value) RETURNS STATUS
Deep-copy form of enqueue. value stays valid for the caller;
the clone is what the backing array stores.
OVERRIDE METHOD peek() RETURNS | REFERENCE T
Remove and return the front element. Pipe-XOR: SUCCESS populates
value; FAILURE when the queue is empty. O(n) — depends on Array
providing popFirst() with move-out semantics for class T.
FIFO peek — the FRONT element, which is NOT what Stack.peek()
returns.
Queue EXTENDS Stack, and Stack's peek() is LIFO: it forwards to
peekLast(). Inherited unchanged, a Queue reported its most recently
ENQUEUED element as its front — dequeue() (which uses popFirst) and
peek() disagreed about which end the queue's head is. queueSmoke caught
it as "expected 11, got 22".
OVERRIDE is required here (W7020): this reimplements a CONCRETE inherited method rather than fulfilling an abstract contract.
MODIFY METHOD dequeue() RETURNS | T
CLASS Stack
EXTENDS Array
Defines the Stack[T] class per ENVZN_CONSTITUTION §Stack.
Stack[T] is a LIFO collection implemented as a thin layer over Array[T] (a.k.a. T[]). All storage and per-element ownership semantics live in Array[T]; Stack contributes the LIFO discipline and the public count field.
Design philosophy: by building Stack on Array[T], all the FOREIGN/T-substitution/ownership questions surfaced by the original Stack.ev preview are relocated to Array[T] — exactly once, instead of once per collection. See Array.ev (forthcoming) for those questions resolved.
kernel/src/Stack.ev:25
Constructors
INIT()
default constructor
INIT(int64 presized)
for presized Stack allocation
Methods
OVERRIDE METHOD clone() RETURNS Stack[T]
Deep copy. Each element is cloned via T->clone(); T must
implement Cloneable when T is a class type.
For T IMPLEMENTS Inoperative (opaque only today): returns
an empty Array — Inoperative values are opaque so we can't
preserve them on clone.
MODIFY METHOD push(T value) RETURNS STATUS
Push value onto the top. Per §10.2 the bare-named form
consumes the source — value is moved through Array's owning
append onto the stack's backing.
MODIFY METHOD pushCopy(REFERENCE T value) RETURNS STATUS
Deep-copy form of push. value stays valid for the caller;
the clone is what the backing array stores.
MODIFY METHOD pop() RETURNS | T
Remove and return the top element. Pipe-XOR: SUCCESS populates
value; FAILURE when the stack is empty. Depends on Array
providing popLast() with move-out semantics for class T.
METHOD peek() RETURNS | REFERENCE T
Return (without removing) the top element as a REFERENCE handle. Multi-return per the kernel peek contract — STATUS == FAILURE when empty, SUCCESS otherwise. Delegates to Array.peekLast.
MODIFY METHOD popLast() RETURNS | T
Remove and return the last element. FAILURE if empty. Ownership
of the returned element transfers to the caller. O(1).
For T IMPLEMENTS Inoperative (opaque): always returns FAILURE —
the stored payload is opaque and can't be lifted out by-value.
MODIFY METHOD popFirst() RETURNS | T
Remove and return the first element. FAILURE if empty. O(n) — shifts every remaining element down by one. For T IMPLEMENTS Inoperative: always returns FAILURE (see popLast).
METHOD peekLast() RETURNS | REFERENCE T
Read the last element without removing it. STATUS is FAILURE
when the array is empty (value is EMPTY); SUCCESS otherwise
with value referencing the element in place. The REFERENCE
handle is valid only while no mutation occurs on the Array.
METHOD peekFirst() RETURNS | REFERENCE T
Read the first element without removing it. Same pipe-XOR shape as peekLast.
CLASS SortedList
IMPLEMENTS Cloneable, Collection
Defines the SortedList[T] class per ENVZN_CONSTITUTION §SortedList (L703–716).
SortedList[T] is a collection that maintains its elements in
ascending order (per T->isLessThan) at all times. Insertion
walks to the sort position and splices; the order is invariant
across the public API — there is no operation that produces an
out-of-order sequence.
Composes on LinkedList[T], as do Stack and Queue — one backing
store shared across the composed collections. LinkedList provides
mid-chain insert/remove via its insertAt / removeAt methods;
SortedList drives them with a Comparable-aware position search.
────────────────────────────────────────────────────────────────── ORDER INVARIANT ──────────────────────────────────────────────────────────────────
At every public-API boundary, for any two indices i < j in the
list, the element at i is isLessThan the element at j OR
isEqualTo the element at j. Duplicates (per Equatable.isEqualTo,
inherited via Comparable[T] EXTENDS Equatable[T]) are allowed and
stored adjacently.
────────────────────────────────────────────────────────────────── SHORT-CIRCUIT SEARCH ──────────────────────────────────────────────────────────────────
Because the list is always sorted, contains and remove walk
from the front and stop early when they cross the value's
expected position:
- For
contains(v): scan from the head; ifv->isLessThan(elem)becomes TRUE, the value is not present (any further elements are >= elem > v). - For
remove(v): same short-circuit; the first match is the one removed (matches duplicates' adjacent-storage rule).
Mean-case work is O(n/2) for both; worst-case is O(n). A real production version could replace LinkedList with a balanced tree (red-black, AVL) for O(log n), but that's a much larger implementation. This V1 form trades sorted-search complexity for implementation simplicity.
kernel/src/SortedList.ev:62
Fields
int32 length
Constructors
INIT()
Methods
METHOD clone() RETURNS SortedList[T]
Hand-written clone() — AUTO refused for the same cascade
reason as Queue / Stack / Deque / LinkedList: the synthesis
walker can't see T's Cloneable status from this instantiation
site through the LinkedList[T] composition. Walk the
underlying LinkedList's iterator and append a deep clone of
each element. Sort order is preserved because the source list
is already sorted and we visit front-to-back; we use
data->append (which adds to tail) rather than going through
SortedList.add's per-insert binary scan.
METHOD iterator() RETURNS LinkedListIterator[T]
ITERATOR — delegate to LinkedList's BidirectionalIterator
The element order is insertion order, which by the order
invariant is also ascending. Callers iterate front-to-back
for ascending traversal; back-to-front via previous() for
descending.
METHOD isEmpty() RETURNS boolean
METHOD size() RETURNS int64
Number of elements currently stored. Read-only, O(1).
MODIFY METHOD clear() RETURNS void
MODIFY METHOD insert(T value) RETURNS STATUS
INSERT
Walks the iterator to find the first position where
value->isLessThan(elem) becomes TRUE — that's where value
belongs in the sorted order. The iterator is held in a
nested block so it drops (releasing the mutation lock on
_data) before the call to _data->insertAt.
Insert value at the position that keeps the list sorted.
Per §10.2 the bare-named form consumes the source —
value is moved into the list. The walk reads value for
comparison (read-only, doesn't consume) before the final
.data->insertAt call which performs the move.
MODIFY METHOD insertCopy(REFERENCE T value) RETURNS STATUS
Deep-copy form of insert. value stays valid for the caller;
the clone is the one moved into the list.
METHOD contains(REFERENCE T value) RETURNS boolean
CONTAINS / REMOVE — short-circuit on Comparable order
MODIFY METHOD remove(REFERENCE T value) RETURNS boolean
Remove the first element equal to value. Returns TRUE if
a match was found and removed, FALSE otherwise.
METHOD peekFirst() RETURNS | REFERENCE T
FRONT / BACK ACCESS — convenience wrappers over LinkedList
peekFirst returns the smallest element; peekLast the largest. popFirst / popLast remove and return them. All four are O(1) by virtue of LinkedList's existing front/back operations.
METHOD peekLast() RETURNS | REFERENCE T
MODIFY METHOD popFirst() RETURNS | T
MODIFY METHOD popLast() RETURNS | T
CLASS Set
IMPLEMENTS Cloneable, Collection
Set is a thin pure-Envzn wrapper over a Dictionary
STORAGE Set[V, H] holds Dictionary[V, int64, H] data (the interface),
backed by a ChainedHashDictionary. The element is the dictionary KEY
(collision-safe: distinct elements that hash equal land in the same
bucket and are told apart by Equatable, never merged); the value is an
unused int64 dummy (0). A boolean value is impossible — the Dictionary
qualifier bans boolean V — so a small int is the minimal placeholder.
TWO TYPE PARAMETERS Set[V, H]:
- V — element type. Hashable + Equatable (the dict's key contract).
- H — the Hasher OF V. Callers name a concrete hasher
(e.g. Set[int32, DefaultHasher[int32]]); the no-arg INIT lets the
backing dictionary CREATE a DefaultHasher[V], a hasher-taking INIT
threads a supplied one through. H is phantom in the body (the dict
stores the hasher as the Hasher OF V interface).
ITERATION yields the elements (the dict's keys), forward-only, order
unspecified. iterator() returns the backing dictionary's key iterator
directly — there is no separate SetIterator.
.length is kept in sync by hand: the Dictionary surface has no count(),
and insert() is replace-or-add (SUCCESS either way), so add/remove gate
on a prior contains to keep the count exact.
Set[V, H] — unordered set of unique elements over a Dictionary.
kernel/src/Set.ev:43
Fields
int64 length
Constructors
INIT()
No hasher — the backing dictionary CREATEs a DefaultHasher[V].
INIT(Hasher[V] h)
Caller-supplied hasher, threaded into the backing dictionary.
INIT(GrowthHint hint, int64 initialCapacity)
Default hasher + sizing hints (forwarded to the dictionary).
INIT(Hasher[V] h, GrowthHint hint, int64 initialCapacity)
Caller hasher + sizing hints.
Methods
METHOD clone() RETURNS Set[V, H]
Deep copy — the backing dictionary clones every key.
METHOD iterator() RETURNS ReferenceIterator[V]
ITERATOR — yields the elements (the dictionary's keys).
METHOD isEmpty() RETURNS boolean
METHOD size() RETURNS int64
Number of elements currently stored. Read-only, O(1).
MODIFY METHOD clear() RETURNS void
MODIFY METHOD add(V value) RETURNS STATUS
ADD / REMOVE / CONTAINS
Add value (consumed). If already present, the element stays and
the moved-in value is dropped; .length is unchanged.
MODIFY METHOD addCopy(REFERENCE V value) RETURNS STATUS
Deep-copy form of add. value stays valid for the caller.
METHOD contains(REFERENCE V value) RETURNS boolean
MODIFY METHOD remove(REFERENCE V value) RETURNS STATUS
Remove value. SUCCESS if it was a member, FAILURE otherwise.
METHOD unionWith(REFERENCE Set[V, H] other) RETURNS Set[V, H]
SET ALGEBRA — |+| / |&| / |-| bind to these.
Union — every element from either input (deduped).
METHOD intersectionWith(REFERENCE Set[V, H] other) RETURNS Set[V, H]
Intersection — elements present in BOTH inputs.
METHOD differenceWith(REFERENCE Set[V, H] other) RETURNS Set[V, H]
Difference — elements in SELF but NOT in other (asymmetric).
CLASS Box
IMPLEMENTS Cloneable
Defines the Box[T] class per ENVZN_CONSTITUTION §Box (L602–616).
Box[T] is a heap-allocating transparent wrapper around a single T. Its primary purpose is to break the type cycle that would otherwise make a self-recursive GROUP infinite-sized:
GROUP Expr {
Number n
BinaryExpr // contains Box[Expr] — the recursion
// through Box's heap pointer breaks
// the cycle for std::variant
}
CLASS BinaryExpr {
Box[Expr] left
Box[Expr] right
String op
}
At the C++ emission layer, Box[T] is std::unique_ptr<T> — single
owner, heap-indirected, fixed-size at the variant level. The
transparency promised by the spec (reading a Box field yields T,
not Box[T]; writing a T to a Box field auto-wraps) is compiler
sugar above this class, not anything the class itself implements.
The methods below are the explicit surface that the sugar
desugars to.
Compiler features (transparency sugar — not in this class):
- Auto-wrap on assignment. A class field declared
Box[T] foo accepts a T-typed RHS; the compiler emits the
equivalent of .foo = CREATE Box[T](rhs) for fresh INITs and
.foo->set(rhs) for reassignments.
- Auto-unwrap on read. Reading instance.foo where
.foo is a Box[T] field yields T (specifically a REFERENCE
T handle via the equivalent of .foo->value()); the user
never sees the Box wrapper.
- MATCH transparency. When a GROUP variant arm contains a
boxed type, MATCH dispatches on the inner T, not on Box[T].
None of those three features live inside this file — they're compiler-side rewrites. They reduce every user-facing interaction with Box to an Envzn-side method call on the surface declared below.
kernel/src/Box.ev:58
Constructors
INIT(T value)
The only constructor. value is consumed (a plain parameter owns). The
compiler's auto-wrap rewrite produces CREATE Box[T](rhs)
when initialising a Box[T] field from a T-typed expression.
Methods
METHOD clone() RETURNS Box[T]
Hand-written rather than AUTO: Box has no zero-arg INIT (the
user-facing model has no "empty box" state), so the cascade
synthesis path doesn't apply. T must implement Cloneable when
T is a class type; the explicit ->clone() call below
surfaces the requirement as a compile error on the Box[T]
instantiation that triggers it.
METHOD value() RETURNS REFERENCE T
── value — return a non-owning handle ────────────────────────
The auto-unwrap rewrite for reads of a Box[T] field. Returns
a non-optional REFERENCE T into SELF's owned .inner.
.inner is always populated (set by INIT and never cleared),
so the reference is always valid; the reference checker proves
it without runtime infrastructure.
METHOD valueClone() RETURNS T
── valueClone — return an owned copy ───────────────────────── The path callers take when they need an owning T (e.g. to store elsewhere, to mutate independently). T must be Cloneable; the cascade-failure rule fires at instantiation otherwise.
MODIFY METHOD set(T value) RETURNS void
── set — replace the wrapped value ───────────────────────────
Drops the previous inner and installs value (owned).
Bug #32: := to a populated non-Optional owning field
drops the previous owner via its destructor, then installs
the new owner from the RHS. Box never enters a "moved-out"
state observable to the user — the drop and the install
happen as a single operator step.
ENUM GrowthHint
GrowthHint — advisory sizing hint for hash-based collections (Dictionary impls, Set). Passed at construction alongside an initial capacity so an implementation can tune its backing storage.
- STABLE — size stays roughly constant after fill; size the backing store once and avoid rehash churn.
- GROW_ONLY — size only increases; pre-grow aggressively, never shrink.
- DYNAMIC — size moves up and down frequently; keep headroom
and tolerate rehash. (
VOLATILEis a reserved concurrency keyword, so the churn case is named DYNAMIC.)
V1: the hint is accepted and stored but only initialCapacity
drives behavior (ChainedHashDictionary pre-sizes its bucket array).
GrowthHint-driven tuning (shrink policy, load-factor selection) is
a future performance pass.
kernel/src/enums.ev:207
| Case | Description |
|---|---|
? |
— |
? |
— |
? |
— |
Lists
CLASS LinkedList
IMPLEMENTS Cloneable, Collection
Defines the LinkedList[T] class per ENVZN_CONSTITUTION §LinkedList.
LinkedList[T] is a doubly-linked list with O(1) push/pop at both
ends and O(n) traversal/lookup. The
forward chain is owning; back-pointers are explicit REFERENCE, re-bound
with =@.
LinkedList is the foundation for Deque (composes on LinkedList for O(1) both-end ops) and SortedList (composes on LinkedList; insert walks to the sort spot via Comparable[T]). Together with Array, this reduces the kernel to two backing stores.
────────────────────────────────────────────────────────────────── STRUCTURE — sentinels + chain ──────────────────────────────────────────────────────────────────
Every list always contains two sentinel nodes — a "starter" at the head and an "ender" at the tail. They exist on construction and are never removed. User nodes live strictly between them.
_head (id=0) ⇄ user1 ⇄ user2 ⇄ ... ⇄ userN ⇄ _tail_ref (id=1)
Forward arrows (→ in the diagram, next field) are owning
unique_ptrs. Backward arrows (← in the diagram, prev_ref field)
are non-owning REFERENCEs.
The list class holds:
- _head — owning non-Optional sentinel; this owns the entire
forward chain transitively through .next.
- _tail_ref — non-owning REFERENCE to the ender sentinel; needed
for O(1) end-side operations without traversing the whole
chain. The ender is itself owned (via the forward chain) by
the node that immediately precedes it.
- _nextId — counter for new user-node IDs (starts at 2).
- length — public count of user nodes (excludes sentinels).
Empty list: _head → ender (head.next owns ender; head.prev_ref
is EMPTY; ender.prev_ref REFERENCEs head; ender.next is EMPTY).
────────────────────────────────────────────────────────────────── SPLICE PATTERN ──────────────────────────────────────────────────────────────────
Each insert/remove follows a consistent shape:
(a) move owning links via := (always taking the existing
value out of an Optional owning field BEFORE overwriting
the field — otherwise the unique_ptr destructor frees the
value mid-splice);
(b) REBIND back-pointers via =@ (REFERENCE rewires;
structural conditions checked);
(c) move the new owning link into the predecessor's .next
slot last, completing the splice.
Optional unwraps use IF X IS NOT VALID { PANIC } to convert
statically-unreachable EMPTY branches (e.g. _tail_ref.prev_ref
is always set after INIT) into explicit kernel-invariant THROWs
— making "this can't happen" auditable instead of hiding behind
silent fallbacks.
kernel/src/LinkedList.ev:72
Fields
int32 length
Constructors
INIT()
Constructs both sentinels and wires them. After this returns,
head → ender (head.next owns the ender), and the list is empty.
Methods
METHOD clone() RETURNS LinkedList[T]
Hand-written clone() — required because LinkedList carries a
REFERENCE field (.last_node_ref) that AUTO clone cannot rewire
onto the cloned topology. Builds a fresh empty list, walks the
current chain directly (no heap iterator), and appends a deep-clone
of each user value. The new list owns its own sentinel chain with
correctly-wired last_node_ref (set by append() on every push).
METHOD iterator() RETURNS LinkedListIterator[T]
Per spec L767, LinkedList exposes BidirectionalIterator OF T.
Concrete class is LinkedListIterator (separate file). Pass
.head by-& so the iterator's REFERENCE binding satisfies
§REFERENCE condition 2 (mutation-locked at INIT) without
moving ownership of .head out of SELF.
METHOD isEmpty() RETURNS boolean
METHOD size() RETURNS int64
Number of elements currently stored. Read-only, O(1).
MODIFY METHOD prepend(T value) RETURNS STATUS
FRONT-SIDE OPERATIONS (right after head sentinel)
Insert value at the front. value is cloned in via
LinkedListNode's INIT (const-ref param-passing, LOCKED 2026-05-12).
Splice ordering (Bug #32 move-out-before-overwrite): step 1: newNode.next := .head.next (move old first into newNode.next; .head.next is now EMPTY) step 2: .head.next := newNode (move newNode into .head.next; local poisoned) step 3: bump last_node_ref if list was empty
MODIFY METHOD prependCopy(REFERENCE T value) RETURNS STATUS
Deep-copy form of prepend. value stays valid for the caller;
the clone is the one moved into the new node.
MODIFY METHOD popFirst() RETURNS | T
Remove and return the front user value. EMPTY if empty.
Splice ordering (Bug #32 move-out-before-overwrite): step 1: popped := .head.next (move; .head.next now EMPTY) step 2: .head.next := popped.next (move the new-first into .head.next) step 3: if popped was the only user node, last_node_ref bumps back to .head
METHOD peekFirst() RETURNS | REFERENCE T
Read (without removing) the front user value. Pipe-XOR shape:
SUCCESS populates value with the reference; empty / invariant
violation populates s with FAILURE. Never throws.
METHOD at(int32 index) RETURNS | REFERENCE T
Non-owning reference to the value at index (0-based), or FAILURE
if out of bounds. O(index) forward walk from the first user node —
backs index-based external walkers that cannot hold a
LinkedListIterator field (iterator Rule A), e.g.
ChainedHashDictionaryIterator. Read-only; the source list stays
mutation-locked while the returned reference is held.
MODIFY METHOD append(T value) RETURNS STATUS
BACK-SIDE OPERATIONS (right before ender sentinel)
Insert value at the back. value is cloned in via
LinkedListNode's INIT (const-ref param-passing, LOCKED 2026-05-12).
O(n) — walks .head's owning chain to find the node whose .next is
the ender (id 1), then splices newNode in front of the ender.
MODIFY METHOD appendCopy(REFERENCE T value) RETURNS STATUS
Deep-copy form of append. value stays valid for the caller;
the clone is the one moved into the new node.
MODIFY METHOD popLast() RETURNS | T
Remove and return the back user value. EMPTY if empty. O(n) walk from .head to find the second-to-last user node so we can splice the ender into its .next slot. Single-element case short-circuits to popFirst().
Splice ordering (Bug #32 move-out-before-overwrite): step 1: popped := wasSecondToLast.next (move popped out of the predecessor's next slot) step 2: wasSecondToLast.next := popped.next (move ender into the predecessor's next slot)
METHOD peekLast() RETURNS | REFERENCE T
Read (without removing) the back user value. Pipe-XOR shape:
SUCCESS populates value with the reference; empty list
populates s with FAILURE. O(n) walk from .head.
MODIFY METHOD insertAt(int32 index, T value) RETURNS STATUS
MID-CHAIN OPERATIONS (insertAt / removeAt)
Used by SortedList[T] and any other consumer that needs to splice at an arbitrary position. O(index) walk to the target node, then O(1) splice via the same move-out-before-overwrite pattern as prepend/append/popFirst/popLast.
Index 0 and index .length (or .length - 1 for removeAt)
delegate to the existing front/back methods to avoid
duplicating splice logic.
Insert value at position index. Valid range is 0..length
(inclusive — index == length appends, equivalent to append).
Per §10.2 the bare-named form consumes value (poisoned
at the call site). STATUS == FAILURE on out-of-bounds index
or kernel-invariant violation; SUCCESS otherwise.
MODIFY METHOD insertAtCopy(int32 index, REFERENCE T value) RETURNS STATUS
Deep-copy form of insertAt. value stays valid for the caller;
the clone is the one moved into the new node.
MODIFY METHOD removeAt(int32 index) RETURNS | T
Remove and return the user value at position index. STATUS
== FAILURE on out-of-bounds index or kernel-invariant violation
(paired with EMPTY in the value slot); SUCCESS otherwise (paired
with the removed value).
CLASS LinkedListIterator
IMPLEMENTS ReferenceIterator
Concrete BidirectionalIterator OF T for LinkedList[T].
A LinkedList[T]'s ->iterator() method yields a fresh
LinkedListIterator[T] positioned at the first user node (or at
the ender sentinel if the list is empty). Multiple iterators on
the same list are independent (each has its own cursor REFERENCE);
the source list is locked against mutation while any iterator is
held (per §Iterator's mutation-lock rule).
────────────────────────────────────────────────────────────────── CURSOR MODEL ──────────────────────────────────────────────────────────────────
The cursor REFERENCEs the node "about to be read" by the next forward operation. Valid cursor positions:
- Any user node (id ≥ 2): peek/next reads this node's value.
- The ender sentinel (id = 1): the off-the-end position; hasNext is false, peek/next return EMPTY.
The cursor never sits at the head sentinel (id = 0). Stepping backward stops at the first user node — going further would land at head (which has no readable value), so hasPrevious returns false there and previous/skipBack return EMPTY without moving.
For an empty list, the cursor starts at the ender; both hasNext and hasPrevious are false.
────────────────────────────────────────────────────────────────── SAFETY ──────────────────────────────────────────────────────────────────
source_head_node_refis a REFERENCE to the source list's head sentinel. Set in INIT from a by-¶meter; satisfies §REFERENCE structural condition 2 (mutation-locked at INIT) — the mutation lock prevents append/prepend/pop on the source while any iterator exists.cursor_refis a REFERENCE to a node owned by the source list's_headchain. Safe under condition 1 (ownership- reachable from SELF via the validated source-head REFERENCE).- The mutation lock guarantees the cursor's referent cannot be deleted out from under the iterator — no dangling.
────────────────────────────────────────────────────────────────── SKIP / SKIP-BACK BOUNDS ──────────────────────────────────────────────────────────────────
skip(n)walks forward; on overshoot the cursor lands on the ender sentinel (a valid cursor position) and EMPTY is returned. No rollback needed — landing on the ender keeps the cursor model intact.skipBack(n)walks backward; on underflow the cursor stops at the first user node (the position whose_prev_ref.id == 0) and EMPTY is returned. The check is peek-before-step so the cursor never lands on the head sentinel — preserving the "cursor never at head" invariant.
No local REFERENCE-typed variables are used (REFERENCE is
restricted to class instance fields per Bug #29 V1; non-owning
local handles use Bug #30's =@ operator instead). The cursor
field is the walker.
kernel/src/LinkedListIterator.ev:75
Constructors
INIT(REFERENCE LinkedListNode[T] source_head_node)
Methods
METHOD hasNext() RETURNS boolean
MODIFY METHOD next() RETURNS | REFERENCE T
METHOD peek() RETURNS | REFERENCE T
MODIFY METHOD skip(uint64 n) RETURNS | REFERENCE T
skip(n) — moves cursor forward by n positions. skip(0) is equivalent to peek(). On overshoot, the cursor lands on the ender sentinel and FAILURE is returned.
CLASS OwnedList
IMPLEMENTS Collection
Owning, move-only, non-Cloneable list.
────────────────────────────────────────────────────────────────── WHAT IT IS ──────────────────────────────────────────────────────────────────
OwnedList[T] is a singly-linked list that OWNS its elements and only
ever MOVES them — it never copies or clones. Because it is not itself
Cloneable, it imposes no Cloneable requirement on T; its bound is
T IS Shareable, so it can hold non-Cloneable kernel concurrency
primitives (Channel, Mutex, Future, …) that the standard collections
(Array / Dictionary / LinkedList / Set, all IMPLEMENTS Cloneable)
reject.
It is the registry behind Broker[T] (a list of per-subscriber
Channels), and is reusable for any "hold a set of owned, non-copyable
objects, walk them, add/remove by id" need.
────────────────────────────────────────────────────────────────── API (minimal) ──────────────────────────────────────────────────────────────────
- add(T value) RETURNS uint64 — take ownership, prepend, return a stable id for removal
- removeById(uint64 id) RETURNS boolean — splice out that node (close/drop its element)
- iterator() RETURNS OwnedListIterator[T] — forward walk by REFERENCE
- length (field) / isEmpty() — count of user nodes
Structure mirrors LinkedList: a head sentinel (id 0) and an ender
sentinel (id 1) bracket the user nodes (id ≥ 2). Splices use the
move-out-before-overwrite ordering. Singly-linked, owning-
forward; dropping .head cascade-frees the chain.
Built to back Broker[T].
kernel/src/OwnedList.ev:48
Fields
int32 length
Constructors
INIT()
Methods
MODIFY METHOD add(T value) RETURNS uint64
Take ownership of value and prepend it (right after the head
sentinel). O(1). Returns the new node's stable id, which the
caller passes to removeById later.
Splice ordering (Bug #32 move-out-before-overwrite): step 1: newNode.next := .head.next (move old first into newNode.next) step 2: .head.next := newNode (move newNode into .head.next)
MODIFY METHOD removeById(uint64 id) RETURNS boolean
Remove the user node with the given id. Returns TRUE if found and
removed; FALSE if no live node carries that id. Walks from the
head sentinel tracking the predecessor, then splices the target
out (its owned element is dropped as popped leaves scope).
METHOD findById(uint64 id) RETURNS | REFERENCE T
Return a REFERENCE to the element of the node with the given id. Pipe-XOR: SUCCESS yields the reference; FAILURE if no live node carries that id. Used by Broker.subscribe to hand a Subscription a non-owning handle to its just-added channel.
METHOD size() RETURNS int64
Countable — the count of user nodes (sentinels excluded). .length is
the int32 field; size() is the int64 contract, and int32 -> int64 is
a widening, so the assignment carries it with no stated conversion.
METHOD isEmpty() RETURNS boolean
METHOD iterator() RETURNS OwnedListIterator[T]
Fresh forward cursor positioned at the first user node (or the
ender if empty). Pass .head by-& so the iterator's REFERENCE
binding is mutation-locked at INIT without moving .head out.
CLASS OwnedListIterator
IMPLEMENTS ReferenceIterator
Forward cursor over OwnedList[T].
A fresh iterator from OwnedList.iterator() is positioned at the
first user node (or the ender sentinel if the list is empty). Drive
it with the pipe-XOR loop:
OwnedListIterator[T] it := list->iterator()
WHILE it->next() DO {
REFERENCE T value =@ $RETURNED
... use value in place (no copy) ...
}
Mirrors LinkedListIterator (the proven cursor model), bound to
T IS Shareable so it can walk a chain of non-Cloneable elements.
next() yields a REFERENCE to the element — the consumer operates on
it in place; the element is never copied out of the list.
Plain FINAL class (not IMPLEMENTS ReferenceIterator): OwnedList's
consumers drive next() directly, and the for-in / ReferenceIterator
protocol carries a Cloneable-shaped contract we don't want here.
kernel/src/OwnedListIterator.ev:33
Constructors
INIT(REFERENCE OwnedListNode[T] sourceHeadNode)
Methods
METHOD hasNext() RETURNS boolean
Iterator — TRUE while the cursor still sits on a user node. The ender
sentinel is id 1, which is exactly the condition next() refuses on,
so the two agree by construction.
This class implemented NO interface at all until now, which is why it
alone of the iterators could not satisfy Collection.iterator() —
ArrayIterator and the rest were already BidirectionalIterator OF T.
METHOD peek() RETURNS | REFERENCE T
ReferenceIterator — the reference at the current cursor WITHOUT
advancing. Same pipe-XOR shape as next() and the same refusal at the
ender sentinel; it simply does not move cursorRef.
MODIFY METHOD skip(uint64 n) RETURNS | REFERENCE T
ReferenceIterator — advance n positions and yield what lands under
the cursor. skip(0) is next(); running off the ender fails exactly
as next() does, so a short list refuses rather than walking past the
sentinel.
MODIFY METHOD next() RETURNS | REFERENCE T
Advance and yield the next element by REFERENCE. Pipe-XOR:
SUCCESS populates value; FAILURE once the cursor reaches the
ender sentinel (end of iteration).
Dictionaries
INTERFACE Dictionary
Dictionary is a pure INTERFACE, not a concrete class, and is no longer special-cased by the compiler. It is an ordinary parametric interface the compiler treats like any other.
THREE TYPE PARAMETERS Dictionary[K, V, H]:
- K — key type. Hashable + Equatable (primitives, String, or a class
implementing both).
- V — value type. Cloneable / primitive (the clone()-bearing surface
requires it; see EXTENDS Cloneable below).
- H — the Hasher OF K used to hash keys. Callers name a concrete
hasher here (e.g. Dictionary[String, int32, StringHasher]); the
hidden impls store it as the Hasher OF K interface type and, when
constructed without one, CREATE a DefaultHasher[K].
IMPLEMENTATIONS (both HIDDEN, pure Envzn, no FOREIGN / std::): - ChainedHashDictionary[K,V,H] — bucket array + chaining; unordered. - SkipListDictionary[K,V,H] — skip list; sorted iteration (K also Comparable). (lands in a later #16 phase.)
Callers construct an impl and hold the interface: Dictionary[String, int32, StringHasher] scores := CREATE ChainedHashDictionaryString, int32, StringHasher
EXTENDS Cloneable — every Dictionary deep-copies.
ITERATION yields KEYS (forward-only), per spec §Iterator. Order is unspecified for the chained-hash impl; the skip-list impl iterates in sorted key order. Dictionary[K, V, H] — unordered key→value map interface. Keys are hashed by the H hasher; values are Cloneable. Construct a concrete impl (ChainedHashDictionary / SkipListDictionary) and hold this interface type.
kernel/src/Dictionary.ev:49
Methods
METHOD clone() RETURNS Dictionary[K, V, H]
Deep copy. (Cloneable contract; covariant return — an impl returns its own concrete type.)
METHOD iterator() RETURNS ReferenceIterator[K]
Forward-only iterator over the keys. FOR/IN over a Dictionary visits each key once.
METHOD keys() RETURNS LinkedList[K]
Snapshot of all keys in a fresh LinkedList (each key cloned).
METHOD isEmpty() RETURNS boolean
check if the dictionary is empty
METHOD size() RETURNS int64
Number of key→value pairs currently stored. All three impls already
provide it; declaring it here makes it callable through a Dictionary
interface handle (Bug #179).
MODIFY METHOD clear() RETURNS void
MODIFY METHOD insert(K key, V value) RETURNS STATUS
insert (key, value). BOTH are MOVEd in (consumed) — the dictionary
owns what it stores, so there is nothing for it to clone. Replacing an
existing key destroys the old value and leaves the count unchanged.
SUCCESS always (FAILURE only on an EMPTY key/value where the type is
Cloneable). Use insertCopy to keep your own key and value.
MODIFY METHOD insertCopy(REFERENCE K key, REFERENCE V value) RETURNS STATUS
Deep-copy insert — both key and value stay valid for the caller.
METHOD lookup(REFERENCE K key) RETURNS | MUTABLE REFERENCE V
Lookup — a reference into the stored value, or FAILURE if the key is absent. Gate on STATUS, not on the returned V. A borrow-propagating ("mirror") accessor: the returned reference mirrors the receiver's mutability — a mutable dictionary yields a MUTABLE REFERENCE (modify the value in place), a const one a read-only REFERENCE. Valid while the dictionary is not mutated (iterator mutation lock).
There is deliberately NO value-cloning lookup: borrowing is the default
and owning is explicit. To own a copy, clone the borrow; to own the
value itself, remove it.
METHOD lookupFast(REFERENCE K key) RETURNS boolean
Allocation-free lookup — found plus the value, no pipe and no STATUS.
This is not a faster lookup; it is a lookup with a different COST on
the miss. lookup builds a FAILURE("...key not found") when the key
is absent, so a loop that misses once per distinct key — a group-by, a
join, a dedupe — allocates a formatted String per miss, a cost the pipe
hides completely at the call site. Every implementation already knows
whether the key is present before it decides what to return, so making
this part of the interface costs nothing and is what lets a consumer
stay on the interface instead of reaching for the concrete class.
On a miss value is the type's absent form (EMPTY for a class V, zero
for a primitive) and MUST NOT be read: found is the only valid
discriminator. C# spells the same contract TryGetValue.
MODIFY METHOD remove(REFERENCE K key) RETURNS | K
remove — MOVES both halves out, or FAILURE if the key is absent. Nothing is cloned: the stored key and value are handed to the caller rather than copied and destroyed. The returned key is the STORED one, which is equal to — but not necessarily the same object as — the probe key, which is why it is worth returning.
METHOD contains(REFERENCE K key) RETURNS boolean
contains - like lookup() but only returns a boolean if the key-value pair is found
CLASS ChainedHashDictionary
IMPLEMENTS Dictionary
ChainedHashDictionary
STORAGE:
- buckets : HashBucket[K,V][] — a RAW value-array of lean value-class buckets,
one per slot. Each HashBucket stores its entries INLINE (2 CollisionNode slots
+ a lazy spill), so there is NO per-bucket heap object and NO per-entry heap
node — the LinkedList-per-bucket + ChainedHashEntry + per-lookup heap iterator
of the old design are gone (that iterator alloc was the measured 174 ns/lookup
S4 bug). An empty bucket is a zero-count HashBucket (no nullable-slot problem).
- In-place bucket mutation binds a MUTABLE REFERENCE into .buckets[idx] (reads
bind a plain REFERENCE); the bucket's own MODIFY methods do the work.
- count : int64; hasher : Hasher OF K (interface-typed, decision 5);
bucketLevel : int64 (power-of-two growth level); growth : GrowthHint
(advisory, stored-unused for V1).
BUCKET INDEX: hash & (bucketCount - 1) over a POWER-OF-TWO bucket count
(HashConstants.pow2CountAtLevel / indexForHash) — replaces the old prime %.
RESIZE: load factor > 0.79 (count100 > bucketCount79) → double the bucket count
(one pow2 level) and rehash every entry by its CACHED hash (no key re-hash).
IMPLEMENTS Dictionary[K, V, H] — Cloneable comes transitively (Dictionary EXTENDS
Cloneable); a redundant explicit Cloneable would create an ambiguous C++ double
base and break clone() covariance. H is phantom in the body; the stored hasher is
the Hasher OF K interface (no-arg INIT CREATEs the DefaultHasher).
ChainedHashDictionary[K, V, H] — separate-chaining hash table over lean inline
value-class buckets. HIDDEN: constructed via CREATE, held through the Dictionary
interface.
kernel/src/ChainedHashDictionary.ev:45
Constructors
INIT()
No hasher, default sizing — CREATE a DefaultHasher[K].
INIT(Hasher[K] h)
Caller-supplied hasher, default sizing.
INIT(GrowthHint hint, int64 initialCapacity)
Default hasher + sizing hints (initialCapacity ignored for V1 — the pow2 ladder governs bucket count; GrowthHint stored, unused).
INIT(Hasher[K] h, GrowthHint hint, int64 initialCapacity)
Caller hasher + sizing hints.
Methods
METHOD clone() RETURNS Dictionary[K, V, H]
METHOD iterator() RETURNS ReferenceIterator[K]
METHOD keys() RETURNS LinkedList[K]
METHOD isEmpty() RETURNS boolean
METHOD size() RETURNS int64
Number of key-value pairs currently stored. Read-only, O(1).
MODIFY METHOD clear() RETURNS void
MODIFY METHOD insert(K key, V value) RETURNS STATUS
MODIFY METHOD insertCopy(REFERENCE K key, REFERENCE V value) RETURNS STATUS
METHOD lookup(REFERENCE K key) RETURNS | MUTABLE REFERENCE V
METHOD lookupFast(REFERENCE K key) RETURNS boolean
Dictionary.lookupFast — same probe as lookup, without the STATUS that
lookup must allocate on a miss.
MODIFY METHOD remove(REFERENCE K key) RETURNS | K
METHOD contains(REFERENCE K key) RETURNS boolean
METHOD equals(REFERENCE Dictionary[K, V, H] other) RETURNS boolean
Two dictionaries are equal when they hold the SAME ASSOCIATIONS: the same keys, each mapping to an EQUAL VALUE.
Not merely the same KEYSET. That would rank {a:1} equal to {a:2}, so
two "equal" dictionaries would answer lookup(a) differently and neither
could stand in for the other — which is the whole point of an equivalence.
Keys-AND-values is what C++ (map and unordered_map), Rust, Swift,
Julia, Python and Java all specify. Keyset equality is a perfectly good
relation, but it is equality of the DOMAIN rather than of the dictionary,
and it deserves its own name (hasSameKeys) rather than this one.
Equal sizes plus a one-way walk is sufficient: with the counts already
equal, other cannot carry a key this one lacks unless this one also
carries a key other lacks — and the walk would have found that.
CLASS HashBucket
IMPLEMENTS Cloneable, Comparable
PARAMETERISED OVER HASH: uint64 for ChainedHashDictionary (FNV), uint128 for
ProHashDictionary (SipHash-2-4-128). Every scan rejects on the cached HASH before the
key compare (CollisionNode.matches). Equal cached hashes form a contiguous run the
scans walk for the key match.
The lookup returns a TRIVIAL (boolean found, V value) multi-return — NOT a
(V|STATUS) pipe-XOR — so the hot path never constructs an _EvStatus (whose
_ValueArray member is non-trivially destructible). The dictionary builds the
interface-mandated (V|STATUS) once at its public boundary.
A method may not be chained off an indexed SELF field (.data[i]->m() is a parse
error), so an element is bound to a local reference before the call; counted
iteration uses WHILE.
See: CollisionNode.ev, HashConstants.ev, ChainedHashDictionary.ev / ProHashDictionary.ev, ENVZN_CONSTITUTION.md §I.J.vi (VALUE CLASS). HashBucket[K, V, HASH] — one sorted array of CollisionNodes (2 inline).
kernel/src/HashBucket.ev:35
Constructors
INIT()
Methods
METHOD size() RETURNS int64
Number of entries in this bucket.
MODIFY METHOD insert(K key, V value, HASH hash) RETURNS boolean
Insert key→value (with cached hash). TRUE if a new entry was added,
FALSE if an existing key's value was replaced in place.
METHOD lookup(HASH hash, REFERENCE K key) RETURNS boolean
Look up key (with cached hash) — TRIVIAL (found, value) multi-return, so
the hot path never builds an _EvStatus. value is the type default on a miss.
HYBRID (measured crossover at depth ~24, 2026-06-25): the common shallow bucket
(≤ LINEAR_SCAN_MAX entries) is a plain linear scan from 0 — no lowerBound
binary search, and one matches() per node (no redundant outer hash pre-check).
Only a deep/flooded bucket (> LINEAR_SCAN_MAX) pays the O(log n) sorted path.
The sorted invariant is kept by insert, so the deep path stays valid.
METHOD lookupRef(HASH hash, REFERENCE K key) RETURNS | MUTABLE REFERENCE V
Look up key — a non-owning reference into the stored value, or FAILURE.
A borrow-propagating ("mirror") accessor: the returned reference mirrors
the receiver's mutability (a mutable bucket yields a MUTABLE REFERENCE, a
const bucket a read-only REFERENCE). Binds directly to the matched node's
INTERNAL value field.
METHOD containsKey(HASH hash, REFERENCE K key) RETURNS boolean
TRUE if key is present.
METHOD keyCloneAt(int64 i) RETURNS K
A clone of the i-th node's key.
METHOD valueCloneAt(int64 i) RETURNS V
A clone of the i-th node's value.
METHOD hashAt(int64 i) RETURNS HASH
The cached hash of the i-th node — backs growAndRehash re-bucketing.
MODIFY METHOD removeEntry(HASH hash, REFERENCE K key) RETURNS | K
Remove key — returns the cloned-out key, or FAILURE. Shift-removes from the
sorted live region (the tail slides left, count--; the physical array is not
shrunk — the freed tail slot is reused by the next insert).
Evict the entry for key and hand BOTH halves back, moved — never
cloned. Replaces the former removeKey, which was written as
read-then-delete (keyCloneAt(pos) followed by the shift) and so
deep-copied the very key it was about to destroy — the Array.popLast
defect shape fixed in 3003ed5f, in its dictionary form.
The survivors still shift down and data's physical length is left
alone: it is a non-shrinking high-water mark and the vacated tail slot
is reused by the next insert (see the header note).
MODIFY METHOD placeNew(K key, V value, HASH hash) RETURNS void
Build a node from (key, value, hash) and sorted shift-insert it. No update scan; the caller guarantees the key is new (rehash re-place / known-fresh).
METHOD clone() RETURNS HashBucket[K, V, HASH]
Deep copy — every live node cloned, in sorted order (so the clone stays sorted).
METHOD equals(REFERENCE HashBucket[K, V, HASH] other) RETURNS boolean
METHOD isLessThan(REFERENCE HashBucket[K, V, HASH] other) RETURNS boolean
CLASS HashBucketIterator
IMPLEMENTS ReferenceIterator
HashBucketIterator walks the dictionary's HashBucket[K,V,HASH][] buckets (a raw value-array of inline
buckets) in bucket order, yielding a non-owning REFERENCE to each entry's key. Holds
a mutation-locked REFERENCE to the buckets array (set in INIT from a by-reference
parameter — §REFERENCE condition 2); the lock pins the source dictionary against
mutation for the iterator's declaring-block lifetime.
CURSOR MODEL: (bi, ei) is the NEXT position to read — bucket bi, entry ei
within that bucket (0→slot0, 1→slot1, k≥2→spill[k-2]). advanceToValid() normalises
the pair so it either names a live entry (bi < nbuckets, ei < bucket.size()) or
sits at end (bi >= nbuckets). hasNext is bi < nbuckets. Iteration order is
unspecified (bucket order, and within a bucket the 2 slots then the hash-sorted
spill) — the Dictionary interface promises no order for the hash impls.
The per-entry key is reached by binding directly to the bucket's / node's INTERNAL
fields (same ENVZN module) — =@ node.key — rather than via a value-class accessor
method, whose inlining into a =@ bind would drop the reference and copy the
move-only key (the long-standing key-bind gotcha). The bucket count is read through
size().
Parameterised over [K, V, HASH] — the dictionary's hasher type is not needed here (the iterator never hashes), so it is left off the iterator's contract; HASH is carried only to name the bucket/node types. HashBucketIterator[K, V, HASH] — forward-only key iterator over a bucket array.
kernel/src/HashBucketIterator.ev:43
Constructors
INIT(REFERENCE HashBucket[K, V, HASH][] buckets)
Methods
METHOD hasNext() RETURNS boolean
MODIFY METHOD next() RETURNS | REFERENCE K
METHOD peek() RETURNS | REFERENCE K
MODIFY METHOD skip(uint64 n) RETURNS | REFERENCE K
CLASS CollisionNode
IMPLEMENTS Cloneable, Comparable
CollisionNode.ev — one (key, value, cachedHash) cell, as a VALUE CLASS so the
hash bucket stores it INLINE (no per-entry heap node). This is the Phase-1 dict-
rebuild substrate replacing the heap-allocated ChainedHashEntry: a HashBucket
holds CollisionNodes by value (two inline slots + a contiguous spill), and an
Array[CollisionNode[K,V]] lowers to inline storage — _ValueArray when K,V are
primitive (the trivially-copyable C-class path), _ObjectArray-by-value when K is
a class (e.g. a String key → owning handle field, the managed value-class form
the compiler deep-copies).
The key's hash is cached at insert (idea from libdict): a resize re-buckets via the cached hash without re-hashing keys, and a lookup rejects on the uint64 hash before the (possibly expensive) key compare.
K IS (Hashable AND Equatable); V is Cloneable / primitive. IMPLEMENTS Cloneable so
a node can be an Array[CollisionNode] element (whose clone() the bucket needs).
See: HashBucket.ev (the holder), ChainedHashDictionary.ev (the consumer),
ENVZN_CONSTITUTION.md §I.J.vi (VALUE CLASS). (Replaces the old heap-class
ChainedHashEntry, removed in the Phase-2 dict rebuild.)
CollisionNode[K, V, HASH] — a (key, value, cachedHash) bucket cell, value-typed.
HASH is the cached-hash width: uint64 for ChainedHashDictionary (FNV), uint128
for ProHashDictionary (SipHash-2-4-128). Parameterising the width keeps the common
int-key node tight (16 B at uint64 — 4 nodes/cache line) while letting the Pro
table keep its full 128-bit digest for the cheap pre-compare reject.
kernel/src/CollisionNode.ev:41
Constructors
INIT(K k, V v, HASH h)
Methods
METHOD hashValue() RETURNS HASH
The cached hash of this node's key (used by resize re-bucketing and the pre-compare reject in lookup).
METHOD matches(HASH h, REFERENCE K other) RETURNS boolean
TRUE if this node matches both the query hash and the query key. The cached
HASH is checked first (cheap reject); only on a hash hit is identity confirmed
via Equatable (class keys) or == (primitive keys) — the design doc's "compare
hash first, confirm key" step. Backs every lookup/insert/remove scan.
MODIFY METHOD setValue(V v) RETURNS void
Replace the stored value in place — backs insert-over-existing-key (the key and cached hash are unchanged).
METHOD valueClone() RETURNS V
A clone of the stored value — backs by-value lookup.
METHOD keyClone() RETURNS K
A clone of the stored key — backs keys().
MODIFY METHOD takeKey() RETURNS K
move the stored key out — backs remove(). The node is being evicted, so
the key is handed to the caller rather than copied and destroyed
("move in => move out"). Reading .key afterward is a use-after-move;
the only caller evicts the node in the same operation.
MODIFY METHOD takeValue() RETURNS V
move the stored value out — the twin of takeKey, same contract.
METHOD clone() RETURNS CollisionNode[K, V, HASH]
Deep copy — key and value cloned, cached hash carried across.
METHOD equals(REFERENCE CollisionNode[K, V, HASH] other) RETURNS boolean
METHOD isLessThan(REFERENCE CollisionNode[K, V, HASH] other) RETURNS boolean
CLASS IntKeyedDictionary
IMPLEMENTS Dictionary
HIDDEN concrete Dictionary impl specialised for INTEGER keys: linear-probe open-addressing over a STRUCT-OF-ARRAYS (K[] keys + V[] vals), with a hash & mask home index. The TRUSTED-KEY FAST TIER. Pure Envzn, zero-dep.
WHY it is fast (the s04 envzn_oa prototype — ~1.1x C for int32->int64, the fastest
s04 result; verified by benchmarks/s04_dict/envzn_intkeyed):
- STRUCT-OF-ARRAYS, not chained buckets: one flat array level, so the hot probe
is a direct keys[s] read (C's own layout) — no bucket->node indirection, no
per-node binary search, no redundant hash+key re-compare.
- hash & mask index (the LOW bits): with an identity hasher on dense/sequential
integer keys this maps sequential keys to SEQUENTIAL slots — cache-line locality
+ prefetch, the effect that made envzn_oa (1.04x C) beat multiply-shift's
scatter (envzn_lpint, 1.38x). Distribution is the pluggable HASHER's job:
identity for trusted/dense keys (fastest), a mixing hasher (DefaultHasher/
SipHasher) for adversarial keys. The key-set-aware multiply-shift + prime
fallback for the robust general case lives in ShallowDictionary, NOT here.
- A non-pipe-XOR fast read: operator[] (d[key]) and lookupFast return by
value / (found,value), skipping the (V | STATUS) tuple the interface lookup
builds per call — the s04 IntDict lever ("no pipe-XOR on the hot path"). Held
CONCRETELY (not behind the Dictionary interface) the whole lookup inlines to
C-class code (~1.1x C vs ~3x through the interface + pipe-XOR).
- ONE compare per probe: keys[s] == key (a primitive ==), then a field read of
vals[s]. No cached-hash pre-check (integer keys are their own cheap hash).
STORAGE — two parallel value-arrays. Empty/tombstone slots are marked in keys[]
by two RESERVED sentinel key values:
-1 = EMPTY (probe stops) -2 = TOMBSTONE (probe continues; slot reusable)
vals[] in a non-live slot holds 0 and is never read. This mirrors LpInt's -1
sentinel, extended with a tombstone so the full Dictionary remove surface works.
KEY DOMAIN (documented precondition): keys must not equal the two reserved sentinels (-1, -2). In practice IntKeyedDictionary is for NON-NEGATIVE / trusted integer keys — the fast opt-in tier (as FastIntHasher is the fast opt-in hasher). Full-range or untrusted keys → ChainedHashDictionary. A dev-build ASSERT guards the precondition.
HASHER — stored CONCRETELY as the type parameter H hasher (a VALUE-CLASS hasher such
as InlineIntHasher lives inline, no _ev_unique handle), so .hasher->hash(key) can
inline. (Measured: the hasher is a minor factor — the dominant lever is the shape, i.e.
concrete-hold + non-pipe-XOR read, not the hash call.) Because a type parameter cannot
be default-constructed generically (CREATE H() mis-codegens — envzn-internals defect),
the hasher is REQUIRED at construction: CREATE IntKeyedDictionary[K,V,H](CREATE
InlineIntHasher[K]()) — the caller-passes-the-hasher pattern ChainedHashDictionary uses.
IMPLEMENTS Dictionary[K, V, H] — Cloneable comes transitively (Dictionary EXTENDS
Cloneable). Full interface surface (rehash / tombstones / iterator / clone) PLUS the
concrete fast path (operator[] / lookupFast) for callers holding the concrete type.
kernel/src/IntKeyedDictionary.ev:62
Fields
int64 countint64 tombstonesint64 capuint64 maskK emptyKK tombK
Constructors
INIT(H h)
Methods
MODIFY METHOD insert(K key, V value) RETURNS STATUS
MODIFY METHOD insertCopy(REFERENCE K key, REFERENCE V value) RETURNS STATUS
METHOD lookup(REFERENCE K key) RETURNS | MUTABLE REFERENCE V
METHOD __op_index__(REFERENCE K key) RETURNS V
Direct value read — d[key]. Returns the stored value on a hit, or the V
default (0) on a miss. NO pipe-XOR (V | STATUS) tuple on the hot path: the
s04 experiments (IntDict) showed the pipe-XOR construction is a real per-lookup
cost, and operator[] returns V by value like ByteBuffer/String subscript.
Use contains/lookup when a miss must be distinguished from a stored 0.
METHOD lookupFast(REFERENCE K key) RETURNS boolean
Comma-return lookup — boolean f, V v := d->lookupFast(key). No pipe-XOR
(V | STATUS) tuple; the trivial (found, value) return the s04 prototypes
(IntDict/OADict) used to stay C-class. found distinguishes a miss from a
stored default.
METHOD contains(REFERENCE K key) RETURNS boolean
MODIFY METHOD remove(REFERENCE K key) RETURNS | K
Remove — hands back the stored key and value.
NOTE: unlike ChainedHashDictionary, this cannot move the halves out. The
storage is a struct-of-arrays open-addressed table, and reading a slot
(.vals[s]) is a subscript READ, which by rule clones rather than moves
(cxx_emit stmt_assign.py:149 — "the subscript is a read accessor, never
a move-out"). HashBucket escapes this because it can evict a whole
CollisionNode and take its fields; an SoA table has no move-out-at-index
primitive to call. So this pays one DUPLICATE per half, and the census
pins it at that rather than at 0. K is an integer here, so the key half
is a bit copy; only a class-typed V costs a real clone.
METHOD isEmpty() RETURNS boolean
METHOD size() RETURNS int64
MODIFY METHOD clear() RETURNS void
METHOD iterator() RETURNS ReferenceIterator[K]
METHOD keys() RETURNS LinkedList[K]
METHOD clone() RETURNS Dictionary[K, V, H]
METHOD equals(REFERENCE Dictionary[K, V, H] other) RETURNS boolean
for Equatable Interface Dictionary is equal if all keys between the two dictionaries are equal Two dictionaries are equal when they hold the SAME ASSOCIATIONS: the same keys, each mapping to an EQUAL VALUE.
Not merely the same KEYSET. That would rank {a:1} equal to {a:2}, so
two "equal" dictionaries would answer lookup(a) differently and neither
could stand in for the other — which is the whole point of an equivalence.
Keys-AND-values is what C++ (map and unordered_map), Rust, Swift,
Julia, Python and Java all specify. Keyset equality is a perfectly good
relation, but it is equality of the DOMAIN rather than of the dictionary,
and it deserves its own name (hasSameKeys) rather than this one.
Equal sizes plus a one-way walk is sufficient: with the counts already
equal, other cannot carry a key this one lacks unless this one also
carries a key other lacks — and the walk would have found that.
CLASS IntKeyedDictionaryIterator
IMPLEMENTS ReferenceIterator
The ReferenceIterator OF K returned by IntKeyedDictionary.iterator(). Walks the struct-of-arrays K[] keys, yielding a REFERENCE K per live slot (a slot whose key is neither the EMPTY sentinel -1 nor the TOMBSTONE sentinel -2).
Holds a mutation-locking REFERENCE to the dictionary's keys value-array (bound
with =@ in INIT); the lock pins the source dictionary against mutation for the
iterator's declaring-block lifetime. Cursor pos is the next slot to examine;
advanceToValid() skips empty (-1) and tombstone (-2) slots.
Parameterised over [K, V] to match the dictionary's arity (the vals array is not
walked and V is otherwise unused, but keeping the pair mirrors ShallowDictionaryIterator
and leaves room for a key+value iterator later). The hasher H is not needed — the
iterator never hashes.
kernel/src/IntKeyedDictionaryIterator.ev:29
Constructors
INIT(REFERENCE K[] keyArr, int64 capacity)
Methods
METHOD hasNext() RETURNS boolean
MODIFY METHOD next() RETURNS | REFERENCE K
METHOD peek() RETURNS | REFERENCE K
MODIFY METHOD skip(uint64 n) RETURNS | REFERENCE K
CLASS ProHashDictionary
IMPLEMENTS Dictionary
The DoS-RESISTANT dictionary, rebuilt (2026-07-13) on the byte-keyed substrate that made StringKeyedDictionary beat C. Pure Envzn, zero-dep beyond the SipHash key shim.
WHY THE REBUILD (the measurement that forced it):
The old ProHash — Dictionary[BinaryWord,V,H] over char32 String keys, interface dispatch,
pipe-XOR returns, heap-node chains — ran the s05 word-dict at 1505 ms, 32x C.
The s05 experiment envzn_strkeyed_sip then ran the SAME SipHash-2-4 over the
byte-keyed open-addressing substrate at 49 ms — a 30x speedup at IDENTICAL
DoS-resistance. So ProHash's cost was never SipHash; it was the SHAPE. This class
keeps everything that made it "Pro" and rebuilds the shape underneath it.
WHAT MAKES IT "PRO" (both properties retained): 1. SipHash-2-4 keying. A process-global 16-byte secret (OS entropy, the same FOREIGN key shim SipHasher uses), so an attacker cannot predict bucket distribution — hash-flooding DoS defence by construction. 2. ADAPTIVE deep tier: chain -> RED-BLACK TREE. A bucket starts as a linear CHAIN (optimal when shallow — the overwhelmingly common case). If a bucket's depth exceeds TREEIFY_THRESHOLD (8) it is TREEIFIED into a red-black tree ordered by (cached hash, then key bytes) — a total order — so even a bucket an attacker somehow manages to overfill degrades to O(log n), never O(n). This is the belt-and-suspenders behind the SipHash keying: SipHash makes deep buckets unforceable; the tree bounds the damage if one ever occurs anyway.
THE SPEED LEVERS (carried over from StringKeyedDictionary):
- char8[] BYTE keys, not String. String stores char32 code points and hashes /
compares them one handle-indirected, bounds-checked element at a time — that was
the dominant cost. The caller transcodes ONCE (s INTO ByteBuffer -> bytes).
(Byte keys also mean this cannot be a Dictionary[BinaryWord,V,H] impl: the interface
cannot fix a type parameter (E2103) and an array key is not PRIMITIVE. It is a
standalone CLASS, as StringKeyedDictionary is.)
- System->memoryWord absorbs the SipHash message 8 BYTES PER ROUND (one wide
load) instead of eight bounds-checked byte reads.
- HOTLOOP hoists the per-byte bounds check out of the tail/compare loops.
- System->memoryCompare confirms a key with one std::memcmp, not a per-byte loop.
- No pipe-XOR on the hot path: lookupFast returns a trivial (found, value) and
operator[] returns V by value — neither builds the (V | STATUS) tuple. The
pipe-XOR lookup remains for the kernel idiom, off the hot path.
STORAGE — a flat byte pool + a flat ENTRY ARENA (SoA), no per-entry heap node:
- keyPool : char8[] — every key's bytes, contiguous.
- per-bucket: bucketList (the member CHAIN head — kept intact in BOTH modes, so
enumeration is always a chain walk), bucketRoot (the red-black index root when
treeified; -1 otherwise), bucketMode (0 chain / 1 tree), bucketDepth. The tree
is an ADDITIONAL lookup index over the same entries, never the sole structure —
that is what keeps rehash/remove/clone simple.
- per-entry: entHash, entOff/entLen (slice into keyPool), entVal,
entNext (chain link, doubles as the free-list link), entLeft/entRight/
entParent/entRed (red-black links, tree mode), entLive.
Freed entries are recycled through freeHead.
REMOVE — a deliberate, bounded simplification (labelled, not hidden): removing from a TREEIFIED bucket does not run the CLRS red-black delete-fixup. It collects the bucket's live entries, drops the target, and REBUILDS the bucket (re-chained if the remaining depth <= UNTREEIFY_THRESHOLD, else re-treeified). That is O(depth) — bounded, correct, and it keeps ~150 lines of the most bug-prone code in a red-black tree out of the kernel. Lookup and insert — the hot paths — use the real red-black tree.
kernel/src/ProHashDictionary.ev:78
Constructors
INIT()
Methods
MODIFY METHOD insertSlice(REFERENCE binary[] buf, int64 off, int64 len, V value) RETURNS STATUS
METHOD lookupFastSlice(REFERENCE binary[] buf, int64 off, int64 len) RETURNS boolean
Comma-return lookup — no pipe-XOR (V | STATUS) tuple on the hot path.
METHOD containsSlice(REFERENCE binary[] buf, int64 off, int64 len) RETURNS boolean
MODIFY METHOD removeSlice(REFERENCE binary[] buf, int64 off, int64 len) RETURNS boolean
Remove. Unlinks the entry from the bucket's member chain, then RE-INDEXES the bucket: a treeified bucket that has fallen to the untreeify threshold drops its red-black index and reverts to a plain chain (hysteresis: treeify at >8, untreeify at <=6, so churn around the boundary cannot thrash); one still above it rebuilds the index. Rebuilding rather than running the CLRS red-black delete-fixup is a deliberate, bounded simplification (O(depth)) — labelled, not hidden. It keeps the single most bug-prone routine in a red-black tree out of the kernel, and remove is not the hot path; lookup and insert use the real tree.
MODIFY METHOD insert(REFERENCE binary[] key, V value) RETURNS STATUS
METHOD lookupFast(REFERENCE binary[] key) RETURNS boolean
METHOD __op_index__(REFERENCE binary[] key) RETURNS V
Direct value read — d[key]. V default (0) on a miss. No pipe-XOR.
METHOD lookup(REFERENCE binary[] key) RETURNS | V
Pipe-XOR lookup — the kernel idiom, kept OFF the hot path.
METHOD lookupRef(REFERENCE binary[] key) RETURNS | MUTABLE REFERENCE V
METHOD contains(REFERENCE binary[] key) RETURNS boolean
MODIFY METHOD remove(REFERENCE binary[] key) RETURNS boolean
METHOD isEmpty() RETURNS boolean
METHOD size() RETURNS int64
MODIFY METHOD clear() RETURNS void
METHOD bucketIndexOf(REFERENCE binary[] key) RETURNS int64
The bucket a key currently maps to.
Exposed for TESTABILITY of the adaptive tier. SipHash keying means an attacker cannot force colliding keys — which also means a TEST cannot, so the chain->tree escalation would otherwise be unreachable, untested kernel code. This lets a test group keys that share a bucket and drive the escalation deterministically.
It does NOT weaken the DoS guarantee: hash-flooding's threat model is "the attacker supplies KEYS (data) that the victim hashes", not "the attacker calls methods on the victim's dictionary". Anyone able to invoke this already has code execution in-process, at which point the hash is not the weak link. Do not, however, echo the result across a trust boundary — that WOULD hand out a collision oracle.
METHOD maxBucketDepth() RETURNS int64
Deepest bucket in the table — the clustering metric the adaptive tier bounds.
METHOD treeifiedBuckets() RETURNS int64
How many buckets have escalated to a red-black tree. Zero under normal load — non-zero means collisions got deep (the adaptive tier is doing its job). Exposed so the escalation is TESTABLE and observable, not a silent internal.
METHOD iterator() RETURNS ReferenceIterator[BinaryWord]
One BinaryWord per live entry, borrowed from this dictionary's key
pool. Valid only while the dictionary is unmodified: an insert may grow
the pool and a rehash may move it.
METHOD keys() RETURNS LinkedList[BinaryWord]
MODIFY METHOD insertCopy(REFERENCE BinaryWord key, REFERENCE V value) RETURNS STATUS
MODIFY METHOD insert(BinaryWord key, V value) RETURNS STATUS
METHOD lookupFast(REFERENCE BinaryWord key) RETURNS boolean
Dictionary.lookupFast — the interface-shaped (BinaryWord-keyed) peer of
the binary[] slice form above. K is BinaryWord for this class, so this
is the overload that satisfies the interface; the slice form stays for
the zero-copy call sites that already hold a buffer and a range.
METHOD lookup(REFERENCE BinaryWord key) RETURNS | MUTABLE REFERENCE V
METHOD contains(REFERENCE BinaryWord key) RETURNS boolean
MODIFY METHOD remove(REFERENCE BinaryWord key) RETURNS | BinaryWord
The removed key is a word over this dictionary's own pool. A freed entry
is recycled through freeHead but its BYTES stay in the pool, so the
returned word reads correctly until the pool is next compacted.
METHOD equals(REFERENCE Dictionary[BinaryWord, V, H] other) RETURNS boolean
Equal when both hold the same keys mapped to the same values. Walks the smaller surface — this dictionary's entries — and asks the other by lookup, so the cost is one hash per key rather than a cross product. Two dictionaries are equal when they hold the SAME ASSOCIATIONS: the same keys, each mapping to an EQUAL VALUE.
Not merely the same KEYSET. That would rank {a:1} equal to {a:2}, so
two "equal" dictionaries would answer lookup(a) differently and neither
could stand in for the other — which is the whole point of an equivalence.
Keys-AND-values is what C++ (map and unordered_map), Rust, Swift,
Julia, Python and Java all specify. Keyset equality is a perfectly good
relation, but it is equality of the DOMAIN rather than of the dictionary,
and it deserves its own name (hasSameKeys) rather than this one.
Equal sizes plus a one-way walk is sufficient: with the counts already
equal, other cannot carry a key this one lacks unless this one also
carries a key other lacks — and the walk would have found that.
METHOD clone() RETURNS Dictionary[BinaryWord, V, H]
Deep copy. Returns the Dictionary INTERFACE, not the concrete class —
the class is HIDDEN, so its name may appear only as a CREATE target. The
copy goes in through the interface's own insert, since insertSlice
is this class's and not the interface's.
CLASS ProHashDictionaryIterator
IMPLEMENTS ReferenceIterator
Walks the ENTRY ARENA, yielding one BinaryWord per live entry.
WHY THE ARENA AND NOT THE BUCKETS. ProHash stores entries in flat parallel arrays and threads them into buckets — a chain when shallow, a red-black tree once a bucket passes the treeify threshold. Walking the buckets would mean two traversal shapes and a recursion for the tree tier; walking the arena is one linear pass that is correct for both, and it touches each array in order rather than chasing indices. Iteration order is arena order, which is neither insertion nor hash order — a dictionary promises no order, and this one says so rather than implying one it would have to keep.
Freed entries are recycled through freeHead, so a dead slot may sit
anywhere in the arena; entLive is the only thing that decides.
Like its StringKeyed sibling, the window is one VALUE field re-assigned per step — no allocation per key.
kernel/src/ProHashDictionaryIterator.ev:32
Constructors
INIT(REFERENCE binary[] pool, REFERENCE int64[] offs, REFERENCE int64[] lens, REFERENCE int8[] live, int64 arenaSize)
Methods
METHOD hasNext() RETURNS boolean
MODIFY METHOD next() RETURNS | REFERENCE BinaryWord
METHOD peek() RETURNS | REFERENCE BinaryWord
MODIFY METHOD skip(uint64 n) RETURNS | REFERENCE BinaryWord
CLASS RedBlackTreeDictionary
IMPLEMENTS Dictionary
An ORDERED Dictionary[K, V, H] backed by a left-leaning red-black tree (Sedgewick LLRB), ARENA form. Guaranteed O(log n) lookup / insert / remove; iteration yields keys in Comparable order.
── Phase-4 arena rewrite (2026-06-24) ────────────────────────────────────────
Nodes live inline in ONE RBNode[K, V][] arena value-array; child links are
int64 arena indices (-1 = null), not owning handles. The owning-handle design's
per-node heap allocation (one _ev_unique<RBNode> per key) collapses to a
single growable array. rootIdx is a plain int64 (no RBRoot holder — index
assignment can't trip the move-out/EMPTY-consume rules that forced the holder).
A free-list (freeHead, threaded through a dead slot's left) reclaims slots
on delete. The iterator walks an explicit index stack (O(n) total), retiring
the subSize/nthKeyRef rank machinery.
THE ARENA DISCIPLINE (load-bearing — proved by kernel_probe/rbArenaProbe):
appending a node (alloc) can REALLOCATE the arena and dangle any live
reference into it. So: indices are stable, references are NOT. Every node
access is wrapped in a tiny index-helper (leftOf, setLeftAt, …) that binds
a fresh =@ .arena[i], touches the node, and drops the ref before returning —
so no reference is ever held across an alloc()/insertNode() that might grow the
arena. Insert allocs exactly one leaf at the bottom of the recursion; delete
never allocs (only frees), so the delete path is realloc-free.
(The helpers also sidestep a parser limitation: a bare { … } block whose
first statement declares a comma-bearing type — REFERENCE RBNode[K, V] n — is
misparsed as a set literal. Encapsulating each bind in a method avoids the bare
block entirely and reads better.)
K must additionally be Comparable (the tree orders by key). The hasher type H
is part of the Dictionary interface but UNUSED here — an ordered tree compares
keys, it does not hash — so no hasher is ever constructed (H bound satisfied
vacuously). Comparison goes through the WHEN K IMPLEMENTS Comparable[K] split
(the SortedList idiom): the compiler lowers built-in Comparable (primitives,
String) under that guard; a bare a->isLessThan(b) emits a literal method
clang rejects for primitives.
kernel/src/RedBlackTreeDictionary.ev:52
Fields
int64 rootIdxint64 countint64 freeHead
Constructors
INIT()
INIT(Hasher[K] h)
INIT(GrowthHint hint, int64 initialCapacity)
INIT(Hasher[K] h, GrowthHint hint, int64 initialCapacity)
Methods
MODIFY METHOD insert(K key, V value) RETURNS STATUS
MODIFY METHOD insertCopy(REFERENCE K key, REFERENCE V value) RETURNS STATUS
METHOD lookup(REFERENCE K key) RETURNS | MUTABLE REFERENCE V
METHOD lookupFast(REFERENCE K key) RETURNS boolean
Dictionary.lookupFast — the same descent as lookup, without the
STATUS that lookup must allocate on a miss.
MODIFY METHOD remove(REFERENCE K key) RETURNS | K
Remove — hands back the STORED key and value (not the probe key, which is what the old key-only remove returned).
Like the SoA dictionaries and unlike ChainedHashDictionary, the halves are cloned rather than moved: the arena node is reached by index and a subscript read clones by rule (stmt_assign.py:149), and the LLRB delete rebalances the tree, so the node cannot simply be evicted first. Census pins this at one DUPLICATE per half.
METHOD contains(REFERENCE K key) RETURNS boolean
METHOD isEmpty() RETURNS boolean
METHOD size() RETURNS int64
Number of key-value pairs currently stored. Read-only, O(1).
MODIFY METHOD clear() RETURNS void
METHOD keys() RETURNS LinkedList[K]
METHOD iterator() RETURNS ReferenceIterator[K]
METHOD clone() RETURNS Dictionary[K, V, H]
METHOD equals(REFERENCE Dictionary[K, V, H] other) RETURNS boolean
for Equatable Interface Dictionary is equal if all keys between the two dictionaries are equal Two dictionaries are equal when they hold the SAME ASSOCIATIONS: the same keys, each mapping to an EQUAL VALUE.
Not merely the same KEYSET. That would rank {a:1} equal to {a:2}, so
two "equal" dictionaries would answer lookup(a) differently and neither
could stand in for the other — which is the whole point of an equivalence.
Keys-AND-values is what C++ (map and unordered_map), Rust, Swift,
Julia, Python and Java all specify. Keyset equality is a perfectly good
relation, but it is equality of the DOMAIN rather than of the dictionary,
and it deserves its own name (hasSameKeys) rather than this one.
Equal sizes plus a one-way walk is sufficient: with the counts already
equal, other cannot carry a key this one lacks unless this one also
carries a key other lacks — and the walk would have found that.
CLASS RedBlackTreeDictionaryIterator
IMPLEMENTS ReferenceIterator
Forward, in-order key iterator for RedBlackTreeDictionary (ARENA form). Yields keys in Comparable order (the tree's natural order), each as a non-owning REFERENCE into the live arena.
This holds an explicit index stack of the un-visited left spine and advances ONE step per next(): pop the top (the next in-order node), then push the left spine of its right child. O(n) total, O(h) space.
Per the iterator rules it holds NEITHER an iterator-typed field (Rule A /
E1112) NOR an owning class field (Rule B / E1113); a mutation-locked REFERENCE
to the arena array plus primitive cursors and an int64[] stack (a value-typed
storage shorthand — exempt from Rule B) are all permitted. The per-node key is
bound DIRECTLY to the node's INTERNAL key field (value =@ nd.key), never via
an accessor (whose inlining into a =@ bind would copy the move-only key —
the key-bind gotcha shared with HashBucketIterator).
Parameterised over [K, V] (V needed to name RBNode[K, V]); H is irrelevant.
kernel/src/RedBlackTreeDictionaryIterator.ev:34
Constructors
INIT(REFERENCE RBNode[K, V][] arena, int64 root, int64 count)
Methods
METHOD hasNext() RETURNS boolean
MODIFY METHOD next() RETURNS | REFERENCE K
METHOD peek() RETURNS | REFERENCE K
MODIFY METHOD skip(uint64 n) RETURNS | REFERENCE K
CLASS ShallowDictionary
IMPLEMENTS Dictionary
A Dictionary[K,V,H] with a WHOLE-KEY-SET-AWARE multiply-shift index and a prime-modulus fallback tier: linear-probe open-addressing over a STRUCT-OF-ARRAYS. Pure Envzn, zero-dep.
STATUS — PARKED as a narrow, documented option, NOT the default. A measurement-driven
finding (2026-07-13) settled its role: a dictionary lookup is memory-bound, and
cache-fast access requires either an IDENTITY hash on dense keys (IntKeyedDictionary's
key & mask, ~1.1x C) or INLINE byte-dense slots (StringKeyedDictionary). A MIXING
index — FNV, SipHash, OR this class's multiply-shift — scatters adjacent keys to
random slots by design, eating a cache miss per probe, which floors any such dict at
~3-4x C. Measured here: ~4.6x C for int keys, SLOWER than ChainedHashDictionary
(~3.3x). So multiply-shift is robust-but-slow; it does NOT unify "fast + robust" (the
two are physically opposed). Its one distinct property — structural (bit-pattern)
attack resistance without a keyed hasher — is better served by ProHashDictionary
(SipHasher, which also stops hash-collision attacks). Kept correct + tested as an
explicit option; use IntKeyedDictionary / StringKeyedDictionary for speed,
ChainedHashDictionary as the general default, ProHashDictionary for DoS.
WHY "SHALLOW": at each rehash it holds the whole live key set, SEARCHES a few
candidate 64-bit multipliers, and adopts the one that keeps home-slot depth
shallowest for THIS key set — a hash chosen for the actual keys, not blind. If no
pow2 multiplier can tame a pathological set (keys whose entropy is trapped in a few
low bits — e.g. all multiples of a power of two), it falls back to a PRIME-sized
table with mod-prime indexing, which is coprime to any 2^k stride and breaks those
structural collisions. This is the robustness IntKeyedDictionary's hash & mask
deliberately trades away for cache speed — ShallowDictionary is the safe default;
IntKeyedDictionary is the trusted-key fast tier.
INDEX — two composed layers (slotFor):
- pow2 (common): slot = (mult * hash) >> shift, probe (s+1) & mask.
- prime (fallback): slot = (mult * hash) % p, probe s+1 wrap-at-p.
The multiply-shift mixes ALL bits of the hasher output, so it rescues a weak/cheap
hasher (FastIntHasher's identity) and is harmless for a strong one (FNV/SipHash is
already uniform → the search converges on the first candidate). Full hasher
pluggability AND a whole-key-set-aware low-collision index, together.
STORAGE — four parallel value-arrays (SoA, blittable → flat _ValueArray; the hot
probe touches only the dense slotState/slotHash, then one keys[s]):
- slotState : int8[] — 0 empty / 1 full / 2 tombstone. Liveness lives here, so
ANY key value is valid (no reserved sentinels — unlike IntKeyedDictionary).
- slotHash : uint64[] — cached full hash per slot; the slotHash[s]==h
fast-reject skips the key compare on almost all non-matches (crucial once String
keys land — a mismatched hash never calls equals).
- keys : K[], vals : V[] — parallel per-slot key/value.
DoS: hash-flooding resistance comes from the pluggable hasher H — construct as
ShallowDictionary[K,V,SipHasher[K]] for a keyed, attacker-proof hash; the prime
fallback additionally bounds structural (bit-pattern) attacks. Default
(DefaultHasher / FNV) is the fast general-purpose choice.
V1 SCOPE: primitive K/V (the blittable fast path — SoA _ValueArray). String keys
(the design's 5x-over-chained headline) are the next step; they need object-array
slot handling (empty-slot placeholders + the cached-hash reject to gate equals),
which the cached-hash column above is already laid out for.
IMPLEMENTS Dictionary[K, V, H] — Cloneable comes transitively (Dictionary EXTENDS
Cloneable). H is the interface hasher slot; the stored hasher is the Hasher OF K
interface (no-arg INIT CREATEs DefaultHasher). Plus the non-pipe-XOR fast path
(operator[] / lookupFast) for callers holding the concrete type.
kernel/src/ShallowDictionary.ev:76
Fields
int64 countint64 tombstonesint64 capint64 shiftAmtuint64 multint64 maskboolean primeModeuint64 primeP
Constructors
INIT()
INIT(Hasher[K] h)
Methods
MODIFY METHOD insert(K key, V value) RETURNS STATUS
MODIFY METHOD insertCopy(REFERENCE K key, REFERENCE V value) RETURNS STATUS
METHOD lookup(REFERENCE K key) RETURNS | MUTABLE REFERENCE V
METHOD __op_index__(REFERENCE K key) RETURNS V
Direct value read — d[key]. Returns the stored value on a hit, or the V
default (0) on a miss. No pipe-XOR (V | STATUS) tuple on the hot path.
METHOD lookupFast(REFERENCE K key) RETURNS boolean
Comma-return lookup — boolean f, V v := d->lookupFast(key). No pipe-XOR;
found distinguishes a miss from a stored default.
METHOD contains(REFERENCE K key) RETURNS boolean
MODIFY METHOD remove(REFERENCE K key) RETURNS | K
Remove — hands back the stored key and value. Like IntKeyedDictionary and unlike ChainedHashDictionary, an SoA open-addressed table has no move-out-at-index primitive, so a subscript read clones rather than moves (stmt_assign.py:149). Census pins this at one DUPLICATE per half.
METHOD isEmpty() RETURNS boolean
METHOD size() RETURNS int64
MODIFY METHOD clear() RETURNS void
METHOD iterator() RETURNS ReferenceIterator[K]
METHOD keys() RETURNS LinkedList[K]
METHOD clone() RETURNS Dictionary[K, V, H]
METHOD equals(REFERENCE Dictionary[K, V, H] other) RETURNS boolean
for Equatable Interface Dictionary is equal if all keys between the two dictionaries are equal Two dictionaries are equal when they hold the SAME ASSOCIATIONS: the same keys, each mapping to an EQUAL VALUE.
Not merely the same KEYSET. That would rank {a:1} equal to {a:2}, so
two "equal" dictionaries would answer lookup(a) differently and neither
could stand in for the other — which is the whole point of an equivalence.
Keys-AND-values is what C++ (map and unordered_map), Rust, Swift,
Julia, Python and Java all specify. Keyset equality is a perfectly good
relation, but it is equality of the DOMAIN rather than of the dictionary,
and it deserves its own name (hasSameKeys) rather than this one.
Equal sizes plus a one-way walk is sufficient: with the counts already
equal, other cannot carry a key this one lacks unless this one also
carries a key other lacks — and the walk would have found that.
CLASS ShallowDictionaryIterator
IMPLEMENTS ReferenceIterator
The ReferenceIterator OF K returned by ShallowDictionary.iterator(). Walks the struct-of-arrays slots, yielding a REFERENCE K per live slot (slotState[pos] == 1).
Holds mutation-locking REFERENCEs to the dictionary's keys value-array and its
slotState liveness array (bound with =@ in INIT); the lock pins the source
dictionary against mutation for the iterator's declaring-block lifetime. Cursor
pos is the next slot to examine; advanceToValid() skips empty (0) and tombstone
(2) slots.
Parameterised over [K, V] to match the dictionary's arity; V is unused (the values are not walked), the hasher H is not needed (the iterator never hashes).
kernel/src/ShallowDictionaryIterator.ev:28
Constructors
INIT(REFERENCE K[] keyArr, REFERENCE int8[] stateArr, int64 capacity)
Methods
METHOD hasNext() RETURNS boolean
MODIFY METHOD next() RETURNS | REFERENCE K
METHOD peek() RETURNS | REFERENCE K
MODIFY METHOD skip(uint64 n) RETURNS | REFERENCE K
CLASS StringKeyedDictionary
IMPLEMENTS Dictionary
A byte-keyed dictionary: linear-probe open-addressing over a STRUCT-OF-ARRAYS with BYTE-DENSE keys held in ONE FLAT binary[] pool, a byte-FNV hash, and a memcmp confirm. The STRING FAST TIER. Pure Envzn, zero-dep.
WHY the key is a byte array, NOT Dictionary[String, V, H]:
Two compiler realities, met head-on, shaped this class (2026-07-13):
1. The Dictionary[BinaryWord,V,H] INTERFACE cannot fix one of its type parameters — an
impl must be parameterised over the SAME type variables as the interface (a
... IMPLEMENTS Dictionary[String, V, H] is rejected E2103: the interface body
still references BinaryWord, unbound in the impl's scope). So there is no way to write a
String-SPECIALISED Dictionary impl; a generic [BinaryWord,V,H] one (ChainedHashDictionary)
is the only shape, and it must hash/compare keys through the pluggable Hasher
+ Equatable == — the SLOW char32 path (String stores char32 code points; each
hash/equals iterates them through handle-indirected, bounds-checked accessors).
2. The s05 word-dict study (FINDINGS.md #5/#9/#17) proved the fast form is
BYTE-DENSE keys read by DIRECT FLAT-ARRAY INDEXING — "byte-dense IS faster; the
slowness was ByteBuffer method dispatch, not byte-density."
So this class takes the KEY AS binary[] bytes (the caller transcodes its String
once, key INTO ByteBuffer → bytes, UNTIMED — exactly the s05 benchmark's contract),
sidestepping BOTH the interface-fix wall and the per-lookup String→bytes transcode.
It is a standalone CLASS, constructed directly. The "relocate cost inward" tier for
string/byte keys.
MODELLED ON benchmarks/s05_worddict/envzn_shimopt/native/native.hpp — the shim's
open-addressing map (byte-FNV + linear probe + stored-hash reject + length reject +
memcmp confirm), REBUILT PURE: what the inline C++ shim did with libc, the new
language performance levers do in-language —
- HOTLOOP → the byte-FNV hash loop (hoists the per-byte bounds check to
one up-front range check; the constitution I.H.iii(f) example
IS this hash).
- System->memoryCompare → the key confirm (std::memcmp, one bounds-checked call per
probe instead of a per-byte Envzn loop). The HOT-path lever.
- System->memoryWord → available for a word-wise long-key hash (not exercised by the
short Shakespeare words; a length-gated follow-up).
STORAGE — a FLAT byte pool + five parallel per-slot arrays (SoA; the hot probe touches
only the dense slotState/slotHash, then the pooled key bytes):
- keyBytes : binary[] — ONE flat, append-only pool; every key's bytes live here,
contiguous — the cache-dense form the s05 study crowned (#17). A removed key's
bytes linger until clear() (no compaction in V1 — a bounded, documented cost;
memoryCopy-based compaction on high-tombstone rehash is the follow-up).
- slotOff : int32[], slotLen : int32[] — each slot's key start + length in the pool.
- slotState : int8[] — 0 empty / 1 full / 2 tombstone (liveness; any byte content
is a valid key).
- slotHash : uint64[] — cached full FNV hash; the slotHash[s]==h fast-reject skips
the length + memcmp on almost every non-match (the crucial gate for byte keys).
- vals : V[] — parallel per-slot value.
INDEX — hash & mask (pow2), probe (s+1) & mask — native.hpp's index exactly. FNV
is a MIXING hash so this scatters, but for arbitrary string keys there is no dense-key
structure to exploit; the byte-dense pool + cached-hash reject is the cache lever here,
not index locality (the s05 finding, distinct from IntKeyedDictionary's key & mask).
SURFACE — a byte-array analogue of the Dictionary fast tier: insert/lookupFast/
lookup (pipe-XOR)/lookupRef/contains/remove/size/isEmpty/clear/clone.
Each comes in a whole-array form (insert(binary[] key, ...), key IS the array) and a
zero-copy SLICE form (insertSlice(buf, off, len, ...)) for the s05 flat-query loop
that never allocates per lookup. Iteration over pooled keys is a deferred follow-up.
kernel/src/StringKeyedDictionary.ev:73
Fields
uint64 primeuint64 h_maskint64 countint64 tombstonesint64 capuint64 mask
Constructors
INIT()
Methods
MODIFY METHOD insertSlice(REFERENCE binary[] buf, int64 off, int64 len, V value) RETURNS STATUS
Insert the key slice buf[off .. off+len) → value (value MOVEd in). New keys
are interned into the flat pool. Replacing an existing key leaves count unchanged.
METHOD lookupFastSlice(REFERENCE binary[] buf, int64 off, int64 len) RETURNS boolean
Comma-return lookup over a slice — boolean f, V v := d->lookupFastSlice(buf,off,len).
No pipe-XOR tuple on the hot path.
METHOD lookupSlice(REFERENCE binary[] buf, int64 off, int64 len) RETURNS | V
METHOD containsSlice(REFERENCE binary[] buf, int64 off, int64 len) RETURNS boolean
MODIFY METHOD removeSlice(REFERENCE binary[] buf, int64 off, int64 len) RETURNS boolean
MODIFY METHOD insert(REFERENCE binary[] key, V value) RETURNS STATUS
METHOD lookupFast(REFERENCE binary[] key) RETURNS boolean
METHOD lookup(REFERENCE binary[] key) RETURNS | V
METHOD lookupRef(REFERENCE binary[] key) RETURNS | MUTABLE REFERENCE V
METHOD contains(REFERENCE binary[] key) RETURNS boolean
MODIFY METHOD remove(REFERENCE binary[] key) RETURNS boolean
METHOD isEmpty() RETURNS boolean
METHOD size() RETURNS int64
MODIFY METHOD clear() RETURNS void
MODIFY METHOD insert(BinaryWord key, V value) RETURNS STATUS
METHOD lookupFast(REFERENCE BinaryWord key) RETURNS boolean
Dictionary.lookupFast — the interface-shaped (BinaryWord-keyed) peer of
the binary[] slice form above. K is BinaryWord for this class, so this
is the overload that satisfies the interface; the slice form stays for
the zero-copy call sites that already hold a buffer and a range.
METHOD lookup(REFERENCE BinaryWord key) RETURNS | MUTABLE REFERENCE V
METHOD contains(REFERENCE BinaryWord key) RETURNS boolean
MODIFY METHOD remove(REFERENCE BinaryWord key) RETURNS | BinaryWord
The removed key is returned as a word over THIS dictionary's pool, and the pool outlives the slot — a tombstone does not reclaim the bytes — so the returned word stays readable until the next rehash compacts them.
METHOD iterator() RETURNS ReferenceIterator[BinaryWord]
One BinaryWord per live key, borrowed from this dictionary's pool.
Valid only while the dictionary is: the words point INTO its storage,
and a rehash moves that storage.
METHOD keys() RETURNS LinkedList[BinaryWord]
Every live key, as borrowed words. Same lifetime caveat as iterator().
MODIFY METHOD insertCopy(REFERENCE BinaryWord key, REFERENCE V value) RETURNS STATUS
METHOD clone() RETURNS Dictionary[BinaryWord, V, H]
Returns the Dictionary INTERFACE, not the concrete class — the class
is HIDDEN, so its name may appear only as a CREATE target, and this is
the shape ProHashDictionary.clone() already had. The copy goes in
through the interface's own insert, since insertSlice is this
class's and not the interface's.
METHOD equals(REFERENCE Dictionary[BinaryWord, V, H] other) RETURNS boolean
for Equatable Interface Dictionary is equal if all keys between the two dictionaries are equal Two dictionaries are equal when they hold the SAME ASSOCIATIONS: the same keys, each mapping to an EQUAL VALUE.
Not merely the same KEYSET. That would rank {a:1} equal to {a:2}, so
two "equal" dictionaries would answer lookup(a) differently and neither
could stand in for the other — which is the whole point of an equivalence.
Keys-AND-values is what C++ (map and unordered_map), Rust, Swift,
Julia, Python and Java all specify. Keyset equality is a perfectly good
relation, but it is equality of the DOMAIN rather than of the dictionary,
and it deserves its own name (hasSameKeys) rather than this one.
Equal sizes plus a one-way walk is sufficient: with the counts already
equal, other cannot carry a key this one lacks unless this one also
carries a key other lacks — and the walk would have found that.
CLASS StringKeyedDictionaryIterator
IMPLEMENTS ReferenceIterator
Walks the open-addressed slot table, yielding one BinaryWord per OCCUPIED
slot — a borrowed window onto that key's bytes in the shared pool.
NO ALLOCATION PER KEY, and that is the whole reason this class exists rather
than materialising keys. The window is a VALUE class held in one field,
.cur, re-assigned at each step; next() hands back a reference to that
field. So a walk of a million keys performs one assignment per key and no
heap traffic at all — which is what keeps the dictionary's measured
advantage intact while it satisfies the Dictionary contract.
Slot states match the dictionary's own encoding: 0 empty, 1 occupied, 2 tombstone. Only 1 is yielded.
kernel/src/StringKeyedDictionaryIterator.ev:28
Constructors
INIT(REFERENCE binary[] pool, REFERENCE int64[] offs, REFERENCE int64[] lens, REFERENCE int8[] states, int64 capacity)
Methods
METHOD hasNext() RETURNS boolean
MODIFY METHOD next() RETURNS | REFERENCE BinaryWord
METHOD peek() RETURNS | REFERENCE BinaryWord
MODIFY METHOD skip(uint64 n) RETURNS | REFERENCE BinaryWord
CLASS BinaryWord
IMPLEMENTS Cloneable, Hashable, Equatable
A BORROWED window onto a run of bytes: the array, where the run starts, and
how long it is. Value-copied and inline — copying a BinaryWord copies three
machine words, never the bytes.
WHY IT EXISTS. A byte-keyed dictionary stores every key end-to-end in one
pool and remembers each key as an (offset, length) pair, because that is what
makes it fast: one allocation for all the keys instead of one per key. But a
key so stored is not a value the language can hand back — binary is a
single byte, and an iterator yielding bytes cannot say where one key ends and
the next begins. BinaryWord is that missing value: the pair the dictionary
already holds, given a type.
It BORROWS. The bytes belong to whatever pool the window was opened on, and
a BinaryWord is valid exactly as long as that pool is. It is the byte-axis
sibling of StringView/ByteBufferView, and differs from them in being a
VALUE class: a view is an object reached through a handle, while a word is
copied inline, which is what lets an iterator yield one per step without
allocating.
kernel/src/BinaryWord.ev:30
Constructors
INIT(REFERENCE binary[] src, int64 off, int64 len)
Methods
METHOD length() RETURNS int64
Bytes in the window.
METHOD offset() RETURNS int64
Where the window opens in the underlying pool. A caller holding the pool
can use this with length() to reach the bytes directly — which is how
the slice fast paths avoid going through the window at all.
METHOD at(int64 i) RETURNS | binary
The byte at i within the window, or FAILURE when i is outside it.
METHOD clone() RETURNS BinaryWord
METHOD hash() RETURNS uint64
METHOD equals(REFERENCE BinaryWord other) RETURNS boolean
Equal when the windows hold the same bytes — not when they name the same place. Two words over different pools are equal if their bytes match.
METHOD isLessThan(REFERENCE BinaryWord other) RETURNS boolean
Lexicographic by bytes, shorter-is-less on a shared prefix — the order a byte-keyed tree needs to stay total.