@suss/datalog 0.32.0 → 0.33.0

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
@@ -48,7 +48,7 @@ second language adapter only has to emit the same facts.
48
48
  ## More
49
49
 
50
50
  - [How the engine evaluates](./DESIGN.md)
51
- - [Documentation](https://nimbuscloud-ai.github.io/suss/)
51
+ - [Documentation](https://suss.sh/)
52
52
  - [Source and issues](https://github.com/nimbuscloud-ai/suss)
53
53
 
54
54
  ![coverage](../../.github/badges/coverage-datalog.svg)
package/dist/index.cjs CHANGED
@@ -48,6 +48,99 @@ __export(index_exports, {
48
48
  });
49
49
  module.exports = __toCommonJS(index_exports);
50
50
 
51
+ // src/factIndex.ts
52
+ var emptyNode = () => ({
53
+ present: false,
54
+ tag: void 0,
55
+ next: void 0
56
+ });
57
+ var FactIndex = class {
58
+ /** How many facts are present. */
59
+ size = 0;
60
+ root = emptyNode();
61
+ /** This tuple's key here, whether or not the fact is present. */
62
+ key(tuple) {
63
+ let node = this.root;
64
+ for (let i = 0; i < tuple.length; i++) {
65
+ let next = node.next;
66
+ if (next === void 0) {
67
+ next = /* @__PURE__ */ new Map();
68
+ node.next = next;
69
+ }
70
+ const atom = tuple[i];
71
+ let child = next.get(atom);
72
+ if (child === void 0) {
73
+ child = emptyNode();
74
+ next.set(atom, child);
75
+ }
76
+ node = child;
77
+ }
78
+ return node;
79
+ }
80
+ /** The key of a fact this relation has, or undefined when it has none. */
81
+ find(tuple) {
82
+ let node = this.root;
83
+ for (let i = 0; i < tuple.length; i++) {
84
+ const child = node.next?.get(tuple[i]);
85
+ if (child === void 0) {
86
+ return void 0;
87
+ }
88
+ node = child;
89
+ }
90
+ return node.present ? node : void 0;
91
+ }
92
+ has(key) {
93
+ return key.present;
94
+ }
95
+ /** Record the fact, under the tag the caller gave it. */
96
+ put(key, tag) {
97
+ key.present = true;
98
+ key.tag = tag;
99
+ this.size += 1;
100
+ }
101
+ tagOf(key) {
102
+ return key.tag;
103
+ }
104
+ setTag(key, tag) {
105
+ key.tag = tag;
106
+ }
107
+ /** Take the fact away, and the tag with it. */
108
+ remove(key) {
109
+ key.present = false;
110
+ key.tag = void 0;
111
+ this.size -= 1;
112
+ }
113
+ /**
114
+ * Drop what is left of a removed tuple's path, up to the first node
115
+ * another fact still needs. Without this a relation that is retracted
116
+ * from over and over keeps a node per tuple it ever had.
117
+ */
118
+ prune(tuple) {
119
+ const path = [this.root];
120
+ let node = this.root;
121
+ for (let i = 0; i < tuple.length; i++) {
122
+ const child = node.next?.get(tuple[i]);
123
+ if (child === void 0) {
124
+ return;
125
+ }
126
+ node = child;
127
+ path.push(child);
128
+ }
129
+ for (let i = path.length - 1; i > 0; i--) {
130
+ const child = path[i];
131
+ if (child.present || (child.next?.size ?? 0) > 0) {
132
+ return;
133
+ }
134
+ path[i - 1].next?.delete(tuple[i - 1]);
135
+ }
136
+ }
137
+ /** Drop every fact, and whatever was kept in order to find them. */
138
+ clear() {
139
+ this.root = emptyNode();
140
+ this.size = 0;
141
+ }
142
+ };
143
+
51
144
  // src/onDemand.ts
52
145
  var adornmentKey = (adornment) => adornment.map((bound) => bound ? "b" : "f").join("");
53
146
  var anyBound = (adornment) => adornment.some(Boolean);
@@ -469,6 +562,58 @@ function formatProfile(profile) {
469
562
  return lines.join("\n");
470
563
  }
471
564
 
565
+ // src/relation.ts
566
+ function addToBucket(buckets, key, tuple) {
567
+ const bucket = buckets.get(key);
568
+ if (bucket === void 0) {
569
+ buckets.set(key, [tuple]);
570
+ } else {
571
+ bucket.push(tuple);
572
+ }
573
+ }
574
+ function bucketIn(relation, column, value) {
575
+ let buckets = relation.columns[column];
576
+ if (buckets === void 0) {
577
+ buckets = /* @__PURE__ */ new Map();
578
+ for (const tuple of relation.tuples) {
579
+ const at = tuple[column];
580
+ if (at !== void 0) {
581
+ addToBucket(buckets, at, tuple);
582
+ }
583
+ }
584
+ relation.columns[column] = buckets;
585
+ }
586
+ return buckets.get(value) ?? [];
587
+ }
588
+
589
+ // src/confidence.ts
590
+ var RANK = {
591
+ low: 0,
592
+ medium: 1,
593
+ high: 2
594
+ };
595
+ function confidenceWith(levelOf) {
596
+ return {
597
+ asserted: "high",
598
+ absent: "high",
599
+ combine(bodyTags, derivation) {
600
+ let lowest = levelOf(derivation.rule) ?? "high";
601
+ for (const tag of bodyTags) {
602
+ if (RANK[tag] < RANK[lowest]) {
603
+ lowest = tag;
604
+ }
605
+ }
606
+ return lowest;
607
+ },
608
+ merge(stored, incoming) {
609
+ return RANK[incoming] > RANK[stored] ? incoming : stored;
610
+ }
611
+ };
612
+ }
613
+ var confidence = confidenceWith(
614
+ () => void 0
615
+ );
616
+
472
617
  // src/tupleKey.ts
473
618
  var atomKey = (a) => typeof a === "number" ? `n${a}:` : `s${a.length}:${a}`;
474
619
  var tupleKey = (parts) => {
@@ -507,34 +652,6 @@ function tupleKeyParts(key) {
507
652
  return parts;
508
653
  }
509
654
 
510
- // src/confidence.ts
511
- var RANK = {
512
- low: 0,
513
- medium: 1,
514
- high: 2
515
- };
516
- function confidenceWith(levelOf) {
517
- return {
518
- asserted: "high",
519
- absent: "high",
520
- combine(bodyTags, derivation) {
521
- let lowest = levelOf(derivation.rule) ?? "high";
522
- for (const tag of bodyTags) {
523
- if (RANK[tag] < RANK[lowest]) {
524
- lowest = tag;
525
- }
526
- }
527
- return lowest;
528
- },
529
- merge(stored, incoming) {
530
- return RANK[incoming] > RANK[stored] ? incoming : stored;
531
- }
532
- };
533
- }
534
- var confidence = confidenceWith(
535
- () => void 0
536
- );
537
-
538
655
  // src/witness.ts
539
656
  var Witness = class {
540
657
  constructor(rule2, body) {
@@ -611,30 +728,6 @@ var notLit = (relation, ...terms) => ({
611
728
  var rule = (relation, terms, body, name) => name === void 0 ? { head: { relation, terms }, body } : { head: { relation, terms }, body, name };
612
729
  var literalName = (literal) => literal.negated ? `!${literal.relation}` : literal.relation;
613
730
  var ruleLabel = (r) => r.name ?? `${r.head.relation} :- ${r.body.map(literalName).join(", ")}`;
614
- var keys = /* @__PURE__ */ new WeakMap();
615
- var keyOf2 = (tuple) => {
616
- const known = keys.get(tuple);
617
- if (known !== void 0) {
618
- return known;
619
- }
620
- const key = tupleKey(tuple);
621
- keys.set(tuple, key);
622
- return key;
623
- };
624
- function tagSlots(relation) {
625
- if (relation.tags === void 0) {
626
- relation.tags = /* @__PURE__ */ new Map();
627
- }
628
- return relation.tags;
629
- }
630
- function addToBucket(index, key, tuple) {
631
- const bucket = index.get(key);
632
- if (bucket === void 0) {
633
- index.set(key, [tuple]);
634
- } else {
635
- bucket.push(tuple);
636
- }
637
- }
638
731
  var Database = class {
639
732
  store = /* @__PURE__ */ new Map();
640
733
  relation(name) {
@@ -643,14 +736,21 @@ var Database = class {
643
736
  return existing;
644
737
  }
645
738
  const created = {
646
- keys: /* @__PURE__ */ new Set(),
739
+ index: new FactIndex(),
647
740
  tuples: [],
648
- tags: void 0,
649
- indexes: /* @__PURE__ */ new Map()
741
+ columns: []
650
742
  };
651
743
  this.store.set(name, created);
652
744
  return created;
653
745
  }
746
+ /**
747
+ * The key this relation gives a tuple, whether or not the fact is
748
+ * present. Evaluation keys its ledger of derived facts by it; a
749
+ * caller outside this module has nowhere to spend one.
750
+ */
751
+ keyFor(relationName, tuple) {
752
+ return this.relation(relationName).index.key(tuple);
753
+ }
654
754
  /**
655
755
  * Add a fact, or merge a tag into one already present. A new fact
656
756
  * stores `tag` when one is given. For a fact already present,
@@ -660,14 +760,15 @@ var Database = class {
660
760
  */
661
761
  add(relationName, tuple, tag, merge) {
662
762
  const relation = this.relation(relationName);
663
- const key = keyOf2(tuple);
664
- if (relation.keys.has(key)) {
763
+ const index = relation.index;
764
+ const key = index.key(tuple);
765
+ if (index.has(key)) {
665
766
  let outcome = "unchanged";
666
767
  if (merge !== void 0) {
667
- const stored = relation.tags?.get(key);
768
+ const stored = index.tagOf(key);
668
769
  const merged = merge(stored, tag);
669
770
  if (merged !== stored) {
670
- tagSlots(relation).set(key, merged);
771
+ index.setTag(key, merged);
671
772
  outcome = "improved";
672
773
  }
673
774
  }
@@ -676,25 +777,28 @@ var Database = class {
676
777
  }
677
778
  return outcome;
678
779
  }
679
- relation.keys.add(key);
780
+ index.put(key, tag);
680
781
  relation.tuples.push(tuple);
681
- if (tag !== void 0) {
682
- tagSlots(relation).set(key, tag);
683
- }
684
- for (const [column, index] of relation.indexes) {
782
+ for (let column = 0; column < relation.columns.length; column++) {
783
+ const buckets = relation.columns[column];
685
784
  const value = tuple[column];
686
- if (value !== void 0) {
687
- addToBucket(index, value, tuple);
785
+ if (buckets !== void 0 && value !== void 0) {
786
+ addToBucket(buckets, value, tuple);
688
787
  }
689
788
  }
690
789
  return "added";
691
790
  }
692
791
  /** The tag stored for this fact, or undefined when there is none. */
693
792
  tagOf(relationName, tuple) {
694
- return this.store.get(relationName)?.tags?.get(keyOf2(tuple));
793
+ const relation = this.store.get(relationName);
794
+ if (relation === void 0) {
795
+ return void 0;
796
+ }
797
+ const key = relation.index.find(tuple);
798
+ return key === void 0 ? void 0 : relation.index.tagOf(key);
695
799
  }
696
800
  has(relationName, tuple) {
697
- return this.store.get(relationName)?.keys.has(keyOf2(tuple)) ?? false;
801
+ return this.store.get(relationName)?.index.find(tuple) !== void 0;
698
802
  }
699
803
  facts(relationName) {
700
804
  return this.store.get(relationName)?.tuples ?? [];
@@ -710,21 +814,11 @@ var Database = class {
710
814
  */
711
815
  lookup(relationName, column, value) {
712
816
  const relation = this.store.get(relationName);
713
- if (relation === void 0) {
714
- return [];
715
- }
716
- let index = relation.indexes.get(column);
717
- if (index === void 0) {
718
- index = /* @__PURE__ */ new Map();
719
- for (const tuple of relation.tuples) {
720
- const at = tuple[column];
721
- if (at !== void 0) {
722
- addToBucket(index, at, tuple);
723
- }
724
- }
725
- relation.indexes.set(column, index);
726
- }
727
- return index.get(value) ?? [];
817
+ return relation === void 0 ? [] : bucketIn(relation, column, value);
818
+ }
819
+ /** @internal The join resolves a literal's relation once, then narrows on each fixed column. */
820
+ relationOf(relationName) {
821
+ return this.store.get(relationName);
728
822
  }
729
823
  /**
730
824
  * Remove facts from a relation. Returns how many were actually there.
@@ -738,22 +832,34 @@ var Database = class {
738
832
  if (relation === void 0) {
739
833
  return 0;
740
834
  }
835
+ const index = relation.index;
741
836
  const going = /* @__PURE__ */ new Set();
837
+ const gone = [];
742
838
  for (const tuple of tuples) {
743
- const key = keyOf2(tuple);
744
- if (relation.keys.has(key)) {
839
+ const key = index.find(tuple);
840
+ if (key !== void 0 && !going.has(key)) {
745
841
  going.add(key);
842
+ gone.push(tuple);
746
843
  }
747
844
  }
748
845
  if (going.size === 0) {
749
846
  return 0;
750
847
  }
751
848
  for (const key of going) {
752
- relation.keys.delete(key);
753
- relation.tags?.delete(key);
849
+ index.remove(key);
850
+ }
851
+ if (index.size === 0) {
852
+ relation.tuples = [];
853
+ index.clear();
854
+ } else {
855
+ relation.tuples = relation.tuples.filter(
856
+ (tuple) => index.find(tuple) !== void 0
857
+ );
858
+ for (const tuple of gone) {
859
+ index.prune(tuple);
860
+ }
754
861
  }
755
- relation.tuples = relation.keys.size === 0 ? [] : relation.tuples.filter((tuple) => !going.has(keyOf2(tuple)));
756
- relation.indexes.clear();
862
+ relation.columns.length = 0;
757
863
  forgetFacts(this, relationName, going);
758
864
  return going.size;
759
865
  }
@@ -965,6 +1071,7 @@ function evaluateRule(db, deltas, r, deltaIndex, budget) {
965
1071
  }
966
1072
  var boundSource = (db, literal, bindings) => narrowedSource(db, literal, bindings) ?? db.facts(literal.relation);
967
1073
  function narrowedSource(db, literal, bindings) {
1074
+ const relation = db.relationOf(literal.relation);
968
1075
  let narrowest = null;
969
1076
  for (let column = 0; column < literal.terms.length; column++) {
970
1077
  const term = literal.terms[column];
@@ -972,7 +1079,7 @@ function narrowedSource(db, literal, bindings) {
972
1079
  if (value === void 0) {
973
1080
  continue;
974
1081
  }
975
- const bucket = db.lookup(literal.relation, column, value);
1082
+ const bucket = relation === void 0 ? [] : bucketIn(relation, column, value);
976
1083
  if (bucket.length === 0) {
977
1084
  return bucket;
978
1085
  }
@@ -1068,7 +1175,7 @@ function claimFact(db, relation, key) {
1068
1175
  state.derived.get(relation)?.delete(key);
1069
1176
  }
1070
1177
  }
1071
- function forgetFacts(db, relation, keys2) {
1178
+ function forgetFacts(db, relation, keys) {
1072
1179
  const states = evaluated.get(db);
1073
1180
  if (states === void 0) {
1074
1181
  return;
@@ -1077,7 +1184,7 @@ function forgetFacts(db, relation, keys2) {
1077
1184
  state.marks = null;
1078
1185
  const ledger = state.derived.get(relation);
1079
1186
  if (ledger !== void 0) {
1080
- for (const key of keys2) {
1187
+ for (const key of keys) {
1081
1188
  ledger.delete(key);
1082
1189
  }
1083
1190
  }
@@ -1199,7 +1306,7 @@ function runRules(db, rules, algebra, given) {
1199
1306
  ledger = /* @__PURE__ */ new Map();
1200
1307
  derived.set(relation, ledger);
1201
1308
  }
1202
- ledger.set(keyOf2(tuple), tuple);
1309
+ ledger.set(db.keyFor(relation, tuple), tuple);
1203
1310
  addedHere?.push([relation, tuple]);
1204
1311
  };
1205
1312
  const mergeStored = algebra === void 0 ? void 0 : storedMergeFor(algebra);
package/dist/index.d.cts CHANGED
@@ -1,3 +1,78 @@
1
+ /**
2
+ * Which tuples a relation has, and what identifies one of them to
3
+ * everything else. The tag stored for a fact, each rule set's ledger of
4
+ * what it derived, and a caller's claim on a fact it asserted all point
5
+ * at a fact through the key this hands out.
6
+ *
7
+ * The join asks for that key again on every candidate the rules
8
+ * consider, so what the key costs to work out sets the cost of
9
+ * evaluation. A relation finds a fact by walking a trie of its tuple's
10
+ * atoms, one map hop per column, and the node the walk lands on is the
11
+ * fact itself. Nothing is built to identify a tuple, and a membership
12
+ * test that fails stops at the first column that misses. DESIGN.md says
13
+ * why the walk is over the atoms rather than over interned integers.
14
+ */
15
+
16
+ /**
17
+ * What identifies one fact inside one relation. Opaque: a key means
18
+ * nothing to the relation that did not hand it out, and nothing outside
19
+ * this module reads its fields.
20
+ */
21
+ interface FactKey {
22
+ present: boolean;
23
+ tag: unknown;
24
+ next: Map<Atom, FactKey> | undefined;
25
+ }
26
+ /** The facts one relation has, and the tag stored against each. */
27
+ declare class FactIndex {
28
+ /** How many facts are present. */
29
+ size: number;
30
+ private root;
31
+ /** This tuple's key here, whether or not the fact is present. */
32
+ key(tuple: Tuple): FactKey;
33
+ /** The key of a fact this relation has, or undefined when it has none. */
34
+ find(tuple: Tuple): FactKey | undefined;
35
+ has(key: FactKey): boolean;
36
+ /** Record the fact, under the tag the caller gave it. */
37
+ put(key: FactKey, tag: unknown): void;
38
+ tagOf(key: FactKey): unknown;
39
+ setTag(key: FactKey, tag: unknown): void;
40
+ /** Take the fact away, and the tag with it. */
41
+ remove(key: FactKey): void;
42
+ /**
43
+ * Drop what is left of a removed tuple's path, up to the first node
44
+ * another fact still needs. Without this a relation that is retracted
45
+ * from over and over keeps a node per tuple it ever had.
46
+ */
47
+ prune(tuple: Tuple): void;
48
+ /** Drop every fact, and whatever was kept in order to find them. */
49
+ clear(): void;
50
+ }
51
+
52
+ /**
53
+ * One relation's facts: which tuples are there, and an index per column
54
+ * so a join that has a value for a column does not scan them all.
55
+ *
56
+ * A column is indexed the first time something asks for it and stays up
57
+ * to date after that, so a relation nobody joins on that way never gets
58
+ * an index. The join resolves the relation once per literal and then
59
+ * narrows on each of that literal's fixed columns, which is why finding
60
+ * a bucket is a function here rather than a method on the database.
61
+ */
62
+
63
+ interface Relation {
64
+ /** Which tuples are here, and what identifies each of them. */
65
+ index: FactIndex;
66
+ tuples: Tuple[];
67
+ /**
68
+ * Per column position, the tuples under each value at that position.
69
+ * The value is the atom itself: a Map already tells 1 from "1", so
70
+ * encoding it first would build a string out of every node id on every
71
+ * lookup and buy nothing.
72
+ */
73
+ columns: (Map<Atom, Tuple[]> | undefined)[];
74
+ }
75
+
1
76
  /**
2
77
  * Confidence levels as a tag algebra: how sure the run is of each
3
78
  * derived fact, given how sure it is of the facts and rules behind it.
@@ -354,6 +429,12 @@ type AddOutcome = "added" | "improved" | "unchanged";
354
429
  declare class Database {
355
430
  private readonly store;
356
431
  private relation;
432
+ /**
433
+ * The key this relation gives a tuple, whether or not the fact is
434
+ * present. Evaluation keys its ledger of derived facts by it; a
435
+ * caller outside this module has nowhere to spend one.
436
+ */
437
+ keyFor(relationName: string, tuple: Tuple): FactKey;
357
438
  /**
358
439
  * Add a fact, or merge a tag into one already present. A new fact
359
440
  * stores `tag` when one is given. For a fact already present,
@@ -376,6 +457,8 @@ declare class Database {
376
457
  * gets an index.
377
458
  */
378
459
  lookup(relationName: string, column: number, value: Atom): readonly Tuple[];
460
+ /** @internal The join resolves a literal's relation once, then narrows on each fixed column. */
461
+ relationOf(relationName: string): Relation | undefined;
379
462
  /**
380
463
  * Remove facts from a relation. Returns how many were actually there.
381
464
  *
package/dist/index.d.ts CHANGED
@@ -1,3 +1,78 @@
1
+ /**
2
+ * Which tuples a relation has, and what identifies one of them to
3
+ * everything else. The tag stored for a fact, each rule set's ledger of
4
+ * what it derived, and a caller's claim on a fact it asserted all point
5
+ * at a fact through the key this hands out.
6
+ *
7
+ * The join asks for that key again on every candidate the rules
8
+ * consider, so what the key costs to work out sets the cost of
9
+ * evaluation. A relation finds a fact by walking a trie of its tuple's
10
+ * atoms, one map hop per column, and the node the walk lands on is the
11
+ * fact itself. Nothing is built to identify a tuple, and a membership
12
+ * test that fails stops at the first column that misses. DESIGN.md says
13
+ * why the walk is over the atoms rather than over interned integers.
14
+ */
15
+
16
+ /**
17
+ * What identifies one fact inside one relation. Opaque: a key means
18
+ * nothing to the relation that did not hand it out, and nothing outside
19
+ * this module reads its fields.
20
+ */
21
+ interface FactKey {
22
+ present: boolean;
23
+ tag: unknown;
24
+ next: Map<Atom, FactKey> | undefined;
25
+ }
26
+ /** The facts one relation has, and the tag stored against each. */
27
+ declare class FactIndex {
28
+ /** How many facts are present. */
29
+ size: number;
30
+ private root;
31
+ /** This tuple's key here, whether or not the fact is present. */
32
+ key(tuple: Tuple): FactKey;
33
+ /** The key of a fact this relation has, or undefined when it has none. */
34
+ find(tuple: Tuple): FactKey | undefined;
35
+ has(key: FactKey): boolean;
36
+ /** Record the fact, under the tag the caller gave it. */
37
+ put(key: FactKey, tag: unknown): void;
38
+ tagOf(key: FactKey): unknown;
39
+ setTag(key: FactKey, tag: unknown): void;
40
+ /** Take the fact away, and the tag with it. */
41
+ remove(key: FactKey): void;
42
+ /**
43
+ * Drop what is left of a removed tuple's path, up to the first node
44
+ * another fact still needs. Without this a relation that is retracted
45
+ * from over and over keeps a node per tuple it ever had.
46
+ */
47
+ prune(tuple: Tuple): void;
48
+ /** Drop every fact, and whatever was kept in order to find them. */
49
+ clear(): void;
50
+ }
51
+
52
+ /**
53
+ * One relation's facts: which tuples are there, and an index per column
54
+ * so a join that has a value for a column does not scan them all.
55
+ *
56
+ * A column is indexed the first time something asks for it and stays up
57
+ * to date after that, so a relation nobody joins on that way never gets
58
+ * an index. The join resolves the relation once per literal and then
59
+ * narrows on each of that literal's fixed columns, which is why finding
60
+ * a bucket is a function here rather than a method on the database.
61
+ */
62
+
63
+ interface Relation {
64
+ /** Which tuples are here, and what identifies each of them. */
65
+ index: FactIndex;
66
+ tuples: Tuple[];
67
+ /**
68
+ * Per column position, the tuples under each value at that position.
69
+ * The value is the atom itself: a Map already tells 1 from "1", so
70
+ * encoding it first would build a string out of every node id on every
71
+ * lookup and buy nothing.
72
+ */
73
+ columns: (Map<Atom, Tuple[]> | undefined)[];
74
+ }
75
+
1
76
  /**
2
77
  * Confidence levels as a tag algebra: how sure the run is of each
3
78
  * derived fact, given how sure it is of the facts and rules behind it.
@@ -354,6 +429,12 @@ type AddOutcome = "added" | "improved" | "unchanged";
354
429
  declare class Database {
355
430
  private readonly store;
356
431
  private relation;
432
+ /**
433
+ * The key this relation gives a tuple, whether or not the fact is
434
+ * present. Evaluation keys its ledger of derived facts by it; a
435
+ * caller outside this module has nowhere to spend one.
436
+ */
437
+ keyFor(relationName: string, tuple: Tuple): FactKey;
357
438
  /**
358
439
  * Add a fact, or merge a tag into one already present. A new fact
359
440
  * stores `tag` when one is given. For a fact already present,
@@ -376,6 +457,8 @@ declare class Database {
376
457
  * gets an index.
377
458
  */
378
459
  lookup(relationName: string, column: number, value: Atom): readonly Tuple[];
460
+ /** @internal The join resolves a literal's relation once, then narrows on each fixed column. */
461
+ relationOf(relationName: string): Relation | undefined;
379
462
  /**
380
463
  * Remove facts from a relation. Returns how many were actually there.
381
464
  *
package/dist/index.js CHANGED
@@ -1,3 +1,96 @@
1
+ // src/factIndex.ts
2
+ var emptyNode = () => ({
3
+ present: false,
4
+ tag: void 0,
5
+ next: void 0
6
+ });
7
+ var FactIndex = class {
8
+ /** How many facts are present. */
9
+ size = 0;
10
+ root = emptyNode();
11
+ /** This tuple's key here, whether or not the fact is present. */
12
+ key(tuple) {
13
+ let node = this.root;
14
+ for (let i = 0; i < tuple.length; i++) {
15
+ let next = node.next;
16
+ if (next === void 0) {
17
+ next = /* @__PURE__ */ new Map();
18
+ node.next = next;
19
+ }
20
+ const atom = tuple[i];
21
+ let child = next.get(atom);
22
+ if (child === void 0) {
23
+ child = emptyNode();
24
+ next.set(atom, child);
25
+ }
26
+ node = child;
27
+ }
28
+ return node;
29
+ }
30
+ /** The key of a fact this relation has, or undefined when it has none. */
31
+ find(tuple) {
32
+ let node = this.root;
33
+ for (let i = 0; i < tuple.length; i++) {
34
+ const child = node.next?.get(tuple[i]);
35
+ if (child === void 0) {
36
+ return void 0;
37
+ }
38
+ node = child;
39
+ }
40
+ return node.present ? node : void 0;
41
+ }
42
+ has(key) {
43
+ return key.present;
44
+ }
45
+ /** Record the fact, under the tag the caller gave it. */
46
+ put(key, tag) {
47
+ key.present = true;
48
+ key.tag = tag;
49
+ this.size += 1;
50
+ }
51
+ tagOf(key) {
52
+ return key.tag;
53
+ }
54
+ setTag(key, tag) {
55
+ key.tag = tag;
56
+ }
57
+ /** Take the fact away, and the tag with it. */
58
+ remove(key) {
59
+ key.present = false;
60
+ key.tag = void 0;
61
+ this.size -= 1;
62
+ }
63
+ /**
64
+ * Drop what is left of a removed tuple's path, up to the first node
65
+ * another fact still needs. Without this a relation that is retracted
66
+ * from over and over keeps a node per tuple it ever had.
67
+ */
68
+ prune(tuple) {
69
+ const path = [this.root];
70
+ let node = this.root;
71
+ for (let i = 0; i < tuple.length; i++) {
72
+ const child = node.next?.get(tuple[i]);
73
+ if (child === void 0) {
74
+ return;
75
+ }
76
+ node = child;
77
+ path.push(child);
78
+ }
79
+ for (let i = path.length - 1; i > 0; i--) {
80
+ const child = path[i];
81
+ if (child.present || (child.next?.size ?? 0) > 0) {
82
+ return;
83
+ }
84
+ path[i - 1].next?.delete(tuple[i - 1]);
85
+ }
86
+ }
87
+ /** Drop every fact, and whatever was kept in order to find them. */
88
+ clear() {
89
+ this.root = emptyNode();
90
+ this.size = 0;
91
+ }
92
+ };
93
+
1
94
  // src/onDemand.ts
2
95
  var adornmentKey = (adornment) => adornment.map((bound) => bound ? "b" : "f").join("");
3
96
  var anyBound = (adornment) => adornment.some(Boolean);
@@ -419,6 +512,58 @@ function formatProfile(profile) {
419
512
  return lines.join("\n");
420
513
  }
421
514
 
515
+ // src/relation.ts
516
+ function addToBucket(buckets, key, tuple) {
517
+ const bucket = buckets.get(key);
518
+ if (bucket === void 0) {
519
+ buckets.set(key, [tuple]);
520
+ } else {
521
+ bucket.push(tuple);
522
+ }
523
+ }
524
+ function bucketIn(relation, column, value) {
525
+ let buckets = relation.columns[column];
526
+ if (buckets === void 0) {
527
+ buckets = /* @__PURE__ */ new Map();
528
+ for (const tuple of relation.tuples) {
529
+ const at = tuple[column];
530
+ if (at !== void 0) {
531
+ addToBucket(buckets, at, tuple);
532
+ }
533
+ }
534
+ relation.columns[column] = buckets;
535
+ }
536
+ return buckets.get(value) ?? [];
537
+ }
538
+
539
+ // src/confidence.ts
540
+ var RANK = {
541
+ low: 0,
542
+ medium: 1,
543
+ high: 2
544
+ };
545
+ function confidenceWith(levelOf) {
546
+ return {
547
+ asserted: "high",
548
+ absent: "high",
549
+ combine(bodyTags, derivation) {
550
+ let lowest = levelOf(derivation.rule) ?? "high";
551
+ for (const tag of bodyTags) {
552
+ if (RANK[tag] < RANK[lowest]) {
553
+ lowest = tag;
554
+ }
555
+ }
556
+ return lowest;
557
+ },
558
+ merge(stored, incoming) {
559
+ return RANK[incoming] > RANK[stored] ? incoming : stored;
560
+ }
561
+ };
562
+ }
563
+ var confidence = confidenceWith(
564
+ () => void 0
565
+ );
566
+
422
567
  // src/tupleKey.ts
423
568
  var atomKey = (a) => typeof a === "number" ? `n${a}:` : `s${a.length}:${a}`;
424
569
  var tupleKey = (parts) => {
@@ -457,34 +602,6 @@ function tupleKeyParts(key) {
457
602
  return parts;
458
603
  }
459
604
 
460
- // src/confidence.ts
461
- var RANK = {
462
- low: 0,
463
- medium: 1,
464
- high: 2
465
- };
466
- function confidenceWith(levelOf) {
467
- return {
468
- asserted: "high",
469
- absent: "high",
470
- combine(bodyTags, derivation) {
471
- let lowest = levelOf(derivation.rule) ?? "high";
472
- for (const tag of bodyTags) {
473
- if (RANK[tag] < RANK[lowest]) {
474
- lowest = tag;
475
- }
476
- }
477
- return lowest;
478
- },
479
- merge(stored, incoming) {
480
- return RANK[incoming] > RANK[stored] ? incoming : stored;
481
- }
482
- };
483
- }
484
- var confidence = confidenceWith(
485
- () => void 0
486
- );
487
-
488
605
  // src/witness.ts
489
606
  var Witness = class {
490
607
  constructor(rule2, body) {
@@ -561,30 +678,6 @@ var notLit = (relation, ...terms) => ({
561
678
  var rule = (relation, terms, body, name) => name === void 0 ? { head: { relation, terms }, body } : { head: { relation, terms }, body, name };
562
679
  var literalName = (literal) => literal.negated ? `!${literal.relation}` : literal.relation;
563
680
  var ruleLabel = (r) => r.name ?? `${r.head.relation} :- ${r.body.map(literalName).join(", ")}`;
564
- var keys = /* @__PURE__ */ new WeakMap();
565
- var keyOf2 = (tuple) => {
566
- const known = keys.get(tuple);
567
- if (known !== void 0) {
568
- return known;
569
- }
570
- const key = tupleKey(tuple);
571
- keys.set(tuple, key);
572
- return key;
573
- };
574
- function tagSlots(relation) {
575
- if (relation.tags === void 0) {
576
- relation.tags = /* @__PURE__ */ new Map();
577
- }
578
- return relation.tags;
579
- }
580
- function addToBucket(index, key, tuple) {
581
- const bucket = index.get(key);
582
- if (bucket === void 0) {
583
- index.set(key, [tuple]);
584
- } else {
585
- bucket.push(tuple);
586
- }
587
- }
588
681
  var Database = class {
589
682
  store = /* @__PURE__ */ new Map();
590
683
  relation(name) {
@@ -593,14 +686,21 @@ var Database = class {
593
686
  return existing;
594
687
  }
595
688
  const created = {
596
- keys: /* @__PURE__ */ new Set(),
689
+ index: new FactIndex(),
597
690
  tuples: [],
598
- tags: void 0,
599
- indexes: /* @__PURE__ */ new Map()
691
+ columns: []
600
692
  };
601
693
  this.store.set(name, created);
602
694
  return created;
603
695
  }
696
+ /**
697
+ * The key this relation gives a tuple, whether or not the fact is
698
+ * present. Evaluation keys its ledger of derived facts by it; a
699
+ * caller outside this module has nowhere to spend one.
700
+ */
701
+ keyFor(relationName, tuple) {
702
+ return this.relation(relationName).index.key(tuple);
703
+ }
604
704
  /**
605
705
  * Add a fact, or merge a tag into one already present. A new fact
606
706
  * stores `tag` when one is given. For a fact already present,
@@ -610,14 +710,15 @@ var Database = class {
610
710
  */
611
711
  add(relationName, tuple, tag, merge) {
612
712
  const relation = this.relation(relationName);
613
- const key = keyOf2(tuple);
614
- if (relation.keys.has(key)) {
713
+ const index = relation.index;
714
+ const key = index.key(tuple);
715
+ if (index.has(key)) {
615
716
  let outcome = "unchanged";
616
717
  if (merge !== void 0) {
617
- const stored = relation.tags?.get(key);
718
+ const stored = index.tagOf(key);
618
719
  const merged = merge(stored, tag);
619
720
  if (merged !== stored) {
620
- tagSlots(relation).set(key, merged);
721
+ index.setTag(key, merged);
621
722
  outcome = "improved";
622
723
  }
623
724
  }
@@ -626,25 +727,28 @@ var Database = class {
626
727
  }
627
728
  return outcome;
628
729
  }
629
- relation.keys.add(key);
730
+ index.put(key, tag);
630
731
  relation.tuples.push(tuple);
631
- if (tag !== void 0) {
632
- tagSlots(relation).set(key, tag);
633
- }
634
- for (const [column, index] of relation.indexes) {
732
+ for (let column = 0; column < relation.columns.length; column++) {
733
+ const buckets = relation.columns[column];
635
734
  const value = tuple[column];
636
- if (value !== void 0) {
637
- addToBucket(index, value, tuple);
735
+ if (buckets !== void 0 && value !== void 0) {
736
+ addToBucket(buckets, value, tuple);
638
737
  }
639
738
  }
640
739
  return "added";
641
740
  }
642
741
  /** The tag stored for this fact, or undefined when there is none. */
643
742
  tagOf(relationName, tuple) {
644
- return this.store.get(relationName)?.tags?.get(keyOf2(tuple));
743
+ const relation = this.store.get(relationName);
744
+ if (relation === void 0) {
745
+ return void 0;
746
+ }
747
+ const key = relation.index.find(tuple);
748
+ return key === void 0 ? void 0 : relation.index.tagOf(key);
645
749
  }
646
750
  has(relationName, tuple) {
647
- return this.store.get(relationName)?.keys.has(keyOf2(tuple)) ?? false;
751
+ return this.store.get(relationName)?.index.find(tuple) !== void 0;
648
752
  }
649
753
  facts(relationName) {
650
754
  return this.store.get(relationName)?.tuples ?? [];
@@ -660,21 +764,11 @@ var Database = class {
660
764
  */
661
765
  lookup(relationName, column, value) {
662
766
  const relation = this.store.get(relationName);
663
- if (relation === void 0) {
664
- return [];
665
- }
666
- let index = relation.indexes.get(column);
667
- if (index === void 0) {
668
- index = /* @__PURE__ */ new Map();
669
- for (const tuple of relation.tuples) {
670
- const at = tuple[column];
671
- if (at !== void 0) {
672
- addToBucket(index, at, tuple);
673
- }
674
- }
675
- relation.indexes.set(column, index);
676
- }
677
- return index.get(value) ?? [];
767
+ return relation === void 0 ? [] : bucketIn(relation, column, value);
768
+ }
769
+ /** @internal The join resolves a literal's relation once, then narrows on each fixed column. */
770
+ relationOf(relationName) {
771
+ return this.store.get(relationName);
678
772
  }
679
773
  /**
680
774
  * Remove facts from a relation. Returns how many were actually there.
@@ -688,22 +782,34 @@ var Database = class {
688
782
  if (relation === void 0) {
689
783
  return 0;
690
784
  }
785
+ const index = relation.index;
691
786
  const going = /* @__PURE__ */ new Set();
787
+ const gone = [];
692
788
  for (const tuple of tuples) {
693
- const key = keyOf2(tuple);
694
- if (relation.keys.has(key)) {
789
+ const key = index.find(tuple);
790
+ if (key !== void 0 && !going.has(key)) {
695
791
  going.add(key);
792
+ gone.push(tuple);
696
793
  }
697
794
  }
698
795
  if (going.size === 0) {
699
796
  return 0;
700
797
  }
701
798
  for (const key of going) {
702
- relation.keys.delete(key);
703
- relation.tags?.delete(key);
799
+ index.remove(key);
800
+ }
801
+ if (index.size === 0) {
802
+ relation.tuples = [];
803
+ index.clear();
804
+ } else {
805
+ relation.tuples = relation.tuples.filter(
806
+ (tuple) => index.find(tuple) !== void 0
807
+ );
808
+ for (const tuple of gone) {
809
+ index.prune(tuple);
810
+ }
704
811
  }
705
- relation.tuples = relation.keys.size === 0 ? [] : relation.tuples.filter((tuple) => !going.has(keyOf2(tuple)));
706
- relation.indexes.clear();
812
+ relation.columns.length = 0;
707
813
  forgetFacts(this, relationName, going);
708
814
  return going.size;
709
815
  }
@@ -915,6 +1021,7 @@ function evaluateRule(db, deltas, r, deltaIndex, budget) {
915
1021
  }
916
1022
  var boundSource = (db, literal, bindings) => narrowedSource(db, literal, bindings) ?? db.facts(literal.relation);
917
1023
  function narrowedSource(db, literal, bindings) {
1024
+ const relation = db.relationOf(literal.relation);
918
1025
  let narrowest = null;
919
1026
  for (let column = 0; column < literal.terms.length; column++) {
920
1027
  const term = literal.terms[column];
@@ -922,7 +1029,7 @@ function narrowedSource(db, literal, bindings) {
922
1029
  if (value === void 0) {
923
1030
  continue;
924
1031
  }
925
- const bucket = db.lookup(literal.relation, column, value);
1032
+ const bucket = relation === void 0 ? [] : bucketIn(relation, column, value);
926
1033
  if (bucket.length === 0) {
927
1034
  return bucket;
928
1035
  }
@@ -1018,7 +1125,7 @@ function claimFact(db, relation, key) {
1018
1125
  state.derived.get(relation)?.delete(key);
1019
1126
  }
1020
1127
  }
1021
- function forgetFacts(db, relation, keys2) {
1128
+ function forgetFacts(db, relation, keys) {
1022
1129
  const states = evaluated.get(db);
1023
1130
  if (states === void 0) {
1024
1131
  return;
@@ -1027,7 +1134,7 @@ function forgetFacts(db, relation, keys2) {
1027
1134
  state.marks = null;
1028
1135
  const ledger = state.derived.get(relation);
1029
1136
  if (ledger !== void 0) {
1030
- for (const key of keys2) {
1137
+ for (const key of keys) {
1031
1138
  ledger.delete(key);
1032
1139
  }
1033
1140
  }
@@ -1149,7 +1256,7 @@ function runRules(db, rules, algebra, given) {
1149
1256
  ledger = /* @__PURE__ */ new Map();
1150
1257
  derived.set(relation, ledger);
1151
1258
  }
1152
- ledger.set(keyOf2(tuple), tuple);
1259
+ ledger.set(db.keyFor(relation, tuple), tuple);
1153
1260
  addedHere?.push([relation, tuple]);
1154
1261
  };
1155
1262
  const mergeStored = algebra === void 0 ? void 0 : storedMergeFor(algebra);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suss/datalog",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "type": "module",
5
5
  "description": "A small semi-naive Datalog evaluator with stratified negation, the rules engine suss runs its whole-program analyses on.",
6
6
  "keywords": [