Module collections/hash_map

collections/hash_map
Stability: unstable — the shape is settled (D9's infallible `insert` + `try_insert`, D14's pointer `iter()`, the single-probe `entry` API), but four fields of the backing struct — `ctrl`, `data`, `capacity`, `size` — are still PUBLIC, and `plans/STD_API_STABILIZATION.md` §4 lists making them private as open work in this group: `HashSet` already took that change (`capacity()` and friends became accessors) and this map has not. §5 also has one undecided question here: `new()` stays deterministically keyed because the bootstrap fixpoint gate needs byte-identical emitted C, so a `with_random_keys()` convenience over `with_keys` may still be added. Freezing follows the field privatization; everything else about this module is additive already. — stable modules only change additively; this one may still change.

Hash map with a SwissTable-style control-byte layout.

Each bucket has a one-byte control value — CTRL_EMPTY, CTRL_DELETED, or the low 7 bits of the key's hash — held in an array beside the entries, so a probe rejects a bucket after one byte compare and only touches the entry when those 7 bits match. The probe sequence itself is LINEAR ((h1 + i) % capacity) and the control bytes are compared ONE AT A TIME: this is hashbrown's memory layout, not its SIMD group scan.

get/insert/remove are O(1) average; the table doubles at a 7/8 load factor, and a table over that threshold because of tombstones rather than live entries is rehashed in place instead of doubled.

Stability

unstable — the shape is settled (D9's infallible insert + try_insert, D14's pointer iter(), the single-probe entry API), but four fields of the backing struct — ctrl, data, capacity, size — are still PUBLIC, and plans/STD_API_STABILIZATION.md §4 lists making them private as open work in this group: HashSet already took that change (capacity() and friends became accessors) and this map has not. §5 also has one undecided question here: new() stays deterministically keyed because the bootstrap fixpoint gate needs byte-identical emitted C, so a with_random_keys() convenience over with_keys may still be added. Freezing follows the field privatization; everything else about this module is additive already.

Types

HashMap type-function
fn(K : Type, V : Type) -> Type

High-performance hash map using SwissTable algorithm. Keys must implement Eq and Hash. Provides O(1) average lookup, insert, and delete.

Type Parameters

NameTypeNotes
KTypecomptime
VTypecomptime

Trait Implementations

impl(generic(K : Type, V : Type), where(K <: (Eq(K), Hash)), HashMap(K, V), ...)
_alloc_with_capacity : (fn(capacity : usize) -> Result(Self, HashMapError))

Allocate memory for HashMap with given capacity Initializes all control bytes to EMPTY

Returns: Result(Self, HashMapError)

_hash : (fn(inout(self) : Self, inout(key) : K) -> u64)

The bucket hash of key under this map's keys.

Returns: u64

new : (fn() -> Self)

Empty map with room for DEFAULT_CAPACITY (16) buckets, hashed with the FIXED SipHash keys.

Fixed keys mean iteration order is reproducible run to run — the compiler's own fixpoint gate depends on it — and therefore that a map fed untrusted keys is open to collision flooding; with_keys is the answer there. Unlike Rust's HashMap::new, this ALLOCATES eagerly, and PANICS if that allocation fails.

Returns: Self

with_keys : (fn(k0 : u64, k1 : u64) -> Self)

Empty map whose hashes are keyed by k0, k1 (SipHash-1-3 keys).

Two maps with different keys place the same key in different buckets, so a map fed by untrusted input can be given per-process random keys (std/crypto/random) to defeat collision flooding. new() uses fixed keys so iteration order is reproducible.

Returns: Self

with_capacity : (fn(requested_capacity : usize) -> Self)

Empty map sized for requested_capacity buckets, rounded UP to a power of two (never below the 16-bucket default) because bucket selection masks the hash.

Note this is a BUCKET count, not the entry count Rust's with_capacity promises: the 7/8 load factor means the map still resizes after about capacity * 7 / 8 insertions. PANICS on allocation failure.

Returns: Self

_ctrl_ptr : (fn(self : Self) -> *(u8))

Get ctrl pointer (unwrapped for internal use)

Returns: *(u8)

_data_ptr : (fn(self : Self) -> *(MapEntry(K, V)))

Get data pointer (unwrapped for internal use)

