993 lines
23 KiB
JavaScript
993 lines
23 KiB
JavaScript
/**
|
|
* 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 = /* @__PURE__ */ new WeakMap();
|
|
const tempDataView = /* @__PURE__ */ new DataView(
|
|
/* @__PURE__ */ 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;
|
|
}
|
|
|
|
try {
|
|
this.forEach((v, k) => {
|
|
if (!isEqual(o.get(k, !v), v)) {
|
|
throw unequalDictSymbol;
|
|
}
|
|
});
|
|
return true;
|
|
} catch (e) {
|
|
if (e === unequalDictSymbol) {
|
|
return false;
|
|
}
|
|
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
// This is thrown internally in Dict.equals() so that it returns false as soon
|
|
// as a non-matching key is found
|
|
const unequalDictSymbol = /* @__PURE__ */ Symbol();
|