@suss/datalog 0.20.0 → 0.21.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/README.md CHANGED
@@ -64,6 +64,24 @@ column gets indexed the first time something asks for it and stays up to
64
64
  date after that, so a relation nobody joins on that way never gets an
65
65
  index at all.
66
66
 
67
+ The body is not always walked in the order it was written. A round draws
68
+ one literal from the facts that arrived last round, and that list is
69
+ usually short, so it goes first and everything after it starts with some
70
+ variables already bound. Take `chain(x, z) :- chain(x, y), binds(y, z)`
71
+ in the round where new `binds` facts arrive. Written order asks for
72
+ `chain(x, y)` with nothing bound, which scans every chain fact and tries
73
+ each one against every new binding. Leading with the new bindings binds
74
+ `y`, and `chain` then comes off its index on that column. On suss's own
75
+ sources that one reordering took the rule from 2.4 seconds to 55
76
+ milliseconds.
77
+
78
+ Once the delta leads, the rest of the body follows greedily: the next
79
+ literal is one sharing a variable that is already bound, so it too comes
80
+ off an index rather than scanning. A negated literal is a filter, so it
81
+ is placed as soon as its variables are bound. A join produces the same
82
+ rows whatever order it walks in, so this changes what a round costs and
83
+ not what it derives.
84
+
67
85
  `evaluate` picks up where it left off. Call it again with the same rules
68
86
  after adding facts and it starts from the facts you added rather than
69
87
  redoing the whole fixpoint. Callers that go back and forth between
@@ -243,6 +261,19 @@ companion becomes a literal in the rule body, and demand travels down
243
261
  each body the same way the join binds variables. A relation nothing asks
244
262
  for is not derived at all, so only read back the relations you listed.
245
263
 
264
+ The rewrite puts each body in the order demand should travel, which is
265
+ not always written order. At each position it takes the literal with the
266
+ most columns already fixed, by a constant or by a variable the head or
267
+ an earlier literal bound, and keeps written order between equals. Take
268
+ `moduleExport(m, n, v) <- reExports(m, n, m2, n2), moduleExport(m2, n2,
269
+ v)` asked with `v` bound. In written order the re-export comes first and
270
+ binds `m2` and `n2` off every re-export in the program, so the recursive
271
+ call wants `moduleExport` with all three columns bound: one demand fact
272
+ per re-export per value asked about. With `moduleExport(m2, n2, v)`
273
+ first, the demand stays on `v` and the re-exports come off an index on
274
+ `m2`. A body the head binds nothing in starts with its written first
275
+ literal.
276
+
246
277
  This costs you two things. The companion relations are stored like any
247
278
  other, at a few tuples per value asked about, so a caller who asks about
248
279
  most of a program ends up deriving more rather than less. And the
package/dist/index.cjs CHANGED
@@ -93,7 +93,8 @@ function deriveOnDemand(rules, complete) {
93
93
  }
