@suss/datalog 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -0
- package/dist/index.cjs +384 -16
- package/dist/index.d.cts +133 -1
- package/dist/index.d.ts +133 -1
- package/dist/index.js +377 -16
- package/package.json +1 -1
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
|
|
47
|
-
var
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
16
|
-
var
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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