hamt.js
JavaScript source for A hash table you don't need to rehash, 219 lines. Raw file
// A persistent hash array mapped trie (HAMT).
//
// An immutable map from string keys to values. Every change returns a new map and leaves the old one
// as it was, sharing every node the change didn't touch. It grows one small node at a time, so it
// never has to be rehashed.
// Hashing and indexing
const BITS = 5; // Bits of the hash used at each level
const WIDTH = 1 << BITS; // 32 slots in a node
const MASK = WIDTH - 1; // 0b11111
// FNV-1a, a simple 32-bit string hash
function hash(key) {
let h = 0x811c9dc5;
for (let i = 0; i < key.length; i++) {
h ^= key.charCodeAt(i);
h = Math.imul(h, 0x01000193);
}
return h >>> 0;
}
// The number of bits set in a 32-bit value
function popcount(x) {
x = x - ((x >>> 1) & 0x55555555);
x = (x & 0x33333333) + ((x >>> 2) & 0x33333333);
x = (x + (x >>> 4)) & 0x0f0f0f0f;
return Math.imul(x, 0x01010101) >>> 24;
}
// Which of the 32 slots a hash falls in at this level
function fragment(h, shift) {
return (h >>> shift) & MASK;
}
// Where a slot's entry is in the packed array: the number of used slots below it
function index(bitmap, bit) {
return popcount(bitmap & (bit - 1));
}
// The nodes
// A leaf holds one key, its value and the key's full hash
function Leaf(h, key, value) {
return { type: 'leaf', hash: h, key, value };
}
// A branch holds a bitmap of its used slots and an array of just those entries
function Branch(bitmap, entries) {
return { type: 'branch', bitmap, entries };
}
// Keys whose whole 32-bit hash is the same share a collision node
function Collision(h, leaves) {
return { type: 'collision', hash: h, leaves };
}
const EMPTY = Branch(0, []);
// Copies of an array with one entry replaced, inserted or removed. The original is never changed.
function arraySet(array, i, value) {
const copy = array.slice();
copy[i] = value;
return copy;
}
function arrayInsert(array, i, value) {
return [...array.slice(0, i), value, ...array.slice(i)];
}
function arrayRemove(array, i) {
return [...array.slice(0, i), ...array.slice(i + 1)];
}
// Looking up a key
function get(map, key) {
const h = hash(key);
let node = map;
let shift = 0;
while (true) {
if (node.type === 'leaf')
return node.key === key ? node.value : undefined;
if (node.type === 'collision') {
const leaf = node.leaves.find(l => l.key === key);
return leaf ? leaf.value : undefined;
}
// A branch: if the slot is empty the key isn't here, otherwise go down into it
const bit = 1 << fragment(h, shift);
if ((node.bitmap & bit) === 0)
return undefined;
node = node.entries[index(node.bitmap, bit)];
shift += BITS;
}
}
// Adding a key
// Returns a new map with key set to value
function set(map, key, value) {
return insert(map, Leaf(hash(key), key, value), 0);
}
// Inserts an entry (a leaf, or a collision node being pushed down) into a node at this level
function insert(node, entry, shift) {
if (node.type === 'branch') {
const bit = 1 << fragment(entry.hash, shift);
const i = index(node.bitmap, bit);
// An empty slot: set its bit and put the entry in the packed array
if ((node.bitmap & bit) === 0)
return Branch(node.bitmap | bit, arrayInsert(node.entries, i, entry));
// A used slot: insert into whatever is there, and copy this branch to point at the result
return Branch(node.bitmap, arraySet(node.entries, i, insert(node.entries[i], entry, shift + BITS)));
}
// A leaf or collision node with a different hash: push it down into a new branch, then insert
// the entry beside it. If their next five bits are the same too, this happens again a level down.
if (node.hash !== entry.hash)
return insert(insert(EMPTY, node, shift), entry, shift);
// The same key: replace the value
if (node.type === 'leaf' && node.key === entry.key)
return entry;
// A different key with exactly the same hash: more bits won't separate them, so keep both
if (node.type === 'leaf')
return Collision(node.hash, [node, entry]);
const i = node.leaves.findIndex(l => l.key === entry.key);
return Collision(node.hash, i >= 0 ? arraySet(node.leaves, i, entry) : [...node.leaves, entry]);
}
// Removing a key
// Returns a new map without key, or the same map if key wasn't in it
function remove(map, key) {
return removeFrom(map, key, hash(key), 0) || EMPTY;
}
// Returns the node without the key: the same node if the key isn't there, or null if nothing is left
function removeFrom(node, key, h, shift) {
if (node.type === 'leaf')
return node.key === key ? null : node;
if (node.type === 'collision') {
const i = node.leaves.findIndex(l => l.key === key);
if (i < 0)
return node;
// Down to one leaf: it doesn't need a collision node any more
if (node.leaves.length === 2)
return node.leaves[1 - i];
return Collision(node.hash, arrayRemove(node.leaves, i));
}
const bit = 1 << fragment(h, shift);
if ((node.bitmap & bit) === 0)
return node;
const i = index(node.bitmap, bit);
const child = node.entries[i];
const newChild = removeFrom(child, key, h, shift + BITS);
// Not found further down: nothing changes, and nothing is copied
if (newChild === child)
return node;
// The slot is now empty: clear its bit and take its entry out
if (newChild === null) {
const bitmap = node.bitmap & ~bit;
if (bitmap === 0)
return null;
const entries = arrayRemove(node.entries, i);
// A branch left with one leaf is replaced by the leaf, which moves up a level (but not at the root)
if (shift > 0 && entries.length === 1 && entries[0].type !== 'branch')
return entries[0];
return Branch(bitmap, entries);
}
// The child collapsed to a single leaf, and it's the only thing here: move it up again
if (shift > 0 && node.entries.length === 1 && newChild.type !== 'branch')
return newChild;
return Branch(node.bitmap, arraySet(node.entries, i, newChild));
}
// Going through every entry
// Every [key, value] pair, depth first, in hash order
function* entries(node) {
if (node.type === 'leaf') {
yield [node.key, node.value];
} else if (node.type === 'collision') {
for (const leaf of node.leaves)
yield [leaf.key, leaf.value];
} else {
for (const entry of node.entries)
yield* entries(entry);
}
}
export { EMPTY, get, set, remove, entries, hash, popcount };