94
94
  while (pending.length > 0) {
95
95
  const adorned = pending.pop();
96
- for (const r of rulesByHead.get(adorned.relation) ?? []) {
96
+ for (const written of rulesByHead.get(adorned.relation) ?? []) {
97
+ const r = boundFirst(written, adorned.adornment);
97
98
  const adornments = bodyAdornments(r, adorned.adornment, derives);
98
99
  r.body.forEach((literal, at) => {
99
100
  const adornment = adornments[at];
@@ -129,7 +130,8 @@ function deriveOnDemand(rules, complete) {
129
130
  demandDriven.add(nameOf(adorned.relation, adorned.adornment));
130
131
  demandDriven.add(demandOn(adorned.relation, adorned.adornment));
131
132
  }
132
- for (const r of rulesByHead.get(adorned.relation) ?? []) {
133
+ for (const written of rulesByHead.get(adorned.relation) ?? []) {
134
+ const r = boundFirst(written, adorned.adornment);
133
135
  const guard = anyBound(adorned.adornment) ? [
134
136
  positiveLiteral(
135
137
  demandOn(adorned.relation, adorned.adornment),
@@ -141,13 +143,14 @@ function deriveOnDemand(rules, complete) {
141
143
  r.body.forEach((literal, at) => {
142
144
  const adornment = adornments[at];
143
145
  if (adornment !== null && anyBound(adornment)) {
144
- emit({
145
- head: {
146
- relation: demandOn(literal.relation, adornment),
147
- terms: boundTerms(literal.terms, adornment)
148
- },
149
- body: [...guard, ...body]
150
- });
146
+ const demand = positiveLiteral(
147
+ demandOn(literal.relation, adornment),
148
+ boundTerms(literal.terms, adornment)
149
+ );
150
+ const premises = [...guard, ...body];
151
+ if (!sameLiteral(premises, demand)) {
152
+ emit({ head: demand, body: premises });
153
+ }
151
154
  }
152
155
  body.push(
153
156
  adornment === null ? literal : positiveLiteral(
@@ -168,6 +171,36 @@ function deriveOnDemand(rules, complete) {
168
171
  demandRewrites.add(rewritten);
169
172
  return { rules: rewritten, demandDriven: [...demandDriven] };
170
173
  }
174
+ var sameLiteral = (premises, conclusion) => premises.length === 1 && JSON.stringify(premises[0]) === JSON.stringify(conclusion);
175
+ function boundFirst(r, headAdornment) {
176
+ const bound = /* @__PURE__ */ new Set();
177
+ r.head.terms.forEach((term, column) => {
178
+ if (headAdornment[column] && term.type === "variable") {
179
+ bound.add(term.name);
180
+ }
181
+ });
182
+ const boundColumns = (literal) => literal.terms.filter(
183
+ (term) => term.type === "constant" || bound.has(term.name)
184
+ ).length;
185
+ const remaining = [...r.body];
186
+ const body = [];
187
+ while (remaining.length > 0) {
188
+ let pick = 0;
189
+ for (let at = 1; at < remaining.length; at++) {
190
+ if (boundColumns(remaining[at]) > boundColumns(remaining[pick])) {
191
+ pick = at;
192
+ }
193
+ }
194
+ const [literal] = remaining.splice(pick, 1);
195
+ body.push(literal);
196
+ for (const term of literal.terms) {
197
+ if (term.type === "variable") {
198
+ bound.add(term.name);
199
+ }
200
+ }
201
+ }
202
+ return { head: r.head, body };
203
+ }
171
204
  function bodyAdornments(r, headAdornment, derives) {
172
205
  const bound = /* @__PURE__ */ new Set();
173
206
  r.head.terms.forEach((term, column) => {
@@ -349,7 +382,13 @@ function formatProfile(profile) {
349
382
 
350
383
  // src/tupleKey.ts
351
384
  var atomKey = (a) => typeof a === "number" ? `n${a}:` : `s${a.length}:${a}`;
352
- var tupleKey = (parts) => parts.map(atomKey).join("");
385
+ var tupleKey = (parts) => {
386
+ let key = "";
387
+ for (let i = 0; i < parts.length; i++) {
388
+ key += atomKey(parts[i]);
389
+ }
390
+ return key;
391
+ };
353
392
  function tupleKeyParts(key) {
354
393
  const parts = [];
355
394
  let at = 0;
@@ -483,7 +522,16 @@ var notLit = (relation, ...terms) => ({
483
522
  var rule = (relation, terms, body, name) => name === void 0 ? { head: { relation, terms }, body } : { head: { relation, terms }, body, name };
484
523
  var literalName = (literal) => literal.negated ? `!${literal.relation}` : literal.relation;
485
524
  var ruleLabel = (r) => r.name ?? `${r.head.relation} :- ${r.body.map(literalName).join(", ")}`;
486
- var keyOf2 = (tuple) => tupleKey(tuple);
525
+ var keys = /* @__PURE__ */ new WeakMap();
526
+ var keyOf2 = (tuple) => {
527
+ const known = keys.get(tuple);
528
+ if (known !== void 0) {
529
+ return known;
530
+ }
531
+ const key = tupleKey(tuple);
532
+ keys.set(tuple, key);
533
+ return key;
534
+ };
487
535
  function tagSlots(relation) {
488
536
  if (relation.tags === void 0) {
489
537
  relation.tags = /* @__PURE__ */ new Map();
@@ -615,9 +663,7 @@ var Database = class {
615
663
  relation.keys.delete(key);
616
664
  relation.tags?.delete(key);
617
665
  }
618
- relation.tuples = relation.tuples.filter(
619
- (tuple) => !going.has(keyOf2(tuple))
620
- );
666
+ relation.tuples = relation.keys.size === 0 ? [] : relation.tuples.filter((tuple) => !going.has(keyOf2(tuple)));
621
667
  relation.indexes.clear();
622
668
  forgetFacts(this, relationName, going);
623
669
  return going.size;
@@ -727,29 +773,90 @@ function headTuple(head, bindings) {
727
773
  return bound;
728
774
  });
729
775
  }
776
+ var variablesOf = (literal) => literal.terms.filter((term) => term.type === "variable").map((term) => term.name);
777
+ var plans = /* @__PURE__ */ new WeakMap();
778
+ function bodyPlan(r, deltaIndex) {
779
+ let byDelta = plans.get(r);
780
+ if (byDelta === void 0) {
781
+ byDelta = /* @__PURE__ */ new Map();
782
+ plans.set(r, byDelta);
783
+ }
784
+ const known = byDelta.get(deltaIndex);
785
+ if (known !== void 0) {
786
+ return known;
787
+ }
788
+ const positives = r.body.map((literal, index) => literal.negated ? -1 : index).filter((index) => index !== -1);
789
+ const deltaAt = positives[deltaIndex] ?? -1;
790
+ const computed = { deltaAt, order: planBodyOrder(r, deltaAt) };
791
+ byDelta.set(deltaIndex, computed);
792
+ return computed;
793
+ }
794
+ function planBodyOrder(r, deltaAt) {
795
+ const natural = r.body.map((_, i) => i);
796
+ if (deltaAt <= 0) {
797
+ return natural;
798
+ }
799
+ const taken = r.body.map(() => false);
800
+ const bound = /* @__PURE__ */ new Set();
801
+ const order = [];
802
+ const take = (index) => {
803
+ taken[index] = true;
804
+ order.push(index);
805
+ for (const name of variablesOf(r.body[index])) {
806
+ bound.add(name);
807
+ }
808
+ };
809
+ const firstWhere = (want) => natural.find((index) => !taken[index] && want(index)) ?? -1;
810
+ const nextLiteral = () => {
811
+ const ready = firstWhere(
812
+ (i) => r.body[i].negated && variablesOf(r.body[i]).every((name) => bound.has(name))
813
+ );
814
+ if (ready !== -1) {
815
+ return ready;
816
+ }
817
+ const joined = firstWhere(
818
+ (i) => !r.body[i].negated && variablesOf(r.body[i]).some((name) => bound.has(name))
819
+ );
820
+ if (joined !== -1) {
821
+ return joined;
822
+ }
823
+ const disconnected = firstWhere((i) => !r.body[i].negated);
824
+ if (disconnected !== -1) {
825
+ return disconnected;
826
+ }
827
+ return firstWhere(() => true);
828
+ };
829
+ take(deltaAt);
830
+ while (order.length < r.body.length) {
831
+ take(nextLiteral());
832
+ }
833
+ return order;
834
+ }
730
835
  function evaluateRule(db, deltas, r, deltaIndex) {
731
836
  const results = [];
732
- const step = (literalIndex, positiveIndex, bindings) => {
733
- if (literalIndex === r.body.length) {
837
+ const { deltaAt, order } = bodyPlan(r, deltaIndex);
838
+ const step = (orderIndex, bindings) => {
839
+ if (orderIndex === order.length) {
734
840
  results.push(headTuple(r.head, bindings));
735
841
  return;
736
842
  }
843
+ const literalIndex = order[orderIndex];
737
844
  const literal = r.body[literalIndex];
738
845
  if (literal.negated) {
739
846
  if (!db.has(literal.relation, groundNegated(literal, bindings))) {
740
- step(literalIndex + 1, positiveIndex, bindings);
847
+ step(orderIndex + 1, bindings);
741
848
  }
742
849
  return;
743
850
  }
744
- const source = positiveIndex === deltaIndex ? deltas.get(literal.relation) ?? [] : boundSource(db, literal, bindings);
851
+ const source = literalIndex === deltaAt ? deltas.get(literal.relation) ?? [] : boundSource(db, literal, bindings);
745
852
  for (const tuple of source) {
746
853
  const next = unify(literal, tuple, bindings);
747
854
  if (next !== NO_MATCH) {
748
- step(literalIndex + 1, positiveIndex + 1, next);
855
+ step(orderIndex + 1, next);
749
856
  }
750
857
  }
751
858
  };
752
- step(0, 0, null);
859
+ step(0, null);
753
860
  return results;
754
861
  }
755
862
  function boundSource(db, literal, bindings) {
@@ -855,7 +962,7 @@ function claimFact(db, relation, key) {
855
962
  state.derived.get(relation)?.delete(key);
856
963
  }
857
964
  }
858
- function forgetFacts(db, relation, keys) {
965
+ function forgetFacts(db, relation, keys2) {
859
966
  const states = evaluated.get(db);
860
967
  if (states === void 0) {
861
968
  return;
@@ -864,7 +971,7 @@ function forgetFacts(db, relation, keys) {
864
971
  state.marks = null;
865
972
  const ledger = state.derived.get(relation);
866
973
  if (ledger !== void 0) {
867
- for (const key of keys) {
974
+ for (const key of keys2) {
868
975
  ledger.delete(key);
869
976
  }
870
977
  }
package/dist/index.js CHANGED
@@ -47,7 +47,8 @@ function deriveOnDemand(rules, complete) {
47
47
  }
48
48
  while (pending.length > 0) {
49
49
  const adorned = pending.pop();
50
- for (const r of rulesByHead.get(adorned.relation) ?? []) {
50
+ for (const written of rulesByHead.get(adorned.relation) ?? []) {
51
+ const r = boundFirst(written, adorned.adornment);
51
52
  const adornments = bodyAdornments(r, adorned.adornment, derives);
52
53
  r.body.forEach((literal, at) => {
53
54
  const adornment = adornments[at];
@@ -83,7 +84,8 @@ function deriveOnDemand(rules, complete) {
83
84
  demandDriven.add(nameOf(adorned.relation, adorned.adornment));
84
85
  demandDriven.add(demandOn(adorned.relation, adorned.adornment));
85
86
  }
86
- for (const r of rulesByHead.get(adorned.relation) ?? []) {
87
+ for (const written of rulesByHead.get(adorned.relation) ?? []) {
88
+ const r = boundFirst(written, adorned.adornment);
87
89
  const guard = anyBound(adorned.adornment) ? [
88
90
  positiveLiteral(
89
91
  demandOn(adorned.relation, adorned.adornment),
@@ -95,13 +97,14 @@ function deriveOnDemand(rules, complete) {
95
97
  r.body.forEach((literal, at) => {
96
98
  const adornment = adornments[at];
97
99
  if (adornment !== null && anyBound(adornment)) {
98
- emit({
99
- head: {
100
- relation: demandOn(literal.relation, adornment),
101
- terms: boundTerms(literal.terms, adornment)
102
- },
103
- body: [...guard, ...body]
104
- });
100
+ const demand = positiveLiteral(
101
+ demandOn(literal.relation, adornment),
102
+ boundTerms(literal.terms, adornment)
103
+ );
104
+ const premises = [...guard, ...body];
105
+ if (!sameLiteral(premises, demand)) {
106
+ emit({ head: demand, body: premises });
107
+ }
105
108
  }
106
109
  body.push(
107
110
  adornment === null ? literal : positiveLiteral(
@@ -122,6 +125,36 @@ function deriveOnDemand(rules, complete) {
122
125
  demandRewrites.add(rewritten);
123
126
  return { rules: rewritten, demandDriven: [...demandDriven] };
124
127
  }
128
+ var sameLiteral = (premises, conclusion) => premises.length === 1 && JSON.stringify(premises[0]) === JSON.stringify(conclusion);
129
+ function boundFirst(r, headAdornment) {
130
+ const bound = /* @__PURE__ */ new Set();
131
+ r.head.terms.forEach((term, column) => {
132
+ if (headAdornment[column] && term.type === "variable") {
133
+ bound.add(term.name);
134
+ }
135
+ });
136
+ const boundColumns = (literal) => literal.terms.filter(
137
+ (term) => term.type === "constant" || bound.has(term.name)
138
+ ).length;
139
+ const remaining = [...r.body];
140
+ const body = [];
141
+ while (remaining.length > 0) {
142
+ let pick = 0;
143
+ for (let at = 1; at < remaining.length; at++) {
144
+ if (boundColumns(remaining[at]) > boundColumns(remaining[pick])) {
145
+ pick = at;
146
+ }
147
+ }
148
+ const [literal] = remaining.splice(pick, 1);
149
+ body.push(literal);
150
+ for (const term of literal.terms) {
151
+ if (term.type === "variable") {
152
+ bound.add(term.name);
153
+ }
154
+ }
155
+ }
156
+ return { head: r.head, body };
157
+ }
125
158
  function bodyAdornments(r, headAdornment, derives) {
126
159
  const bound = /* @__PURE__ */ new Set();
127
160
  r.head.terms.forEach((term, column) => {
@@ -303,7 +336,13 @@ function formatProfile(profile) {
303
336
 
304
337
  // src/tupleKey.ts
305
338
  var atomKey = (a) => typeof a === "number" ? `n${a}:` : `s${a.length}:${a}`;
306
- var tupleKey = (parts) => parts.map(atomKey).join("");
339
+ var tupleKey = (parts) => {
340
+ let key = "";
341
+ for (let i = 0; i < parts.length; i++) {
342
+ key += atomKey(parts[i]);
343
+ }
344
+ return key;
345
+ };
307
346
  function tupleKeyParts(key) {
308
347
  const parts = [];
309
348
  let at = 0;
@@ -437,7 +476,16 @@ var notLit = (relation, ...terms) => ({
437
476
  var rule = (relation, terms, body, name) => name === void 0 ? { head: { relation, terms }, body } : { head: { relation, terms }, body, name };
438
477
  var literalName = (literal) => literal.negated ? `!${literal.relation}` : literal.relation;
439
478
  var ruleLabel = (r) => r.name ?? `${r.head.relation} :- ${r.body.map(literalName).join(", ")}`;
440
- var keyOf2 = (tuple) => tupleKey(tuple);
479
+ var keys = /* @__PURE__ */ new WeakMap();
480
+ var keyOf2 = (tuple) => {
481
+ const known = keys.get(tuple);
482
+ if (known !== void 0) {
483
+ return known;
484
+ }
485
+ const key = tupleKey(tuple);
486
+ keys.set(tuple, key);
487
+ return key;
488
+ };
441
489
  function tagSlots(relation) {
442
490
  if (relation.tags === void 0) {
443
491
  relation.tags = /* @__PURE__ */ new Map();
@@ -569,9 +617,7 @@ var Database = class {
569
617
  relation.keys.delete(key);
570
618
  relation.tags?.delete(key);
571
619
  }
572
- relation.tuples = relation.tuples.filter(
573
- (tuple) => !going.has(keyOf2(tuple))
574
- );
620
+ relation.tuples = relation.keys.size === 0 ? [] : relation.tuples.filter((tuple) => !going.has(keyOf2(tuple)));
575
621
  relation.indexes.clear();
576
622
  forgetFacts(this, relationName, going);
577
623
  return going.size;
@@ -681,29 +727,90 @@ function headTuple(head, bindings) {
681
727
  return bound;
682
728
  });
683
729
  }
730
+ var variablesOf = (literal) => literal.terms.filter((term) => term.type === "variable").map((term) => term.name);
731
+ var plans = /* @__PURE__ */ new WeakMap();
732
+ function bodyPlan(r, deltaIndex) {
733
+ let byDelta = plans.get(r);
734
+ if (byDelta === void 0) {
735
+ byDelta = /* @__PURE__ */ new Map();
736
+ plans.set(r, byDelta);
737
+ }
738
+ const known = byDelta.get(deltaIndex);
739
+ if (known !== void 0) {
740
+ return known;
741
+ }
742
+ const positives = r.body.map((literal, index) => literal.negated ? -1 : index).filter((index) => index !== -1);
743
+ const deltaAt = positives[deltaIndex] ?? -1;
744
+ const computed = { deltaAt, order: planBodyOrder(r, deltaAt) };
745
+ byDelta.set(deltaIndex, computed);
746
+ return computed;
747
+ }
748
+ function planBodyOrder(r, deltaAt) {
749
+ const natural = r.body.map((_, i) => i);
750
+ if (deltaAt <= 0) {
751
+ return natural;
752
+ }
753
+ const taken = r.body.map(() => false);
754
+ const bound = /* @__PURE__ */ new Set();
755
+ const order = [];
756
+ const take = (index) => {
757
+ taken[index] = true;
758
+ order.push(index);
759
+ for (const name of variablesOf(r.body[index])) {
760
+ bound.add(name);
761
+ }
762
+ };
763
+ const firstWhere = (want) => natural.find((index) => !taken[index] && want(index)) ?? -1;
764
+ const nextLiteral = () => {
765
+ const ready = firstWhere(
766
+ (i) => r.body[i].negated && variablesOf(r.body[i]).every((name) => bound.has(name))
767
+ );
768
+ if (ready !== -1) {
769
+ return ready;
770
+ }
771
+ const joined = firstWhere(
772
+ (i) => !r.body[i].negated && variablesOf(r.body[i]).some((name) => bound.has(name))
773
+ );
774
+ if (joined !== -1) {
775
+ return joined;
776
+ }
777
+ const disconnected = firstWhere((i) => !r.body[i].negated);
778
+ if (disconnected !== -1) {
779
+ return disconnected;
780
+ }
781
+ return firstWhere(() => true);
782
+ };
783
+ take(deltaAt);
784
+ while (order.length < r.body.length) {
785
+ take(nextLiteral());
786
+ }
787
+ return order;
788
+ }
684
789
  function evaluateRule(db, deltas, r, deltaIndex) {
685
790
  const results = [];
686
- const step = (literalIndex, positiveIndex, bindings) => {
687
- if (literalIndex === r.body.length) {
791
+ const { deltaAt, order } = bodyPlan(r, deltaIndex);
792
+ const step = (orderIndex, bindings) => {
793
+ if (orderIndex === order.length) {
688
794
  results.push(headTuple(r.head, bindings));
689
795
  return;
690
796
  }
797
+ const literalIndex = order[orderIndex];
691
798
  const literal = r.body[literalIndex];
692
799
  if (literal.negated) {
693
800
  if (!db.has(literal.relation, groundNegated(literal, bindings))) {
694
- step(literalIndex + 1, positiveIndex, bindings);
801
+ step(orderIndex + 1, bindings);
695
802
  }
696
803
  return;
697
804
  }
698
- const source = positiveIndex === deltaIndex ? deltas.get(literal.relation) ?? [] : boundSource(db, literal, bindings);
805
+ const source = literalIndex === deltaAt ? deltas.get(literal.relation) ?? [] : boundSource(db, literal, bindings);
699
806
  for (const tuple of source) {
700
807
  const next = unify(literal, tuple, bindings);
701
808
  if (next !== NO_MATCH) {
702
- step(literalIndex + 1, positiveIndex + 1, next);
809
+ step(orderIndex + 1, next);
703
810
  }
704
811
  }
705
812
  };
706
- step(0, 0, null);
813
+ step(0, null);
707
814
  return results;
708
815
  }
709
816
  function boundSource(db, literal, bindings) {
@@ -809,7 +916,7 @@ function claimFact(db, relation, key) {
809
916
  state.derived.get(relation)?.delete(key);
810
917
  }
811
918
  }
812
- function forgetFacts(db, relation, keys) {
919
+ function forgetFacts(db, relation, keys2) {
813
920
  const states = evaluated.get(db);
814
921
  if (states === void 0) {
815
922
  return;
@@ -818,7 +925,7 @@ function forgetFacts(db, relation, keys) {
818
925
  state.marks = null;
819
926
  const ledger = state.derived.get(relation);
820
927
  if (ledger !== void 0) {
821
- for (const key of keys) {
928
+ for (const key of keys2) {
822
929
  ledger.delete(key);
823
930
  }
824
931
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suss/datalog",
3
- "version": "0.20.0",
3
+ "version": "0.21.1",
4
4
  "type": "module",
5
5
  "description": "A small semi-naïve Datalog evaluator with stratified negation — the rules engine behind suss's derived program facts.",
6
6
  "keywords": [