@suss/datalog 0.37.1 → 0.38.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.d.cts CHANGED
@@ -7,10 +7,10 @@
7
7
  * The join asks for that key again on every candidate the rules
8
8
  * consider, so what the key costs to work out sets the cost of
9
9
  * evaluation. A relation finds a fact by walking a trie of its tuple's
10
- * atoms, one map hop per column, and the node the walk lands on is the
11
- * fact itself. Nothing is built to identify a tuple, and a membership
12
- * test that fails stops at the first column that misses. DESIGN.md says
13
- * why the walk is over the atoms rather than over interned integers.
10
+ * atoms, one map hop per column, to the first column where no other
11
+ * tuple shares the atoms so far. The key is stored there, and the walk
12
+ * compares the rest of the tuple instead of taking a map per column.
13
+ * DESIGN.md says why the walk is over the atoms, not interned integers.
14
14
  */
15
15
 
16
16
  /**
@@ -18,10 +18,11 @@
18
18
  * nothing to the relation that did not hand it out, and nothing outside
19
19
  * this module reads its fields.
20
20
  */
21
- interface FactKey {
21
+ declare class FactKey {
22
+ readonly tuple: Tuple;
22
23
  present: boolean;
23
24
  tag: unknown;
24
- next: Map<Atom, FactKey> | undefined;
25
+ constructor(tuple: Tuple);
25
26
  }
26
27
  /** The facts one relation has, and the tag stored against each. */
27
28
  declare class FactIndex {
@@ -40,15 +41,117 @@ declare class FactIndex {
40
41
  /** Take the fact away, and the tag with it. */
41
42
  remove(key: FactKey): void;
42
43
  /**
43
- * Drop what is left of a removed tuple's path, up to the first node
44
- * another fact still needs. Without this a relation that is retracted
45
- * from over and over keeps a node per tuple it ever had.
44
+ * Drop a removed tuple's key, and any branch left with one key to
45
+ * tell apart. Without this a relation that is retracted from over and
46
+ * over keeps a key per tuple it ever had.
46
47
  */
47
48
  prune(tuple: Tuple): void;
48
49
  /** Drop every fact, and whatever was kept in order to find them. */
49
50
  clear(): void;
50
51
  }
51
52
 
53
+ /**
54
+ * What an evaluation looked up, so a caller that cached its answer can
55
+ * tell later whether a fact that arrived since could change it.
56
+ *
57
+ * While `noteReads` runs, every lookup the join makes is noted as the
58
+ * relation, the column and the value, and a relation read whole is
59
+ * noted whole. At the end each lookup's facts are counted. The cached
60
+ * answer is still right when every count is the same and nothing was
61
+ * retracted from those relations. Without a retraction counts only
62
+ * grow, so the sum of the counts changes exactly when one of them does.
63
+ * DESIGN.md explains why a fact no lookup would find cannot change the
64
+ * answer, and what that asks of the rules.
65
+ */
66
+
67
+ /** @internal One relation's reads: the values looked up per column, unless something read it whole. */
68
+ interface RelationReads {
69
+ whole: boolean;
70
+ columns: (Set<Atom> | undefined)[];
71
+ }
72
+ /** @internal The join notes its reads here while `noteReads` runs. */
73
+ declare class ReadLog {
74
+ private readonly skipped;
75
+ /** Null for a relation the caller said to leave out. */
76
+ private readonly byRelation;
77
+ constructor(skipped: ReadonlySet<string>);
78
+ lookedUp(relation: string, column: number, value: Atom): void;
79
+ readWhole(relation: string): void;
80
+ /** Note everything an inner log noted, for an answer cached inside another. */
81
+ absorb(inner: ReadLog): void;
82
+ /**
83
+ * The reads with what each one finds in `db` now. One question looks
84
+ * up the same values in many columns, so each value is kept once, and
85
+ * each column keeps a bit per value saying whether it was looked up
86
+ * there.
87
+ */
88
+ counted(db: Database): ReadsNoted;
89
+ /** @internal Null for a relation the caller said to leave out. */
90
+ readsOf(relation: string): RelationReads | null;
91
+ }
92
+ /**
93
+ * Group `g` is the column `columns[g]` of the relation at
94
+ * `names[groups[g]]`. Its bits start at word `g` times the words one
95
+ * group takes, and bit `i` says it looked up `values[i]`.
96
+ */
97
+ interface Lookups {
98
+ names: readonly string[];
99
+ groups: readonly number[];
100
+ columns: readonly number[];
101
+ values: readonly Atom[];
102
+ bits: Uint32Array;
103
+ }
104
+ /**
105
+ * The lookups one evaluation made, and how many facts they found when
106
+ * it finished. The values are the atoms the facts already contain, so
107
+ * a value costs a reference, and each column it was looked up in costs
108
+ * a bit.
109
+ */
110
+ declare class ReadsNoted {
111
+ private readonly skipped;
112
+ private readonly found;
113
+ private readonly names;
114
+ private readonly groups;
115
+ private readonly columns;
116
+ private readonly values;
117
+ private readonly bits;
118
+ private readonly words;
119
+ private readonly removed;
120
+ /** The facts in every relation read, when the counts last matched. */
121
+ private facts;
122
+ /** Each relation read, found by name the first time a check needs it. */
123
+ private readonly relations;
124
+ constructor(db: Database, skipped: ReadonlySet<string>, lookups: Lookups, found: number);
125
+ /**
126
+ * Whether every read would find what it found then. A fact left in a
127
+ * skipped relation would join a question asked now without anything
128
+ * here having looked it up, so one there makes the answer no. The
129
+ * counts are taken again only when a relation read has grown.
130
+ */
131
+ unchangedIn(db: Database): boolean;
132
+ private relationAt;
133
+ private sumOver;
134
+ /**
135
+ * The facts the reads find, stopping once past `limit`. With nothing
136
+ * retracted every count only grows, so passing the old total already
137
+ * says that one of them grew.
138
+ */
139
+ private countIn;
140
+ private countGroup;
141
+ }
142
+ /**
143
+ * Run `work` and note what the join and the database's readers look up
144
+ * in every relation outside `skipped`. A caller skips the
145
+ * relations it empties after every question, so that the work derives
146
+ * their facts again and the lookups behind them get noted. The reads
147
+ * are null when a skipped relation was not empty to begin with, since
148
+ * the work could then build on facts nothing here looked up.
149
+ */
150
+ declare function noteReads<T>(db: Database, skipped: ReadonlySet<string>, work: () => T): {
151
+ value: T;
152
+ reads: ReadsNoted | null;
153
+ };
154
+
52
155
  /**
53
156
  * One relation's facts: which tuples are there, and an index per column
54
157
  * so a join that has a value for a column does not scan them all.
@@ -70,13 +173,15 @@ interface Relation {
70
173
  * encoding it first would build a string out of every node id on every
71
174
  * lookup and buy nothing.
72
175
  */
73
- columns: (Map<Atom, Tuple[]> | undefined)[];
176
+ columns: (Map<Atom, Bucket> | undefined)[];
74
177
  /**
75
178
  * Per set of columns, the tuples under each combination of values at
76
179
  * those columns, keyed by a bitmask of the column positions. Built and
77
180
  * kept up to date the same way as `columns`.
78
181
  */
79
182
  combined: Map<number, CombinedIndex>;
183
+ /** How many facts have ever been retracted, which `ReadsNoted` checks before trusting a count. */
184
+ removed: number;
80
185
  }
81
186
  /**
82
187
  * A tree of maps with one level per column in `columns`, lowest position
@@ -86,6 +191,13 @@ interface CombinedIndex {
86
191
  columns: readonly number[];
87
192
  root: Map<Atom, unknown>;
88
193
  }
194
+ /**
195
+ * The tuples an index has under one value, in the order they arrived.
196
+ * Most values in a column of node ids belong to a single tuple, and an
197
+ * array for each of those cost more than the tuple itself, so a lone
198
+ * tuple is kept on its own.
199
+ */
200
+ type Bucket = Tuple | Tuple[];
89
201
 
90
202
  /**
91
203
  * Confidence levels as a tag algebra: how sure the run is of each
@@ -482,6 +594,8 @@ type AddOutcome = "added" | "improved" | "unchanged";
482
594
  /** A set of facts per relation, with O(1) membership. */
483
595
  declare class Database {
484
596
  private readonly store;
597
+ /** @internal Where reads are noted while `noteReads` runs. */
598
+ readLog: ReadLog | null;
485
599
  private relation;
486
600
  /**
487
601
  * The key this relation gives a tuple, whether or not the fact is
@@ -500,6 +614,8 @@ declare class Database {
500
614
  /** The tag stored for this fact, or undefined when there is none. */
501
615
  tagOf(relationName: string, tuple: Tuple): unknown;
502
616
  has(relationName: string, tuple: Tuple): boolean;
617
+ /** A fact matching the whole tuple agrees on its first column, so that lookup is enough to note. */
618
+ private noteTupleRead;
503
619
  facts(relationName: string): readonly Tuple[];
504
620
  /**
505
621
  * The facts with `value` at `column`. Once one of a literal's terms
@@ -663,4 +779,4 @@ declare function evaluate<Tag = never>(db: Database, rules: Rule[], algebra?: Ta
663
779
  */
664
780
  declare function clearRelations(db: Database, rules: Rule[], relations: readonly string[]): void;
665
781
 
666
- 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, rulesDeriving, stratify, tupleKey, tupleKeyParts, variable, witnesses };
782
+ 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, ReadsNoted, 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, noteReads, profileEvaluation, profileEvaluationAsync, proofOf, rowBudget, rule, ruleLabel, rulesDeriving, stratify, tupleKey, tupleKeyParts, variable, witnesses };
package/dist/index.d.ts CHANGED
@@ -7,10 +7,10 @@
7
7
  * The join asks for that key again on every candidate the rules
