@suss/datalog 0.1.0 → 0.2.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
@@ -75,6 +75,84 @@ re-run with negated rules takes back what the previous pass derived and
75
75
  works the answer out again from the base facts. Both paths leave the
76
76
  database holding the answer for the facts it has now.
77
77
 
78
+ ## Deriving only what somebody asked for
79
+
80
+ A rule set written for a whole program derives every conclusion its
81
+ facts support, and a caller asking about one value reads a handful of
82
+ them. `deriveOnDemand` rewrites the rules so the ones nobody is waiting
83
+ on are never derived.
84
+
85
+ Name the relations that have to come out whole, and give each of them a
86
+ rule that starts at a base relation you assert:
87
+
88
+ ```ts
89
+ const rules = [
90
+ rule("reaches", [v("x"), v("y")], [lit("edge", v("x"), v("y"))]),
91
+ rule(
92
+ "reaches",
93
+ [v("x"), v("z")],
94
+ [lit("edge", v("x"), v("y")), lit("reaches", v("y"), v("z"))],
95
+ ),
96
+ rule(
97
+ "answer",
98
+ [v("x"), v("y")],
99
+ [lit("asked", v("x")), lit("reaches", v("x"), v("y"))],
100
+ ),
101
+ ];
102
+
103
+ const db = new Database();
104
+ db.add("edge", ["a", "b"]);
105
+ db.add("edge", ["b", "c"]);
106
+ db.add("edge", ["m", "n"]);
107
+ db.add("asked", ["a"]);
108
+
109
+ const program = deriveOnDemand(rules, ["answer"]);
110
+ evaluate(db, program.rules);
111
+ db.facts("answer"); // [["a", "b"], ["a", "c"]]
112
+ db.facts("reaches"); // the chain from a, and nothing from m
113
+ ```
114
+
115
+ The rewrite is magic sets. Each derived relation gains a companion
116
+ relation saying which of its rows something is waiting on, that
117
+ companion becomes a literal in the rule body, and demand travels down
118
+ each body the way the join binds variables. A relation nothing asks for
119
+ is not derived at all, so read back only the relations you named.
120
+
121
+ Two things this costs. The companion relations are stored like any
122
+ other, at a few tuples per value asked about, so a caller asking about
123
+ most of a program derives more rather than less. And negation is
124
+ refused: a relation derived only where somebody asked is smaller than
125
+ the one a negated literal was written against, which would make `not
126
+ p(x)` match where it did not.
127
+
128
+ ### Taking a question back
129
+
130
+ Demand is a fact, and a fact stays until somebody removes it. A caller
131
+ that asks a thousand questions of one database derives over all thousand
132
+ every time new facts arrive, so the last question costs a thousand
133
+ questions.
134
+
135
+ `program.demandDriven` names the relations the rewrite restricts, and
136
+ `clearRelations` empties them once an answer has been read:
137
+
138
+ ```ts
139
+ clearRelations(db, program.rules, [...program.demandDriven, "asked"]);
140
+
141
+ db.add("asked", ["m"]);
142
+ evaluate(db, program.rules);
143
+ db.facts("reaches"); // the chain from m, and nothing from a
144
+ db.facts("answer"); // a's answers, which nothing took away, plus m's
145
+ ```
146
+
147
+ `retract` would send the next run back to the base facts, since a fact
148
+ leaving the database can take away a conclusion drawn anywhere.
149
+ `clearRelations` keeps the resume, because a caller passing these
150
+ relations is saying nothing outside them was derived from them. That
151
+ holds for the relations `deriveOnDemand` restricts: every one of them is
152
+ derived under a demand fact, and only the relations you named as
153
+ complete read from them. Those keep what they hold, which is the answers
154
+ you already read.
155
+
78
156
  Keep the facts you add and the facts rules derive in separate relations.
79
157
  Taking a conclusion back cannot tell one from the other, and the
80
158
  separation is how Datalog is normally written anyway.
package/dist/index.cjs CHANGED
@@ -21,15 +21,322 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  Database: () => Database,
24
+ clearRelations: () => clearRelations,
24
25
  constant: () => constant,
26
+ deriveOnDemand: () => deriveOnDemand,
25
27
  evaluate: () => evaluate,
28
+ formatProfile: () => formatProfile,
26
29
  lit: () => lit,
27
30
  notLit: () => notLit,
31
+ profileEvaluation: () => profileEvaluation,
32
+ profileEvaluationAsync: () => profileEvaluationAsync,
28
33
  rule: () => rule,
29
34
  stratify: () => stratify,
30
35
  variable: () => variable
31
36
  });
32
37
  module.exports = __toCommonJS(index_exports);
