@suss/datalog 0.21.0 → 0.22.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
@@ -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
@@ -282,6 +313,24 @@ Keep the facts you add and the facts the rules derive in separate
282
313
  relations. Taking a conclusion back cannot tell one from the other, and
283
314
  splitting them is how people normally write Datalog anyway.
284
315
 
316
+ ### Reading what the rules are waiting on
317
+
318
+ Demand is also something a caller can read. `program.demands` lists
319
+ each demand relation with the relation it is for and which of that
320
+ relation's columns a demand row fixes, in column order:
321
+
322
+ ```ts
323
+ program.demands;
324
+ // [{ relation: "reaches", bound: [true, false], demand: "wanted:reaches" }]
325
+ db.facts("wanted:reaches"); // [["a"], ["b"], ["c"]]
326
+ ```
327
+
328
+ A caller that loads its base facts lazily uses this to find out what to
329
+ load next. Evaluate, read the demand rows on the relation whose base
330
+ facts come from somewhere else, fetch those, evaluate again, and stop
331
+ when no demand row is new. The rules then decide what gets loaded, and
332
+ the caller never has to guess how far a chain runs.
333
+
285
334
  Extraction-scale fact sets run to thousands of tuples. If that changes,
286
335
  the rule data model is the part that stays and this evaluator is the
