eyeprolog 1.3.3 → 1.3.7

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.
Files changed (38) hide show
  1. package/README.md +78 -4
  2. package/examples/book/README.md +2 -1
  3. package/examples/book/chapter-07/02-move.pl +4 -0
  4. package/examples/book/chapter-08/01-findall.pl +1 -0
  5. package/examples/book/chapter-39/04-cost.pl +1 -0
  6. package/examples/chart-parser.pl +1 -0
  7. package/examples/dog.pl +1 -0
  8. package/examples/donald-gerald-robert.pl +1 -0
  9. package/examples/equality-saturation.pl +1 -0
  10. package/examples/job-shop-scheduling.pl +1 -0
  11. package/examples/knapsack-optimization.pl +1 -0
  12. package/examples/knuth-bendix-completion.pl +1 -0
  13. package/examples/matrix-chain-order.pl +1 -0
  14. package/examples/missionaries-cannibals.pl +1 -0
  15. package/examples/prime-range.pl +1 -0
  16. package/examples/register-allocation.pl +1 -0
  17. package/examples/send-more-money.pl +1 -0
  18. package/examples/stable-marriage.pl +1 -0
  19. package/examples/totient-summatory.pl +1 -0
  20. package/examples/weighted-interval-scheduling.pl +1 -0
  21. package/index.d.ts +7 -0
  22. package/package.json +1 -1
  23. package/src/datalog.js +354 -0
  24. package/src/iso.js +148 -14
  25. package/src/lib/iso_ext.pl +4 -0
  26. package/src/lib/lists.pl +0 -3
  27. package/src/program.js +258 -20
  28. package/src/solver.js +890 -46
  29. package/src/standard-library.js +12 -7
  30. package/src/term.js +1 -1
  31. package/src/wfs.js +338 -0
  32. package/test/conformance/ISO-COMPLIANCE.md +2 -1
  33. package/test/conformance/README.md +4 -1
  34. package/test/run-conformance.mjs +1 -0
  35. package/test/run-regression.mjs +230 -3
  36. package/the-art-of-eyeprolog.md +95 -21
  37. package/why-eyeprolog.md +17 -0
  38. /package/examples/book/chapter-07/{02-all_tests_pass.pl → 03-all_tests_pass.pl} +0 -0
package/README.md CHANGED
@@ -86,6 +86,72 @@ error, and later quads still run. When a query has multiple indented answer
86
86
  descriptions, each description is checked and counted independently, so one
87
87
  failed expectation does not prevent the later ones from running.
88
88
 
