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