├── .editorconfig ├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── __tests__ └── pessimism_test.re ├── bsconfig.json ├── package.json ├── rollup.config.js ├── src ├── index.d.ts ├── index.js ├── pessimism.re ├── pessimism.rei ├── pessimism_mutable.re └── pessimism_mutable.rei ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_size = 2 6 | end_of_line = lf 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.obj 3 | *.out 4 | *.compile 5 | *.native 6 | *.byte 7 | *.cmo 8 | *.annot 9 | *.cmi 10 | *.cmx 11 | *.cmt 12 | *.cmti 13 | *.cma 14 | *.a 15 | *.cmxa 16 | *.obj 17 | *~ 18 | *.annot 19 | *.cmj 20 | *.bak 21 | lib/bs 22 | *.mlast 23 | *.mliast 24 | *.install 25 | .vscode 26 | .merlin 27 | .bsb.lock 28 | .cache/ 29 | 30 | yarn-error.log 31 | dist/ 32 | node_modules/ 33 | lib/ 34 | src/**/*.js 35 | src/**/*.js.flow 36 | include/**/*.js 37 | __tests__/**/*.js 38 | coverage/ 39 | _esy/ 40 | public/ 41 | 42 | !src/index.js 43 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | /esy.lock 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Phil Plückthun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

pessimism

2 |

3 | A fast and compact HAMT-based KV-Cache with optimistic entries 4 |