8
8
  * consider, so what the key costs to work out sets the cost of
9
9
  * evaluation. A relation finds a fact by walking a trie of its tuple's
10
- * atoms, one map hop per column, and the node the walk lands on is the
11
- * fact itself. Nothing is built to identify a tuple, and a membership
12
- * test that fails stops at the first column that misses. DESIGN.md says
13
- * why the walk is over the atoms rather than over interned integers.
10
+ * atoms, one map hop per column, to the first column where no other
11
+ * tuple shares the atoms so far. The key is stored there, and the walk
12
+ * compares the rest of the tuple instead of taking a map per column.
13
+ * DESIGN.md says why the walk is over the atoms, not interned integers.
14
14
  */
15
15
 
16
16
  /**
@@ -18,10 +18,11 @@
18
18
  * nothing to the relation that did not hand it out, and nothing outside
19
19
  * this module reads its fields.
20
20
  */
21
- interface FactKey {
21
+ declare class FactKey {
22
+ readonly tuple: Tuple;
22
23
  present: boolean;
23
24
  tag: unknown;
24
- next: Map<Atom, FactKey> | undefined;
25
+ constructor(tuple: Tuple);
25
26
  }
26
27
  /** The facts one relation has, and the tag stored against each. */
27
28
  declare class FactIndex {
@@ -40,15 +41,117 @@ declare class FactIndex {
40
41
  /** Take the fact away, and the tag with it. */
41
42
  remove(key: FactKey): void;
42
43
  /**
43
- * Drop what is left of a removed tuple's path, up to the first node
44
- * another fact still needs. Without this a relation that is retracted
45
- * from over and over keeps a node per tuple it ever had.
44
+ * Drop a removed tuple's key, and any branch left with one key to
45
+ * tell apart. Without this a relation that is retracted from over and
46
+ * over keeps a key per tuple it ever had.
46
47
  */
47
48
  prune(tuple: Tuple): void;
48
49
  /** Drop every fact, and whatever was kept in order to find them. */
49
50
  clear(): void;
50
51
  }
51
52
 
53
+ /**
54
+ * What an evaluation looked up, so a caller that cached its answer can
55
+ * tell later whether a fact that arrived since could change it.
56
+ *
57
+ * While `noteReads` runs, every lookup the join makes is noted as the
58
+ * relation, the column and the value, and a relation read whole is
59
+ * noted whole. At the end each lookup's facts are counted. The cached
60
+ * answer is still right when every count is the same and nothing was
61
+ * retracted from those relations. Without a retraction counts only
62
+ * grow, so the sum of the counts changes exactly when one of them does.
63
+ * DESIGN.md explains why a fact no lookup would find cannot change the
64
+ * answer, and what that asks of the rules.
65
+ */
66
+
67
+ /** @internal One relation's reads: the values looked up per column, unless something read it whole. */
68
+ interface RelationReads {
69
+ whole: boolean;
70
+ columns: (Set<Atom> | undefined)[];
71
+ }
72
+ /** @internal The join notes its reads here while `noteReads` runs. */
73
+ declare class ReadLog {
74
+ private readonly skipped;
75
+ /** Null for a relation the caller said to leave out. */
76
+ private readonly byRelation;
77
+ constructor(skipped: ReadonlySet<string>);
78
+ lookedUp(relation: string, column: number, value: Atom): void;
79
+ readWhole(relation: string): void;
80
+ /** Note everything an inner log noted, for an answer cached inside another. */
81
+ absorb(inner: ReadLog): void;
82
+ /**
83
+ * The reads with what each one finds in `db` now. One question looks
84
+ * up the same values in many columns, so each value is kept once, and
85
+ * each column keeps a bit per value saying whether it was looked up
86
+ * there.
87
+ */
88
+ counted(db: Database): ReadsNoted;
89
+ /** @internal Null for a relation the caller said to leave out. */
90
+ readsOf(relation: string): RelationReads | null;
91
+ }
92
+ /**
93
+ * Group `g` is the column `columns[g]` of the relation at
94
+ * `names[groups[g]]`. Its bits start at word `g` times the words one
95
+ * group takes, and bit `i` says it looked up `values[i]`.
96
+ */
97
+ interface Lookups {
98
+ names: readonly string[];
99
+ groups: readonly number[];
100
+ columns: readonly number[];
101
+ values: readonly Atom[];
102
+ bits: Uint32Array;
103
+ }
104
+ /**
105
+ * The lookups one evaluation made, and how many facts they found when
106
+ * it finished. The values are the atoms the facts already contain, so
107
+ * a value costs a reference, and each column it was looked up in costs
108
+ * a bit.
109
+ */
110
+ declare class ReadsNoted {
111
+ private readonly skipped;
112
+ private readonly found;
113
+ private readonly names;
114
+ private readonly groups;
115
+ private readonly columns;
116
+ private readonly values;
117
+ private readonly bits;
118
+ private readonly words;
119
+ private readonly removed;
120
+ /** The facts in every relation read, when the counts last matched. */
121
+ private facts;
122
+ /** Each relation read, found by name the first time a check needs it. */
123
+ private readonly relations;
124
+ constructor(db: Database, skipped: ReadonlySet<string>, lookups: Lookups, found: number);
125
+ /**
126
+ * Whether every read would find what it found then. A fact left in a
127
+ * skipped relation would join a question asked now without anything
128
+ * here having looked it up, so one there makes the answer no. The
129
+ * counts are taken again only when a relation read has grown.
130
+ */
131
+ unchangedIn(db: Database): boolean;
132
+ private relationAt;
133
+ private sumOver;
134
+ /**
135
+ * The facts the reads find, stopping once past `limit`. With nothing
136
+ * retracted every count only grows, so passing the old total already
137
+ * says that one of them grew.
138
+ */
139
+ private countIn;
140
+ private countGroup;
141
+ }
142
+ /**
143
+ * Run `work` and note what the join and the database's readers look up
144
+ * in every relation outside `skipped`. A caller skips the
145
+ * relations it empties after every question, so that the work derives
146
+ * their facts again and the lookups behind them get noted. The reads
147
+ * are null when a skipped relation was not empty to begin with, since
148
+ * the work could then build on facts nothing here looked up.
149
+ */
150
+ declare function noteReads<T>(db: Database, skipped: ReadonlySet<string>, work: () => T): {
151
+ value: T;
152
+ reads: ReadsNoted | null;
153
+ };
154
+
52
155
  /**
53
156
  * One relation's facts: which tuples are there, and an index per column
54
157
  * so a join that has a value for a column does not scan them all.
@@ -70,13 +173,15 @@ interface Relation {
70
173
  * encoding it first would build a string out of every node id on every
71
174
  * lookup and buy nothing.
72
175
  */
73
- columns: (Map<Atom, Tuple[]> | undefined)[];
176
+ columns: (Map<Atom, Bucket> | undefined)[];
74
177
  /**
75
178
  * Per set of columns, the tuples under each combination of values at
76
179
  * those columns, keyed by a bitmask of the column positions. Built and
77
180
  * kept up to date the same way as `columns`.
78
181
  */
79
182
  combined: Map<number, CombinedIndex>;
183
+ /** How many facts have ever been retracted, which `ReadsNoted` checks before trusting a count. */
184
+ removed: number;
80
185
  }
81
186
  /**
82
187
  * A tree of maps with one level per column in `columns`, lowest position
@@ -86,6 +191,13 @@ interface CombinedIndex {
86
191
  columns: readonly number[];
87
192
  root: Map<Atom, unknown>;
88
193
  }
194
+ /**
195
+ * The tuples an index has under one value, in the order they arrived.
196
+ * Most values in a column of node ids belong to a single tuple, and an
197
+ * array for each of those cost more than the tuple itself, so a lone
198
+ * tuple is kept on its own.
199
+ */
200
+ type Bucket = Tuple | Tuple[];
89
201
 
90
202
  /**
91
203
  * Confidence levels as a tag algebra: how sure the run is of each
@@ -482,6 +594,8 @@ type AddOutcome = "added" | "improved" | "unchanged";
482
594
  /** A set of facts per relation, with O(1) membership. */
483
595
  declare class Database {
484
596
  private readonly store;
597
+ /** @internal Where reads are noted while `noteReads` runs. */
598
+ readLog: ReadLog | null;
485
599
  private relation;
486
600
  /**
487
601
  * The key this relation gives a tuple, whether or not the fact is
@@ -500,6 +614,8 @@ declare class Database {
500
614
  /** The tag stored for this fact, or undefined when there is none. */
501
615
  tagOf(relationName: string, tuple: Tuple): unknown;
502
616
  has(relationName: string, tuple: Tuple): boolean;
617
+ /** A fact matching the whole tuple agrees on its first column, so that lookup is enough to note. */
618
+ private noteTupleRead;
503
619
  facts(relationName: string): readonly Tuple[];
504
620
  /**
505
621
  * The facts with `value` at `column`. Once one of a literal's terms
@@ -663,4 +779,4 @@ declare function evaluate<Tag = never>(db: Database, rules: Rule[], algebra?: Ta
663
779
  */
664
780
  declare function clearRelations(db: Database, rules: Rule[], relations: readonly string[]): void;
665
781
 
666
- 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, rulesDeriving, stratify, tupleKey, tupleKeyParts, variable, witnesses };
782
+ 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, ReadsNoted, 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, noteReads, profileEvaluation, profileEvaluationAsync, proofOf, rowBudget, rule, ruleLabel, rulesDeriving, stratify, tupleKey, tupleKeyParts, variable, witnesses };