Persistent, immutable hash maps with structural sharing via Hash Array Mapped Tries.
A Hash Array Mapped Trie (HAMT) is an immutable hash map where every modification produces a new version of the map rather than mutating the original. Old and new versions share unmodified subtrees, so both time and space costs of updates are O(log N) rather than O(N).
Turmeric's HAMT uses xxHash64 for hashing, 5-bit hash chunks (32 slots per level), and reference counting for memory management. Keys and values are untyped pointers — the caller owns their lifetimes.
(import "stdlib/hamt.tur")
;; Create an empty map (ref_count = 1)
(def m (hamt/new))
;; Free when done (decrements ref count; frees if zero)
(hamt/free m)
;; Share ownership (increments ref count; returns same pointer)
(def m2 (hamt/retain m))
The HAMT itself does not free keys or values — the caller owns those lifetimes. Each operation that returns a new Hamt* has ref_count = 1; the old map is unchanged and must be freed independently.
All mutations return a new HAMT; the original is untouched.
;; Hash a string key
(def h (hamt/hash-str "hello"))
;; Insert or update
(def m1 (hamt/set m h "hello" "world"))
(def m2 (hamt/set m1 h "hello" "updated")) ; m1 still has "world"
;; Delete a key
(def m3 (hamt/del m2 h "hello"))
;; Lookup — returns nil if not found
(def val (hamt/get m2 h "hello")) ; => "updated"
;; Membership test
(hamt/has? m2 h "hello") ; => true
;; Count — O(1)
(hamt/count m2) ; => 1
Two built-in hash functions cover the common cases:
;; Hash a NUL-terminated C string
(def h (hamt/hash-str "my-key"))
;; Hash a pointer value directly (identity map)
(def ptr (some-object))
(def h (hamt/hash-ptr ptr))
For custom key types, compute a 64-bit hash yourself (e.g. via inline C calling tur_hamt_hash_xxh64) and pass it directly to hamt/set / hamt/get etc.
;; Merge b into a — b wins on collision
(def merged (hamt/merge a b))
;; Merge with a custom conflict resolver
;; fn receives (val-from-a val-from-b ctx) and returns the winning value
(def merged (hamt/merge-with a b resolve-fn nil))
;; Allocate an iterator (at least 64 bytes; use inline C for stack allocation)
(def iter (malloc 128))
(hamt/iter-init iter m)
;; Allocate output slots
(def hash-out (malloc 8))
(def key-out (malloc 8))
(def val-out (malloc 8))
(loop
(when (hamt/iter-next iter hash-out key-out val-out)
(println (ptr-deref key-out))
(recur)))
(hamt/iter-free iter)
Iteration order is unspecified (hash order, not insertion order).
;; Map: new HAMT with each value replaced by (fn val ctx)
(def doubled (hamt/map m double-fn nil))
;; Filter: new HAMT keeping only entries where (fn key val ctx) is true
(def filtered (hamt/filter m keep?-fn nil))
;; Reduce: fold all entries; returns final accumulator
(def sum (hamt/reduce m add-fn (int->ptr 0) nil))
;; Merge-with: merge a and b; call (fn val-a val-b ctx) for duplicate keys
(def merged (hamt/merge-with a b resolver nil))
The fn arguments are C function pointers. Use inline C to define them:
(defn my-map-fn [] :ptr
```c
void *double_val(void *val, void *ctx) {
return (void*)((intptr_t)val * 2);
}
return (void*)double_val;
```)
Transients allow batch construction with in-place mutation, then seal back into an immutable map. This avoids allocating intermediate copies during bulk inserts.
;; Fork a transient from an existing map (original unchanged)
(def t (hamt/transient m))
;; Mutate in place — no new HAMT created per operation
(hamt/transient-set! t h1 "a" "alpha")
(hamt/transient-set! t h2 "b" "beta")
(hamt/transient-del! t h1 "a")
;; Seal into an immutable map; t must not be used after this
(def result (hamt/persistent! t))
Rule: never read from or mutate a transient after calling hamt/persistent!.
;; Print a human-readable "{k->v, ...}" string (caller must free result)
(def s (hamt/show m))
(println s)
(free s)
;; Dump the trie structure in DOT format to stderr (for Graphviz)
(hamt/dump m)
| Function | Description |
|---|---|
hamt/new |
Create empty map |
hamt/free |
Decrement ref count / free |
hamt/retain |
Increment ref count |
hamt/set m h k v |
Insert/update; returns new map |
hamt/del m h k |
Delete key; returns new map |
hamt/get m h k |
Lookup; nil if absent |
hamt/has? m h k |
Membership test |
hamt/count m |
Number of entries, O(1) |
hamt/merge a b |
Merge; b wins on collision |
hamt/merge-with a b fn ctx |
Merge with conflict resolver |
hamt/map m fn ctx |
Transform values |
hamt/filter m fn ctx |
Retain matching entries |
hamt/reduce m fn init ctx |
Fold all entries |
hamt/iter-init iter m |
Start iteration |
hamt/iter-next iter h k v |
Advance; returns false when done |
hamt/iter-free iter |
Release iterator resources |
hamt/transient m |
Fork mutable transient |
hamt/transient-set! t h k v |
Mutate transient (insert/update) |
hamt/transient-del! t h k |
Mutate transient (delete) |
hamt/persistent! t |
Seal transient into immutable map |
hamt/hash-str s |
xxHash64 of a C string |
hamt/hash-ptr p |
Hash a pointer value |
hamt/show m |
Heap-allocated display string |
hamt/dump m |
DOT dump to stderr |
;; Use a transient for O(N) bulk construction
(defn list->hamt [pairs]
(let [t (hamt/transient (hamt/new))]
(for-each pairs
(fn [[k v]]
(hamt/transient-set! t (hamt/hash-str k) k v)))
(hamt/persistent! t)))
;; Each update is a new version; retain old ones cheaply
(def v0 (hamt/new))
(def v1 (hamt/set v0 (hamt/hash-str "x") "x" "1"))
(def v2 (hamt/set v1 (hamt/hash-str "y") "y" "2"))
;; v0, v1, and v2 all remain valid; share structure
;; Merge user config on top of defaults (user wins)
(def config (hamt/merge defaults user-config))