Returns: *(MapEntry(K, V))

_find_bucket : (fn(self : Self, key : K, hash : u64) -> Option(usize))

Bucket holding key, or .None. Walks the LINEAR probe sequence and stops at the first CTRL_EMPTY, which is what makes remove able to turn a slot back into EMPTY only when its successor already is.

Returns: Option(usize)

_probe : (fn(self : Self, key : K, hash : u64) -> BucketProbe)

Walk the probe sequence for key ONCE, reporting either the bucket that holds it or the bucket it would be inserted into.

The vacant answer prefers the first TOMBSTONE on the chain over the terminating empty slot, so insert/remove churn reuses slots instead of lengthening every chain — the same choice _find_insert_bucket makes.

Returns: BucketProbe

_find_insert_bucket : (fn(self : Self, hash : u64) -> usize)

First reusable bucket (EMPTY or DELETED) on hash's LINEAR probe sequence. Panics on a full table — the caller resizes first.

Returns: usize

_needs_resize : (fn(self : Self) -> bool)

Check if HashMap needs resizing based on load factor

Returns: bool

_resize : (fn(self : Self, new_capacity : usize) -> Result(unit, HashMapError))

Resize and rehash the HashMap to a new capacity

Returns: Result(unit, HashMapError)

try_insert : (fn(self : Self, key : K, value : V) -> Result(Option(V), HashMapError))

Insert or overwrite, reporting allocation failure instead of panicking: .Ok(.Some(old)) when the key was present, .Ok(.None) when it is new.

The allocator-aware form (D9); insert is the one to reach for. An insertion that crosses the load factor rehashes the table first, which INVALIDATES every entry pointer previously handed out.

Returns: Result(Option(V), HashMapError)

_insert_at : (fn(self : Self, index : usize, hash : u64, key : K, value : V) -> V)

Write key/value into index, a slot _probe reported VACANT, and return the value written.

Resizes first when the new entry would cross the load factor — and in that case re-finds the slot, because the rehash moved everything. This is the half of try_insert that runs after the probe; entry reuses it so that a miss costs one probe rather than two.

PANICS on allocation failure, like insert (D9).

Returns: V

get : (fn(self : Self, key : K) -> Option(V))

Copy of the value stored under key, or .None — O(1) average.

The value is dup'd for a refcounted V; get_entry_ptr borrows the entry instead, and self(key) (the Index impl) panics on a missing key rather than answering .None.

Returns: Option(V)

get_entry_ptr : (fn(self : Self, key : K) -> Option(*(MapEntry(K, V))))

Pointer to the LIVE entry for key, or .None when the key is absent.

The pointer aliases the map's own bucket array — the same storage iter() hands out (D14) — so writing p.*.value updates the map in place, and any operation that REHASHES the table (an insert that grows it, remove, clear) invalidates it.

Returns: Option(*(MapEntry(K, V)))

insert : (fn(self : Self, key : K, value : V) -> Option(V))

Insert a key/value pair, returning the PREVIOUS value for that key — Rust's insert.

PANICS on allocation failure (D9): HashMap.new already does, and so does ArrayList.push. Before D9 this returned a Result that essentially every caller discarded through a throwaway _x := binding. Use try_insert when the caller genuinely handles OOM.

The .Some(old) result is a trap worth knowing: map.insert(k, v) .unwrap() used to unwrap a Result and now unwraps the OPTION, so it panics on a FRESH key.

Returns: Option(V)

get_key_value : (fn(self : Self, key : K) -> Option(MapEntry(K, V)))

The stored KEY alongside its value — Rust's get_key_value.

Distinct from get whenever the key type has equal-but-distinguishable values (a case-insensitive string key, say): this returns the key the map actually holds, not the one used to look it up.

Returns: Option(MapEntry(K, V))

remove_entry : (fn(self : Self, key : K) -> Option(MapEntry(K, V)))

Remove key and return the entry that was stored — Rust's remove_entry.

Returns: Option(MapEntry(K, V))

contains_key : (fn(self : Self, key : K) -> bool)

Whether key is in the map — O(1) average, and it does not touch the value, so nothing is dup'd for a refcounted V.

Returns: bool

remove : (fn(self : Self, key : K) -> Option(V))

Remove this key from the map, returning the value it held.