38
+
39
+ // src/profile.ts
40
+ var open = [];
41
+ var isProfiling = () => open.length > 0;
42
+ var ruleKey = (head, body) => `${head} <- ${body.join(", ")}`;
43
+ function addCost(into, key, head, body, ms, derived) {
44
+ const existing = into.get(key);
45
+ if (existing === void 0) {
46
+ into.set(key, { head, body, ms, derived, attempts: 1 });
47
+ return;
48
+ }
49
+ existing.ms += ms;
50
+ existing.derived += derived;
51
+ existing.attempts += 1;
52
+ }
53
+ function chargeRule(ruleSet, head, body, ms, derived) {
54
+ const key = ruleKey(head, body);
55
+ for (const collector of open) {
56
+ addCost(collector.rules, key, head, body, ms, derived);
57
+ addCost(setIn(collector, ruleSet).rules, key, head, body, ms, derived);
58
+ }
59
+ }
60
+ function chargeEvaluation(ruleSet) {
61
+ for (const collector of open) {
62
+ collector.evaluations += 1;
63
+ setIn(collector, ruleSet).evaluations += 1;
64
+ }
65
+ }
66
+ function chargeRound(ruleSet) {
67
+ for (const collector of open) {
68
+ collector.rounds += 1;
69
+ setIn(collector, ruleSet).rounds += 1;
70
+ }
71
+ }
72
+ function setIn(collector, ruleSet) {
73
+ const existing = collector.ruleSets.get(ruleSet);
74
+ if (existing !== void 0) {
75
+ return existing;
76
+ }
77
+ const created = {
78
+ derives: new Set(ruleSet.split(", ")),
79
+ evaluations: 0,
80
+ rounds: 0,
81
+ rules: /* @__PURE__ */ new Map()
82
+ };
83
+ collector.ruleSets.set(ruleSet, created);
84
+ return created;
85
+ }
86
+ function chargeRelationSizes(entries, derivedRelations) {
87
+ const sizes = [...entries];
88
+ const derived = [...derivedRelations];
89
+ for (const collector of open) {
90
+ for (const [relation, tuples] of sizes) {
91
+ const seen = collector.relations.get(relation) ?? 0;
92
+ if (tuples > seen) {
93
+ collector.relations.set(relation, tuples);
94
+ }
95
+ }
96
+ for (const relation of derived) {
97
+ collector.derivedRelations.add(relation);
98
+ }
99
+ }
100
+ }
101
+ function profileEvaluation(fn) {
102
+ const mine = openScope();
103
+ try {
104
+ return { result: fn(), profile: summarise(mine) };
105
+ } finally {
106
+ closeScope(mine);
107
+ }
108
+ }
109
+ async function profileEvaluationAsync(fn) {
110
+ const mine = openScope();
111
+ try {
112
+ return { result: await fn(), profile: summarise(mine) };
113
+ } finally {
114
+ closeScope(mine);
115
+ }
116
+ }
117
+ function openScope() {
118
+ const mine = {
119
+ startedAt: performance.now(),
120
+ rounds: 0,
121
+ evaluations: 0,
122
+ rules: /* @__PURE__ */ new Map(),
123
+ relations: /* @__PURE__ */ new Map(),
124
+ derivedRelations: /* @__PURE__ */ new Set(),
125
+ ruleSets: /* @__PURE__ */ new Map()
126
+ };
127
+ open.push(mine);
128
+ return mine;
129
+ }
130
+ function closeScope(mine) {
131
+ const at = open.lastIndexOf(mine);
132
+ if (at !== -1) {
133
+ open.splice(at, 1);
134
+ }
135
+ }
136
+ var totalMs = (rules) => {
137
+ let ms = 0;
138
+ for (const cost of rules) {
139
+ ms += cost.ms;
140
+ }
141
+ return ms;
142
+ };
143
+ var byCost = (a, b) => b.ms - a.ms;
144
+ function summarise(mine) {
145
+ return {
146
+ wallMs: performance.now() - mine.startedAt,
147
+ datalogMs: totalMs(mine.rules.values()),
148
+ rounds: mine.rounds,
149
+ evaluations: mine.evaluations,
150
+ relations: [...mine.relations].map(([relation, tuples]) => ({
151
+ relation,
152
+ tuples,
153
+ derived: mine.derivedRelations.has(relation)
154
+ })).sort((a, b) => b.tuples - a.tuples),
155
+ rules: [...mine.rules.values()].sort(byCost),
156
+ ruleSets: [...mine.ruleSets.values()].map((set) => ({
157
+ derives: [...set.derives],
158
+ ms: totalMs(set.rules.values()),
159
+ evaluations: set.evaluations,
160
+ rounds: set.rounds,
161
+ rules: [...set.rules.values()].sort(byCost)
162
+ })).sort((a, b) => b.ms - a.ms)
163
+ };
164
+ }
165
+ var share = (part, whole) => `${whole === 0 ? "0.0" : (part / whole * 100).toFixed(1)}%`.padStart(6);
166
+ function ruleLines(rules, datalogMs) {
167
+ return rules.slice(0, 20).map((r) => {
168
+ const ms = `${r.ms.toFixed(0)}ms`.padStart(8);
169
+ const derived = String(r.derived).padStart(7);
170
+ return ` ${ms} ${share(r.ms, datalogMs)} ${derived} tuples ${r.attempts} attempts ${ruleKey(r.head, r.body)}`;
171
+ });
172
+ }
173
+ function formatProfile(profile) {
174
+ const lines = [];
175
+ lines.push(
176
+ `datalog: ${profile.datalogMs.toFixed(0)}ms (${share(profile.datalogMs, profile.wallMs).trim()} of ${profile.wallMs.toFixed(0)}ms wall), ${profile.evaluations} evaluations, ${profile.rounds} rounds`
177
+ );
178
+ const derivedTuples = profile.relations.filter((r) => r.derived).reduce((sum, r) => sum + r.tuples, 0);
179
+ lines.push(" tuples by relation (d = derived by a rule):");
180
+ for (const { relation, tuples, derived } of profile.relations.slice(0, 20)) {
181
+ const mark = derived ? "d" : " ";
182
+ const of = derived ? share(tuples, derivedTuples) : " -";
183
+ lines.push(` ${mark} ${String(tuples).padStart(8)} ${of} ${relation}`);
184
+ }
185
+ lines.push(" time by rule:");
186
+ lines.push(...ruleLines(profile.rules, profile.datalogMs));
187
+ if (profile.ruleSets.length > 1) {
188
+ lines.push(" by rule set:");
189
+ for (const set of profile.ruleSets) {
190
+ lines.push(
191
+ ` ${`${set.ms.toFixed(0)}ms`.padStart(8)} ${share(set.ms, profile.datalogMs)} ${set.evaluations} evaluations, ${set.rounds} rounds ${set.derives.join(", ")}`
192
+ );
193
+ }
194
+ }
195
+ return lines.join("\n");
196
+ }
197
+
198
+ // src/onDemand.ts
199
+ var adornmentKey = (adornment) => adornment.map((bound) => bound ? "b" : "f").join("");
200
+ var anyBound = (adornment) => adornment.some(Boolean);
201
+ var keyOf = ({ relation, adornment }) => `${relation}/${adornmentKey(adornment)}`;
202
+ var boundTerms = (terms, adornment) => terms.filter((_, column) => adornment[column]);
203
+ var positiveLiteral = (relation, terms) => ({
204
+ relation,
205
+ terms,
206
+ negated: false
207
+ });
208
+ function deriveOnDemand(rules, complete) {
209
+ const negated = rules.find((r) => r.body.some((l) => l.negated));
210
+ if (negated !== void 0) {
211
+ throw new Error(
212
+ `cannot derive "${negated.head.relation}" on demand: its rule uses negation, and a relation derived only where somebody asked can make a negated literal match where it did not`
213
+ );
214
+ }
215
+ const rulesByHead = /* @__PURE__ */ new Map();
216
+ for (const r of rules) {
217
+ const existing = rulesByHead.get(r.head.relation);
218
+ if (existing === void 0) {
219
+ rulesByHead.set(r.head.relation, [r]);
220
+ } else {
221
+ existing.push(r);
222
+ }
223
+ }
224
+ const derives = (relation) => rulesByHead.has(relation);
225
+ const asked = /* @__PURE__ */ new Map();
226
+ const pending = [];
227
+ const want = (adorned) => {
228
+ const key = keyOf(adorned);
229
+ if (!asked.has(key)) {
230
+ asked.set(key, adorned);
231
+ pending.push(adorned);
232
+ }
233
+ };
234
+ for (const relation of complete) {
235
+ const derivedBy = rulesByHead.get(relation);
236
+ if (derivedBy === void 0) {
237
+ throw new Error(
238
+ `no rule derives "${relation}", so it cannot be asked for in full`
239
+ );
240
+ }
241
+ want({ relation, adornment: derivedBy[0].head.terms.map(() => false) });
242
+ }
243
+ while (pending.length > 0) {
244
+ const adorned = pending.pop();
245
+ for (const r of rulesByHead.get(adorned.relation) ?? []) {
246
+ const adornments = bodyAdornments(r, adorned.adornment, derives);
247
+ r.body.forEach((literal, at) => {
248
+ const adornment = adornments[at];
249
+ if (adornment !== null) {
250
+ want({ relation: literal.relation, adornment });
251
+ }
252
+ });
253
+ }
254
+ }
255
+ const variantsOf = /* @__PURE__ */ new Map();
256
+ for (const { relation } of asked.values()) {
257
+ variantsOf.set(relation, (variantsOf.get(relation) ?? 0) + 1);
258
+ }
259
+ const nameOf = (relation, adornment) => {
260
+ if (!anyBound(adornment) || variantsOf.get(relation) === 1) {
261
+ return relation;
262
+ }
263
+ return `${relation}:${adornmentKey(adornment)}`;
264
+ };
265
+ const demandOn = (relation, adornment) => `wanted:${nameOf(relation, adornment)}`;
266
+ const rewritten = [];
267
+ const demandDriven = /* @__PURE__ */ new Set();
268
+ const already = /* @__PURE__ */ new Set();
269
+ const emit = (r) => {
270
+ const key = JSON.stringify(r);
271
+ if (!already.has(key)) {
272
+ already.add(key);
273
+ rewritten.push(r);
274
+ }
275
+ };
276
+ for (const adorned of asked.values()) {
277
+ if (anyBound(adorned.adornment)) {
278
+ demandDriven.add(nameOf(adorned.relation, adorned.adornment));
279
+ demandDriven.add(demandOn(adorned.relation, adorned.adornment));
280
+ }
281
+ for (const r of rulesByHead.get(adorned.relation) ?? []) {
282
+ const guard = anyBound(adorned.adornment) ? [
283
+ positiveLiteral(
284
+ demandOn(adorned.relation, adorned.adornment),
285
+ boundTerms(r.head.terms, adorned.adornment)
286
+ )
287
+ ] : [];
288
+ const adornments = bodyAdornments(r, adorned.adornment, derives);
289
+ const body = [];
290
+ r.body.forEach((literal, at) => {
291
+ const adornment = adornments[at];
292
+ if (adornment !== null && anyBound(adornment)) {
293
+ emit({
294
+ head: {
295
+ relation: demandOn(literal.relation, adornment),
296
+ terms: boundTerms(literal.terms, adornment)
297
+ },
298
+ body: [...guard, ...body]
299
+ });
300
+ }
301
+ body.push(
302
+ adornment === null ? literal : positiveLiteral(
303
+ nameOf(literal.relation, adornment),
304
+ literal.terms
305
+ )
306
+ );
307
+ });
308
+ emit({
309
+ head: {
310
+ relation: nameOf(adorned.relation, adorned.adornment),
311
+ terms: r.head.terms
312
+ },
313
+ body: [...guard, ...body]
314
+ });
315
+ }
316
+ }
317
+ return { rules: rewritten, demandDriven: [...demandDriven] };
318
+ }
319
+ function bodyAdornments(r, headAdornment, derives) {
320
+ const bound = /* @__PURE__ */ new Set();
321
+ r.head.terms.forEach((term, column) => {
322
+ if (headAdornment[column] && term.type === "variable") {
323
+ bound.add(term.name);
324
+ }
325
+ });
326
+ return r.body.map((literal) => {
327
+ const adornment = derives(literal.relation) ? literal.terms.map(
328
+ (term) => term.type === "constant" || bound.has(term.name)
329
+ ) : null;
330
+ for (const term of literal.terms) {
331
+ if (term.type === "variable") {
332
+ bound.add(term.name);
333
+ }
334
+ }
335
+ return adornment;
336
+ });
337
+ }
338
+
339
+ // src/index.ts
33
340
  var variable = (name) => ({ type: "variable", name });