89
+ ## Tabling and well-founded negation
90
+
91
+ In normal mode, EyeProlog automatically tables eligible positive recursive
92
+ predicates. For large finite, function-free Datalog dependency cones it may use
93
+ one shared relation-wide table, so an open query such as `tc(X, Y)` computes a
94
+ finite closure once and bound recursive calls can reuse indexed answers. The
95
+ choice of when to use relation-wide tabling is an implementation optimization,
96
+ not a language-level threshold or directive.
97
+
98
+ Recursive nonterminals reached through `phrase/2-3` use a dedicated table
99
+ scope keyed by the complete invocation rather than the caller's general memo.
100
+ When a new input is seen, recursive DCGs that static analysis proves consume a
101
+ list tail on every recursive step run directly without automatic tabling; this
102
+ avoids building and retaining a family of suffix tables that cannot help the
103
+ next distinct input. If the same `phrase/2-3` invocation repeats, normal tabling
104
+ is enabled again and its small completed table can be reused. The retained
105
+ phrase table is also bounded, with active fixed points never evicted. This
106
+ keeps both long-running filters such as `\+ phrase((..., Pattern), Sequence)`
107
+ and repeated same-input probes bounded without turning memory safety into a
108
+ per-call cache-eviction cost.
109
+
110
+ Recursion through negation is explicit. EyeProlog provides `tnot/1` for
111
+ well-founded negation over finite, range-restricted, function-free Datalog
112
+ components. Ordinary `\+/1` remains negation-as-failure and is not silently
113
+ reinterpreted as WFS.
114
+
115
+ ```prolog
116
+ move(1, 2).
117
+ move(2, 1).
118
+ win(X) :- move(X, Y), tnot(win(Y)).
119
+
120
+ %% goal: win(X)
121
+ ```
122
+
123
+ A cycle through `tnot/1` can leave atoms *undefined* rather than true or false.
124
+ EyeProlog exposes those as conditional successes so they can participate in
125
+ collectors such as `findall/3`; it does not currently expose a residual-program
126
+ API. Direct `tnot/1` calls must be ground. Variables used in an eligible WFS
127
+ rule are range-restricted by positive body literals before they are negated.
128
+
129
+ The normal-mode statistics interface includes `wfs_fixpoint_rounds` and
130
+ `wfs_undefined_answers`:
131
+
132
+ ```prolog
133
+ statistics(wfs_fixpoint_rounds, Rounds).
134
+ statistics(wfs_undefined_answers, UndefinedObservations).
135
+ ```
136
+
137
+ Both automatic tabling and `tnot/1` are EyeProlog extensions. Strict ISO mode
138
+ disables automatic tabling and does not provide `tnot/1`.
139
+
140
+ ## OpenRuleBench portable profile
141
+
142
+ The `openrulebench/` directory contains a deterministic four-engine adaptation
143
+ for EyeProlog, Trealla, Scryer, and SWI-Prolog. Its default `portable` profile
144
+ keeps the characteristic joins, recursive closures, and WFS cases while
145
+ avoiding benchmark sizes that require multi-gigabyte collectors on some
146
+ engines. Run EyeProlog's complete profile with:
147
+
148
+ ```sh
149
+ ./openrulebench/run-eyeprolog.sh
150
+ ```
151
+
152
+ The benchmark README records the expected answer counts. Timing values are
153
+ machine-dependent; use the same generated profile when comparing engines.
154
+
89
155
  ## Strict ISO/IEC 13211-1 core
90
156
 
91
157
  For portability and conformance work, run the Part 1 core with Technical
@@ -143,12 +209,20 @@ Trealla/Scryer organization for predicates such as `member/2`, `memberchk/2`,
143
209
  `length(Xs, N)` enumerates lists of increasing length together with `N = 0, 1,
144
210
  2, ...`.
145
211
 
212
+ `library(iso_ext)` is also accepted as a common interop module name.
213
+ EyeProlog exports `call_nth/2` there, so Scryer-style source can explicitly use
214
+ `:- use_module(library(iso_ext)).`; unqualified source may still autoload it.
215
+ The aligned `library(lists)` and `library(iso_ext)` exports are kept disjoint so
216
+ they can be imported together without an accidental import conflict. EyeProlog's
217
+ legacy `library(prologue)` remains a compatibility umbrella and should be
218
+ selectively imported when mixed with the aligned modules.
219
+
146
220
  Outside `--iso-strict`, an otherwise undefined unqualified call may autoload a
147
221
  predicate only when the interop profile has one canonical EyeProlog provider.
148
- For example, `member/2` autoloads from `library(lists)`, while `between/3` and
149
- `call_nth/2` can use EyeProlog's internal Prologue implementation without
150
- requiring portable source to name `library(prologue)`. Use `--no-autoload` to
151
- disable this convenience. Strict ISO mode never autoloads library predicates.
222
+ For example, `member/2` autoloads from `library(lists)`, `call_nth/2` from
223
+ `library(iso_ext)`, and `between/3` from EyeProlog's internal
224
+ `library(prologue)` implementation. Use `--no-autoload` to disable this
225
+ convenience. Strict ISO mode never autoloads library predicates.
152
226
 
153
227
  Use `-w` / `--warnings` to diagnose dependencies outside the interop profile,
154
228
  or `--portable` to make such diagnostics fail the run. This catches both
@@ -56,7 +56,8 @@ npm run generate
56
56
  ## Chapter 7: Failure, negation, and quantification
57
57
 
58
58
  - [01-allowed.pl](chapter-07/01-allowed.pl)
59
- - [02-all_tests_pass.pl](chapter-07/02-all_tests_pass.pl)
59
+ - [02-move.pl](chapter-07/02-move.pl)
60
+ - [03-all_tests_pass.pl](chapter-07/03-all_tests_pass.pl)
60
61
 
61
62
  ## Chapter 8: Collecting and choosing answers
62
63
 