Re-probes rather than using the pinned slot: removal has to decide between an empty slot and a tombstone from the chain that follows it, and that logic lives in remove.

Returns: Option(V)

len : (fn(self : Self) -> usize)

Number of live entries — O(1). Not the bucket count; that is capacity.

Returns: usize

is_empty : (fn(self : Self) -> bool)

True when the map holds no entries. A map with only tombstones left is empty by this test.

Returns: bool

retain : (fn(self : Self, keep : Impl(Fn(k : K, v : V) -> bool)) -> unit)

Drop every entry for which keep(key, value) is false — Rust's retain.

Scans the bucket array directly rather than probing per key. Removal only rewrites the removed bucket's control byte and never moves another entry, so the scan stays valid across the removals it performs.

Returns: unit

extend : ( fn( generic(I : Type), self : Self, iterable : I, where(I <: IntoIterator(Item := MapEntry(K, V))) ) -> unit )

Insert every entry of iterable, overwriting on duplicate keys — Rust's Extend.

Takes anything that implements IntoIterator over map entries, so a map, an OrderedMap, a BTreeMap or an ArrayList(MapEntry(K, V)) all work:

defaults.extend(overrides);

An ITERATOR is not accepted, only something that yields one. The prelude's blanket into_iter on Iterator is a bare METHOD, not an IntoIterator impl, so an iterator does not satisfy this bound however much its doc comment says every iterator is its own IntoIterator — see issues/blanket-into-iter-is-not-an-intoiterator-impl.md. Feed a chain through collect first.

Returns: unit

clear : (fn(self : Self) -> unit)

Drop every entry and reset every control byte to EMPTY, KEEPING the bucket array — Rust's clear. O(capacity), and it clears the tombstone count too.

Returns: unit

impl(generic(K : Type, V : Type), where(K <: (Eq(K), Hash)), HashMap(K, V), ...)
entry : (fn(self : Self, key : K) -> HashMapEntry(K, V))

The slot for key, resolved in one probe — Rust's entry.

counts.entry(word).and_modify((n) => (n + i32(1))).or_insert(i32(1));

See HashMapEntry for the invalidation rule.

Returns: HashMapEntry(K, V)

get_or_insert : (fn(self : Self, key : K, default : V) -> V)

The value for key, inserting default first when absent.

Returns: V

get_or_insert_with : (fn(self : Self, key : K, make : Impl(Fn() -> V)) -> V)

The value for key, inserting make()'s result first when absent — use over get_or_insert when constructing the default is costly.

Returns: V

update_with : (fn(self : Self, key : K, f : Impl(Fn(v : V) -> V)) -> bool)

Update the value for key through f when present; no-op when absent. Returns true when an update happened.

Returns: bool

impl(generic(K : Type, V : Type), where(K <: (Eq(K), Hash)), HashMap(K, V), Dispose(...))
dispose : (fn(self : Self) -> unit)

Drops every live entry and frees the control and bucket arrays. Runs when the last reference to the map goes away.

Returns: unit

impl(generic(K : Type, V : Type), where(K <: (Eq(K), Hash)), HashMap(K, V), IntoIterator(...))
Item : MapEntry(K, V)
IntoIter : HashMapIter(K, V)
into_iter : (fn(self : Self) -> HashMapIter(K, V))

Returns: HashMapIter(K, V)

impl(generic(K : Type, V : Type), where(K <: (Eq(K), Hash)), HashMap(K, V), ...)
iter : (fn(self : Self) -> HashMapIterPtr(K, V))

Borrowing walk over the occupied buckets: yields a *(MapEntry(K, V)) into the map's own bucket array (D14), so nothing is dup'd and p.*.value can be written in place. into_iter() — what for(map, ...) uses — yields entries by value instead.

Order follows the bucket array, so it is arbitrary but reproducible for a map built with new()'s fixed keys. Do not insert or remove during the walk: a rehash invalidates every pointer already yielded.

Returns: HashMapIterPtr(K, V)

impl(generic(K : Type, V : Type), where(K <: (Eq(K), Hash)), HashMap(K, V), ...)
keys : (fn(self : Self) -> HashMapKeys(K, V))

Walk the keys, in bucket order — Rust's keys. Yields them by value; iter() is the way to see keys without dup'ing them.