5 | 6 | `pessimism` is a fast and compact KV-Cache primarily built for `@urql/exchange-graphcache`. 7 | It's a functional and immutable HAMT structure, which increases structural sharing, while 8 | keeping memory usage compact. It supports optimistic entries, which can be invalidated in a single go. 9 | 10 | ## Usage 11 | 12 | This library works with both TypeScript and BuckleScript (Reason/OCaml). 13 | 14 | ```sh 15 | yarn add pessimism 16 | # or 17 | npm install --save pessimism 18 | ``` 19 | 20 | The basic methods support making a map and setting, getting, and removing entries: 21 | 22 | ```js 23 | import * as Map from 'pessimism'; 24 | 25 | let map = Map.set(Map.make(), "key", "value"); 26 | Map.get(map, "key"); // "value" 27 | 28 | map = Map.remove(map, "key"); 29 | Map.get(map, "key"); // undefined 30 | ``` 31 | 32 | Optimistic entries can be set using `setOptimistic` and cleared using `clearOptimistic`: 33 | 34 | ```js 35 | import * as Map from 'pessimism'; 36 | 37 | let map = Map.set(Map.make(), "key", "value"); 38 | // Set an optimistic entry with the ID 1 39 | map = Map.setOptimistic("key", "temp", 1); 40 | 41 | Map.get(map, "key"); // "temp" which is the optimistic value 42 | 43 | // Clear all optimistic entries with ID 1 44 | map = Map.clearOptimistic(map, 1); 45 | Map.get(map, "key"); // "value" which was the original value 46 | ``` 47 | -------------------------------------------------------------------------------- /__tests__/pessimism_test.re: -------------------------------------------------------------------------------- 1 | open Jest; 2 | open Pessimism; 3 | 4 | let it = test; 5 | 6 | /* These two strings collide and cause an Index to be created */ 7 | let ca = "coest"; 8 | let cb = "01pcj"; 9 | 10 | it("works for values on a single Index node", () => { 11 | open Expect; 12 | open! Expect.Operators; 13 | let words = [|"hello", "world", "test"|]; 14 | let map = 15 | make() 16 | ->set(words[0], words[0]) 17 | ->set(words[1], words[1]) 18 | ->set(words[2], words[2]); 19 | let words_out = Array.map(x => map->get(x), words); 20 | expect(words_out) == Array.map(x => Some(x), words); 21 | }); 22 | 23 | it("works for values on nested Index nodes", () => { 24 | open Expect; 25 | open! Expect.Operators; 26 | 27 | let map = make()->set(ca, ca)->set(cb, cb); 28 | let expected = [|Some(ca), Some(cb)|]; 29 | let actual = [|map->get(ca), map->get(cb)|]; 30 | expect(actual) == expected; 31 | }); 32 | 33 | it("works for values that are colliding", () => { 34 | open Expect; 35 | open! Expect.Operators; 36 | let words = [|"hetairas", "mentioner", "heliotropes", "neurospora"|]; 37 | let map = 38 | make() 39 | ->set(words[0], words[0]) 40 | ->set(words[1], words[1]) 41 | ->set(words[2], words[2]) 42 | ->set(words[3], words[3]); 43 | let words_out = Array.map(x => map->get(x), words); 44 | expect(words_out) == Array.map(x => Some(x), words); 45 | }); 46 | 47 | it("works for transitive changes", () => { 48 | open Expect; 49 | open! Expect.Operators; 50 | let words = [| 51 | "hello", 52 | "world", 53 | "test", 54 | "hetairas", 55 | "mentioner", 56 | "heliotropes", 57 | "neurospora", 58 | |]; 59 | let map = ref(make()->asMutable); 60 | Array.iter(word => map := set(map^, word, word), words); 61 | let map = asImmutable(map^); 62 | let words_out = Array.map(x => map->get(x), words); 63 | expect(words_out) == Array.map(x => Some(x), words); 64 | }); 65 | 66 | it("deletes values correctly", () => { 67 | open Expect; 68 | open! Expect.Operators; 69 | let words = [|"hello", "world", "test"|]; 70 | let map = 71 | make() 72 | ->set(words[0], words[0]) 73 | ->set(words[1], words[1]) 74 | ->set(words[2], words[2]) 75 | ->remove(words[1]); 76 | let a = map->get(words[0]); 77 | let b = map->get(words[1]); 78 | expect([|a, b|]) == [|Some(words[0]), None|]; 79 | }); 80 | 81 | it("deleted values correctly on nested Index nodes", () => { 82 | open Expect; 83 | open! Expect.Operators; 84 | 85 | let map = make()->set(ca, ca)->set(cb, cb)->remove(ca); 86 | let expected = [|None, Some(cb)|]; 87 | let actual = [|map->get(ca), map->get(cb)|]; 88 | expect(actual) == expected; 89 | }); 90 | 91 | it("sets optimistic values and clears them", () => { 92 | open Expect; 93 | open! Expect.Operators; 94 | let map = 95 | make()->set("key", "permanent")->setOptimistic("key", "temporary", 1); 96 | let before = map->get("key"); 97 | let map = map->setOptimistic("key", "temporary2", 2); 98 | let middle = map->get("key"); 99 | let map = map->clearOptimistic(1); 100 | let after = map->get("key"); 101 | let map = map->clearOptimistic(2); 102 | let after2 = map->get("key"); 103 | expect([|before, middle, after, after2|]) 104 | == [| 105 | Some("temporary"), 106 | Some("temporary2"), 107 | Some("temporary2"), 108 | Some("permanent"), 109 | |]; 110 | }); 111 | 112 | it("sets optimistic values and overrides them if needed", () => { 113 | open Expect; 114 | open! Expect.Operators; 115 | let map = 116 | make()->set("key", "permanent")->setOptimistic("key", "temporary", 1); 117 | let before = map->get("key"); 118 | let map = map->set("key", "permanent2"); 119 | let after = map->get("key"); 120 | expect([|before, after|]) == [|Some("temporary"), Some("permanent2")|]; 121 | }); 122 | 123 | it("supports setting and retrieving undefined", () => { 124 | open Expect; 125 | open! Expect.Operators; 126 | let map = 127 | make() 128 | ->set("key", Js.Undefined.return("permanent")) 129 | ->setOptimistic("key", Js.Undefined.empty, 1); 130 | let before = map->getUndefined("key"); 131 | let map = map->clearOptimistic(1); 132 | let after = map->getUndefined("key"); 133 | expect([|before, after|]) == [%raw "[undefined, 'permanent']"]; 134 | }); 135 | -------------------------------------------------------------------------------- /bsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pessimism", 3 | "version": "1.0.0", 4 | "namespace": false, 5 | "bsc-flags": ["-bs-super-errors", "-bs-no-version-header"], 6 | "refmt": 3, 7 | "package-specs": [ 8 | { 9 | "module": "commonjs" 10 | }, 11 | { 12 | "module": "es6", 13 | "in-source": true 14 | } 15 | ], 16 | "sources": [ 17 | { 18 | "dir": "src" 19 | }, 20 | { 21 | "dir": "__tests__", 22 | "type": "dev" 23 | } 24 | ], 25 | "bs-dependencies": [], 26 | "bs-dev-dependencies": ["@glennsl/bs-jest"] 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pessimism", 3 | "description": "A fast HAMT Map intended for KV caching and optimistic updates", 4 | "version": "1.1.4", 5 | "author": "Phil Pluckthun ", 6 | "source": "./src/index.js", 7 | "main": "./dist/pessimism.js", 8 | "module": "./dist/pessimism.es.js", 9 | "jsnext:main": "./dist/pessimism.es.js", 10 | "types": "./src/index.d.ts", 11 | "sideEffects": false, 12 | "files": [ 13 | "src", 14 | "lib/js", 15 | "dist", 16 | "docs/*.md", 17 | "*.md", 18 | "bsconfig.json" 19 | ], 20 | "scripts": { 21 | "check": "tsc --noEmit", 22 | "clean": "bsb -clean-world", 23 | "build": "bsb -make-world", 24 | "watch": "bsb -make-world -w", 25 | "test": "jest", 26 | "coverage": "jest -c ./.jest.coverage.json --coverage", 27 | "test:watch": "jest --watch", 28 | "refmt": "bsrefmt --in-place **/**/*.{re,rei}", 29 | "bundle": "rollup -c rollup.config.js", 30 | "prepublishOnly": "run-s clean build bundle check test" 31 | }, 32 | "keywords": [ 33 | "hamt", 34 | "kv", 35 | "cache", 36 | "optimistic" 37 | ], 38 | "repository": "https://github.com/kitten/pessimism", 39 | "bugs": { 40 | "url": "https://github.com/kitten/pessimism/issues" 41 | }, 42 | "license": "MIT", 43 | "dependencies": {}, 44 | "devDependencies": { 45 | "@babel/core": "^7.5.5", 46 | "@glennsl/bs-jest": "^0.4.8", 47 | "babel-plugin-closure-elimination": "^1.3.0", 48 | "bs-platform": "^6.0.3", 49 | "husky": "^3.0.1", 50 | "lint-staged": "^9.2.1", 51 | "npm-run-all": "^4.1.5", 52 | "prettier": "^1.18.2", 53 | "rollup": "^1.17.0", 54 | "rollup-plugin-babel": "^4.3.3", 55 | "rollup-plugin-buble": "^0.19.8", 56 | "rollup-plugin-commonjs": "^10.0.1", 57 | "rollup-plugin-node-resolve": "^5.2.0", 58 | "rollup-plugin-terser": "^5.1.1", 59 | "typescript": "^3.5.3" 60 | }, 61 | "lint-staged": { 62 | "*.{d.ts,js}": [ 63 | "prettier --write", 64 | "git add" 65 | ], 66 | "*.{re,rei}": [ 67 | "bsrefmt --in-place", 68 | "git add" 69 | ] 70 | }, 71 | "husky": { 72 | "hooks": { 73 | "pre-commit": "lint-staged" 74 | } 75 | }, 76 | "prettier": { 77 | "singleQuote": true, 78 | "printWidth": 100 79 | }, 80 | "jest": { 81 | "moduleFileExtensions": [ 82 | "js" 83 | ], 84 | "testMatch": [ 85 | "**/lib/js/__tests__/*_test.js" 86 | ] 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { DEFAULT_EXTENSIONS } from '@babel/core'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import nodeResolve from 'rollup-plugin-node-resolve'; 4 | import buble from 'rollup-plugin-buble'; 5 | import babel from 'rollup-plugin-babel'; 6 | import { terser } from 'rollup-plugin-terser'; 7 | 8 | const plugins = [ 9 | nodeResolve({ 10 | mainFields: ['module', 'jsnext', 'main'], 11 | browser: true 12 | }), 13 | commonjs({ 14 | include: /\/node_modules\// 15 | }), 16 | buble({ 17 | transforms: { 18 | dangerousForOf: true, 19 | dangerousTaggedTemplateString: true 20 | }, 21 | objectAssign: 'Object.assign' 22 | }), 23 | babel({ 24 | babelrc: false, 25 | extensions: [...DEFAULT_EXTENSIONS, 'ts', 'tsx'], 26 | exclude: 'node_modules/**', 27 | presets: [], 28 | plugins: [['babel-plugin-closure-elimination', {}]] 29 | }), 30 | 31 | terser({ 32 | warnings: true, 33 | ecma: 5, 34 | ie8: false, 35 | mangle: true, 36 | toplevel: true, 37 | keep_fnames: true, 38 | compress: { 39 | reduce_funcs: false, 40 | passes: 4 41 | }, 42 | output: { 43 | beautify: true, 44 | braces: true, 45 | indent_level: 2 46 | } 47 | }) 48 | ]; 49 | 50 | const config = { 51 | input: './src/index.js', 52 | external: () => false, 53 | plugins, 54 | treeshake: { 55 | propertyReadSideEffects: false 56 | }, 57 | output: [ 58 | { 59 | legacy: true, 60 | freeze: false, 61 | esModule: false, 62 | file: './dist/pessimism.js', 63 | format: 'cjs' 64 | }, 65 | { 66 | legacy: true, 67 | freeze: false, 68 | esModule: false, 69 | file: './dist/pessimism.es.js', 70 | format: 'esm' 71 | } 72 | ] 73 | }; 74 | 75 | export default config; 76 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | export class Map {} 2 | 3 | export const make: () => Map; 4 | export const asMutable: (map: Map) => Map; 5 | export const asImmutable: (map: Map) => Map; 6 | export const get: (map: Map, key: string) => undefined | T; 7 | export const remove: (map: Map, key: string) => Map; 8 | export const set: (map: Map, key: string, value: T) => Map; 9 | export const setOptimistic: ( 10 | map: Map, 11 | key: string, 12 | value: T, 13 | optimisticKey: number 14 | ) => Map; 15 | export const clearOptimistic: (map: Map, optimisticKey: number) => Map; 16 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { 2 | make, 3 | asMutable, 4 | asImmutable, 5 | getUndefined as get, 6 | remove, 7 | set, 8 | setOptimistic, 9 | clearOptimistic 10 | } from './pessimism.js'; 11 | -------------------------------------------------------------------------------- /src/pessimism.re: -------------------------------------------------------------------------------- 1 | type keyT = string; 2 | type hashT = int; 3 | type ownerT = ref(unit); 4 | 5 | type valueT('v) = { 6 | key: keyT, 7 | value: 'v, 8 | id: int, 9 | mutable prev: option(valueT('v)), 10 | }; 11 | 12 | type nodeT('v) = 13 | | Index(t('v)) 14 | | Value(keyT, 'v, hashT) 15 | | Values(valueT('v), hashT) 16 | | Collision(array(valueT('v)), hashT) 17 | | Empty(hashT) 18 | and t('v) = { 19 | mutable bitmap: int, 20 | mutable contents: array(nodeT('v)), 21 | mutable owner: ownerT, 22 | }; 23 | 24 | /*-- Helpers -------------------------------------*/ 25 | 26 | let owner = () => ref(); 27 | let anon = owner(); 28 | let isOwner = (a: ownerT, b: ownerT) => a !== anon && a === b; 29 | 30 | let mask = (x: int, pos: int) => 1 lsl (x lsr (pos * 5) land 31); 31 | 32 | let smi = (x: int) => x lsr 1 land 0x40000000 lor (x land 0xbfffffff); 33 | 34 | let hash = (x: string): hashT => { 35 | let until = String.length(x) - 1; 36 | let h = ref(5381); 37 | for (i in 0 to until) { 38 | h := h^ lsl 5 + h^ + int_of_char(String.unsafe_get(x, i)); 39 | }; 40 | smi(h^); 41 | }; 42 | 43 | let hammingWeight = (input: int) => { 44 | let x = ref(input); 45 | x := x^ - x^ asr 1 land 0x55555555; 46 | x := x^ land 0x33333333 + x^ asr 2 land 0x33333333; 47 | x := (x^ + x^ asr 4) land 0x0f0f0f0f; 48 | x := x^ + x^ asr 8; 49 | x := x^ + x^ asr 16; 50 | x^ land 0x7f; 51 | }; 52 | 53 | let indexBit = (x: int, pos: int) => hammingWeight(x land (pos - 1)); 54 | 55 | /*-- Array helpers -------------------------------------*/ 56 | 57 | [@bs.get_index] external arrayGet: (array('a), int) => 'a = ""; 58 | [@bs.set_index] external arraySet: (array('a), int, 'a) => unit = ""; 59 | [@bs.send] external arrayCopy: array('a) => array('a) = "slice"; 60 | [@bs.send] 61 | external arrayRemove: (array('a), int, [@bs.as 1] _) => unit = "splice"; 62 | [@bs.send] 63 | external arrayAdd: (array('a), int, [@bs.as 0] _, 'a) => unit = "splice"; 64 | [@bs.send] external arrayPush: (array('a), 'a) => unit = "push"; 65 | [@bs.get] external arraySize: array('a) => int = "length"; 66 | 67 | /*-- Index helpers -------------------------------------*/ 68 | 69 | let copyIndex = (index: t('v), owner: ownerT) => 70 | !isOwner(owner, index.owner) 71 | ? {...index, contents: arrayCopy(index.contents), owner} : index; 72 | 73 | let traverseCopy = (map, code, owner) => { 74 | let rec traverse = (index, depth) => { 75 | let {bitmap, contents} = index; 76 | let pos = mask(code, depth); 77 | if (bitmap lor pos !== bitmap) { 78 | (depth, index); 79 | } else { 80 | let i = indexBit(bitmap, pos); 81 | let child = arrayGet(contents, i); 82 | switch (child) { 83 | | Index(childIndex) => 84 | let newChildIndex = copyIndex(childIndex, owner); 85 | arraySet(contents, i, Index(newChildIndex)); 86 | traverse(newChildIndex, depth + 1); 87 | | _ => (depth, index) 88 | }; 89 | }; 90 | }; 91 | 92 | traverse(map, 0); 93 | }; 94 | 95 | let rec resolveConflict = (codeA, codeB, nodeA, nodeB, depth, owner) => { 96 | let posA = mask(codeA, depth); 97 | let posB = mask(codeB, depth); 98 | let bitmap = posA lor posB; 99 | let contents = 100 | posA === posB 101 | ? [|resolveConflict(codeA, codeB, nodeA, nodeB, depth + 1, owner)|] 102 | : indexBit(bitmap, posA) !== 0 ? [|nodeB, nodeA|] : [|nodeA, nodeB|]; 103 | Index({bitmap, contents, owner}); 104 | }; 105 | 106 | let removeFromIndex = (index: t('v), pos: int, owner: ownerT) => { 107 | let {bitmap} = index; 108 | let newBitmap = bitmap lxor pos; 109 | if (newBitmap !== bitmap) { 110 | let index = copyIndex(index, owner); 111 | arrayRemove(index.contents, indexBit(bitmap, pos)); 112 | index.bitmap = newBitmap; 113 | index; 114 | } else { 115 | index; 116 | }; 117 | }; 118 | 119 | let rec clearBox = (box: valueT('a), optid: int) => 120 | switch (box, box.prev) { 121 | | ({id}, Some(prev)) when id === optid => clearBox(prev, optid) 122 | | ({id}, None) when id === optid => None 123 | | (_, Some(prev)) => Some({...box, prev: clearBox(prev, optid)}) 124 | | _ => Some(box) 125 | }; 126 | 127 | let clearOptimisticNode = (node: nodeT('v), optid: int) => 128 | switch (node) { 129 | | Empty(_) 130 | | Index(_) 131 | | Value(_) => node 132 | 133 | | Values(box, _) when box.id === 0 => node 134 | 135 | | Values(box, code) => 136 | switch (clearBox(box, optid)) { 137 | | Some(box) => Values(box, code) 138 | | None => Empty(code) 139 | } 140 | 141 | | Collision(bucket, code) => 142 | let bucketSize = arraySize(bucket); 143 | let newBucket = [||]; 144 | let i = ref(0); 145 | while (i^ < bucketSize) { 146 | let box = arrayGet(bucket, i^); 147 | if (box.id === 0) { 148 | arrayPush(newBucket, box); 149 | } else { 150 | switch (clearBox(box, optid)) { 151 | | Some(box) => arrayPush(newBucket, box) 152 | | None => () 153 | }; 154 | }; 155 | i := i^ + 1; 156 | }; 157 | 158 | arraySize(newBucket) > 0 ? Collision(newBucket, code) : Empty(code); 159 | }; 160 | 161 | let addToBucket = (bucket: array(valueT('v)), box: valueT('v)) => { 162 | let bucket = arrayCopy(bucket); 163 | let bucketSize = arraySize(bucket); 164 | let optimistic = box.id !== 0; 165 | let i = ref(0); 166 | let hasReplaced = ref(false); 167 | while (i^ < bucketSize && ! hasReplaced^) { 168 | let prev = arrayGet(bucket, i^); 169 | if (prev.key === box.key) { 170 | if (optimistic) { 171 | box.prev = Some(prev); 172 | }; 173 | arraySet(bucket, i^, box); 174 | hasReplaced := true; 175 | }; 176 | 177 | i := i^ + 1; 178 | }; 179 | 180 | if (! hasReplaced^) { 181 | arrayPush(bucket, box); 182 | }; 183 | bucket; 184 | }; 185 | 186 | let removeFromBucket = (bucket: array(valueT('v)), key: keyT) => { 187 | let bucketSize = arraySize(bucket); 188 | let newBucket = [||]; 189 | let i = ref(0); 190 | while (i^ < bucketSize) { 191 | let box = arrayGet(bucket, i^); 192 | if (box.key !== key) { 193 | arrayPush(newBucket, box); 194 | }; 195 | i := i^ + 1; 196 | }; 197 | newBucket; 198 | }; 199 | 200 | let findInBucket = (bucket: array(valueT('v)), key: keyT) => { 201 | let bucketSize = arraySize(bucket); 202 | let res = ref(Js.Undefined.empty); 203 | let i = ref(0); 204 | while (Js.Undefined.testAny(res^) && i^ < bucketSize) { 205 | let box = arrayGet(bucket, i^); 206 | if (box.key === key) { 207 | res := Js.Undefined.return(box.value); 208 | }; 209 | i := i^ + 1; 210 | }; 211 | res^; 212 | }; 213 | 214 | let rec rebuildWithStack = (stack, innerIndex, depth, code, owner) => { 215 | let pos = mask(code, depth); 216 | switch (stack) { 217 | | [index, ...rest] when innerIndex.bitmap === 0 => 218 | let index = removeFromIndex(index, pos, owner); 219 | rebuildWithStack(rest, index, depth - 1, code, owner); 220 | | [index, ...rest] => 221 | let index = copyIndex(index, owner); 222 | let i = indexBit(index.bitmap, pos); 223 | arraySet(index.contents, i, Index(innerIndex)); 224 | rebuildWithStack(rest, index, depth - 1, code, owner); 225 | | [] => innerIndex 226 | }; 227 | }; 228 | 229 | /*-- Main methods -------------------------------------*/ 230 | 231 | let make = () => {bitmap: 0, contents: [||], owner: anon}; 232 | 233 | let asMutable = (index: t('v)) => 234 | index.owner === anon ? {...index, owner: owner()} : index; 235 | 236 | let asImmutable = (index: t('v)) => { 237 | index.owner = anon; 238 | index; 239 | }; 240 | 241 | let getUndefined = (map: t('v), key: keyT): Js.Undefined.t('v) => { 242 | let code = hash(key); 243 | 244 | let rec traverse = (index, depth) => { 245 | let {bitmap, contents} = index; 246 | let pos = mask(code, depth); 247 | if (bitmap lor pos !== bitmap) { 248 | Js.Undefined.empty; 249 | } else { 250 | let child = arrayGet(contents, indexBit(bitmap, pos)); 251 | switch (child) { 252 | | Index(index) => traverse(index, depth + 1) 253 | 254 | | Collision(bucket, _) => findInBucket(bucket, key) 255 | | Value(k, v, _) when k === key => Js.Undefined.return(v) 256 | | Values(box, _) when box.key === key => Js.Undefined.return(box.value) 257 | 258 | | Value(_) 259 | | Values(_) 260 | | Empty(_) => Js.Undefined.empty 261 | }; 262 | }; 263 | }; 264 | 265 | traverse(map, 0); 266 | }; 267 | 268 | let get = (map, k) => Js.Undefined.toOption(getUndefined(map, k)); 269 | 270 | let setOptimistic = (map: t('v), key: keyT, value: 'v, id: int): t('v) => { 271 | let {owner} = map; 272 | let map = copyIndex(map, owner); 273 | 274 | let code = hash(key); 275 | let (depth, index) = traverseCopy(map, code, owner); 276 | 277 | let optimistic = id !== 0; 278 | let pos = mask(code, depth); 279 | let newBitmap = index.bitmap lor pos; 280 | let i = indexBit(newBitmap, pos); 281 | if (newBitmap !== index.bitmap) { 282 | arrayAdd(index.contents, i, Value(key, value, code)); 283 | index.bitmap = newBitmap; 284 | } else { 285 | let node = 286 | switch (arrayGet(index.contents, i)) { 287 | | Value(k, v, _) when k === key && optimistic => 288 | let prev = {key: k, value: v, id: 0, prev: None}; 289 | let next = {key, value, id, prev: Some(prev)}; 290 | Values(next, code); 291 | 292 | | Values(box, _) when box.key === key && optimistic => 293 | Values({key, value, id, prev: Some(box)}, code) 294 | 295 | | Value(k, _, _) when k === key => Value(key, value, code) 296 | | Values(box, _) when box.key === key => 297 | Values({key, value, id: 0, prev: None}, code) 298 | 299 | | Value(k, v, c) when c === code => 300 | let prev = {key: k, value: v, id: 0, prev: None}; 301 | let next = {key, value, id: 0, prev: None}; 302 | Collision([|prev, next|], code); 303 | 304 | | Values(prev, c) when c === code => 305 | let next = {key, value, id: 0, prev: None}; 306 | Collision([|prev, next|], code); 307 | 308 | | Collision(bucket, c) when c === code => 309 | let box = {key, value, id, prev: None}; 310 | Collision(addToBucket(bucket, box), code); 311 | 312 | | Empty(_) when optimistic => 313 | Values({key, value, id, prev: None}, code) 314 | | Empty(_) => Value(key, value, code) 315 | 316 | | Value(_, _, prevCode) as prev 317 | | Values(_, prevCode) as prev 318 | | Collision(_, prevCode) as prev => 319 | let next = 320 | optimistic 321 | ? Values({key, value, id, prev: None}, code) 322 | : Value(key, value, code); 323 | resolveConflict(prevCode, code, prev, next, depth + 1, owner); 324 | 325 | | Index(_) as prev => prev /* this should never happen */ 326 | }; 327 | 328 | arraySet(index.contents, i, node); 329 | }; 330 | 331 | map; 332 | }; 333 | 334 | let set = (map, k, v) => setOptimistic(map, k, v, 0); 335 | 336 | let remove = (map: t('v), key: keyT) => { 337 | let {owner} = map; 338 | let code = hash(key); 339 | 340 | let rec traverse = (stack, index, depth) => { 341 | let {bitmap, contents} = index; 342 | let pos = mask(code, depth); 343 | if (bitmap lor pos !== bitmap) { 344 | map; 345 | } else { 346 | let i = indexBit(bitmap, pos); 347 | let child = arrayGet(contents, i); 348 | switch (child) { 349 | | Index(innerIndex) => 350 | traverse([index, ...stack], innerIndex, depth + 1) 351 | 352 | | Value(k, _, _) 353 | | Values({key: k}, _) when k === key => 354 | let index = removeFromIndex(index, pos, owner); 355 | rebuildWithStack(stack, index, depth - 1, code, owner); 356 | 357 | | Collision(bucket, c) when c === code => 358 | let bucket = removeFromBucket(bucket, key); 359 | if (arraySize(bucket) === 0) { 360 | let index = removeFromIndex(index, pos, owner); 361 | rebuildWithStack(stack, index, depth - 1, code, owner); 362 | } else { 363 | let index = copyIndex(index, owner); 364 | arraySet(index.contents, i, Collision(bucket, code)); 365 | rebuildWithStack(stack, index, depth - 1, code, owner); 366 | }; 367 | 368 | | _ => map 369 | }; 370 | }; 371 | }; 372 | 373 | traverse([], map, 0); 374 | }; 375 | 376 | let rec clearOptimistic = (map: t('v), optid: int): t('v) => { 377 | let {owner} = map; 378 | let index = copyIndex(map, owner); 379 | 380 | for (x in 0 to 31) { 381 | let pos = 1 lsl x; 382 | if (pos land index.bitmap !== 0) { 383 | let i = indexBit(index.bitmap, pos); 384 | switch (clearOptimisticNode(arrayGet(index.contents, i), optid)) { 385 | | Index(innerIndex) => 386 | let newInnerIndex = clearOptimistic(innerIndex, optid); 387 | if (newInnerIndex.bitmap === 0) { 388 | arrayRemove(index.contents, i); 389 | index.bitmap = index.bitmap lxor pos; 390 | } else { 391 | arraySet(index.contents, i, Index(newInnerIndex)); 392 | }; 393 | 394 | | Empty(_) => 395 | arrayRemove(index.contents, i); 396 | index.bitmap = index.bitmap lxor pos; 397 | 398 | | node => arraySet(index.contents, i, node) 399 | }; 400 | }; 401 | }; 402 | 403 | index; 404 | }; 405 | -------------------------------------------------------------------------------- /src/pessimism.rei: -------------------------------------------------------------------------------- 1 | type keyT = string; 2 | type hashT = int; 3 | type ownerT = ref(unit); 4 | 5 | type valueT('v) = { 6 | key: keyT, 7 | value: 'v, 8 | id: int, 9 | mutable prev: option(valueT('v)), 10 | }; 11 | 12 | type nodeT('v) = 13 | | Index(t('v)) 14 | | Value(keyT, 'v, hashT) 15 | | Values(valueT('v), hashT) 16 | | Collision(array(valueT('v)), hashT) 17 | | Empty(hashT) 18 | and t('v) = { 19 | mutable bitmap: int, 20 | mutable contents: array(nodeT('v)), 21 | mutable owner: ownerT, 22 | }; 23 | 24 | let make: unit => t('v); 25 | let asMutable: t('v) => t('v); 26 | let asImmutable: t('v) => t('v); 27 | let get: (t('v), keyT) => option('v); 28 | let getUndefined: (t('v), keyT) => Js.Undefined.t('v); 29 | let remove: (t('v), keyT) => t('v); 30 | let set: (t('v), keyT, 'v) => t('v); 31 | let setOptimistic: (t('v), keyT, 'v, int) => t('v); 32 | let clearOptimistic: (t('v), int) => t('v); 33 | -------------------------------------------------------------------------------- /src/pessimism_mutable.re: -------------------------------------------------------------------------------- 1 | type k = string; 2 | 3 | type boxT('v) = { 4 | key: k, 5 | mutable id: int, 6 | mutable value: 'v, 7 | mutable prev: option(boxT('v)), 8 | }; 9 | 10 | type valueT('v) = 11 | | Raw({ 12 | key: k, 13 | mutable value: 'v, 14 | }) 15 | | Chain(boxT('v)) 16 | | Collision(array(boxT('v))); 17 | 18 | type mapT('v); 19 | type t('v) = mapT(valueT('v)); 20 | 21 | /*-- Helpers -------------------------------------*/ 22 | 23 | let smi = (x: int) => x lsr 1 land 0x40000000 lor (x land 0xbfffffff); 24 | 25 | let hash = (x: string) => { 26 | let length = String.length(x); 27 | let rec explode = (h, i) => 28 | if (i < length) { 29 | let h2 = h lsl 5 + h + int_of_char(String.unsafe_get(x, i)); 30 | explode(h2, i + 1); 31 | } else { 32 | h; 33 | }; 34 | smi(explode(5381, 0)); 35 | }; 36 | 37 | let arraySet = Js.Array.unsafe_set; 38 | let arrayGet = Js.Array.unsafe_get; 39 | 40 | let arrayRemove = (arr, index) => 41 | Js.Array.removeCountInPlace(~pos=index, ~count=1, arr) |> ignore; 42 | 43 | [@bs.new] external makeMap: unit => mapT('v) = "Map"; 44 | [@bs.send] external mapGet: (mapT('v), int) => Js.Undefined.t('v) = "get"; 45 | [@bs.send] external mapSet: (mapT('v), int, 'v) => unit = "set"; 46 | [@bs.send] external mapRem: (mapT('v), int) => unit = "delete"; 47 | [@bs.send] 48 | external mapIter: (mapT('v), ('v, int) => unit) => unit = "forEach"; 49 | 50 | /*-- Main methods -------------------------------------*/ 51 | 52 | let make = (): t('v) => makeMap(); 53 | 54 | let getUndefined = (map: t('v), k: k): Js.Undefined.t('v) => { 55 | let item = mapGet(map, hash(k)); 56 | 57 | if (Js.Undefined.testAny(item)) { 58 | Js.Undefined.empty; 59 | } else { 60 | switch (Js.Undefined.getUnsafe(item)) { 61 | | Collision(bucket) => 62 | switch (Js.Array.find(({key}) => key === k, bucket)) { 63 | | Some({value}) => Js.Undefined.return(value) 64 | | None => Js.Undefined.empty 65 | } 66 | 67 | | Raw({key, value}) when key === k => Js.Undefined.return(value) 68 | | Raw(_) => Js.Undefined.empty 69 | 70 | | Chain({key, value}) when key === k => Js.Undefined.return(value) 71 | | Chain(_) => Js.Undefined.empty 72 | }; 73 | }; 74 | }; 75 | 76 | let get = (map, k) => Js.Undefined.toOption(getUndefined(map, k)); 77 | 78 | let setOptimistic = (map: t('v), k: k, v: 'v, id: int) => { 79 | let code = hash(k); 80 | let optimistic = id !== 0; 81 | let item = mapGet(map, code); 82 | 83 | let newItem = 84 | if (Js.Undefined.testAny(item)) { 85 | optimistic 86 | ? Chain({key: k, id, value: v, prev: None}) : Raw({key: k, value: v}); 87 | } else { 88 | let item = Js.Undefined.getUnsafe(item); 89 | 90 | switch (item) { 91 | | Collision(bucket) => 92 | let index = Js.Array.findIndex(({key}) => key === k, bucket); 93 | if (index > (-1)) { 94 | let prev = arrayGet(bucket, index); 95 | let box = { 96 | key: k, 97 | value: v, 98 | id, 99 | prev: optimistic ? Some(prev) : None, 100 | }; 101 | arraySet(bucket, index, box); 102 | } else { 103 | let box = {key: k, value: v, id, prev: None}; 104 | Js.Array.push(box, bucket) |> ignore; 105 | }; 106 | 107 | item; 108 | 109 | | Chain({key}) when key === k && !optimistic => Raw({key, value: v}) 110 | 111 | | Chain({key} as prev) when key === k => 112 | let box = {key, value: v, id, prev: Some(prev)}; 113 | Chain(box); 114 | 115 | | Raw({key} as record) when key === k && !optimistic => 116 | record.value = v; 117 | item; 118 | 119 | | Raw({key, value}) when key === k => 120 | let prev = {key, value, id: 0, prev: None}; 121 | let box = {key, value: v, id, prev: Some(prev)}; 122 | Chain(box); 123 | 124 | | Chain(other) => 125 | let box = {key: k, value: v, id, prev: None}; 126 | Collision([|box, other|]); 127 | 128 | | Raw({key, value}) => 129 | let box = {key: k, value: v, id, prev: None}; 130 | let other = {key, value, id: 0, prev: None}; 131 | Collision([|box, other|]); 132 | }; 133 | }; 134 | 135 | mapSet(map, code, newItem); 136 | map; 137 | }; 138 | 139 | let set = (map, k, v) => setOptimistic(map, k, v, 0); 140 | 141 | let remove = (map: t('v), k: k): t('v) => { 142 | let code = hash(k); 143 | let item = mapGet(map, code); 144 | if (!Js.Undefined.testAny(item)) { 145 | switch (Js.Undefined.getUnsafe(item)) { 146 | | Collision(bucket) => 147 | let index = Js.Array.findIndex(({key}) => key === k, bucket); 148 | if (index > (-1)) { 149 | arrayRemove(bucket, index); 150 | }; 151 | | Chain({key}) when key === k => mapRem(map, code) 152 | | Raw({key}) when key === k => mapRem(map, code) 153 | | Chain(_) 154 | | Raw(_) => () 155 | }; 156 | }; 157 | 158 | map; 159 | }; 160 | 161 | let clearBoxOptimistic = (box: boxT('a), optid: int) => { 162 | let rec filter = (x: option(boxT('a))) => 163 | switch (x) { 164 | | Some(b) when b.id !== optid => 165 | b.prev = filter(b.prev); 166 | x; 167 | | Some({prev}) => filter(prev) 168 | | None => None 169 | }; 170 | filter(Some(box)); 171 | }; 172 | 173 | let clearOptimistic = (map: t('v), optid: int): t('v) => { 174 | mapIter(map, (node, code) => 175 | switch (node) { 176 | | Collision(bucket) => 177 | let bucket = 178 | Js.Array.reduce( 179 | (acc, box) => 180 | if (box.id !== 0) { 181 | switch (clearBoxOptimistic(box, optid)) { 182 | | Some(box) => 183 | ignore(Js.Array.push(box, acc)); 184 | acc; 185 | | None => acc 186 | }; 187 | } else { 188 | ignore(Js.Array.push(box, acc)); 189 | acc; 190 | }, 191 | [||], 192 | bucket, 193 | ); 194 | switch (bucket) { 195 | | [||] => mapRem(map, code) 196 | | [|{key, value, id: 0}|] => mapSet(map, code, Raw({key, value})) 197 | | [|box|] => mapSet(map, code, Chain(box)) 198 | | _ => mapSet(map, code, Collision(bucket)) 199 | }; 200 | 201 | | Chain(box) when box.id !== 0 => 202 | switch (clearBoxOptimistic(box, optid)) { 203 | | None => mapRem(map, code) 204 | | Some(box) => mapSet(map, code, Chain(box)) 205 | } 206 | 207 | | Chain(_) 208 | | Raw(_) => () 209 | } 210 | ); 211 | 212 | map; 213 | }; 214 | -------------------------------------------------------------------------------- /src/pessimism_mutable.rei: -------------------------------------------------------------------------------- 1 | type k = string; 2 | 3 | type boxT('v) = { 4 | key: k, 5 | mutable id: int, 6 | mutable value: 'v, 7 | mutable prev: option(boxT('v)), 8 | }; 9 | 10 | type valueT('v) = 11 | | Raw({ 12 | key: k, 13 | mutable value: 'v, 14 | }) 15 | | Chain(boxT('v)) 16 | | Collision(array(boxT('v))); 17 | 18 | type mapT('v); 19 | type t('v) = mapT(valueT('v)); 20 | 21 | let make: unit => t('v); 22 | let get: (t('v), k) => option('v); 23 | let getUndefined: (t('v), k) => Js.Undefined.t('v); 24 | let remove: (t('v), k) => t('v); 25 | let set: (t('v), k, 'v) => t('v); 26 | let setOptimistic: (t('v), k, 'v, int) => t('v); 27 | let clearOptimistic: (t('v), int) => t('v); 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "esModuleInterop": true, 5 | "noUnusedLocals": true, 6 | "rootDir": "./src", 7 | "baseUrl": ".", 8 | "outDir": "dist/cjs", 9 | "lib": ["dom", "esnext"], 10 | "jsx": "react", 11 | "declaration": false, 12 | "module": "es2015", 13 | "moduleResolution": "node", 14 | "target": "esnext", 15 | "strict": true, 16 | "noImplicitAny": false, 17 | "noUnusedParameters": true, 18 | "sourceMap": true, 19 | "pretty": true 20 | }, 21 | "include": ["src/**/*.d.ts"] 22 | } 23 | --------------------------------------------------------------------------------