@@ -0,0 +1,4 @@
1
+ % From The Art of EyeProlog, Chapter 7.
2
+ move(a, b).
3
+ move(b, a).
4
+ win(X) :- move(X, Y), tnot(win(Y)).
@@ -1,6 +1,7 @@
1
1
  % From The Art of EyeProlog, Chapter 8.
2
2
  :- use_module(library(aggregate)).
3
3
  :- use_module(library(lists)).
4
+ :- use_module(library(iso_ext)).
4
5
 
5
6
  findall(Template, Goal, List).
6
7
  countall(Goal, Count).
@@ -1,6 +1,7 @@
1
1
  % From The Art of EyeProlog, Chapter 39.
2
2
  :- use_module(library(aggregate)).
3
3
  :- use_module(library(lists)).
4
+ :- use_module(library(iso_ext)).
4
5
 
5
6
  cost(a, 8).
6
7
  cost(b, 3).
@@ -1,4 +1,5 @@
1
1
  :- use_module(library(lists)).
2
+ :- use_module(library(iso_ext)).
2
3
 
3
4
  % A tiny automatically tabled chart parser for a context-free grammar.
4
5
  %
package/examples/dog.pl CHANGED
@@ -1,4 +1,5 @@
1
1
  :- use_module(library(lists)).
2
+ :- use_module(library(iso_ext)).
2
3
 
3
4
  % Dog-license compliance rule adapted from Eyeling dog.n3.
4
5
  %
@@ -1,4 +1,5 @@
1
1
  :- use_module(library(lists)).
2
+ :- use_module(library(iso_ext)).
2
3
 
3
4
  % A pandigital cryptarithm: DONALD + GERALD = ROBERT.
4
5
  %
@@ -1,6 +1,7 @@
1
1
  :- use_module(library(aggregate)).
2
2
  :- use_module(library(prologue), [between/3]).
3
3
  :- use_module(library(lists)).
4
+ :- use_module(library(iso_ext)).
4
5
 
5
6
  % Bounded equality saturation over tiny arithmetic expression terms.
6
7
  %
@@ -1,6 +1,7 @@
1
1
  :- use_module(library(aggregate)).
2
2
  :- use_module(library(prologue), [between/3]).
3
3
  :- use_module(library(lists)).
4
+ :- use_module(library(iso_ext)).
4
5
 
5
6
  % Tiny job-shop scheduling benchmark.
6
7
  %
@@ -1,5 +1,6 @@
1
1
  :- use_module(library(aggregate)).
2
2
  :- use_module(library(lists)).
3
+ :- use_module(library(iso_ext)).
3
4
 
4
5
  % 0/1 knapsack optimization with aggregate_max/5.
5
6
  %
@@ -1,4 +1,5 @@
1
1
  :- use_module(library(lists)).
2
+ :- use_module(library(iso_ext)).
2
3
 
3
4
  % Bounded Knuth-Bendix-style completion for append/2 terms.
4
5
  %
@@ -1,6 +1,7 @@
1
1
  :- use_module(library(aggregate)).
2
2
  :- use_module(library(prologue), [between/3]).
3
3
  :- use_module(library(lists)).
4
+ :- use_module(library(iso_ext)).
4
5
 
5
6
  % Matrix-chain multiplication order by automatically tabled interval dynamic programming.
6
7
  %
@@ -1,5 +1,6 @@
1
1
  :- use_module(library(prologue), [between/3]).
2
2
  :- use_module(library(lists)).
3
+ :- use_module(library(iso_ext)).
3
4
 
4
5
  % Missionaries-and-cannibals river crossing as guarded state-space search.
5
6
  %
@@ -1,5 +1,6 @@
1
1
  :- use_module(library(prologue), [between/3]).
2
2
  :- use_module(library(lists)).
3
+ :- use_module(library(iso_ext)).
3
4
 
4
5
  % Prime ranges and Euler totient over finite integer domains.
5
6
  %
@@ -1,5 +1,6 @@
1
1
  :- use_module(library(aggregate)).
2
2
  :- use_module(library(lists)).
3
+ :- use_module(library(iso_ext)).
3
4
 
4
5
  % Register allocation as bounded graph coloring with spilling.
5
6
  %
