eyeprolog 1.3.4 → 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 +66 -0
- package/examples/book/README.md +2 -1
- package/examples/book/chapter-07/02-move.pl +4 -0
- package/index.d.ts +7 -0
- package/package.json +1 -1
- package/src/datalog.js +354 -0
- package/src/iso.js +107 -9
- package/src/program.js +258 -20
- package/src/solver.js +841 -41
- package/src/standard-library.js +5 -0
- 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-regression.mjs +188 -1
- package/the-art-of-eyeprolog.md +70 -8
- 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,6 +474,31 @@ export class Solver {
|
|
|
374
474
|
}
|
|
375
475
|
qualifyMetaArguments(goal, group);
|
|
376
476
|
|
|
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
|
+
|
|
377
502
|
const memberIterator = bundledMemberIterator(this, group, goal, env);
|
|
378
503
|
if (memberIterator != null) {
|
|
379
504
|
const firstResult = memberIterator.next();
|
|
@@ -408,14 +533,29 @@ export class Solver {
|
|
|
408
533
|
continue;
|
|
409
534
|
}
|
|
410
535
|
|
|
411
|
-
if (group.tabled) {
|
|
536
|
+
if (group.tabled && !(this.skipListTailTabling && group.listTailRecursive)) {
|
|
412
537
|
const key = memoKey(goal, env, group);
|
|
413
538
|
if (key.hasBound) {
|
|
414
539
|
const mapKey = `${group.module}:${goal.name}/${goal.arity}:${key.text}`;
|
|
415
|
-
|
|
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
|
+
}
|
|
416
553
|
if (!entry) {
|
|
417
|
-
entry =
|
|
418
|
-
|
|
554
|
+
entry = this.memo.get(mapKey);
|
|
555
|
+
if (!entry) {
|
|
556
|
+
entry = makeMemoEntry(goal.arity);
|
|
557
|
+
this.memo.set(mapKey, entry);
|
|
558
|
+
}
|
|
419
559
|
}
|
|
420
560
|
if (this.tableCoordinator) this.tableCoordinator.entries.add(entry);
|
|
421
561
|
if (entry.complete) {
|
|
@@ -433,7 +573,7 @@ export class Solver {
|
|
|
433
573
|
}
|
|
434
574
|
break;
|
|
435
575
|
}
|
|
436
|
-
if (this.tableCoordinator && activeVariantIn(goal, env, active)) {
|
|
576
|
+
if (this.tableCoordinator && (usingBroadTable || activeVariantIn(goal, env, active))) {
|
|
437
577
|
this.tableCoordinator.cycleSeen = true;
|
|
438
578
|
}
|
|
439
579
|
pushMemoAnswerFrames(stack, entry, goal, rest, env, depth, active, this);
|
|
@@ -466,6 +606,84 @@ export class Solver {
|
|
|
466
606
|
}
|
|
467
607
|
}
|
|
468
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
|
+
|
|
469
687
|
activeVariant(goal, env) {
|
|
470
688
|
return activeVariantIn(goal, env, this.active);
|
|
471
689
|
}
|
|
@@ -525,6 +743,14 @@ export class Solver {
|
|
|
525
743
|
const group = this.program.findGroup(goal.name, goal.arity, goal.module ?? 'user');
|
|
526
744
|
if (!group) return;
|
|
527
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
|
+
}
|
|
528
754
|
if (group.tabled) {
|
|
529
755
|
yield* this.solveMemoizedGoal(group, goal, rest, env, depth);
|
|
530
756
|
return;
|
|
@@ -532,6 +758,15 @@ export class Solver {
|
|
|
532
758
|
yield* this.solveUserGoalUncached(group, goal, rest, env, depth);
|
|
533
759
|
}
|
|
534
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
|
+
|
|
535
770
|
*solveMemoizedGoal(group, goal, rest, env, depth) {
|
|
536
771
|
yield* this.solve([goal, ...rest], env, depth);
|
|
537
772
|
}
|
|
@@ -641,8 +876,30 @@ function defaultPrologFlags(unknown = 'error', strictIso = false) {
|
|
|
641
876
|
}
|
|
642
877
|
|
|
643
878
|
|
|
644
|
-
function makeMemoEntry() {
|
|
645
|
-
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;
|
|
646
903
|
}
|
|
647
904
|
|
|
648
905
|
function scheduleTableFixpointRound(stack, solver, frame) {
|
|
@@ -680,10 +937,19 @@ function scheduleTableFixpointRound(stack, solver, frame) {
|
|
|
680
937
|
|
|
681
938
|
|
|
682
939
|
function pushMemoAnswerFrames(stack, entry, goal, rest, env, depth, active, solver) {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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) => {
|
|
687
953
|
const storedArgs = entry.answers[answerIndex];
|
|
688
954
|
const answerArgs = storedArgs.every((arg) => termIsGround(arg))
|
|
689
955
|
? storedArgs
|
|
@@ -695,12 +961,19 @@ function pushMemoAnswerFrames(stack, entry, goal, rest, env, depth, active, solv
|
|
|
695
961
|
if (!unify(goal.args[i], answerArgs[i], next)) { ok = false; break; }
|
|
696
962
|
}
|
|
697
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;
|
|
698
969
|
}
|
|
970
|
+
for (let answerIndex = entry.answers.length - 1; answerIndex >= 0; answerIndex--) replay(answerIndex);
|
|
699
971
|
}
|
|
700
972
|
|
|
701
973
|
function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth, active) {
|
|
702
974
|
if (group.recursive && !group.cutRecursive && !group.linearNumeric && activeVariantIn(goal, env, active)) return;
|
|
703
975
|
if (group.fastPi && pushFastPiFrames(stack, goal, rest, env, depth, active)) return;
|
|
976
|
+
if (tryPushGroundScalarRuleFrame(stack, solver, group, goal, rest, env, depth, active)) return;
|
|
704
977
|
if (tryPushGroundChainFrames(stack, solver, group, goal, rest, env, depth, active)) return;
|
|
705
978
|
const candidates = selectClauseCandidates(group, goal, env);
|
|
706
979
|
const frames = [];
|
|
@@ -774,6 +1047,49 @@ function groupNeedsActiveFrame(group) {
|
|
|
774
1047
|
return group.cutReachable !== false || (group.recursive && !group.linearNumeric);
|
|
775
1048
|
}
|
|
776
1049
|
|
|
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
|
+
|
|
777
1093
|
function bundledMemberIterator(solver, group, goal, env) {
|
|
778
1094
|
if (solver.registry.eyePrologLibrary !== true ||
|
|
779
1095
|
!['lists', 'prologue'].includes(group.module) || group.name !== 'member' || group.arity !== 2 ||
|
|
@@ -815,12 +1131,17 @@ function bundledLengthIterator(solver, group, goal, env) {
|
|
|
815
1131
|
if (length.type === VAR && env._delays?.has(length.name)) return null;
|
|
816
1132
|
|
|
817
1133
|
let cursor = deref(goal.args[0], env);
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
if (cursor
|
|
822
|
-
|
|
823
|
-
|
|
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
|
+
}
|
|
824
1145
|
}
|
|
825
1146
|
return bundledLengthSolutions(solver, goal, env);
|
|
826
1147
|
}
|
|
@@ -842,6 +1163,11 @@ function* bundledLengthSolutions(solver, goal, env) {
|
|
|
842
1163
|
|
|
843
1164
|
function* fixedLengthSolutions(solver, list, length, env) {
|
|
844
1165
|
let cursor = deref(list, env);
|
|
1166
|
+
const compactLength = compactListLength(cursor);
|
|
1167
|
+
if (compactLength != null) {
|
|
1168
|
+
if (compactLength === length) yield env;
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
845
1171
|
let remaining = length;
|
|
846
1172
|
let steps = 0n;
|
|
847
1173
|
while (isCons(cursor)) {
|
|
@@ -882,6 +1208,12 @@ function* fixedLengthSolutions(solver, list, length, env) {
|
|
|
882
1208
|
|
|
883
1209
|
function* generatedLengthSolutions(solver, list, length, env) {
|
|
884
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
|
+
}
|
|
885
1217
|
let count = 0n;
|
|
886
1218
|
let steps = 0n;
|
|
887
1219
|
while (isCons(cursor)) {
|
|
@@ -952,13 +1284,352 @@ function pushFastPiFrames(stack, goal, rest, env, depth, active) {
|
|
|
952
1284
|
|
|
953
1285
|
|
|
954
1286
|
|
|
955
|
-
|
|
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
|
+
}
|
|
956
1628
|
|
|
957
1629
|
function tryPushScalarFactRunFrames(stack, solver, goals, env, depth, active) {
|
|
958
|
-
// Consecutive lookups
|
|
959
|
-
// are
|
|
960
|
-
//
|
|
961
|
-
// 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.
|
|
962
1633
|
let runLength = 0;
|
|
963
1634
|
const groups = [];
|
|
964
1635
|
while (runLength < goals.length) {
|
|
@@ -974,10 +1645,16 @@ function tryPushScalarFactRunFrames(stack, solver, goals, env, depth, active) {
|
|
|
974
1645
|
}
|
|
975
1646
|
if (runLength < 2) return false;
|
|
976
1647
|
|
|
1648
|
+
const runGoals = goals.slice(0, runLength);
|
|
977
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
|
+
|
|
978
1655
|
const localStack = [{ index: 0, names: [], values: [], depth }];
|
|
979
1656
|
const frames = [];
|
|
980
|
-
|
|
1657
|
+
const frameLimit = 100000;
|
|
981
1658
|
while (localStack.length) {
|
|
982
1659
|
const state = localStack.pop();
|
|
983
1660
|
solver.stats.max_depth = Math.max(solver.stats.max_depth, state.depth);
|
|
@@ -985,11 +1662,17 @@ function tryPushScalarFactRunFrames(stack, solver, goals, env, depth, active) {
|
|
|
985
1662
|
const next = env.clone();
|
|
986
1663
|
for (let i = 0; i < state.names.length; i++) next.bind(state.names[i], state.values[i]);
|
|
987
1664
|
frames.push({ kind: 'goals', goals: rest, env: next, depth: state.depth, active });
|
|
988
|
-
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
|
+
}
|
|
989
1672
|
continue;
|
|
990
1673
|
}
|
|
991
1674
|
|
|
992
|
-
const goal =
|
|
1675
|
+
const goal = runGoals[state.index];
|
|
993
1676
|
if (activeMightContain(goal, active) && activeVariantIn(goal, envWithLocal(env, state.names, state.values), active)) continue;
|
|
994
1677
|
solver.stats.solve_one_goal_calls++;
|
|
995
1678
|
const candidates = selectScalarFactCandidates(groups[state.index], goal, env, state.names, state.values);
|
|
@@ -1011,6 +1694,57 @@ function tryPushScalarFactRunFrames(stack, solver, goals, env, depth, active) {
|
|
|
1011
1694
|
return true;
|
|
1012
1695
|
}
|
|
1013
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
|
+
|
|
1014
1748
|
|
|
1015
1749
|
function activeMightContain(goal, active) {
|
|
1016
1750
|
if (active.length === 0 || goal.type !== COMPOUND) return false;
|
|
@@ -1190,6 +1924,65 @@ function tryPushCompactBinaryChainFrames(stack, solver, group, goal, rest, env,
|
|
|
1190
1924
|
}
|
|
1191
1925
|
}
|
|
1192
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
|
+
|
|
1193
1986
|
function tryPushGroundChainFrames(stack, solver, group, goal, rest, env, depth, active) {
|
|
1194
1987
|
if (tryPushCompactBinaryChainFrames(stack, solver, group, goal, rest, env, depth, active)) return true;
|
|
1195
1988
|
// Compress deterministic ground single-goal chains such as deep taxonomy
|
|
@@ -1402,7 +2195,20 @@ function rememberMemoAnswer(entry, goal, env) {
|
|
|
1402
2195
|
const key = answerKeys.join('\x1f');
|
|
1403
2196
|
if (entry.answerKeys.has(key)) return;
|
|
1404
2197
|
entry.answerKeys.add(key);
|
|
2198
|
+
const answerIndex = entry.answers.length;
|
|
1405
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
|
+
}
|
|
1406
2212
|
}
|
|
1407
2213
|
|
|
1408
2214
|
function activeVariantIn(goal, env, active) {
|
|
@@ -1534,19 +2340,13 @@ function derefForLocal(term, env) {
|
|
|
1534
2340
|
function memoKey(goal, env, group = null) {
|
|
1535
2341
|
const required = group?.tableInputPositions ?? [];
|
|
1536
2342
|
const ground = goal.args.map((arg) => termIsGround(arg, env));
|
|
1537
|
-
const hasBound = required.length > 0
|
|
2343
|
+
const hasBound = group?.tableAllVariants === true || (required.length > 0
|
|
1538
2344
|
? required.some((index) => ground[index])
|
|
1539
|
-
: ground.some(Boolean);
|
|
1540
|
-
// Automatic tabling only admits calls with a ground input. Avoid building a
|
|
1541
|
-
// potentially huge canonical key for the non-ground structural calls that
|
|
1542
|
-
// will use the ordinary active-call guard instead.
|
|
2345
|
+
: ground.some(Boolean));
|
|
1543
2346
|
if (!hasBound) return { hasBound: false, text: '' };
|
|
1544
2347
|
|
|
1545
2348
|
const variables = new Map();
|
|
1546
|
-
const parts = goal.args.map((arg) =>
|
|
1547
|
-
const value = derefForLocal(arg, env);
|
|
1548
|
-
return value.type === 'var' ? '_' : canonicalTermKey(value, env, variables);
|
|
1549
|
-
});
|
|
2349
|
+
const parts = goal.args.map((arg) => canonicalTermKey(arg, env, variables));
|
|
1550
2350
|
return { hasBound, text: parts.join('|') };
|
|
1551
2351
|
}
|
|
1552
2352
|
|
|
@@ -1597,10 +2397,10 @@ function copyResolvedWithKey(term, env, variables) {
|
|
|
1597
2397
|
return { term: termModuleCache.variable(value.name), key: `var:${id}` };
|
|
1598
2398
|
}
|
|
1599
2399
|
if (!value.args?.length) {
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
};
|
|
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}` };
|
|
1604
2404
|
}
|
|
1605
2405
|
const children = value.args.map((arg) => copyResolvedWithKey(arg, env, variables));
|
|
1606
2406
|
return {
|