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.
- package/README.md +78 -4
- package/examples/book/README.md +2 -1
- package/examples/book/chapter-07/02-move.pl +4 -0
- package/examples/book/chapter-08/01-findall.pl +1 -0
- package/examples/book/chapter-39/04-cost.pl +1 -0
- package/examples/chart-parser.pl +1 -0
- package/examples/dog.pl +1 -0
- package/examples/donald-gerald-robert.pl +1 -0
- package/examples/equality-saturation.pl +1 -0
- package/examples/job-shop-scheduling.pl +1 -0
- package/examples/knapsack-optimization.pl +1 -0
- package/examples/knuth-bendix-completion.pl +1 -0
- package/examples/matrix-chain-order.pl +1 -0
- package/examples/missionaries-cannibals.pl +1 -0
- package/examples/prime-range.pl +1 -0
- package/examples/register-allocation.pl +1 -0
- package/examples/send-more-money.pl +1 -0
- package/examples/stable-marriage.pl +1 -0
- package/examples/totient-summatory.pl +1 -0
- package/examples/weighted-interval-scheduling.pl +1 -0
- package/index.d.ts +7 -0
- package/package.json +1 -1
- package/src/datalog.js +354 -0
- package/src/iso.js +148 -14
- package/src/lib/iso_ext.pl +4 -0
- package/src/lib/lists.pl +0 -3
- package/src/program.js +258 -20
- package/src/solver.js +890 -46
- package/src/standard-library.js +12 -7
- package/src/term.js +1 -1
- package/src/wfs.js +338 -0
- package/test/conformance/ISO-COMPLIANCE.md +2 -1
- package/test/conformance/README.md +4 -1
- package/test/run-conformance.mjs +1 -0
- package/test/run-regression.mjs +230 -3
- package/the-art-of-eyeprolog.md +95 -21
- package/why-eyeprolog.md +17 -0
- /package/examples/book/chapter-07/{02-all_tests_pass.pl → 03-all_tests_pass.pl} +0 -0
package/src/solver.js
CHANGED
|
@@ -5,15 +5,18 @@ import {
|
|
|
5
5
|
flattenConjunction, freshTerm, isCons, isDecimalInteger, isEmptyList,
|
|
6
6
|
numberTerm, numberTextFromDouble, termIsGround, termToString, unify, variable, variantTerms,
|
|
7
7
|
} from './term.js';
|
|
8
|
-
import { sameNumberValue } from './number-value.js';
|
|
8
|
+
import { numberValueKey, sameNumberValue } from './number-value.js';
|
|
9
9
|
import { PrologError, getStrictIsoRegistry } from './iso.js';
|
|
10
10
|
import { getEyePrologRegistry } from './standard-library.js';
|
|
11
11
|
import { selectClauseCandidates, selectClauseCandidatesForValues, selectGroundClauseCandidates } from './program.js';
|
|
12
12
|
import { StreamManager } from './io.js';
|
|
13
13
|
import { clpzStateConsistent } from './clpz.js';
|
|
14
14
|
import { hardHeapLimit, softHeapLimit, usedHeapSize } from './platform.js';
|
|
15
|
+
import { evaluateWfs, relationForGroup, truthOfGroundGoal } from './wfs.js';
|
|
16
|
+
import { evaluatePositiveDatalog, relationForDatalogGroup, datalogCandidateIndexes } from './datalog.js';
|
|
15
17
|
|
|
16
18
|
let freshCounter = 0;
|
|
19
|
+
const DEFAULT_INNER_TABLE_SCOPE_LIMIT = 1024;
|
|
17
20
|
|
|
18
21
|
function qualifyTerm(term, module) {
|
|
19
22
|
if (!term || (term.type !== COMPOUND && term.type !== 'atom')) return term;
|
|
@@ -62,6 +65,9 @@ export class Solver {
|
|
|
62
65
|
// still supply solutionLimit explicitly.
|
|
63
66
|
this.solutionLimit = options.solutionLimit ?? Infinity;
|
|
64
67
|
this.solutionsSeen = 0;
|
|
68
|
+
// Meta-call fast path: a caller may bypass automatic tabling only for
|
|
69
|
+
// recursion proven to consume a list tail on every recursive step.
|
|
70
|
+
this.skipListTailTabling = options.skipListTailTabling === true;
|
|
65
71
|
this.prologFlags = options.prologFlags ?? defaultPrologFlags('error', this.isoStrict);
|
|
66
72
|
if (this.isoStrict) {
|
|
67
73
|
for (const name of [...this.prologFlags.keys()]) {
|
|
@@ -104,9 +110,17 @@ export class Solver {
|
|
|
104
110
|
this.active = [];
|
|
105
111
|
this.cutEpoch = 0;
|
|
106
112
|
this.memo = new Map();
|
|
113
|
+
this.subsumptiveMemo = new Map();
|
|
114
|
+
this.wfsModels = new Map();
|
|
115
|
+
this.datalogModels = new Map();
|
|
107
116
|
this.tableCoordinator = null;
|
|
108
117
|
this.groundChainSuccess = new Set();
|
|
109
118
|
this.compactChainSuccess = new Map();
|
|
119
|
+
// Bounded table caches for isolated meta-call domains such as phrase/2-3.
|
|
120
|
+
// They deliberately do not share the caller's general memo: a stream of
|
|
121
|
+
// distinct meta-call inputs must not turn completed table variants into
|
|
122
|
+
// process-lifetime roots.
|
|
123
|
+
this.innerTableScopes = options.innerTableScopes ?? new Map();
|
|
110
124
|
this.stats = {
|
|
111
125
|
completed_goal_lists: 0,
|
|
112
126
|
solve_goals_calls: 0,
|
|
@@ -117,10 +131,15 @@ export class Solver {
|
|
|
117
131
|
deterministic_builtin_successes: 0,
|
|
118
132
|
deterministic_builtin_failures: 0,
|
|
119
133
|
table_fixpoint_rounds: 0,
|
|
134
|
+
datalog_evaluations: 0,
|
|
135
|
+
datalog_rule_firings: 0,
|
|
136
|
+
datalog_facts_derived: 0,
|
|
137
|
+
wfs_fixpoint_rounds: 0,
|
|
138
|
+
wfs_undefined_answers: 0,
|
|
120
139
|
};
|
|
121
140
|
}
|
|
122
141
|
|
|
123
|
-
cloneForInnerGoal(solutionLimit = this.solutionLimit) {
|
|
142
|
+
cloneForInnerGoal(solutionLimit = this.solutionLimit, options = {}) {
|
|
124
143
|
const solver = new Solver(this.program, {
|
|
125
144
|
registry: this.registry,
|
|
126
145
|
maxDepth: this.maxDepth,
|
|
@@ -132,13 +151,74 @@ export class Solver {
|
|
|
132
151
|
prologFlags: this.prologFlags,
|
|
133
152
|
charConversions: this.charConversions,
|
|
134
153
|
io: this.io,
|
|
154
|
+
innerTableScopes: this.innerTableScopes,
|
|
155
|
+
skipListTailTabling: options.skipListTailTabling ?? this.skipListTailTabling,
|
|
135
156
|
});
|
|
136
|
-
|
|
157
|
+
if (options.tableScope != null) {
|
|
158
|
+
const scope = this.innerTableScope(options.tableScope, options.tableScopeSignature ?? null);
|
|
159
|
+
solver.memo = scope.memo;
|
|
160
|
+
solver.subsumptiveMemo = scope.subsumptiveMemo;
|
|
161
|
+
} else {
|
|
162
|
+
solver.memo = this.memo;
|
|
163
|
+
solver.subsumptiveMemo = this.subsumptiveMemo;
|
|
164
|
+
}
|
|
165
|
+
solver.wfsModels = this.wfsModels;
|
|
166
|
+
solver.datalogModels = this.datalogModels;
|
|
137
167
|
solver.groundChainSuccess = this.groundChainSuccess;
|
|
138
168
|
solver.compactChainSuccess = this.compactChainSuccess;
|
|
139
169
|
return solver;
|
|
140
170
|
}
|
|
141
171
|
|
|
172
|
+
innerTableScope(name, signature = null) {
|
|
173
|
+
let scope = this.innerTableScopes.get(name);
|
|
174
|
+
if (scope == null) {
|
|
175
|
+
scope = { memo: new Map(), subsumptiveMemo: new Map(), signature };
|
|
176
|
+
this.innerTableScopes.set(name, scope);
|
|
177
|
+
return scope;
|
|
178
|
+
}
|
|
179
|
+
if (signature == null || scope.signature === signature) return scope;
|
|
180
|
+
|
|
181
|
+
// Keep at most one completed invocation variant for isolated meta-call
|
|
182
|
+
// domains. Reusing the same phrase/2-3 call remains fast, while a stream of
|
|
183
|
+
// distinct inputs releases the previous table as one unit instead of
|
|
184
|
+
// churning thousands of individual memo entries. A nested meta-call must
|
|
185
|
+
// not invalidate an active fixed point, so it receives a temporary scope.
|
|
186
|
+
for (const entry of scope.memo.values()) {
|
|
187
|
+
if (entry?.computing === true) {
|
|
188
|
+
return { memo: new Map(), subsumptiveMemo: new Map(), signature };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
scope.memo.clear();
|
|
192
|
+
scope.subsumptiveMemo.clear();
|
|
193
|
+
scope.signature = signature;
|
|
194
|
+
return scope;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
innerTableSignature(terms, env, prefix = '') {
|
|
198
|
+
const variables = new Map();
|
|
199
|
+
return prefix + terms.map((term) => canonicalTermKey(term, env, variables)).join('|');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
trimInnerTableScope(name, limit = DEFAULT_INNER_TABLE_SCOPE_LIMIT) {
|
|
203
|
+
const scope = this.innerTableScopes.get(name);
|
|
204
|
+
if (scope == null || scope.memo.size <= limit) return;
|
|
205
|
+
// Map iteration is insertion ordered. Delete completed entries directly by
|
|
206
|
+
// key as we encounter them so a steady stream of distinct meta-call inputs
|
|
207
|
+
// pays O(evictions), not an O(cache-size) rescan for every phrase/2 call.
|
|
208
|
+
// Active fixed-point entries are skipped and can only make the cache
|
|
209
|
+
// temporarily exceed its soft bound.
|
|
210
|
+
for (const [key, entry] of scope.memo) {
|
|
211
|
+
if (scope.memo.size <= limit) break;
|
|
212
|
+
if (entry?.complete !== true || entry?.computing === true) continue;
|
|
213
|
+
scope.memo.delete(key);
|
|
214
|
+
if (scope.subsumptiveMemo.size !== 0) {
|
|
215
|
+
for (const [broadKey, broadEntry] of scope.subsumptiveMemo) {
|
|
216
|
+
if (broadEntry === entry) scope.subsumptiveMemo.delete(broadKey);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
142
222
|
syncProgramRevision() {
|
|
143
223
|
if (!this.mutableProgram) {
|
|
144
224
|
if (this.program.mutable !== true) return;
|
|
@@ -148,9 +228,16 @@ export class Solver {
|
|
|
148
228
|
if (revision === this.programRevision) return;
|
|
149
229
|
this.programRevision = revision;
|
|
150
230
|
this.memo.clear();
|
|
231
|
+
this.subsumptiveMemo.clear();
|
|
232
|
+
this.wfsModels.clear();
|
|
233
|
+
this.datalogModels.clear();
|
|
151
234
|
this.tableCoordinator = null;
|
|
152
235
|
this.groundChainSuccess.clear();
|
|
153
236
|
this.compactChainSuccess.clear();
|
|
237
|
+
for (const scope of this.innerTableScopes.values()) {
|
|
238
|
+
scope.memo.clear();
|
|
239
|
+
scope.subsumptiveMemo.clear();
|
|
240
|
+
}
|
|
154
241
|
}
|
|
155
242
|
|
|
156
243
|
absorbStatsFrom(child) {
|
|
@@ -330,6 +417,19 @@ export class Solver {
|
|
|
330
417
|
if (goal.type === 'var') throw new PrologError('instantiation_error');
|
|
331
418
|
const callable = goal.type === COMPOUND || goal.type === 'atom';
|
|
332
419
|
if (!callable) throw new PrologError('type_error(callable)', goal);
|
|
420
|
+
|
|
421
|
+
if (selectedIndex === 0) {
|
|
422
|
+
const fused = findallLengthFusion(this, goal, rest, env);
|
|
423
|
+
if (fused != null) {
|
|
424
|
+
const firstResult = fused.iterator.next();
|
|
425
|
+
if (firstResult.done) break;
|
|
426
|
+
goals = fused.rest;
|
|
427
|
+
env = firstResult.value;
|
|
428
|
+
depth++;
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
333
433
|
const def = callable ? this.registry.get(goal.name, goal.arity) : null;
|
|
334
434
|
this.active = active;
|
|
335
435
|
if (def && builtinIsReadyOrAuthoritative(def, this, goal, env)) {
|
|
@@ -374,7 +474,49 @@ export class Solver {
|
|
|
374
474
|
}
|
|
375
475
|
qualifyMetaArguments(goal, group);
|
|
376
476
|
|
|
377
|
-
|
|
477
|
+
if (group.datalogLeastModel === true && !termIsGround(goal, env)) {
|
|
478
|
+
const model = this.datalogModelFor(group);
|
|
479
|
+
const relation = relationForDatalogGroup(model, group);
|
|
480
|
+
const iterator = datalogAnswerSolutions(this, relation, goal, env);
|
|
481
|
+
const firstResult = iterator.next();
|
|
482
|
+
if (firstResult.done) break;
|
|
483
|
+
stack.push({
|
|
484
|
+
kind: 'resumeBuiltin',
|
|
485
|
+
iterator,
|
|
486
|
+
goals: rest,
|
|
487
|
+
depth: depth + 1,
|
|
488
|
+
active,
|
|
489
|
+
});
|
|
490
|
+
goals = rest;
|
|
491
|
+
env = firstResult.value;
|
|
492
|
+
depth++;
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (group.wfsDatalog === true) {
|
|
497
|
+
const model = this.wfsModelFor(group);
|
|
498
|
+
pushWfsAnswerFrames(stack, model, group, goal, rest, env, depth, active, this);
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const memberIterator = bundledMemberIterator(this, group, goal, env);
|
|
503
|
+
if (memberIterator != null) {
|
|
504
|
+
const firstResult = memberIterator.next();
|
|
505
|
+
if (firstResult.done) break;
|
|
506
|
+
stack.push({
|
|
507
|
+
kind: 'resumeBuiltin',
|
|
508
|
+
iterator: memberIterator,
|
|
509
|
+
goals: rest,
|
|
510
|
+
depth: depth + 1,
|
|
511
|
+
active,
|
|
512
|
+
});
|
|
513
|
+
goals = rest;
|
|
514
|
+
env = firstResult.value;
|
|
515
|
+
depth++;
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const lengthIterator = bundledLengthIterator(this, group, goal, env);
|
|
378
520
|
if (lengthIterator != null) {
|
|
379
521
|
const firstResult = lengthIterator.next();
|
|
380
522
|
if (firstResult.done) break;
|
|
@@ -391,14 +533,29 @@ export class Solver {
|
|
|
391
533
|
continue;
|
|
392
534
|
}
|
|
393
535
|
|
|
394
|
-
if (group.tabled) {
|
|
536
|
+
if (group.tabled && !(this.skipListTailTabling && group.listTailRecursive)) {
|
|
395
537
|
const key = memoKey(goal, env, group);
|
|
396
538
|
if (key.hasBound) {
|
|
397
539
|
const mapKey = `${group.module}:${goal.name}/${goal.arity}:${key.text}`;
|
|
398
|
-
|
|
540
|
+
const broadKey = `${group.module}:${goal.name}/${goal.arity}`;
|
|
541
|
+
let entry = null;
|
|
542
|
+
let usingBroadTable = false;
|
|
543
|
+
if (group.tableAllVariants) {
|
|
544
|
+
entry = this.subsumptiveMemo.get(broadKey) ?? null;
|
|
545
|
+
usingBroadTable = entry != null;
|
|
546
|
+
if (!entry && isMostGeneralTableGoal(goal, env)) {
|
|
547
|
+
entry = makeMemoEntry(goal.arity);
|
|
548
|
+
this.subsumptiveMemo.set(broadKey, entry);
|
|
549
|
+
this.memo.set(mapKey, entry);
|
|
550
|
+
usingBroadTable = true;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
399
553
|
if (!entry) {
|
|
400
|
-
entry =
|
|
401
|
-
|
|
554
|
+
entry = this.memo.get(mapKey);
|
|
555
|
+
if (!entry) {
|
|
556
|
+
entry = makeMemoEntry(goal.arity);
|
|
557
|
+
this.memo.set(mapKey, entry);
|
|
558
|
+
}
|
|
402
559
|
}
|
|
403
560
|
if (this.tableCoordinator) this.tableCoordinator.entries.add(entry);
|
|
404
561
|
if (entry.complete) {
|
|
@@ -416,7 +573,7 @@ export class Solver {
|
|
|
416
573
|
}
|
|
417
574
|
break;
|
|
418
575
|
}
|
|
419
|
-
if (this.tableCoordinator && activeVariantIn(goal, env, active)) {
|
|
576
|
+
if (this.tableCoordinator && (usingBroadTable || activeVariantIn(goal, env, active))) {
|
|
420
577
|
this.tableCoordinator.cycleSeen = true;
|
|
421
578
|
}
|
|
422
579
|
pushMemoAnswerFrames(stack, entry, goal, rest, env, depth, active, this);
|
|
@@ -449,6 +606,84 @@ export class Solver {
|
|
|
449
606
|
}
|
|
450
607
|
}
|
|
451
608
|
|
|
609
|
+
fastCountGoal(goal, env) {
|
|
610
|
+
return fastCountPureGoal(this, goal, env);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
fastGroundGoalTruth(goal, env) {
|
|
614
|
+
return fastGroundPureGoalTruth(this, goal, env);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
datalogModelFor(group) {
|
|
618
|
+
let model = this.datalogModels.get(group);
|
|
619
|
+
if (model) return model;
|
|
620
|
+
model = evaluatePositiveDatalog(this.program, group);
|
|
621
|
+
this.stats.datalog_evaluations++;
|
|
622
|
+
this.stats.datalog_rule_firings += model.ruleFirings;
|
|
623
|
+
this.stats.datalog_facts_derived += model.derivedFacts;
|
|
624
|
+
for (const member of model.groups) {
|
|
625
|
+
if (!this.datalogModels.has(member)) this.datalogModels.set(member, model);
|
|
626
|
+
}
|
|
627
|
+
this.datalogModels.set(group, model);
|
|
628
|
+
return model;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
wfsModelFor(group) {
|
|
632
|
+
let model = this.wfsModels.get(group);
|
|
633
|
+
if (model) return model;
|
|
634
|
+
model = evaluateWfs(this.program, group);
|
|
635
|
+
this.stats.wfs_fixpoint_rounds += model.rounds;
|
|
636
|
+
this.wfsModels.set(group, model);
|
|
637
|
+
return model;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
*solveWfsGoal(group, goal, rest, env, depth) {
|
|
641
|
+
const model = this.wfsModelFor(group);
|
|
642
|
+
const relation = relationForGroup(model, group, 'upper');
|
|
643
|
+
const lower = relationForGroup(model, group, 'lower');
|
|
644
|
+
if (!relation) return;
|
|
645
|
+
for (const row of relation.rows) {
|
|
646
|
+
const next = env.clone();
|
|
647
|
+
let ok = true;
|
|
648
|
+
for (let i = 0; i < goal.arity; i++) {
|
|
649
|
+
this.stats.unify_calls++;
|
|
650
|
+
if (!unify(goal.args[i], row[i], next)) { ok = false; break; }
|
|
651
|
+
}
|
|
652
|
+
if (!ok) continue;
|
|
653
|
+
if (!lower?.has(row)) this.stats.wfs_undefined_answers++;
|
|
654
|
+
yield* this.solve(rest, next, depth + 1);
|
|
655
|
+
if (this.solutionsSeen >= this.solutionLimit) return;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
*solveTabledNegation(argument, env) {
|
|
660
|
+
const invoked = copyResolved(argument, env);
|
|
661
|
+
if (invoked.type === VAR) throw new PrologError('instantiation_error');
|
|
662
|
+
if (invoked.type !== COMPOUND && invoked.type !== 'atom') {
|
|
663
|
+
throw new PrologError('type_error(callable)', invoked);
|
|
664
|
+
}
|
|
665
|
+
if (!termIsGround(invoked)) throw new PrologError('instantiation_error');
|
|
666
|
+
const group = this.program.findGroup(invoked.name, invoked.arity, invoked.module ?? 'user');
|
|
667
|
+
if (group?.wfsDatalog === true) {
|
|
668
|
+
const model = this.wfsModelFor(group);
|
|
669
|
+
const truth = truthOfGroundGoal(model, invoked);
|
|
670
|
+
if (truth === 'true') return;
|
|
671
|
+
if (truth === 'undefined') this.stats.wfs_undefined_answers++;
|
|
672
|
+
yield env;
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Outside an unstratified WFS component, a ground tabled negation has the
|
|
677
|
+
// ordinary two-valued result after the positive goal is evaluated.
|
|
678
|
+
const child = this.cloneForInnerGoal(1);
|
|
679
|
+
for (const _ of child.solve([invoked], env.clone(), 0)) {
|
|
680
|
+
this.absorbStatsFrom(child);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
this.absorbStatsFrom(child);
|
|
684
|
+
yield env;
|
|
685
|
+
}
|
|
686
|
+
|
|
452
687
|
activeVariant(goal, env) {
|
|
453
688
|
return activeVariantIn(goal, env, this.active);
|
|
454
689
|
}
|
|
@@ -508,6 +743,14 @@ export class Solver {
|
|
|
508
743
|
const group = this.program.findGroup(goal.name, goal.arity, goal.module ?? 'user');
|
|
509
744
|
if (!group) return;
|
|
510
745
|
qualifyMetaArguments(goal, group);
|
|
746
|
+
if (group.datalogLeastModel === true && !termIsGround(goal, env)) {
|
|
747
|
+
yield* this.solveDatalogGoal(group, goal, rest, env, depth);
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
if (group.wfsDatalog === true) {
|
|
751
|
+
yield* this.solveWfsGoal(group, goal, rest, env, depth);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
511
754
|
if (group.tabled) {
|
|
512
755
|
yield* this.solveMemoizedGoal(group, goal, rest, env, depth);
|
|
513
756
|
return;
|
|
@@ -515,6 +758,15 @@ export class Solver {
|
|
|
515
758
|
yield* this.solveUserGoalUncached(group, goal, rest, env, depth);
|
|
516
759
|
}
|
|
517
760
|
|
|
761
|
+
*solveDatalogGoal(group, goal, rest, env, depth) {
|
|
762
|
+
const model = this.datalogModelFor(group);
|
|
763
|
+
const relation = relationForDatalogGroup(model, group);
|
|
764
|
+
for (const next of datalogAnswerSolutions(this, relation, goal, env)) {
|
|
765
|
+
yield* this.solve(rest, next, depth + 1);
|
|
766
|
+
if (this.solutionsSeen >= this.solutionLimit) return;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
518
770
|
*solveMemoizedGoal(group, goal, rest, env, depth) {
|
|
519
771
|
yield* this.solve([goal, ...rest], env, depth);
|
|
520
772
|
}
|
|
@@ -624,8 +876,30 @@ function defaultPrologFlags(unknown = 'error', strictIso = false) {
|
|
|
624
876
|
}
|
|
625
877
|
|
|
626
878
|
|
|
627
|
-
function makeMemoEntry() {
|
|
628
|
-
return {
|
|
879
|
+
function makeMemoEntry(arity = 0) {
|
|
880
|
+
return {
|
|
881
|
+
computing: false,
|
|
882
|
+
complete: false,
|
|
883
|
+
answers: [],
|
|
884
|
+
answerKeys: new Set(),
|
|
885
|
+
answerIndexes: Array.from({ length: arity }, () => new Map()),
|
|
886
|
+
answerVariableFallbacks: Array.from({ length: arity }, () => []),
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function memoAnswerScalarKey(term) {
|
|
891
|
+
if (term?.type !== 'atom' && term?.type !== 'string' && term?.type !== 'number') return null;
|
|
892
|
+
return `${term.type}\u0000${term.type === 'number' ? numberValueKey(term.name) : term.name}`;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function isMostGeneralTableGoal(goal, env) {
|
|
896
|
+
const seen = new Set();
|
|
897
|
+
for (const arg of goal.args) {
|
|
898
|
+
const value = derefForLocal(arg, env);
|
|
899
|
+
if (value.type !== 'var' || seen.has(value.name)) return false;
|
|
900
|
+
seen.add(value.name);
|
|
901
|
+
}
|
|
902
|
+
return true;
|
|
629
903
|
}
|
|
630
904
|
|
|
631
905
|
function scheduleTableFixpointRound(stack, solver, frame) {
|
|
@@ -663,10 +937,19 @@ function scheduleTableFixpointRound(stack, solver, frame) {
|
|
|
663
937
|
|
|
664
938
|
|
|
665
939
|
function pushMemoAnswerFrames(stack, entry, goal, rest, env, depth, active, solver) {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
940
|
+
let selected = null;
|
|
941
|
+
for (let position = 0; position < goal.arity; position++) {
|
|
942
|
+
const value = derefForLocal(goal.args[position], env);
|
|
943
|
+
const key = memoAnswerScalarKey(value);
|
|
944
|
+
if (key == null) continue;
|
|
945
|
+
const bucket = entry.answerIndexes[position]?.get(key) ?? [];
|
|
946
|
+
const fallback = entry.answerVariableFallbacks[position] ?? [];
|
|
947
|
+
const candidateLength = bucket.length + fallback.length;
|
|
948
|
+
if (selected == null || candidateLength < selected.length) {
|
|
949
|
+
selected = { bucket, fallback, length: candidateLength };
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
const replay = (answerIndex) => {
|
|
670
953
|
const storedArgs = entry.answers[answerIndex];
|
|
671
954
|
const answerArgs = storedArgs.every((arg) => termIsGround(arg))
|
|
672
955
|
? storedArgs
|
|
@@ -678,12 +961,19 @@ function pushMemoAnswerFrames(stack, entry, goal, rest, env, depth, active, solv
|
|
|
678
961
|
if (!unify(goal.args[i], answerArgs[i], next)) { ok = false; break; }
|
|
679
962
|
}
|
|
680
963
|
if (ok) stack.push({ kind: 'goals', goals: rest, env: next, depth: depth + 1, active });
|
|
964
|
+
};
|
|
965
|
+
if (selected != null) {
|
|
966
|
+
for (let i = selected.fallback.length - 1; i >= 0; i--) replay(selected.fallback[i]);
|
|
967
|
+
for (let i = selected.bucket.length - 1; i >= 0; i--) replay(selected.bucket[i]);
|
|
968
|
+
return;
|
|
681
969
|
}
|
|
970
|
+
for (let answerIndex = entry.answers.length - 1; answerIndex >= 0; answerIndex--) replay(answerIndex);
|
|
682
971
|
}
|
|
683
972
|
|
|
684
973
|
function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth, active) {
|
|
685
974
|
if (group.recursive && !group.cutRecursive && !group.linearNumeric && activeVariantIn(goal, env, active)) return;
|
|
686
975
|
if (group.fastPi && pushFastPiFrames(stack, goal, rest, env, depth, active)) return;
|
|
976
|
+
if (tryPushGroundScalarRuleFrame(stack, solver, group, goal, rest, env, depth, active)) return;
|
|
687
977
|
if (tryPushGroundChainFrames(stack, solver, group, goal, rest, env, depth, active)) return;
|
|
688
978
|
const candidates = selectClauseCandidates(group, goal, env);
|
|
689
979
|
const frames = [];
|
|
@@ -757,9 +1047,79 @@ function groupNeedsActiveFrame(group) {
|
|
|
757
1047
|
return group.cutReachable !== false || (group.recursive && !group.linearNumeric);
|
|
758
1048
|
}
|
|
759
1049
|
|
|
760
|
-
function
|
|
1050
|
+
function* datalogAnswerSolutions(solver, relation, goal, env) {
|
|
1051
|
+
if (!relation) return;
|
|
1052
|
+
const candidates = datalogCandidateIndexes(
|
|
1053
|
+
relation,
|
|
1054
|
+
goal.args ?? [],
|
|
1055
|
+
env,
|
|
1056
|
+
derefForLocal,
|
|
1057
|
+
memoAnswerScalarKey,
|
|
1058
|
+
);
|
|
1059
|
+
const visit = function* (rowIndex) {
|
|
1060
|
+
const row = relation.rows[rowIndex];
|
|
1061
|
+
const next = env.clone();
|
|
1062
|
+
for (let i = 0; i < goal.arity; i++) {
|
|
1063
|
+
solver.stats.unify_calls++;
|
|
1064
|
+
if (!unify(goal.args[i], row[i], next)) return;
|
|
1065
|
+
}
|
|
1066
|
+
yield next;
|
|
1067
|
+
};
|
|
1068
|
+
if (candidates == null) {
|
|
1069
|
+
for (let rowIndex = 0; rowIndex < relation.rows.length; rowIndex++) yield* visit(rowIndex);
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
for (const rowIndex of candidates) yield* visit(rowIndex);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function pushWfsAnswerFrames(stack, model, group, goal, rest, env, depth, active, solver) {
|
|
1076
|
+
const relation = relationForGroup(model, group, 'upper');
|
|
1077
|
+
const lower = relationForGroup(model, group, 'lower');
|
|
1078
|
+
if (!relation) return;
|
|
1079
|
+
for (let rowIndex = relation.rows.length - 1; rowIndex >= 0; rowIndex--) {
|
|
1080
|
+
const row = relation.rows[rowIndex];
|
|
1081
|
+
const next = env.clone();
|
|
1082
|
+
let ok = true;
|
|
1083
|
+
for (let i = 0; i < goal.arity; i++) {
|
|
1084
|
+
solver.stats.unify_calls++;
|
|
1085
|
+
if (!unify(goal.args[i], row[i], next)) { ok = false; break; }
|
|
1086
|
+
}
|
|
1087
|
+
if (!ok) continue;
|
|
1088
|
+
if (!lower?.has(row)) solver.stats.wfs_undefined_answers++;
|
|
1089
|
+
stack.push({ kind: 'goals', goals: rest, env: next, depth: depth + 1, active });
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function bundledMemberIterator(solver, group, goal, env) {
|
|
761
1094
|
if (solver.registry.eyePrologLibrary !== true ||
|
|
762
|
-
|
|
1095
|
+
!['lists', 'prologue'].includes(group.module) || group.name !== 'member' || group.arity !== 2 ||
|
|
1096
|
+
group.bundledLibrary !== true || group.clauses.length !== 2) {
|
|
1097
|
+
return null;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// Keep the relational/open-list cases in ordinary Prolog. When the second
|
|
1101
|
+
// argument is already a finite list, however, the canonical two-clause
|
|
1102
|
+
// member/2 definition is exactly a left-to-right scan and can avoid clause
|
|
1103
|
+
// freshening and recursive solver frames for every element.
|
|
1104
|
+
let cursor = deref(goal.args[1], env);
|
|
1105
|
+
while (isCons(cursor)) cursor = deref(cursor.args[1], env);
|
|
1106
|
+
if (!isEmptyList(cursor)) return null;
|
|
1107
|
+
return bundledMemberSolutions(solver, goal, env);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function* bundledMemberSolutions(solver, goal, env) {
|
|
1111
|
+
let cursor = deref(goal.args[1], env);
|
|
1112
|
+
while (isCons(cursor)) {
|
|
1113
|
+
const next = env.clone();
|
|
1114
|
+
solver.stats.unify_calls++;
|
|
1115
|
+
if (unify(goal.args[0], cursor.args[0], next)) yield next;
|
|
1116
|
+
cursor = deref(cursor.args[1], env);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function bundledLengthIterator(solver, group, goal, env) {
|
|
1121
|
+
if (solver.registry.eyePrologLibrary !== true ||
|
|
1122
|
+
!['lists', 'prologue'].includes(group.module) || group.name !== 'length' || group.arity !== 2 ||
|
|
763
1123
|
group.bundledLibrary !== true || group.clauses.length !== 2) {
|
|
764
1124
|
return null;
|
|
765
1125
|
}
|
|
@@ -771,17 +1131,22 @@ function prologueLengthIterator(solver, group, goal, env) {
|
|
|
771
1131
|
if (length.type === VAR && env._delays?.has(length.name)) return null;
|
|
772
1132
|
|
|
773
1133
|
let cursor = deref(goal.args[0], env);
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
if (cursor
|
|
778
|
-
|
|
779
|
-
|
|
1134
|
+
// findall/3 can return an internal compact proper-list spine. Its length is
|
|
1135
|
+
// already known; do not expand millions of lazy cons cells merely to decide
|
|
1136
|
+
// whether the bundled length/2 fast path is applicable.
|
|
1137
|
+
if (compactListLength(cursor) == null) {
|
|
1138
|
+
while (isCons(cursor)) {
|
|
1139
|
+
cursor = deref(cursor.args[1], env);
|
|
1140
|
+
}
|
|
1141
|
+
if (cursor.type === VAR) {
|
|
1142
|
+
if (env._delays?.has(cursor.name)) return null;
|
|
1143
|
+
if (length.type === VAR && cursor.name === length.name) return null;
|
|
1144
|
+
}
|
|
780
1145
|
}
|
|
781
|
-
return
|
|
1146
|
+
return bundledLengthSolutions(solver, goal, env);
|
|
782
1147
|
}
|
|
783
1148
|
|
|
784
|
-
function*
|
|
1149
|
+
function* bundledLengthSolutions(solver, goal, env) {
|
|
785
1150
|
const requestedLength = deref(goal.args[1], env);
|
|
786
1151
|
if (requestedLength.type !== VAR) {
|
|
787
1152
|
if (requestedLength.type !== NUMBER || !isDecimalInteger(requestedLength.name)) {
|
|
@@ -798,6 +1163,11 @@ function* prologueLengthSolutions(solver, goal, env) {
|
|
|
798
1163
|
|
|
799
1164
|
function* fixedLengthSolutions(solver, list, length, env) {
|
|
800
1165
|
let cursor = deref(list, env);
|
|
1166
|
+
const compactLength = compactListLength(cursor);
|
|
1167
|
+
if (compactLength != null) {
|
|
1168
|
+
if (compactLength === length) yield env;
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
801
1171
|
let remaining = length;
|
|
802
1172
|
let steps = 0n;
|
|
803
1173
|
while (isCons(cursor)) {
|
|
@@ -838,6 +1208,12 @@ function* fixedLengthSolutions(solver, list, length, env) {
|
|
|
838
1208
|
|
|
839
1209
|
function* generatedLengthSolutions(solver, list, length, env) {
|
|
840
1210
|
let cursor = deref(list, env);
|
|
1211
|
+
const compactLength = compactListLength(cursor);
|
|
1212
|
+
if (compactLength != null) {
|
|
1213
|
+
const next = bindGeneratedLength(solver, length, compactLength, env);
|
|
1214
|
+
if (next != null) yield next;
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
841
1217
|
let count = 0n;
|
|
842
1218
|
let steps = 0n;
|
|
843
1219
|
while (isCons(cursor)) {
|
|
@@ -908,13 +1284,352 @@ function pushFastPiFrames(stack, goal, rest, env, depth, active) {
|
|
|
908
1284
|
|
|
909
1285
|
|
|
910
1286
|
|
|
911
|
-
|
|
1287
|
+
function fastGroundPureGoalTruth(solver, goal, env = new Env(), visiting = new Set()) {
|
|
1288
|
+
if (!termIsGround(goal, env)) return null;
|
|
1289
|
+
const resolved = copyResolved(goal, env);
|
|
1290
|
+
if (resolved.type !== COMPOUND && resolved.type !== 'atom') return null;
|
|
1291
|
+
const group = solver.program.findGroup(resolved.name, resolved.arity, resolved.module ?? 'user');
|
|
1292
|
+
if (!group || group.hasCut || group.wfsDatalog) return null;
|
|
1293
|
+
|
|
1294
|
+
if (group.datalogLeastModel === true) {
|
|
1295
|
+
const model = solver.datalogModelFor(group);
|
|
1296
|
+
const relation = relationForDatalogGroup(model, group);
|
|
1297
|
+
return relation?.has(resolved.args ?? []) ?? false;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
if (group.scalarFactsOnly) {
|
|
1301
|
+
const candidates = selectGroundClauseCandidates(group, resolved);
|
|
1302
|
+
for (let index = 0; index < clauseCandidateLength(candidates); index++) {
|
|
1303
|
+
const clause = clauseCandidateAt(candidates, index);
|
|
1304
|
+
if (matchGroundClause(resolved, clause)?.done) return true;
|
|
1305
|
+
}
|
|
1306
|
+
return false;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
if (group.recursive || group.tabled) return null;
|
|
1310
|
+
const key = `${group.module}:${group.name}/${group.arity}`;
|
|
1311
|
+
if (visiting.has(key)) return null;
|
|
1312
|
+
const nextVisiting = new Set(visiting);
|
|
1313
|
+
nextVisiting.add(key);
|
|
1314
|
+
|
|
1315
|
+
const candidates = selectGroundClauseCandidates(group, resolved);
|
|
1316
|
+
for (let candidateIndex = 0; candidateIndex < clauseCandidateLength(candidates); candidateIndex++) {
|
|
1317
|
+
const clause = clauseCandidateAt(candidates, candidateIndex);
|
|
1318
|
+
const bindings = new Map();
|
|
1319
|
+
let headMatches = true;
|
|
1320
|
+
for (let i = 0; i < resolved.arity; i++) {
|
|
1321
|
+
const pattern = clause.head.args[i];
|
|
1322
|
+
const value = resolved.args[i];
|
|
1323
|
+
if (pattern.type === 'var') {
|
|
1324
|
+
const previous = bindings.get(pattern.name);
|
|
1325
|
+
if (previous == null) bindings.set(pattern.name, value);
|
|
1326
|
+
else if (!sameGroundTerm(previous, value)) { headMatches = false; break; }
|
|
1327
|
+
} else if (isScalarTerm(pattern)) {
|
|
1328
|
+
if (!sameScalarTerm(pattern, value)) { headMatches = false; break; }
|
|
1329
|
+
} else {
|
|
1330
|
+
return null;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
if (!headMatches) continue;
|
|
1334
|
+
|
|
1335
|
+
let clauseTrue = true;
|
|
1336
|
+
for (const body of clause.body) {
|
|
1337
|
+
if (body.type !== COMPOUND && body.type !== 'atom') return null;
|
|
1338
|
+
const args = [];
|
|
1339
|
+
for (const arg of body.args ?? []) {
|
|
1340
|
+
if (arg.type === 'var') {
|
|
1341
|
+
const value = bindings.get(arg.name);
|
|
1342
|
+
if (value == null) return null;
|
|
1343
|
+
args.push(value);
|
|
1344
|
+
} else if (isScalarTerm(arg)) {
|
|
1345
|
+
args.push(arg);
|
|
1346
|
+
} else {
|
|
1347
|
+
return null;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
const bodyGoal = body.type === 'atom' ? body : compound(body.name, args);
|
|
1351
|
+
if (body.module != null) bodyGoal.module = body.module;
|
|
1352
|
+
const truth = fastGroundPureGoalTruth(solver, bodyGoal, new Env(), nextVisiting);
|
|
1353
|
+
if (truth == null) return null;
|
|
1354
|
+
if (!truth) { clauseTrue = false; break; }
|
|
1355
|
+
}
|
|
1356
|
+
if (clauseTrue) return true;
|
|
1357
|
+
}
|
|
1358
|
+
return false;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function termReferencesResolvedVariable(term, variableName, env) {
|
|
1362
|
+
const pending = [term];
|
|
1363
|
+
const seen = new Set();
|
|
1364
|
+
while (pending.length > 0) {
|
|
1365
|
+
const value = derefForLocal(pending.pop(), env);
|
|
1366
|
+
if (value.type === 'var') {
|
|
1367
|
+
if (value.name === variableName) return true;
|
|
1368
|
+
continue;
|
|
1369
|
+
}
|
|
1370
|
+
if (value.type !== COMPOUND || seen.has(value)) continue;
|
|
1371
|
+
seen.add(value);
|
|
1372
|
+
for (const arg of value.args) pending.push(arg);
|
|
1373
|
+
}
|
|
1374
|
+
return false;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
function envHasObservableAliasTo(variableName, env) {
|
|
1378
|
+
for (let state = env?._state; state != null; state = state.parent) {
|
|
1379
|
+
if (state.bindingName != null && state.bindingName !== variableName &&
|
|
1380
|
+
termReferencesResolvedVariable(state.bindingValue, variableName, env)) return true;
|
|
1381
|
+
if (state.bindings) {
|
|
1382
|
+
for (const [name, value] of state.bindings) {
|
|
1383
|
+
if (name !== variableName && termReferencesResolvedVariable(value, variableName, env)) return true;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
return false;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
function* fusedFindallLengthSolutions(solver, innerGoal, countArg, env) {
|
|
1391
|
+
const invoked = copyResolved(innerGoal, env);
|
|
1392
|
+
if (invoked.type !== COMPOUND && invoked.type !== 'atom') return;
|
|
1393
|
+
let count = fastCountPureGoal(solver, invoked, env);
|
|
1394
|
+
if (count != null) {
|
|
1395
|
+
// findall/3's existing collector intentionally carries a ten-million
|
|
1396
|
+
// solution safety cap. Preserve that observable bound in the fused path.
|
|
1397
|
+
if (count > 10000000n) count = 10000000n;
|
|
1398
|
+
} else {
|
|
1399
|
+
const child = solver.cloneForInnerGoal(10000000);
|
|
1400
|
+
count = 0n;
|
|
1401
|
+
try {
|
|
1402
|
+
for (const _ of child.solve([invoked], env.clone(), 0)) count++;
|
|
1403
|
+
} finally {
|
|
1404
|
+
solver.absorbStatsFrom(child);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
const next = env.clone();
|
|
1408
|
+
if (unify(countArg, numberTerm(count), next)) yield next;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
function findallLengthFusion(solver, goal, rest, env) {
|
|
1412
|
+
if (goal.type !== COMPOUND || goal.name !== 'findall' || goal.arity !== 3 || rest.length === 0) return null;
|
|
1413
|
+
const bag = derefForLocal(goal.args[2], env);
|
|
1414
|
+
if (bag.type !== 'var') return null;
|
|
1415
|
+
|
|
1416
|
+
const lengthGoal = derefForLocal(rest[0], env);
|
|
1417
|
+
if (lengthGoal.type !== COMPOUND || lengthGoal.name !== 'length' || lengthGoal.arity !== 2) return null;
|
|
1418
|
+
const lengthGroup = solver.program.findGroup('length', 2, lengthGoal.module ?? 'user');
|
|
1419
|
+
if (!lengthGroup || lengthGroup.bundledLibrary !== true ||
|
|
1420
|
+
!['lists', 'prologue'].includes(lengthGroup.module)) return null;
|
|
1421
|
+
const lengthList = derefForLocal(lengthGoal.args[0], env);
|
|
1422
|
+
const countArg = derefForLocal(lengthGoal.args[1], env);
|
|
1423
|
+
if (lengthList.type !== 'var' || lengthList.name !== bag.name || countArg.type !== 'var') return null;
|
|
1424
|
+
if (envHasObservableAliasTo(bag.name, env)) return null;
|
|
1425
|
+
|
|
1426
|
+
// The bag may be elided only when no other part of the computation can
|
|
1427
|
+
// observe it. In particular, sharing it with Template or Goal changes
|
|
1428
|
+
// findall/3's variable-scoping behavior and must stay on the ordinary path.
|
|
1429
|
+
if (termReferencesResolvedVariable(goal.args[0], bag.name, env) ||
|
|
1430
|
+
termReferencesResolvedVariable(goal.args[1], bag.name, env) ||
|
|
1431
|
+
termReferencesResolvedVariable(lengthGoal.args[1], bag.name, env)) return null;
|
|
1432
|
+
for (let i = 1; i < rest.length; i++) {
|
|
1433
|
+
if (termReferencesResolvedVariable(rest[i], bag.name, env)) return null;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
return {
|
|
1437
|
+
iterator: fusedFindallLengthSolutions(solver, goal.args[1], lengthGoal.args[1], env),
|
|
1438
|
+
rest: rest.slice(1),
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
function countPlanTermIsFlat(term) {
|
|
1443
|
+
return term?.type === 'var' || isScalarTerm(term);
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function expandPureCountGoal(solver, goal, callerModule, visiting, budget) {
|
|
1447
|
+
if ((goal.type !== COMPOUND && goal.type !== 'atom') ||
|
|
1448
|
+
!(goal.args ?? []).every(countPlanTermIsFlat)) return null;
|
|
1449
|
+
const group = solver.program.findGroup(goal.name, goal.arity, goal.module ?? callerModule ?? 'user');
|
|
1450
|
+
if (!group || group.hasCut || group.recursive || group.tabled || group.wfsDatalog || group.datalogLeastModel) return null;
|
|
1451
|
+
if (group.scalarFactsOnly) return [{ equalities: [], leaves: [{ goal, group }] }];
|
|
1452
|
+
|
|
1453
|
+
const key = `${group.module}:${group.name}/${group.arity}`;
|
|
1454
|
+
if (visiting.has(key)) return null;
|
|
1455
|
+
const nextVisiting = new Set(visiting);
|
|
1456
|
+
nextVisiting.add(key);
|
|
1457
|
+
const out = [];
|
|
1458
|
+
|
|
1459
|
+
for (const clause of group.clauses) {
|
|
1460
|
+
if (budget.remaining-- <= 0) return null;
|
|
1461
|
+
const id = nextFreshId();
|
|
1462
|
+
const variables = new Map();
|
|
1463
|
+
const head = freshTerm(clause.head, `count${id}`, variables);
|
|
1464
|
+
const body = clause.body.map((term) => freshTerm(term, `count${id}`, variables));
|
|
1465
|
+
if (!(head.args ?? []).every(countPlanTermIsFlat)) return null;
|
|
1466
|
+
let branches = [{
|
|
1467
|
+
equalities: (goal.args ?? []).map((arg, index) => [arg, head.args[index]]),
|
|
1468
|
+
leaves: [],
|
|
1469
|
+
}];
|
|
1470
|
+
|
|
1471
|
+
for (const bodyGoal of body) {
|
|
1472
|
+
const expanded = expandPureCountGoal(solver, bodyGoal, group.module, nextVisiting, budget);
|
|
1473
|
+
if (expanded == null) return null;
|
|
1474
|
+
const combined = [];
|
|
1475
|
+
for (const left of branches) {
|
|
1476
|
+
for (const right of expanded) {
|
|
1477
|
+
combined.push({
|
|
1478
|
+
equalities: [...left.equalities, ...right.equalities],
|
|
1479
|
+
leaves: [...left.leaves, ...right.leaves],
|
|
1480
|
+
});
|
|
1481
|
+
if (combined.length > 4096) return null;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
branches = combined;
|
|
1485
|
+
}
|
|
1486
|
+
out.push(...branches);
|
|
1487
|
+
if (out.length > 4096) return null;
|
|
1488
|
+
}
|
|
1489
|
+
return out;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
function countPlanDeref(term, env, bindings) {
|
|
1493
|
+
let current = term;
|
|
1494
|
+
const seen = new Set();
|
|
1495
|
+
while (current?.type === 'var') {
|
|
1496
|
+
if (seen.has(current.name)) break;
|
|
1497
|
+
seen.add(current.name);
|
|
1498
|
+
const local = bindings.get(current.name);
|
|
1499
|
+
if (local !== undefined) {
|
|
1500
|
+
current = local;
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1503
|
+
const outer = env.get(current.name);
|
|
1504
|
+
if (outer !== undefined) {
|
|
1505
|
+
current = outer;
|
|
1506
|
+
continue;
|
|
1507
|
+
}
|
|
1508
|
+
break;
|
|
1509
|
+
}
|
|
1510
|
+
return current;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function countPlanUnify(left, right, env, bindings, trail) {
|
|
1514
|
+
const a = countPlanDeref(left, env, bindings);
|
|
1515
|
+
const b = countPlanDeref(right, env, bindings);
|
|
1516
|
+
if (a.type === 'var') {
|
|
1517
|
+
if (b.type === 'var' && a.name === b.name) return true;
|
|
1518
|
+
bindings.set(a.name, b);
|
|
1519
|
+
trail.push(a.name);
|
|
1520
|
+
return true;
|
|
1521
|
+
}
|
|
1522
|
+
if (b.type === 'var') {
|
|
1523
|
+
bindings.set(b.name, a);
|
|
1524
|
+
trail.push(b.name);
|
|
1525
|
+
return true;
|
|
1526
|
+
}
|
|
1527
|
+
return sameScalarTerm(a, b);
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
function undoCountBindings(bindings, trail, start) {
|
|
1531
|
+
for (let i = trail.length - 1; i >= start; i--) bindings.delete(trail[i]);
|
|
1532
|
+
trail.length = start;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
function countLeafCandidateParts(leaf, env, bindings) {
|
|
1536
|
+
const positions = [];
|
|
1537
|
+
const values = [];
|
|
1538
|
+
for (let i = 0; i < leaf.goal.arity; i++) {
|
|
1539
|
+
const value = countPlanDeref(leaf.goal.args[i], env, bindings);
|
|
1540
|
+
if (!isScalarTerm(value)) continue;
|
|
1541
|
+
positions.push(i);
|
|
1542
|
+
values.push(value);
|
|
1543
|
+
}
|
|
1544
|
+
return selectClauseCandidatesForValues(leaf.group, positions, values);
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
function countCandidateTotal(parts) {
|
|
1548
|
+
return clauseCandidateLength(parts.primary) + clauseCandidateLength(parts.fallback);
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
function fastCountBranch(branch, env) {
|
|
1552
|
+
const bindings = new Map();
|
|
1553
|
+
const trail = [];
|
|
1554
|
+
for (const [left, right] of branch.equalities) {
|
|
1555
|
+
if (!countPlanUnify(left, right, env, bindings, trail)) return 0n;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
let numberCount = 0;
|
|
1559
|
+
let bigCount = null;
|
|
1560
|
+
const increment = () => {
|
|
1561
|
+
if (bigCount != null) {
|
|
1562
|
+
bigCount++;
|
|
1563
|
+
} else if (numberCount < Number.MAX_SAFE_INTEGER) {
|
|
1564
|
+
numberCount++;
|
|
1565
|
+
} else {
|
|
1566
|
+
bigCount = BigInt(numberCount) + 1n;
|
|
1567
|
+
}
|
|
1568
|
+
};
|
|
1569
|
+
|
|
1570
|
+
const remaining = branch.leaves.map((_, index) => index);
|
|
1571
|
+
const visit = (active) => {
|
|
1572
|
+
if (active.length === 0) {
|
|
1573
|
+
increment();
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
let bestPosition = 0;
|
|
1578
|
+
let bestParts = null;
|
|
1579
|
+
let bestLength = Infinity;
|
|
1580
|
+
for (let position = 0; position < active.length; position++) {
|
|
1581
|
+
const leaf = branch.leaves[active[position]];
|
|
1582
|
+
const parts = countLeafCandidateParts(leaf, env, bindings);
|
|
1583
|
+
const length = countCandidateTotal(parts);
|
|
1584
|
+
if (length < bestLength) {
|
|
1585
|
+
bestLength = length;
|
|
1586
|
+
bestPosition = position;
|
|
1587
|
+
bestParts = parts;
|
|
1588
|
+
if (length === 0) return;
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
const leafIndex = active[bestPosition];
|
|
1593
|
+
const leaf = branch.leaves[leafIndex];
|
|
1594
|
+
const nextActive = active.length === 1
|
|
1595
|
+
? []
|
|
1596
|
+
: [...active.slice(0, bestPosition), ...active.slice(bestPosition + 1)];
|
|
1597
|
+
for (const pass of [bestParts.primary, bestParts.fallback]) {
|
|
1598
|
+
for (let candidateIndex = 0; candidateIndex < clauseCandidateLength(pass); candidateIndex++) {
|
|
1599
|
+
const clause = clauseCandidateAt(pass, candidateIndex);
|
|
1600
|
+
const start = trail.length;
|
|
1601
|
+
let ok = true;
|
|
1602
|
+
for (let i = 0; i < leaf.goal.arity; i++) {
|
|
1603
|
+
if (!countPlanUnify(leaf.goal.args[i], clause.head.args[i], env, bindings, trail)) {
|
|
1604
|
+
ok = false;
|
|
1605
|
+
break;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
if (ok) visit(nextActive);
|
|
1609
|
+
undoCountBindings(bindings, trail, start);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
|
|
1614
|
+
visit(remaining);
|
|
1615
|
+
return bigCount ?? BigInt(numberCount);
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
function fastCountPureGoal(solver, goal, env) {
|
|
1619
|
+
if (solver.isoStrict || solver.solutionLimit !== Infinity || solver.maxInferences !== Infinity ||
|
|
1620
|
+
env._clpz != null || (env._delays != null && env._delays.size !== 0)) return null;
|
|
1621
|
+
const budget = { remaining: 8192 };
|
|
1622
|
+
const branches = expandPureCountGoal(solver, goal, goal.module ?? 'user', new Set(), budget);
|
|
1623
|
+
if (branches == null) return null;
|
|
1624
|
+
let count = 0n;
|
|
1625
|
+
for (const branch of branches) count += fastCountBranch(branch, env);
|
|
1626
|
+
return count;
|
|
1627
|
+
}
|
|
912
1628
|
|
|
913
1629
|
function tryPushScalarFactRunFrames(stack, solver, goals, env, depth, active) {
|
|
914
|
-
// Consecutive lookups
|
|
915
|
-
// are
|
|
916
|
-
//
|
|
917
|
-
// cloned Env maps.
|
|
1630
|
+
// Consecutive scalar-fact lookups are common in data-heavy joins. Short
|
|
1631
|
+
// joins are fastest when their continuation frames are materialized locally;
|
|
1632
|
+
// wide joins can explode combinatorially, so they are streamed lazily.
|
|
918
1633
|
let runLength = 0;
|
|
919
1634
|
const groups = [];
|
|
920
1635
|
while (runLength < goals.length) {
|
|
@@ -930,10 +1645,16 @@ function tryPushScalarFactRunFrames(stack, solver, goals, env, depth, active) {
|
|
|
930
1645
|
}
|
|
931
1646
|
if (runLength < 2) return false;
|
|
932
1647
|
|
|
1648
|
+
const runGoals = goals.slice(0, runLength);
|
|
933
1649
|
const rest = goals.slice(runLength);
|
|
1650
|
+
if (runLength >= 4) {
|
|
1651
|
+
pushStreamingScalarFactRun(stack, solver, runGoals, groups, rest, env, depth, active);
|
|
1652
|
+
return true;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
934
1655
|
const localStack = [{ index: 0, names: [], values: [], depth }];
|
|
935
1656
|
const frames = [];
|
|
936
|
-
|
|
1657
|
+
const frameLimit = 100000;
|
|
937
1658
|
while (localStack.length) {
|
|
938
1659
|
const state = localStack.pop();
|
|
939
1660
|
solver.stats.max_depth = Math.max(solver.stats.max_depth, state.depth);
|
|
@@ -941,11 +1662,17 @@ function tryPushScalarFactRunFrames(stack, solver, goals, env, depth, active) {
|
|
|
941
1662
|
const next = env.clone();
|
|
942
1663
|
for (let i = 0; i < state.names.length; i++) next.bind(state.names[i], state.values[i]);
|
|
943
1664
|
frames.push({ kind: 'goals', goals: rest, env: next, depth: state.depth, active });
|
|
944
|
-
if (frames.length >
|
|
1665
|
+
if (frames.length > frameLimit) {
|
|
1666
|
+
// Do not repeat the old bug of abandoning the optimization and then
|
|
1667
|
+
// re-solving through the generic engine. Restart this rare oversized
|
|
1668
|
+
// short join as a streaming local join instead.
|
|
1669
|
+
pushStreamingScalarFactRun(stack, solver, runGoals, groups, rest, env, depth, active);
|
|
1670
|
+
return true;
|
|
1671
|
+
}
|
|
945
1672
|
continue;
|
|
946
1673
|
}
|
|
947
1674
|
|
|
948
|
-
const goal =
|
|
1675
|
+
const goal = runGoals[state.index];
|
|
949
1676
|
if (activeMightContain(goal, active) && activeVariantIn(goal, envWithLocal(env, state.names, state.values), active)) continue;
|
|
950
1677
|
solver.stats.solve_one_goal_calls++;
|
|
951
1678
|
const candidates = selectScalarFactCandidates(groups[state.index], goal, env, state.names, state.values);
|
|
@@ -967,6 +1694,57 @@ function tryPushScalarFactRunFrames(stack, solver, goals, env, depth, active) {
|
|
|
967
1694
|
return true;
|
|
968
1695
|
}
|
|
969
1696
|
|
|
1697
|
+
function pushStreamingScalarFactRun(stack, solver, runGoals, groups, rest, env, depth, active) {
|
|
1698
|
+
const iterator = scalarFactRunSolutions(solver, runGoals, groups, env, depth, active);
|
|
1699
|
+
const first = iterator.next();
|
|
1700
|
+
if (first.done) return;
|
|
1701
|
+
stack.push({
|
|
1702
|
+
kind: 'resumeBuiltin',
|
|
1703
|
+
iterator,
|
|
1704
|
+
goals: rest,
|
|
1705
|
+
depth: depth + runGoals.length,
|
|
1706
|
+
active,
|
|
1707
|
+
});
|
|
1708
|
+
stack.push({
|
|
1709
|
+
kind: 'goals',
|
|
1710
|
+
goals: rest,
|
|
1711
|
+
env: first.value,
|
|
1712
|
+
depth: depth + runGoals.length,
|
|
1713
|
+
active,
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
function* scalarFactRunSolutions(solver, goals, groups, env, depth, active) {
|
|
1718
|
+
const localStack = [{ index: 0, names: [], values: [] }];
|
|
1719
|
+
while (localStack.length) {
|
|
1720
|
+
const state = localStack.pop();
|
|
1721
|
+
solver.stats.max_depth = Math.max(solver.stats.max_depth, depth + state.index);
|
|
1722
|
+
if (state.index === goals.length) {
|
|
1723
|
+
const next = env.clone();
|
|
1724
|
+
for (let i = 0; i < state.names.length; i++) next.bind(state.names[i], state.values[i]);
|
|
1725
|
+
yield next;
|
|
1726
|
+
continue;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
const goal = goals[state.index];
|
|
1730
|
+
if (activeMightContain(goal, active) && activeVariantIn(goal, envWithLocal(env, state.names, state.values), active)) continue;
|
|
1731
|
+
solver.stats.solve_one_goal_calls++;
|
|
1732
|
+
const candidates = selectScalarFactCandidates(groups[state.index], goal, env, state.names, state.values);
|
|
1733
|
+
const nextStates = [];
|
|
1734
|
+
for (const pass of [candidates.primary, candidates.fallback]) {
|
|
1735
|
+
for (let candidateIndex = 0; candidateIndex < clauseCandidateLength(pass); candidateIndex++) {
|
|
1736
|
+
const clause = clauseCandidateAt(pass, candidateIndex);
|
|
1737
|
+
const match = matchScalarFactLocal(goal, clause.head, env, state.names, state.values);
|
|
1738
|
+
if (!match) continue;
|
|
1739
|
+
solver.stats.unify_calls++;
|
|
1740
|
+
nextStates.push({ index: state.index + 1, names: match.names, values: match.values });
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
for (let i = nextStates.length - 1; i >= 0; i--) localStack.push(nextStates[i]);
|
|
1744
|
+
if (solver.solutionsSeen >= solver.solutionLimit) return;
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
|
|
970
1748
|
|
|
971
1749
|
function activeMightContain(goal, active) {
|
|
972
1750
|
if (active.length === 0 || goal.type !== COMPOUND) return false;
|
|
@@ -1146,6 +1924,65 @@ function tryPushCompactBinaryChainFrames(stack, solver, group, goal, rest, env,
|
|
|
1146
1924
|
}
|
|
1147
1925
|
}
|
|
1148
1926
|
|
|
1927
|
+
function tryPushGroundScalarRuleFrame(stack, solver, group, goal, rest, env, depth, active) {
|
|
1928
|
+
// A fully-ground call to a single flat rule can be checked without freshening
|
|
1929
|
+
// the rule or allocating an Env for every body literal. This is especially
|
|
1930
|
+
// valuable for data-validation joins such as rb(A,B,C,D,E) in ORB Join2.
|
|
1931
|
+
if (!termIsGround(goal, env) || group.clauses.length !== 1) return false;
|
|
1932
|
+
const clause = group.clauses[0];
|
|
1933
|
+
if (clause?.compactBinary === true || clause.body.length < 2 || clause.head.type !== COMPOUND) return false;
|
|
1934
|
+
if (clause.head.name !== goal.name || clause.head.arity !== goal.arity) return false;
|
|
1935
|
+
|
|
1936
|
+
const bindings = new Map();
|
|
1937
|
+
const resolvedGoal = copyResolved(goal, env);
|
|
1938
|
+
for (let i = 0; i < clause.head.arity; i++) {
|
|
1939
|
+
const pattern = clause.head.args[i];
|
|
1940
|
+
const value = resolvedGoal.args[i];
|
|
1941
|
+
if (pattern.type === 'var') {
|
|
1942
|
+
const previous = bindings.get(pattern.name);
|
|
1943
|
+
if (previous == null) bindings.set(pattern.name, value);
|
|
1944
|
+
else if (!sameGroundTerm(previous, value)) return true;
|
|
1945
|
+
} else if (isScalarTerm(pattern)) {
|
|
1946
|
+
if (!sameGroundTerm(pattern, value)) return true;
|
|
1947
|
+
} else {
|
|
1948
|
+
return false;
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
for (const body of clause.body) {
|
|
1953
|
+
if (body.type !== COMPOUND) return false;
|
|
1954
|
+
const args = [];
|
|
1955
|
+
for (const arg of body.args) {
|
|
1956
|
+
if (arg.type === 'var') {
|
|
1957
|
+
const value = bindings.get(arg.name);
|
|
1958
|
+
if (value == null) return false;
|
|
1959
|
+
args.push(value);
|
|
1960
|
+
} else if (isScalarTerm(arg)) {
|
|
1961
|
+
args.push(arg);
|
|
1962
|
+
} else {
|
|
1963
|
+
return false;
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
const bodyGroup = solver.program.findGroup(body.name, body.arity, body.module ?? group.module);
|
|
1967
|
+
if (!bodyGroup || bodyGroup.tabled || !bodyGroup.scalarFactsOnly) return false;
|
|
1968
|
+
const bodyGoal = compound(body.name, args);
|
|
1969
|
+
const candidates = selectGroundClauseCandidates(bodyGroup, bodyGoal);
|
|
1970
|
+
let matches = 0;
|
|
1971
|
+
for (let candidateIndex = 0; candidateIndex < clauseCandidateLength(candidates); candidateIndex++) {
|
|
1972
|
+
const fact = clauseCandidateAt(candidates, candidateIndex);
|
|
1973
|
+
const matched = matchGroundClause(bodyGoal, fact);
|
|
1974
|
+
if (matched?.done) {
|
|
1975
|
+
matches++;
|
|
1976
|
+
if (matches > 1) return false; // Preserve duplicate-proof semantics.
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
if (matches === 0) return true;
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
stack.push({ kind: 'goals', goals: rest, env, depth: depth + 1, active });
|
|
1983
|
+
return true;
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1149
1986
|
function tryPushGroundChainFrames(stack, solver, group, goal, rest, env, depth, active) {
|
|
1150
1987
|
if (tryPushCompactBinaryChainFrames(stack, solver, group, goal, rest, env, depth, active)) return true;
|
|
1151
1988
|
// Compress deterministic ground single-goal chains such as deep taxonomy
|
|
@@ -1358,7 +2195,20 @@ function rememberMemoAnswer(entry, goal, env) {
|
|
|
1358
2195
|
const key = answerKeys.join('\x1f');
|
|
1359
2196
|
if (entry.answerKeys.has(key)) return;
|
|
1360
2197
|
entry.answerKeys.add(key);
|
|
2198
|
+
const answerIndex = entry.answers.length;
|
|
1361
2199
|
entry.answers.push(answerArgs);
|
|
2200
|
+
for (let position = 0; position < answerArgs.length; position++) {
|
|
2201
|
+
const value = answerArgs[position];
|
|
2202
|
+
const scalarKey = memoAnswerScalarKey(value);
|
|
2203
|
+
if (scalarKey != null) {
|
|
2204
|
+
const index = entry.answerIndexes[position];
|
|
2205
|
+
let bucket = index.get(scalarKey);
|
|
2206
|
+
if (bucket == null) index.set(scalarKey, bucket = []);
|
|
2207
|
+
bucket.push(answerIndex);
|
|
2208
|
+
} else if (value.type === 'var') {
|
|
2209
|
+
entry.answerVariableFallbacks[position].push(answerIndex);
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
1362
2212
|
}
|
|
1363
2213
|
|
|
1364
2214
|
function activeVariantIn(goal, env, active) {
|
|
@@ -1490,19 +2340,13 @@ function derefForLocal(term, env) {
|
|
|
1490
2340
|
function memoKey(goal, env, group = null) {
|
|
1491
2341
|
const required = group?.tableInputPositions ?? [];
|
|
1492
2342
|
const ground = goal.args.map((arg) => termIsGround(arg, env));
|
|
1493
|
-
const hasBound = required.length > 0
|
|
2343
|
+
const hasBound = group?.tableAllVariants === true || (required.length > 0
|
|
1494
2344
|
? required.some((index) => ground[index])
|
|
1495
|
-
: ground.some(Boolean);
|
|
1496
|
-
// Automatic tabling only admits calls with a ground input. Avoid building a
|
|
1497
|
-
// potentially huge canonical key for the non-ground structural calls that
|
|
1498
|
-
// will use the ordinary active-call guard instead.
|
|
2345
|
+
: ground.some(Boolean));
|
|
1499
2346
|
if (!hasBound) return { hasBound: false, text: '' };
|
|
1500
2347
|
|
|
1501
2348
|
const variables = new Map();
|
|
1502
|
-
const parts = goal.args.map((arg) =>
|
|
1503
|
-
const value = derefForLocal(arg, env);
|
|
1504
|
-
return value.type === 'var' ? '_' : canonicalTermKey(value, env, variables);
|
|
1505
|
-
});
|
|
2349
|
+
const parts = goal.args.map((arg) => canonicalTermKey(arg, env, variables));
|
|
1506
2350
|
return { hasBound, text: parts.join('|') };
|
|
1507
2351
|
}
|
|
1508
2352
|
|
|
@@ -1553,10 +2397,10 @@ function copyResolvedWithKey(term, env, variables) {
|
|
|
1553
2397
|
return { term: termModuleCache.variable(value.name), key: `var:${id}` };
|
|
1554
2398
|
}
|
|
1555
2399
|
if (!value.args?.length) {
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
};
|
|
2400
|
+
// Atomic terms are immutable in the solver. Share them across table
|
|
2401
|
+
// answers instead of allocating a fresh host object for every cell of a
|
|
2402
|
+
// large closure such as tc/2.
|
|
2403
|
+
return { term: value, key: `${value.type}:${value.name}` };
|
|
1560
2404
|
}
|
|
1561
2405
|
const children = value.args.map((arg) => copyResolvedWithKey(arg, env, variables));
|
|
1562
2406
|
return {
|