@@ -1,4 +1,5 @@
1
1
  :- use_module(library(lists)).
2
+ :- use_module(library(iso_ext)).
2
3
 
3
4
  % Cryptarithm search for SEND + MORE = MONEY.
4
5
  %
@@ -1,4 +1,5 @@
1
1
  :- use_module(library(lists)).
2
+ :- use_module(library(iso_ext)).
2
3
 
3
4
  % Stable-marriage search with explicit blocking-pair detection.
4
5
  %
@@ -1,6 +1,7 @@
1
1
  :- use_module(library(aggregate)).
2
2
  :- use_module(library(prologue), [between/3]).
3
3
  :- use_module(library(lists)).
4
+ :- use_module(library(iso_ext)).
4
5
 
5
6
  % Euler totients and coprimality by automatically tabled Euclidean gcd.
6
7
  %
@@ -1,5 +1,6 @@
1
1
  :- use_module(library(aggregate)).
2
2
  :- use_module(library(lists)).
3
+ :- use_module(library(iso_ext)).
3
4
 
4
5
  % Weighted interval scheduling via automatically tabled dynamic programming.
5
6
  %
package/index.d.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  export interface EyePrologStats {
2
+ /** Alternating-fixed-point rounds used to build WFS models. */
3
+ wfs_fixpoint_rounds: number;
4
+ /** Undefined WFS answers observed while producing query results. */
5
+ wfs_undefined_answers: number;
2
6
  [key: string]: number;
3
7
  }
4
8
 
@@ -97,6 +101,9 @@ export interface EyePrologPredicateGroup {
97
101
  recursive: boolean;
98
102
  listTailRecursive: boolean;
99
103
  tableInputPositions: number[];
104
+ tableAllVariants: boolean;
105
+ /** True when the group is evaluated by EyeProlog's finite-Datalog WFS evaluator. */
106
+ wfsDatalog: boolean;
100
107
  negationStratum: number | null;
101
108
  }
