@bablr/btree 0.4.0 → 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,23 +1,50 @@
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
  };
18
32
 
33
+ const getHeight = (tree) => {
34
+ let height = 0;
35
+ let node = tree;
36
+
37
+ while (Array.isArray(node)) {
38
+ height++;
39
+ node = getValues(node)[0];
40
+ }
41
+
42
+ return height;
43
+ };
44
+
19
45
  const treeFrom = (...values) => {
20
- if (!statsReducer && values.length <= NODE_SIZE) {
46
+ validateValues(values);
47
+ if (!buildStats && values.length <= NODE_SIZE) {
21
48
  return freeze(values);
22
49
  } else {
23
50
  let tree = [];
@@ -28,18 +55,14 @@ export const buildModule = (
28
55
  }
29
56
  };
30
57
 
31
- const buildStats = (values) => {
32
- let sums = values.reduce(statsReducer, getSumsInitial());
33
- finalizeSums(sums);
34
- return sums;
35
- };
36
-
37
58
  const treeFromValues = (values) => {
38
- if (!statsReducer && values.length <= NODE_SIZE) {
59
+ validateValues(values);
60
+
61
+ if (!buildStats && values.length <= NODE_SIZE) {
39
62
  return isArray(values[0]) ? freeze([sumNodes(values), freeze(values)]) : freeze(values);
40
63
  } else {
41
64
  if (values.length <= NODE_SIZE) {
42
- if (statsReducer) {
65
+ if (buildStats) {
43
66
  return freeze([sumNodes(values), freeze(values), buildStats(values)]);
44
67
  } else {
45
68
  return freeze([sumNodes(values), freeze(values)]);
@@ -78,11 +101,11 @@ export const buildModule = (
78
101
 
79
102
  let midIndex;
80
103
 
81
- if (isLeaf) {
82
- midIndex = Math.floor(values.length / 2 + 0.01);
83
- } else {
84
- midIndex = findBalancePoint(values);
85
- }
104
+ // if (isLeaf) {
105
+ midIndex = Math.floor(values.length / 2 + 0.01);
106
+ // } else {
107
+ // midIndex = findBalancePoint(values);
108
+ // }
86
109
 
87
110
  let leftValues = values.slice(0, midIndex);
88
111
  let rightValues = values.slice(midIndex);
@@ -91,32 +114,81 @@ export const buildModule = (
91
114
  };
92
115
 
93
116
  const setValuesAt = (idx, node, value) => {
94
- const isLeaf = isLeafNode(node);
95
117
  const values = getValues(node);
96
118
 
97
119
  if (!Number.isFinite(idx)) throw new Error();
98
120
 
99
- if (!isLeaf && !isArray(value)) {
121
+ if (!value == null) {
100
122
  throw new Error();
101
123
  }
102
124
 
103
- if (isLeaf && isArray(value) && node.length) throw new Error();
104
-
105
125
  const newValues = values.slice();
106
126
  newValues[idx] = value;
107
127
  return treeFromValues(newValues);
108
128
  };
109
129
 
130
+ const concat = (first, second) => {
131
+ if (!isArray(first) || !isArray(second)) throw new Error();
132
+
133
+ let firstHeight = getHeight(first);
134
+ let secondHeight = getHeight(second);
135
+ let firstValues = getValues(first);
136
+ let secondValues = getValues(second);
137
+
138
+ if (!secondValues.length) return first;
139
+ if (!firstValues.length) return second;
140
+
141
+ if (firstHeight === secondHeight) {
142
+ if (firstValues.length + secondValues.length <= NODE_SIZE) {
143
+ return treeFromValues([...firstValues, ...secondValues]);
144
+ } else {
145
+ let { leftValues, rightValues } = splitValues([...firstValues, ...secondValues]);
146
+ return treeFromValues([treeFromValues(leftValues), treeFromValues(rightValues)]);
147
+ }
148
+ } else {
149
+ let tree = firstHeight <= secondHeight ? second : first;
150
+ let pushout = firstHeight > secondHeight ? second : first;
151
+
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);
169
+
170
+ if (firstHeight > secondHeight) {
171
+ return replaceAt(targetIndex, addAt(targetIndex + 1, tree, rightValues), leftValues);
172
+ // return addAt(targetIndex + 1, replaceAt(targetIndex, tree, leftValues), rightValues);
173
+ } else {
174
+ // TODO change order of operations like above?
175
+ return addAt(targetIndex, replaceAt(targetIndex, tree, rightValues), leftValues);
176
+ }
177
+ }
178
+ }
179
+ }
180
+ };
181
+
110
182
  const addAt = (idx, tree, value) => {
111
183
  if (idx < 0 || !Number.isFinite(idx)) throw new Error('invalid argument');
112
184
  if (!isArray(tree)) throw new Error();
113
- if (isArray(value)) throw new Error('cannot add arrays to btrees');
185
+ if (!value) throw new Error();
114
186
 
115
- let path = findPath(idx, tree);
187
+ let path = findPath(idx, tree, getHeight(tree) - getHeight(value) - 1);
116
188
 
117
- let { node, index } = path.value;
189
+ let pathIdx = path.length - 1;
190
+ let { node, index } = path[pathIdx];
118
191
 
119
- // left pushout vs right pushout?
120
192
  let pushout = value;
121
193
 
122
194
  let values = getValues(node);
@@ -128,7 +200,7 @@ export const buildModule = (
128
200
  let finiteIndex = index === Infinity ? values.length : index;
129
201
 
130
202
  if (!isFinite(finiteIndex)) throw new Error();
131
- if (values.length + getValues(pushout).length > NODE_SIZE) {
203
+ if (values.length + (pushout ? 1 : 0) > NODE_SIZE) {
132
204
  values.splice(finiteIndex, 0, pushout);
133
205
  const { leftValues, rightValues } = splitValues(values);
134
206
 
@@ -141,7 +213,7 @@ export const buildModule = (
141
213
  }
142
214
  }
143
215
 
144
- if (path.size === 1) {
216
+ if (pathIdx === 0) {
145
217
  if (pushout) {
146
218
  return treeFromValues([pushout, node]);
147
219
  } else {
@@ -150,21 +222,21 @@ export const buildModule = (
150
222
  }
151
223
 
152
224
  const poppedNode = node;
153
- path = path.pop();
154
- ({ node, index } = path.value);
225
+ pathIdx--;
226
+ ({ node, index } = path[pathIdx]);
155
227
 
156
228
  node = setValuesAt(index, node, poppedNode);
157
229
  values = getValues(node);
158
- path = path.replace({ node, index });
159
230
  }
160
231
  };
161
232
 
162
233
  const push = (tree, value) => {
163
- return addAt(getSize(tree), tree, value);
234
+ let size = getSize(tree);
235
+ return size ? addAt(size, tree, value) : treeFromValues([value]);
164
236
  };
165
237
 
166
238
  const collapses = (size) => {
167
- return size > NODE_SIZE / 2 + 0.01;
239
+ return size < NODE_SIZE / 2 - 0.01;
168
240
  };
169
241
 
170
242
  const nodeCollapses = (node) => {
@@ -172,25 +244,29 @@ export const buildModule = (
172
244
  };
173
245
 
174
246
  const nodeCanDonate = (node) => {
175
- return collapses(getValues(node).length - 1);
247
+ return !collapses(getValues(node).length - 1);
176
248
  };
177
249
 
178
- const pop = (tree) => {
179
- let path = findPath(-1, tree);
250
+ const removeAt = (idx, tree) => {
251
+ if (idx > getSize(tree)) throw new Error('Index exceeds tree bounds');
180
252
 
181
- let { node, index } = path.value;
253
+ let path = findPath(idx, tree);
182
254
 
183
- const initialValues = freeze(getValues(node).slice(0, -1));
184
- let returnValue = isLeafNode(node)
185
- ? initialValues
186
- : freeze([sumNodes(initialValues), initialValues]);
255
+ let pathIdx = path.length - 1;
256
+ let { node, index } = path[pathIdx];
257
+
258
+ const initialValues = [...getValues(node)];
259
+
260
+ initialValues.splice(index, 1);
261
+
262
+ let returnValue = !isFinite(node[0]) ? initialValues : treeFromValues(initialValues);
187
263
 
188
264
  for (;;) {
189
265
  let values = getValues(returnValue);
190
266
  let adjustSibling = null;
191
267
 
192
- if (path.size > 1 && nodeCollapses(returnValue)) {
193
- let { node: parentNode, index: parentIndex } = path.prev.value;
268
+ if (pathIdx >= 1 && nodeCollapses(returnValue)) {
269
+ let { node: parentNode, index: parentIndex } = path[pathIdx - 1];
194
270
  const prevSibling = getValues(parentNode)[parentIndex - 1];
195
271
  const nextSibling = getValues(parentNode)[parentIndex + 1];
196
272
  let targetSibling = nodeCanDonate(prevSibling)
@@ -220,15 +296,12 @@ export const buildModule = (
220
296
  }
221
297
  }
222
298
 
223
- if (path.size === 1) {
224
- if (isFinite(returnValue[0]) && getSize(returnValue) <= NODE_SIZE) {
225
- returnValue = returnValue[1].flat();
226
- }
299
+ if (pathIdx === 0) {
227
300
  return returnValue;
228
301
  }
229
302
 
230
- path = path.pop();
231
- ({ node, index } = path.value);
303
+ pathIdx--;
304
+ ({ node, index } = path[pathIdx]);
232
305
 
233
306
  values = getValues(node).slice();
234
307
 
@@ -247,10 +320,30 @@ export const buildModule = (
247
320
  }
248
321
  };
249
322
 
323
+ const pop = (tree) => {
324
+ return removeAt(-1, tree);
325
+ };
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
+
250
341
  const isValidNode = (node) => {
251
342
  if (!isArray(node)) return false;
252
343
  const values = getValues(node);
253
- return isArray(values); // && values.length <= NODE_SIZE;
344
+ if (!isArray(values) || values.length > NODE_SIZE) return false; // ;
345
+
346
+ return !node[0] || isFinite(node[0]) || ['object', 'string'].includes(typeof node[0]);
254
347
  };
255
348
 
256
349
  const assertValidNode = (node) => {
@@ -264,45 +357,46 @@ export const buildModule = (
264
357
 
265
358
  const getSums = (node) => {
266
359
  if (!isValidNode(node)) throw new Error();
267
- return node[2];
360
+ let sums = Number.isFinite(node[0]) ? node[2] : null;
361
+ return sums || (buildStats ? buildStats(getValues(node)) : null);
268
362
  };
269
363
 
270
364
  const setValues = (node, values) => {
271
- if (values.length > NODE_SIZE) throw new Error();
365
+ if (isFinite(node[0]) || buildStats) {
366
+ return treeFromValues(values);
367
+ }
272
368
 
273
- return isFinite(node[0]) || statsReducer ? treeFromValues(values) : freeze(values);
274
- };
369
+ validateValues(values);
275
370
 
276
- const isLeafNode = (node) => {
277
- return !isArray(getValues(node)[0]);
371
+ return freeze(values);
278
372
  };
279
373
 
280
374
  function* traverse(tree) {
281
- let states = emptyStack.push({ node: tree, i: 0 });
375
+ let states = [{ node: tree, i: 0 }];
282
376
 
283
377
  assertValidNode(tree);
284
378
 
285
- stack: while (states.size) {
286
- const s = states.value;
379
+ stack: while (states.length) {
380
+ const s = states[states.length - 1];
287
381
  const { node } = s;
288
- const isLeaf = isLeafNode(node);
289
382
 
290
383
  const values = getValues(node);
291
384
 
292
- if (isLeaf) {
293
- for (let i = 0; i < values.length; i++) {
294
- yield values[i];
295
- }
296
- } else {
297
- for (let { i } = s; s.i < values.length; ) {
298
- const node = values[i];
385
+ for (let { i } = s; s.i < values.length; ) {
386
+ const value = values[i];
387
+ if (isArray(value)) {
388
+ let node = value;
299
389
  assertValidNode(node);
300
- states = states.push({ node, i: 0 });
390
+ states.push({ node, i: 0 });
301
391
  i = ++s.i;
302
392
  continue stack;
393
+ } else {
394
+ yield value;
395
+ i = ++s.i;
303
396
  }
304
397
  }
305
- states = states.pop();
398
+
399
+ states.pop();
306
400
  }
307
401
  }
308
402
 
@@ -318,65 +412,103 @@ export const buildModule = (
318
412
  }
319
413
  };
320
414
 
321
- const findPath = (idx, tree) => {
415
+ const findPath = (idx, tree, depth = Infinity) => {
322
416
  if (idx == null) throw new Error();
417
+ if (tree && !isArray(tree)) throw new Error();
418
+
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
+ }
323
435
 
324
- let path = emptyStack;
436
+ return freeze(path);
437
+ }
325
438
 
326
439
  let treeSum = getSize(tree);
327
- let currentIdx = idx < 0 ? treeSum : 0;
440
+ let currentIdx = idx < 0 ? treeSum - 1 : 0;
328
441
  let direction = idx < 0 ? -1 : 1;
329
442
  let targetIdx = idx < 0 ? treeSum + idx : idx;
330
443
 
331
- let node = tree;
332
444
  stack: while (node) {
333
445
  assertValidNode(node);
334
446
 
335
- if (isLeafNode(node)) {
336
- const startIdx = idx < 0 ? currentIdx - getSize(node) : currentIdx;
337
- let index = isFinite(currentIdx) ? targetIdx - startIdx : currentIdx;
338
- if (index < 0) {
339
- index = -Infinity;
340
- } else if (index >= getSize(node)) {
341
- index = Infinity;
342
- }
343
- return path.push({ index, node });
344
- } else {
345
- const values = node[1];
346
- let candidateNode;
347
- let i;
348
-
349
- let backwards = idx < 0;
350
- const increment = backwards ? -1 : 1;
351
- for (
352
- let i = backwards ? values.length - 1 : 0;
353
- backwards ? i >= 0 : i < values.length;
354
- i += increment
355
- ) {
356
- candidateNode = values[i];
447
+ const values = getValues(node);
448
+ let candidateNode;
449
+
450
+ let backwards = idx < 0;
451
+ const increment = backwards ? -1 : 1;
452
+ for (
453
+ let i = backwards ? values.length - 1 : 0;
454
+ backwards ? i >= 0 : i < values.length;
455
+ i += increment
456
+ ) {
457
+ let value = values[i];
458
+ if (isArray(value) && path.length < depth) {
459
+ candidateNode = value;
460
+
357
461
  const sum = getSize(candidateNode);
358
- const nextCount = currentIdx + sum * direction;
359
- if (backwards ? nextCount <= targetIdx : nextCount > targetIdx || nextCount >= treeSum) {
360
- path = path.push({ index: i, node });
462
+ const nextIndex = currentIdx + sum * direction;
463
+ if (
464
+ (backwards ? nextIndex < targetIdx : nextIndex > targetIdx) ||
465
+ (backwards ? nextIndex < 0 : nextIndex >= treeSum)
466
+ ) {
467
+ path.push({ index: i, node });
361
468
  node = candidateNode;
362
469
  continue stack;
363
470
  } else {
364
471
  currentIdx += sum * direction;
365
472
  }
473
+ } else {
474
+ const sum = getSize(value);
475
+ const nextIndex = currentIdx + sum * direction;
476
+ if (!isFinite(targetIdx)) {
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);
484
+ } else if (
485
+ backwards
486
+ ? nextIndex < targetIdx || nextIndex < 0
487
+ : nextIndex > targetIdx || nextIndex >= treeSum
488
+ ) {
489
+ break;
490
+ } else {
491
+ currentIdx += direction * sum;
492
+ }
366
493
  }
367
494
  }
495
+
496
+ path.push({ index: backwards ? -Infinity : Infinity, node });
497
+
498
+ return freeze(path);
368
499
  }
369
500
 
370
501
  return null;
371
502
  };
372
503
 
373
504
  const getAt = (idx, tree) => {
374
- const v = findPath(idx, tree)?.value;
375
- 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];
376
508
  };
377
509
 
378
510
  const replaceAt = (idx, tree, value) => {
379
- let path = findPath(idx, tree);
511
+ let path = findPath(idx, tree, getHeight(tree) - getHeight(value) - 1);
380
512
 
381
513
  if (getSize(tree) < idx) {
382
514
  throw new Error('Cannot add past the end of a list');
@@ -384,16 +516,17 @@ export const buildModule = (
384
516
  return addAt(idx, tree, value);
385
517
  }
386
518
 
387
- let { node, index } = path.value;
519
+ let pathIndex = path.length - 1;
520
+ let { node, index } = path[pathIndex];
388
521
 
389
522
  let returnValue = setValuesAt(index, node, value);
390
523
 
391
524
  for (;;) {
392
- ({ node, index } = path.value);
525
+ ({ node, index } = path[pathIndex]);
393
526
 
394
- if (path.size > 1) {
395
- path = path.pop();
396
- ({ node, index } = path.value);
527
+ if (pathIndex > 0) {
528
+ pathIndex--;
529
+ ({ node, index } = path[pathIndex]);
397
530
 
398
531
  returnValue = setValuesAt(index, node, returnValue);
399
532
  } else {
@@ -414,14 +547,15 @@ export const buildModule = (
414
547
  nodeCollapses,
415
548
  nodeCanDonate,
416
549
  pop,
550
+ removeAt,
417
551
  push,
418
552
  addAt,
553
+ concat,
419
554
  isValidNode,
420
555
  assertValidNode,
421
556
  getValues,
422
557
  getSums,
423
558
  setValues,
424
- isLeafNode,
425
559
  traverse,
426
560
  getSize,
427
561
  findPath,
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,13 +14,13 @@ export const {
14
14
  nodeCanDonate,
15
15
  pop,
16
16
  push,
17
+ concat,
17
18
  addAt,
18
19
  isValidNode,
19
20
  assertValidNode,
20
21
  getValues,
21
22
  getSums,
22
23
  setValues,
23
- isLeafNode,
24
24
  traverse,
25
25
  getSize,
26
26
  findPath,
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.0",
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.1.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
  }