@suss/datalog 0.31.0 → 0.31.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +139 -19
- package/dist/index.d.cts +89 -7
- package/dist/index.d.ts +89 -7
- package/dist/index.js +135 -19
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -20,8 +20,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
BudgetExhausted: () => BudgetExhausted,
|
|
23
24
|
Database: () => Database,
|
|
24
25
|
Witness: () => Witness,
|
|
26
|
+
chargeAbandoned: () => chargeAbandoned,
|
|
27
|
+
chargeQuestion: () => chargeQuestion,
|
|
25
28
|
clearRelations: () => clearRelations,
|
|
26
29
|
confidence: () => confidence,
|
|
27
30
|
confidenceWith: () => confidenceWith,
|
|
@@ -34,6 +37,7 @@ __export(index_exports, {
|
|
|
34
37
|
profileEvaluation: () => profileEvaluation,
|
|
35
38
|
profileEvaluationAsync: () => profileEvaluationAsync,
|
|
36
39
|
proofOf: () => proofOf,
|
|
40
|
+
rowBudget: () => rowBudget,
|
|
37
41
|
rule: () => rule,
|
|
38
42
|
ruleLabel: () => ruleLabel,
|
|
39
43
|
stratify: () => stratify,
|
|
@@ -231,21 +235,43 @@ function bodyAdornments(r, headAdornment, derives) {
|
|
|
231
235
|
var open = [];
|
|
232
236
|
var isProfiling = () => open.length > 0;
|
|
233
237
|
var ruleKey = (head, body) => `${head} <- ${body.join(", ")}`;
|
|
234
|
-
function addCost(into, key, head, body, ms, derived) {
|
|
238
|
+
function addCost(into, key, head, body, ms, derived, examined) {
|
|
235
239
|
const existing = into.get(key);
|
|
236
240
|
if (existing === void 0) {
|
|
237
|
-
into.set(key, { head, body, ms, derived, attempts: 1 });
|
|
241
|
+
into.set(key, { head, body, ms, derived, examined, attempts: 1 });
|
|
238
242
|
return;
|
|
239
243
|
}
|
|
240
244
|
existing.ms += ms;
|
|
241
245
|
existing.derived += derived;
|
|
246
|
+
existing.examined += examined;
|
|
242
247
|
existing.attempts += 1;
|
|
243
248
|
}
|
|
244
|
-
function chargeRule(ruleSet, head, body, ms, derived) {
|
|
249
|
+
function chargeRule(ruleSet, head, body, ms, derived, examined) {
|
|
245
250
|
const key = ruleKey(head, body);
|
|
246
251
|
for (const collector of open) {
|
|
247
|
-
addCost(collector.rules, key, head, body, ms, derived);
|
|
248
|
-
addCost(
|
|
252
|
+
addCost(collector.rules, key, head, body, ms, derived, examined);
|
|
253
|
+
addCost(
|
|
254
|
+
setIn(collector, ruleSet).rules,
|
|
255
|
+
key,
|
|
256
|
+
head,
|
|
257
|
+
body,
|
|
258
|
+
ms,
|
|
259
|
+
derived,
|
|
260
|
+
examined
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function chargeAbandoned(question, examined) {
|
|
265
|
+
for (const collector of open) {
|
|
266
|
+
collector.abandoned.push({ question, examined });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function chargeQuestion(outcome) {
|
|
270
|
+
for (const collector of open) {
|
|
271
|
+
collector.questions.asked += 1;
|
|
272
|
+
if (outcome !== void 0) {
|
|
273
|
+
collector.questions[outcome] += 1;
|
|
274
|
+
}
|
|
249
275
|
}
|
|
250
276
|
}
|
|
251
277
|
function chargeEvaluation(ruleSet) {
|
|
@@ -254,6 +280,13 @@ function chargeEvaluation(ruleSet) {
|
|
|
254
280
|
setIn(collector, ruleSet).evaluations += 1;
|
|
255
281
|
}
|
|
256
282
|
}
|
|
283
|
+
function chargeEvaluationRows(examined) {
|
|
284
|
+
for (const collector of open) {
|
|
285
|
+
if (examined > collector.largestEvaluation) {
|
|
286
|
+
collector.largestEvaluation = examined;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
257
290
|
function chargeRound(ruleSet) {
|
|
258
291
|
for (const collector of open) {
|
|
259
292
|
collector.rounds += 1;
|
|
@@ -313,7 +346,10 @@ function openScope() {
|
|
|
313
346
|
rules: /* @__PURE__ */ new Map(),
|
|
314
347
|
relations: /* @__PURE__ */ new Map(),
|
|
315
348
|
derivedRelations: /* @__PURE__ */ new Set(),
|
|
316
|
-
ruleSets: /* @__PURE__ */ new Map()
|
|
349
|
+
ruleSets: /* @__PURE__ */ new Map(),
|
|
350
|
+
abandoned: [],
|
|
351
|
+
largestEvaluation: 0,
|
|
352
|
+
questions: { asked: 0, abandoned: 0, skipped: 0 }
|
|
317
353
|
};
|
|
318
354
|
open.push(mine);
|
|
319
355
|
return mine;
|
|
@@ -331,6 +367,13 @@ var totalMs = (rules) => {
|
|
|
331
367
|
}
|
|
332
368
|
return ms;
|
|
333
369
|
};
|
|
370
|
+
var totalExamined = (rules) => {
|
|
371
|
+
let rows = 0;
|
|
372
|
+
for (const cost of rules) {
|
|
373
|
+
rows += cost.examined;
|
|
374
|
+
}
|
|
375
|
+
return rows;
|
|
376
|
+
};
|
|
334
377
|
var byCost = (a, b) => b.ms - a.ms;
|
|
335
378
|
function summarise(mine) {
|
|
336
379
|
return {
|
|
@@ -338,6 +381,10 @@ function summarise(mine) {
|
|
|
338
381
|
datalogMs: totalMs(mine.rules.values()),
|
|
339
382
|
rounds: mine.rounds,
|
|
340
383
|
evaluations: mine.evaluations,
|
|
384
|
+
examined: totalExamined(mine.rules.values()),
|
|
385
|
+
largestEvaluation: mine.largestEvaluation,
|
|
386
|
+
abandoned: [...mine.abandoned],
|
|
387
|
+
questions: { ...mine.questions },
|
|
341
388
|
relations: [...mine.relations].map(([relation, tuples]) => ({
|
|
342
389
|
relation,
|
|
343
390
|
tuples,
|
|
@@ -358,14 +405,26 @@ function ruleLines(rules, datalogMs) {
|
|
|
358
405
|
return rules.slice(0, 20).map((r) => {
|
|
359
406
|
const ms = `${r.ms.toFixed(0)}ms`.padStart(8);
|
|
360
407
|
const derived = String(r.derived).padStart(7);
|
|
361
|
-
|
|
408
|
+
const examined = String(r.examined).padStart(9);
|
|
409
|
+
return ` ${ms} ${share(r.ms, datalogMs)} ${derived} tuples ${examined} rows read ${r.attempts} attempts ${ruleKey(r.head, r.body)}`;
|
|
362
410
|
});
|
|
363
411
|
}
|
|
364
412
|
function formatProfile(profile) {
|
|
365
413
|
const lines = [];
|
|
366
414
|
lines.push(
|
|
367
|
-
`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`
|
|
415
|
+
`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)`
|
|
368
416
|
);
|
|
417
|
+
const { asked, abandoned, skipped } = profile.questions;
|
|
418
|
+
if (asked > 0) {
|
|
419
|
+
lines.push(
|
|
420
|
+
` ${asked} questions under a site: ${abandoned} given up part way, ${skipped} never put once the run's rows were spent`
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
for (const given of profile.abandoned) {
|
|
424
|
+
lines.push(
|
|
425
|
+
` gave up on ${given.question} after reading ${given.examined} rows`
|
|
426
|
+
);
|
|
427
|
+
}
|
|
369
428
|
const derivedTuples = profile.relations.filter((r) => r.derived).reduce((sum, r) => sum + r.tuples, 0);
|
|
370
429
|
lines.push(" tuples by relation (d = derived by a rule):");
|
|
371
430
|
for (const { relation, tuples, derived } of profile.relations.slice(0, 20)) {
|
|
@@ -838,7 +897,22 @@ function planBodyOrder(r, deltaAt) {
|
|
|
838
897
|
}
|
|
839
898
|
return order;
|
|
840
899
|
}
|
|
841
|
-
|
|
900
|
+
var rowBudget = (limit) => ({
|
|
901
|
+
examined: 0,
|
|
902
|
+
limit
|
|
903
|
+
});
|
|
904
|
+
var OUT_OF_BUDGET = /* @__PURE__ */ Symbol("datalog row budget exhausted");
|
|
905
|
+
var BudgetExhausted = class extends Error {
|
|
906
|
+
constructor(ruleSet, examined) {
|
|
907
|
+
super(`gave up after reading ${examined} rows without settling`);
|
|
908
|
+
this.ruleSet = ruleSet;
|
|
909
|
+
this.examined = examined;
|
|
910
|
+
this.name = "BudgetExhausted";
|
|
911
|
+
}
|
|
912
|
+
ruleSet;
|
|
913
|
+
examined;
|
|
914
|
+
};
|
|
915
|
+
function evaluateRule(db, deltas, r, deltaIndex, budget) {
|
|
842
916
|
const results = [];
|
|
843
917
|
const { deltaAt, order } = bodyPlan(r, deltaIndex);
|
|
844
918
|
const step = (orderIndex, bindings) => {
|
|
@@ -856,6 +930,10 @@ function evaluateRule(db, deltas, r, deltaIndex) {
|
|
|
856
930
|
}
|
|
857
931
|
const source = literalIndex === deltaAt ? deltas.get(literal.relation) ?? [] : boundSource(db, literal, bindings);
|
|
858
932
|
for (const tuple of source) {
|
|
933
|
+
budget.examined++;
|
|
934
|
+
if (budget.examined > budget.limit) {
|
|
935
|
+
throw OUT_OF_BUDGET;
|
|
936
|
+
}
|
|
859
937
|
const next = unify(literal, tuple, bindings);
|
|
860
938
|
if (next !== NO_MATCH) {
|
|
861
939
|
step(orderIndex + 1, next);
|
|
@@ -884,7 +962,7 @@ function boundSource(db, literal, bindings) {
|
|
|
884
962
|
return narrowest ?? db.facts(literal.relation);
|
|
885
963
|
}
|
|
886
964
|
var EMPTY_TAGS = [];
|
|
887
|
-
function evaluateRuleTagged(db, deltas, r, deltaIndex, algebra) {
|
|
965
|
+
function evaluateRuleTagged(db, deltas, r, deltaIndex, algebra, budget) {
|
|
888
966
|
const results = [];
|
|
889
967
|
const bodyTags = [];
|
|
890
968
|
const bodyMatches = [];
|
|
@@ -922,6 +1000,7 @@ function evaluateRuleTagged(db, deltas, r, deltaIndex, algebra) {
|
|
|
922
1000
|
}
|
|
923
1001
|
const source = positiveIndex === deltaIndex ? deltas.get(literal.relation) ?? [] : boundSource(db, literal, bindings);
|
|
924
1002
|
for (const tuple of source) {
|
|
1003
|
+
budget.examined++;
|
|
925
1004
|
const next = unify(literal, tuple, bindings);
|
|
926
1005
|
if (next !== NO_MATCH) {
|
|
927
1006
|
if (readsTags) {
|
|
@@ -1005,15 +1084,18 @@ function currentMarks(db) {
|
|
|
1005
1084
|
}
|
|
1006
1085
|
return marks;
|
|
1007
1086
|
}
|
|
1008
|
-
function evaluate(db, rules, algebra) {
|
|
1087
|
+
function evaluate(db, rules, algebra, budget) {
|
|
1009
1088
|
if (algebra !== void 0 && isDemandRewritten(rules)) {
|
|
1010
1089
|
throw new Error(
|
|
1011
1090
|
"cannot evaluate demand-rewritten rules with a tag algebra: tags would follow the demand-transformed rules rather than the ones the caller wrote"
|
|
1012
1091
|
);
|
|
1013
1092
|
}
|
|
1093
|
+
if (algebra !== void 0 && budget !== void 0) {
|
|
1094
|
+
throw new Error("cannot evaluate with both a tag algebra and a row budget");
|
|
1095
|
+
}
|
|
1014
1096
|
deriving.set(db, (deriving.get(db) ?? 0) + 1);
|
|
1015
1097
|
try {
|
|
1016
|
-
return runRules(db, rules, algebra);
|
|
1098
|
+
return runRules(db, rules, algebra, budget);
|
|
1017
1099
|
} finally {
|
|
1018
1100
|
const depth = (deriving.get(db) ?? 1) - 1;
|
|
1019
1101
|
if (depth === 0) {
|
|
@@ -1063,13 +1145,19 @@ function storedMergeFor(algebra) {
|
|
|
1063
1145
|
return stored === void 0 && merged === algebra.asserted ? void 0 : merged;
|
|
1064
1146
|
};
|
|
1065
1147
|
}
|
|
1066
|
-
function runRules(db, rules, algebra) {
|
|
1148
|
+
function runRules(db, rules, algebra, given) {
|
|
1067
1149
|
const {
|
|
1068
1150
|
signature,
|
|
1069
1151
|
name: ruleSetName,
|
|
1070
1152
|
derivedRelations,
|
|
1071
1153
|
strata
|
|
1072
1154
|
} = shapeOf(rules);
|
|
1155
|
+
const budget = given ?? {
|
|
1156
|
+
examined: 0,
|
|
1157
|
+
limit: Number.POSITIVE_INFINITY
|
|
1158
|
+
};
|
|
1159
|
+
const startedAt = budget.examined;
|
|
1160
|
+
const addedHere = given === void 0 ? null : [];
|
|
1073
1161
|
chargeEvaluation(ruleSetName);
|
|
1074
1162
|
const states = statesFor(db);
|
|
1075
1163
|
const state = states.get(signature) ?? {
|
|
@@ -1091,10 +1179,11 @@ function runRules(db, rules, algebra) {
|
|
|
1091
1179
|
derived.set(relation, ledger);
|
|
1092
1180
|
}
|
|
1093
1181
|
ledger.set(keyOf2(tuple), tuple);
|
|
1182
|
+
addedHere?.push([relation, tuple]);
|
|
1094
1183
|
};
|
|
1095
1184
|
const mergeStored = algebra === void 0 ? void 0 : storedMergeFor(algebra);
|
|
1096
1185
|
const marks = canResume(rules, state) ? state.marks : void 0;
|
|
1097
|
-
|
|
1186
|
+
const runStratum = (stratum) => {
|
|
1098
1187
|
let delta = /* @__PURE__ */ new Map();
|
|
1099
1188
|
const derivedHere = new Set(stratum.map((r) => r.head.relation));
|
|
1100
1189
|
const record = (relation, tuple) => {
|
|
@@ -1120,13 +1209,14 @@ function runRules(db, rules, algebra) {
|
|
|
1120
1209
|
seed,
|
|
1121
1210
|
r,
|
|
1122
1211
|
deltaIndex,
|
|
1123
|
-
algebra
|
|
1212
|
+
algebra,
|
|
1213
|
+
budget
|
|
1124
1214
|
)) {
|
|
1125
1215
|
recordTagged(r.head.relation, found.tuple, found.tag);
|
|
1126
1216
|
}
|
|
1127
1217
|
return;
|
|
1128
1218
|
}
|
|
1129
|
-
for (const tuple of evaluateRule(db, seed, r, deltaIndex)) {
|
|
1219
|
+
for (const tuple of evaluateRule(db, seed, r, deltaIndex, budget)) {
|
|
1130
1220
|
record(r.head.relation, tuple);
|
|
1131
1221
|
}
|
|
1132
1222
|
};
|
|
@@ -1135,15 +1225,17 @@ function runRules(db, rules, algebra) {
|
|
|
1135
1225
|
deriveInto(r, seed, deltaIndex);
|
|
1136
1226
|
return;
|
|
1137
1227
|
}
|
|
1138
|
-
const
|
|
1228
|
+
const startedAt2 = performance.now();
|
|
1139
1229
|
const before = db.size(r.head.relation);
|
|
1230
|
+
const readBefore = budget.examined;
|
|
1140
1231
|
deriveInto(r, seed, deltaIndex);
|
|
1141
1232
|
chargeRule(
|
|
1142
1233
|
ruleSetName,
|
|
1143
1234
|
r.head.relation,
|
|
1144
1235
|
r.body.map(literalName),
|
|
1145
|
-
performance.now() -
|
|
1146
|
-
db.size(r.head.relation) - before
|
|
1236
|
+
performance.now() - startedAt2,
|
|
1237
|
+
db.size(r.head.relation) - before,
|
|
1238
|
+
budget.examined - readBefore
|
|
1147
1239
|
);
|
|
1148
1240
|
};
|
|
1149
1241
|
const applyDelta = (seed, derivedOnly) => {
|
|
@@ -1188,7 +1280,24 @@ function runRules(db, rules, algebra) {
|
|
|
1188
1280
|
applyDelta(lastDelta, true);
|
|
1189
1281
|
chargeRound(ruleSetName);
|
|
1190
1282
|
}
|
|
1283
|
+
};
|
|
1284
|
+
const readHere = () => budget.examined - startedAt;
|
|
1285
|
+
try {
|
|
1286
|
+
for (const stratum of strata) {
|
|
1287
|
+
runStratum(stratum);
|
|
1288
|
+
}
|
|
1289
|
+
} catch (error) {
|
|
1290
|
+
if (error !== OUT_OF_BUDGET) {
|
|
1291
|
+
throw error;
|
|
1292
|
+
}
|
|
1293
|
+
for (const [relation, tuples] of byRelation(addedHere ?? [])) {
|
|
1294
|
+
db.retract(relation, tuples);
|
|
1295
|
+
}
|
|
1296
|
+
state.marks = null;
|
|
1297
|
+
chargeEvaluationRows(readHere());
|
|
1298
|
+
throw new BudgetExhausted(ruleSetName, readHere());
|
|
1191
1299
|
}
|
|
1300
|
+
chargeEvaluationRows(readHere());
|
|
1192
1301
|
state.marks = currentMarks(db);
|
|
1193
1302
|
if (isProfiling()) {
|
|
1194
1303
|
chargeRelationSizes(
|
|
@@ -1198,6 +1307,13 @@ function runRules(db, rules, algebra) {
|
|
|
1198
1307
|
}
|
|
1199
1308
|
return db;
|
|
1200
1309
|
}
|
|
1310
|
+
function byRelation(added) {
|
|
1311
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
1312
|
+
for (const [relation, tuple] of added) {
|
|
1313
|
+
addTo(buckets, relation, tuple);
|
|
1314
|
+
}
|
|
1315
|
+
return buckets;
|
|
1316
|
+
}
|
|
1201
1317
|
function addTo(buckets, relation, tuple) {
|
|
1202
1318
|
const bucket = buckets.get(relation);
|
|
1203
1319
|
if (bucket === void 0) {
|
|
@@ -1208,8 +1324,11 @@ function addTo(buckets, relation, tuple) {
|
|
|
1208
1324
|
}
|
|
1209
1325
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1210
1326
|
0 && (module.exports = {
|
|
1327
|
+
BudgetExhausted,
|
|
1211
1328
|
Database,
|
|
1212
1329
|
Witness,
|
|
1330
|
+
chargeAbandoned,
|
|
1331
|
+
chargeQuestion,
|
|
1213
1332
|
clearRelations,
|
|
1214
1333
|
confidence,
|
|
1215
1334
|
confidenceWith,
|
|
@@ -1222,6 +1341,7 @@ function addTo(buckets, relation, tuple) {
|
|
|
1222
1341
|
profileEvaluation,
|
|
1223
1342
|
profileEvaluationAsync,
|
|
1224
1343
|
proofOf,
|
|
1344
|
+
rowBudget,
|
|
1225
1345
|
rule,
|
|
1226
1346
|
ruleLabel,
|
|
1227
1347
|
stratify,
|
package/dist/index.d.cts
CHANGED
|
@@ -90,9 +90,32 @@ interface RuleCost {
|
|
|
90
90
|
ms: number;
|
|
91
91
|
/** Tuples this rule added that no earlier derivation had reached. */
|
|
92
92
|
derived: number;
|
|
93
|
+
/**
|
|
94
|
+
* Rows the join read for this rule. A rule that examines a hundred
|
|
95
|
+
* thousand rows to derive a dozen tuples is doing a walk somebody
|
|
96
|
+
* meant to be a lookup, and `derived` alone never shows that.
|
|
97
|
+
*/
|
|
98
|
+
examined: number;
|
|
93
99
|
/** How many times the rule was evaluated, over all rounds and deltas. */
|
|
94
100
|
attempts: number;
|
|
95
101
|
}
|
|
102
|
+
/** A question the engine gave up on, and how much it had read by then. */
|
|
103
|
+
interface AbandonedQuestion {
|
|
104
|
+
/** What the caller was asking, in whatever words the caller uses. */
|
|
105
|
+
question: string;
|
|
106
|
+
examined: number;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* How a caller that asks one question at a time got on. `skipped` are
|
|
110
|
+
* the ones it never put, because what it had already spent was gone.
|
|
111
|
+
*/
|
|
112
|
+
interface QuestionTally {
|
|
113
|
+
asked: number;
|
|
114
|
+
abandoned: number;
|
|
115
|
+
skipped: number;
|
|
116
|
+
}
|
|
117
|
+
/** How one question ended, for the tally. */
|
|
118
|
+
type QuestionOutcome = keyof Omit<QuestionTally, "asked">;
|
|
96
119
|
/**
|
|
97
120
|
* How big a relation grew. `derived` separates the relations rules
|
|
98
121
|
* concluded from the base facts an adapter emitted, because "derive fewer
|
|
@@ -130,6 +153,18 @@ interface EvaluationProfile {
|
|
|
130
153
|
* corpus size is worth more attention than the rule table.
|
|
131
154
|
*/
|
|
132
155
|
evaluations: number;
|
|
156
|
+
/** Rows every join read, summed over every rule set. */
|
|
157
|
+
examined: number;
|
|
158
|
+
/**
|
|
159
|
+
* The most rows any one `evaluate` call read. A caller that asks a
|
|
160
|
+
* question at a time sizes a per-question budget off this, where the
|
|
161
|
+
* total only says how many questions it asked.
|
|
162
|
+
*/
|
|
163
|
+
largestEvaluation: number;
|
|
164
|
+
/** Every question abandoned on its budget, in the order they were given up. */
|
|
165
|
+
abandoned: AbandonedQuestion[];
|
|
166
|
+
/** How many questions a one-at-a-time caller asked, and how they went. */
|
|
167
|
+
questions: QuestionTally;
|
|
133
168
|
/** Final tuple count per relation, largest first. */
|
|
134
169
|
relations: RelationSize[];
|
|
135
170
|
/** Per-rule cost across every rule set, most expensive first. */
|
|
@@ -137,6 +172,17 @@ interface EvaluationProfile {
|
|
|
137
172
|
/** The same time, split by the rule set that spent it. */
|
|
138
173
|
ruleSets: RuleSetCost[];
|
|
139
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Note that a question was given up on. A caller reading this in a
|
|
177
|
+
* profile is looking at the reason an answer came back empty, so the
|
|
178
|
+
* question is recorded in the caller's own words.
|
|
179
|
+
*/
|
|
180
|
+
declare function chargeAbandoned(question: string, examined: number): void;
|
|
181
|
+
/**
|
|
182
|
+
* Note that a caller put one question. `outcome` says whether it was
|
|
183
|
+
* given up part way or never put at all; leave it out when it answered.
|
|
184
|
+
*/
|
|
185
|
+
declare function chargeQuestion(outcome?: QuestionOutcome): void;
|
|
140
186
|
/**
|
|
141
187
|
* Run `fn` with evaluation profiling on, and hand back what it returned
|
|
142
188
|
* alongside the numbers. Nested calls share the outer collector, so a
|
|
@@ -403,18 +449,54 @@ interface Derivation {
|
|
|
403
449
|
* stratified, so this throws.
|
|
404
450
|
*/
|
|
405
451
|
declare function stratify(rules: Rule[]): Rule[][];
|
|
452
|
+
/**
|
|
453
|
+
* How many rows the joins of one evaluation may read, and how many they
|
|
454
|
+
* have read so far. Counting rows rather than new tuples is what catches
|
|
455
|
+
* a question that walks: the walk finds the same conclusions over and
|
|
456
|
+
* over, so nothing new arrives while the reading runs away.
|
|
457
|
+
*
|
|
458
|
+
* The caller builds one with `rowBudget` and reads `examined` back after
|
|
459
|
+
* the call, which is how it learns what a question cost whether or not
|
|
460
|
+
* the question settled. An evaluation with no budget gets a limit of
|
|
461
|
+
* Infinity, so the hot loop does the same increment and the same
|
|
462
|
+
* compare either way.
|
|
463
|
+
*/
|
|
464
|
+
interface RowBudget {
|
|
465
|
+
examined: number;
|
|
466
|
+
limit: number;
|
|
467
|
+
}
|
|
468
|
+
/** A budget for one evaluation, ready to hand to `evaluate`. */
|
|
469
|
+
declare const rowBudget: (limit: number) => RowBudget;
|
|
470
|
+
/**
|
|
471
|
+
* What `evaluate` throws when a row budget runs out.
|
|
472
|
+
*
|
|
473
|
+
* By the time this reaches a caller the database is back where it
|
|
474
|
+
* started: every fact that evaluation derived has been retracted, and
|
|
475
|
+
* the rule set will start over from the base facts next time rather
|
|
476
|
+
* than from a delta that was never worked through. The caller's own
|
|
477
|
+
* facts, the question it asked among them, are still there for it to
|
|
478
|
+
* take back.
|
|
479
|
+
*/
|
|
480
|
+
declare class BudgetExhausted extends Error {
|
|
481
|
+
readonly ruleSet: string;
|
|
482
|
+
readonly examined: number;
|
|
483
|
+
constructor(ruleSet: string, examined: number);
|
|
484
|
+
}
|
|
406
485
|
/**
|
|
407
486
|
* Evaluate `rules` over `db` to fixpoint, adding derived facts in
|
|
408
487
|
* place. Rules are stratified first; within a stratum, semi-naïve
|
|
409
|
-
* iteration joins each rule against the last round's delta so work
|
|
410
|
-
* proportional to new facts
|
|
488
|
+
* iteration joins each rule against the last round's delta, so work
|
|
489
|
+
* is proportional to new facts rather than to all facts.
|
|
411
490
|
*
|
|
412
491
|
* With an `algebra`, every derivation also tags its conclusion (see
|
|
413
|
-
* `TagAlgebra`). Supply the same algebra
|
|
414
|
-
*
|
|
415
|
-
*
|
|
492
|
+
* `TagAlgebra`). Supply the same algebra every time over one
|
|
493
|
+
* database: a resumed run tags only what the new facts reach.
|
|
494
|
+
*
|
|
495
|
+
* With a `budget`, evaluation gives up once its joins have read that
|
|
496
|
+
* many rows and throws `BudgetExhausted`. Either way the budget says
|
|
497
|
+
* afterwards what the evaluation cost.
|
|
416
498
|
*/
|
|
417
|
-
declare function evaluate<Tag = never>(db: Database, rules: Rule[], algebra?: TagAlgebra<Tag
|
|
499
|
+
declare function evaluate<Tag = never>(db: Database, rules: Rule[], algebra?: TagAlgebra<Tag>, budget?: RowBudget): Database;
|
|
418
500
|
/**
|
|
419
501
|
* Empty these relations, and leave `rules` able to carry on from where
|
|
420
502
|
* it got to.
|
|
@@ -438,4 +520,4 @@ declare function evaluate<Tag = never>(db: Database, rules: Rule[], algebra?: Ta
|
|
|
438
520
|
*/
|
|
439
521
|
declare function clearRelations(db: Database, rules: Rule[], relations: readonly string[]): void;
|
|
440
522
|
|
|
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 };
|
|
523
|
+
export { type AbandonedQuestion, type AddOutcome, type Atom, type BodyMatch, BudgetExhausted, type ConfidenceLevel, Database, type Demand, type Derivation, type EvaluationProfile, type Literal, type OnDemandRules, type Proof, type ProofDerived, type ProofOptions, type QuestionOutcome, type QuestionTally, type RowBudget, type Rule, type RuleCost, type TagAlgebra, type Term, type Tuple, Witness, type WitnessTag, chargeAbandoned, chargeQuestion, clearRelations, confidence, confidenceWith, constant, deriveOnDemand, evaluate, formatProfile, lit, notLit, profileEvaluation, profileEvaluationAsync, proofOf, rowBudget, rule, ruleLabel, stratify, tupleKey, tupleKeyParts, variable, witnesses };
|
package/dist/index.d.ts
CHANGED
|
@@ -90,9 +90,32 @@ interface RuleCost {
|
|
|
90
90
|
ms: number;
|
|
91
91
|
/** Tuples this rule added that no earlier derivation had reached. */
|
|
92
92
|
derived: number;
|
|
93
|
+
/**
|
|
94
|
+
* Rows the join read for this rule. A rule that examines a hundred
|
|
95
|
+
* thousand rows to derive a dozen tuples is doing a walk somebody
|
|
96
|
+
* meant to be a lookup, and `derived` alone never shows that.
|
|
97
|
+
*/
|
|
98
|
+
examined: number;
|
|
93
99
|
/** How many times the rule was evaluated, over all rounds and deltas. */
|
|
94
100
|
attempts: number;
|
|
95
101
|
}
|
|
102
|
+
/** A question the engine gave up on, and how much it had read by then. */
|
|
103
|
+
interface AbandonedQuestion {
|
|
104
|
+
/** What the caller was asking, in whatever words the caller uses. */
|
|
105
|
+
question: string;
|
|
106
|
+
examined: number;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* How a caller that asks one question at a time got on. `skipped` are
|
|
110
|
+
* the ones it never put, because what it had already spent was gone.
|
|
111
|
+
*/
|
|
112
|
+
interface QuestionTally {
|
|
113
|
+
asked: number;
|
|
114
|
+
abandoned: number;
|
|
115
|
+
skipped: number;
|
|
116
|
+
}
|
|
117
|
+
/** How one question ended, for the tally. */
|
|
118
|
+
type QuestionOutcome = keyof Omit<QuestionTally, "asked">;
|
|
96
119
|
/**
|
|
97
120
|
* How big a relation grew. `derived` separates the relations rules
|
|
98
121
|
* concluded from the base facts an adapter emitted, because "derive fewer
|
|
@@ -130,6 +153,18 @@ interface EvaluationProfile {
|
|
|
130
153
|
* corpus size is worth more attention than the rule table.
|
|
131
154
|
*/
|
|
132
155
|
evaluations: number;
|
|
156
|
+
/** Rows every join read, summed over every rule set. */
|
|
157
|
+
examined: number;
|
|
158
|
+
/**
|
|
159
|
+
* The most rows any one `evaluate` call read. A caller that asks a
|
|
160
|
+
* question at a time sizes a per-question budget off this, where the
|
|
161
|
+
* total only says how many questions it asked.
|
|
162
|
+
*/
|
|
163
|
+
largestEvaluation: number;
|
|
164
|
+
/** Every question abandoned on its budget, in the order they were given up. */
|
|
165
|
+
abandoned: AbandonedQuestion[];
|
|
166
|
+
/** How many questions a one-at-a-time caller asked, and how they went. */
|
|
167
|
+
questions: QuestionTally;
|
|
133
168
|
/** Final tuple count per relation, largest first. */
|
|
134
169
|
relations: RelationSize[];
|
|
135
170
|
/** Per-rule cost across every rule set, most expensive first. */
|
|
@@ -137,6 +172,17 @@ interface EvaluationProfile {
|
|
|
137
172
|
/** The same time, split by the rule set that spent it. */
|
|
138
173
|
ruleSets: RuleSetCost[];
|
|
139
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Note that a question was given up on. A caller reading this in a
|
|
177
|
+
* profile is looking at the reason an answer came back empty, so the
|
|
178
|
+
* question is recorded in the caller's own words.
|
|
179
|
+
*/
|
|
180
|
+
declare function chargeAbandoned(question: string, examined: number): void;
|
|
181
|
+
/**
|
|
182
|
+
* Note that a caller put one question. `outcome` says whether it was
|
|
183
|
+
* given up part way or never put at all; leave it out when it answered.
|
|
184
|
+
*/
|
|
185
|
+
declare function chargeQuestion(outcome?: QuestionOutcome): void;
|
|
140
186
|
/**
|
|
141
187
|
* Run `fn` with evaluation profiling on, and hand back what it returned
|
|
142
188
|
* alongside the numbers. Nested calls share the outer collector, so a
|
|
@@ -403,18 +449,54 @@ interface Derivation {
|
|
|
403
449
|
* stratified, so this throws.
|
|
404
450
|
*/
|
|
405
451
|
declare function stratify(rules: Rule[]): Rule[][];
|
|
452
|
+
/**
|
|
453
|
+
* How many rows the joins of one evaluation may read, and how many they
|
|
454
|
+
* have read so far. Counting rows rather than new tuples is what catches
|
|
455
|
+
* a question that walks: the walk finds the same conclusions over and
|
|
456
|
+
* over, so nothing new arrives while the reading runs away.
|
|
457
|
+
*
|
|
458
|
+
* The caller builds one with `rowBudget` and reads `examined` back after
|
|
459
|
+
* the call, which is how it learns what a question cost whether or not
|
|
460
|
+
* the question settled. An evaluation with no budget gets a limit of
|
|
461
|
+
* Infinity, so the hot loop does the same increment and the same
|
|
462
|
+
* compare either way.
|
|
463
|
+
*/
|
|
464
|
+
interface RowBudget {
|
|
465
|
+
examined: number;
|
|
466
|
+
limit: number;
|
|
467
|
+
}
|
|
468
|
+
/** A budget for one evaluation, ready to hand to `evaluate`. */
|
|
469
|
+
declare const rowBudget: (limit: number) => RowBudget;
|
|
470
|
+
/**
|
|
471
|
+
* What `evaluate` throws when a row budget runs out.
|
|
472
|
+
*
|
|
473
|
+
* By the time this reaches a caller the database is back where it
|
|
474
|
+
* started: every fact that evaluation derived has been retracted, and
|
|
475
|
+
* the rule set will start over from the base facts next time rather
|
|
476
|
+
* than from a delta that was never worked through. The caller's own
|
|
477
|
+
* facts, the question it asked among them, are still there for it to
|
|
478
|
+
* take back.
|
|
479
|
+
*/
|
|
480
|
+
declare class BudgetExhausted extends Error {
|
|
481
|
+
readonly ruleSet: string;
|
|
482
|
+
readonly examined: number;
|
|
483
|
+
constructor(ruleSet: string, examined: number);
|
|
484
|
+
}
|
|
406
485
|
/**
|
|
407
486
|
* Evaluate `rules` over `db` to fixpoint, adding derived facts in
|
|
408
487
|
* place. Rules are stratified first; within a stratum, semi-naïve
|
|
409
|
-
* iteration joins each rule against the last round's delta so work
|
|
410
|
-
* proportional to new facts
|
|
488
|
+
* iteration joins each rule against the last round's delta, so work
|
|
489
|
+
* is proportional to new facts rather than to all facts.
|
|
411
490
|
*
|
|
412
491
|
* With an `algebra`, every derivation also tags its conclusion (see
|
|
413
|
-
* `TagAlgebra`). Supply the same algebra
|
|
414
|
-
*
|
|
415
|
-
*
|
|
492
|
+
* `TagAlgebra`). Supply the same algebra every time over one
|
|
493
|
+
* database: a resumed run tags only what the new facts reach.
|
|
494
|
+
*
|
|
495
|
+
* With a `budget`, evaluation gives up once its joins have read that
|
|
496
|
+
* many rows and throws `BudgetExhausted`. Either way the budget says
|
|
497
|
+
* afterwards what the evaluation cost.
|
|
416
498
|
*/
|
|
417
|
-
declare function evaluate<Tag = never>(db: Database, rules: Rule[], algebra?: TagAlgebra<Tag
|
|
499
|
+
declare function evaluate<Tag = never>(db: Database, rules: Rule[], algebra?: TagAlgebra<Tag>, budget?: RowBudget): Database;
|
|
418
500
|
/**
|
|
419
501
|
* Empty these relations, and leave `rules` able to carry on from where
|
|
420
502
|
* it got to.
|
|
@@ -438,4 +520,4 @@ declare function evaluate<Tag = never>(db: Database, rules: Rule[], algebra?: Ta
|
|
|
438
520
|
*/
|
|
439
521
|
declare function clearRelations(db: Database, rules: Rule[], relations: readonly string[]): void;
|
|
440
522
|
|
|
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 };
|
|
523
|
+
export { type AbandonedQuestion, type AddOutcome, type Atom, type BodyMatch, BudgetExhausted, type ConfidenceLevel, Database, type Demand, type Derivation, type EvaluationProfile, type Literal, type OnDemandRules, type Proof, type ProofDerived, type ProofOptions, type QuestionOutcome, type QuestionTally, type RowBudget, type Rule, type RuleCost, type TagAlgebra, type Term, type Tuple, Witness, type WitnessTag, chargeAbandoned, chargeQuestion, clearRelations, confidence, confidenceWith, constant, deriveOnDemand, evaluate, formatProfile, lit, notLit, profileEvaluation, profileEvaluationAsync, proofOf, rowBudget, rule, ruleLabel, stratify, tupleKey, tupleKeyParts, variable, witnesses };
|
package/dist/index.js
CHANGED
|
@@ -185,21 +185,43 @@ function bodyAdornments(r, headAdornment, derives) {
|
|
|
185
185
|
var open = [];
|
|
186
186
|
var isProfiling = () => open.length > 0;
|
|
187
187
|
var ruleKey = (head, body) => `${head} <- ${body.join(", ")}`;
|
|
188
|
-
function addCost(into, key, head, body, ms, derived) {
|
|
188
|
+
function addCost(into, key, head, body, ms, derived, examined) {
|
|
189
189
|
const existing = into.get(key);
|
|
190
190
|
if (existing === void 0) {
|
|
191
|
-
into.set(key, { head, body, ms, derived, attempts: 1 });
|
|
191
|
+
into.set(key, { head, body, ms, derived, examined, attempts: 1 });
|
|
192
192
|
return;
|
|
193
193
|
}
|
|
194
194
|
existing.ms += ms;
|
|
195
195
|
existing.derived += derived;
|
|
196
|
+
existing.examined += examined;
|
|
196
197
|
existing.attempts += 1;
|
|
197
198
|
}
|
|
198
|
-
function chargeRule(ruleSet, head, body, ms, derived) {
|
|
199
|
+
function chargeRule(ruleSet, head, body, ms, derived, examined) {
|
|
199
200
|
const key = ruleKey(head, body);
|
|
200
201
|
for (const collector of open) {
|
|
201
|
-
addCost(collector.rules, key, head, body, ms, derived);
|
|
202
|
-
addCost(
|
|
202
|
+
addCost(collector.rules, key, head, body, ms, derived, examined);
|
|
203
|
+
addCost(
|
|
204
|
+
setIn(collector, ruleSet).rules,
|
|
205
|
+
key,
|
|
206
|
+
head,
|
|
207
|
+
body,
|
|
208
|
+
ms,
|
|
209
|
+
derived,
|
|
210
|
+
examined
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function chargeAbandoned(question, examined) {
|
|
215
|
+
for (const collector of open) {
|
|
216
|
+
collector.abandoned.push({ question, examined });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function chargeQuestion(outcome) {
|
|
220
|
+
for (const collector of open) {
|
|
221
|
+
collector.questions.asked += 1;
|
|
222
|
+
if (outcome !== void 0) {
|
|
223
|
+
collector.questions[outcome] += 1;
|
|
224
|
+
}
|
|
203
225
|
}
|
|
204
226
|
}
|
|
205
227
|
function chargeEvaluation(ruleSet) {
|
|
@@ -208,6 +230,13 @@ function chargeEvaluation(ruleSet) {
|
|
|
208
230
|
setIn(collector, ruleSet).evaluations += 1;
|
|
209
231
|
}
|
|
210
232
|
}
|
|
233
|
+
function chargeEvaluationRows(examined) {
|
|
234
|
+
for (const collector of open) {
|
|
235
|
+
if (examined > collector.largestEvaluation) {
|
|
236
|
+
collector.largestEvaluation = examined;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
211
240
|
function chargeRound(ruleSet) {
|
|
212
241
|
for (const collector of open) {
|
|
213
242
|
collector.rounds += 1;
|
|
@@ -267,7 +296,10 @@ function openScope() {
|
|
|
267
296
|
rules: /* @__PURE__ */ new Map(),
|
|
268
297
|
relations: /* @__PURE__ */ new Map(),
|
|
269
298
|
derivedRelations: /* @__PURE__ */ new Set(),
|
|
270
|
-
ruleSets: /* @__PURE__ */ new Map()
|
|
299
|
+
ruleSets: /* @__PURE__ */ new Map(),
|
|
300
|
+
abandoned: [],
|
|
301
|
+
largestEvaluation: 0,
|
|
302
|
+
questions: { asked: 0, abandoned: 0, skipped: 0 }
|
|
271
303
|
};
|
|
272
304
|
open.push(mine);
|
|
273
305
|
return mine;
|
|
@@ -285,6 +317,13 @@ var totalMs = (rules) => {
|
|
|
285
317
|
}
|
|
286
318
|
return ms;
|
|
287
319
|
};
|
|
320
|
+
var totalExamined = (rules) => {
|
|
321
|
+
let rows = 0;
|
|
322
|
+
for (const cost of rules) {
|
|
323
|
+
rows += cost.examined;
|
|
324
|
+
}
|
|
325
|
+
return rows;
|
|
326
|
+
};
|
|
288
327
|
var byCost = (a, b) => b.ms - a.ms;
|
|
289
328
|
function summarise(mine) {
|
|
290
329
|
return {
|
|
@@ -292,6 +331,10 @@ function summarise(mine) {
|
|
|
292
331
|
datalogMs: totalMs(mine.rules.values()),
|
|
293
332
|
rounds: mine.rounds,
|
|
294
333
|
evaluations: mine.evaluations,
|
|
334
|
+
examined: totalExamined(mine.rules.values()),
|
|
335
|
+
largestEvaluation: mine.largestEvaluation,
|
|
336
|
+
abandoned: [...mine.abandoned],
|
|
337
|
+
questions: { ...mine.questions },
|
|
295
338
|
relations: [...mine.relations].map(([relation, tuples]) => ({
|
|
296
339
|
relation,
|
|
297
340
|
tuples,
|
|
@@ -312,14 +355,26 @@ function ruleLines(rules, datalogMs) {
|
|
|
312
355
|
return rules.slice(0, 20).map((r) => {
|
|
313
356
|
const ms = `${r.ms.toFixed(0)}ms`.padStart(8);
|
|
314
357
|
const derived = String(r.derived).padStart(7);
|
|
315
|
-
|
|
358
|
+
const examined = String(r.examined).padStart(9);
|
|
359
|
+
return ` ${ms} ${share(r.ms, datalogMs)} ${derived} tuples ${examined} rows read ${r.attempts} attempts ${ruleKey(r.head, r.body)}`;
|
|
316
360
|
});
|
|
317
361
|
}
|
|
318
362
|
function formatProfile(profile) {
|
|
319
363
|
const lines = [];
|
|
320
364
|
lines.push(
|
|
321
|
-
`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`
|
|
365
|
+
`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)`
|
|
322
366
|
);
|
|
367
|
+
const { asked, abandoned, skipped } = profile.questions;
|
|
368
|
+
if (asked > 0) {
|
|
369
|
+
lines.push(
|
|
370
|
+
` ${asked} questions under a site: ${abandoned} given up part way, ${skipped} never put once the run's rows were spent`
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
for (const given of profile.abandoned) {
|
|
374
|
+
lines.push(
|
|
375
|
+
` gave up on ${given.question} after reading ${given.examined} rows`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
323
378
|
const derivedTuples = profile.relations.filter((r) => r.derived).reduce((sum, r) => sum + r.tuples, 0);
|
|
324
379
|
lines.push(" tuples by relation (d = derived by a rule):");
|
|
325
380
|
for (const { relation, tuples, derived } of profile.relations.slice(0, 20)) {
|
|
@@ -792,7 +847,22 @@ function planBodyOrder(r, deltaAt) {
|
|
|
792
847
|
}
|
|
793
848
|
return order;
|
|
794
849
|
}
|
|
795
|
-
|
|
850
|
+
var rowBudget = (limit) => ({
|
|
851
|
+
examined: 0,
|
|
852
|
+
limit
|
|
853
|
+
});
|
|
854
|
+
var OUT_OF_BUDGET = /* @__PURE__ */ Symbol("datalog row budget exhausted");
|
|
855
|
+
var BudgetExhausted = class extends Error {
|
|
856
|
+
constructor(ruleSet, examined) {
|
|
857
|
+
super(`gave up after reading ${examined} rows without settling`);
|
|
858
|
+
this.ruleSet = ruleSet;
|
|
859
|
+
this.examined = examined;
|
|
860
|
+
this.name = "BudgetExhausted";
|
|
861
|
+
}
|
|
862
|
+
ruleSet;
|
|
863
|
+
examined;
|
|
864
|
+
};
|
|
865
|
+
function evaluateRule(db, deltas, r, deltaIndex, budget) {
|
|
796
866
|
const results = [];
|
|
797
867
|
const { deltaAt, order } = bodyPlan(r, deltaIndex);
|
|
798
868
|
const step = (orderIndex, bindings) => {
|
|
@@ -810,6 +880,10 @@ function evaluateRule(db, deltas, r, deltaIndex) {
|
|
|
810
880
|
}
|
|
811
881
|
const source = literalIndex === deltaAt ? deltas.get(literal.relation) ?? [] : boundSource(db, literal, bindings);
|
|
812
882
|
for (const tuple of source) {
|
|
883
|
+
budget.examined++;
|
|
884
|
+
if (budget.examined > budget.limit) {
|
|
885
|
+
throw OUT_OF_BUDGET;
|
|
886
|
+
}
|
|
813
887
|
const next = unify(literal, tuple, bindings);
|
|
814
888
|
if (next !== NO_MATCH) {
|
|
815
889
|
step(orderIndex + 1, next);
|
|
@@ -838,7 +912,7 @@ function boundSource(db, literal, bindings) {
|
|
|
838
912
|
return narrowest ?? db.facts(literal.relation);
|
|
839
913
|
}
|
|
840
914
|
var EMPTY_TAGS = [];
|
|
841
|
-
function evaluateRuleTagged(db, deltas, r, deltaIndex, algebra) {
|
|
915
|
+
function evaluateRuleTagged(db, deltas, r, deltaIndex, algebra, budget) {
|
|
842
916
|
const results = [];
|
|
843
917
|
const bodyTags = [];
|
|
844
918
|
const bodyMatches = [];
|
|
@@ -876,6 +950,7 @@ function evaluateRuleTagged(db, deltas, r, deltaIndex, algebra) {
|
|
|
876
950
|
}
|
|
877
951
|
const source = positiveIndex === deltaIndex ? deltas.get(literal.relation) ?? [] : boundSource(db, literal, bindings);
|
|
878
952
|
for (const tuple of source) {
|
|
953
|
+
budget.examined++;
|
|
879
954
|
const next = unify(literal, tuple, bindings);
|
|
880
955
|
if (next !== NO_MATCH) {
|
|
881
956
|
if (readsTags) {
|
|
@@ -959,15 +1034,18 @@ function currentMarks(db) {
|
|
|
959
1034
|
}
|
|
960
1035
|
return marks;
|
|
961
1036
|
}
|
|
962
|
-
function evaluate(db, rules, algebra) {
|
|
1037
|
+
function evaluate(db, rules, algebra, budget) {
|
|
963
1038
|
if (algebra !== void 0 && isDemandRewritten(rules)) {
|
|
964
1039
|
throw new Error(
|
|
965
1040
|
"cannot evaluate demand-rewritten rules with a tag algebra: tags would follow the demand-transformed rules rather than the ones the caller wrote"
|
|
966
1041
|
);
|
|
967
1042
|
}
|
|
1043
|
+
if (algebra !== void 0 && budget !== void 0) {
|
|
1044
|
+
throw new Error("cannot evaluate with both a tag algebra and a row budget");
|
|
1045
|
+
}
|
|
968
1046
|
deriving.set(db, (deriving.get(db) ?? 0) + 1);
|
|
969
1047
|
try {
|
|
970
|
-
return runRules(db, rules, algebra);
|
|
1048
|
+
return runRules(db, rules, algebra, budget);
|
|
971
1049
|
} finally {
|
|
972
1050
|
const depth = (deriving.get(db) ?? 1) - 1;
|
|
973
1051
|
if (depth === 0) {
|
|
@@ -1017,13 +1095,19 @@ function storedMergeFor(algebra) {
|
|
|
1017
1095
|
return stored === void 0 && merged === algebra.asserted ? void 0 : merged;
|
|
1018
1096
|
};
|
|
1019
1097
|
}
|
|
1020
|
-
function runRules(db, rules, algebra) {
|
|
1098
|
+
function runRules(db, rules, algebra, given) {
|
|
1021
1099
|
const {
|
|
1022
1100
|
signature,
|
|
1023
1101
|
name: ruleSetName,
|
|
1024
1102
|
derivedRelations,
|
|
1025
1103
|
strata
|
|
1026
1104
|
} = shapeOf(rules);
|
|
1105
|
+
const budget = given ?? {
|
|
1106
|
+
examined: 0,
|
|
1107
|
+
limit: Number.POSITIVE_INFINITY
|
|
1108
|
+
};
|
|
1109
|
+
const startedAt = budget.examined;
|
|
1110
|
+
const addedHere = given === void 0 ? null : [];
|
|
1027
1111
|
chargeEvaluation(ruleSetName);
|
|
1028
1112
|
const states = statesFor(db);
|
|
1029
1113
|
const state = states.get(signature) ?? {
|
|
@@ -1045,10 +1129,11 @@ function runRules(db, rules, algebra) {
|
|
|
1045
1129
|
derived.set(relation, ledger);
|
|
1046
1130
|
}
|
|
1047
1131
|
ledger.set(keyOf2(tuple), tuple);
|
|
1132
|
+
addedHere?.push([relation, tuple]);
|
|
1048
1133
|
};
|
|
1049
1134
|
const mergeStored = algebra === void 0 ? void 0 : storedMergeFor(algebra);
|
|
1050
1135
|
const marks = canResume(rules, state) ? state.marks : void 0;
|
|
1051
|
-
|
|
1136
|
+
const runStratum = (stratum) => {
|
|
1052
1137
|
let delta = /* @__PURE__ */ new Map();
|
|
1053
1138
|
const derivedHere = new Set(stratum.map((r) => r.head.relation));
|
|
1054
1139
|
const record = (relation, tuple) => {
|
|
@@ -1074,13 +1159,14 @@ function runRules(db, rules, algebra) {
|
|
|
1074
1159
|
seed,
|
|
1075
1160
|
r,
|
|
1076
1161
|
deltaIndex,
|
|
1077
|
-
algebra
|
|
1162
|
+
algebra,
|
|
1163
|
+
budget
|
|
1078
1164
|
)) {
|
|
1079
1165
|
recordTagged(r.head.relation, found.tuple, found.tag);
|
|
1080
1166
|
}
|
|
1081
1167
|
return;
|
|
1082
1168
|
}
|
|
1083
|
-
for (const tuple of evaluateRule(db, seed, r, deltaIndex)) {
|
|
1169
|
+
for (const tuple of evaluateRule(db, seed, r, deltaIndex, budget)) {
|
|
1084
1170
|
record(r.head.relation, tuple);
|
|
1085
1171
|
}
|
|
1086
1172
|
};
|
|
@@ -1089,15 +1175,17 @@ function runRules(db, rules, algebra) {
|
|
|
1089
1175
|
deriveInto(r, seed, deltaIndex);
|
|
1090
1176
|
return;
|
|
1091
1177
|
}
|
|
1092
|
-
const
|
|
1178
|
+
const startedAt2 = performance.now();
|
|
1093
1179
|
const before = db.size(r.head.relation);
|
|
1180
|
+
const readBefore = budget.examined;
|
|
1094
1181
|
deriveInto(r, seed, deltaIndex);
|
|
1095
1182
|
chargeRule(
|
|
1096
1183
|
ruleSetName,
|
|
1097
1184
|
r.head.relation,
|
|
1098
1185
|
r.body.map(literalName),
|
|
1099
|
-
performance.now() -
|
|
1100
|
-
db.size(r.head.relation) - before
|
|
1186
|
+
performance.now() - startedAt2,
|
|
1187
|
+
db.size(r.head.relation) - before,
|
|
1188
|
+
budget.examined - readBefore
|
|
1101
1189
|
);
|
|
1102
1190
|
};
|
|
1103
1191
|
const applyDelta = (seed, derivedOnly) => {
|
|
@@ -1142,7 +1230,24 @@ function runRules(db, rules, algebra) {
|
|
|
1142
1230
|
applyDelta(lastDelta, true);
|
|
1143
1231
|
chargeRound(ruleSetName);
|
|
1144
1232
|
}
|
|
1233
|
+
};
|
|
1234
|
+
const readHere = () => budget.examined - startedAt;
|
|
1235
|
+
try {
|
|
1236
|
+
for (const stratum of strata) {
|
|
1237
|
+
runStratum(stratum);
|
|
1238
|
+
}
|
|
1239
|
+
} catch (error) {
|
|
1240
|
+
if (error !== OUT_OF_BUDGET) {
|
|
1241
|
+
throw error;
|
|
1242
|
+
}
|
|
1243
|
+
for (const [relation, tuples] of byRelation(addedHere ?? [])) {
|
|
1244
|
+
db.retract(relation, tuples);
|
|
1245
|
+
}
|
|
1246
|
+
state.marks = null;
|
|
1247
|
+
chargeEvaluationRows(readHere());
|
|
1248
|
+
throw new BudgetExhausted(ruleSetName, readHere());
|
|
1145
1249
|
}
|
|
1250
|
+
chargeEvaluationRows(readHere());
|
|
1146
1251
|
state.marks = currentMarks(db);
|
|
1147
1252
|
if (isProfiling()) {
|
|
1148
1253
|
chargeRelationSizes(
|
|
@@ -1152,6 +1257,13 @@ function runRules(db, rules, algebra) {
|
|
|
1152
1257
|
}
|
|
1153
1258
|
return db;
|
|
1154
1259
|
}
|
|
1260
|
+
function byRelation(added) {
|
|
1261
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
1262
|
+
for (const [relation, tuple] of added) {
|
|
1263
|
+
addTo(buckets, relation, tuple);
|
|
1264
|
+
}
|
|
1265
|
+
return buckets;
|
|
1266
|
+
}
|
|
1155
1267
|
function addTo(buckets, relation, tuple) {
|
|
1156
1268
|
const bucket = buckets.get(relation);
|
|
1157
1269
|
if (bucket === void 0) {
|
|
@@ -1161,8 +1273,11 @@ function addTo(buckets, relation, tuple) {
|
|
|
1161
1273
|
}
|
|
1162
1274
|
}
|
|
1163
1275
|
export {
|
|
1276
|
+
BudgetExhausted,
|
|
1164
1277
|
Database,
|
|
1165
1278
|
Witness,
|
|
1279
|
+
chargeAbandoned,
|
|
1280
|
+
chargeQuestion,
|
|
1166
1281
|
clearRelations,
|
|
1167
1282
|
confidence,
|
|
1168
1283
|
confidenceWith,
|
|
@@ -1175,6 +1290,7 @@ export {
|
|
|
1175
1290
|
profileEvaluation,
|
|
1176
1291
|
profileEvaluationAsync,
|
|
1177
1292
|
proofOf,
|
|
1293
|
+
rowBudget,
|
|
1178
1294
|
rule,
|
|
1179
1295
|
ruleLabel,
|
|
1180
1296
|
stratify,
|
package/package.json
CHANGED