287
336
  part you replace.
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];
@@ -116,6 +117,7 @@ function deriveOnDemand(rules, complete) {
116
117
  const demandOn = (relation, adornment) => `wanted:${nameOf(relation, adornment)}`;
117
118
  const rewritten = [];
118
119
  const demandDriven = /* @__PURE__ */ new Set();
120
+ const demands = [];
119
121
  const already = /* @__PURE__ */ new Set();
120
122
  const emit = (r) => {
121
123
  const key = JSON.stringify(r);
@@ -128,8 +130,14 @@ function deriveOnDemand(rules, complete) {
128
130
  if (anyBound(adorned.adornment)) {
129
131
  demandDriven.add(nameOf(adorned.relation, adorned.adornment));
130
132
  demandDriven.add(demandOn(adorned.relation, adorned.adornment));
133
+ demands.push({
134
+ relation: adorned.relation,
135
+ bound: adorned.adornment,
136
+ demand: demandOn(adorned.relation, adorned.adornment)
137
+ });
131
138
  }
132
- for (const r of rulesByHead.get(adorned.relation) ?? []) {
139
+ for (const written of rulesByHead.get(adorned.relation) ?? []) {
140
+ const r = boundFirst(written, adorned.adornment);
133
141
  const guard = anyBound(adorned.adornment) ? [
134
142
  positiveLiteral(
135
143
  demandOn(adorned.relation, adorned.adornment),
@@ -141,13 +149,14 @@ function deriveOnDemand(rules, complete) {
141
149
  r.body.forEach((literal, at) => {
142
150
  const adornment = adornments[at];
143
151
  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
- });
152
+ const demand = positiveLiteral(
153
+ demandOn(literal.relation, adornment),
154
+ boundTerms(literal.terms, adornment)
155
+ );
156
+ const premises = [...guard, ...body];
157
+ if (!sameLiteral(premises, demand)) {
158
+ emit({ head: demand, body: premises });
159
+ }
151
160
  }
152
161
  body.push(
153
162
  adornment === null ? literal : positiveLiteral(
@@ -166,7 +175,37 @@ function deriveOnDemand(rules, complete) {
166
175
  }
167
176
  }
168
177
  demandRewrites.add(rewritten);
169
- return { rules: rewritten, demandDriven: [...demandDriven] };
178
+ return { rules: rewritten, demandDriven: [...demandDriven], demands };
179
+ }
180
+ var sameLiteral = (premises, conclusion) => premises.length === 1 && JSON.stringify(premises[0]) === JSON.stringify(conclusion);
181
+ function boundFirst(r, headAdornment) {
182
+ const bound = /* @__PURE__ */ new Set();
183
+ r.head.terms.forEach((term, column) => {
184
+ if (headAdornment[column] && term.type === "variable") {
185
+ bound.add(term.name);
186
+ }
187
+ });
188
+ const boundColumns = (literal) => literal.terms.filter(
189
+ (term) => term.type === "constant" || bound.has(term.name)
190
+ ).length;
191
+ const remaining = [...r.body];
192
+ const body = [];
193
+ while (remaining.length > 0) {
194
+ let pick = 0;
195
+ for (let at = 1; at < remaining.length; at++) {
196
+ if (boundColumns(remaining[at]) > boundColumns(remaining[pick])) {
197
+ pick = at;
198
+ }
199
+ }
200
+ const [literal] = remaining.splice(pick, 1);
201
+ body.push(literal);
202
+ for (const term of literal.terms) {
203
+ if (term.type === "variable") {
204
+ bound.add(term.name);
205
+ }
206
+ }
207
+ }
208
+ return { head: r.head, body };
170
209
  }
171
210
  function bodyAdornments(r, headAdornment, derives) {
172
211
  const bound = /* @__PURE__ */ new Set();
@@ -349,7 +388,13 @@ function formatProfile(profile) {
349
388
 
350
389
  // src/tupleKey.ts
351
390
  var atomKey = (a) => typeof a === "number" ? `n${a}:` : `s${a.length}:${a}`;
352
- var tupleKey = (parts) => parts.map(atomKey).join("");
391
+ var tupleKey = (parts) => {
392
+ let key = "";
393
+ for (let i = 0; i < parts.length; i++) {
394
+ key += atomKey(parts[i]);
395
+ }
396
+ return key;
397
+ };
353
398
  function tupleKeyParts(key) {
354
399
  const parts = [];
355
400
  let at = 0;
@@ -483,7 +528,16 @@ var notLit = (relation, ...terms) => ({
483
528
  var rule = (relation, terms, body, name) => name === void 0 ? { head: { relation, terms }, body } : { head: { relation, terms }, body, name };
484
529
  var literalName = (literal) => literal.negated ? `!${literal.relation}` : literal.relation;
485
530
  var ruleLabel = (r) => r.name ?? `${r.head.relation} :- ${r.body.map(literalName).join(", ")}`;
486
- var keyOf2 = (tuple) => tupleKey(tuple);
531
+ var keys = /* @__PURE__ */ new WeakMap();
532
+ var keyOf2 = (tuple) => {
533
+ const known = keys.get(tuple);
534
+ if (known !== void 0) {
535
+ return known;
536
+ }
537
+ const key = tupleKey(tuple);
538
+ keys.set(tuple, key);
539
+ return key;
540
+ };
487
541
  function tagSlots(relation) {
488
542
  if (relation.tags === void 0) {
489
543
  relation.tags = /* @__PURE__ */ new Map();
@@ -615,9 +669,7 @@ var Database = class {
615
669
  relation.keys.delete(key);
616
670
  relation.tags?.delete(key);
617
671
  }
618
- relation.tuples = relation.tuples.filter(
619
- (tuple) => !going.has(keyOf2(tuple))
620
- );
672
+ relation.tuples = relation.keys.size === 0 ? [] : relation.tuples.filter((tuple) => !going.has(keyOf2(tuple)));
621
673
  relation.indexes.clear();
622
674
  forgetFacts(this, relationName, going);
623
675
  return going.size;
@@ -727,29 +779,90 @@ function headTuple(head, bindings) {
727
779
  return bound;
728
780
  });
729
781
  }
782
+ var variablesOf = (literal) => literal.terms.filter((term) => term.type === "variable").map((term) => term.name);
783
+ var plans = /* @__PURE__ */ new WeakMap();
784
+ function bodyPlan(r, deltaIndex) {
785
+ let byDelta = plans.get(r);
786
+ if (byDelta === void 0) {
787
+ byDelta = /* @__PURE__ */ new Map();
788
+ plans.set(r, byDelta);
789
+ }
790
+ const known = byDelta.get(deltaIndex);
791
+ if (known !== void 0) {
792
+ return known;
793
+ }
794
+ const positives = r.body.map((literal, index) => literal.negated ? -1 : index).filter((index) => index !== -1);
795
+ const deltaAt = positives[deltaIndex] ?? -1;
796
+ const computed = { deltaAt, order: planBodyOrder(r, deltaAt) };
797
+ byDelta.set(deltaIndex, computed);
798
+ return computed;
799
+ }
800
+ function planBodyOrder(r, deltaAt) {
801
+ const natural = r.body.map((_, i) => i);
802
+ if (deltaAt <= 0) {
803
+ return natural;
804
+ }
805
+ const taken = r.body.map(() => false);
806
+ const bound = /* @__PURE__ */ new Set();
807
+ const order = [];
808
+ const take = (index) => {
809
+ taken[index] = true;
810
+ order.push(index);
811
+ for (const name of variablesOf(r.body[index])) {
812
+ bound.add(name);
813
+ }
814
+ };
815
+ const firstWhere = (want) => natural.find((index) => !taken[index] && want(index)) ?? -1;
816
+ const nextLiteral = () => {
817
+ const ready = firstWhere(
818
+ (i) => r.body[i].negated && variablesOf(r.body[i]).every((name) => bound.has(name))
819
+ );
820
+ if (ready !== -1) {
821
+ return ready;
822
+ }
823
+ const joined = firstWhere(
824
+ (i) => !r.body[i].negated && variablesOf(r.body[i]).some((name) => bound.has(name))
825
+ );
826
+ if (joined !== -1) {
827
+ return joined;
828
+ }
829
+ const disconnected = firstWhere((i) => !r.body[i].negated);
830
+ if (disconnected !== -1) {
831
+ return disconnected;
832
+ }
833
+ return firstWhere(() => true);
834
+ };
835
+ take(deltaAt);
836
+ while (order.length < r.body.length) {
837
+ take(nextLiteral());
838
+ }
839
+ return order;
840
+ }
730
841
  function evaluateRule(db, deltas, r, deltaIndex) {
731
842
  const results = [];
732
- const step = (literalIndex, positiveIndex, bindings) => {
733
- if (literalIndex === r.body.length) {
843
+ const { deltaAt, order } = bodyPlan(r, deltaIndex);
844
+ const step = (orderIndex, bindings) => {
845
+ if (orderIndex === order.length) {
734
846
  results.push(headTuple(r.head, bindings));
735
847
  return;
736
848
  }
849
+ const literalIndex = order[orderIndex];
737
850
  const literal = r.body[literalIndex];
738
851
  if (literal.negated) {
739
852
  if (!db.has(literal.relation, groundNegated(literal, bindings))) {
740
- step(literalIndex + 1, positiveIndex, bindings);
853
+ step(orderIndex + 1, bindings);
741
854
  }
742
855
  return;
743
856
  }
744
- const source = positiveIndex === deltaIndex ? deltas.get(literal.relation) ?? [] : boundSource(db, literal, bindings);
857
+ const source = literalIndex === deltaAt ? deltas.get(literal.relation) ?? [] : boundSource(db, literal, bindings);
745
858
  for (const tuple of source) {
746
859
  const next = unify(literal, tuple, bindings);
747
860
  if (next !== NO_MATCH) {
748
- step(literalIndex + 1, positiveIndex + 1, next);
861
+ step(orderIndex + 1, next);
749
862
  }
750
863
  }
751
864
  };
752
- step(0, 0, null);
865
+ step(0, null);
753
866
  return results;
754
867
  }
755
868
  function boundSource(db, literal, bindings) {
@@ -855,7 +968,7 @@ function claimFact(db, relation, key) {
855
968
  state.derived.get(relation)?.delete(key);
856
969
  }
857
970
  }
858
- function forgetFacts(db, relation, keys) {
971
+ function forgetFacts(db, relation, keys2) {
859
972
  const states = evaluated.get(db);
860
973
  if (states === void 0) {
861
974
  return;
@@ -864,7 +977,7 @@ function forgetFacts(db, relation, keys) {
864
977
  state.marks = null;
865
978
  const ledger = state.derived.get(relation);
866
979
  if (ledger !== void 0) {
867
- for (const key of keys) {
980
+ for (const key of keys2) {
868
981
  ledger.delete(key);
869
982
  }
870
983
  }
package/dist/index.d.cts CHANGED
@@ -31,6 +31,18 @@ declare function confidenceWith(levelOf: (rule: Rule) => ConfidenceLevel | undef
31
31
  /** The confidence algebra with every rule counted as exact. */
32
32
  declare const confidence: TagAlgebra<ConfidenceLevel>;
33
33
 
34
+ /** One demand relation the rewrite created, and what its rows say. */
35
+ interface Demand {
36
+ /** The relation whose rows are being waited on. */
37
+ relation: string;
38
+ /** Which columns of that relation a demand row fixes. */
39
+ bound: readonly boolean[];
40
+ /**
41
+ * The demand relation's name. Each of its rows lists the bound
42
+ * columns' values, in the relation's column order.
43
+ */
44
+ demand: string;
45
+ }
34
46
  /** A rewritten rule set, and which of its relations demand restricts. */
35
47
  interface OnDemandRules {
36
48
  /** The rewritten rules, ready to evaluate. */
@@ -43,6 +55,13 @@ interface OnDemandRules {
43
55
  * answers.
44
56
  */
45
57
  demandDriven: string[];
58
+ /**
59
+ * The demand relations by what they ask for, so a caller can read
60
+ * what the rules are waiting on and go and get it: a store that
61
+ * loads facts lazily reads the demand on a module's exports and
62
+ * extracts that module.
63
+ */
64
+ demands: Demand[];
46
65
  }
47
66
  /**
48
67
  * Rewrite `rules` so the relations listed in `complete` still come out
@@ -419,4 +438,4 @@ declare function evaluate<Tag = never>(db: Database, rules: Rule[], algebra?: Ta
419
438
  */
420
439
  declare function clearRelations(db: Database, rules: Rule[], relations: readonly string[]): void;
421
440
 
422
- export { type AddOutcome, type Atom, type BodyMatch, type ConfidenceLevel, Database, type Derivation, type EvaluationProfile, type Literal, type OnDemandRules, type Proof, type ProofDerived, type ProofOptions, type Rule, type RuleCost, type TagAlgebra, type Term, type Tuple, Witness, type WitnessTag, clearRelations, confidence, confidenceWith, constant, deriveOnDemand, evaluate, formatProfile, lit, notLit, profileEvaluation, profileEvaluationAsync, proofOf, rule, ruleLabel, stratify, tupleKey, tupleKeyParts, variable, witnesses };
441
+ export { type AddOutcome, type Atom, type BodyMatch, type ConfidenceLevel, Database, type Demand, type Derivation, type EvaluationProfile, type Literal, type OnDemandRules, type Proof, type ProofDerived, type ProofOptions, type Rule, type RuleCost, type TagAlgebra, type Term, type Tuple, Witness, type WitnessTag, clearRelations, confidence, confidenceWith, constant, deriveOnDemand, evaluate, formatProfile, lit, notLit, profileEvaluation, profileEvaluationAsync, proofOf, rule, ruleLabel, stratify, tupleKey, tupleKeyParts, variable, witnesses };
package/dist/index.d.ts CHANGED
@@ -31,6 +31,18 @@ declare function confidenceWith(levelOf: (rule: Rule) => ConfidenceLevel | undef
31
31
  /** The confidence algebra with every rule counted as exact. */
32
32
  declare const confidence: TagAlgebra<ConfidenceLevel>;
33
33
 
34
+ /** One demand relation the rewrite created, and what its rows say. */
35
+ interface Demand {
36
+ /** The relation whose rows are being waited on. */
37
+ relation: string;
38
+ /** Which columns of that relation a demand row fixes. */
39
+ bound: readonly boolean[];
40
+ /**
41
+ * The demand relation's name. Each of its rows lists the bound
42
+ * columns' values, in the relation's column order.
43
+ */
44
+ demand: string;
45
+ }
34
46
  /** A rewritten rule set, and which of its relations demand restricts. */
35
47
  interface OnDemandRules {
36
48
  /** The rewritten rules, ready to evaluate. */
@@ -43,6 +55,13 @@ interface OnDemandRules {
43
55
  * answers.
44
56
  */
45
57
  demandDriven: string[];
58
+ /**
59
+ * The demand relations by what they ask for, so a caller can read
60
+ * what the rules are waiting on and go and get it: a store that
61
+ * loads facts lazily reads the demand on a module's exports and
62
+ * extracts that module.
63
+ */
64
+ demands: Demand[];
46
65
  }
47
66
  /**
48
67
  * Rewrite `rules` so the relations listed in `complete` still come out
@@ -419,4 +438,4 @@ declare function evaluate<Tag = never>(db: Database, rules: Rule[], algebra?: Ta
419
438
  */
420
439
  declare function clearRelations(db: Database, rules: Rule[], relations: readonly string[]): void;
421
440
 
422
- export { type AddOutcome, type Atom, type BodyMatch, type ConfidenceLevel, Database, type Derivation, type EvaluationProfile, type Literal, type OnDemandRules, type Proof, type ProofDerived, type ProofOptions, type Rule, type RuleCost, type TagAlgebra, type Term, type Tuple, Witness, type WitnessTag, clearRelations, confidence, confidenceWith, constant, deriveOnDemand, evaluate, formatProfile, lit, notLit, profileEvaluation, profileEvaluationAsync, proofOf, rule, ruleLabel, stratify, tupleKey, tupleKeyParts, variable, witnesses };
441
+ export { type AddOutcome, type Atom, type BodyMatch, type ConfidenceLevel, Database, type Demand, type Derivation, type EvaluationProfile, type Literal, type OnDemandRules, type Proof, type ProofDerived, type ProofOptions, type Rule, type RuleCost, type TagAlgebra, type Term, type Tuple, Witness, type WitnessTag, clearRelations, confidence, confidenceWith, constant, deriveOnDemand, evaluate, formatProfile, lit, notLit, profileEvaluation, profileEvaluationAsync, proofOf, rule, ruleLabel, stratify, tupleKey, tupleKeyParts, variable, witnesses };
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];
@@ -70,6 +71,7 @@ function deriveOnDemand(rules, complete) {
70
71
  const demandOn = (relation, adornment) => `wanted:${nameOf(relation, adornment)}`;
71
72
  const rewritten = [];
72
73
  const demandDriven = /* @__PURE__ */ new Set();
74
+ const demands = [];
73
75
  const already = /* @__PURE__ */ new Set();
74
76
  const emit = (r) => {
75
77
  const key = JSON.stringify(r);
@@ -82,8 +84,14 @@ function deriveOnDemand(rules, complete) {
82
84
  if (anyBound(adorned.adornment)) {
83
85
  demandDriven.add(nameOf(adorned.relation, adorned.adornment));
84
86
  demandDriven.add(demandOn(adorned.relation, adorned.adornment));
87
+ demands.push({
88
+ relation: adorned.relation,
89
+ bound: adorned.adornment,
90
+ demand: demandOn(adorned.relation, adorned.adornment)
91
+ });
85
92
  }
86
- for (const r of rulesByHead.get(adorned.relation) ?? []) {
93
+ for (const written of rulesByHead.get(adorned.relation) ?? []) {
94
+ const r = boundFirst(written, adorned.adornment);
87
95
  const guard = anyBound(adorned.adornment) ? [
88
96
  positiveLiteral(
89
97
  demandOn(adorned.relation, adorned.adornment),
@@ -95,13 +103,14 @@ function deriveOnDemand(rules, complete) {
95
103
  r.body.forEach((literal, at) => {
96
104
  const adornment = adornments[at];
97
105
  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
- });
106
+ const demand = positiveLiteral(
107
+ demandOn(literal.relation, adornment),
108
+ boundTerms(literal.terms, adornment)
109
+ );
110
+ const premises = [...guard, ...body];
111
+ if (!sameLiteral(premises, demand)) {
112
+ emit({ head: demand, body: premises });
113
+ }
105
114
  }
106
115
  body.push(
107
116
  adornment === null ? literal : positiveLiteral(
@@ -120,7 +129,37 @@ function deriveOnDemand(rules, complete) {
120
129
  }
121
130
  }
122
131
  demandRewrites.add(rewritten);
123
- return { rules: rewritten, demandDriven: [...demandDriven] };
132
+ return { rules: rewritten, demandDriven: [...demandDriven], demands };
133
+ }
134
+ var sameLiteral = (premises, conclusion) => premises.length === 1 && JSON.stringify(premises[0]) === JSON.stringify(conclusion);
135
+ function boundFirst(r, headAdornment) {
136
+ const bound = /* @__PURE__ */ new Set();
137
+ r.head.terms.forEach((term, column) => {
138
+ if (headAdornment[column] && term.type === "variable") {
139
+ bound.add(term.name);
140
+ }
141
+ });
142
+ const boundColumns = (literal) => literal.terms.filter(
143
+ (term) => term.type === "constant" || bound.has(term.name)
144
+ ).length;
145
+ const remaining = [...r.body];
146
+ const body = [];
147
+ while (remaining.length > 0) {
148
+ let pick = 0;
149
+ for (let at = 1; at < remaining.length; at++) {
150
+ if (boundColumns(remaining[at]) > boundColumns(remaining[pick])) {
151
+ pick = at;
152
+ }
153
+ }
154
+ const [literal] = remaining.splice(pick, 1);
155
+ body.push(literal);
156
+ for (const term of literal.terms) {
157
+ if (term.type === "variable") {
158
+ bound.add(term.name);
159
+ }
160
+ }
161
+ }
162
+ return { head: r.head, body };
124
163
  }
125
164
  function bodyAdornments(r, headAdornment, derives) {
126
165
  const bound = /* @__PURE__ */ new Set();
@@ -303,7 +342,13 @@ function formatProfile(profile) {
303
342
 
304
343
  // src/tupleKey.ts
305
344
  var atomKey = (a) => typeof a === "number" ? `n${a}:` : `s${a.length}:${a}`;
306
- var tupleKey = (parts) => parts.map(atomKey).join("");
345
+ var tupleKey = (parts) => {
346
+ let key = "";
347
+ for (let i = 0; i < parts.length; i++) {
348
+ key += atomKey(parts[i]);
349
+ }
350
+ return key;
351
+ };
307
352
  function tupleKeyParts(key) {
308
353
  const parts = [];
309
354
  let at = 0;
@@ -437,7 +482,16 @@ var notLit = (relation, ...terms) => ({
437
482
  var rule = (relation, terms, body, name) => name === void 0 ? { head: { relation, terms }, body } : { head: { relation, terms }, body, name };
438
483
  var literalName = (literal) => literal.negated ? `!${literal.relation}` : literal.relation;
439
484
  var ruleLabel = (r) => r.name ?? `${r.head.relation} :- ${r.body.map(literalName).join(", ")}`;
440
- var keyOf2 = (tuple) => tupleKey(tuple);
485
+ var keys = /* @__PURE__ */ new WeakMap();
486
+ var keyOf2 = (tuple) => {
487
+ const known = keys.get(tuple);
488
+ if (known !== void 0) {
489
+ return known;
490
+ }
491
+ const key = tupleKey(tuple);
492
+ keys.set(tuple, key);
493
+ return key;
494
+ };
441
495
  function tagSlots(relation) {
442
496
  if (relation.tags === void 0) {
443
497
  relation.tags = /* @__PURE__ */ new Map();
@@ -569,9 +623,7 @@ var Database = class {
569
623
  relation.keys.delete(key);
570
624
  relation.tags?.delete(key);
571
625
  }
572
- relation.tuples = relation.tuples.filter(
573
- (tuple) => !going.has(keyOf2(tuple))
574
- );
626
+ relation.tuples = relation.keys.size === 0 ? [] : relation.tuples.filter((tuple) => !going.has(keyOf2(tuple)));
575
627
  relation.indexes.clear();
576
628
  forgetFacts(this, relationName, going);
577
629
  return going.size;
@@ -681,29 +733,90 @@ function headTuple(head, bindings) {
681
733
  return bound;
682
734
  });
683
735
  }
736
+ var variablesOf = (literal) => literal.terms.filter((term) => term.type === "variable").map((term) => term.name);
737
+ var plans = /* @__PURE__ */ new WeakMap();
738
+ function bodyPlan(r, deltaIndex) {
739
+ let byDelta = plans.get(r);
740
+ if (byDelta === void 0) {
741
+ byDelta = /* @__PURE__ */ new Map();
742
+ plans.set(r, byDelta);
743
+ }
744
+ const known = byDelta.get(deltaIndex);
745
+ if (known !== void 0) {
746
+ return known;
747
+ }
748
+ const positives = r.body.map((literal, index) => literal.negated ? -1 : index).filter((index) => index !== -1);
749
+ const deltaAt = positives[deltaIndex] ?? -1;
750
+ const computed = { deltaAt, order: planBodyOrder(r, deltaAt) };
751
+ byDelta.set(deltaIndex, computed);
752
+ return computed;
753
+ }
754
+ function planBodyOrder(r, deltaAt) {
755
+ const natural = r.body.map((_, i) => i);
756
+ if (deltaAt <= 0) {
757
+ return natural;
758
+ }
759
+ const taken = r.body.map(() => false);
760
+ const bound = /* @__PURE__ */ new Set();
761
+ const order = [];
762
+ const take = (index) => {
763
+ taken[index] = true;
764
+ order.push(index);
765
+ for (const name of variablesOf(r.body[index])) {
766
+ bound.add(name);
767
+ }
768
+ };
769
+ const firstWhere = (want) => natural.find((index) => !taken[index] && want(index)) ?? -1;
770
+ const nextLiteral = () => {
771
+ const ready = firstWhere(
772
+ (i) => r.body[i].negated && variablesOf(r.body[i]).every((name) => bound.has(name))
773
+ );
774
+ if (ready !== -1) {
775
+ return ready;
776
+ }
777
+ const joined = firstWhere(
778
+ (i) => !r.body[i].negated && variablesOf(r.body[i]).some((name) => bound.has(name))
779
+ );
780
+ if (joined !== -1) {
781
+ return joined;
782
+ }
783
+ const disconnected = firstWhere((i) => !r.body[i].negated);
784
+ if (disconnected !== -1) {
785
+ return disconnected;
786
+ }
787
+ return firstWhere(() => true);
788
+ };
789
+ take(deltaAt);
790
+ while (order.length < r.body.length) {
791
+ take(nextLiteral());
792
+ }
793
+ return order;
794
+ }
684
795
  function evaluateRule(db, deltas, r, deltaIndex) {
685
796
  const results = [];
686
- const step = (literalIndex, positiveIndex, bindings) => {
687
- if (literalIndex === r.body.length) {
797
+ const { deltaAt, order } = bodyPlan(r, deltaIndex);
798
+ const step = (orderIndex, bindings) => {
799
+ if (orderIndex === order.length) {
688
800
  results.push(headTuple(r.head, bindings));
689
801
  return;
690
802
  }
803
+ const literalIndex = order[orderIndex];
691
804
  const literal = r.body[literalIndex];
692
805
  if (literal.negated) {
693
806
  if (!db.has(literal.relation, groundNegated(literal, bindings))) {
694
- step(literalIndex + 1, positiveIndex, bindings);
807
+ step(orderIndex + 1, bindings);
695
808
  }
696
809
  return;
697
810
  }
698
- const source = positiveIndex === deltaIndex ? deltas.get(literal.relation) ?? [] : boundSource(db, literal, bindings);
811
+ const source = literalIndex === deltaAt ? deltas.get(literal.relation) ?? [] : boundSource(db, literal, bindings);
699
812
  for (const tuple of source) {
700
813
  const next = unify(literal, tuple, bindings);
701
814
  if (next !== NO_MATCH) {
702
- step(literalIndex + 1, positiveIndex + 1, next);
815
+ step(orderIndex + 1, next);
703
816
  }
704
817
  }
705
818
  };
706
- step(0, 0, null);
819
+ step(0, null);
707
820
  return results;
708
821
  }
709
822
  function boundSource(db, literal, bindings) {
@@ -809,7 +922,7 @@ function claimFact(db, relation, key) {
809
922
  state.derived.get(relation)?.delete(key);
810
923
  }
811
924
  }
812
- function forgetFacts(db, relation, keys) {
925
+ function forgetFacts(db, relation, keys2) {
813
926
  const states = evaluated.get(db);
814
927
  if (states === void 0) {
815
928
  return;
@@ -818,7 +931,7 @@ function forgetFacts(db, relation, keys) {
818
931
  state.marks = null;
819
932
  const ledger = state.derived.get(relation);
820
933
  if (ledger !== void 0) {
821
- for (const key of keys) {
934
+ for (const key of keys2) {
822
935
  ledger.delete(key);
823
936
  }
824
937
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suss/datalog",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
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": [