@josd/eyecode 0.0.0-stage → 0.1.4

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/src/engine.js ADDED
@@ -0,0 +1,867 @@
1
+ // Backward resolution, forward fixpoints and proof recording.
2
+ import { PRIMITIVE_KEYS, primitive } from './builtins.js';
3
+ import { freshClause, renaming, text, variant } from './common.js';
4
+ import { readGoal } from './goals.js';
5
+ import { checkRendered, clauseDisplay, renderProof } from './proof.js';
6
+ import { CONTROL_KEYS, Program } from './program.js';
7
+ import {
8
+ Env, EyecodeError, Struct, Var, conjunction, copyResolved, deref, flattenConjunction, freshTerm,
9
+ identity, isCallable, isGround, isGroundButAnonymous, isTerm, key, keyArity, listFromItems, unify,
10
+ variables,
11
+ } from './terms.js';
12
+ import { Lettering, write } from './writer.js';
13
+
14
+ export const BUILTIN = 'builtin';
15
+ export const CONTROL = 'control';
16
+ export const ABSENT = 'absent';
17
+ export const COLLECTED = 'collected';
18
+
19
+ // One proof step: a goal, how it was justified, and the steps it used.
20
+ export class Node {
21
+ constructor(goal, by, bindings = NO_NODES, children = NO_NODES, collectedUses = null, final = false) {
22
+ this.goal = goal;
23
+ // A derived fact's node is resolved once and for all when the fact is
24
+ // derived, so whatever uses the fact shares the node, derivation and all.
25
+ this.final = final;
26
+ this.by = by;
27
+ this.bindings = bindings;
28
+ this.children = children;
29
+ // A collection has no children in a proof, so its node keeps the clauses
30
+ // its answers used.
31
+ this.collectedUses = collectedUses;
32
+ }
33
+ }
34
+
35
+ // The step a frame's body will justify once it is finished.
36
+ class Pending {
37
+ constructor(goal, by, bindings, cut) {
38
+ this.goal = goal;
39
+ this.by = by;
40
+ this.bindings = bindings;
41
+ this.cut = cut;
42
+ }
43
+ }
44
+
45
+ // One body being worked through. Frames never change once made.
46
+ class Frame {
47
+ constructor(goals, index, nodes, parent, pending, depth) {
48
+ this.goals = goals;
49
+ this.index = index;
50
+ this.nodes = nodes;
51
+ this.parent = parent;
52
+ this.pending = pending;
53
+ this.depth = depth;
54
+ }
55
+ }
56
+
57
+ function bisectLeft(order, value) {
58
+ let low = 0;
59
+ let high = order.length;
60
+ while (low < high) {
61
+ const mid = (low + high) >> 1;
62
+ if (order[mid] < value) low = mid + 1;
63
+ else high = mid;
64
+ }
65
+ return low;
66
+ }
67
+
68
+ // A choice point: the frame it was made in and its untried alternatives: the
69
+ // derived facts from start to stop, then the source clauses. With an order,
70
+ // the facts are those at the indexes order[start:stop].
71
+ class Point {
72
+ constructor(kind, frame, mark, goal, { alternatives = NO_NODES, facts = NO_NODES, clauses = NO_NODES,
73
+ iterator = null, start = 0, stop = null, order = null } = {}) {
74
+ this.kind = kind;
75
+ this.frame = frame;
76
+ this.mark = mark;
77
+ this.goal = goal;
78
+ this.alternatives = alternatives;
79
+ this.facts = facts;
80
+ this.clauses = clauses;
81
+ this.position = 0;
82
+ this.iterator = iterator;
83
+ this.order = order;
84
+ if (order === null) {
85
+ this.start = start;
86
+ this.stop = stop === null ? facts.length : stop;
87
+ } else {
88
+ this.start = bisectLeft(order, start);
89
+ this.stop = stop === null ? order.length : bisectLeft(order, stop);
90
+ }
91
+ }
92
+
93
+ count() {
94
+ if (this.kind === 'branch') return this.alternatives.length;
95
+ return this.stop - this.start + this.clauses.length;
96
+ }
97
+ }
98
+
99
+ const NO_NODES = Object.freeze([]);
100
+ const QUIET = new Pending(null, null, NO_NODES, null);
101
+ const SPLICE = 'splice';
102
+
103
+ function resolveOne(node, env) {
104
+ return new Node(copyResolved(node.goal, env), node.by,
105
+ node.bindings.map(([name, value]) => [name, copyResolved(value, env)]),
106
+ new Array(node.children.length).fill(null), node.collectedUses);
107
+ }
108
+
109
+ // Resolve a proof forest with an explicit stack: a derivation chain is as deep
110
+ // as the search that produced it. A derived fact's node is already final and
111
+ // is shared rather than copied, which keeps a long forward chain from being
112
+ // copied again at every step.
113
+ export function resolveNode(node, env) {
114
+ if (node.final) return node;
115
+ const root = resolveOne(node, env);
116
+ const pending = [[node, root]];
117
+ while (pending.length) {
118
+ const [source, target] = pending.pop();
119
+ source.children.forEach((child, index) => {
120
+ if (child.final) {
121
+ target.children[index] = child;
122
+ return;
123
+ }
124
+ const copy = resolveOne(child, env);
125
+ target.children[index] = copy;
126
+ if (child.children.length) pending.push([child, copy]);
127
+ });
128
+ }
129
+ return root;
130
+ }
131
+
132
+ // The source clauses a set of proof nodes rests on.
133
+ export function clausesUsed(nodes, used = new Set()) {
134
+ const pending = [...nodes];
135
+ while (pending.length) {
136
+ const node = pending.pop();
137
+ if (node.by instanceof Struct) used.add(Number(node.by.args[0]));
138
+ if (node.collectedUses) for (const id of node.collectedUses) used.add(id);
139
+ for (const child of node.children) pending.push(child);
140
+ }
141
+ return used;
142
+ }
143
+
144
+ // The source clauses that searches behind a trusted boundary could consult.
145
+ // A negation or a collection records no derivation in a proof, so follow its
146
+ // goal through the program instead: every clause whose head could answer a
147
+ // goal it reaches, and the goals in that clause's body, transitively.
148
+ export function clausesBehindBoundaries(program, roots) {
149
+ const byKey = new Map();
150
+ for (const clause of program.clauses) {
151
+ for (const head of clause.heads || [clause.head]) {
152
+ if (isCallable(head)) {
153
+ const headKey = key(head);
154
+ if (!byKey.has(headKey)) byKey.set(headKey, []);
155
+ byKey.get(headKey).push(clause);
156
+ }
157
+ }
158
+ }
159
+ const pending = [];
160
+ function visit(goal) {
161
+ if (!isCallable(goal)) return;
162
+ if (isTerm(goal, 'and', 2) || isTerm(goal, 'or', 2)) {
163
+ visit(goal.args[0]);
164
+ visit(goal.args[1]);
165
+ } else if (isTerm(goal, 'not', 1) || isTerm(goal, 'call', 1) || isTerm(goal, 'once', 1)) {
166
+ visit(goal.args[0]);
167
+ } else if (isTerm(goal, 'findall', 3)) {
168
+ visit(goal.args[1]);
169
+ } else if (!PRIMITIVE_KEYS.has(key(goal))) {
170
+ pending.push(key(goal));
171
+ }
172
+ }
173
+ const nodes = [...roots];
174
+ while (nodes.length) {
175
+ const node = nodes.pop();
176
+ if (node.by === ABSENT || node.by === COLLECTED) visit(node.goal);
177
+ for (const child of node.children) nodes.push(child);
178
+ }
179
+ const seen = new Set();
180
+ const reached = new Set();
181
+ while (pending.length) {
182
+ const goalKey = pending.pop();
183
+ if (seen.has(goalKey)) continue;
184
+ seen.add(goalKey);
185
+ for (const clause of byKey.get(goalKey) || []) {
186
+ reached.add(clause.id);
187
+ for (const goal of clause.body) visit(goal);
188
+ }
189
+ }
190
+ return reached;
191
+ }
192
+
193
+ function compareChosen(a, b) {
194
+ for (let i = 0; i < Math.min(a.length, b.length); i++) {
195
+ const x = a[i];
196
+ const y = b[i];
197
+ if (x[0] !== y[0]) return x[0] < y[0] ? -1 : 1;
198
+ if (x[1] !== y[1]) return x[1] < y[1] ? -1 : 1;
199
+ }
200
+ return a.length - b.length;
201
+ }
202
+
203
+ const isIndexable = (value) => typeof value === 'string' || typeof value === 'bigint' || typeof value === 'number';
204
+
205
+ export class Solver {
206
+ constructor(program, options) {
207
+ this.program = program;
208
+ this.options = options;
209
+ this.recording = Boolean(options.proof);
210
+ this.serial = 0;
211
+ this.facts = new Map();
212
+ // Derived facts indexed by argument: for each name and arity, one map
213
+ // per argument position from an atom or number to the indexes, in order,
214
+ // of the facts with that value there.
215
+ this.factIndex = new Map();
216
+ this.factKeys = new Set(program.groundFactKeys);
217
+ this.derived = [];
218
+ this.reported = new Map();
219
+ this.stats = { inferences: 0, rounds: 0, derived: 0 };
220
+ this.haltCode = null;
221
+ this.maxDepth = options.maxDepth || 1000000;
222
+ // Semi-naive forward reasoning: while a rule's body is searched with a
223
+ // window, each of its goals sees only the derived facts in its window,
224
+ // and the alternative each goal took is noted in chosen.
225
+ this.window = null;
226
+ this.windowGoals = null;
227
+ this.chosen = null;
228
+ this.snapshots = new Map();
229
+ this.direct = new Map();
230
+ this.maxInferences = options.maxInferences || 1000000;
231
+ }
232
+
233
+ // Proof nodes are recorded only when a proof is asked for. Without one,
234
+ // frames share an empty array and a finished body has nothing to hand back.
235
+ advance(frame, node) {
236
+ return new Frame(frame.goals, frame.index + 1,
237
+ this.recording ? [...frame.nodes, node] : NO_NODES,
238
+ frame.parent, frame.pending, frame.depth);
239
+ }
240
+
241
+ childFrame(parent, goals, pending, depth) {
242
+ // A last goal can return straight to its caller when there is no proof
243
+ // step or control cut to finish. Choice points still retain their own
244
+ // frames, so alternatives backtrack normally.
245
+ if (!this.recording && (pending === QUIET || pending === SPLICE) && parent.index + 1 === parent.goals.length) {
246
+ return new Frame(goals, 0, NO_NODES, parent.parent, parent.pending, depth);
247
+ }
248
+ return new Frame(goals, 0, this.recording ? [] : NO_NODES, parent, pending, depth);
249
+ }
250
+
251
+ controlPending(goal, cut) {
252
+ if (this.recording || cut !== null) return new Pending(goal, CONTROL, NO_NODES, cut);
253
+ return QUIET;
254
+ }
255
+
256
+ // A finished body hands its nodes to the frame that started it: a
257
+ // conjunction splices them in place, anything else wraps them as one step's
258
+ // children.
259
+ closeFrame(frame) {
260
+ const parent = frame.parent;
261
+ if (!this.recording) {
262
+ return new Frame(parent.goals, parent.index + 1, NO_NODES, parent.parent, parent.pending, parent.depth);
263
+ }
264
+ let nodes;
265
+ if (frame.pending === SPLICE) {
266
+ nodes = [...parent.nodes, ...frame.nodes];
267
+ } else {
268
+ const pending = frame.pending;
269
+ nodes = [...parent.nodes, new Node(pending.goal, pending.by, pending.bindings, [...frame.nodes])];
270
+ }
271
+ return new Frame(parent.goals, parent.index + 1, nodes, parent.parent, parent.pending, parent.depth);
272
+ }
273
+
274
+ // Backward search as an explicit machine rather than nested generators. A
275
+ // frame is one body being worked through; frames are immutable, so a choice
276
+ // point only has to remember the frame it was made in and backtracking is a
277
+ // pointer assignment rather than an undo log. One substitution is threaded
278
+ // through the whole search and restored by the trail, so an answer's
279
+ // bindings are only valid until the next one is requested: every consumer
280
+ // copies what it needs first.
281
+ *solve(goals, env = new Env(), depth = 0) {
282
+ let frame = new Frame(goals, 0, this.recording ? [] : NO_NODES, null, null, depth);
283
+ const choices = [];
284
+ const entry = env.mark();
285
+ let failed = false;
286
+ const stats = this.stats;
287
+ try {
288
+ while (true) {
289
+ if (failed) {
290
+ if (!choices.length) return;
291
+ const point = choices[choices.length - 1];
292
+ env.undo(point.mark);
293
+ const resumed = this.retry(point, env);
294
+ if (resumed === null) {
295
+ choices.pop();
296
+ continue;
297
+ }
298
+ frame = resumed;
299
+ failed = false;
300
+ continue;
301
+ }
302
+ if (frame.index >= frame.goals.length) {
303
+ if (frame.parent === null) {
304
+ yield [env, frame.nodes];
305
+ failed = true;
306
+ continue;
307
+ }
308
+ // once/1 commits to its first solution by discarding the choice
309
+ // points its own goal created.
310
+ const cut = frame.pending !== SPLICE ? frame.pending.cut : null;
311
+ if (cut !== null && choices.length > cut) choices.length = cut;
312
+ frame = this.closeFrame(frame);
313
+ continue;
314
+ }
315
+ if (frame.depth > this.maxDepth) throw new EyecodeError('backward reasoning exceeded max_depth');
316
+ stats.inferences++;
317
+ if (stats.inferences > this.maxInferences) throw new EyecodeError('reasoning exceeded max_inferences');
318
+ const goal = deref(frame.goals[frame.index], env);
319
+ if (!isCallable(goal)) throw new EyecodeError(`expected a callable goal, got ${text(goal)}`);
320
+ const next = this.step(goal, frame, env, choices);
321
+ if (next === null) {
322
+ failed = true;
323
+ continue;
324
+ }
325
+ frame = next;
326
+ }
327
+ } finally {
328
+ env.undo(entry);
329
+ }
330
+ }
331
+
332
+ // Start one goal: the frame to continue from, or null when it has no solution.
333
+ step(goal, frame, env, choices) {
334
+ const recording = this.recording;
335
+ const goalKey = key(goal);
336
+ const windowed = this.window !== null && frame.goals === this.windowGoals;
337
+ if (windowed) this.chosen[frame.index] = [0, 0];
338
+ if (goalKey === 'and/2') return this.childFrame(frame, flattenConjunction(goal), SPLICE, frame.depth);
339
+ if (goalKey === 'not/1') {
340
+ // Negation is a test, not a way to bind its variables; its anonymous
341
+ // variables stand for any value.
342
+ if (!isGroundButAnonymous(goal.args[0], env)) {
343
+ throw new EyecodeError('negation requires a goal whose variables are bound, ' +
344
+ `except for anonymous ones: ${text(goal, env)}`);
345
+ }
346
+ const mark = env.mark();
347
+ const iterator = this.solve([goal.args[0]], env, frame.depth + 1);
348
+ let absent;
349
+ try {
350
+ absent = iterator.next().done;
351
+ } finally {
352
+ iterator.return();
353
+ env.undo(mark);
354
+ }
355
+ if (!absent) return null;
356
+ return this.advance(frame, recording ? new Node(goal, ABSENT) : null);
357
+ }
358
+ if (goalKey === 'findall/3') {
359
+ const items = [];
360
+ const uses = recording ? new Set() : null;
361
+ const mark = env.mark();
362
+ for (const [answerEnv, nodes] of this.solve([goal.args[1]], env, frame.depth + 1)) {
363
+ this.serial++;
364
+ items.push(freshTerm(copyResolved(goal.args[0], answerEnv), `collection${this.serial}`));
365
+ if (recording) clausesUsed(nodes, uses);
366
+ }
367
+ env.undo(mark);
368
+ if (!unify(goal.args[2], listFromItems(items), env)) {
369
+ env.undo(mark);
370
+ return null;
371
+ }
372
+ if (!recording) return this.advance(frame, null);
373
+ return this.advance(frame, new Node(goal, COLLECTED, NO_NODES, NO_NODES, uses));
374
+ }
375
+ let point;
376
+ if (goalKey === 'or/2') {
377
+ point = new Point('branch', frame, env.mark(), goal, { alternatives: goal.args });
378
+ } else if (goalKey === 'call/1' || goalKey === 'once/1') {
379
+ const cut = goalKey === 'once/1' ? choices.length : null;
380
+ return this.childFrame(frame, [goal.args[0]], this.controlPending(goal, cut), frame.depth + 1);
381
+ } else if (PRIMITIVE_KEYS.has(goalKey)) {
382
+ point = new Point('primitive', frame, env.mark(), goal, { iterator: primitive(goal, env) });
383
+ } else if (windowed) {
384
+ const [start, stop, withClauses] = this.window[frame.index];
385
+ point = new Point('resolve', frame, env.mark(), goal, {
386
+ facts: this.facts.get(goalKey) || NO_NODES,
387
+ clauses: withClauses ? this.program.candidates(goal, env, goalKey) : NO_NODES,
388
+ start, stop, order: this.factOrder(goal, env, goalKey),
389
+ });
390
+ } else {
391
+ // Derived facts are tried before source clauses. Neither list changes
392
+ // while a search runs.
393
+ point = new Point('resolve', frame, env.mark(), goal, {
394
+ facts: this.facts.get(goalKey) || NO_NODES,
395
+ clauses: this.program.candidates(goal, env, goalKey),
396
+ order: this.factOrder(goal, env, goalKey),
397
+ });
398
+ }
399
+ const next = this.retry(point, env);
400
+ if (next === null) return null;
401
+ if (point.kind === 'primitive' || point.position < point.count()) choices.push(point);
402
+ return next;
403
+ }
404
+
405
+ addFact(goalKey, node) {
406
+ let facts = this.facts.get(goalKey);
407
+ if (facts === undefined) {
408
+ facts = [];
409
+ this.facts.set(goalKey, facts);
410
+ }
411
+ const number = facts.length;
412
+ facts.push(node);
413
+ const arity = keyArity(goalKey);
414
+ if (arity) {
415
+ let positions = this.factIndex.get(goalKey);
416
+ if (positions === undefined) {
417
+ positions = Array.from({ length: arity }, () => new Map());
418
+ this.factIndex.set(goalKey, positions);
419
+ }
420
+ node.goal.args.forEach((arg, position) => {
421
+ if (isIndexable(arg)) {
422
+ const bucket = positions[position].get(arg);
423
+ if (bucket === undefined) positions[position].set(arg, [number]);
424
+ else bucket.push(number);
425
+ }
426
+ });
427
+ }
428
+ }
429
+
430
+ // The indexes of the derived facts a goal could match, in order, when an
431
+ // argument of the goal narrows them down; null to try them all.
432
+ factOrder(goal, env, goalKey) {
433
+ const positions = this.factIndex.get(goalKey);
434
+ if (positions === undefined) return null;
435
+ let best = null;
436
+ let size = this.facts.get(goalKey).length;
437
+ for (let position = 0; position < goal.args.length; position++) {
438
+ let arg = goal.args[position];
439
+ if (arg instanceof Var && env !== null) arg = deref(arg, env);
440
+ if (isIndexable(arg)) {
441
+ const bucket = positions[position].get(arg) || NO_NODES;
442
+ if (bucket.length < size) {
443
+ best = bucket;
444
+ size = bucket.length;
445
+ if (!size) break;
446
+ }
447
+ }
448
+ }
449
+ return best;
450
+ }
451
+
452
+ // Take the next untried alternative of a choice point, or null. The caller
453
+ // has already undone the bindings of the previous one.
454
+ retry(point, env) {
455
+ const recording = this.recording;
456
+ const windowed = this.window !== null && point.frame.goals === this.windowGoals;
457
+ if (point.kind === 'primitive') {
458
+ if (point.iterator.next().done) return null;
459
+ if (windowed) {
460
+ this.chosen[point.frame.index] = [0, point.position];
461
+ point.position++;
462
+ }
463
+ return this.advance(point.frame, recording ? new Node(point.goal, BUILTIN) : null);
464
+ }
465
+ const count = point.count();
466
+ while (point.position < count) {
467
+ const position = point.position;
468
+ point.position++;
469
+ if (point.kind === 'branch') {
470
+ return this.childFrame(point.frame, [point.alternatives[position]],
471
+ this.controlPending(point.goal, null), point.frame.depth + 1);
472
+ }
473
+ const inWindow = point.stop - point.start;
474
+ if (position < inWindow) {
475
+ let index = point.start + position;
476
+ if (point.order !== null) index = point.order[index];
477
+ const fact = point.facts[index];
478
+ if (unify(point.goal, fact.goal, env)) {
479
+ if (windowed) this.chosen[point.frame.index] = [0, index];
480
+ return this.advance(point.frame, fact);
481
+ }
482
+ env.undo(point.mark);
483
+ continue;
484
+ }
485
+ const clause = point.clauses[position - inWindow];
486
+ if (windowed) this.chosen[point.frame.index] = [1, clause.id];
487
+ this.serial++;
488
+ // The body is renamed only once the head unifies: many candidate
489
+ // clauses of a goal fail on their head.
490
+ const renamed = clause.renaming || renaming(clause);
491
+ if (renamed === null) {
492
+ const names = new Map();
493
+ if (!unify(point.goal, freshTerm(clause.head, this.serial, names), env)) {
494
+ env.undo(point.mark);
495
+ continue;
496
+ }
497
+ const body = clause.body.map((goal) => freshTerm(goal, this.serial, names));
498
+ const pending = recording
499
+ ? new Pending(point.goal, new Struct('clause', [BigInt(clause.id)]), [...names], null)
500
+ : QUIET;
501
+ return this.childFrame(point.frame, body, pending, point.frame.depth + 1);
502
+ }
503
+ const suffix = String(this.serial);
504
+ const prefixes = renamed.prefixes;
505
+ const values = [];
506
+ for (let i = 0; i < renamed.headCount; i++) values.push(new Var(prefixes[i] + suffix));
507
+ if (!unify(point.goal, renamed.head(values), env)) {
508
+ env.undo(point.mark);
509
+ continue;
510
+ }
511
+ for (let i = renamed.headCount; i < prefixes.length; i++) values.push(new Var(prefixes[i] + suffix));
512
+ const body = renamed.body(values);
513
+ const pending = recording
514
+ ? new Pending(point.goal, new Struct('clause', [BigInt(clause.id)]),
515
+ renamed.names.map((name, i) => [name, values[i]]), null)
516
+ : QUIET;
517
+ return this.childFrame(point.frame, body, pending, point.frame.depth + 1);
518
+ }
519
+ return null;
520
+ }
521
+
522
+ // For a rule semi-naive search can take, the key of each body goal answered
523
+ // by facts alone, or null for a primitive or a negation or collection; null
524
+ // for other rules. Negations and collections consult lower strata only
525
+ // (Section 4.5), so they do not change within one.
526
+ directKeys(clause) {
527
+ if (this.direct.has(clause.id)) return this.direct.get(clause.id);
528
+ let keys = [];
529
+ for (const goal of clause.body) {
530
+ if (!isCallable(goal)) {
531
+ keys = null;
532
+ break;
533
+ }
534
+ const goalKey = key(goal);
535
+ if (PRIMITIVE_KEYS.has(goalKey) || goalKey === 'not/1' || goalKey === 'findall/3') {
536
+ keys.push(null);
537
+ } else if (CONTROL_KEYS.has(goalKey) || (this.program.groups.get(goalKey) || []).some((item) => item.body.length)) {
538
+ keys = null;
539
+ break;
540
+ } else {
541
+ keys.push(goalKey);
542
+ }
543
+ }
544
+ this.direct.set(clause.id, keys);
545
+ return keys;
546
+ }
547
+
548
+ // The activations of a forward rule against the current state, in the
549
+ // order a full search of its body finds them. After a rule's first search,
550
+ // a rule whose body goals are answered by facts alone is searched
551
+ // semi-naively: only activations that use a fact derived since its last
552
+ // search can conclude anything new, so the body is searched once for each
553
+ // goal that has such facts, that goal limited to them and the goals before
554
+ // it to the older ones. The activations are then put back in the order a
555
+ // full search would find them.
556
+ activations(clause) {
557
+ const keys = this.directKeys(clause);
558
+ let now = null;
559
+ let previous = null;
560
+ if (keys !== null) {
561
+ now = new Map();
562
+ for (const goalKey of keys) if (goalKey) now.set(goalKey, (this.facts.get(goalKey) || NO_NODES).length);
563
+ previous = this.snapshots.get(clause.id) || null;
564
+ this.snapshots.set(clause.id, now);
565
+ }
566
+ let positions = null;
567
+ if (previous !== null) {
568
+ positions = [];
569
+ keys.forEach((goalKey, i) => {
570
+ if (goalKey && now.get(goalKey) > previous.get(goalKey)) positions.push(i);
571
+ });
572
+ if (!positions.some((i) => this.mayMatchNew(clause.body[i], keys[i], previous, now))) {
573
+ return []; // nothing derived since its last search can be used, so nothing new follows
574
+ }
575
+ }
576
+ this.serial++;
577
+ const [head, body, names] = freshClause(clause, this.serial);
578
+ if (previous === null) return this.scan(clause, head, body, names).map(([found]) => found);
579
+ const found = [];
580
+ for (const delta of positions) {
581
+ const window = keys.map((goalKey, i) => {
582
+ if (goalKey === null) return null;
583
+ if (i < delta) return [0, previous.get(goalKey), true];
584
+ if (i === delta) return [previous.get(goalKey), now.get(goalKey), false];
585
+ return [0, now.get(goalKey), true];
586
+ });
587
+ this.window = window;
588
+ this.windowGoals = body;
589
+ this.chosen = new Array(body.length).fill(null);
590
+ try {
591
+ found.push(...this.scan(clause, head, body, names));
592
+ } finally {
593
+ this.window = this.windowGoals = this.chosen = null;
594
+ }
595
+ }
596
+ found.sort((a, b) => compareChosen(a[1], b[1]));
597
+ return found.map(([activation]) => activation);
598
+ }
599
+
600
+ // Whether a body goal, as the rule writes it, could match a fact derived
601
+ // since the rule's last search. The goal's own atoms and numbers narrow the
602
+ // facts down before the rule is renamed and searched.
603
+ mayMatchNew(goal, goalKey, previous, now) {
604
+ const order = this.factOrder(goal, null, goalKey);
605
+ if (order === null) return true;
606
+ const first = bisectLeft(order, previous.get(goalKey));
607
+ return first < order.length && order[first] < now.get(goalKey);
608
+ }
609
+
610
+ // Each solution of a renamed rule's body, as what it concludes, with the
611
+ // alternatives its goals took when the body is searched in a window.
612
+ // Bindings last only until the next answer is requested, and adding facts
613
+ // mid-scan would extend a live search, so each activation is copied out
614
+ // first and concluded afterwards.
615
+ scan(clause, head, body, names) {
616
+ const found = [];
617
+ for (const [answerEnv, nodes] of this.solve(body)) {
618
+ const mark = answerEnv.mark();
619
+ // Head variables the body leaves unbound become Skolem terms, shared
620
+ // between the heads of one conclusion.
621
+ const resolved = copyResolved(head, answerEnv);
622
+ const unresolved = variables(resolved);
623
+ if (unresolved.size) {
624
+ const witnesses = skolemTerms(clause, head, unresolved, answerEnv);
625
+ [...unresolved.values()].forEach((value, i) => unify(value, witnesses[i], answerEnv));
626
+ }
627
+ const conclusions = unresolved.size
628
+ ? flattenConjunction(head).map((item) => copyResolved(item, answerEnv))
629
+ : flattenConjunction(resolved);
630
+ let children = NO_NODES;
631
+ let bindings = NO_NODES;
632
+ if (this.recording) {
633
+ children = nodes.map((node) => resolveNode(node, answerEnv));
634
+ bindings = [...names].map(([name, value]) => [name, copyResolved(value, answerEnv)]);
635
+ }
636
+ const claim = conclusions.some((item) => item === 'true') ? copyResolved(conjunction(body), answerEnv) : null;
637
+ found.push([[conclusions, children, bindings, claim], this.chosen !== null ? [...this.chosen] : null]);
638
+ answerEnv.undo(mark);
639
+ }
640
+ return found;
641
+ }
642
+
643
+ // Run the forward rules to a fixpoint, one stratum after another. A rule
644
+ // whose heads are all 'true' only publishes output; when the caller asks its
645
+ // own question that output is discarded, so the rule is skipped.
646
+ forward(reporting = true) {
647
+ const layers = new Map();
648
+ for (const clause of this.program.forward) {
649
+ const rank = this.program.strata.get(clause.id) || 0;
650
+ if (!layers.has(rank)) layers.set(rank, []);
651
+ layers.get(rank).push(clause);
652
+ }
653
+ const maxIterations = this.options.maxIterations || 1000;
654
+ for (const rank of [...layers.keys()].sort((a, b) => a - b)) {
655
+ const rules = layers.get(rank);
656
+ let changed = true;
657
+ let rounds = 0;
658
+ while (changed) {
659
+ rounds++;
660
+ if (rounds > maxIterations) throw new EyecodeError('forward reasoning exceeded max_iterations');
661
+ this.stats.rounds++;
662
+ changed = false;
663
+ for (const clause of rules) {
664
+ if (!reporting && clause.heads.every((item) => item === 'true')) continue;
665
+ for (const [conclusions, children, bindings, claim] of this.activations(clause)) {
666
+ for (const conclusion of conclusions) {
667
+ if (conclusion === 'true') {
668
+ const claimId = text(claim);
669
+ if (!this.reported.has(claimId)) this.reported.set(claimId, [claim, children, clause.id]);
670
+ continue;
671
+ }
672
+ const node = new Node(conclusion, new Struct('clause', [BigInt(clause.id)]), bindings, children, null, true);
673
+ if (conclusion === 'false') {
674
+ this.derived.push(node);
675
+ this.haltCode = 65;
676
+ return;
677
+ }
678
+ const conclusionId = identity(conclusion);
679
+ if (this.factKeys.has(conclusionId)) continue;
680
+ this.factKeys.add(conclusionId);
681
+ this.addFact(key(conclusion), node);
682
+ this.derived.push(node);
683
+ this.stats.derived++;
684
+ changed = true;
685
+ }
686
+ }
687
+ }
688
+ }
689
+ }
690
+ }
691
+ }
692
+
693
+ // The Skolem terms for the variables a solution leaves unbound in a rule's
694
+ // heads: skolem(Rule, Variable, Arguments), a function of the rule, the
695
+ // variable and the values the solution gave the heads' other variables. The
696
+ // same activation thus always gives the same terms, and a different one
697
+ // different terms.
698
+ export function skolemTerms(clause, head, unresolved, env) {
699
+ const values = [];
700
+ for (const variable of variables(head).values()) {
701
+ const value = copyResolved(variable, env);
702
+ if (isGround(value)) values.push(value);
703
+ }
704
+ const argumentsList = listFromItems(values);
705
+ const names = [];
706
+ for (const variable of unresolved.values()) {
707
+ // Each variable is named as the program names it; a variable that a value
708
+ // of the solution brought in may share a name, so it gets a number to
709
+ // keep the terms apart.
710
+ const hash = variable.name.indexOf('#');
711
+ const base = hash < 0 ? variable.name : variable.name.slice(0, hash);
712
+ let name = base;
713
+ let number = 1;
714
+ while (names.includes(name)) {
715
+ name = `${base}${number}`;
716
+ number++;
717
+ }
718
+ names.push(name);
719
+ }
720
+ return names.map((name) => new Struct('skolem', [BigInt(clause.id), name, argumentsList]));
721
+ }
722
+
723
+ // What a run concluded. answers are the conclusions as printable text and
724
+ // bindings one object per answer of the goal's variables; with a proof, proof
725
+ // is the document and proofReport its check, clausesUsed the ids of the
726
+ // source clauses the conclusions rest on, and clausesBehindBoundaries the ids
727
+ // of clauses only searches behind a trusted boundary could consult.
728
+ export class Result {
729
+ constructor(fields) {
730
+ Object.assign(this, fields);
731
+ }
732
+
733
+ // The derived facts as text, written only when asked for.
734
+ get inferred() {
735
+ return this.derived.map((node) => text(node.goal));
736
+ }
737
+ }
738
+
739
+ // Reason over a program: forward to a fixpoint, then answer any goals.
740
+ // program is a Program; goal or goals are terms or goal text. Without one,
741
+ // the program's own query() rules answer, and without those every newly
742
+ // derived fact is a conclusion.
743
+ export function run(program, { goal = null, goals = null, proof = false, maxDepth = null,
744
+ maxIterations = null, maxInferences = null } = {}) {
745
+ if (!(program instanceof Program)) throw new EyecodeError('run() needs a Program; load one first');
746
+ const options = { proof, maxDepth, maxIterations, maxInferences };
747
+ if (goals === null) goals = goal === null ? [] : [goal];
748
+ try {
749
+ return reason(program, [...goals], options);
750
+ } catch (error) {
751
+ if (error instanceof RangeError) {
752
+ // Search itself is iterative, but a few term walks are recursive, so a
753
+ // term nested deeply enough can exhaust the host stack.
754
+ throw new EyecodeError('reasoning exhausted the host stack on a deeply nested term');
755
+ }
756
+ throw error;
757
+ }
758
+ }
759
+
760
+ function goalTerm(goal) {
761
+ return typeof goal === 'string' ? readGoal(goal) : goal;
762
+ }
763
+
764
+ function reason(program, goalInputs, options) {
765
+ const solver = new Solver(program, options);
766
+ const goals = goalInputs.map(goalTerm);
767
+ solver.forward(!goals.length);
768
+ const roots = [];
769
+ const bindings = [];
770
+ const claims = [];
771
+ const seen = new Set();
772
+ if (goals.length && solver.haltCode === null) {
773
+ for (const goal of goals) {
774
+ for (const [answerEnv, nodes] of solver.solve([goal])) {
775
+ const conclusion = copyResolved(goal, answerEnv);
776
+ const conclusionId = variant(conclusion);
777
+ if (seen.has(conclusionId)) continue;
778
+ seen.add(conclusionId);
779
+ if (options.proof) for (const node of nodes) roots.push(resolveNode(node, answerEnv));
780
+ claims.push(conclusion);
781
+ const answer = new Map();
782
+ for (const [name, value] of variables(goal)) {
783
+ if (!name.startsWith('_')) answer.set(name, copyResolved(value, answerEnv));
784
+ }
785
+ bindings.push(answer);
786
+ }
787
+ }
788
+ } else if (solver.haltCode === null && program.forward.some((clause) => clause.heads.includes('true'))) {
789
+ // A program that asks its own questions concludes their answers, even none.
790
+ for (const [claim, children] of solver.reported.values()) {
791
+ claims.push(claim);
792
+ roots.push(...children);
793
+ }
794
+ } else {
795
+ for (const node of solver.derived) {
796
+ roots.push(node);
797
+ claims.push(node.goal);
798
+ }
799
+ }
800
+ // Variables are named A, B, ... in the order the conclusions show them; the
801
+ // bindings of each answer name them as its conclusion does.
802
+ const lettering = new Lettering();
803
+ const answers = claims.map((claim) => write(claim, null, lettering));
804
+ const answerBindings = bindings.map((answer) => {
805
+ const out = {};
806
+ for (const [name, value] of answer) out[name] = write(value, null, lettering);
807
+ return out;
808
+ });
809
+ const rendered = options.proof ? renderProof(program, claims, roots, true) : null;
810
+ const document = rendered !== null ? rendered[0] : null;
811
+ // A rule that reports a claim is used by it, though a proof records only
812
+ // the claim's support.
813
+ const used = options.proof ? clausesUsed(roots) : null;
814
+ if (used !== null && solver.haltCode === null && !goals.length) {
815
+ for (const [, , clauseId] of solver.reported.values()) used.add(clauseId);
816
+ }
817
+ let report = null;
818
+ if (document !== null) {
819
+ // The generated proof is checked before it is returned.
820
+ report = checkRendered(program, rendered[1], goals);
821
+ if (!report.valid) throw new EyecodeError(`cannot certify this result: ${report.failures[0].detail}`);
822
+ }
823
+ let behind = null;
824
+ if (used !== null) {
825
+ behind = [...clausesBehindBoundaries(program, roots)].filter((id) => !used.has(id)).sort((a, b) => a - b);
826
+ }
827
+ return new Result({
828
+ answers,
829
+ bindings: answerBindings,
830
+ derived: solver.derived,
831
+ clausesUsed: used !== null ? [...used].sort((a, b) => a - b) : null,
832
+ clausesBehindBoundaries: behind,
833
+ stdout: document !== null ? document : answers.map((answer) => `${answer}\n`).join(''),
834
+ proof: document,
835
+ proofReport: report,
836
+ stats: solver.stats,
837
+ haltCode: solver.haltCode,
838
+ });
839
+ }
840
+
841
+ // The clauses that make no difference to the conclusions, as text: one
842
+ // unused(line(N), Clause) per clause no conclusion's proof rests on, unless
843
+ // leaving it out changes what the program concludes: a clause only a negation
844
+ // or a collection consults may still decide a conclusion.
845
+ export function unusedClauses(program, options = {}) {
846
+ const settings = { ...options, proof: false };
847
+ const result = run(program, { ...settings, proof: true });
848
+ const used = new Set(result.clausesUsed);
849
+ const behind = new Set(result.clausesBehindBoundaries);
850
+ const conclusions = (outcome) => JSON.stringify([[...outcome.answers].sort(), outcome.haltCode]);
851
+ const baseline = conclusions(result);
852
+ function matters(clause) {
853
+ try {
854
+ return conclusions(run(new Program(program.sources, clause.id), settings)) !== baseline;
855
+ } catch (error) {
856
+ if (error instanceof EyecodeError) return true;
857
+ throw error;
858
+ }
859
+ }
860
+ const lines = [];
861
+ for (const clause of program.clauses) {
862
+ if (used.has(clause.id) || (behind.has(clause.id) && matters(clause))) continue;
863
+ const written = new Struct('unused', [new Struct('line', [BigInt(clause.line ?? 0)]), clauseDisplay(clause)]);
864
+ lines.push(`${write(written)}\n`);
865
+ }
866
+ return lines.join('');
867
+ }