@suss/datalog 0.33.1 → 0.34.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/dist/index.cjs CHANGED
@@ -40,6 +40,7 @@ __export(index_exports, {
40
40
  rowBudget: () => rowBudget,
41
41
  rule: () => rule,
42
42
  ruleLabel: () => ruleLabel,
43
+ rulesDeriving: () => rulesDeriving,
43
44
  stratify: () => stratify,
44
45
  tupleKey: () => tupleKey,
45
46
  tupleKeyParts: () => tupleKeyParts,
@@ -307,7 +308,7 @@ function boundFirst(r, headAdornment, labels) {
307
308
  }
308
309
  });
309
310
  const boundColumns = (literal) => literal.terms.filter(
310
- (term, column) => !labels.get(literal.relation)?.has(column) && (term.type === "constant" || bound.has(term.name))
311
+ (term, column) => !labels.get(literal.relation)?.has(column) && term.type === "variable" && bound.has(term.name)
311
312
  ).length;
312
313
  const remaining = [...r.body];
313
314
  const body = [];
@@ -404,6 +405,11 @@ function chargeEvaluationRows(examined) {
404
405
  }
405
406
  }
406
407
  }
408
+ function chargeEngine(part, ms) {
409
+ for (const collector of open) {
410
+ collector.engineMs[part] += ms;
411
+ }
412
+ }
407
413
  function chargeRound(ruleSet) {
408
414
  for (const collector of open) {
409
415
  collector.rounds += 1;
@@ -460,6 +466,7 @@ function openScope() {
460
466
  startedAt: performance.now(),
461
467
  rounds: 0,
462
468
  evaluations: 0,
469
+ engineMs: { evaluate: 0, clear: 0 },
463
470
  rules: /* @__PURE__ */ new Map(),
464
471
  relations: /* @__PURE__ */ new Map(),
465
472
  derivedRelations: /* @__PURE__ */ new Set(),
@@ -496,6 +503,7 @@ function summarise(mine) {
496
503
  return {
497
504
  wallMs: performance.now() - mine.startedAt,
498
505
  datalogMs: totalMs(mine.rules.values()),
506
+ engineMs: { ...mine.engineMs },
499
507
  rounds: mine.rounds,
500
508
  evaluations: mine.evaluations,
501
509
  examined: totalExamined(mine.rules.values()),
@@ -531,6 +539,9 @@ function formatProfile(profile) {
531
539
  lines.push(
532
540
  `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, ${profile.examined} rows read (${profile.largestEvaluation} in the biggest evaluation)`
533
541
  );
542
+ lines.push(
543
+ ` engine: ${profile.engineMs.evaluate.toFixed(0)}ms inside evaluate (${profile.datalogMs.toFixed(0)}ms of it in rules), ${profile.engineMs.clear.toFixed(0)}ms clearing relations between questions`
544
+ );
534
545
  const { asked, abandoned, skipped } = profile.questions;
535
546
  if (asked > 0) {
536
547
  lines.push(
@@ -563,6 +574,102 @@ function formatProfile(profile) {
563
574
  }
564
575
 
565
576
  // src/relation.ts
577
+ var sameTuple = (a, b) => a.length === b.length && a.every((atom, at) => atom === b[at]);
578
+ function storedTuplesOf(relation, facts) {
579
+ const column = relation.columns.findIndex((buckets2) => buckets2 !== void 0);
580
+ const buckets = relation.columns[column];
581
+ if (buckets === void 0) {
582
+ return null;
583
+ }
584
+ const stored = facts.map(
585
+ (fact) => buckets.get(fact[column])?.find((tuple) => sameTuple(tuple, fact))
586
+ );
587
+ return stored.every((tuple) => tuple !== void 0) ? stored : null;
588
+ }
589
+ function removeFrom(buckets, key, tuple) {
590
+ const bucket = buckets.get(key);
591
+ const at = bucket?.indexOf(tuple) ?? -1;
592
+ if (bucket === void 0 || at === -1) {
593
+ return;
594
+ }
595
+ bucket.splice(at, 1);
596
+ if (bucket.length === 0) {
597
+ buckets.delete(key);
598
+ }
599
+ }
600
+ function removeTuples(relation, stored) {
601
+ const leaving = new Set(stored);
602
+ relation.tuples = relation.tuples.filter((tuple) => !leaving.has(tuple));
603
+ for (const tuple of stored) {
604
+ relation.columns.forEach((buckets, column) => {
605
+ if (buckets !== void 0) {
606
+ removeFrom(buckets, tuple[column], tuple);
607
+ }
608
+ });
609
+ for (const index of relation.combined.values()) {
610
+ let level = index.root;
611
+ const last = index.columns.length - 1;
612
+ for (let at = 0; at < last && level !== void 0; at++) {
613
+ level = level.get(tuple[index.columns[at]]);
614
+ }
615
+ if (level !== void 0) {
616
+ removeFrom(
617
+ level,
618
+ tuple[index.columns[last]],
619
+ tuple
620
+ );
621
+ }
622
+ }
623
+ }
624
+ }
625
+ function columnsIn(mask) {
626
+ const columns = [];
627
+ for (let column = 0; mask >> column !== 0; column++) {
628
+ if (mask & 1 << column) {
629
+ columns.push(column);
630
+ }
631
+ }
632
+ return columns;
633
+ }
634
+ function addToCombined(index, tuple) {
635
+ const last = index.columns.length - 1;
636
+ if (index.columns[last] >= tuple.length) {
637
+ return;
638
+ }
639
+ let level = index.root;
640
+ for (let at = 0; at < last; at++) {
641
+ const value = tuple[index.columns[at]];
642
+ let next = level.get(value);
643
+ if (next === void 0) {
644
+ next = /* @__PURE__ */ new Map();
645
+ level.set(value, next);
646
+ }
647
+ level = next;
648
+ }
649
+ addToBucket(
650
+ level,
651
+ tuple[index.columns[last]],
652
+ tuple
653
+ );
654
+ }
655
+ function combinedBucketIn(relation, mask, values) {
656
+ let index = relation.combined.get(mask);
657
+ if (index === void 0) {
658
+ index = { columns: columnsIn(mask), root: /* @__PURE__ */ new Map() };
659
+ for (const tuple of relation.tuples) {
660
+ addToCombined(index, tuple);
661
+ }
662
+ relation.combined.set(mask, index);
663
+ }
664
+ let level = index.root;
665
+ for (const value of values) {
666
+ level = level.get(value);
667
+ if (level === void 0) {
668
+ return [];
669
+ }
670
+ }
671
+ return level;
672
+ }
566
673
  function addToBucket(buckets, key, tuple) {
567
674
  const bucket = buckets.get(key);
568
675
  if (bucket === void 0) {
@@ -586,6 +693,72 @@ function bucketIn(relation, column, value) {
586
693
  return buckets.get(value) ?? [];
587
694
  }
588
695
 
696
+ // src/stratum.ts
697
+ function planStratum(rules) {
698
+ const derived = /* @__PURE__ */ new Set();
699
+ const readers = /* @__PURE__ */ new Map();
700
+ for (let at = 0; at < rules.length; at++) {
701
+ const r = rules[at];
702
+ derived.add(r.head.relation);
703
+ for (const literal of r.body) {
704
+ if (literal.negated) {
705
+ continue;
706
+ }
707
+ const positions = readers.get(literal.relation);
708
+ if (positions === void 0) {
709
+ readers.set(literal.relation, [at]);
710
+ continue;
711
+ }
712
+ if (positions[positions.length - 1] !== at) {
713
+ positions.push(at);
714
+ }
715
+ }
716
+ }
717
+ return { rules, derived, readers, marked: new Uint8Array(rules.length) };
718
+ }
719
+ var NONE = [];
720
+ function rulesReading(stratum, seed, derivedOnly) {
721
+ const lists = [];
722
+ for (const [relation, tuples] of seed) {
723
+ if (tuples.length === 0) {
724
+ continue;
725
+ }
726
+ if (derivedOnly && !stratum.derived.has(relation)) {
727
+ continue;
728
+ }
729
+ const positions = stratum.readers.get(relation);
730
+ if (positions !== void 0) {
731
+ lists.push(positions);
732
+ }
733
+ }
734
+ if (lists.length === 0) {
735
+ return NONE;
736
+ }
737
+ if (lists.length === 1) {
738
+ return lists[0];
739
+ }
740
+ return unionInOrder(lists, stratum.marked);
741
+ }
742
+ function unionInOrder(lists, marked) {
743
+ let first = marked.length;
744
+ let last = -1;
745
+ for (const positions of lists) {
746
+ for (const at of positions) {
747
+ marked[at] = 1;
748
+ }
749
+ first = Math.min(first, positions[0]);
750
+ last = Math.max(last, positions[positions.length - 1]);
751
+ }
752
+ const union = [];
753
+ for (let at = first; at <= last; at++) {
754
+ if (marked[at] === 1) {
755
+ marked[at] = 0;
756
+ union.push(at);
757
+ }
758
+ }
759
+ return union;
760
+ }
761
+
589
762
  // src/confidence.ts
590
763
  var RANK = {
591
764
  low: 0,
@@ -664,9 +837,7 @@ var Witness = class {
664
837
  var witnesses = {
665
838
  asserted: "asserted",
666
839
  absent: "absent",
667
- // A witness is built from the derivation alone, and saying so lets
668
- // the evaluator skip the body-tag lookups, which were most of the
669
- // tagged path's cost on a large sweep.
840
+ // A witness is built from the derivation alone.
670
841
  ignoresBodyTags: true,
671
842
  combine: (_bodyTags, derivation) => new Witness(derivation.rule, derivation.body),
672
843
  merge: (stored) => stored
@@ -728,6 +899,7 @@ var notLit = (relation, ...terms) => ({
728
899
  var rule = (relation, terms, body, name) => name === void 0 ? { head: { relation, terms }, body } : { head: { relation, terms }, body, name };
729
900
  var literalName = (literal) => literal.negated ? `!${literal.relation}` : literal.relation;
730
901
  var ruleLabel = (r) => r.name ?? `${r.head.relation} :- ${r.body.map(literalName).join(", ")}`;
902
+ var FEW_REMOVED = 16;
731
903
  var Database = class {
732
904
  store = /* @__PURE__ */ new Map();
733
905
  relation(name) {
@@ -738,15 +910,16 @@ var Database = class {
738
910
  const created = {
739
911
  index: new FactIndex(),
740
912
  tuples: [],
741
- columns: []
913
+ columns: [],
914
+ combined: /* @__PURE__ */ new Map()
742
915
  };
743
916
  this.store.set(name, created);
744
917
  return created;
745
918
  }
746
919
  /**
747
920
  * The key this relation gives a tuple, whether or not the fact is
748
- * present. Evaluation keys its ledger of derived facts by it; a
749
- * caller outside this module has nowhere to spend one.
921
+ * present. Evaluation keys its ledger of derived facts by it. Nothing
922
+ * outside this module accepts a key.
750
923
  */
751
924
  keyFor(relationName, tuple) {
752
925
  return this.relation(relationName).index.key(tuple);
@@ -786,6 +959,9 @@ var Database = class {
786
959
  addToBucket(buckets, value, tuple);
787
960
  }
788
961
  }
962
+ for (const combined of relation.combined.values()) {
963
+ addToCombined(combined, tuple);
964
+ }
789
965
  return "added";
790
966
  }
791
967
  /** The tag stored for this fact, or undefined when there is none. */
@@ -851,18 +1027,45 @@ var Database = class {
851
1027
  if (index.size === 0) {
852
1028
  relation.tuples = [];
853
1029
  index.clear();
1030
+ relation.columns.length = 0;
1031
+ relation.combined.clear();
1032
+ forgetFacts(this, relationName, going);
1033
+ return going.size;
1034
+ }
1035
+ for (const tuple of gone) {
1036
+ index.prune(tuple);
1037
+ }
1038
+ const stored = gone.length * FEW_REMOVED < relation.tuples.length ? storedTuplesOf(relation, gone) : null;
1039
+ if (stored !== null) {
1040
+ removeTuples(relation, stored);
854
1041
  } else {
855
1042
  relation.tuples = relation.tuples.filter(
856
1043
  (tuple) => index.find(tuple) !== void 0
857
1044
  );
858
- for (const tuple of gone) {
859
- index.prune(tuple);
860
- }
1045
+ relation.columns.length = 0;
1046
+ relation.combined.clear();
861
1047
  }
862
- relation.columns.length = 0;
863
1048
  forgetFacts(this, relationName, going);
864
1049
  return going.size;
865
1050
  }
1051
+ /**
1052
+ * Remove every fact in a relation, and return how many there were. The
1053
+ * database ends up as `retract` leaves it when handed every tuple, but
1054
+ * nothing looks a tuple up to remove it.
1055
+ */
1056
+ retractAll(relationName) {
1057
+ const relation = this.store.get(relationName);
1058
+ if (relation === void 0 || relation.tuples.length === 0) {
1059
+ return 0;
1060
+ }
1061
+ const removed = relation.tuples.length;
1062
+ relation.tuples = [];
1063
+ relation.index.clear();
1064
+ relation.columns.length = 0;
1065
+ relation.combined.clear();
1066
+ forgetRelation(this, relationName);
1067
+ return removed;
1068
+ }
866
1069
  size(relationName) {
867
1070
  return this.store.get(relationName)?.tuples.length ?? 0;
868
1071
  }
@@ -870,6 +1073,25 @@ var Database = class {
870
1073
  return [...this.store.keys()];
871
1074
  }
872
1075
  };
1076
+ function rulesDeriving(rules, relations) {
1077
+ const wanted = new Set(relations);
1078
+ const pending = [...relations];
1079
+ while (pending.length > 0) {
1080
+ const relation = pending.pop();
1081
+ for (const r of rules) {
1082
+ if (r.head.relation !== relation) {
1083
+ continue;
1084
+ }
1085
+ for (const literal of r.body) {
1086
+ if (!wanted.has(literal.relation)) {
1087
+ wanted.add(literal.relation);
1088
+ pending.push(literal.relation);
1089
+ }
1090
+ }
1091
+ }
1092
+ }
1093
+ return rules.filter((r) => wanted.has(r.head.relation));
1094
+ }
873
1095
  function stratify(rules) {
874
1096
  const derived = new Set(rules.map((r) => r.head.relation));
875
1097
  const stratum = /* @__PURE__ */ new Map();
@@ -969,13 +1191,16 @@ function headTuple(head, bindings) {
969
1191
  });
970
1192
  }
971
1193
  var positiveLiterals = /* @__PURE__ */ new WeakMap();
972
- function deltaLiteral(r, deltaIndex) {
1194
+ function positivesOf(r) {
973
1195
  let positives = positiveLiterals.get(r);
974
1196
  if (positives === void 0) {
975
1197
  positives = r.body.map((literal, index) => literal.negated ? -1 : index).filter((index) => index !== -1);
976
1198
  positiveLiterals.set(r, positives);
977
1199
  }
978
- return positives[deltaIndex] ?? -1;
1200
+ return positives;
1201
+ }
1202
+ function deltaLiteral(r, deltaIndex) {
1203
+ return positivesOf(r)[deltaIndex] ?? -1;
979
1204
  }
980
1205
  var allBound = (literal, bindings) => literal.terms.every(
981
1206
  (term) => term.type === "constant" || boundValue(bindings, term.name) !== void 0
@@ -1018,58 +1243,139 @@ function evaluateRule(db, deltas, r, deltaIndex, budget) {
1018
1243
  results.push(headTuple(r.head, bindings));
1019
1244
  return;
1020
1245
  }
1021
- let pick = -1;
1022
- let narrowest = null;
1023
- for (let index = 0; index < body.length; index++) {
1024
- if (taken & 1 << index) {
1025
- continue;
1026
- }
1027
- const literal = body[index];
1028
- if (literal.negated) {
1029
- if (allBound(literal, bindings)) {
1030
- if (!db.has(literal.relation, groundNegated(literal, bindings))) {
1031
- step(taken | 1 << index, bindings);
1032
- }
1033
- return;
1034
- }
1035
- continue;
1036
- }
1037
- const source = narrowedSource(db, literal, bindings);
1038
- if (source === null) {
1039
- continue;
1040
- }
1041
- if (source.length === 0) {
1042
- return;
1043
- }
1044
- if (narrowest === null || source.length < narrowest.length) {
1045
- pick = index;
1046
- narrowest = source;
1047
- }
1048
- }
1049
- if (narrowest !== null) {
1050
- walk2(pick, narrowest, taken, bindings);
1246
+ const index = nextLiteral(db, body, taken, bindings);
1247
+ if (index === -1) {
1051
1248
  return;
1052
1249
  }
1053
- for (let index = 0; index < body.length; index++) {
1054
- if (!(taken & 1 << index) && !body[index].negated) {
1055
- walk2(index, db.facts(body[index].relation), taken, bindings);
1056
- return;
1057
- }
1058
- }
1059
- for (let index = 0; index < body.length; index++) {
1060
- if (!(taken & 1 << index)) {
1061
- groundNegated(body[index], bindings);
1250
+ const literal = body[index];
1251
+ if (literal.negated) {
1252
+ if (!db.has(literal.relation, groundNegated(literal, bindings))) {
1253
+ step(taken | 1 << index, bindings);
1062
1254
  }
1255
+ return;
1063
1256
  }
1257
+ walk2(index, nextSource, taken, bindings);
1064
1258
  };
1065
1259
  if (deltaAt === -1) {
1066
1260
  step(0, null);
1067
- } else {
1068
- walk2(deltaAt, deltas.get(body[deltaAt].relation) ?? [], 0, null);
1261
+ return results;
1262
+ }
1263
+ const rows = deltaRows(deltas, body[deltaAt], budget);
1264
+ const start = smallerStart(db, body, deltaAt, rows);
1265
+ if (start === null) {
1266
+ walk2(deltaAt, rows, 0, null);
1267
+ return results;
1268
+ }
1269
+ const byValue = newFactsByColumn(rows, start.column, budget);
1270
+ const first = body[start.index];
1271
+ for (const tuple of start.source) {
1272
+ budget.examined++;
1273
+ if (budget.examined > budget.limit) {
1274
+ throw OUT_OF_BUDGET;
1275
+ }
1276
+ const bindings = unify(first, tuple, null);
1277
+ if (bindings === NO_MATCH) {
1278
+ continue;
1279
+ }
1280
+ const value = boundValue(bindings, start.variable);
1281
+ const matching = value === void 0 ? void 0 : byValue.get(value);
1282
+ if (matching !== void 0) {
1283
+ walk2(deltaAt, matching, 1 << start.index, bindings);
1284
+ }
1069
1285
  }
1070
1286
  return results;
1071
1287
  }
1072
- var boundSource = (db, literal, bindings) => narrowedSource(db, literal, bindings) ?? db.facts(literal.relation);
1288
+ var newFactIndexes = /* @__PURE__ */ new WeakMap();
1289
+ function smallerStart(db, body, deltaAt, rows) {
1290
+ const newFacts = body[deltaAt];
1291
+ const indexed = newFactIndexes.get(rows);
1292
+ let best = null;
1293
+ for (let index = 0; index < body.length; index++) {
1294
+ const literal = body[index];
1295
+ if (index === deltaAt || literal.negated) {
1296
+ continue;
1297
+ }
1298
+ const column = sharedColumn(newFacts, literal);
1299
+ if (column === -1) {
1300
+ continue;
1301
+ }
1302
+ const source = narrowedSource(db, literal, null) ?? db.facts(literal.relation);
1303
+ const worthIt = indexed?.has(column) ? source.length < rows.length : source.length * 2 < rows.length;
1304
+ if (worthIt && (best === null || source.length < best.source.length)) {
1305
+ best = {
1306
+ index,
1307
+ source,
1308
+ variable: newFacts.terms[column].name,
1309
+ column
1310
+ };
1311
+ }
1312
+ }
1313
+ return best;
1314
+ }
1315
+ function sharedColumn(from, other) {
1316
+ return from.terms.findIndex(
1317
+ (term) => term.type === "variable" && other.terms.some((t) => t.type === "variable" && t.name === term.name)
1318
+ );
1319
+ }
1320
+ function newFactsByColumn(rows, column, budget) {
1321
+ let byColumn = newFactIndexes.get(rows);
1322
+ if (byColumn === void 0) {
1323
+ byColumn = /* @__PURE__ */ new Map();
1324
+ newFactIndexes.set(rows, byColumn);
1325
+ }
1326
+ let byValue = byColumn.get(column);
1327
+ if (byValue === void 0) {
1328
+ byValue = /* @__PURE__ */ new Map();
1329
+ for (const tuple of rows) {
1330
+ const value = tuple[column];
1331
+ if (value !== void 0) {
1332
+ addToBucket(byValue, value, tuple);
1333
+ }
1334
+ }
1335
+ budget.examined += rows.length;
1336
+ byColumn.set(column, byValue);
1337
+ }
1338
+ return byValue;
1339
+ }
1340
+ var nextSource = [];
1341
+ function nextLiteral(db, body, taken, bindings) {
1342
+ let pick = -1;
1343
+ let narrowest = null;
1344
+ for (let index = 0; index < body.length; index++) {
1345
+ if (taken & 1 << index) {
1346
+ continue;
1347
+ }
1348
+ const literal = body[index];
1349
+ if (literal.negated) {
1350
+ if (allBound(literal, bindings)) {
1351
+ return index;
1352
+ }
1353
+ continue;
1354
+ }
1355
+ const source = narrowedSource(db, literal, bindings);
1356
+ if (source === null) {
1357
+ continue;
1358
+ }
1359
+ if (source.length === 0) {
1360
+ return -1;
1361
+ }
1362
+ if (narrowest === null || source.length < narrowest.length) {
1363
+ pick = index;
1364
+ narrowest = source;
1365
+ }
1366
+ }
1367
+ if (narrowest !== null) {
1368
+ nextSource = narrowedFurther(db, body[pick], bindings, narrowest);
1369
+ return nextSource.length === 0 ? -1 : pick;
1370
+ }
1371
+ for (let index = 0; index < body.length; index++) {
1372
+ if (!(taken & 1 << index) && !body[index].negated) {
1373
+ nextSource = db.facts(body[index].relation);
1374
+ return index;
1375
+ }
1376
+ }
1377
+ return body.findIndex((_, index) => !(taken & 1 << index));
1378
+ }
1073
1379
  function narrowedSource(db, literal, bindings) {
1074
1380
  const relation = db.relationOf(literal.relation);
1075
1381
  let narrowest = null;
@@ -1089,14 +1395,119 @@ function narrowedSource(db, literal, bindings) {
1089
1395
  }
1090
1396
  return narrowest;
1091
1397
  }
1398
+ function narrowedFurther(db, literal, bindings, source) {
1399
+ if (source.length <= 1) {
1400
+ return source;
1401
+ }
1402
+ let mask = 0;
1403
+ const values = [];
1404
+ for (let column = 0; column < literal.terms.length && column < 31; column++) {
1405
+ const term = literal.terms[column];
1406
+ const value = term.type === "constant" ? term.value : boundValue(bindings, term.name);
1407
+ if (value !== void 0) {
1408
+ mask |= 1 << column;
1409
+ values.push(value);
1410
+ }
1411
+ }
1412
+ const relation = db.relationOf(literal.relation);
1413
+ if (values.length < 2 || relation === void 0) {
1414
+ return source;
1415
+ }
1416
+ return combinedBucketIn(relation, mask, values);
1417
+ }
1418
+ var constantColumns = /* @__PURE__ */ new WeakMap();
1419
+ function constantColumnsOf(literal) {
1420
+ let known = constantColumns.get(literal);
1421
+ if (known === void 0) {
1422
+ const columns = [];
1423
+ const values = [];
1424
+ literal.terms.forEach((term, column) => {
1425
+ if (term.type === "constant") {
1426
+ columns.push(column);
1427
+ values.push(term.value);
1428
+ }
1429
+ });
1430
+ known = columns.length === 0 ? null : { columns, values };
1431
+ constantColumns.set(literal, known);
1432
+ }
1433
+ return known;
1434
+ }
1435
+ var deltaBuckets = /* @__PURE__ */ new WeakMap();
1436
+ var deltaBucketKey = (relation, column) => `${relation}\0${column}`;
1437
+ function hasNoNewRows(deltas, literal) {
1438
+ const rows = deltas.get(literal.relation) ?? [];
1439
+ const constants = constantColumnsOf(literal);
1440
+ if (rows.length === 0 || constants === null) {
1441
+ return rows.length === 0;
1442
+ }
1443
+ const buckets = deltaBuckets.get(deltas)?.get(deltaBucketKey(literal.relation, constants.columns[0]));
1444
+ return buckets !== void 0 && !buckets.has(constants.values[0]);
1445
+ }
1446
+ function deltaRows(deltas, literal, budget) {
1447
+ const rows = deltas.get(literal.relation) ?? [];
1448
+ const constants = constantColumnsOf(literal);
1449
+ if (constants === null || rows.length <= 1) {
1450
+ return rows;
1451
+ }
1452
+ let byColumn = deltaBuckets.get(deltas);
1453
+ if (byColumn === void 0) {
1454
+ byColumn = /* @__PURE__ */ new Map();
1455
+ deltaBuckets.set(deltas, byColumn);
1456
+ }
1457
+ const column = constants.columns[0];
1458
+ const key = deltaBucketKey(literal.relation, column);
1459
+ let buckets = byColumn.get(key);
1460
+ if (buckets === void 0) {
1461
+ buckets = /* @__PURE__ */ new Map();
1462
+ for (const tuple of rows) {
1463
+ const value = tuple[column];
1464
+ if (value !== void 0) {
1465
+ addToBucket(buckets, value, tuple);
1466
+ }
1467
+ }
1468
+ budget.examined += rows.length;
1469
+ byColumn.set(key, buckets);
1470
+ }
1471
+ const bucket = buckets.get(constants.values[0]) ?? [];
1472
+ if (constants.columns.length === 1) {
1473
+ return bucket;
1474
+ }
1475
+ return bucket.filter(
1476
+ (tuple) => constants.columns.every(
1477
+ (at, i) => at === column || tuple[at] === constants.values[i]
1478
+ )
1479
+ );
1480
+ }
1092
1481
  var EMPTY_TAGS = [];
1093
1482
  function evaluateRuleTagged(db, deltas, r, deltaIndex, algebra, budget) {
1094
1483
  const results = [];
1095
- const bodyTags = [];
1096
- const bodyMatches = [];
1484
+ const body = r.body;
1485
+ const deltaAt = deltaLiteral(r, deltaIndex);
1486
+ const whole = 2 ** body.length - 1;
1487
+ const bodyTags = new Array(body.length);
1488
+ const bodyMatches = new Array(body.length);
1097
1489
  const readsTags = algebra.ignoresBodyTags !== true;
1098
- const step = (literalIndex, positiveIndex, bindings) => {
1099
- if (literalIndex === r.body.length) {
1490
+ const walk2 = (index, source, taken, bindings) => {
1491
+ const literal = body[index];
1492
+ for (const tuple of source) {
1493
+ budget.examined++;
1494
+ const next = unify(literal, tuple, bindings);
1495
+ if (next !== NO_MATCH) {
1496
+ if (readsTags) {
1497
+ const stored = db.tagOf(literal.relation, tuple);
1498
+ bodyTags[index] = stored === void 0 ? algebra.asserted : stored;
1499
+ }
1500
+ bodyMatches[index] = {
1501
+ kind: "fact",
1502
+ relation: literal.relation,
1503
+ tuple
1504
+ };
1505
+ step(taken | 1 << index, next);
1506
+ }
1507
+ }
1508
+ };
1509
+ const step = (taken, bindings) => {
1510
+ if (taken === whole) {
1100
1511
  results.push({
1101
1512
  tuple: headTuple(r.head, bindings),
1102
1513
  tag: algebra.combine(readsTags ? bodyTags.slice() : EMPTY_TAGS, {
@@ -1106,51 +1517,33 @@ function evaluateRuleTagged(db, deltas, r, deltaIndex, algebra, budget) {
1106
1517
  });
1107
1518
  return;
1108
1519
  }
1109
- const literal = r.body[literalIndex];
1520
+ const index = nextLiteral(db, body, taken, bindings);
1521
+ if (index === -1) {
1522
+ return;
1523
+ }
1524
+ const literal = body[index];
1110
1525
  if (literal.negated) {
1111
1526
  const grounded = groundNegated(literal, bindings);
1112
1527
  if (!db.has(literal.relation, grounded)) {
1113
1528
  if (readsTags) {
1114
- bodyTags.push(algebra.absent);
1529
+ bodyTags[index] = algebra.absent;
1115
1530
  }
1116
- bodyMatches.push({
1531
+ bodyMatches[index] = {
1117
1532
  kind: "absence",
1118
1533
  relation: literal.relation,
1119
1534
  tuple: grounded
1120
- });
1121
- step(literalIndex + 1, positiveIndex, bindings);
1122
- if (readsTags) {
1123
- bodyTags.pop();
1124
- }
1125
- bodyMatches.pop();
1535
+ };
1536
+ step(taken | 1 << index, bindings);
1126
1537
  }
1127
1538
  return;
1128
1539
  }
1129
- const source = positiveIndex === deltaIndex ? deltas.get(literal.relation) ?? [] : boundSource(db, literal, bindings);
1130
- for (const tuple of source) {
1131
- budget.examined++;
1132
- const next = unify(literal, tuple, bindings);
1133
- if (next !== NO_MATCH) {
1134
- if (readsTags) {
1135
- const stored = db.tagOf(literal.relation, tuple);
1136
- bodyTags.push(
1137
- stored === void 0 ? algebra.asserted : stored
1138
- );
1139
- }
1140
- bodyMatches.push({
1141
- kind: "fact",
1142
- relation: literal.relation,
1143
- tuple
1144
- });
1145
- step(literalIndex + 1, positiveIndex + 1, next);
1146
- if (readsTags) {
1147
- bodyTags.pop();
1148
- }
1149
- bodyMatches.pop();
1150
- }
1151
- }
1540
+ walk2(index, nextSource, taken, bindings);
1152
1541
  };
1153
- step(0, 0, null);
1542
+ if (deltaAt === -1) {
1543
+ step(0, null);
1544
+ } else {
1545
+ walk2(deltaAt, deltaRows(deltas, body[deltaAt], budget), 0, null);
1546
+ }
1154
1547
  return results;
1155
1548
  }
1156
1549
  var evaluated = /* @__PURE__ */ new WeakMap();
@@ -1190,6 +1583,16 @@ function forgetFacts(db, relation, keys) {
1190
1583
  }
1191
1584
  }
1192
1585
  }
1586
+ function forgetRelation(db, relation) {
1587
+ const states = evaluated.get(db);
1588
+ if (states === void 0) {
1589
+ return;
1590
+ }
1591
+ for (const state of states.values()) {
1592
+ state.marks = null;
1593
+ state.derived.get(relation)?.clear();
1594
+ }
1595
+ }
1193
1596
  var usesNegation = (rules) => rules.some((r) => r.body.some((l) => l.negated));
1194
1597
  function canResume(rules, state) {
1195
1598
  return !usesNegation(rules) && state.marks !== null;
@@ -1222,6 +1625,8 @@ function evaluate(db, rules, algebra, budget) {
1222
1625
  throw new Error("cannot evaluate with both a tag algebra and a row budget");
1223
1626
  }
1224
1627
  deriving.set(db, (deriving.get(db) ?? 0) + 1);
1628
+ const profiling = isProfiling();
1629
+ const startedAt = profiling ? performance.now() : 0;
1225
1630
  try {
1226
1631
  return runRules(db, rules, algebra, budget);
1227
1632
  } finally {
@@ -1231,16 +1636,24 @@ function evaluate(db, rules, algebra, budget) {
1231
1636
  } else {
1232
1637
  deriving.set(db, depth);
1233
1638
  }
1639
+ if (profiling) {
1640
+ chargeEngine("evaluate", performance.now() - startedAt);
1641
+ }
1234
1642
  }
1235
1643
  }
1236
1644
  function clearRelations(db, rules, relations) {
1645
+ const profiling = isProfiling();
1646
+ const startedAt = profiling ? performance.now() : 0;
1237
1647
  for (const relation of relations) {
1238
- db.retract(relation, [...db.facts(relation)]);
1648
+ db.retractAll(relation);
1239
1649
  }
1240
1650
  const state = statesFor(db).get(signatureOf(rules));
1241
1651
  if (state !== void 0) {
1242
1652
  state.marks = currentMarks(db);
1243
1653
  }
1654
+ if (profiling) {
1655
+ chargeEngine("clear", performance.now() - startedAt);
1656
+ }
1244
1657
  }
1245
1658
  var signatureOf = (rules) => shapeOf(rules).signature;
1246
1659
  var shapes = /* @__PURE__ */ new WeakMap();
@@ -1254,7 +1667,7 @@ function shapeOf(rules) {
1254
1667
  signature: JSON.stringify(rules),
1255
1668
  name: [...derivedRelations].sort().join(", "),
1256
1669
  derivedRelations,
1257
- strata: stratify(rules)
1670
+ strata: stratify(rules).map(planStratum)
1258
1671
  };
1259
1672
  shapes.set(rules, shape);
1260
1673
  return shape;
@@ -1313,7 +1726,6 @@ function runRules(db, rules, algebra, given) {
1313
1726
  const marks = canResume(rules, state) ? state.marks : void 0;
1314
1727
  const runStratum = (stratum) => {
1315
1728
  let delta = /* @__PURE__ */ new Map();
1316
- const derivedHere = new Set(stratum.map((r) => r.head.relation));
1317
1729
  const record = (relation, tuple) => {
1318
1730
  if (db.add(relation, tuple) === "added") {
1319
1731
  addTo(delta, relation, tuple);
@@ -1367,17 +1779,18 @@ function runRules(db, rules, algebra, given) {
1367
1779
  );
1368
1780
  };
1369
1781
  const applyDelta = (seed, derivedOnly) => {
1370
- for (const r of stratum) {
1782
+ for (const at of rulesReading(stratum, seed, derivedOnly)) {
1783
+ const r = stratum.rules[at];
1371
1784
  if (!couldProduce(db, r)) {
1372
1785
  continue;
1373
1786
  }
1374
- const positives = r.body.filter((l) => !l.negated);
1787
+ const positives = positivesOf(r);
1375
1788
  for (let i = 0; i < positives.length; i++) {
1376
- const literal = positives[i];
1377
- if (derivedOnly && !derivedHere.has(literal.relation)) {
1789
+ const literal = r.body[positives[i]];
1790
+ if (derivedOnly && !stratum.derived.has(literal.relation)) {
1378
1791
  continue;
1379
1792
  }
1380
- if ((seed.get(literal.relation) ?? []).length === 0) {
1793
+ if (hasNoNewRows(seed, literal)) {
1381
1794
  continue;
1382
1795
  }
1383
1796
  runOneRule(r, seed, i);
@@ -1385,7 +1798,7 @@ function runRules(db, rules, algebra, given) {
1385
1798
  }
1386
1799
  };
1387
1800
  if (marks === void 0 || marks === null) {
1388
- for (const r of stratum) {
1801
+ for (const r of stratum.rules) {
1389
1802
  if (!couldProduce(db, r)) {
1390
1803
  continue;
1391
1804
  }
@@ -1472,6 +1885,7 @@ function addTo(buckets, relation, tuple) {
1472
1885
  rowBudget,
1473
1886
  rule,
1474
1887
  ruleLabel,
1888
+ rulesDeriving,
1475
1889
  stratify,
1476
1890
  tupleKey,
1477
1891
  tupleKeyParts,