├── .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 |
--------------------------------------------------------------------------------