Module collections/hash_map
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
High-performance hash map using SwissTable algorithm.
Keys must implement Eq and Hash. Provides O(1) average lookup, insert, and delete.
Type Parameters
| Name | Type | Notes |
|---|---|---|
K | Type | comptime |
V | Type | comptime |
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.
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)))remove_entry : (fn(self : Self, key : K) -> 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 : Vindex : (fn(inout(self) : Self, idx : K, where(K <: (Eq(K), Hash))) -> *(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
| Name | Type | Notes |
|---|---|---|
tracer | GcTracer |
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
| Name | Type | Notes |
|---|---|---|
acc | Self | |
item | MapEntry(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
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
| Name | Type | Notes |
|---|---|---|
K | Type | comptime |
V | Type | comptime |
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)
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
| Variant | Fields | Description |
|---|---|---|
Occupied | index: usize | The key is present, in this bucket. |
Vacant | index: 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. |
Error variants for HashMap operations.
Variants
| Variant | Fields | Description |
|---|---|---|
AllocError | error: AllocError | Memory allocation failed. |
CapacityOverflow | Capacity calculation overflowed. |
A single key/value entry in a map.
Type Parameters
| Name | Type | Notes |
|---|---|---|
K | Type | comptime |
V | Type | comptime |
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
| Name | Type | Notes |
|---|---|---|
K | Type | comptime |
V | Type | comptime |
Trait Implementations
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
| Name | Type | Notes |
|---|---|---|
K | Type | comptime |
V | Type | comptime |
Trait Implementations
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
| Name | Type | Notes |
|---|---|---|
K | Type | comptime |
V | Type | comptime |
Trait Implementations
impl(generic(K : Type, V : Type), where(K <: (Eq(K), Hash)), HashMapKeys(K, V), Iterator(...))
Item : Knext : (fn(inout(self) : Self) -> Option(K))Advance the iterator and return the next value, or None when exhausted.
Returns: Option(K)
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
| Name | Type | Notes |
|---|---|---|
K | Type | comptime |
V | Type | comptime |
Trait Implementations
impl(generic(K : Type, V : Type), where(K <: (Eq(K), Hash)), HashMapValues(K, V), Iterator(...))
Item : Vnext : (fn(inout(self) : Self) -> Option(V))Advance the iterator and return the next value, or None when exhausted.
Returns: Option(V)
Functions
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)