Packages
gleam_stdlib
0.44.0
1.0.3
1.0.2
1.0.1
1.0.0
0.71.0
0.70.0
0.69.0
0.68.1
0.68.0
0.67.1
0.67.0
0.65.0
0.64.0
0.63.2
0.63.1
0.63.0
0.62.1
0.62.0
0.61.0
0.60.0
0.59.0
0.58.0
0.57.0
0.56.0
0.55.0
0.54.0
0.53.0
0.52.0
0.51.0
0.50.0
0.49.0
0.48.0
0.47.0
0.46.0
0.45.0
0.44.0
0.43.0
0.42.0
0.41.0
0.40.0
0.39.0
0.38.0
0.37.0
0.36.0
0.35.1
0.35.0
0.34.0
0.33.1
0.33.0
0.32.1
0.32.0
0.31.0
0.30.2
0.30.1
0.30.0
0.29.2
0.29.1
0.29.0
0.28.2
0.28.1
0.28.0
0.27.0
0.26.1
0.26.0
0.25.0
0.24.0
0.23.0
0.22.3
0.22.2
0.22.1
0.22.0
0.21.0
0.20.0
0.19.3
0.19.2
0.19.1
0.19.0
0.18.1
0.18.0
0.18.0-rc1
0.17.1
0.17.0
0.16.0
0.15.0
0.14.0
0.13.0
0.12.0
0.11.0
0.10.1
0.10.0
0.9.0
0.8.0
0.7.0
0.6.0
0.5.0
0.4.0
0.4.0-rc1
0.3.1
0.3.0
0.2.0
retired
A standard library for the Gleam programming language
Current section
Files
Jump to
Current section
Files
src/dict.mjs
/**
* This file uses jsdoc to annotate types.
* These types can be checked using the typescript compiler with "checkjs" option.
*/
import { isEqual } from "./gleam.mjs";
const referenceMap = new WeakMap();
const tempDataView = new DataView(new ArrayBuffer(8));
let referenceUID = 0;
/**
* hash the object by reference using a weak map and incrementing uid
* @param {any} o
* @returns {number}
*/
function hashByReference(o) {
const known = referenceMap.get(o);
if (known !== undefined) {
return known;
}
const hash = referenceUID++;
if (referenceUID === 0x7fffffff) {
referenceUID = 0;
}
referenceMap.set(o, hash);
return hash;
}
/**
* merge two hashes in an order sensitive way
* @param {number} a
* @param {number} b
* @returns {number}
*/
function hashMerge(a, b) {
return (a ^ (b + 0x9e3779b9 + (a << 6) + (a >> 2))) | 0;
}
/**
* standard string hash popularised by java
* @param {string} s
* @returns {number}
*/
function hashString(s) {
let hash = 0;
const len = s.length;
for (let i = 0; i < len; i++) {
hash = (Math.imul(31, hash) + s.charCodeAt(i)) | 0;
}
return hash;
}
/**
* hash a number by converting to two integers and do some jumbling
* @param {number} n
* @returns {number}
*/
function hashNumber(n) {
tempDataView.setFloat64(0, n);
const i = tempDataView.getInt32(0);
const j = tempDataView.getInt32(4);
return Math.imul(0x45d9f3b, (i >> 16) ^ i) ^ j;
}
/**
* hash a BigInt by converting it to a string and hashing that
* @param {BigInt} n
* @returns {number}
*/
function hashBigInt(n) {
return hashString(n.toString());
}
/**
* hash any js object
* @param {any} o
* @returns {number}
*/
function hashObject(o) {
const proto = Object.getPrototypeOf(o);
if (proto !== null && typeof proto.hashCode === "function") {
try {
const code = o.hashCode(o);
if (typeof code === "number") {
return code;
}
} catch {}
}
if (o instanceof Promise || o instanceof WeakSet || o instanceof WeakMap) {
return hashByReference(o);
}
if (o instanceof Date) {
return hashNumber(o.getTime());
}
let h = 0;
if (o instanceof ArrayBuffer) {
o = new Uint8Array(o);
}
if (Array.isArray(o) || o instanceof Uint8Array) {
for (let i = 0; i < o.length; i++) {
h = (Math.imul(31, h) + getHash(o[i])) | 0;
}
} else if (o instanceof Set) {
o.forEach((v) => {
h = (h + getHash(v)) | 0;
});
} else if (o instanceof Map) {
o.forEach((v, k) => {
h = (h + hashMerge(getHash(v), getHash(k))) | 0;
});
} else {
const keys = Object.keys(o);
for (let i = 0; i < keys.length; i++) {
const k = keys[i];
const v = o[k];
h = (h + hashMerge(getHash(v), hashString(k))) | 0;
}
}
return h;
}
/**
* hash any js value
* @param {any} u
* @returns {number}
*/
export function getHash(u) {
if (u === null) return 0x42108422;
if (u === undefined) return 0x42108423;
if (u === true) return 0x42108421;
if (u === false) return 0x42108420;
switch (typeof u) {
case "number":
return hashNumber(u);
case "string":
return hashString(u);
case "bigint":
return hashBigInt(u);
case "object":
return hashObject(u);
case "symbol":
return hashByReference(u);
case "function":
return hashByReference(u);
default:
return 0; // should be unreachable
}
}
/**
* @template K,V
* @typedef {ArrayNode<K,V> | IndexNode<K,V> | CollisionNode<K,V>} Node
*/
/**
* @template K,V
* @typedef {{ type: typeof ENTRY, k: K, v: V }} Entry
*/
/**
* @template K,V
* @typedef {{ type: typeof ARRAY_NODE, size: number, array: (undefined | Entry<K,V> | Node<K,V>)[] }} ArrayNode
*/
/**
* @template K,V
* @typedef {{ type: typeof INDEX_NODE, bitmap: number, array: (Entry<K,V> | Node<K,V>)[] }} IndexNode
*/
/**
* @template K,V
* @typedef {{ type: typeof COLLISION_NODE, hash: number, array: Entry<K, V>[] }} CollisionNode
*/
/**
* @typedef {{ val: boolean }} Flag
*/
const SHIFT = 5; // number of bits you need to shift by to get the next bucket
const BUCKET_SIZE = Math.pow(2, SHIFT);
const MASK = BUCKET_SIZE - 1; // used to zero out all bits not in the bucket
const MAX_INDEX_NODE = BUCKET_SIZE / 2; // when does index node grow into array node
const MIN_ARRAY_NODE = BUCKET_SIZE / 4; // when does array node shrink to index node
const ENTRY = 0;
const ARRAY_NODE = 1;
const INDEX_NODE = 2;
const COLLISION_NODE = 3;
/** @type {IndexNode<any,any>} */
const EMPTY = {
type: INDEX_NODE,
bitmap: 0,
array: [],
};
/**
* Mask the hash to get only the bucket corresponding to shift
* @param {number} hash
* @param {number} shift
* @returns {number}
*/
function mask(hash, shift) {
return (hash >>> shift) & MASK;
}
/**
* Set only the Nth bit where N is the masked hash
* @param {number} hash
* @param {number} shift
* @returns {number}
*/
function bitpos(hash, shift) {
return 1 << mask(hash, shift);
}
/**
* Count the number of 1 bits in a number
* @param {number} x
* @returns {number}
*/
function bitcount(x) {
x -= (x >> 1) & 0x55555555;
x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
x = (x + (x >> 4)) & 0x0f0f0f0f;
x += x >> 8;
x += x >> 16;
return x & 0x7f;
}
/**
* Calculate the array index of an item in a bitmap index node
* @param {number} bitmap
* @param {number} bit
* @returns {number}
*/
function index(bitmap, bit) {
return bitcount(bitmap & (bit - 1));
}
/**
* Efficiently copy an array and set one value at an index
* @template T
* @param {T[]} arr
* @param {number} at
* @param {T} val
* @returns {T[]}
*/
function cloneAndSet(arr, at, val) {
const len = arr.length;
const out = new Array(len);
for (let i = 0; i < len; ++i) {
out[i] = arr[i];
}
out[at] = val;
return out;
}
/**
* Efficiently copy an array and insert one value at an index
* @template T
* @param {T[]} arr
* @param {number} at
* @param {T} val
* @returns {T[]}
*/
function spliceIn(arr, at, val) {
const len = arr.length;
const out = new Array(len + 1);
let i = 0;
let g = 0;
while (i < at) {
out[g++] = arr[i++];
}
out[g++] = val;
while (i < len) {
out[g++] = arr[i++];
}
return out;
}
/**
* Efficiently copy an array and remove one value at an index
* @template T
* @param {T[]} arr
* @param {number} at
* @returns {T[]}
*/
function spliceOut(arr, at) {
const len = arr.length;
const out = new Array(len - 1);
let i = 0;
let g = 0;
while (i < at) {
out[g++] = arr[i++];
}
++i;
while (i < len) {
out[g++] = arr[i++];
}
return out;
}
/**
* Create a new node containing two entries
* @template K,V
* @param {number} shift
* @param {K} key1
* @param {V} val1
* @param {number} key2hash
* @param {K} key2
* @param {V} val2
* @returns {Node<K,V>}
*/
function createNode(shift, key1, val1, key2hash, key2, val2) {
const key1hash = getHash(key1);
if (key1hash === key2hash) {
return {
type: COLLISION_NODE,
hash: key1hash,
array: [
{ type: ENTRY, k: key1, v: val1 },
{ type: ENTRY, k: key2, v: val2 },
],
};
}
const addedLeaf = { val: false };
return assoc(
assocIndex(EMPTY, shift, key1hash, key1, val1, addedLeaf),
shift,
key2hash,
key2,
val2,
addedLeaf
);
}
/**
* @template T,K,V
* @callback AssocFunction
* @param {T} root
* @param {number} shift
* @param {number} hash
* @param {K} key
* @param {V} val
* @param {Flag} addedLeaf
* @returns {Node<K,V>}
*/
/**
* Associate a node with a new entry, creating a new node
* @template T,K,V
* @type {AssocFunction<Node<K,V>,K,V>}
*/
function assoc(root, shift, hash, key, val, addedLeaf) {
switch (root.type) {
case ARRAY_NODE:
return assocArray(root, shift, hash, key, val, addedLeaf);
case INDEX_NODE:
return assocIndex(root, shift, hash, key, val, addedLeaf);
case COLLISION_NODE:
return assocCollision(root, shift, hash, key, val, addedLeaf);
}
}
/**
* @template T,K,V
* @type {AssocFunction<ArrayNode<K,V>,K,V>}
*/
function assocArray(root, shift, hash, key, val, addedLeaf) {
const idx = mask(hash, shift);
const node = root.array[idx];
// if the corresponding index is empty set the index to a newly created node
if (node === undefined) {
addedLeaf.val = true;
return {
type: ARRAY_NODE,
size: root.size + 1,
array: cloneAndSet(root.array, idx, { type: ENTRY, k: key, v: val }),
};
}
if (node.type === ENTRY) {
// if keys are equal replace the entry
if (isEqual(key, node.k)) {
if (val === node.v) {
return root;
}
return {
type: ARRAY_NODE,
size: root.size,
array: cloneAndSet(root.array, idx, {
type: ENTRY,
k: key,
v: val,
}),
};
}
// otherwise upgrade the entry to a node and insert
addedLeaf.val = true;
return {
type: ARRAY_NODE,
size: root.size,
array: cloneAndSet(
root.array,
idx,
createNode(shift + SHIFT, node.k, node.v, hash, key, val)
),
};
}
// otherwise call assoc on the child node
const n = assoc(node, shift + SHIFT, hash, key, val, addedLeaf);
// if the child node hasn't changed just return the old root
if (n === node) {
return root;
}
// otherwise set the index to the new node
return {
type: ARRAY_NODE,
size: root.size,
array: cloneAndSet(root.array, idx, n),
};
}
/**
* @template T,K,V
* @type {AssocFunction<IndexNode<K,V>,K,V>}
*/
function assocIndex(root, shift, hash, key, val, addedLeaf) {
const bit = bitpos(hash, shift);
const idx = index(root.bitmap, bit);
// if there is already a item at this hash index..
if ((root.bitmap & bit) !== 0) {
// if there is a node at the index (not an entry), call assoc on the child node
const node = root.array[idx];
if (node.type !== ENTRY) {
const n = assoc(node, shift + SHIFT, hash, key, val, addedLeaf);
if (n === node) {
return root;
}
return {
type: INDEX_NODE,
bitmap: root.bitmap,
array: cloneAndSet(root.array, idx, n),
};
}
// otherwise there is an entry at the index
// if the keys are equal replace the entry with the updated value
const nodeKey = node.k;
if (isEqual(key, nodeKey)) {
if (val === node.v) {
return root;
}
return {
type: INDEX_NODE,
bitmap: root.bitmap,
array: cloneAndSet(root.array, idx, {
type: ENTRY,
k: key,
v: val,
}),
};
}
// if the keys are not equal, replace the entry with a new child node
addedLeaf.val = true;
return {
type: INDEX_NODE,
bitmap: root.bitmap,
array: cloneAndSet(
root.array,
idx,
createNode(shift + SHIFT, nodeKey, node.v, hash, key, val)
),
};
} else {
// else there is currently no item at the hash index
const n = root.array.length;
// if the number of nodes is at the maximum, expand this node into an array node
if (n >= MAX_INDEX_NODE) {
// create a 32 length array for the new array node (one for each bit in the hash)
const nodes = new Array(32);
// create and insert a node for the new entry
const jdx = mask(hash, shift);
nodes[jdx] = assocIndex(EMPTY, shift + SHIFT, hash, key, val, addedLeaf);
let j = 0;
let bitmap = root.bitmap;
// place each item in the index node into the correct spot in the array node
// loop through all 32 bits / array positions
for (let i = 0; i < 32; i++) {
if ((bitmap & 1) !== 0) {
const node = root.array[j++];
nodes[i] = node;
}
// shift the bitmap to process the next bit
bitmap = bitmap >>> 1;
}
return {
type: ARRAY_NODE,
size: n + 1,
array: nodes,
};
} else {
// else there is still space in this index node
// simply insert a new entry at the hash index
const newArray = spliceIn(root.array, idx, {
type: ENTRY,
k: key,
v: val,
});
addedLeaf.val = true;
return {
type: INDEX_NODE,
bitmap: root.bitmap | bit,
array: newArray,
};
}
}
}
/**
* @template T,K,V
* @type {AssocFunction<CollisionNode<K,V>,K,V>}
*/
function assocCollision(root, shift, hash, key, val, addedLeaf) {
// if there is a hash collision
if (hash === root.hash) {
const idx = collisionIndexOf(root, key);
// if this key already exists replace the entry with the new value
if (idx !== -1) {
const entry = root.array[idx];
if (entry.v === val) {
return root;
}
return {
type: COLLISION_NODE,
hash: hash,
array: cloneAndSet(root.array, idx, { type: ENTRY, k: key, v: val }),
};
}
// otherwise insert the entry at the end of the array
const size = root.array.length;
addedLeaf.val = true;
return {
type: COLLISION_NODE,
hash: hash,
array: cloneAndSet(root.array, size, { type: ENTRY, k: key, v: val }),
};
}
// if there is no hash collision, upgrade to an index node
return assoc(
{
type: INDEX_NODE,
bitmap: bitpos(root.hash, shift),
array: [root],
},
shift,
hash,
key,
val,
addedLeaf
);
}
/**
* Find the index of a key in the collision node's array
* @template K,V
* @param {CollisionNode<K,V>} root
* @param {K} key
* @returns {number}
*/
function collisionIndexOf(root, key) {
const size = root.array.length;
for (let i = 0; i < size; i++) {
if (isEqual(key, root.array[i].k)) {
return i;
}
}
return -1;
}
/**
* @template T,K,V
* @callback FindFunction
* @param {T} root
* @param {number} shift
* @param {number} hash
* @param {K} key
* @returns {undefined | Entry<K,V>}
*/
/**
* Return the found entry or undefined if not present in the root
* @template K,V
* @type {FindFunction<Node<K,V>,K,V>}
*/
function find(root, shift, hash, key) {
switch (root.type) {
case ARRAY_NODE:
return findArray(root, shift, hash, key);
case INDEX_NODE:
return findIndex(root, shift, hash, key);
case COLLISION_NODE:
return findCollision(root, key);
}
}
/**
* @template K,V
* @type {FindFunction<ArrayNode<K,V>,K,V>}
*/
function findArray(root, shift, hash, key) {
const idx = mask(hash, shift);
const node = root.array[idx];
if (node === undefined) {
return undefined;
}
if (node.type !== ENTRY) {
return find(node, shift + SHIFT, hash, key);
}
if (isEqual(key, node.k)) {
return node;
}
return undefined;
}
/**
* @template K,V
* @type {FindFunction<IndexNode<K,V>,K,V>}
*/
function findIndex(root, shift, hash, key) {
const bit = bitpos(hash, shift);
if ((root.bitmap & bit) === 0) {
return undefined;
}
const idx = index(root.bitmap, bit);
const node = root.array[idx];
if (node.type !== ENTRY) {
return find(node, shift + SHIFT, hash, key);
}
if (isEqual(key, node.k)) {
return node;
}
return undefined;
}
/**
* @template K,V
* @param {CollisionNode<K,V>} root
* @param {K} key
* @returns {undefined | Entry<K,V>}
*/
function findCollision(root, key) {
const idx = collisionIndexOf(root, key);
if (idx < 0) {
return undefined;
}
return root.array[idx];
}
/**
* @template T,K,V
* @callback WithoutFunction
* @param {T} root
* @param {number} shift
* @param {number} hash
* @param {K} key
* @returns {undefined | Node<K,V>}
*/
/**
* Remove an entry from the root, returning the updated root.
* Returns undefined if the node should be removed from the parent.
* @template K,V
* @type {WithoutFunction<Node<K,V>,K,V>}
* */
function without(root, shift, hash, key) {
switch (root.type) {
case ARRAY_NODE:
return withoutArray(root, shift, hash, key);
case INDEX_NODE:
return withoutIndex(root, shift, hash, key);
case COLLISION_NODE:
return withoutCollision(root, key);
}
}
/**
* @template K,V
* @type {WithoutFunction<ArrayNode<K,V>,K,V>}
*/
function withoutArray(root, shift, hash, key) {
const idx = mask(hash, shift);
const node = root.array[idx];
if (node === undefined) {
return root; // already empty
}
let n = undefined;
// if node is an entry and the keys are not equal there is nothing to remove
// if node is not an entry do a recursive call
if (node.type === ENTRY) {
if (!isEqual(node.k, key)) {
return root; // no changes
}
} else {
n = without(node, shift + SHIFT, hash, key);
if (n === node) {
return root; // no changes
}
}
// if the recursive call returned undefined the node should be removed
if (n === undefined) {
// if the number of child nodes is at the minimum, pack into an index node
if (root.size <= MIN_ARRAY_NODE) {
const arr = root.array;
const out = new Array(root.size - 1);
let i = 0;
let j = 0;
let bitmap = 0;
while (i < idx) {
const nv = arr[i];
if (nv !== undefined) {
out[j] = nv;
bitmap |= 1 << i;
++j;
}
++i;
}
++i; // skip copying the removed node
while (i < arr.length) {
const nv = arr[i];
if (nv !== undefined) {
out[j] = nv;
bitmap |= 1 << i;
++j;
}
++i;
}
return {
type: INDEX_NODE,
bitmap: bitmap,
array: out,
};
}
return {
type: ARRAY_NODE,
size: root.size - 1,
array: cloneAndSet(root.array, idx, n),
};
}
return {
type: ARRAY_NODE,
size: root.size,
array: cloneAndSet(root.array, idx, n),
};
}
/**
* @template K,V
* @type {WithoutFunction<IndexNode<K,V>,K,V>}
*/
function withoutIndex(root, shift, hash, key) {
const bit = bitpos(hash, shift);
if ((root.bitmap & bit) === 0) {
return root; // already empty
}
const idx = index(root.bitmap, bit);
const node = root.array[idx];
// if the item is not an entry
if (node.type !== ENTRY) {
const n = without(node, shift + SHIFT, hash, key);
if (n === node) {
return root; // no changes
}
// if not undefined, the child node still has items, so update it
if (n !== undefined) {
return {
type: INDEX_NODE,
bitmap: root.bitmap,
array: cloneAndSet(root.array, idx, n),
};
}
// otherwise the child node should be removed
// if it was the only child node, remove this node from the parent
if (root.bitmap === bit) {
return undefined;
}
// otherwise just remove the child node
return {
type: INDEX_NODE,
bitmap: root.bitmap ^ bit,
array: spliceOut(root.array, idx),
};
}
// otherwise the item is an entry, remove it if the key matches
if (isEqual(key, node.k)) {
if (root.bitmap === bit) {
return undefined;
}
return {
type: INDEX_NODE,
bitmap: root.bitmap ^ bit,
array: spliceOut(root.array, idx),
};
}
return root;
}
/**
* @template K,V
* @param {CollisionNode<K,V>} root
* @param {K} key
* @returns {undefined | Node<K,V>}
*/
function withoutCollision(root, key) {
const idx = collisionIndexOf(root, key);
// if the key not found, no changes
if (idx < 0) {
return root;
}
// otherwise the entry was found, remove it
// if it was the only entry in this node, remove the whole node
if (root.array.length === 1) {
return undefined;
}
// otherwise just remove the entry
return {
type: COLLISION_NODE,
hash: root.hash,
array: spliceOut(root.array, idx),
};
}
/**
* @template K,V
* @param {undefined | Node<K,V>} root
* @param {(value:V,key:K)=>void} fn
* @returns {void}
*/
function forEach(root, fn) {
if (root === undefined) {
return;
}
const items = root.array;
const size = items.length;
for (let i = 0; i < size; i++) {
const item = items[i];
if (item === undefined) {
continue;
}
if (item.type === ENTRY) {
fn(item.v, item.k);
continue;
}
forEach(item, fn);
}
}
/**
* Extra wrapper to keep track of Dict size and clean up the API
* @template K,V
*/
export default class Dict {
/**
* @template V
* @param {Record<string,V>} o
* @returns {Dict<string,V>}
*/
static fromObject(o) {
const keys = Object.keys(o);
/** @type Dict<string,V> */
let m = Dict.new();
for (let i = 0; i < keys.length; i++) {
const k = keys[i];
m = m.set(k, o[k]);
}
return m;
}
/**
* @template K,V
* @param {Map<K,V>} o
* @returns {Dict<K,V>}
*/
static fromMap(o) {
/** @type Dict<K,V> */
let m = Dict.new();
o.forEach((v, k) => {
m = m.set(k, v);
});
return m;
}
static new() {
return new Dict(undefined, 0);
}
/**
* @param {undefined | Node<K,V>} root
* @param {number} size
*/
constructor(root, size) {
this.root = root;
this.size = size;
}
/**
* @template NotFound
* @param {K} key
* @param {NotFound} notFound
* @returns {NotFound | V}
*/
get(key, notFound) {
if (this.root === undefined) {
return notFound;
}
const found = find(this.root, 0, getHash(key), key);
if (found === undefined) {
return notFound;
}
return found.v;
}
/**
* @param {K} key
* @param {V} val
* @returns {Dict<K,V>}
*/
set(key, val) {
const addedLeaf = { val: false };
const root = this.root === undefined ? EMPTY : this.root;
const newRoot = assoc(root, 0, getHash(key), key, val, addedLeaf);
if (newRoot === this.root) {
return this;
}
return new Dict(newRoot, addedLeaf.val ? this.size + 1 : this.size);
}
/**
* @param {K} key
* @returns {Dict<K,V>}
*/
delete(key) {
if (this.root === undefined) {
return this;
}
const newRoot = without(this.root, 0, getHash(key), key);
if (newRoot === this.root) {
return this;
}
if (newRoot === undefined) {
return Dict.new();
}
return new Dict(newRoot, this.size - 1);
}
/**
* @param {K} key
* @returns {boolean}
*/
has(key) {
if (this.root === undefined) {
return false;
}
return find(this.root, 0, getHash(key), key) !== undefined;
}
/**
* @returns {[K,V][]}
*/
entries() {
if (this.root === undefined) {
return [];
}
/** @type [K,V][] */
const result = [];
this.forEach((v, k) => result.push([k, v]));
return result;
}
/**
*
* @param {(val:V,key:K)=>void} fn
*/
forEach(fn) {
forEach(this.root, fn);
}
hashCode() {
let h = 0;
this.forEach((v, k) => {
h = (h + hashMerge(getHash(v), getHash(k))) | 0;
});
return h;
}
/**
* @param {unknown} o
* @returns {boolean}
*/
equals(o) {
if (!(o instanceof Dict) || this.size !== o.size) {
return false;
}
let equal = true;
this.forEach((v, k) => {
equal = equal && isEqual(o.get(k, !v), v);
});
return equal;
}
}