@suss/datalog 0.37.1 → 0.38.1

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/dist/index.cjs CHANGED
@@ -34,6 +34,7 @@ __export(index_exports, {
34
34
  formatProfile: () => formatProfile,
35
35
  lit: () => lit,
36
36
  notLit: () => notLit,
37
+ noteReads: () => noteReads,
37
38
  profileEvaluation: () => profileEvaluation,
38
39
  profileEvaluationAsync: () => profileEvaluationAsync,
39
40
  proofOf: () => proofOf,
@@ -50,45 +51,93 @@ __export(index_exports, {
50
51
  module.exports = __toCommonJS(index_exports);
51
52
 
52
53
  // src/factIndex.ts
53
- var emptyNode = () => ({
54
- present: false,
55
- tag: void 0,
56
- next: void 0
57
- });
54
+ var FactKey = class {
55
+ constructor(tuple) {
56
+ this.tuple = tuple;
57
+ }
58
+ tuple;
59
+ present = false;
60
+ tag = void 0;
61
+ };
62
+ var Branch = class {
63
+ /** The key of a tuple that ends at this column, as a shorter one in a relation of mixed arity does. */
64
+ here = void 0;
65
+ next = /* @__PURE__ */ new Map();
66
+ };
67
+ function sameFrom(a, b, from) {
68
+ if (a.length !== b.length) {
69
+ return false;
70
+ }
71
+ for (let at = from; at < a.length; at++) {
72
+ if (a[at] !== b[at]) {
73
+ return false;
74
+ }
75
+ }
76
+ return true;
77
+ }
78
+ function place(branch, key, depth) {
79
+ if (depth === key.tuple.length) {
80
+ branch.here = key;
81
+ } else {
82
+ branch.next.set(key.tuple[depth], key);
83
+ }
84
+ }
85
+ function soleKey(branch) {
86
+ if (branch.here !== void 0) {
87
+ return branch.next.size === 0 ? branch.here : void 0;
88
+ }
89
+ if (branch.next.size !== 1) {
90
+ return void 0;
91
+ }
92
+ const only = branch.next.values().next().value;
93
+ return only instanceof FactKey ? only : void 0;
94
+ }
58
95
  var FactIndex = class {
59
96
  /** How many facts are present. */
60
97
  size = 0;
61
- root = emptyNode();
98
+ root = new Branch();
62
99
  /** This tuple's key here, whether or not the fact is present. */
63
100
  key(tuple) {
64
- let node = this.root;
65
- for (let i = 0; i < tuple.length; i++) {
66
- let next = node.next;
67
- if (next === void 0) {
68
- next = /* @__PURE__ */ new Map();
69
- node.next = next;
101
+ let branch = this.root;
102
+ for (let depth = 0; ; depth++) {
103
+ if (depth === tuple.length) {
104
+ branch.here ??= new FactKey(tuple);
105
+ return branch.here;
70
106
  }
71
- const atom = tuple[i];
72
- let child = next.get(atom);
107
+ const atom = tuple[depth];
108
+ const child = branch.next.get(atom);
73
109
  if (child === void 0) {
74
- child = emptyNode();
75
- next.set(atom, child);
110
+ const key = new FactKey(tuple);
111
+ branch.next.set(atom, key);
112
+ return key;
76
113
  }
77
- node = child;
114
+ if (child instanceof Branch) {
115
+ branch = child;
116
+ continue;
117
+ }
118
+ if (sameFrom(child.tuple, tuple, depth + 1)) {
119
+ return child;
120
+ }
121
+ const split = new Branch();
122
+ place(split, child, depth + 1);
123
+ branch.next.set(atom, split);
124
+ branch = split;
78
125
  }
79
- return node;
80
126
  }
81
127
  /** The key of a fact this relation has, or undefined when it has none. */
82
128
  find(tuple) {
83
- let node = this.root;
84
- for (let i = 0; i < tuple.length; i++) {
85
- const child = node.next?.get(tuple[i]);
129
+ let branch = this.root;
130
+ for (let depth = 0; ; depth++) {
131
+ const child = depth === tuple.length ? branch.here : branch.next.get(tuple[depth]);
86
132
  if (child === void 0) {
87
133
  return void 0;
88
134
  }
89
- node = child;
135
+ if (child instanceof Branch) {
136
+ branch = child;
137
+ continue;
138
+ }
139
+ return child.present && sameFrom(child.tuple, tuple, depth + 1) ? child : void 0;
90
140
  }
91
- return node.present ? node : void 0;
92
141
  }
93
142
  has(key) {
94
143
  return key.present;
@@ -112,32 +161,45 @@ var FactIndex = class {
112
161
  this.size -= 1;
113
162
  }
114
163
  /**
115
- * Drop what is left of a removed tuple's path, up to the first node
116
- * another fact still needs. Without this a relation that is retracted
117
- * from over and over keeps a node per tuple it ever had.
164
+ * Drop a removed tuple's key, and any branch left with one key to
165
+ * tell apart. Without this a relation that is retracted from over and
166
+ * over keeps a key per tuple it ever had.
118
167
  */
119
168
  prune(tuple) {
120
- const path = [this.root];
121
- let node = this.root;
122
- for (let i = 0; i < tuple.length; i++) {
123
- const child = node.next?.get(tuple[i]);
169
+ const path = [];
170
+ let branch = this.root;
171
+ let depth = 0;
172
+ for (; ; depth++) {
173
+ path.push(branch);
174
+ const child = depth === tuple.length ? branch.here : branch.next.get(tuple[depth]);
124
175
  if (child === void 0) {
125
176
  return;
126
177
  }
127
- node = child;
128
- path.push(child);
178
+ if (child instanceof Branch) {
179
+ branch = child;
180
+ continue;
181
+ }
182
+ if (child.present || !sameFrom(child.tuple, tuple, depth + 1)) {
183
+ return;
184
+ }
185
+ break;
186
+ }
187
+ if (depth === tuple.length) {
188
+ branch.here = void 0;
189
+ } else {
190
+ branch.next.delete(tuple[depth]);
129
191
  }
130
- for (let i = path.length - 1; i > 0; i--) {
131
- const child = path[i];
132
- if (child.present || (child.next?.size ?? 0) > 0) {
192
+ for (let at = path.length - 1; at > 0; at--) {
193
+ const sole = soleKey(path[at]);
194
+ if (sole === void 0) {
133
195
  return;
134
196
  }
135
- path[i - 1].next?.delete(tuple[i - 1]);
197
+ path[at - 1].next.set(tuple[at - 1], sole);
136
198
  }
137
199
  }
138
200
  /** Drop every fact, and whatever was kept in order to find them. */
139
201
  clear() {
140
- this.root = emptyNode();
202
+ this.root = new Branch();
141
203
  this.size = 0;
142
204
  }
143
205
  };
@@ -574,6 +636,24 @@ function formatProfile(profile) {
574
636
  }
575
637
 
576
638
  // src/relation.ts
639
+ var isLone = (bucket) => typeof bucket[0] !== "object";
640
+ var NO_TUPLES = [];
641
+ function tuplesIn(bucket) {
642
+ if (bucket === void 0) {
643
+ return NO_TUPLES;
644
+ }
645
+ return isLone(bucket) ? [bucket] : bucket;
646
+ }
647
+ function fileUnder(buckets, key, tuple) {
648
+ const bucket = buckets.get(key);
649
+ if (bucket === void 0) {
650
+ buckets.set(key, tuple);
651
+ } else if (isLone(bucket)) {
652
+ buckets.set(key, [bucket, tuple]);
653
+ } else {
654
+ bucket.push(tuple);
655
+ }
656
+ }
577
657
  var sameTuple = (a, b) => a.length === b.length && a.every((atom, at) => atom === b[at]);
578
658
  function storedTuplesOf(relation, facts) {
579
659
  const column = relation.columns.findIndex((buckets2) => buckets2 !== void 0);
@@ -582,19 +662,24 @@ function storedTuplesOf(relation, facts) {
582
662
  return null;
583
663
  }
584
664
  const stored = facts.map(
585
- (fact) => buckets.get(fact[column])?.find((tuple) => sameTuple(tuple, fact))
665
+ (fact) => tuplesIn(buckets.get(fact[column])).find(
666
+ (tuple) => sameTuple(tuple, fact)
667
+ )
586
668
  );
587
669
  return stored.every((tuple) => tuple !== void 0) ? stored : null;
588
670
  }
589
671
  function removeFrom(buckets, key, tuple) {
590
672
  const bucket = buckets.get(key);
591
- const at = bucket?.indexOf(tuple) ?? -1;
592
- if (bucket === void 0 || at === -1) {
673
+ if (bucket === void 0) {
593
674
  return;
594
675
  }
595
- bucket.splice(at, 1);
596
- if (bucket.length === 0) {
676
+ if (isLone(bucket)) {
597
677
  buckets.delete(key);
678
+ return;
679
+ }
680
+ bucket.splice(bucket.indexOf(tuple), 1);
681
+ if (bucket.length === 1) {
682
+ buckets.set(key, bucket[0]);
598
683
  }
599
684
  }
600
685
  function removeTuples(relation, stored) {
@@ -646,7 +731,7 @@ function addToCombined(index, tuple) {
646
731
  }
647
732
  level = next;
648
733
  }
649
- addToBucket(
734
+ fileUnder(
650
735
  level,
651
736
  tuple[index.columns[last]],
652
737
  tuple
@@ -665,10 +750,10 @@ function combinedBucketIn(relation, mask, values) {
665
750
  for (const value of values) {
666
751
  level = level.get(value);
667
752
  if (level === void 0) {
668
- return [];
753
+ return NO_TUPLES;
669
754
  }
670
755
  }
671
- return level;
756
+ return tuplesIn(level);
672
757
  }
673
758
  function addToBucket(buckets, key, tuple) {
674
759
  const bucket = buckets.get(key);
@@ -678,19 +763,291 @@ function addToBucket(buckets, key, tuple) {
678
763
  bucket.push(tuple);
679
764
  }
680
765
  }
681
- function bucketIn(relation, column, value) {
766
+ function columnIndex(relation, column) {
682
767
  let buckets = relation.columns[column];
683
768
  if (buckets === void 0) {
684
769
  buckets = /* @__PURE__ */ new Map();
685
770
  for (const tuple of relation.tuples) {
686
771
  const at = tuple[column];
687
772
  if (at !== void 0) {
688
- addToBucket(buckets, at, tuple);
773
+ fileUnder(buckets, at, tuple);
689
774
  }
690
775
  }
691
776
  relation.columns[column] = buckets;
692
777
  }
693
- return buckets.get(value) ?? [];
778
+ return buckets;
779
+ }
780
+ function bucketIn(relation, column, value) {
781
+ return tuplesIn(columnIndex(relation, column).get(value));
782
+ }
783
+ function bucketSize(relation, column, value) {
784
+ const bucket = columnIndex(relation, column).get(value);
785
+ if (bucket === void 0) {
786
+ return 0;
787
+ }
788
+ return isLone(bucket) ? 1 : bucket.length;
789
+ }
790
+
791
+ // src/reads.ts
792
+ function noteIn(reads, column, value) {
793
+ if (reads.whole) {
794
+ return;
795
+ }
796
+ let values = reads.columns[column];
797
+ if (values === void 0) {
798
+ values = /* @__PURE__ */ new Set();
799
+ reads.columns[column] = values;
800
+ }
801
+ values.add(value);
802
+ }
803
+ var ReadLog = class {
804
+ constructor(skipped) {
805
+ this.skipped = skipped;
806
+ }
807
+ skipped;
808
+ /** Null for a relation the caller said to leave out. */
809
+ byRelation = /* @__PURE__ */ new Map();
810
+ lookedUp(relation, column, value) {
811
+ const reads = this.readsOf(relation);
812
+ if (reads !== null) {
813
+ noteIn(reads, column, value);
814
+ }
815
+ }
816
+ readWhole(relation) {
817
+ const reads = this.readsOf(relation);
818
+ if (reads !== null) {
819
+ reads.whole = true;
820
+ }
821
+ }
822
+ /** Note everything an inner log noted, for an answer cached inside another. */
823
+ absorb(inner) {
824
+ for (const [relation, reads] of inner.byRelation) {
825
+ if (reads === null) {
826
+ continue;
827
+ }
828
+ if (reads.whole) {
829
+ this.readWhole(relation);
830
+ continue;
831
+ }
832
+ reads.columns.forEach((values, column) => {
833
+ for (const value of values ?? []) {
834
+ this.lookedUp(relation, column, value);
835
+ }
836
+ });
837
+ }
838
+ }
839
+ /**
840
+ * The reads with what each one finds in `db` now. One question looks
841
+ * up the same values in many columns, so each value is kept once, and
842
+ * each column keeps a bit per value saying whether it was looked up
843
+ * there.
844
+ */
845
+ counted(db) {
846
+ const names = [];
847
+ const groups = [];
848
+ const columns = [];
849
+ const lookedUp = [];
850
+ const positions = /* @__PURE__ */ new Map();
851
+ const values = [];
852
+ const addGroup = (column, seen) => {
853
+ groups.push(names.length - 1);
854
+ columns.push(column);
855
+ lookedUp.push(seen);
856
+ for (const value of seen) {
857
+ if (!positions.has(value)) {
858
+ positions.set(value, values.length);
859
+ values.push(value);
860
+ }
861
+ }
862
+ };
863
+ for (const [relation, reads] of this.byRelation) {
864
+ if (reads === null) {
865
+ continue;
866
+ }
867
+ names.push(relation);
868
+ if (reads.whole) {
869
+ addGroup(WHOLE, NONE);
870
+ continue;
871
+ }
872
+ reads.columns.forEach((seen, column) => {
873
+ if (seen !== void 0) {
874
+ addGroup(column, seen);
875
+ }
876
+ });
877
+ }
878
+ const words = Math.ceil(values.length / 32);
879
+ const bits = new Uint32Array(groups.length * words);
880
+ let found = 0;
881
+ lookedUp.forEach((seen, group) => {
882
+ const relation = db.relationOf(names[groups[group]]);
883
+ const column = columns[group];
884
+ if (column === WHOLE) {
885
+ found += relation?.tuples.length ?? 0;
886
+ }
887
+ for (const value of seen) {
888
+ const at = positions.get(value);
889
+ bits[group * words + (at >>> 5)] |= 1 << (at & 31);
890
+ found += relation === void 0 ? 0 : bucketSize(relation, column, value);
891
+ }
892
+ });
893
+ return new ReadsNoted(
894
+ db,
895
+ this.skipped,
896
+ { names, groups, columns, values, bits },
897
+ found
898
+ );
899
+ }
900
+ /** @internal Null for a relation the caller said to leave out. */
901
+ readsOf(relation) {
902
+ let reads = this.byRelation.get(relation);
903
+ if (reads === void 0) {
904
+ reads = this.skipped.has(relation) ? null : { whole: false, columns: [] };
905
+ this.byRelation.set(relation, reads);
906
+ }
907
+ return reads;
908
+ }
909
+ };
910
+ var WHOLE = -1;
911
+ var NONE = /* @__PURE__ */ new Set();
912
+ var ReadsNoted = class {
913
+ constructor(db, skipped, lookups, found) {
914
+ this.skipped = skipped;
915
+ this.found = found;
916
+ this.names = lookups.names;
917
+ this.groups = lookups.groups;
918
+ this.columns = lookups.columns;
919
+ this.values = lookups.values;
920
+ this.bits = lookups.bits;
921
+ this.words = Math.ceil(lookups.values.length / 32);
922
+ this.relations = lookups.names.map((name) => db.relationOf(name));
923
+ this.removed = this.sumOver(db, removedFrom);
924
+ this.facts = this.sumOver(db, factsIn);
925
+ }
926
+ skipped;
927
+ found;
928
+ names;
929
+ groups;
930
+ columns;
931
+ values;
932
+ bits;
933
+ words;
934
+ removed;
935
+ /** The facts in every relation read, when the counts last matched. */
936
+ facts;
937
+ /** Each relation read, found by name the first time a check needs it. */
938
+ relations;
939
+ /**
940
+ * Whether every read would find what it found then. A fact left in a
941
+ * skipped relation would join a question asked now without anything
942
+ * here having looked it up, so one there makes the answer no. The
943
+ * counts are taken again only when a relation read has grown.
944
+ */
945
+ unchangedIn(db) {
946
+ if (!allEmpty(db, this.skipped)) {
947
+ return false;
948
+ }
949
+ if (this.sumOver(db, removedFrom) !== this.removed) {
950
+ return false;
951
+ }
952
+ const facts = this.sumOver(db, factsIn);
953
+ if (facts === this.facts) {
954
+ return true;
955
+ }
956
+ if (this.countIn(db, this.found) !== this.found) {
957
+ return false;
958
+ }
959
+ this.facts = facts;
960
+ return true;
961
+ }
962
+ relationAt(db, at) {
963
+ let relation = this.relations[at];
964
+ if (relation === void 0) {
965
+ relation = db.relationOf(this.names[at]);
966
+ this.relations[at] = relation;
967
+ }
968
+ return relation;
969
+ }
970
+ sumOver(db, measure) {
971
+ let sum = 0;
972
+ for (let at = 0; at < this.names.length; at++) {
973
+ const relation = this.relationAt(db, at);
974
+ sum += relation === void 0 ? 0 : measure(relation);
975
+ }
976
+ return sum;
977
+ }
978
+ /**
979
+ * The facts the reads find, stopping once past `limit`. With nothing
980
+ * retracted every count only grows, so passing the old total already
981
+ * says that one of them grew.
982
+ */
983
+ countIn(db, limit) {
984
+ let found = 0;
985
+ for (let group = 0; group < this.groups.length && found <= limit; group++) {
986
+ const relation = this.relationAt(db, this.groups[group]);
987
+ const column = this.columns[group];
988
+ if (column === WHOLE) {
989
+ found += relation?.tuples.length ?? 0;
990
+ } else if (relation !== void 0) {
991
+ found += this.countGroup(relation, group, column);
992
+ }
993
+ }
994
+ return found;
995
+ }
996
+ countGroup(relation, group, column) {
997
+ let found = 0;
998
+ const first = group * this.words;
999
+ for (let word = 0; word < this.words; word++) {
1000
+ let left = this.bits[first + word];
1001
+ while (left !== 0) {
1002
+ const bit = 31 - Math.clz32(left & -left);
1003
+ left &= left - 1;
1004
+ const value = this.values[word * 32 + bit];
1005
+ found += bucketSize(relation, column, value);
1006
+ }
1007
+ }
1008
+ return found;
1009
+ }
1010
+ };
1011
+ var removedFrom = (relation) => relation.removed;
1012
+ var factsIn = (relation) => relation.tuples.length;
1013
+ var skippedIn = /* @__PURE__ */ new WeakMap();
1014
+ function allEmpty(db, skipped) {
1015
+ let bySet = skippedIn.get(db);
1016
+ if (bySet === void 0) {
1017
+ bySet = /* @__PURE__ */ new WeakMap();
1018
+ skippedIn.set(db, bySet);
1019
+ }
1020
+ let known = bySet.get(skipped);
1021
+ if (known === void 0) {
1022
+ known = { found: [], missing: [...skipped] };
1023
+ bySet.set(skipped, known);
1024
+ }
1025
+ if (known.missing.length > 0) {
1026
+ const stillMissing = [];
1027
+ for (const name of known.missing) {
1028
+ const relation = db.relationOf(name);
1029
+ if (relation === void 0) {
1030
+ stillMissing.push(name);
1031
+ } else {
1032
+ known.found.push(relation);
1033
+ }
1034
+ }
1035
+ known.missing = stillMissing;
1036
+ }
1037
+ return known.found.every((relation) => relation.tuples.length === 0);
1038
+ }
1039
+ function noteReads(db, skipped, work) {
1040
+ const outer = db.readLog;
1041
+ const log = new ReadLog(skipped);
1042
+ const emptyAtStart = allEmpty(db, skipped);
1043
+ db.readLog = log;
1044
+ try {
1045
+ const value = work();
1046
+ return { value, reads: emptyAtStart ? log.counted(db) : null };
1047
+ } finally {
1048
+ db.readLog = outer;
1049
+ outer?.absorb(log);
1050
+ }
694
1051
  }
695
1052
 
696
1053
  // src/stratum.ts
@@ -716,7 +1073,7 @@ function planStratum(rules) {
716
1073
  }
717
1074
  return { rules, derived, readers, marked: new Uint8Array(rules.length) };
718
1075
  }
719
- var NONE = [];
1076
+ var NONE2 = [];
720
1077
  function rulesReading(stratum, seed, derivedOnly) {
721
1078
  const lists = [];
722
1079
  for (const [relation, tuples] of seed) {
@@ -732,7 +1089,7 @@ function rulesReading(stratum, seed, derivedOnly) {
732
1089
  }
733
1090
  }
734
1091
  if (lists.length === 0) {
735
- return NONE;
1092
+ return NONE2;
736
1093
  }
737
1094
  if (lists.length === 1) {
738
1095
  return lists[0];
@@ -902,6 +1259,8 @@ var ruleLabel = (r) => r.name ?? `${r.head.relation} :- ${r.body.map(literalName
902
1259
  var FEW_REMOVED = 16;
903
1260
  var Database = class {
904
1261
  store = /* @__PURE__ */ new Map();
1262
+ /** @internal Where reads are noted while `noteReads` runs. */
1263
+ readLog = null;
905
1264
  relation(name) {
906
1265
  const existing = this.store.get(name);
907
1266
  if (existing !== void 0) {
@@ -911,7 +1270,8 @@ var Database = class {
911
1270
  index: new FactIndex(),
912
1271
  tuples: [],
913
1272
  columns: [],
914
- combined: /* @__PURE__ */ new Map()
1273
+ combined: /* @__PURE__ */ new Map(),
1274
+ removed: 0
915
1275
  };
916
1276
  this.store.set(name, created);
917
1277
  return created;
@@ -956,7 +1316,7 @@ var Database = class {
956
1316
  const buckets = relation.columns[column];
957
1317
  const value = tuple[column];
958
1318
  if (buckets !== void 0 && value !== void 0) {
959
- addToBucket(buckets, value, tuple);
1319
+ fileUnder(buckets, value, tuple);
960
1320
  }
961
1321
  }
962
1322
  for (const combined of relation.combined.values()) {
@@ -966,6 +1326,7 @@ var Database = class {
966
1326
  }
967
1327
  /** The tag stored for this fact, or undefined when there is none. */
968
1328
  tagOf(relationName, tuple) {
1329
+ this.noteTupleRead(relationName, tuple);
969
1330
  const relation = this.store.get(relationName);
970
1331
  if (relation === void 0) {
971
1332
  return void 0;
@@ -974,9 +1335,20 @@ var Database = class {
974
1335
  return key === void 0 ? void 0 : relation.index.tagOf(key);
975
1336
  }
976
1337
  has(relationName, tuple) {
1338
+ this.noteTupleRead(relationName, tuple);
977
1339
  return this.store.get(relationName)?.index.find(tuple) !== void 0;
978
1340
  }
1341
+ /** A fact matching the whole tuple agrees on its first column, so that lookup is enough to note. */
1342
+ noteTupleRead(relationName, tuple) {
1343
+ const first = tuple[0];
1344
+ if (first === void 0) {
1345
+ this.readLog?.readWhole(relationName);
1346
+ return;
1347
+ }
1348
+ this.readLog?.lookedUp(relationName, 0, first);
1349
+ }
979
1350
  facts(relationName) {
1351
+ this.readLog?.readWhole(relationName);
980
1352
  return this.store.get(relationName)?.tuples ?? [];
981
1353
  }
982
1354
  /**
@@ -989,6 +1361,7 @@ var Database = class {
989
1361
  * gets an index.
990
1362
  */
991
1363
  lookup(relationName, column, value) {
1364
+ this.readLog?.lookedUp(relationName, column, value);
992
1365
  const relation = this.store.get(relationName);
993
1366
  return relation === void 0 ? [] : bucketIn(relation, column, value);
994
1367
  }
@@ -1024,6 +1397,7 @@ var Database = class {
1024
1397
  for (const key of going) {
1025
1398
  index.remove(key);
1026
1399
  }
1400
+ relation.removed += going.size;
1027
1401
  if (index.size === 0) {
1028
1402
  relation.tuples = [];
1029
1403
  index.clear();
@@ -1059,6 +1433,7 @@ var Database = class {
1059
1433
  return 0;
1060
1434
  }
1061
1435
  const removed = relation.tuples.length;
1436
+ relation.removed += removed;
1062
1437
  relation.tuples = [];
1063
1438
  relation.index.clear();
1064
1439
  relation.columns.length = 0;
@@ -1067,12 +1442,15 @@ var Database = class {
1067
1442
  return removed;
1068
1443
  }
1069
1444
  size(relationName) {
1070
- return this.store.get(relationName)?.tuples.length ?? 0;
1445
+ this.readLog?.readWhole(relationName);
1446
+ return sizeOf(this, relationName);
1071
1447
  }
1072
1448
  relationNames() {
1073
1449
  return [...this.store.keys()];
1074
1450
  }
1075
1451
  };
1452
+ var tuplesOf = (db, relation) => db.relationOf(relation)?.tuples ?? [];
1453
+ var sizeOf = (db, relation) => db.relationOf(relation)?.tuples.length ?? 0;
1076
1454
  function rulesDeriving(rules, relations) {
1077
1455
  const wanted = new Set(relations);
1078
1456
  const pending = [...relations];
@@ -1268,6 +1646,7 @@ function evaluateRule(db, deltas, r, deltaIndex, budget) {
1268
1646
  }
1269
1647
  const byValue = newFactsByColumn(rows, start.column, budget);
1270
1648
  const first = body[start.index];
1649
+ noteSharedLookups(db, first, start, rows);
1271
1650
  for (const tuple of start.source) {
1272
1651
  budget.examined++;
1273
1652
  if (budget.examined > budget.limit) {
@@ -1299,7 +1678,7 @@ function smallerStart(db, body, deltaAt, rows) {
1299
1678
  if (column === -1) {
1300
1679
  continue;
1301
1680
  }
1302
- const source = narrowedSource(db, literal, null) ?? db.facts(literal.relation);
1681
+ const source = narrowedSource(db, literal, null) ?? tuplesOf(db, literal.relation);
1303
1682
  const worthIt = indexed?.has(column) ? source.length < rows.length : source.length * 2 < rows.length;
1304
1683
  if (worthIt && (best === null || source.length < best.source.length)) {
1305
1684
  best = {
@@ -1357,6 +1736,7 @@ function nextLiteral(db, body, taken, bindings) {
1357
1736
  continue;
1358
1737
  }
1359
1738
  if (source.length === 0) {
1739
+ noteLookup(db, literal, bindings);
1360
1740
  return -1;
1361
1741
  }
1362
1742
  if (narrowest === null || source.length < narrowest.length) {
@@ -1365,6 +1745,7 @@ function nextLiteral(db, body, taken, bindings) {
1365
1745
  }
1366
1746
  }
1367
1747
  if (narrowest !== null) {
1748
+ noteLookup(db, body[pick], bindings);
1368
1749
  nextSource = narrowedFurther(db, body[pick], bindings, narrowest);
1369
1750
  return nextSource.length === 0 ? -1 : pick;
1370
1751
  }
@@ -1376,6 +1757,59 @@ function nextLiteral(db, body, taken, bindings) {
1376
1757
  }
1377
1758
  return body.findIndex((_, index) => !(taken & 1 << index));
1378
1759
  }
1760
+ function noteLookup(db, literal, bindings) {
1761
+ const reads = db.readLog?.readsOf(literal.relation) ?? null;
1762
+ if (reads === null) {
1763
+ return;
1764
+ }
1765
+ for (let column2 = 0; column2 < literal.terms.length; column2++) {
1766
+ const term2 = literal.terms[column2];
1767
+ if (term2.type === "variable") {
1768
+ const value = boundValue(bindings, term2.name);
1769
+ if (value !== void 0) {
1770
+ noteIn(reads, column2, value);
1771
+ return;
1772
+ }
1773
+ }
1774
+ }
1775
+ const column = fewestFactsUnderConstant(db, literal);
1776
+ const term = literal.terms[column];
1777
+ if (term?.type === "constant") {
1778
+ noteIn(reads, column, term.value);
1779
+ }
1780
+ }
1781
+ function fewestFactsUnderConstant(db, literal) {
1782
+ const relation = db.relationOf(literal.relation);
1783
+ let best = -1;
1784
+ let bestFacts = Number.POSITIVE_INFINITY;
1785
+ for (let column = 0; column < literal.terms.length; column++) {
1786
+ const term = literal.terms[column];
1787
+ if (term.type !== "constant") {
1788
+ continue;
1789
+ }
1790
+ const facts = relation === void 0 ? 0 : bucketSize(relation, column, term.value);
1791
+ if (facts < bestFacts) {
1792
+ best = column;
1793
+ bestFacts = facts;
1794
+ }
1795
+ }
1796
+ return best;
1797
+ }
1798
+ function noteSharedLookups(db, first, start, rows) {
1799
+ const reads = db.readLog?.readsOf(first.relation) ?? null;
1800
+ if (reads === null) {
1801
+ return;
1802
+ }
1803
+ const column = first.terms.findIndex(
1804
+ (term) => term.type === "variable" && term.name === start.variable
1805
+ );
1806
+ for (const row of rows) {
1807
+ const value = row[start.column];
1808
+ if (value !== void 0) {
1809
+ noteIn(reads, column, value);
1810
+ }
1811
+ }
1812
+ }
1379
1813
  function narrowedSource(db, literal, bindings) {
1380
1814
  const relation = db.relationOf(literal.relation);
1381
1815
  let narrowest = null;
@@ -1427,7 +1861,11 @@ function constantColumnsOf(literal) {
1427
1861
  values.push(term.value);
1428
1862
  }
1429
1863
  });
1430
- known = columns.length === 0 ? null : { columns, values };
1864
+ known = columns.length === 0 ? null : {
1865
+ columns,
1866
+ values,
1867
+ bucketKey: deltaBucketKey(literal.relation, columns[0])
1868
+ };
1431
1869
  constantColumns.set(literal, known);
1432
1870
  }
1433
1871
  return known;
@@ -1440,7 +1878,7 @@ function hasNoNewRows(deltas, literal) {
1440
1878
  if (rows.length === 0 || constants === null) {
1441
1879
  return rows.length === 0;
1442
1880
  }
1443
- const buckets = deltaBuckets.get(deltas)?.get(deltaBucketKey(literal.relation, constants.columns[0]));
1881
+ const buckets = deltaBuckets.get(deltas)?.get(constants.bucketKey);
1444
1882
  return buckets !== void 0 && !buckets.has(constants.values[0]);
1445
1883
  }
1446
1884
  function deltaRows(deltas, literal, budget) {
@@ -1455,7 +1893,7 @@ function deltaRows(deltas, literal, budget) {
1455
1893
  deltaBuckets.set(deltas, byColumn);
1456
1894
  }
1457
1895
  const column = constants.columns[0];
1458
- const key = deltaBucketKey(literal.relation, column);
1896
+ const key = constants.bucketKey;
1459
1897
  let buckets = byColumn.get(key);
1460
1898
  if (buckets === void 0) {
1461
1899
  buckets = /* @__PURE__ */ new Map();
@@ -1600,7 +2038,7 @@ function canResume(rules, state) {
1600
2038
  function factsSince(db, marks) {
1601
2039
  const added = /* @__PURE__ */ new Map();
1602
2040
  for (const name of db.relationNames()) {
1603
- const all = db.facts(name);
2041
+ const all = tuplesOf(db, name);
1604
2042
  const mark = marks.get(name) ?? 0;
1605
2043
  if (all.length > mark) {
1606
2044
  added.set(name, all.slice(mark));
@@ -1611,7 +2049,7 @@ function factsSince(db, marks) {
1611
2049
  function currentMarks(db) {
1612
2050
  const marks = /* @__PURE__ */ new Map();
1613
2051
  for (const name of db.relationNames()) {
1614
- marks.set(name, db.size(name));
2052
+ marks.set(name, sizeOf(db, name));
1615
2053
  }
1616
2054
  return marks;
1617
2055
  }
@@ -1673,9 +2111,14 @@ function shapeOf(rules) {
1673
2111
  return shape;
1674
2112
  }
1675
2113
  function couldProduce(db, rule2) {
1676
- return rule2.body.every(
1677
- (literal) => literal.negated || db.size(literal.relation) > 0
2114
+ const empty = rule2.body.find(
2115
+ (literal) => !literal.negated && sizeOf(db, literal.relation) === 0
1678
2116
  );
2117
+ if (empty === void 0) {
2118
+ return true;
2119
+ }
2120
+ db.readLog?.readWhole(empty.relation);
2121
+ return false;
1679
2122
  }
1680
2123
  function storedMergeFor(algebra) {
1681
2124
  return (stored, incoming) => {
@@ -1724,6 +2167,17 @@ function runRules(db, rules, algebra, given) {
1724
2167
  };
1725
2168
  const mergeStored = algebra === void 0 ? void 0 : storedMergeFor(algebra);
1726
2169
  const marks = canResume(rules, state) ? state.marks : void 0;
2170
+ const producible = /* @__PURE__ */ new Set();
2171
+ const canProduce = (r) => {
2172
+ if (producible.has(r)) {
2173
+ return true;
2174
+ }
2175
+ if (!couldProduce(db, r)) {
2176
+ return false;
2177
+ }
2178
+ producible.add(r);
2179
+ return true;
2180
+ };
1727
2181
  const runStratum = (stratum) => {
1728
2182
  let delta = /* @__PURE__ */ new Map();
1729
2183
  const record = (relation, tuple) => {
@@ -1766,7 +2220,7 @@ function runRules(db, rules, algebra, given) {
1766
2220
  return;
1767
2221
  }
1768
2222
  const startedAt2 = performance.now();
1769
- const before = db.size(r.head.relation);
2223
+ const before = sizeOf(db, r.head.relation);
1770
2224
  const readBefore = budget.examined;
1771
2225
  deriveInto(r, seed, deltaIndex);
1772
2226
  chargeRule(
@@ -1774,14 +2228,14 @@ function runRules(db, rules, algebra, given) {
1774
2228
  r.head.relation,
1775
2229
  r.body.map(literalName),
1776
2230
  performance.now() - startedAt2,
1777
- db.size(r.head.relation) - before,
2231
+ sizeOf(db, r.head.relation) - before,
1778
2232
  budget.examined - readBefore
1779
2233
  );
1780
2234
  };
1781
2235
  const applyDelta = (seed, derivedOnly) => {
1782
2236
  for (const at of rulesReading(stratum, seed, derivedOnly)) {
1783
2237
  const r = stratum.rules[at];
1784
- if (!couldProduce(db, r)) {
2238
+ if (!canProduce(r)) {
1785
2239
  continue;
1786
2240
  }
1787
2241
  const positives = positivesOf(r);
@@ -1799,13 +2253,13 @@ function runRules(db, rules, algebra, given) {
1799
2253
  };
1800
2254
  if (marks === void 0 || marks === null) {
1801
2255
  for (const r of stratum.rules) {
1802
- if (!couldProduce(db, r)) {
2256
+ if (!canProduce(r)) {
1803
2257
  continue;
1804
2258
  }
1805
2259
  const all = /* @__PURE__ */ new Map();
1806
2260
  for (const l of r.body) {
1807
2261
  if (!l.negated) {
1808
- all.set(l.relation, db.facts(l.relation));
2262
+ all.set(l.relation, tuplesOf(db, l.relation));
1809
2263
  }
1810
2264
  }
1811
2265
  runOneRule(r, all, -1);
@@ -1842,7 +2296,7 @@ function runRules(db, rules, algebra, given) {
1842
2296
  state.marks = currentMarks(db);
1843
2297
  if (isProfiling()) {
1844
2298
  chargeRelationSizes(
1845
- db.relationNames().map((name) => [name, db.size(name)]),
2299
+ db.relationNames().map((name) => [name, sizeOf(db, name)]),
1846
2300
  derivedRelations
1847
2301
  );
1848
2302
  }
@@ -1879,6 +2333,7 @@ function addTo(buckets, relation, tuple) {
1879
2333
  formatProfile,
1880
2334
  lit,
1881
2335
  notLit,
2336
+ noteReads,
1882
2337
  profileEvaluation,
1883
2338
  profileEvaluationAsync,
1884
2339
  proofOf,