@bablr/btree 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,18 +1,19 @@
1
1
  # @bablr/btree
2
2
 
3
- Functional utilities for working with btrees such as those used in agAST.
3
+ This library offers a set of functional utilities for working with immutable btrees such as those used in agAST.
4
4
 
5
5
  ```js
6
- expect(push(['a', 'b'], 'c')).toEqual([
7
- 3,
8
- [['a'], ['b', 'c']],
9
- ]);
6
+ import * as btree from '@bablr/btree';
10
7
 
11
- expect(addAt(0, [3, [['x'], ['y', 'z']]], 'w')).toEqual([
12
- 4,
13
- [
14
- ['w', 'x'],
15
- ['y', 'z'],
16
- ],
17
- ]);
8
+ let tree = btree.fromValues([]);
9
+ tree = btree.push('a');
10
+ tree = btree.push('b');
11
+ tree = btree.push('c');
12
+ let tree3 = tree;
13
+ tree = btree.concat(tree, tree);
14
+ tree = btree.concat(tree3, tree);
15
+
16
+ btree.getSize(tree); // 9
17
+ btree.getAt(-2, tree); // 'b'
18
+ [...btree.traverse(tree)]; // ['a', 'b', 'c', 'a', 'b', 'c', 'a', 'b', 'c']
18
19
  ```
@@ -1,17 +1,31 @@
1
- import emptyStack from '@iter-tools/imm-stack';
2
-
3
1
  const { isArray } = Array;
4
2
  const { freeze } = Object;
5
3
  const { isFinite } = Number;
6
4
 
7
5
  export const defaultNodeSize = 8;
8
6
 
