reign
Version:
A persistent, typed-objects implementation.
621 lines (530 loc) • 21.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.MIN_TYPE_ID = exports.HashMap = undefined;
var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
exports.make = make;
var _backing = require("backing");
var _backing2 = _interopRequireDefault(_backing);
var _ = require("../");
var _2 = require("../../");
var _util = require("../../util");
var _symbols = require("../../symbols");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
class HashMap extends _.TypedObject {
/**
* Return the size of the hash map.
*/
get size() {
// Issue 252
return this[_symbols.$Backing].getUint32(this[_symbols.$Address] + CARDINALITY_OFFSET);
}
/**
* Get the value associated with the given key, otherwise undefined.
*/
get(key) {
return undefined;
}
/**
* Set the value associated with the given key.
*/
set(key) {
return this;
}
/**
* Determine whether the hash map contains the given key or not.
*/
has(key) {
return false;
}
/**
* Deletes a key from the hash map.
* Returns `true` if the given key was deleted, otherwise `false`.
*/
delete(key) {
return false;
}
}
exports.HashMap = HashMap;
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) * 6;
/**
* Makes a HashMapType type class for the given realm.
*/
function make(realm) {
const TypeClass = realm.TypeClass;
const StructType = realm.StructType;
const T = realm.T;
const backing = realm.backing;
let typeCounter = 0;
return new TypeClass('HashMapType', function (KeyType, ValueType) {
let config = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2];
return Partial => {
const name = typeof config.name === 'string' ? config.name : `HashMap<${ KeyType.name }, ${ ValueType.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
}
});
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], ['key', KeyType], ['value', ValueType]]);
const KEY_OFFSET = Bucket.fieldOffsets.key;
const VALUE_OFFSET = Bucket.fieldOffsets.value;
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 getBucketKey(backing, bucketAddress) {
return KeyType.load(backing, bucketAddress + KEY_OFFSET);
}
function setBucketKey(backing, bucketAddress, value) {
return KeyType.store(backing, bucketAddress + KEY_OFFSET, value);
}
function getBucketValue(backing, bucketAddress) {
return ValueType.load(backing, bucketAddress + VALUE_OFFSET);
}
function setBucketValue(backing, bucketAddress, value) {
return ValueType.store(backing, bucketAddress + VALUE_OFFSET, value);
}
/**
* The constructor for this kind of hash map.
*/
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] = createHashMapAt(backing, backing.gc.calloc(HEADER_SIZE, id), backingOrInput);
}
this[_symbols.$CanBeReferenced] = true;
}
/**
* Create a new hash map from the given input and return its address.
*/
function createHashMapAt(backing, address, input) {
if (input == null) {
createEmptyHashMap(backing, address);
} else if (typeof input !== 'object') {
throw new TypeError(`Cannot create a ${ name } from invalid input.`);
} else if (Array.isArray(input)) {
createHashMapFromArray(backing, address, input);
} else if (typeof input[Symbol.iterator] === 'function') {
createHashMapFromIterable(backing, address, input);
} else {
createHashMapFromObject(backing, address, input);
}
return address;
}
/**
* Create an empty hashmap 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 createEmptyHashMap(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 hashmap from an array of key / values.
*/
function createHashMapFromArray(backing, header, input) {
const length = input.length;
createEmptyHashMap(backing, header, length);
for (let i = 0; i < length; i++) {
var _input$i = _slicedToArray(input[i], 2);
const key = _input$i[0];
const value = _input$i[1];
const hash = KeyType.hashValue(key);
setBucketValue(backing, lookupOrInsert(backing, header, key, hash), value);
}
}
/**
* Create a hashmap from an iterable.
*/
function createHashMapFromIterable(backing, header, input) {
createEmptyHashMap(backing, header);
for (const _ref of input) {
var _ref2 = _slicedToArray(_ref, 2);
const key = _ref2[0];
const value = _ref2[1];
const hash = KeyType.hashValue(key);
setBucketValue(backing, lookupOrInsert(backing, header, key, hash), value);
}
}
/**
* Create a hashmap from an object.
*/
function createHashMapFromObject(backing, header, input) {
const keys = Object.keys(input);
const length = keys.length;
createEmptyHashMap(backing, header, length);
for (let i = 0; i < length; i++) {
const key = keys[i];
const value = input[key];
const hash = KeyType.hashValue(key);
setBucketValue(backing, lookupOrInsert(backing, header, key, hash), value);
}
}
/**
* Return the appropriate bucket for the given key + hash.
*/
function probe(backing, header, key, 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 || !KeyType.equal(key, getBucketKey(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 key + hash, or 0 if it does not exist.
*/
function lookup(backing, header, key, hash) {
const address = probe(backing, header, key, hash);
return getBucketHash(backing, address) === 0 ? 0 : address;
}
/**
* Find the address of the bucket for the given key + hash, or create it if it does not exist.
*/
function lookupOrInsert(backing, header, key, hash) {
const bucketArrayLength = getArrayLength(backing, header);
const address = probe(backing, header, key, hash);
if (getBucketHash(backing, address) !== 0) {
return address;
}
setBucketKey(backing, address, key);
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, key, hash);
} else {
return address;
}
}
/**
* Remove the given key + hash from the hash map.
*/
function remove(backing, header, key, hash) {
let p = probe(backing, header, key, 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;
}
}
// Clear the entry which is allowed to en emptied.
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) {
const newAddress = lookupOrInsert(backing, header, getBucketKey(backing, oldAddress), bucketHash);
setBucketValue(backing, newAddress, getBucketValue(backing, oldAddress));
}
}
setCardinality(backing, header, cardinality);
backing.free(bucketArrayAddress);
}
/**
* Destroy the hashmap 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);
setArrayAddress(backing, header, 0);
setArrayLength(backing, header, 0);
let current = bucketArrayAddress;
for (let index = 0; index < bucketArrayLength; index++) {
Bucket.clear(backing, current);
current += BUCKET_SIZE;
}
backing.free(bucketArrayAddress);
}
}
const prototype = Object.create(HashMap.prototype, {
/**
* Get the value associated with the given key, otherwise undefined.
*/
get: {
value: function value(key) {
const hash = KeyType.hashValue(key);
const backing = this[_symbols.$Backing];
const address = lookup(backing, this[_symbols.$Address], key, hash);
if (address === 0) {
return undefined;
} else {
return getBucketValue(backing, address);
}
}
},
/**
* Set the value associated with the given key.
*/
set: {
value: function (_value) {
function value(_x3, _x4) {
return _value.apply(this, arguments);
}
value.toString = function () {
return _value.toString();
};
return value;
}(function (key, value) {
const hash = KeyType.hashValue(key);
const backing = this[_symbols.$Backing];
const address = lookupOrInsert(backing, this[_symbols.$Address], key, hash);
setBucketValue(backing, address, value);
return this;
})
},
/**
* Determine whether the hash map contains the given key or not.
*/
has: {
value: function value(key) {
const hash = KeyType.hashValue(key);
return lookup(this[_symbols.$Backing], this[_symbols.$Address], key, hash) !== 0;
}
},
/**
* Deletes a key from the hash map.
* Returns `true` if the given key was deleted, otherwise `false`.
*/
delete: {
value: function value(key) {
const hash = KeyType.hashValue(key);
return remove(this[_symbols.$Backing], this[_symbols.$Address], key, hash);
}
},
/**
* Return a representation of the hash map 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++] = [getBucketKey(backing, current), getBucketValue(backing, current)];
}
current += BUCKET_SIZE;
}
return arr;
}
},
forEach: {
value: function value(visitor) {
const backing = this[_symbols.$Backing];
const address = this[_symbols.$Address];
const bucketArrayLength = getArrayLength(backing, address);
let current = getArrayAddress(backing, address);
for (let index = 0; index < bucketArrayLength; index++) {
if (getBucketHash(backing, current) !== 0) {
visitor(getBucketValue(backing, current), getBucketKey(backing, current), this);
}
current += BUCKET_SIZE;
}
return this;
}
},
/**
* Iterate the key / values in the map.
* 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 SkipListMap.
*/
[Symbol.iterator]: {
value: function* value() {
const backing = this[_symbols.$Backing];
const address = this[_symbols.$Address];
const bucketArrayLength = getArrayLength(backing, address);
let current = getArrayAddress(backing, address);
for (let index = 0; index < bucketArrayLength; index++) {
if (getBucketHash(backing, current) !== 0) {
yield [getBucketKey(backing, current), getBucketValue(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.calloc(HEADER_SIZE, Partial.id, 1);
createHashMapAt(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.calloc(HEADER_SIZE, Partial.id, 1);
createHashMapAt(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(mapA, mapB) {
if (mapA[_symbols.$Backing] === mapB[_symbols.$Backing] && mapA[_symbols.$Address] === mapB[_symbols.$Address]) {
return true;
} else if (mapA.size !== mapB.size) {
return false;
}
for (const _ref3 of mapA) {
var _ref4 = _slicedToArray(_ref3, 2);
const key = _ref4[0];
const a = _ref4[1];
const b = mapB.get(key);
// Fixme we should check the value types here.
if (a !== b && (b === undefined || !ValueType.equal(a, b))) {
return false;
}
}
return true;
},
randomValue: function randomValue() {
const map = new Partial();
const size = Math.ceil(Math.random() * 32);
for (let i = 0; i < size; i++) {
map.set(KeyType.randomValue(), ValueType.randomValue());
}
return map;
},
emptyValue: function emptyValue() {
return new Partial();
},
flowType: function flowType() {
return `HashMap<${ KeyType.flowType() }, ${ ValueType.flowType() }>`;
},
hashValue: function hashValue(input) {
return input[_symbols.$Address];
}
};
};
});
}