UNPKG

reign

Version:

A persistent, typed-objects implementation.

529 lines (452 loc) 17.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MIN_TYPE_ID = exports.HashSet = undefined; exports.make = make; var _backing = require("backing"); var _backing2 = _interopRequireDefault(_backing); var _ = require("../"); var _util = require("../../util"); var _2 = require("../../"); var _symbols = require("../../symbols"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } class HashSet extends _.TypedObject { /** * Return the size of the hash set. */ get size() { // Issue 252 return this[_symbols.$Backing].getUint32(this[_symbols.$Address] + CARDINALITY_OFFSET); } /** * Add the given entry to the set if it does not already exist. */ add(entry) { return this; } /** * Determine whether the hash set contains the given entry or not. */ has(entry) { return false; } /** * Deletes an entry from the hash set. * Returns `true` if the given entry was deleted, otherwise `false`. */ delete(entry) { return false; } } exports.HashSet = HashSet; const HEADER_SIZE = 16; const ARRAY_POINTER_OFFSET = 0; const ARRAY_LENGTH_OFFSET = 8; const CARDINALITY_OFFSET = 12; const INITIAL_BUCKET_COUNT = 16; const MIN_TYPE_ID = exports.MIN_TYPE_ID = Math.pow(2, 20) * 7; /** * Makes a HashSetType type class for the given realm. */ function make(realm) { const TypeClass = realm.TypeClass; const StructType = realm.StructType; const ReferenceType = realm.ReferenceType; const T = realm.T; const backing = realm.backing; let typeCounter = 0; return new TypeClass('HashSetType', function (EntryType) { let config = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; return Partial => { const name = typeof config.name === 'string' ? config.name : `HashSet<${ EntryType.name }>`; if (realm.T[name]) { return realm.T[name]; } typeCounter++; const id = typeof config.id === 'number' ? config.id : MIN_TYPE_ID + typeCounter; Partial[_symbols.$CanBeEmbedded] = false; Partial[_symbols.$CanBeReferenced] = true; Partial[_symbols.$CanContainReferences] = true; Object.defineProperties(Partial, { id: { value: id }, name: { value: name } }); Partial.ref = new ReferenceType(Partial); function getArrayAddress(backing, address) { return backing.getFloat64(address); } function setArrayAddress(backing, address, value) { backing.setFloat64(address, value); } function getArrayLength(backing, address) { return backing.getUint32(address + ARRAY_LENGTH_OFFSET); } function setArrayLength(backing, address, value) { backing.setUint32(address + ARRAY_LENGTH_OFFSET, value); } function getCardinality(backing, address) { return backing.getUint32(address + CARDINALITY_OFFSET); } function setCardinality(backing, address, value) { backing.setUint32(address + CARDINALITY_OFFSET, value); } const Bucket = new StructType([['hash', T.Uint32], ['entry', EntryType]]); const ENTRY_OFFSET = Bucket.fieldOffsets.entry; const BUCKET_SIZE = (0, _util.alignTo)(Bucket.byteLength, Bucket.byteAlignment); function getBucketHash(backing, bucketAddress) { return backing.getUint32(bucketAddress); } function setBucketHash(backing, bucketAddress, value) { backing.setUint32(bucketAddress, value); } function getBucketEntry(backing, bucketAddress) { return EntryType.load(backing, bucketAddress + ENTRY_OFFSET); } function setBucketEntry(backing, bucketAddress, value) { return EntryType.store(backing, bucketAddress + ENTRY_OFFSET, value); } /** * The constructor for this kind of hash set. */ function constructor(backingOrInput, address) { if (backingOrInput instanceof _backing2.default) { this[_symbols.$Backing] = backingOrInput; this[_symbols.$Address] = address; } else { this[_symbols.$Backing] = backing; this[_symbols.$Address] = createHashSetAt(backing, backing.gc.calloc(HEADER_SIZE, id), backingOrInput); } this[_symbols.$CanBeReferenced] = true; } /** * Create a new hash set from the given input and return its address. */ function createHashSetAt(backing, address, input) { if (input == null) { createEmptyHashSet(backing, address); } else if (Array.isArray(input)) { createHashSetFromArray(backing, address, input); } // Issue 252 else if (typeof input[Symbol.iterator] === 'function') { createHashSetFromIterable(backing, address, input); } else { throw new TypeError(`Cannot create a ${ name } from invalid input.`); } return address; } /** * Create an empty hashset with a bucket array. * Use `initialCardinalityHint` to pre-allocate a bucket array which can * handle at least the given number of entries. Note that specifying this * argument does not actually write the cardinality value. */ function createEmptyHashSet(backing, header) { let initialCardinalityHint = arguments.length <= 2 || arguments[2] === undefined ? 0 : arguments[2]; let initialSize; if (initialCardinalityHint * 2 < INITIAL_BUCKET_COUNT) { initialSize = INITIAL_BUCKET_COUNT; } else { initialSize = initialCardinalityHint * 2; } const body = backing.calloc(initialSize * BUCKET_SIZE); setArrayAddress(backing, header, body); setArrayLength(backing, header, initialSize); return header; } /** * Create a hashset from an array of entries. */ function createHashSetFromArray(backing, header, input) { const length = input.length; createEmptyHashSet(backing, header, length); for (let i = 0; i < length; i++) { const entry = input[i]; const hash = EntryType.hashValue(entry); lookupOrInsert(backing, header, entry, hash); } } /** * Create a hashset from an iterable. */ function createHashSetFromIterable(backing, header, input) { createEmptyHashSet(backing, header); for (const entry of input) { const hash = EntryType.hashValue(entry); lookupOrInsert(backing, header, entry, hash); } } /** * Return the appropriate bucket for the given entry + hash. */ function probe(backing, header, entry, hash) { const bucketArrayLength = getArrayLength(backing, header); const bucketArrayAddress = getArrayAddress(backing, header); let index = hash & bucketArrayLength - 1; let address = bucketArrayAddress + index * BUCKET_SIZE; let bucketHash = getBucketHash(backing, address); while (bucketHash !== 0 && (bucketHash !== hash || !EntryType.equal(entry, getBucketEntry(backing, address)))) { index++; if (index >= bucketArrayLength) { index = 0; } address = bucketArrayAddress + index * BUCKET_SIZE; bucketHash = getBucketHash(backing, address); } return address; } /** * Find the address of the bucket for the given entry + hash, or 0 if it does not exist. */ function lookup(backing, header, entry, hash) { const address = probe(backing, header, entry, hash); return getBucketHash(backing, address) === 0 ? 0 : address; } /** * Find the address of the bucket for the given entry + hash, or create it if it does not exist. */ function lookupOrInsert(backing, header, entry, hash) { const bucketArrayLength = getArrayLength(backing, header); const address = probe(backing, header, entry, hash); if (getBucketHash(backing, address) !== 0) { return address; } setBucketEntry(backing, address, entry); setBucketHash(backing, address, hash); const cardinality = getCardinality(backing, header) + 1; setCardinality(backing, header, cardinality); if (cardinality + (cardinality >> 2) >= bucketArrayLength) { grow(backing, header); return probe(backing, header, entry, hash); } else { return address; } } /** * Remove the given entry + hash from the hash set. */ function remove(backing, header, entry, hash) { let p = probe(backing, header, entry, hash); if (getBucketHash(backing, p) === 0) { return false; } const bucketArrayLength = getArrayLength(backing, header); const bucketArrayAddress = getArrayAddress(backing, header); const end = bucketArrayAddress + bucketArrayLength * BUCKET_SIZE; // @fixme free the bucket value? let q = p; while (true) { // Move q to the next entry q = q + BUCKET_SIZE; if (q === end) { q = bucketArrayAddress; } const qHash = getBucketHash(backing, q); // All entries between p and q have their initial position between p and q // and the entry p can be cleared without breaking the search for these // entries. if (qHash === 0) { break; } // Find the initial position for the entry at position q. const r = bucketArrayAddress + (qHash & bucketArrayLength - 1) * BUCKET_SIZE; // If the entry at position q has its initial position outside the range // between p and q it can be moved forward to position p and will still be // found. There is now a new candidate entry for clearing. if (q > p && (r <= p || r > q) || q < p && r <= p && r > q) { backing.copy(p, q, BUCKET_SIZE); p = q; } } setBucketHash(backing, p, 0); const cardinality = getCardinality(backing, header); setCardinality(backing, header, cardinality - 1); return true; } /** * Grow the backing array to twice its current capacity. */ function grow(backing, header) { const bucketArrayLength = getArrayLength(backing, header); const bucketArrayAddress = getArrayAddress(backing, header); const cardinality = getCardinality(backing, header); const newBuckets = backing.calloc(bucketArrayLength * 2 * BUCKET_SIZE); setArrayAddress(backing, header, newBuckets); setArrayLength(backing, header, bucketArrayLength * 2); setCardinality(backing, header, 1); for (let index = 0; index < bucketArrayLength; index++) { const oldAddress = bucketArrayAddress + index * BUCKET_SIZE; const bucketHash = getBucketHash(backing, oldAddress); if (bucketHash !== 0) { lookupOrInsert(backing, header, getBucketEntry(backing, oldAddress), bucketHash); } } setCardinality(backing, header, cardinality); backing.free(bucketArrayAddress); } /** * Destroy the hashset at the given address, along with all its contents. */ function destructor(backing, header) { const bucketArrayAddress = getArrayAddress(backing, header); if (bucketArrayAddress !== 0) { const bucketArrayLength = getArrayLength(backing, header); let current = bucketArrayAddress; for (let index = 0; index < bucketArrayLength; index++) { Bucket.destructor(backing, current); current += BUCKET_SIZE; } setArrayAddress(backing, header, 0); setArrayLength(backing, header, 0); backing.free(bucketArrayAddress); } } const prototype = Object.create(HashSet.prototype, { /** * Add the given entry to the hash set. */ add: { value: function value(entry) { const hash = EntryType.hashValue(entry); lookupOrInsert(this[_symbols.$Backing], this[_symbols.$Address], entry, hash); return this; } }, /** * Determine whether the hash set contains the given entry or not. */ has: { value: function value(entry) { const hash = EntryType.hashValue(entry); return lookup(this[_symbols.$Backing], this[_symbols.$Address], entry, hash) !== 0; } }, /** * Deletes an entry from the hash set. * Returns `true` if the given entry was deleted, otherwise `false`. */ delete: { value: function value(entry) { const hash = EntryType.hashValue(entry); return remove(this[_symbols.$Backing], this[_symbols.$Address], entry, hash); } }, /** * Return a representation of the hash set which can be encoded as JSON. */ toJSON: { value: function value() { const backing = this[_symbols.$Backing]; const address = this[_symbols.$Address]; const size = getCardinality(backing, address); const bucketArrayLength = getArrayLength(backing, address); const arr = new Array(size); let current = getArrayAddress(backing, address); let index = 0; for (let i = 0; i < bucketArrayLength; i++) { if (getBucketHash(backing, current) !== 0) { arr[index++] = getBucketEntry(backing, current); } current += BUCKET_SIZE; } return arr; } }, /** * Iterate the key / values in the set. * IMPORTANT: The iteration order is not stable and should not be relied on! * It is guaranteed that every entry will be yielded exactly once, but the order * depends on the hashed value and the size of the backing array. * If you need ordered iteration, use a SkipListSet. */ [Symbol.iterator]: { value: function* value() { let backing = this[_symbols.$Backing]; let address = this[_symbols.$Address]; let bucketArrayLength = getArrayLength(backing, address); let current = getArrayAddress(backing, address); for (let index = 0; index < bucketArrayLength; index++) { if (getBucketHash(backing, current) !== 0) { yield getBucketEntry(backing, current); } current += BUCKET_SIZE; } } } }); return { id: id, name: name, byteLength: 8, byteAlignment: 8, constructor: constructor, prototype: prototype, accepts: function accepts(input) { return input !== null && typeof input === 'object'; }, initialize: function initialize(backing, pointerAddress, initialValue) { const address = backing.gc.alloc(HEADER_SIZE, Partial.id, 1); createHashSetAt(backing, address, initialValue); backing.setFloat64(pointerAddress, address); }, store: function store(backing, pointerAddress, input) { const existing = backing.getFloat64(pointerAddress); if (existing !== 0) { backing.setFloat64(pointerAddress, 0); backing.gc.unref(existing); } const address = backing.gc.alloc(HEADER_SIZE, Partial.id, 1); createHashSetAt(backing, address, input); backing.setFloat64(pointerAddress, address); }, load: function load(backing, pointerAddress) { const address = backing.getFloat64(pointerAddress); return address === 0 ? null : new Partial(backing, address); }, clear: function clear(backing, pointerAddress) { const address = backing.getFloat64(pointerAddress); if (address !== 0) { backing.setFloat64(pointerAddress, 0); backing.gc.unref(address); } }, destructor: destructor, equal: function equal(setA, setB) { if (setA[_symbols.$Backing] === setB[_symbols.$Backing] && setA[_symbols.$Address] === setB[_symbols.$Address]) { return true; } else if (setA.size !== setB.size) { return false; } for (const key of setA) { if (!setB.has(key)) { return false; } } return true; }, randomValue: function randomValue() { const set = new Partial(); const size = Math.ceil(Math.random() * 32); for (let i = 0; i < size; i++) { set.add(EntryType.randomValue()); } return set; }, emptyValue: function emptyValue() { return new Partial(); }, flowType: function flowType() { return `HashSet<${ EntryType.flowType() }>`; }, hashValue: function hashValue(input) { return input[_symbols.$Address]; } }; }; }); }