Returns: HashMapKeys(K, V)

impl(generic(K : Type, V : Type), where(K <: (Eq(K), Hash)), HashMap(K, V), ...)
values : (fn(self : Self) -> HashMapValues(K, V))

Walk the values, in bucket order — Rust's values. Yields them by value; iter() hands out entry pointers instead, which is the cheaper way to read a refcounted V.

Returns: HashMapValues(K, V)

impl(generic(K : Type, V : Type), HashMap(K, V), Index(K)(...))
Output : V
index : (fn(inout(self) : Self, idx : K, where(K <: (Eq(K), Hash))) -> *(Self.Output))

Parameters

NameTypeNotes
idxK

Returns: *(Self.Output)

impl(generic(K : Type, V : Type), where(K <: (Clone, Eq(K), Hash), V <: Clone), HashMap(K, V), Clone(...))
clone : (fn(inout(self) : Self) -> Self)

Create an independent clone of self.

Returns: Self

impl(generic(K : Type, V : Type), where(K <: (Eq(K), Hash)), HashMap(K, V), Trace(...))
trace : (fn(self : Self, tracer : GcTracer) -> unit)

Cycle-GC tracing. Buckets live in a malloc'd open-addressing buffer the compiler's auto-derived field walk cannot reach, so trace each FULL slot (its control byte is neither EMPTY nor DELETED). tracer.visit takes the MapEntry(K, V) slot POINTER; the per-value traversal descends inline through the value struct, tracing BOTH the key and the value, WITHOUT touching any reference count (a by-value managed handle would be dup'd then dropped, freeing a live element mid-collection). Mirrors the ArrayList Trace impl + the SwissTable slot scan.

Parameters

NameTypeNotes
tracerGcTracer

Returns: unit

impl(generic(K : Type, V : Type), where(K <: (Eq(K), Hash)), HashMap(K, V), FromIterator(...))
Elem : MapEntry(K, V)
from_iter_new : (fn() -> Self)

The empty collection collect starts from.

Returns: Self

from_iter_add : (fn(acc : Self, item : MapEntry(K, V)) -> Self)

Add one element to a partially-built collection and return it.

Parameters

NameTypeNotes
accSelf
itemMapEntry(K, V)

Returns: Self

impl(generic(K : Type, V : Type), where(K <: (Eq(K), Hash)), HashMap(K, V), Default(...))
default : (fn() -> Self)

The default value of the type.

Returns: Self

HashMapEntry type-function
fn(K : Type, V : Type) -> Type

One key's slot in a HashMap, resolved by a SINGLE probe — Rust's Entry.

Yo has no borrow object, so this is a plain struct that pins the map, the key, its hash, and the bucket the probe landed on. That last field is what makes the entry worth having over get + insert: the miss path already knows where to write.

It is invalidated by anything that rehashes the table — an insert that grows it, remove, clear — exactly like the pointer get_entry_ptr hands out. Use an entry and drop it; do not store one.

Type Parameters

NameTypeNotes
KTypecomptime
VTypecomptime
impl(generic(K : Type, V : Type), where(K <: (Eq(K), Hash)), HashMapEntry(K, V), ...)
key : (fn(self : Self) -> K)

The key this entry was looked up with.

Returns: K

is_occupied : (fn(self : Self) -> bool)

True when the key is already in the map.

Returns: bool

is_vacant : (fn(self : Self) -> bool)

True when the key is absent.

Returns: bool

value : (fn(self : Self) -> Option(V))

The stored value, or .None when the entry is vacant.

Returns: Option(V)

or_insert : (fn(self : Self, default : V) -> V)

The value for this key, inserting default first when vacant.

Returns: V

or_insert_with : (fn(self : Self, make : Impl(Fn() -> V)) -> V)

The value for this key, inserting make()'s result first when vacant — use over or_insert when constructing the default is costly.

Returns: V

and_modify : (fn(self : Self, f : Impl(Fn(v : V) -> V)) -> Self)

Replace the stored value with f(value) when occupied; a no-op when vacant. Returns the entry, so it chains ahead of or_insert:

counts.entry(word).and_modify((n) => (n + i32(1))).or_insert(i32(1));

Returns: Self

remove : (fn(self : Self) -> Option(V))

Remove this key from the map, returning the value it held.

Re-probes rather than using the pinned slot: removal has to decide between an empty slot and a tombstone from the chain that follows it, and that logic lives in remove.

Returns: Option(V)

BucketProbe

The outcome of ONE walk of a key's probe sequence — "where does this key live, or where would it go?".

_find_bucket answers only the first half and _find_insert_bucket only the second, so every get-then-insert accessor walked the sequence TWICE. _probe answers both at once (hashbrown's find_or_find_insert_slot).

Variants

VariantFieldsDescription
Occupiedindex: usize

The key is present, in this bucket.

Vacantindex: usize

The key is absent; this is the bucket it would be written to — PROVIDED the table is not resized first, since a rehash moves everything.

HashMapError

Error variants for HashMap operations.

Variants

VariantFieldsDescription
AllocErrorerror: AllocError

Memory allocation failed.

CapacityOverflow

Capacity calculation overflowed.

MapEntry type-function
fn(K : Type, V : Type) -> Type

A single key/value entry in a map.

Type Parameters

NameTypeNotes
KTypecomptime
VTypecomptime
HashMapIter type-function
fn(K : Type, V : Type) -> Type

Value iterator over a HashMap — yields each MapEntry(K, V) BY VALUE (both key and value dup'd for refcounted types). This is what into_iter(), and therefore for(map, ...), returns; iter() yields pointers.

It walks the control-byte array from bucket 0, so the order is arbitrary but reproducible for a map keyed by new()'s fixed keys.

Type Parameters

NameTypeNotes
KTypecomptime
VTypecomptime

Trait Implementations

impl(generic(K : Type, V : Type), where(K <: (Eq(K), Hash)), HashMapIter(K, V), Iterator(...))
Item : MapEntry(K, V)
next : (fn(inout(self) : Self) -> Option(MapEntry(K, V)))

Advance the iterator and return the next value, or None when exhausted.

Returns: Option(MapEntry(K, V))

HashMapIterPtr type-function
fn(K : Type, V : Type) -> Type

Pointer iterator over a HashMap — yields *(MapEntry(K, V)) into the map's own bucket array. This is what iter() returns (D14).

A yielded pointer stays valid only while the table is not rehashed: an insert that grows the map, a remove or a clear during the walk leaves it dangling.

Type Parameters

NameTypeNotes
KTypecomptime
VTypecomptime

Trait Implementations

impl(generic(K : Type, V : Type), where(K <: (Eq(K), Hash)), HashMapIterPtr(K, V), Iterator(...))
Item : *(MapEntry(K, V))
next : (fn(inout(self) : Self) -> Option(*(MapEntry(K, V))))

Advance the iterator and return the next value, or None when exhausted.

Returns: Option(*(MapEntry(K, V)))

HashMapKeys type-function
fn(K : Type, V : Type) -> Type

Iterator over a map's keys, in bucket order — what keys() returns. The keys are yielded by value, so a refcounted K is dup'd.

Type Parameters

NameTypeNotes
KTypecomptime
VTypecomptime

Trait Implementations

impl(generic(K : Type, V : Type), where(K <: (Eq(K), Hash)), HashMapKeys(K, V), Iterator(...))
Item : K
next : (fn(inout(self) : Self) -> Option(K))

Advance the iterator and return the next value, or None when exhausted.

Returns: Option(K)

HashMapValues type-function
fn(K : Type, V : Type) -> Type

Iterator over a map's values, in bucket order — what values() returns. The values are yielded by value, so a refcounted V is dup'd.

Type Parameters

NameTypeNotes
KTypecomptime
VTypecomptime

Trait Implementations

impl(generic(K : Type, V : Type), where(K <: (Eq(K), Hash)), HashMapValues(K, V), Iterator(...))
Item : V
next : (fn(inout(self) : Self) -> Option(V))

Advance the iterator and return the next value, or None when exhausted.

Returns: Option(V)

Functions

hash_map function
fn(...(quote(entries))) -> unquote(Expr)

hash_map macro — construct a HashMap(K, V) literal.

K and V are inferred from the first entry's key and value via typeof, so at least one entry is required.

Example

m := hash_map(`a` => i32(1), `b` => i32(2));

Returns: unquote(Expr)