9
- export const buildModule = (
10
- NODE_SIZE = defaultNodeSize,
11
- statsReducer,
12
- getSumsInitial,
13
- finalizeSums,
14
- ) => {
7
+ export const deepFreeze = (object) => {
8
+ let stack = [object];
9
+ while (stack.length) {
10
+ let item = stack[stack.length - 1];
11
+ stack.pop();
12
+
13
+ for (const value of Object.values(item)) {
14
+ if (value && typeof value === 'object') {
15
+ if (
16
+ !(Array.isArray(value) || [Object.prototype, null].includes(Object.getPrototypeOf(value)))
17
+ )
18
+ throw new Error();
19
+ stack.push(value);
20
+ }
21
+ }
22
+
23
+ Object.freeze(item);
24
+ }
25
+ return object;
26
+ };
27
+
28
+ export const buildModule = (NODE_SIZE = defaultNodeSize, buildStats) => {
15
29
  const sumNodes = (nodes) => {
16
30
  return nodes.map(getSize).reduce((a, b) => a + b, 0);
17
31
  };
@@ -29,7 +43,8 @@ export const buildModule = (
29
43
  };
30
44
 
31
45
  const treeFrom = (...values) => {
32
- if (!statsReducer && values.length <= NODE_SIZE) {
46
+ validateValues(values);
47
+ if (!buildStats && values.length <= NODE_SIZE) {
33
48
  return freeze(values);
34
49
  } else {
35
50
  let tree = [];
@@ -40,19 +55,14 @@ export const buildModule = (
40
55
  }
41
56
  };
42
57
 
43
- const buildStats = (values) => {
44
- let sums = values.reduce(statsReducer, getSumsInitial());
45
- sums = finalizeSums(sums);
46
- return sums;
47
- };
48
-
49
58
  const treeFromValues = (values) => {
50
- if (values.includes(undefined)) throw new Error();
51
- if (!statsReducer && values.length <= NODE_SIZE) {
59
+ validateValues(values);
60
+
61
+ if (!buildStats && values.length <= NODE_SIZE) {
52
62
  return isArray(values[0]) ? freeze([sumNodes(values), freeze(values)]) : freeze(values);
53
63
  } else {
54
64
  if (values.length <= NODE_SIZE) {
55
- if (statsReducer) {
65
+ if (buildStats) {
56
66
  return freeze([sumNodes(values), freeze(values), buildStats(values)]);
57
67
  } else {
58
68
  return freeze([sumNodes(values), freeze(values)]);
@@ -91,11 +101,11 @@ export const buildModule = (
91
101
 
92
102
  let midIndex;
93
103
 
94
- if (isLeaf) {
95
- midIndex = Math.floor(values.length / 2 + 0.01);
96
- } else {
97
- midIndex = findBalancePoint(values);
98
- }
104
+ // if (isLeaf) {
105
+ midIndex = Math.floor(values.length / 2 + 0.01);
106
+ // } else {
107
+ // midIndex = findBalancePoint(values);
108
+ // }
99
109
 
100
110
  let leftValues = values.slice(0, midIndex);
101
111
  let rightValues = values.slice(midIndex);
@@ -125,60 +135,46 @@ export const buildModule = (
125
135
  let firstValues = getValues(first);
126
136
  let secondValues = getValues(second);
127
137
 
138
+ if (!secondValues.length) return first;
139
+ if (!firstValues.length) return second;
140
+
128
141
  if (firstHeight === secondHeight) {
129
- if (firstValues.length + secondValues.length < NODE_SIZE) {
142
+ if (firstValues.length + secondValues.length <= NODE_SIZE) {
130
143
  return treeFromValues([...firstValues, ...secondValues]);
131
144
  } else {
132
- return treeFromValues([first, second]);
145
+ let { leftValues, rightValues } = splitValues([...firstValues, ...secondValues]);
146
+ return treeFromValues([treeFromValues(leftValues), treeFromValues(rightValues)]);
133
147
  }
134
148
  } else {
135
- let targetDepth = Math.abs(firstHeight - secondHeight);
136
- let path =
137
- firstHeight > secondHeight
138
- ? findPath(Infinity, first, targetDepth)
139
- : findPath(0, second, targetDepth);
140
-
141
- let { node, index } = path.value;
142
-
149
+ let tree = firstHeight <= secondHeight ? second : first;
143
150
  let pushout = firstHeight > secondHeight ? second : first;
144
151
 
145
- let values = getValues(node);
146
-
147
- for (;;) {
148
- if (pushout) {
149
- values = values.slice();
150
-
151
- let finiteIndex = index === Infinity ? values.length : index;
152
-
153
- if (!isFinite(finiteIndex)) throw new Error();
154
- if (values.length + getValues(pushout).length > NODE_SIZE) {
155
- values.splice(finiteIndex, 0, pushout);
156
- const { leftValues, rightValues } = splitValues(values);
157
-
158
- pushout = treeFromValues(leftValues);
159
- node = treeFromValues(rightValues);
160
- } else {
161
- values.splice(finiteIndex, 0, pushout);
162
- node = setValues(node, values);
163
- pushout = null;
164
- }
165
- }
152
+ if (!nodeCollapses(pushout)) {
153
+ let targetIndex = firstHeight > secondHeight ? getSize(tree) : 0;
154
+ return addAt(targetIndex, tree, pushout);
155
+ } else {
156
+ let depth = Math.abs(firstHeight - secondHeight) - 1;
157
+ let targetIndex = firstHeight > secondHeight ? getSize(tree) - 1 : 0;
158
+ let targetPath = findPath(targetIndex, tree, depth);
159
+ let lastIndex = targetPath.length - 1;
160
+ let targetNode = getValues(targetPath[lastIndex].node)[targetPath[lastIndex].index];
161
+ let combinedValues =
162
+ firstHeight > secondHeight
163
+ ? [...getValues(targetNode), ...getValues(pushout)]
164
+ : [...getValues(pushout), ...getValues(targetNode)];
165
+ if (getValues(pushout).length + getValues(targetNode).length <= NODE_SIZE) {
166
+ return replaceAt(targetIndex, tree, treeFromValues(combinedValues));
167
+ } else {
168
+ let { leftValues, rightValues } = splitValues(combinedValues);
166
169
 
167
- if (path.size === 1) {
168
- if (pushout) {
169
- return treeFromValues([pushout, node]);
170
+ if (firstHeight > secondHeight) {
171
+ return replaceAt(targetIndex, addAt(targetIndex + 1, tree, rightValues), leftValues);
172
+ // return addAt(targetIndex + 1, replaceAt(targetIndex, tree, leftValues), rightValues);
170
173
  } else {
171
- return node;
174
+ // TODO change order of operations like above?
175
+ return addAt(targetIndex, replaceAt(targetIndex, tree, rightValues), leftValues);
172
176
  }
173
177
  }
174
-
175
- const poppedNode = node;
176
- path = path.pop();
177
- ({ node, index } = path.value);
178
-
179
- node = setValuesAt(index, node, poppedNode);
180
- values = getValues(node);
181
- path = path.replace({ node, index });
182
178
  }
183
179
  }
184
180
  };
@@ -188,9 +184,10 @@ export const buildModule = (
188
184
  if (!isArray(tree)) throw new Error();
189
185
  if (!value) throw new Error();
190
186
 
191
- let path = findPath(idx, tree);
187
+ let path = findPath(idx, tree, getHeight(tree) - getHeight(value) - 1);
192
188
 
193
- let { node, index } = path.value;
189
+ let pathIdx = path.length - 1;
190
+ let { node, index } = path[pathIdx];
194
191
 
195
192
  let pushout = value;
196
193
 
@@ -203,7 +200,7 @@ export const buildModule = (
203
200
  let finiteIndex = index === Infinity ? values.length : index;
204
201
 
205
202
  if (!isFinite(finiteIndex)) throw new Error();
206
- if (values.length + getValues(pushout).length > NODE_SIZE) {
203
+ if (values.length + (pushout ? 1 : 0) > NODE_SIZE) {
207
204
  values.splice(finiteIndex, 0, pushout);
208
205
  const { leftValues, rightValues } = splitValues(values);
209
206
 
@@ -216,7 +213,7 @@ export const buildModule = (
216
213
  }
217
214
  }
218
215
 
219
- if (path.size === 1) {
216
+ if (pathIdx === 0) {
220
217
  if (pushout) {
221
218
  return treeFromValues([pushout, node]);
222
219
  } else {
@@ -225,12 +222,11 @@ export const buildModule = (
225
222
  }
226
223
 
227
224
  const poppedNode = node;
228
- path = path.pop();
229
- ({ node, index } = path.value);
225
+ pathIdx--;
226
+ ({ node, index } = path[pathIdx]);
230
227
 
231
228
  node = setValuesAt(index, node, poppedNode);
232
229
  values = getValues(node);
233
- path = path.replace({ node, index });
234
230
  }
235
231
  };
236
232
 
@@ -240,7 +236,7 @@ export const buildModule = (
240
236
  };
241
237
 
242
238
  const collapses = (size) => {
243
- return size > NODE_SIZE / 2 + 0.01;
239
+ return size < NODE_SIZE / 2 - 0.01;
244
240
  };
245
241
 
246
242
  const nodeCollapses = (node) => {
@@ -248,13 +244,16 @@ export const buildModule = (
248
244
  };
249
245
 
250
246
  const nodeCanDonate = (node) => {
251
- return collapses(getValues(node).length - 1);
247
+ return !collapses(getValues(node).length - 1);
252
248
  };
253
249
 
254
250
  const removeAt = (idx, tree) => {
251
+ if (idx > getSize(tree)) throw new Error('Index exceeds tree bounds');
252
+
255
253
  let path = findPath(idx, tree);
256
254
 
257
- let { node, index } = path.value;
255
+ let pathIdx = path.length - 1;
256
+ let { node, index } = path[pathIdx];
258
257
 
259
258
  const initialValues = [...getValues(node)];
260
259
 
@@ -266,8 +265,8 @@ export const buildModule = (
266
265
  let values = getValues(returnValue);
267
266
  let adjustSibling = null;
268
267
 
269
- if (path.size > 1 && nodeCollapses(returnValue)) {
270
- let { node: parentNode, index: parentIndex } = path.prev.value;
268
+ if (pathIdx >= 1 && nodeCollapses(returnValue)) {
269
+ let { node: parentNode, index: parentIndex } = path[pathIdx - 1];
271
270
  const prevSibling = getValues(parentNode)[parentIndex - 1];
272
271
  const nextSibling = getValues(parentNode)[parentIndex + 1];
273
272
  let targetSibling = nodeCanDonate(prevSibling)
@@ -297,15 +296,12 @@ export const buildModule = (
297
296
  }
298
297
  }
299
298
 
300
- if (path.size === 1) {
301
- if (getSize(returnValue) <= NODE_SIZE) {
302
- returnValue = setValues(returnValue, getValues(returnValue).flat());
303
- }
299
+ if (pathIdx === 0) {
304
300
  return returnValue;
305
301
  }
306
302
 
307
- path = path.pop();
308
- ({ node, index } = path.value);
303
+ pathIdx--;
304
+ ({ node, index } = path[pathIdx]);
309
305
 
310
306
  values = getValues(node).slice();
311
307
 
@@ -328,6 +324,20 @@ export const buildModule = (
328
324
  return removeAt(-1, tree);
329
325
  };
330
326
 
327
+ const validateValues = (values) => {
328
+ if (values.length > NODE_SIZE) throw new Error();
329
+ if (!values.length) return;
330
+
331
+ let nodeIsLeaf = !isArray(values[0]);
332
+ for (let value of values) {
333
+ if (!nodeIsLeaf && nodeCollapses(value)) {
334
+ throw new Error('invalid btree: uncollapsed node');
335
+ } else if (!nodeIsLeaf && value == null) {
336
+ throw new Error('invalid btree: nil sibling');
337
+ }
338
+ }
339
+ };
340
+
331
341
  const isValidNode = (node) => {
332
342
  if (!isArray(node)) return false;
333
343
  const values = getValues(node);
@@ -347,22 +357,27 @@ export const buildModule = (
347
357
 
348
358
  const getSums = (node) => {
349
359
  if (!isValidNode(node)) throw new Error();
350
- return node[2];
360
+ let sums = Number.isFinite(node[0]) ? node[2] : null;
361
+ return sums || (buildStats ? buildStats(getValues(node)) : null);
351
362
  };
352
363
 
353
364
  const setValues = (node, values) => {
354
- if (values.length > NODE_SIZE) throw new Error();
365
+ if (isFinite(node[0]) || buildStats) {
366
+ return treeFromValues(values);
367
+ }
368
+
369
+ validateValues(values);
355
370
 
356
- return isFinite(node[0]) || statsReducer ? treeFromValues(values) : freeze(values);
371
+ return freeze(values);
357
372
  };
358
373
 
359
374
  function* traverse(tree) {
360
- let states = emptyStack.push({ node: tree, i: 0 });
375
+ let states = [{ node: tree, i: 0 }];
361
376
 
362
377
  assertValidNode(tree);
363
378
 
364
- stack: while (states.size) {
365
- const s = states.value;
379
+ stack: while (states.length) {
380
+ const s = states[states.length - 1];
366
381
  const { node } = s;
367
382
 
368
383
  const values = getValues(node);
@@ -372,7 +387,7 @@ export const buildModule = (
372
387
  if (isArray(value)) {
373
388
  let node = value;
374
389
  assertValidNode(node);
375
- states = states.push({ node, i: 0 });
390
+ states.push({ node, i: 0 });
376
391
  i = ++s.i;
377
392
  continue stack;
378
393
  } else {
@@ -381,7 +396,7 @@ export const buildModule = (
381
396
  }
382
397
  }
383
398
 
384
- states = states.pop();
399
+ states.pop();
385
400
  }
386
401
  }
387
402
 
@@ -399,15 +414,33 @@ export const buildModule = (
399
414
 
400
415
  const findPath = (idx, tree, depth = Infinity) => {
401
416
  if (idx == null) throw new Error();
417
+ if (tree && !isArray(tree)) throw new Error();
402
418
 
403
- let path = emptyStack;
419
+ let path = [];
420
+ let node = tree;
421
+
422
+ if (isArray(idx)) {
423
+ for (let seg of idx) {
424
+ let index = typeof seg !== 'object' ? seg : seg.index;
425
+ if (typeof index === 'string') throw new Error();
426
+ if (!isArray(node)) return null;
427
+ let index_ = index < 0 ? getSize(node) + index : index;
428
+ path.push({ index: index_, node });
429
+ node = getValues(node)[index_];
430
+ if (node && !isArray(node)) {
431
+ return freeze(path);
432
+ }
433
+ if (!node) return null;
434
+ }
435
+
436
+ return freeze(path);
437
+ }
404
438
 
405
439
  let treeSum = getSize(tree);
406
440
  let currentIdx = idx < 0 ? treeSum - 1 : 0;
407
441
  let direction = idx < 0 ? -1 : 1;
408
442
  let targetIdx = idx < 0 ? treeSum + idx : idx;
409
443
 
410
- let node = tree;
411
444
  stack: while (node) {
412
445
  assertValidNode(node);
413
446
 
@@ -422,17 +455,16 @@ export const buildModule = (
422
455
  i += increment
423
456
  ) {
424
457
  let value = values[i];
425
- if (isArray(value) && path.size < depth - 1) {
458
+ if (isArray(value) && path.length < depth) {
426
459
  candidateNode = value;
427
460
 
428
461
  const sum = getSize(candidateNode);
429
-
430
- const nextCount = currentIdx + sum * direction;
462
+ const nextIndex = currentIdx + sum * direction;
431
463
  if (
432
- (backwards ? nextCount <= targetIdx : nextCount > targetIdx) ||
433
- (backwards ? nextCount < 0 : nextCount >= treeSum)
464
+ (backwards ? nextIndex < targetIdx : nextIndex > targetIdx) ||
465
+ (backwards ? nextIndex < 0 : nextIndex >= treeSum)
434
466
  ) {
435
- path = path.push({ index: i, node });
467
+ path.push({ index: i, node });
436
468
  node = candidateNode;
437
469
  continue stack;
438
470
  } else {
@@ -440,35 +472,43 @@ export const buildModule = (
440
472
  }
441
473
  } else {
442
474
  const sum = getSize(value);
443
- const nextCount = currentIdx + sum * direction;
475
+ const nextIndex = currentIdx + sum * direction;
444
476
  if (!isFinite(targetIdx)) {
445
- return path.push({ index: targetIdx, node });
446
- } else if (currentIdx === targetIdx) {
447
- return path.push({ index: i, node });
477
+ path.push({ index: targetIdx, node });
478
+
479
+ return freeze(path);
480
+ } else if (backwards ? nextIndex < targetIdx : nextIndex > targetIdx) {
481
+ path.push({ index: i, node });
482
+
483
+ return freeze(path);
448
484
  } else if (
449
485
  backwards
450
- ? nextCount < targetIdx || nextCount < 0
451
- : nextCount > targetIdx || nextCount >= treeSum
486
+ ? nextIndex < targetIdx || nextIndex < 0
487
+ : nextIndex > targetIdx || nextIndex >= treeSum
452
488
  ) {
453
489
  break;
454
490
  } else {
455
- currentIdx += direction;
491
+ currentIdx += direction * sum;
456
492
  }
457
493
  }
458
494
  }
459
- return path.push({ index: backwards ? -Infinity : Infinity, node });
495
+
496
+ path.push({ index: backwards ? -Infinity : Infinity, node });
497
+
498
+ return freeze(path);
460
499
  }
461
500
 
462
501
  return null;
463
502
  };
464
503
 
465
504
  const getAt = (idx, tree) => {
466
- const v = findPath(idx, tree)?.value;
467
- return v && getValues(v.node)[v.index];
505
+ const path = findPath(idx, tree);
506
+ let seg = path && path[path.length - 1];
507
+ return seg && getValues(seg.node)[seg.index];
468
508
  };
469
509
 
470
510
  const replaceAt = (idx, tree, value) => {
471
- let path = findPath(idx, tree);
511
+ let path = findPath(idx, tree, getHeight(tree) - getHeight(value) - 1);
472
512
 
473
513
  if (getSize(tree) < idx) {
474
514
  throw new Error('Cannot add past the end of a list');
@@ -476,16 +516,17 @@ export const buildModule = (
476
516
  return addAt(idx, tree, value);
477
517
  }
478
518
 
479
- let { node, index } = path.value;
519
+ let pathIndex = path.length - 1;
520
+ let { node, index } = path[pathIndex];
480
521
 
481
522
  let returnValue = setValuesAt(index, node, value);
482
523
 
483
524
  for (;;) {
484
- ({ node, index } = path.value);
525
+ ({ node, index } = path[pathIndex]);
485
526
 
486
- if (path.size > 1) {
487
- path = path.pop();
488
- ({ node, index } = path.value);
527
+ if (pathIndex > 0) {
528
+ pathIndex--;
529
+ ({ node, index } = path[pathIndex]);
489
530
 
490
531
  returnValue = setValuesAt(index, node, returnValue);
491
532
  } else {
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { defaultNodeSize, buildModule } from './enhanceable.js';
1
+ import { defaultNodeSize, deepFreeze, buildModule } from './enhanceable.js';
2
2
 
3
- export { defaultNodeSize };
3
+ export { defaultNodeSize, deepFreeze };
4
4
 
5
5
  export const {
6
6
  btreeFrom,
@@ -14,6 +14,7 @@ export const {
14
14
  nodeCanDonate,
15
15
  pop,
16
16
  push,
17
+ concat,
17
18
  addAt,
18
19
  isValidNode,
19
20
  assertValidNode,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bablr/btree",
3
3
  "description": "Functional utilities for working with btrees such as those used in agAST",
4
- "version": "0.4.1",
4
+ "version": "0.4.2",
5
5
  "author": "Conrad Buck<conartist6@gmail.com>",
6
6
  "type": "module",
7
7
  "files": [
@@ -15,9 +15,7 @@
15
15
  "scripts": {
16
16
  "test": "mocha test/*.test.js"
17
17
  },
18
- "dependencies": {
19
- "@iter-tools/imm-stack": "1.2.0"
20
- },
18
+ "dependencies": {},
21
19
  "devDependencies": {
22
20
  "@bablr/eslint-config-base": "github:bablr-lang/eslint-config-base#c97bfa4b3663f8378e9b3e42bb5a41e685406cf9",
23
21
  "enhanced-resolve": "^5.12.0",
@@ -29,7 +27,10 @@
29
27
  "mocha": "10.7.3",
30
28
  "prettier": "^2.6.2"
31
29
  },
32
- "repository": "github:bablr-lang/btree",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/bablr-lang/btree.git"
33
+ },
33
34
  "homepage": "https://github.com/bablr-lang/btree",
34
35
  "license": "MIT"
35
36
  }