102
109
 
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.3.3",
6
+ "version": "1.3.7",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/datalog.js ADDED
@@ -0,0 +1,354 @@
1
+ // Indexed semi-naive evaluator for finite, range-restricted positive Datalog.
2
+ //
3
+ // Program analysis marks only large function-free recursive dependency cones
4
+ // for this path. Rows are derived once and propagated through an agenda; each
5
+ // newly-added row fires only the rule occurrences that mention its predicate.
6
+ // This avoids replaying the whole recursive program on every table round.
7
+
8
+ import { ATOM, COMPOUND, VAR } from './term.js';
9
+ import { numberValueKey } from './number-value.js';
10
+
11
+ const EMPTY_ARRAY = Object.freeze([]);
12
+
13
+ function predicateKey(module, name, arity) {
14
+ return `${module ?? 'user'}:${name}/${arity}`;
15
+ }
16
+
17
+ const scalarKeyCache = new WeakMap();
18
+
19
+ function scalarKey(term) {
20
+ const cached = scalarKeyCache.get(term);
21
+ if (cached != null) return cached;
22
+ const key = term.type === 'number'
23
+ ? `number\u0000${numberValueKey(term.name)}`
24
+ : `${term.type}\u0000${term.name}`;
25
+ scalarKeyCache.set(term, key);
26
+ return key;
27
+ }
28
+
29
+ function sameScalar(left, right) {
30
+ return scalarKey(left) === scalarKey(right);
31
+ }
32
+
33
+ function tupleKey(tuple) {
34
+ if (tuple.length === 1) return scalarKey(tuple[0]);
35
+ return tuple.map(scalarKey).join('\u0001');
36
+ }
37
+
38
+ export class DatalogRelation {
39
+ constructor(arity) {
40
+ this.arity = arity;
41
+ this.rows = [];
42
+ this.keys = arity > 2 ? new Set() : null;
43
+ this.unaryKeys = arity === 1 ? new Set() : null;
44
+ this.binaryKeys = arity === 2 ? new Map() : null;
45
+ this.indexes = Array.from({ length: arity }, () => new Map());
46
+ }
47
+
48
+ containsKeys(parts) {
49
+ if (this.arity === 0) return this.rows.length !== 0;
50
+ if (this.arity === 1) return this.unaryKeys.has(parts[0]);
51
+ if (this.arity === 2) return this.binaryKeys.get(parts[0])?.has(parts[1]) === true;
52
+ return this.keys.has(parts.join('\u0001'));
53
+ }
54
+
55
+ rememberKeys(parts) {
56
+ if (this.arity === 0) return;
57
+ if (this.arity === 1) {
58
+ this.unaryKeys.add(parts[0]);
59
+ return;
60
+ }
61
+ if (this.arity === 2) {
62
+ let seconds = this.binaryKeys.get(parts[0]);
63
+ if (seconds == null) this.binaryKeys.set(parts[0], seconds = new Set());
64
+ seconds.add(parts[1]);
65
+ return;
66
+ }
67
+ this.keys.add(parts.join('\u0001'));
68
+ }
69
+
70
+ add(tuple) {
71
+ const parts = tuple.map(scalarKey);
72
+ if (this.containsKeys(parts)) return false;
73
+ this.rememberKeys(parts);
74
+ this.appendRow(tuple, parts);
75
+ return true;
76
+ }
77
+
78
+ addProjected(args, bindings) {
79
+ const tuple = new Array(args.length);
80
+ const parts = new Array(args.length);
81
+ for (let i = 0; i < args.length; i++) {
82
+ const value = args[i].type === VAR ? bindings.get(args[i].name) : args[i];
83
+ if (value == null || value.type === VAR) return null;
84
+ tuple[i] = value;
85
+ parts[i] = scalarKey(value);
86
+ }
87
+ if (this.containsKeys(parts)) return null;
88
+ this.rememberKeys(parts);
89
+ this.appendRow(tuple, parts);
90
+ return tuple;
91
+ }
92
+
93
+ appendRow(tuple, parts) {
94
+ const rowIndex = this.rows.length;
95
+ this.rows.push(tuple);
96
+ for (let i = 0; i < tuple.length; i++) {
97
+ let bucket = this.indexes[i].get(parts[i]);
98
+ if (bucket == null) this.indexes[i].set(parts[i], bucket = []);
99
+ bucket.push(rowIndex);
100
+ }
101
+ }
102
+
103
+ has(tuple) {
104
+ return this.containsKeys(tuple.map(scalarKey));
105
+ }
106
+
107
+ candidateIndexes(args, bindings) {
108
+ let selected = null;
109
+ for (let i = 0; i < args.length; i++) {
110
+ const value = resolvePatternTerm(args[i], bindings);
111
+ if (value == null) continue;
112
+ const bucket = this.indexes[i].get(scalarKey(value)) ?? EMPTY_ARRAY;
113
+ if (selected == null || bucket.length < selected.length) selected = bucket;
114
+ if (selected.length === 0) break;
115
+ }
116
+ return selected;
117
+ }
118
+ }
119
+
120
+ function directLiteral(goal, module) {
121
+ if (goal?.type !== COMPOUND && goal?.type !== ATOM) return null;
122
+ return {
123
+ key: predicateKey(goal.module ?? module, goal.name, goal.arity),
124
+ name: goal.name,
125
+ arity: goal.arity,
126
+ module: goal.module ?? module,
127
+ args: goal.args ?? EMPTY_ARRAY,
128
+ };
129
+ }
130
+
131
+ function dependencyCone(program, rootGroup) {
132
+ const groups = [];
133
+ const seen = new Set();
134
+ const stack = [rootGroup];
135
+ while (stack.length > 0) {
136
+ const group = stack.pop();
137
+ const key = predicateKey(group.module, group.name, group.arity);
138
+ if (seen.has(key)) continue;
139
+ seen.add(key);
140
+ groups.push(group);
141
+ for (const clause of group.clauses) {
142
+ for (const goal of clause.body) {
143
+ const literal = directLiteral(goal, group.module);
144
+ if (!literal) continue;
145
+ const target = program.findGroup(literal.name, literal.arity, literal.module);
146
+ if (target) stack.push(target);
147
+ }
148
+ }
149
+ }
150
+ return groups;
151
+ }
152
+
153
+ function compileProgram(program, rootGroup) {
154
+ const groups = dependencyCone(program, rootGroup);
155
+ const relations = new Map();
156
+ const groupByKey = new Map();
157
+ const rules = [];
158
+ const triggers = new Map();
159
+
160
+ for (const group of groups) {
161
+ const key = predicateKey(group.module, group.name, group.arity);
162
+ relations.set(key, new DatalogRelation(group.arity));
163
+ groupByKey.set(key, group);
164
+ }
165
+
166
+ for (const group of groups) {
167
+ const headKey = predicateKey(group.module, group.name, group.arity);
168
+ for (const clause of group.clauses) {
169
+ const headArgs = clause.head.args ?? EMPTY_ARRAY;
170
+ if (clause.body.length === 0) continue;
171
+ const body = clause.body.map((goal) => directLiteral(goal, group.module));
172
+ if (body.some((literal) => literal == null)) continue;
173
+ const rule = { headKey, headArgs, body };
174
+ rules.push(rule);
175
+ for (let index = 0; index < body.length; index++) {
176
+ const key = body[index].key;
177
+ let entries = triggers.get(key);
178
+ if (entries == null) triggers.set(key, entries = []);
179
+ entries.push({ rule, literalIndex: index });
180
+ }
181
+ }
182
+ }
183
+
184
+ return { groups, groupByKey, relations, rules, triggers };
185
+ }
186
+
187
+ function resolvePatternTerm(term, bindings) {
188
+ if (term.type === VAR) return bindings.get(term.name) ?? null;
189
+ return term;
190
+ }
191
+
192
+ function matchTupleMutable(args, tuple, bindings) {
193
+ const added = [];
194
+ for (let i = 0; i < args.length; i++) {
195
+ const pattern = args[i];
196
+ if (pattern.type === VAR) {
197
+ const current = bindings.get(pattern.name);
198
+ if (current != null) {
199
+ if (!sameScalar(current, tuple[i])) {
200
+ for (let j = added.length - 1; j >= 0; j--) bindings.delete(added[j]);
201
+ return null;
202
+ }
203
+ } else {
204
+ bindings.set(pattern.name, tuple[i]);
205
+ added.push(pattern.name);
206
+ }
207
+ continue;
208
+ }
209
+ if (!sameScalar(pattern, tuple[i])) {
210
+ for (let j = added.length - 1; j >= 0; j--) bindings.delete(added[j]);
211
+ return null;
212
+ }
213
+ }
214
+ return added;
215
+ }
216
+
217
+ function undoBindings(bindings, added) {
218
+ for (let i = added.length - 1; i >= 0; i--) bindings.delete(added[i]);
219
+ }
220
+
221
+ function estimateLiteral(literal, relation, bindings) {
222
+ const candidates = relation.candidateIndexes(literal.args, bindings);
223
+ return candidates == null ? relation.rows.length : candidates.length;
224
+ }
225
+
226
+ function forEachBinding(body, relations, callback, bindings, remaining) {
227
+ if (remaining.length === 0) {
228
+ callback(bindings);
229
+ return;
230
+ }
231
+
232
+ let bestPosition = 0;
233
+ let bestEstimate = Infinity;
234
+ for (let position = 0; position < remaining.length; position++) {
235
+ const literal = body[remaining[position]];
236
+ const relation = relations.get(literal.key);
237
+ const estimate = relation == null ? 0 : estimateLiteral(literal, relation, bindings);
238
+ if (estimate < bestEstimate) {
239
+ bestEstimate = estimate;
240
+ bestPosition = position;
241
+ if (estimate === 0) return;
242
+ }
243
+ }
244
+
245
+ const literalIndex = remaining[bestPosition];
246
+ const literal = body[literalIndex];
247
+ const relation = relations.get(literal.key);
248
+ if (!relation) return;
249
+ const candidates = relation.candidateIndexes(literal.args, bindings);
250
+ const nextRemaining = remaining.length === 1
251
+ ? EMPTY_ARRAY
252
+ : [...remaining.slice(0, bestPosition), ...remaining.slice(bestPosition + 1)];
253
+
254
+ if (candidates == null) {
255
+ for (let rowIndex = 0; rowIndex < relation.rows.length; rowIndex++) {
256
+ const added = matchTupleMutable(literal.args, relation.rows[rowIndex], bindings);
257
+ if (added) {
258
+ forEachBinding(body, relations, callback, bindings, nextRemaining);
259
+ undoBindings(bindings, added);
260
+ }
261
+ }
262
+ return;
263
+ }
264
+
265
+ for (const rowIndex of candidates) {
266
+ const added = matchTupleMutable(literal.args, relation.rows[rowIndex], bindings);
267
+ if (added) {
268
+ forEachBinding(body, relations, callback, bindings, nextRemaining);
269
+ undoBindings(bindings, added);
270
+ }
271
+ }
272
+ }
273
+
274
+ function instantiateTuple(args, bindings) {
275
+ const tuple = new Array(args.length);
276
+ for (let i = 0; i < args.length; i++) {
277
+ const arg = args[i];
278
+ const value = arg.type === VAR ? bindings.get(arg.name) : arg;
279
+ if (value == null || value.type === VAR) return null;
280
+ tuple[i] = value;
281
+ }
282
+ return tuple;
283
+ }
284
+
285
+ export function evaluatePositiveDatalog(program, rootGroup) {
286
+ const compiled = compileProgram(program, rootGroup);
287
+ const agenda = [];
288
+ let ruleFirings = 0;
289
+ let derivedFacts = 0;
290
+
291
+ const add = (key, tuple) => {
292
+ const relation = compiled.relations.get(key);
293
+ if (!relation || !relation.add(tuple)) return false;
294
+ agenda.push({ key, tuple });
295
+ derivedFacts++;
296
+ return true;
297
+ };
298
+
299
+ // Seed all EDB/source facts in the dependency cone. Source order is retained
300
+ // within each predicate relation, which keeps answer enumeration stable.
301
+ for (const group of compiled.groups) {
302
+ const key = predicateKey(group.module, group.name, group.arity);
303
+ for (const clause of group.clauses) {
304
+ if (clause.body.length !== 0) continue;
305
+ const tuple = clause.head.args ?? EMPTY_ARRAY;
306
+ if (tuple.every((term) => term.type !== VAR)) add(key, tuple);
307
+ }
308
+ }
309
+
310
+ for (let cursor = 0; cursor < agenda.length; cursor++) {
311
+ const event = agenda[cursor];
312
+ const triggerEntries = compiled.triggers.get(event.key) ?? EMPTY_ARRAY;
313
+ for (const { rule, literalIndex } of triggerEntries) {
314
+ const fixed = rule.body[literalIndex];
315
+ const bindings = new Map();
316
+ if (!matchTupleMutable(fixed.args, event.tuple, bindings)) continue;
317
+ const remaining = [];
318
+ for (let i = 0; i < rule.body.length; i++) if (i !== literalIndex) remaining.push(i);
319
+ ruleFirings++;
320
+ forEachBinding(rule.body, compiled.relations, (completeBindings) => {
321
+ const relation = compiled.relations.get(rule.headKey);
322
+ const tuple = relation?.addProjected(rule.headArgs, completeBindings) ?? null;
323
+ if (tuple != null) {
324
+ agenda.push({ key: rule.headKey, tuple });
325
+ derivedFacts++;
326
+ }
327
+ }, bindings, remaining);
328
+ }
329
+ }
330
+
331
+ return {
332
+ relations: compiled.relations,
333
+ groups: compiled.groups,
334
+ ruleFirings,
335
+ derivedFacts,
336
+ };
337
+ }
338
+
339
+ export function relationForDatalogGroup(model, group) {
340
+ return model.relations.get(predicateKey(group.module, group.name, group.arity)) ?? null;
341
+ }
342
+
343
+ export function datalogCandidateIndexes(relation, goalArgs, env, derefValue, scalarKeyForTerm) {
344
+ let selected = null;
345
+ for (let i = 0; i < goalArgs.length; i++) {
346
+ const value = derefValue(goalArgs[i], env);
347
+ if (value?.type !== 'atom' && value?.type !== 'string' && value?.type !== 'number') continue;
348
+ const key = scalarKeyForTerm ? scalarKeyForTerm(value) : scalarKey(value);
349
+ const bucket = relation.indexes[i].get(key) ?? EMPTY_ARRAY;
350
+ if (selected == null || bucket.length < selected.length) selected = bucket;
351
+ if (selected.length === 0) break;
352
+ }
353
+ return selected;
354
+ }