34
341
  var constant = (value) => ({ type: "constant", value });
35
342
  var lit = (relation, ...terms) => ({
@@ -43,9 +350,8 @@ var notLit = (relation, ...terms) => ({
43
350
  negated: true
44
351
  });
45
352
  var rule = (relation, terms, body) => ({ head: { relation, terms }, body });
46
- var SEP = "\0";
47
- var atomKey = (a) => `${typeof a === "number" ? "n" : "s"}${a}`;
48
- var keyOf = (tuple) => tuple.map(atomKey).join(SEP);
353
+ var atomKey = (a) => typeof a === "number" ? `n${a}:` : `s${a.length}:${a}`;
354
+ var keyOf2 = (tuple) => tuple.map(atomKey).join("");
49
355
  function addToBucket(index, key, tuple) {
50
356
  const bucket = index.get(key);
51
357
  if (bucket === void 0) {
@@ -72,7 +378,7 @@ var Database = class {
72
378
  /** Add a fact; returns true when the tuple is new. */
73
379
  add(relationName, tuple) {
74
380
  const relation = this.relation(relationName);
75
- const key = keyOf(tuple);
381
+ const key = keyOf2(tuple);
76
382
  if (relation.keys.has(key)) {
77
383
  if (!isDeriving(this)) {
78
384
  claimFact(this, relationName, key);
@@ -90,7 +396,7 @@ var Database = class {
90
396
  return true;
91
397
  }
92
398
  has(relationName, tuple) {
93
- return this.store.get(relationName)?.keys.has(keyOf(tuple)) ?? false;
399
+ return this.store.get(relationName)?.keys.has(keyOf2(tuple)) ?? false;
94
400
  }
95
401
  facts(relationName) {
96
402
  return this.store.get(relationName)?.tuples ?? [];
@@ -136,7 +442,7 @@ var Database = class {
136
442
  }
137
443
  const going = /* @__PURE__ */ new Set();
138
444
  for (const tuple of tuples) {
139
- const key = keyOf(tuple);
445
+ const key = keyOf2(tuple);
140
446
  if (relation.keys.has(key)) {
141
447
  going.add(key);
142
448
  }
@@ -148,7 +454,7 @@ var Database = class {
148
454
  relation.keys.delete(key);
149
455
  }
150
456
  relation.tuples = relation.tuples.filter(
151
- (tuple) => !going.has(keyOf(tuple))
457
+ (tuple) => !going.has(keyOf2(tuple))
152
458
  );
153
459
  relation.indexes.clear();
154
460
  forgetFacts(this, relationName, going);
@@ -358,8 +664,40 @@ function evaluate(db, rules) {
358
664
  }
359
665
  }
360
666
  }
667
+ function clearRelations(db, rules, relations) {
668
+ for (const relation of relations) {
669
+ db.retract(relation, [...db.facts(relation)]);
670
+ }
671
+ const state = statesFor(db).get(signatureOf(rules));
672
+ if (state !== void 0) {
673
+ state.marks = currentMarks(db);
674
+ }
675
+ }
676
+ var signatureOf = (rules) => shapeOf(rules).signature;
677
+ var shapes = /* @__PURE__ */ new WeakMap();
678
+ function shapeOf(rules) {
679
+ const known = shapes.get(rules);
680
+ if (known !== void 0) {
681
+ return known;
682
+ }
683
+ const derivedRelations = [...new Set(rules.map((r) => r.head.relation))];
684
+ const shape = {
685
+ signature: JSON.stringify(rules),
686
+ name: [...derivedRelations].sort().join(", "),
687
+ derivedRelations,
688
+ strata: stratify(rules)
689
+ };
690
+ shapes.set(rules, shape);
691
+ return shape;
692
+ }
361
693
  function runRules(db, rules) {
362
- const signature = JSON.stringify(rules);
694
+ const {
695
+ signature,
696
+ name: ruleSetName,
697
+ derivedRelations,
698
+ strata
699
+ } = shapeOf(rules);
700
+ chargeEvaluation(ruleSetName);
363
701
  const states = statesFor(db);
364
702
  const state = states.get(signature) ?? {
365
703
  marks: null,
@@ -374,7 +712,7 @@ function runRules(db, rules) {
374
712
  }
375
713
  const derived = state.derived;
376
714
  const marks = canResume(rules, state) ? state.marks : void 0;
377
- for (const stratum of stratify(rules)) {
715
+ for (const stratum of strata) {
378
716
  let delta = /* @__PURE__ */ new Map();
379
717
  const derivedHere = new Set(stratum.map((r) => r.head.relation));
380
718
  const record = (relation, tuple) => {
@@ -385,8 +723,28 @@ function runRules(db, rules) {
385
723
  ledger = /* @__PURE__ */ new Map();
386
724
  derived.set(relation, ledger);
387
725
  }
388
- ledger.set(keyOf(tuple), tuple);
726
+ ledger.set(keyOf2(tuple), tuple);
727
+ }
728
+ };
729
+ const runOneRule = (r, seed, deltaIndex) => {
730
+ if (!isProfiling()) {
731
+ for (const tuple of evaluateRule(db, seed, r, deltaIndex)) {
732
+ record(r.head.relation, tuple);
733
+ }
734
+ return;
735
+ }
736
+ const startedAt = performance.now();
737
+ const before = db.size(r.head.relation);
738
+ for (const tuple of evaluateRule(db, seed, r, deltaIndex)) {
739
+ record(r.head.relation, tuple);
389
740
  }
741
+ chargeRule(
742
+ ruleSetName,
743
+ r.head.relation,
744
+ r.body.map((l) => l.negated ? `!${l.relation}` : l.relation),
745
+ performance.now() - startedAt,
746
+ db.size(r.head.relation) - before
747
+ );
390
748
  };
391
749
  const applyDelta = (seed, derivedOnly) => {
392
750
  for (const r of stratum) {
@@ -399,9 +757,7 @@ function runRules(db, rules) {
399
757
  if ((seed.get(literal.relation) ?? []).length === 0) {
400
758
  continue;
401
759
  }
402
- for (const tuple of evaluateRule(db, seed, r, i)) {
403
- record(r.head.relation, tuple);
404
- }
760
+ runOneRule(r, seed, i);
405
761
  }
406
762
  }
407
763
  };
@@ -413,20 +769,27 @@ function runRules(db, rules) {
413
769
  all.set(l.relation, db.facts(l.relation));
414
770
  }
415
771
  }
416
- for (const tuple of evaluateRule(db, all, r, -1)) {
417
- record(r.head.relation, tuple);
418
- }
772
+ runOneRule(r, all, -1);
419
773
  }
774
+ chargeRound(ruleSetName);
420
775
  } else {
421
776
  applyDelta(factsSince(db, marks), false);
777
+ chargeRound(ruleSetName);
422
778
  }
423
779
  while (delta.size > 0) {
424
780
  const lastDelta = delta;
425
781
  delta = /* @__PURE__ */ new Map();
426
782
  applyDelta(lastDelta, true);
783
+ chargeRound(ruleSetName);
427
784
  }
428
785
  }
429
786
  state.marks = currentMarks(db);
787
+ if (isProfiling()) {
788
+ chargeRelationSizes(
789
+ db.relationNames().map((name) => [name, db.size(name)]),
790
+ derivedRelations
791
+ );
792
+ }
430
793
  return db;
431
794
  }
432
795
  function addTo(buckets, relation, tuple) {
@@ -440,10 +803,15 @@ function addTo(buckets, relation, tuple) {
440
803
  // Annotate the CommonJS export names for ESM import in node:
441
804
  0 && (module.exports = {
442
805
  Database,
806
+ clearRelations,
443
807
  constant,
808
+ deriveOnDemand,
444
809
  evaluate,
810
+ formatProfile,
445
811
  lit,
446
812
  notLit,
813
+ profileEvaluation,
814
+ profileEvaluationAsync,
447
815
  rule,
448
816
  stratify,
449
817
  variable
package/dist/index.d.cts CHANGED
@@ -1,3 +1,113 @@
1
+ /** A rewritten rule set, and which of its relations demand restricts. */
2
+ interface OnDemandRules {
3
+ /** The rewritten rules, ready to evaluate. */
4
+ rules: Rule[];
5
+ /**
6
+ * The relations derived only where a demand fact reaches them, the
7
+ * demand relations included. Every one of these is empty until
8
+ * somebody asks, and only the complete relations are derived from
9
+ * them, so a caller between questions can clear the lot and keep the
10
+ * answers.
11
+ */
12
+ demandDriven: string[];
13
+ }
14
+ /**
15
+ * Rewrite `rules` so the relations named in `complete` still come out
16
+ * whole and everything else is derived only where those relations reach.
17
+ *
18
+ * Demand enters as ordinary facts. Give a complete relation a rule whose
19
+ * first literal is a base relation the caller asserts, as in `answer(x,
20
+ * z) <- asked(x), comesTo(x, z)`, and the rewrite turns each `asked`
21
+ * fact into demand for one value's chain.
22
+ *
23
+ * Relations no complete relation reaches are dropped, so a caller reading
24
+ * a relation it did not name gets nothing. Negation is rejected: a
25
+ * demand-restricted relation is smaller than the one a negated literal
26
+ * was written against, and a smaller relation makes `not p(x)` true where
27
+ * it was false.
28
+ */
29
+ declare function deriveOnDemand(rules: Rule[], complete: readonly string[]): OnDemandRules;
30
+
31
+ /** What one rule cost, summed over every attempt across every round. */
32
+ interface RuleCost {
33
+ /** The relation this rule derives. */
34
+ head: string;
35
+ /** The body relations, in order, so a rule is recognisable in a report. */
36
+ body: string[];
37
+ /** Wall time spent evaluating this rule. */
38
+ ms: number;
39
+ /** Tuples this rule added that no earlier derivation had reached. */
40
+ derived: number;
41
+ /** How many times the rule was evaluated, over all rounds and deltas. */
42
+ attempts: number;
43
+ }
44
+ /**
45
+ * How big a relation grew. `derived` separates the relations rules
46
+ * concluded from the base facts an adapter emitted, because "derive fewer
47
+ * tuples" is advice about the first kind and meaningless about the second.
48
+ */
49
+ interface RelationSize {
50
+ relation: string;
51
+ tuples: number;
52
+ derived: boolean;
53
+ }
54
+ /**
55
+ * What one rule set cost. A run evaluates several over several databases,
56
+ * and somebody comparing two changes wants to know which of them moved,
57
+ * so the totals are kept apart as well as summed.
58
+ */
59
+ interface RuleSetCost {
60
+ /** The relations this rule set derives, which is how to tell them apart. */
61
+ derives: string[];
62
+ ms: number;
63
+ evaluations: number;
64
+ rounds: number;
65
+ rules: RuleCost[];
66
+ }
67
+ interface EvaluationProfile {
68
+ /** Wall time for everything the profiled scope did, datalog or not. */
69
+ wallMs: number;
70
+ /** Wall time inside rule evaluation, which is the engine's own cost. */
71
+ datalogMs: number;
72
+ /** Semi-naive rounds, summed over strata. The seed round counts as one. */
73
+ rounds: number;
74
+ /**
75
+ * How many times a caller asked for a fixpoint. A caller that queries
76
+ * as it goes evaluates once per query, and each of those re-joins the
77
+ * whole relation against a small delta, so this number rising with
78
+ * corpus size is worth more attention than the rule table.
79
+ */
80
+ evaluations: number;
81
+ /** Final tuple count per relation, largest first. */
82
+ relations: RelationSize[];
83
+ /** Per-rule cost across every rule set, most expensive first. */
84
+ rules: RuleCost[];
85
+ /** The same time, split by the rule set that spent it. */
86
+ ruleSets: RuleSetCost[];
87
+ }
88
+ /**
89
+ * Run `fn` with evaluation profiling on, and hand back what it returned
90
+ * alongside the numbers. Nested calls share the outer collector, so a
91
+ * caller that profiles a whole extraction gets one profile covering every
92
+ * `evaluate` inside it rather than a profile per call.
93
+ */
94
+ declare function profileEvaluation<T>(fn: () => T): {
95
+ result: T;
96
+ profile: EvaluationProfile;
97
+ };
98
+ /**
99
+ * The async twin, for callers whose extraction is a promise. The scope
100
+ * stays open across every await inside `fn`, so anything else that
101
+ * evaluates rules on the same thread meanwhile is charged here too. One
102
+ * extraction at a time is the case this is built for.
103
+ */
104
+ declare function profileEvaluationAsync<T>(fn: () => Promise<T>): Promise<{
105
+ result: T;
106
+ profile: EvaluationProfile;
107
+ }>;
108
+ /** Render a profile as the table a human reads in a terminal. */
109
+ declare function formatProfile(profile: EvaluationProfile): string;
110
+
1
111
  /** Tuple values. Callers intern richer identities (AST nodes, summaries) to atoms. */
2
112
  type Atom = string | number;
3
113
  type Tuple = readonly Atom[];
@@ -76,5 +186,27 @@ declare function stratify(rules: Rule[]): Rule[][];
76
186
  * proportional to new facts, not all facts.
77
187
  */
78
188
  declare function evaluate(db: Database, rules: Rule[]): Database;
189
+ /**
190
+ * Empty these relations, and leave `rules` able to carry on from where
191
+ * it got to.
192
+ *
193
+ * `retract` cannot do this. A fact leaving the database can take away a
194
+ * conclusion drawn anywhere, so it sends the next run back to the base
195
+ * facts. A caller here is saying something stronger than "these facts
196
+ * are gone": nothing outside `relations` was derived from them, so what
197
+ * is left is already the fixpoint and the next run has only the facts
198
+ * that arrive after this to work through.
199
+ *
200
+ * That is what a demand-driven rule set gives a caller between
201
+ * questions. Every relation `deriveOnDemand` restricts is derived under
202
+ * a demand fact, so clearing the demand together with everything under
203
+ * it costs one question's worth of derivation rather than every
204
+ * question asked so far. The relations named as complete keep what they
205
+ * hold, which is the answers a caller has already read.
206
+ *
207
+ * Any other rule set over the same database does start over, since a
208
+ * relation it derived from may be one of these.
209
+ */
210
+ declare function clearRelations(db: Database, rules: Rule[], relations: readonly string[]): void;
79
211
 
80
- export { type Atom, Database, type Literal, type Rule, type Term, type Tuple, constant, evaluate, lit, notLit, rule, stratify, variable };
212
+ export { type Atom, Database, type EvaluationProfile, type Literal, type OnDemandRules, type Rule, type RuleCost, type Term, type Tuple, clearRelations, constant, deriveOnDemand, evaluate, formatProfile, lit, notLit, profileEvaluation, profileEvaluationAsync, rule, stratify, variable };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,113 @@
1
+ /** A rewritten rule set, and which of its relations demand restricts. */
2
+ interface OnDemandRules {
3
+ /** The rewritten rules, ready to evaluate. */
4
+ rules: Rule[];
5
+ /**
6
+ * The relations derived only where a demand fact reaches them, the
7
+ * demand relations included. Every one of these is empty until
8
+ * somebody asks, and only the complete relations are derived from
9
+ * them, so a caller between questions can clear the lot and keep the
10
+ * answers.
11
+ */
12
+ demandDriven: string[];
13
+ }
14
+ /**
15
+ * Rewrite `rules` so the relations named in `complete` still come out
16
+ * whole and everything else is derived only where those relations reach.
17
+ *
18
+ * Demand enters as ordinary facts. Give a complete relation a rule whose
19
+ * first literal is a base relation the caller asserts, as in `answer(x,
20
+ * z) <- asked(x), comesTo(x, z)`, and the rewrite turns each `asked`
21
+ * fact into demand for one value's chain.
22
+ *
23
+ * Relations no complete relation reaches are dropped, so a caller reading
24
+ * a relation it did not name gets nothing. Negation is rejected: a
25
+ * demand-restricted relation is smaller than the one a negated literal
26
+ * was written against, and a smaller relation makes `not p(x)` true where
27
+ * it was false.
28
+ */
29
+ declare function deriveOnDemand(rules: Rule[], complete: readonly string[]): OnDemandRules;
30
+
31
+ /** What one rule cost, summed over every attempt across every round. */
32
+ interface RuleCost {
33
+ /** The relation this rule derives. */
34
+ head: string;
35
+ /** The body relations, in order, so a rule is recognisable in a report. */
36
+ body: string[];
37
+ /** Wall time spent evaluating this rule. */
38
+ ms: number;
39
+ /** Tuples this rule added that no earlier derivation had reached. */
40
+ derived: number;
41
+ /** How many times the rule was evaluated, over all rounds and deltas. */
42
+ attempts: number;
43
+ }
44
+ /**
45
+ * How big a relation grew. `derived` separates the relations rules
46
+ * concluded from the base facts an adapter emitted, because "derive fewer
47
+ * tuples" is advice about the first kind and meaningless about the second.
48
+ */
49
+ interface RelationSize {
50
+ relation: string;
51
+ tuples: number;
52
+ derived: boolean;
53
+ }
54
+ /**
55
+ * What one rule set cost. A run evaluates several over several databases,
56
+ * and somebody comparing two changes wants to know which of them moved,
57
+ * so the totals are kept apart as well as summed.
58
+ */
59
+ interface RuleSetCost {
60
+ /** The relations this rule set derives, which is how to tell them apart. */
61
+ derives: string[];
62
+ ms: number;
63
+ evaluations: number;
64
+ rounds: number;
65
+ rules: RuleCost[];
66
+ }
67
+ interface EvaluationProfile {
68
+ /** Wall time for everything the profiled scope did, datalog or not. */
69
+ wallMs: number;
70
+ /** Wall time inside rule evaluation, which is the engine's own cost. */
71
+ datalogMs: number;
72
+ /** Semi-naive rounds, summed over strata. The seed round counts as one. */
73
+ rounds: number;
74
+ /**
75
+ * How many times a caller asked for a fixpoint. A caller that queries
76
+ * as it goes evaluates once per query, and each of those re-joins the
77
+ * whole relation against a small delta, so this number rising with
78
+ * corpus size is worth more attention than the rule table.
79
+ */
80
+ evaluations: number;
81
+ /** Final tuple count per relation, largest first. */
82
+ relations: RelationSize[];
83
+ /** Per-rule cost across every rule set, most expensive first. */
84
+ rules: RuleCost[];
85
+ /** The same time, split by the rule set that spent it. */
86
+ ruleSets: RuleSetCost[];
87
+ }
88
+ /**
89
+ * Run `fn` with evaluation profiling on, and hand back what it returned
90
+ * alongside the numbers. Nested calls share the outer collector, so a
91
+ * caller that profiles a whole extraction gets one profile covering every
92
+ * `evaluate` inside it rather than a profile per call.
93
+ */
94
+ declare function profileEvaluation<T>(fn: () => T): {
95
+ result: T;
96
+ profile: EvaluationProfile;
97
+ };
98
+ /**
99
+ * The async twin, for callers whose extraction is a promise. The scope
100
+ * stays open across every await inside `fn`, so anything else that
101
+ * evaluates rules on the same thread meanwhile is charged here too. One
102
+ * extraction at a time is the case this is built for.
103
+ */
104
+ declare function profileEvaluationAsync<T>(fn: () => Promise<T>): Promise<{
105
+ result: T;
106
+ profile: EvaluationProfile;
107
+ }>;
108
+ /** Render a profile as the table a human reads in a terminal. */
109
+ declare function formatProfile(profile: EvaluationProfile): string;
110
+
1
111
  /** Tuple values. Callers intern richer identities (AST nodes, summaries) to atoms. */
2
112
  type Atom = string | number;
3
113
  type Tuple = readonly Atom[];
@@ -76,5 +186,27 @@ declare function stratify(rules: Rule[]): Rule[][];
76
186
  * proportional to new facts, not all facts.
77
187
  */
78
188
  declare function evaluate(db: Database, rules: Rule[]): Database;
189
+ /**
190
+ * Empty these relations, and leave `rules` able to carry on from where
191
+ * it got to.
192
+ *
193
+ * `retract` cannot do this. A fact leaving the database can take away a
194
+ * conclusion drawn anywhere, so it sends the next run back to the base
195
+ * facts. A caller here is saying something stronger than "these facts
196
+ * are gone": nothing outside `relations` was derived from them, so what
197
+ * is left is already the fixpoint and the next run has only the facts
198
+ * that arrive after this to work through.
199
+ *
200
+ * That is what a demand-driven rule set gives a caller between
201
+ * questions. Every relation `deriveOnDemand` restricts is derived under
202
+ * a demand fact, so clearing the demand together with everything under
203
+ * it costs one question's worth of derivation rather than every
204
+ * question asked so far. The relations named as complete keep what they
205
+ * hold, which is the answers a caller has already read.
206
+ *
207
+ * Any other rule set over the same database does start over, since a
208
+ * relation it derived from may be one of these.
209
+ */
210
+ declare function clearRelations(db: Database, rules: Rule[], relations: readonly string[]): void;
79
211
 
80
- export { type Atom, Database, type Literal, type Rule, type Term, type Tuple, constant, evaluate, lit, notLit, rule, stratify, variable };
212
+ export { type Atom, Database, type EvaluationProfile, type Literal, type OnDemandRules, type Rule, type RuleCost, type Term, type Tuple, clearRelations, constant, deriveOnDemand, evaluate, formatProfile, lit, notLit, profileEvaluation, profileEvaluationAsync, rule, stratify, variable };
package/dist/index.js CHANGED
@@ -1,3 +1,303 @@
1
+ // src/profile.ts
2
+ var open = [];
3
+ var isProfiling = () => open.length > 0;
4
+ var ruleKey = (head, body) => `${head} <- ${body.join(", ")}`;
5
+ function addCost(into, key, head, body, ms, derived) {
6
+ const existing = into.get(key);
7
+ if (existing === void 0) {
8
+ into.set(key, { head, body, ms, derived, attempts: 1 });
9
+ return;
10
+ }
11
+ existing.ms += ms;
12
+ existing.derived += derived;
13
+ existing.attempts += 1;
14
+ }
15
+ function chargeRule(ruleSet, head, body, ms, derived) {
16
+ const key = ruleKey(head, body);
17
+ for (const collector of open) {
18
+ addCost(collector.rules, key, head, body, ms, derived);
19
+ addCost(setIn(collector, ruleSet).rules, key, head, body, ms, derived);
20
+ }
21
+ }
22
+ function chargeEvaluation(ruleSet) {
23
+ for (const collector of open) {
24
+ collector.evaluations += 1;
25
+ setIn(collector, ruleSet).evaluations += 1;
26
+ }
27
+ }
28
+ function chargeRound(ruleSet) {
29
+ for (const collector of open) {
30
+ collector.rounds += 1;
31
+ setIn(collector, ruleSet).rounds += 1;
32
+ }
33
+ }
34
+ function setIn(collector, ruleSet) {
35
+ const existing = collector.ruleSets.get(ruleSet);
36
+ if (existing !== void 0) {
37
+ return existing;
38
+ }
39
+ const created = {
40
+ derives: new Set(ruleSet.split(", ")),
41
+ evaluations: 0,
42
+ rounds: 0,
43
+ rules: /* @__PURE__ */ new Map()
44
+ };
45
+ collector.ruleSets.set(ruleSet, created);
46
+ return created;
47
+ }
48
+ function chargeRelationSizes(entries, derivedRelations) {
49
+ const sizes = [...entries];
50
+ const derived = [...derivedRelations];
51
+ for (const collector of open) {
52
+ for (const [relation, tuples] of sizes) {
53
+ const seen = collector.relations.get(relation) ?? 0;
54
+ if (tuples > seen) {
55
+ collector.relations.set(relation, tuples);
56
+ }
57
+ }
58
+ for (const relation of derived) {
59
+ collector.derivedRelations.add(relation);
60
+ }
61
+ }
62
+ }
63
+ function profileEvaluation(fn) {
64
+ const mine = openScope();
65
+ try {
66
+ return { result: fn(), profile: summarise(mine) };
67
+ } finally {
68
+ closeScope(mine);
69
+ }
70
+ }
71
+ async function profileEvaluationAsync(fn) {
72
+ const mine = openScope();
73
+ try {
74
+ return { result: await fn(), profile: summarise(mine) };
75
+ } finally {
76
+ closeScope(mine);
77
+ }
78
+ }
79
+ function openScope() {
80
+ const mine = {
81
+ startedAt: performance.now(),
82
+ rounds: 0,
83
+ evaluations: 0,
84
+ rules: /* @__PURE__ */ new Map(),
85
+ relations: /* @__PURE__ */ new Map(),
86
+ derivedRelations: /* @__PURE__ */ new Set(),
87
+ ruleSets: /* @__PURE__ */ new Map()
88
+ };
89
+ open.push(mine);
90
+ return mine;
91
+ }
92
+ function closeScope(mine) {
93
+ const at = open.lastIndexOf(mine);
94
+ if (at !== -1) {
95
+ open.splice(at, 1);
96
+ }
97
+ }
98
+ var totalMs = (rules) => {
99
+ let ms = 0;
100
+ for (const cost of rules) {
101
+ ms += cost.ms;
102
+ }
103
+ return ms;
104
+ };
105
+ var byCost = (a, b) => b.ms - a.ms;
106
+ function summarise(mine) {
107
+ return {
108
+ wallMs: performance.now() - mine.startedAt,
109
+ datalogMs: totalMs(mine.rules.values()),
110
+ rounds: mine.rounds,
111
+ evaluations: mine.evaluations,
112
+ relations: [...mine.relations].map(([relation, tuples]) => ({
113
+ relation,
114
+ tuples,
115
+ derived: mine.derivedRelations.has(relation)
116
+ })).sort((a, b) => b.tuples - a.tuples),
117
+ rules: [...mine.rules.values()].sort(byCost),
118
+ ruleSets: [...mine.ruleSets.values()].map((set) => ({
119
+ derives: [...set.derives],
120
+ ms: totalMs(set.rules.values()),
121
+ evaluations: set.evaluations,
122
+ rounds: set.rounds,
123
+ rules: [...set.rules.values()].sort(byCost)
124
+ })).sort((a, b) => b.ms - a.ms)
125
+ };
126
+ }
127
+ var share = (part, whole) => `${whole === 0 ? "0.0" : (part / whole * 100).toFixed(1)}%`.padStart(6);
128
+ function ruleLines(rules, datalogMs) {
129
+ return rules.slice(0, 20).map((r) => {
130
+ const ms = `${r.ms.toFixed(0)}ms`.padStart(8);
131
+ const derived = String(r.derived).padStart(7);
132
+ return ` ${ms} ${share(r.ms, datalogMs)} ${derived} tuples ${r.attempts} attempts ${ruleKey(r.head, r.body)}`;
133
+ });
134
+ }
135
+ function formatProfile(profile) {
136
+ const lines = [];
137
+ lines.push(
138
+ `datalog: ${profile.datalogMs.toFixed(0)}ms (${share(profile.datalogMs, profile.wallMs).trim()} of ${profile.wallMs.toFixed(0)}ms wall), ${profile.evaluations} evaluations, ${profile.rounds} rounds`
139
+ );
140
+ const derivedTuples = profile.relations.filter((r) => r.derived).reduce((sum, r) => sum + r.tuples, 0);
141
+ lines.push(" tuples by relation (d = derived by a rule):");
142
+ for (const { relation, tuples, derived } of profile.relations.slice(0, 20)) {
143
+ const mark = derived ? "d" : " ";
144
+ const of = derived ? share(tuples, derivedTuples) : " -";
145
+ lines.push(` ${mark} ${String(tuples).padStart(8)} ${of} ${relation}`);
146
+ }
147
+ lines.push(" time by rule:");
148
+ lines.push(...ruleLines(profile.rules, profile.datalogMs));
149
+ if (profile.ruleSets.length > 1) {
150
+ lines.push(" by rule set:");
151
+ for (const set of profile.ruleSets) {
152
+ lines.push(
153
+ ` ${`${set.ms.toFixed(0)}ms`.padStart(8)} ${share(set.ms, profile.datalogMs)} ${set.evaluations} evaluations, ${set.rounds} rounds ${set.derives.join(", ")}`
154
+ );
155
+ }
156
+ }
157
+ return lines.join("\n");
158
+ }
159
+
160
+ // src/onDemand.ts
161
+ var adornmentKey = (adornment) => adornment.map((bound) => bound ? "b" : "f").join("");
162
+ var anyBound = (adornment) => adornment.some(Boolean);
163
+ var keyOf = ({ relation, adornment }) => `${relation}/${adornmentKey(adornment)}`;
164
+ var boundTerms = (terms, adornment) => terms.filter((_, column) => adornment[column]);
165
+ var positiveLiteral = (relation, terms) => ({
166
+ relation,
167
+ terms,
168
+ negated: false
169
+ });
170
+ function deriveOnDemand(rules, complete) {
171
+ const negated = rules.find((r) => r.body.some((l) => l.negated));
172
+ if (negated !== void 0) {
173
+ throw new Error(
174
+ `cannot derive "${negated.head.relation}" on demand: its rule uses negation, and a relation derived only where somebody asked can make a negated literal match where it did not`
175
+ );
176
+ }
177
+ const rulesByHead = /* @__PURE__ */ new Map();
178
+ for (const r of rules) {
179
+ const existing = rulesByHead.get(r.head.relation);
180
+ if (existing === void 0) {
181
+ rulesByHead.set(r.head.relation, [r]);
182
+ } else {
183
+ existing.push(r);
184
+ }
185
+ }
186
+ const derives = (relation) => rulesByHead.has(relation);
187
+ const asked = /* @__PURE__ */ new Map();
188
+ const pending = [];
189
+ const want = (adorned) => {
190
+ const key = keyOf(adorned);
191
+ if (!asked.has(key)) {
192
+ asked.set(key, adorned);
193
+ pending.push(adorned);
194
+ }
195
+ };
196
+ for (const relation of complete) {
197
+ const derivedBy = rulesByHead.get(relation);
198
+ if (derivedBy === void 0) {
199
+ throw new Error(
200
+ `no rule derives "${relation}", so it cannot be asked for in full`
201
+ );
202
+ }
203
+ want({ relation, adornment: derivedBy[0].head.terms.map(() => false) });
204
+ }
205
+ while (pending.length > 0) {
206
+ const adorned = pending.pop();
207
+ for (const r of rulesByHead.get(adorned.relation) ?? []) {
208
+ const adornments = bodyAdornments(r, adorned.adornment, derives);
209
+ r.body.forEach((literal, at) => {
210
+ const adornment = adornments[at];
211
+ if (adornment !== null) {
212
+ want({ relation: literal.relation, adornment });
213
+ }
214
+ });
215
+ }
216
+ }
217
+ const variantsOf = /* @__PURE__ */ new Map();
218
+ for (const { relation } of asked.values()) {
219
+ variantsOf.set(relation, (variantsOf.get(relation) ?? 0) + 1);
220
+ }
221
+ const nameOf = (relation, adornment) => {
222
+ if (!anyBound(adornment) || variantsOf.get(relation) === 1) {
223
+ return relation;
224
+ }
225
+ return `${relation}:${adornmentKey(adornment)}`;
226
+ };
227
+ const demandOn = (relation, adornment) => `wanted:${nameOf(relation, adornment)}`;
228
+ const rewritten = [];
229
+ const demandDriven = /* @__PURE__ */ new Set();
230
+ const already = /* @__PURE__ */ new Set();
231
+ const emit = (r) => {
232
+ const key = JSON.stringify(r);
233
+ if (!already.has(key)) {
234
+ already.add(key);
235
+ rewritten.push(r);
236
+ }
237
+ };
238
+ for (const adorned of asked.values()) {
239
+ if (anyBound(adorned.adornment)) {
240
+ demandDriven.add(nameOf(adorned.relation, adorned.adornment));
241
+ demandDriven.add(demandOn(adorned.relation, adorned.adornment));
242
+ }
243
+ for (const r of rulesByHead.get(adorned.relation) ?? []) {
244
+ const guard = anyBound(adorned.adornment) ? [
245
+ positiveLiteral(
246
+ demandOn(adorned.relation, adorned.adornment),
247
+ boundTerms(r.head.terms, adorned.adornment)
248
+ )
249
+ ] : [];
250
+ const adornments = bodyAdornments(r, adorned.adornment, derives);
251
+ const body = [];
252
+ r.body.forEach((literal, at) => {
253
+ const adornment = adornments[at];
254
+ if (adornment !== null && anyBound(adornment)) {
255
+ emit({
256
+ head: {
257
+ relation: demandOn(literal.relation, adornment),
258
+ terms: boundTerms(literal.terms, adornment)
259
+ },
260
+ body: [...guard, ...body]
261
+ });
262
+ }
263
+ body.push(
264
+ adornment === null ? literal : positiveLiteral(
265
+ nameOf(literal.relation, adornment),
266
+ literal.terms
267
+ )
268
+ );
269
+ });
270
+ emit({
271
+ head: {
272
+ relation: nameOf(adorned.relation, adorned.adornment),
273
+ terms: r.head.terms
274
+ },
275
+ body: [...guard, ...body]
276
+ });
277
+ }
278
+ }
279
+ return { rules: rewritten, demandDriven: [...demandDriven] };
280
+ }
281
+ function bodyAdornments(r, headAdornment, derives) {
282
+ const bound = /* @__PURE__ */ new Set();
283
+ r.head.terms.forEach((term, column) => {
284
+ if (headAdornment[column] && term.type === "variable") {
285
+ bound.add(term.name);
286
+ }
287
+ });
288
+ return r.body.map((literal) => {
289
+ const adornment = derives(literal.relation) ? literal.terms.map(
290
+ (term) => term.type === "constant" || bound.has(term.name)
291
+ ) : null;
292
+ for (const term of literal.terms) {
293
+ if (term.type === "variable") {
294
+ bound.add(term.name);
295
+ }
296
+ }
297
+ return adornment;
298
+ });
299
+ }
300
+
1
301
  // src/index.ts
2
302
  var variable = (name) => ({ type: "variable", name });
3
303
  var constant = (value) => ({ type: "constant", value });
@@ -12,9 +312,8 @@ var notLit = (relation, ...terms) => ({
12
312
  negated: true
13
313
  });
14
314
  var rule = (relation, terms, body) => ({ head: { relation, terms }, body });
15
- var SEP = "\0";
16
- var atomKey = (a) => `${typeof a === "number" ? "n" : "s"}${a}`;
17
- var keyOf = (tuple) => tuple.map(atomKey).join(SEP);
315
+ var atomKey = (a) => typeof a === "number" ? `n${a}:` : `s${a.length}:${a}`;
316
+ var keyOf2 = (tuple) => tuple.map(atomKey).join("");
18
317
  function addToBucket(index, key, tuple) {
19
318
  const bucket = index.get(key);
20
319
  if (bucket === void 0) {
@@ -41,7 +340,7 @@ var Database = class {
41
340
  /** Add a fact; returns true when the tuple is new. */
42
341
  add(relationName, tuple) {
43
342
  const relation = this.relation(relationName);
44
- const key = keyOf(tuple);
343
+ const key = keyOf2(tuple);
45
344
  if (relation.keys.has(key)) {
46
345
  if (!isDeriving(this)) {
47
346
  claimFact(this, relationName, key);
@@ -59,7 +358,7 @@ var Database = class {
59
358
  return true;
60
359
  }
61
360
  has(relationName, tuple) {
62
- return this.store.get(relationName)?.keys.has(keyOf(tuple)) ?? false;
361
+ return this.store.get(relationName)?.keys.has(keyOf2(tuple)) ?? false;
63
362
  }
64
363
  facts(relationName) {
65
364
  return this.store.get(relationName)?.tuples ?? [];
@@ -105,7 +404,7 @@ var Database = class {
105
404
  }
106
405
  const going = /* @__PURE__ */ new Set();
107
406
  for (const tuple of tuples) {
108
- const key = keyOf(tuple);
407
+ const key = keyOf2(tuple);
109
408
  if (relation.keys.has(key)) {
110
409
  going.add(key);
111
410
  }
@@ -117,7 +416,7 @@ var Database = class {
117
416
  relation.keys.delete(key);
118
417
  }
119
418
  relation.tuples = relation.tuples.filter(
120
- (tuple) => !going.has(keyOf(tuple))
419
+ (tuple) => !going.has(keyOf2(tuple))
121
420
  );
122
421
  relation.indexes.clear();
123
422
  forgetFacts(this, relationName, going);
@@ -327,8 +626,40 @@ function evaluate(db, rules) {
327
626
  }
328
627
  }
329
628
  }
629
+ function clearRelations(db, rules, relations) {
630
+ for (const relation of relations) {
631
+ db.retract(relation, [...db.facts(relation)]);
632
+ }
633
+ const state = statesFor(db).get(signatureOf(rules));
634
+ if (state !== void 0) {
635
+ state.marks = currentMarks(db);
636
+ }
637
+ }
638
+ var signatureOf = (rules) => shapeOf(rules).signature;
639
+ var shapes = /* @__PURE__ */ new WeakMap();
640
+ function shapeOf(rules) {
641
+ const known = shapes.get(rules);
642
+ if (known !== void 0) {
643
+ return known;
644
+ }
645
+ const derivedRelations = [...new Set(rules.map((r) => r.head.relation))];
646
+ const shape = {
647
+ signature: JSON.stringify(rules),
648
+ name: [...derivedRelations].sort().join(", "),
649
+ derivedRelations,
650
+ strata: stratify(rules)
651
+ };
652
+ shapes.set(rules, shape);
653
+ return shape;
654
+ }
330
655
  function runRules(db, rules) {
331
- const signature = JSON.stringify(rules);
656
+ const {
657
+ signature,
658
+ name: ruleSetName,
659
+ derivedRelations,
660
+ strata
661
+ } = shapeOf(rules);
662
+ chargeEvaluation(ruleSetName);
332
663
  const states = statesFor(db);
333
664
  const state = states.get(signature) ?? {
334
665
  marks: null,
@@ -343,7 +674,7 @@ function runRules(db, rules) {
343
674
  }
344
675
  const derived = state.derived;
345
676
  const marks = canResume(rules, state) ? state.marks : void 0;
346
- for (const stratum of stratify(rules)) {
677
+ for (const stratum of strata) {
347
678
  let delta = /* @__PURE__ */ new Map();
348
679
  const derivedHere = new Set(stratum.map((r) => r.head.relation));
349
680
  const record = (relation, tuple) => {
@@ -354,9 +685,29 @@ function runRules(db, rules) {
354
685
  ledger = /* @__PURE__ */ new Map();
355
686
  derived.set(relation, ledger);
356
687
  }
357
- ledger.set(keyOf(tuple), tuple);
688
+ ledger.set(keyOf2(tuple), tuple);
358
689
  }
359
690
  };
691
+ const runOneRule = (r, seed, deltaIndex) => {
692
+ if (!isProfiling()) {
693
+ for (const tuple of evaluateRule(db, seed, r, deltaIndex)) {
694
+ record(r.head.relation, tuple);
695
+ }
696
+ return;
697
+ }
698
+ const startedAt = performance.now();
699
+ const before = db.size(r.head.relation);
700
+ for (const tuple of evaluateRule(db, seed, r, deltaIndex)) {
701
+ record(r.head.relation, tuple);
702
+ }
703
+ chargeRule(
704
+ ruleSetName,
705
+ r.head.relation,
706
+ r.body.map((l) => l.negated ? `!${l.relation}` : l.relation),
707
+ performance.now() - startedAt,
708
+ db.size(r.head.relation) - before
709
+ );
710
+ };
360
711
  const applyDelta = (seed, derivedOnly) => {
361
712
  for (const r of stratum) {
362
713
  const positives = r.body.filter((l) => !l.negated);
@@ -368,9 +719,7 @@ function runRules(db, rules) {
368
719
  if ((seed.get(literal.relation) ?? []).length === 0) {
369
720
  continue;
370
721
  }
371
- for (const tuple of evaluateRule(db, seed, r, i)) {
372
- record(r.head.relation, tuple);
373
- }
722
+ runOneRule(r, seed, i);
374
723
  }
375
724
  }
376
725
  };
@@ -382,20 +731,27 @@ function runRules(db, rules) {
382
731
  all.set(l.relation, db.facts(l.relation));
383
732
  }
384
733
  }
385
- for (const tuple of evaluateRule(db, all, r, -1)) {
386
- record(r.head.relation, tuple);
387
- }
734
+ runOneRule(r, all, -1);
388
735
  }
736
+ chargeRound(ruleSetName);
389
737
  } else {
390
738
  applyDelta(factsSince(db, marks), false);
739
+ chargeRound(ruleSetName);
391
740
  }
392
741
  while (delta.size > 0) {
393
742
  const lastDelta = delta;
394
743
  delta = /* @__PURE__ */ new Map();
395
744
  applyDelta(lastDelta, true);
745
+ chargeRound(ruleSetName);
396
746
  }
397
747
  }
398
748
  state.marks = currentMarks(db);
749
+ if (isProfiling()) {
750
+ chargeRelationSizes(
751
+ db.relationNames().map((name) => [name, db.size(name)]),
752
+ derivedRelations
753
+ );
754
+ }
399
755
  return db;
400
756
  }
401
757
  function addTo(buckets, relation, tuple) {
@@ -408,10 +764,15 @@ function addTo(buckets, relation, tuple) {
408
764
  }
409
765
  export {
410
766
  Database,
767
+ clearRelations,
411
768
  constant,
769
+ deriveOnDemand,
412
770
  evaluate,
771
+ formatProfile,
413
772
  lit,
414
773
  notLit,
774
+ profileEvaluation,
775
+ profileEvaluationAsync,
415
776
  rule,
416
777
  stratify,
417
778
  variable
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suss/datalog",
3
- "version": "0.1.0",
3
+ "version": "0.2.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": [