/** * 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 | IndexNode | CollisionNode} 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 | Node)[] }} ArrayNode */ /** * @template K,V * @typedef {{ type: typeof INDEX_NODE, bitmap: number, array: (Entry | Node)[] }} IndexNode */ /** * @template K,V * @typedef {{ type: typeof COLLISION_NODE, hash: number, array: Entry[] }} 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} */ 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} */ 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} */ /** * Associate a node with a new entry, creating a new node * @template T,K,V * @type {AssocFunction,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,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,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,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} 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} */ /** * Return the found entry or undefined if not present in the root * @template K,V * @type {FindFunction,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,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,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} root * @param {K} key * @returns {undefined | Entry} */ 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} */ /** * 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,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,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,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} root * @param {K} key * @returns {undefined | Node} */ 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} 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} o * @returns {Dict} */ static fromObject(o) { const keys = Object.keys(o); /** @type Dict */ 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} o * @returns {Dict} */ static fromMap(o) { /** @type Dict */ 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} 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} */ 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} */ 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; } }