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/iso.js
CHANGED
|
@@ -1734,17 +1734,85 @@ function numberListBuiltin(kind) {
|
|
|
1734
1734
|
const numberCharsBuiltin = numberListBuiltin('chars');
|
|
1735
1735
|
const numberCodesBuiltin = numberListBuiltin('codes');
|
|
1736
1736
|
|
|
1737
|
+
class FindallListTerm {
|
|
1738
|
+
constructor(items, offset = 0) {
|
|
1739
|
+
this.type = COMPOUND;
|
|
1740
|
+
this.name = '.';
|
|
1741
|
+
this._items = items;
|
|
1742
|
+
this._offset = offset;
|
|
1743
|
+
this._compactLength = BigInt(items.length - offset);
|
|
1744
|
+
this._args = null;
|
|
1745
|
+
}
|
|
1746
|
+
get arity() { return 2; }
|
|
1747
|
+
get args() {
|
|
1748
|
+
if (this._args == null) {
|
|
1749
|
+
const next = this._offset + 1;
|
|
1750
|
+
this._args = [
|
|
1751
|
+
typeof this._items.get === 'function' ? this._items.get(this._offset) : this._items[this._offset],
|
|
1752
|
+
next >= this._items.length ? emptyList() : new FindallListTerm(this._items, next),
|
|
1753
|
+
];
|
|
1754
|
+
}
|
|
1755
|
+
return this._args;
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
class FlatCompoundFindallItems {
|
|
1760
|
+
constructor(template) {
|
|
1761
|
+
this.name = template.name;
|
|
1762
|
+
this.arity = template.arity;
|
|
1763
|
+
this.columns = Array.from({ length: this.arity }, () => []);
|
|
1764
|
+
}
|
|
1765
|
+
get length() {
|
|
1766
|
+
return this.arity === 0 ? 0 : this.columns[0].length;
|
|
1767
|
+
}
|
|
1768
|
+
push(template, env) {
|
|
1769
|
+
const values = new Array(this.arity);
|
|
1770
|
+
for (let i = 0; i < this.arity; i++) {
|
|
1771
|
+
const value = deref(template.args[i], env);
|
|
1772
|
+
if (value.type === VAR || value.type === COMPOUND) return false;
|
|
1773
|
+
values[i] = value;
|
|
1774
|
+
}
|
|
1775
|
+
for (let i = 0; i < this.arity; i++) this.columns[i].push(values[i]);
|
|
1776
|
+
return true;
|
|
1777
|
+
}
|
|
1778
|
+
get(index) {
|
|
1779
|
+
return compound(this.name, this.columns.map((column) => column[index]));
|
|
1780
|
+
}
|
|
1781
|
+
materialize() {
|
|
1782
|
+
const out = new Array(this.length);
|
|
1783
|
+
for (let i = 0; i < out.length; i++) out[i] = this.get(i);
|
|
1784
|
+
return out;
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
function findallListFromItems(items) {
|
|
1789
|
+
return items.length === 0 ? emptyList() : new FindallListTerm(items);
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
function flatFindallTemplate(template) {
|
|
1793
|
+
return template.type === COMPOUND && template.arity > 0 &&
|
|
1794
|
+
template.args.every((arg) => arg.type !== COMPOUND)
|
|
1795
|
+
? new FlatCompoundFindallItems(template)
|
|
1796
|
+
: null;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1737
1799
|
function* findallBuiltin({ solver, goal, env }) {
|
|
1738
1800
|
const [template, innerGoal, bag] = goal.args;
|
|
1739
1801
|
assertListOrPartial(bag, env);
|
|
1740
1802
|
const collector = solver.cloneForInnerGoal(10000000);
|
|
1741
|
-
|
|
1803
|
+
let compact = flatFindallTemplate(template);
|
|
1804
|
+
let collected = compact == null ? [] : null;
|
|
1742
1805
|
for (const answerEnv of collector.solve([callable(innerGoal, env)], env.clone(), 0)) {
|
|
1806
|
+
if (compact != null && compact.push(template, answerEnv)) continue;
|
|
1807
|
+
if (compact != null) {
|
|
1808
|
+
collected = compact.materialize();
|
|
1809
|
+
compact = null;
|
|
1810
|
+
}
|
|
1743
1811
|
collected.push(freshCopy(template, answerEnv));
|
|
1744
1812
|
}
|
|
1745
1813
|
solver.absorbStatsFrom(collector);
|
|
1746
1814
|
const next = env.clone();
|
|
1747
|
-
if (unify(bag,
|
|
1815
|
+
if (unify(bag, findallListFromItems(compact ?? collected), next)) yield next;
|
|
1748
1816
|
}
|
|
1749
1817
|
|
|
1750
1818
|
function collectVariableNames(term, env, names = new Set()) {
|
|
@@ -1888,12 +1956,15 @@ function* countAllBuiltin({ solver, goal, env }) {
|
|
|
1888
1956
|
}
|
|
1889
1957
|
|
|
1890
1958
|
const invoked = callable(goal.args[0], env);
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1959
|
+
let count = solver.fastCountGoal?.(invoked, env) ?? null;
|
|
1960
|
+
if (count == null) {
|
|
1961
|
+
const child = solver.cloneForInnerGoal();
|
|
1962
|
+
count = 0n;
|
|
1963
|
+
try {
|
|
1964
|
+
for (const _ of child.solve([invoked], env.clone(), 0)) count++;
|
|
1965
|
+
} finally {
|
|
1966
|
+
solver.absorbStatsFrom(child);
|
|
1967
|
+
}
|
|
1897
1968
|
}
|
|
1898
1969
|
|
|
1899
1970
|
const next = env.clone();
|
|
@@ -1970,11 +2041,33 @@ function* phraseBuiltin({ solver, goal, env }) {
|
|
|
1970
2041
|
module: goal.module ?? grammarBody.module ?? 'user',
|
|
1971
2042
|
});
|
|
1972
2043
|
const finish = compound('=', [finalOutput, requestedOutput]);
|
|
1973
|
-
|
|
2044
|
+
// Recursive DCGs are automatically tabled in normal mode. Keep tables in a
|
|
2045
|
+
// phrase-local scope keyed by the whole invocation. Repeatedly testing the
|
|
2046
|
+
// same grammar/input (issue #48) reuses its completed table, while switching
|
|
2047
|
+
// to a distinct input (issue #28) drops the previous invocation as one unit
|
|
2048
|
+
// instead of retaining or individually evicting every recursive tail.
|
|
2049
|
+
const phraseModule = goal.module ?? grammarBody.module ?? 'user';
|
|
2050
|
+
const tableScopeSignature = solver.innerTableSignature(
|
|
2051
|
+
[grammarBody, input, requestedOutput],
|
|
2052
|
+
env,
|
|
2053
|
+
`${phraseModule}:`,
|
|
2054
|
+
);
|
|
2055
|
+
const previousPhraseScope = solver.innerTableScopes.get('phrase');
|
|
2056
|
+
const repeatedInvocation = previousPhraseScope?.signature === tableScopeSignature;
|
|
2057
|
+
const child = solver.cloneForInnerGoal(solver.solutionLimit, {
|
|
2058
|
+
tableScope: 'phrase',
|
|
2059
|
+
tableScopeSignature,
|
|
2060
|
+
// A new phrase input cannot benefit from tables retained for a different
|
|
2061
|
+
// input. For structurally tail-consuming DCGs, execute it directly. If the
|
|
2062
|
+
// same invocation repeats (the issue #48 pattern), normal tabling resumes
|
|
2063
|
+
// and the compact phrase-local table is reused by subsequent repetitions.
|
|
2064
|
+
skipListTailTabling: !repeatedInvocation,
|
|
2065
|
+
});
|
|
1974
2066
|
try {
|
|
1975
2067
|
yield* child.solve([expanded, finish], env, 0);
|
|
1976
2068
|
} finally {
|
|
1977
2069
|
solver.absorbStatsFrom(child);
|
|
2070
|
+
solver.trimInnerTableScope('phrase');
|
|
1978
2071
|
}
|
|
1979
2072
|
}
|
|
1980
2073
|
const defaultErrorContext = atom('eyeprolog');
|
|
@@ -2032,11 +2125,28 @@ function prologErrorBall(error) {
|
|
|
2032
2125
|
return freshCopy(term, new Env());
|
|
2033
2126
|
}
|
|
2034
2127
|
function* catchBuiltin({ solver, goal, env }) {
|
|
2035
|
-
|
|
2128
|
+
let child = null;
|
|
2036
2129
|
try {
|
|
2037
2130
|
// Corrigendum 2 removed catch/3's own callability errors so that errors
|
|
2038
2131
|
// raised while converting/executing the protected goal are catchable.
|
|
2039
|
-
|
|
2132
|
+
const invoked = callable(goal.args[0], env);
|
|
2133
|
+
const direct = solver.registry.get(invoked.name, invoked.arity);
|
|
2134
|
+
if (direct?.deterministic && (direct.shouldUse == null || direct.shouldUse({ solver, goal: invoked, env }))) {
|
|
2135
|
+
// A deterministic builtin has no choice points whose lifetime must be
|
|
2136
|
+
// isolated in a child solver. Running it directly avoids constructing a
|
|
2137
|
+
// complete Solver for hot caught failures such as number_chars/2 syntax
|
|
2138
|
+
// probes, while the cloned environment keeps catch/3's rollback boundary.
|
|
2139
|
+
const iterator = direct.handler({ solver, goal: invoked, env: env.clone() });
|
|
2140
|
+
const result = iterator.next();
|
|
2141
|
+
if (result.done) solver.stats.deterministic_builtin_failures++;
|
|
2142
|
+
else {
|
|
2143
|
+
solver.stats.deterministic_builtin_successes++;
|
|
2144
|
+
yield result.value;
|
|
2145
|
+
}
|
|
2146
|
+
return;
|
|
2147
|
+
}
|
|
2148
|
+
child = solver.cloneForInnerGoal();
|
|
2149
|
+
yield* child.solve([invoked], env.clone(), 0);
|
|
2040
2150
|
} catch (error) {
|
|
2041
2151
|
const ball = error instanceof ThrownTerm
|
|
2042
2152
|
? error.term
|
|
@@ -2046,9 +2156,15 @@ function* catchBuiltin({ solver, goal, env }) {
|
|
|
2046
2156
|
if (ball == null) throw error;
|
|
2047
2157
|
const recovered = env.clone();
|
|
2048
2158
|
if (!unify(goal.args[1], ball, recovered)) throw error;
|
|
2049
|
-
|
|
2159
|
+
const recovery = callable(goal.args[2], recovered);
|
|
2160
|
+
if (recovery.type === ATOM && (recovery.name === 'false' || recovery.name === 'fail')) return;
|
|
2161
|
+
if (recovery.type === ATOM && recovery.name === 'true') {
|
|
2162
|
+
yield recovered;
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
yield* solver.solve([recovery], recovered, 0);
|
|
2050
2166
|
} finally {
|
|
2051
|
-
solver.absorbStatsFrom(child);
|
|
2167
|
+
if (child != null) solver.absorbStatsFrom(child);
|
|
2052
2168
|
}
|
|
2053
2169
|
}
|
|
2054
2170
|
function* throwBuiltin({ goal, env }) {
|
|
@@ -2071,7 +2187,25 @@ function* repeatBuiltin({ env }) {
|
|
|
2071
2187
|
while (true) yield env;
|
|
2072
2188
|
}
|
|
2073
2189
|
function* negationBuiltin({ solver, goal, env }) {
|
|
2074
|
-
|
|
2190
|
+
const invoked = callable(goal.args[0], env);
|
|
2191
|
+
const direct = solver.registry.get(invoked.name, invoked.arity);
|
|
2192
|
+
if (direct?.deterministic && (direct.shouldUse == null || direct.shouldUse({ solver, goal: invoked, env }))) {
|
|
2193
|
+
const iterator = direct.handler({ solver, goal: invoked, env: env.clone() });
|
|
2194
|
+
const result = iterator.next();
|
|
2195
|
+
if (result.done) {
|
|
2196
|
+
solver.stats.deterministic_builtin_failures++;
|
|
2197
|
+
yield env;
|
|
2198
|
+
} else {
|
|
2199
|
+
solver.stats.deterministic_builtin_successes++;
|
|
2200
|
+
}
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2203
|
+
const fastTruth = solver.fastGroundGoalTruth?.(invoked, env) ?? null;
|
|
2204
|
+
if (fastTruth != null) {
|
|
2205
|
+
if (!fastTruth) yield env;
|
|
2206
|
+
return;
|
|
2207
|
+
}
|
|
2208
|
+
for (const _ of solver.cloneForInnerGoal(1).solve([invoked], env.clone(), 0)) return;
|
|
2075
2209
|
yield env;
|
|
2076
2210
|
}
|
|
2077
2211
|
function* solveControlBranch(solver, goal, env) {
|
package/src/lib/iso_ext.pl
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Widely implemented extensions to the ISO Prolog core. */
|
|
2
2
|
|
|
3
3
|
:- module(iso_ext, [
|
|
4
|
+
call_nth/2,
|
|
4
5
|
countall/2,
|
|
5
6
|
forall/2,
|
|
6
7
|
succ/2,
|
|
@@ -9,6 +10,7 @@
|
|
|
9
10
|
variant/2
|
|
10
11
|
]).
|
|
11
12
|
|
|
13
|
+
:- meta_predicate(call_nth(0, '?')).
|
|
12
14
|
:- meta_predicate(countall(0, '?')).
|
|
13
15
|
:- meta_predicate(forall(0, 0)).
|
|
14
16
|
:- meta_predicate(findall('?', 0, '?', '?')).
|
|
@@ -17,6 +19,8 @@
|
|
|
17
19
|
% Trealla. These definitions use only EyeProlog's ISO profile; extensions that
|
|
18
20
|
% require runtime cleanup, choice-point, alarm, or timeout hooks are omitted.
|
|
19
21
|
|
|
22
|
+
call_nth(Goal, Nth) :- eyeprolog__call_nth(Goal, Nth).
|
|
23
|
+
|
|
20
24
|
countall(Goal, Count) :- eyeprolog__countall(Goal, Count).
|
|
21
25
|
|
|
22
26
|
forall(Condition, Action) :-
|
package/src/lib/lists.pl
CHANGED
|
@@ -28,7 +28,6 @@
|
|
|
28
28
|
min_list/2,
|
|
29
29
|
max_list/2,
|
|
30
30
|
list_to_set/2,
|
|
31
|
-
countall/2,
|
|
32
31
|
set_nth0/4,
|
|
33
32
|
take/3,
|
|
34
33
|
drop/3,
|
|
@@ -45,7 +44,6 @@
|
|
|
45
44
|
:- meta_predicate(foldl(3, '?', '?', '?')).
|
|
46
45
|
:- meta_predicate(foldl(4, '?', '?', '?', '?')).
|
|
47
46
|
:- meta_predicate(foldl(5, '?', '?', '?', '?', '?')).
|
|
48
|
-
:- meta_predicate(countall(0, '?')).
|
|
49
47
|
|
|
50
48
|
% Common pure-Prolog library predicates for EyeProlog.
|
|
51
49
|
%
|
|
@@ -168,7 +166,6 @@ max_list([X|Xs], Max) :- lists__max_list(Xs, X, Max).
|
|
|
168
166
|
|
|
169
167
|
list_to_set(List, Set) :- lists__list_to_set(List, [], Set).
|
|
170
168
|
|
|
171
|
-
countall(Goal, Count) :- eyeprolog__countall(Goal, Count).
|
|
172
169
|
|
|
173
170
|
set_nth0(0, [_|Xs], X, [X|Xs]).
|
|
174
171
|
set_nth0(N, [Y|Ys], X, [Y|Zs]) :-
|
package/src/program.js
CHANGED
|
@@ -166,6 +166,9 @@ export class Program {
|
|
|
166
166
|
tabled: false,
|
|
167
167
|
recursive: false,
|
|
168
168
|
tableInputPositions: [],
|
|
169
|
+
tableAllVariants: false,
|
|
170
|
+
datalogLeastModel: false,
|
|
171
|
+
wfsDatalog: false,
|
|
169
172
|
scalarFactsOnly: true,
|
|
170
173
|
dynamic: this.dynamicPredicates.has(modulePredicateKey(module, name, arity)),
|
|
171
174
|
negationStratum: null,
|
|
@@ -308,6 +311,7 @@ export class Program {
|
|
|
308
311
|
const deps = groups.map(() => new Set());
|
|
309
312
|
const cutDeps = groups.map(() => new Set());
|
|
310
313
|
const negativeEdges = [];
|
|
314
|
+
const wfsNegativeEdges = [];
|
|
311
315
|
for (const group of groups) {
|
|
312
316
|
const groupIndex = indexByGroup.get(group);
|
|
313
317
|
for (const clause of group.clauses) {
|
|
@@ -337,12 +341,17 @@ export class Program {
|
|
|
337
341
|
if (dep) {
|
|
338
342
|
const dependencyIndex = indexByGroup.get(dep);
|
|
339
343
|
deps[groupIndex].add(dependencyIndex);
|
|
340
|
-
if (dependency.negative)
|
|
344
|
+
if (dependency.negative) {
|
|
345
|
+
negativeEdges.push([groupIndex, dependencyIndex]);
|
|
346
|
+
if (dependency.wfs === true) wfsNegativeEdges.push([groupIndex, dependencyIndex]);
|
|
347
|
+
}
|
|
341
348
|
}
|
|
342
349
|
}
|
|
343
350
|
}
|
|
344
351
|
}
|
|
345
352
|
}
|
|
353
|
+
const finiteDatalogCache = new Map();
|
|
354
|
+
const rangeRestrictedDatalogCache = new Map();
|
|
346
355
|
for (const group of groups) {
|
|
347
356
|
const start = indexByGroup.get(group);
|
|
348
357
|
const standardLibraryModule = group.module !== 'user' &&
|
|
@@ -386,6 +395,36 @@ export class Program {
|
|
|
386
395
|
!componentHasNegativeEdge(start, deps, negativeEdges) &&
|
|
387
396
|
!group.cutRecursive &&
|
|
388
397
|
!linearNumeric;
|
|
398
|
+
// Function-free positive recursive rules have a finite Herbrand base.
|
|
399
|
+
// They can safely use a shared most-general table, which avoids computing
|
|
400
|
+
// a separate recursive closure for every bound input (TC/SG/Wine style).
|
|
401
|
+
group.tableAllVariants = group.tabled && isFiniteDatalogGroup(this, group, finiteDatalogCache) &&
|
|
402
|
+
datalogDependencyClauseCount(this, group) >= 128;
|
|
403
|
+
// Large finite, range-restricted positive Datalog can be evaluated as a
|
|
404
|
+
// single indexed least model. Keep tableAllVariants as the semantic
|
|
405
|
+
// fallback for finite programs whose answers may contain variables.
|
|
406
|
+
group.datalogLeastModel = group.tableAllVariants &&
|
|
407
|
+
isRangeRestrictedFiniteDatalogGroup(this, group, rangeRestrictedDatalogCache);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Explicit tnot/1 opts finite, function-free Datalog into well-founded
|
|
411
|
+
// evaluation. Keep ISO \+/1 and not/1 on their existing negation-as-failure
|
|
412
|
+
// path: WFS is an extension with different three-valued semantics and must
|
|
413
|
+
// not silently change ordinary Prolog programs. A group is WFS-backed when
|
|
414
|
+
// its dependency cone reaches an SCC containing an edge introduced by
|
|
415
|
+
// tnot/1 and the whole cone is range-restricted finite Datalog.
|
|
416
|
+
const wfsCycleNodes = new Set();
|
|
417
|
+
for (const [from, to] of wfsNegativeEdges) {
|
|
418
|
+
if (!reachableIndexes(to, deps).has(from)) continue;
|
|
419
|
+
wfsCycleNodes.add(from);
|
|
420
|
+
wfsCycleNodes.add(to);
|
|
421
|
+
}
|
|
422
|
+
const finiteWfsCache = new Map();
|
|
423
|
+
for (const group of groups) {
|
|
424
|
+
const start = indexByGroup.get(group);
|
|
425
|
+
const reachable = reachableIndexes(start, deps);
|
|
426
|
+
const reachesWfsCycle = [...wfsCycleNodes].some((index) => reachable.has(index));
|
|
427
|
+
group.wfsDatalog = reachesWfsCycle && isFiniteWfsDatalogGroup(this, group, finiteWfsCache);
|
|
389
428
|
}
|
|
390
429
|
}
|
|
391
430
|
|
|
@@ -1253,6 +1292,185 @@ function reachableIndexes(start, deps) {
|
|
|
1253
1292
|
return seen;
|
|
1254
1293
|
}
|
|
1255
1294
|
|
|
1295
|
+
|
|
1296
|
+
function isFiniteDatalogArgument(term) {
|
|
1297
|
+
return term?.type === VAR || term?.type === ATOM || term?.type === 'string' || term?.type === 'number';
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function datalogDependencyClauseCount(program, group, seen = new Set()) {
|
|
1301
|
+
if (seen.has(group)) return 0;
|
|
1302
|
+
seen.add(group);
|
|
1303
|
+
let count = group.clauses.length;
|
|
1304
|
+
for (const clause of group.clauses) {
|
|
1305
|
+
if (isCompactBinaryClause(clause)) {
|
|
1306
|
+
if (clause.bodyName != null) {
|
|
1307
|
+
const target = program.findGroup(clause.bodyName, 2, group.module);
|
|
1308
|
+
if (target) count += datalogDependencyClauseCount(program, target, seen);
|
|
1309
|
+
}
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
for (const goal of clause.body) {
|
|
1313
|
+
if (goal.type !== COMPOUND && goal.type !== ATOM) continue;
|
|
1314
|
+
const target = program.findGroup(goal.name, goal.arity, goal.module ?? group.module);
|
|
1315
|
+
if (target) count += datalogDependencyClauseCount(program, target, seen);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
return count;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
function isFiniteDatalogGroup(program, group, cache = new Map(), visiting = new Set()) {
|
|
1322
|
+
const cached = cache.get(group);
|
|
1323
|
+
if (cached != null) return cached;
|
|
1324
|
+
if (visiting.has(group)) return true;
|
|
1325
|
+
visiting.add(group);
|
|
1326
|
+
let finite = true;
|
|
1327
|
+
for (const clause of group.clauses) {
|
|
1328
|
+
if (isCompactBinaryClause(clause)) {
|
|
1329
|
+
if (clause.bodyName != null) {
|
|
1330
|
+
const target = program.findGroup(clause.bodyName, 2, group.module);
|
|
1331
|
+
if (!target || !isFiniteDatalogGroup(program, target, cache, visiting)) { finite = false; break; }
|
|
1332
|
+
}
|
|
1333
|
+
continue;
|
|
1334
|
+
}
|
|
1335
|
+
if (clause.head.type === COMPOUND && !clause.head.args.every(isFiniteDatalogArgument)) { finite = false; break; }
|
|
1336
|
+
if (clause.head.type !== COMPOUND && clause.head.type !== ATOM) { finite = false; break; }
|
|
1337
|
+
for (const goal of clause.body) {
|
|
1338
|
+
if (goal.type !== COMPOUND && goal.type !== ATOM) { finite = false; break; }
|
|
1339
|
+
if (goal.type === COMPOUND && !goal.args.every(isFiniteDatalogArgument)) { finite = false; break; }
|
|
1340
|
+
// Walk the whole positive dependency cone. Requiring every reachable
|
|
1341
|
+
// predicate to be source-defined and flat excludes builtins/generators
|
|
1342
|
+
// that could manufacture an unbounded stream of fresh terms.
|
|
1343
|
+
const target = program.findGroup(goal.name, goal.arity, goal.module ?? group.module);
|
|
1344
|
+
if (!target || !isFiniteDatalogGroup(program, target, cache, visiting)) { finite = false; break; }
|
|
1345
|
+
}
|
|
1346
|
+
if (!finite) break;
|
|
1347
|
+
}
|
|
1348
|
+
visiting.delete(group);
|
|
1349
|
+
cache.set(group, finite);
|
|
1350
|
+
return finite;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
|
|
1354
|
+
function isRangeRestrictedFiniteDatalogGroup(program, group, cache = new Map(), visiting = new Set()) {
|
|
1355
|
+
const cached = cache.get(group);
|
|
1356
|
+
if (cached != null) return cached;
|
|
1357
|
+
if (visiting.has(group)) return true;
|
|
1358
|
+
visiting.add(group);
|
|
1359
|
+
let finite = true;
|
|
1360
|
+
|
|
1361
|
+
for (const clause of group.clauses) {
|
|
1362
|
+
const head = clause.head;
|
|
1363
|
+
if ((head.type !== COMPOUND && head.type !== ATOM) ||
|
|
1364
|
+
(head.type === COMPOUND && !head.args.every(isFiniteDatalogArgument))) {
|
|
1365
|
+
finite = false;
|
|
1366
|
+
break;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
const headVariables = new Set();
|
|
1370
|
+
const positiveVariables = new Set();
|
|
1371
|
+
collectVariables(head, headVariables);
|
|
1372
|
+
for (const goal of clause.body) {
|
|
1373
|
+
if ((goal.type !== COMPOUND && goal.type !== ATOM) ||
|
|
1374
|
+
(goal.type === COMPOUND && !goal.args.every(isFiniteDatalogArgument))) {
|
|
1375
|
+
finite = false;
|
|
1376
|
+
break;
|
|
1377
|
+
}
|
|
1378
|
+
collectVariables(goal, positiveVariables);
|
|
1379
|
+
const target = program.findGroup(goal.name, goal.arity, goal.module ?? group.module);
|
|
1380
|
+
if (!target || !isRangeRestrictedFiniteDatalogGroup(program, target, cache, visiting)) {
|
|
1381
|
+
finite = false;
|
|
1382
|
+
break;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
if (!finite) break;
|
|
1386
|
+
for (const name of headVariables) {
|
|
1387
|
+
if (!positiveVariables.has(name)) {
|
|
1388
|
+
// Ground facts have no head variables and pass naturally. A variable
|
|
1389
|
+
// fact or a rule with an unbound head variable needs ordinary Prolog
|
|
1390
|
+
// table semantics rather than finite relational materialization.
|
|
1391
|
+
finite = false;
|
|
1392
|
+
break;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
if (!finite) break;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
visiting.delete(group);
|
|
1399
|
+
cache.set(group, finite);
|
|
1400
|
+
return finite;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
|
|
1404
|
+
function isFiniteWfsDatalogGroup(program, group, cache = new Map(), visiting = new Set()) {
|
|
1405
|
+
const cached = cache.get(group);
|
|
1406
|
+
if (cached != null) return cached;
|
|
1407
|
+
if (visiting.has(group)) return true;
|
|
1408
|
+
visiting.add(group);
|
|
1409
|
+
let finite = true;
|
|
1410
|
+
|
|
1411
|
+
for (const clause of group.clauses) {
|
|
1412
|
+
if (isCompactBinaryClause(clause)) {
|
|
1413
|
+
if (clause.bodyName != null) {
|
|
1414
|
+
const target = program.findGroup(clause.bodyName, 2, group.module);
|
|
1415
|
+
if (!target || !isFiniteWfsDatalogGroup(program, target, cache, visiting)) { finite = false; break; }
|
|
1416
|
+
}
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
if ((clause.head.type !== COMPOUND && clause.head.type !== ATOM) ||
|
|
1420
|
+
(clause.head.type === COMPOUND && !clause.head.args.every(isFiniteDatalogArgument))) {
|
|
1421
|
+
finite = false;
|
|
1422
|
+
break;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
const positiveVariables = new Set();
|
|
1426
|
+
const requiredVariables = new Set();
|
|
1427
|
+
collectVariables(clause.head, requiredVariables);
|
|
1428
|
+
for (const goal of clause.body) {
|
|
1429
|
+
if (goal.type !== COMPOUND && goal.type !== ATOM) { finite = false; break; }
|
|
1430
|
+
if (goal.type === COMPOUND && goal.name === 'tnot' && goal.arity === 1) {
|
|
1431
|
+
const inner = goal.args[0];
|
|
1432
|
+
if ((inner.type !== COMPOUND && inner.type !== ATOM) ||
|
|
1433
|
+
(inner.type === COMPOUND && !inner.args.every(isFiniteDatalogArgument))) {
|
|
1434
|
+
finite = false;
|
|
1435
|
+
break;
|
|
1436
|
+
}
|
|
1437
|
+
collectVariables(inner, requiredVariables);
|
|
1438
|
+
const target = program.findGroup(inner.name, inner.arity, inner.module ?? group.module);
|
|
1439
|
+
if (!target || !isFiniteWfsDatalogGroup(program, target, cache, visiting)) { finite = false; break; }
|
|
1440
|
+
continue;
|
|
1441
|
+
}
|
|
1442
|
+
// WFS is explicit. Ordinary \+/1 and not/1 retain NAF and therefore
|
|
1443
|
+
// disqualify this component from the WFS evaluator.
|
|
1444
|
+
if (goal.type === COMPOUND && (goal.name === '\\+' || goal.name === 'not')) {
|
|
1445
|
+
finite = false;
|
|
1446
|
+
break;
|
|
1447
|
+
}
|
|
1448
|
+
if (goal.type === COMPOUND && !goal.args.every(isFiniteDatalogArgument)) { finite = false; break; }
|
|
1449
|
+
collectVariables(goal, positiveVariables);
|
|
1450
|
+
const target = program.findGroup(goal.name, goal.arity, goal.module ?? group.module);
|
|
1451
|
+
if (!target || !isFiniteWfsDatalogGroup(program, target, cache, visiting)) { finite = false; break; }
|
|
1452
|
+
}
|
|
1453
|
+
if (!finite) break;
|
|
1454
|
+
for (const name of requiredVariables) {
|
|
1455
|
+
if (!positiveVariables.has(name)) { finite = false; break; }
|
|
1456
|
+
}
|
|
1457
|
+
if (!finite) break;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
visiting.delete(group);
|
|
1461
|
+
cache.set(group, finite);
|
|
1462
|
+
return finite;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
function collectVariables(term, output) {
|
|
1466
|
+
if (!term) return;
|
|
1467
|
+
if (term.type === VAR) {
|
|
1468
|
+
output.add(term.name);
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
for (const arg of term.args ?? []) collectVariables(arg, output);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1256
1474
|
function inferStructuralInputPositions(group) {
|
|
1257
1475
|
let firstPatternedPosition = -1;
|
|
1258
1476
|
let firstLinkedInputPosition = -1;
|
|
@@ -1304,14 +1522,31 @@ function hasStrictListTailRecursion(group) {
|
|
|
1304
1522
|
let foundRecursiveCall = false;
|
|
1305
1523
|
for (const clause of group.clauses) {
|
|
1306
1524
|
if (isCompactBinaryClause(clause)) return false;
|
|
1307
|
-
for (
|
|
1525
|
+
for (let goalIndex = 0; goalIndex < clause.body.length; goalIndex++) {
|
|
1526
|
+
const goal = clause.body[goalIndex];
|
|
1308
1527
|
if (goal.type !== COMPOUND || goal.name !== group.name || goal.arity !== group.arity) continue;
|
|
1309
1528
|
foundRecursiveCall = true;
|
|
1310
1529
|
const decreases = goal.args.some((argument, index) => {
|
|
1311
1530
|
const head = clause.head.args[index];
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1531
|
+
if (head?.type === COMPOUND && head.name === '.' && head.arity === 2 &&
|
|
1532
|
+
head.args[1]?.type === VAR && argument?.type === VAR &&
|
|
1533
|
+
head.args[1].name === argument.name) return true;
|
|
1534
|
+
// DCG expansion represents [_], nt as Input = [_|Tail], nt(Tail,...).
|
|
1535
|
+
// Treat that normalized prefix unification as the same strict list-tail
|
|
1536
|
+
// decrease so phrase/2-3 can choose a non-tabled fast path for new
|
|
1537
|
+
// inputs while retaining ordinary tabling for repeated invocations.
|
|
1538
|
+
if (head?.type !== VAR || argument?.type !== VAR) return false;
|
|
1539
|
+
for (let prefixIndex = 0; prefixIndex < goalIndex; prefixIndex++) {
|
|
1540
|
+
const prefix = clause.body[prefixIndex];
|
|
1541
|
+
if (prefix?.type !== COMPOUND || prefix.name !== '=' || prefix.arity !== 2) continue;
|
|
1542
|
+
for (const [left, right] of [[prefix.args[0], prefix.args[1]], [prefix.args[1], prefix.args[0]]]) {
|
|
1543
|
+
if (left?.type !== VAR || left.name !== head.name ||
|
|
1544
|
+
right?.type !== COMPOUND || right.name !== '.' || right.arity !== 2 ||
|
|
1545
|
+
right.args[1]?.type !== VAR) continue;
|
|
1546
|
+
if (right.args[1].name === argument.name) return true;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
return false;
|
|
1315
1550
|
});
|
|
1316
1551
|
if (!decreases) return false;
|
|
1317
1552
|
}
|
|
@@ -1377,7 +1612,7 @@ function directGoalDependencyKey(goal) {
|
|
|
1377
1612
|
if (goal.type === ATOM) return `${goal.name}/0`;
|
|
1378
1613
|
if (goal.type !== COMPOUND) return null;
|
|
1379
1614
|
if (goal.name === ',' && goal.arity === 2) return null;
|
|
1380
|
-
if ((goal.name === '\\+' || goal.name === 'not') && goal.arity === 1) return null;
|
|
1615
|
+
if ((goal.name === '\\+' || goal.name === 'not' || goal.name === 'tnot') && goal.arity === 1) return null;
|
|
1381
1616
|
if (goal.name === 'once' && goal.arity === 1) return null;
|
|
1382
1617
|
if (goal.name === 'forall' && goal.arity === 2) return null;
|
|
1383
1618
|
if ((goal.name === 'findall' || goal.name === 'sumall') && goal.arity === 3) return null;
|
|
@@ -1386,43 +1621,46 @@ function directGoalDependencyKey(goal) {
|
|
|
1386
1621
|
return `${goal.name}/${goal.arity}`;
|
|
1387
1622
|
}
|
|
1388
1623
|
|
|
1389
|
-
function collectGoalDependencies(goal, negated, traverseConditionals = false) {
|
|
1390
|
-
if (goal.type === ATOM) return [{ key: `${goal.name}/0`, name: goal.name, arity: 0, module: goal.module, negative: negated }];
|
|
1624
|
+
function collectGoalDependencies(goal, negated, traverseConditionals = false, wfs = false) {
|
|
1625
|
+
if (goal.type === ATOM) return [{ key: `${goal.name}/0`, name: goal.name, arity: 0, module: goal.module, negative: negated, wfs }];
|
|
1391
1626
|
if (goal.type !== COMPOUND) return [];
|
|
1392
1627
|
if (goal.name === ',' && goal.arity === 2) {
|
|
1393
1628
|
return [
|
|
1394
|
-
...collectGoalDependencies(goal.args[0], negated, traverseConditionals),
|
|
1395
|
-
...collectGoalDependencies(goal.args[1], negated, traverseConditionals),
|
|
1629
|
+
...collectGoalDependencies(goal.args[0], negated, traverseConditionals, wfs),
|
|
1630
|
+
...collectGoalDependencies(goal.args[1], negated, traverseConditionals, wfs),
|
|
1396
1631
|
];
|
|
1397
1632
|
}
|
|
1398
1633
|
if (traverseConditionals && (goal.name === ';' || goal.name === '->') && goal.arity === 2) {
|
|
1399
1634
|
return [
|
|
1400
|
-
...collectGoalDependencies(goal.args[0], negated, true),
|
|
1401
|
-
...collectGoalDependencies(goal.args[1], negated, true),
|
|
1635
|
+
...collectGoalDependencies(goal.args[0], negated, true, wfs),
|
|
1636
|
+
...collectGoalDependencies(goal.args[1], negated, true, wfs),
|
|
1402
1637
|
];
|
|
1403
1638
|
}
|
|
1404
1639
|
if ((goal.name === '\\+' || goal.name === 'not') && goal.arity === 1) {
|
|
1405
|
-
return collectGoalDependencies(goal.args[0], !negated, traverseConditionals);
|
|
1640
|
+
return collectGoalDependencies(goal.args[0], !negated, traverseConditionals, wfs);
|
|
1641
|
+
}
|
|
1642
|
+
if (goal.name === 'tnot' && goal.arity === 1) {
|
|
1643
|
+
return collectGoalDependencies(goal.args[0], !negated, traverseConditionals, true);
|
|
1406
1644
|
}
|
|
1407
1645
|
if (goal.name === 'once' && goal.arity === 1) {
|
|
1408
|
-
return collectGoalDependencies(goal.args[0], negated, traverseConditionals);
|
|
1646
|
+
return collectGoalDependencies(goal.args[0], negated, traverseConditionals, wfs);
|
|
1409
1647
|
}
|
|
1410
1648
|
if (goal.name === 'forall' && goal.arity === 2) {
|
|
1411
1649
|
return [
|
|
1412
|
-
...collectGoalDependencies(goal.args[0], negated, traverseConditionals),
|
|
1413
|
-
...collectGoalDependencies(goal.args[1], negated, traverseConditionals),
|
|
1650
|
+
...collectGoalDependencies(goal.args[0], negated, traverseConditionals, wfs),
|
|
1651
|
+
...collectGoalDependencies(goal.args[1], negated, traverseConditionals, wfs),
|
|
1414
1652
|
];
|
|
1415
1653
|
}
|
|
1416
1654
|
if ((goal.name === 'findall' || goal.name === 'sumall') && goal.arity === 3) {
|
|
1417
|
-
return collectGoalDependencies(goal.args[1], negated, traverseConditionals);
|
|
1655
|
+
return collectGoalDependencies(goal.args[1], negated, traverseConditionals, wfs);
|
|
1418
1656
|
}
|
|
1419
1657
|
if (goal.name === 'countall' && goal.arity === 2) {
|
|
1420
|
-
return collectGoalDependencies(goal.args[0], negated, traverseConditionals);
|
|
1658
|
+
return collectGoalDependencies(goal.args[0], negated, traverseConditionals, wfs);
|
|
1421
1659
|
}
|
|
1422
1660
|
if ((goal.name === 'aggregate_min' || goal.name === 'aggregate_max') && goal.arity === 5) {
|
|
1423
|
-
return collectGoalDependencies(goal.args[2], negated, traverseConditionals);
|
|
1661
|
+
return collectGoalDependencies(goal.args[2], negated, traverseConditionals, wfs);
|
|
1424
1662
|
}
|
|
1425
|
-
return [{ key: `${goal.name}/${goal.arity}`, name: goal.name, arity: goal.arity, module: goal.module, negative: negated }];
|
|
1663
|
+
return [{ key: `${goal.name}/${goal.arity}`, name: goal.name, arity: goal.arity, module: goal.module, negative: negated, wfs }];
|
|
1426
1664
|
}
|
|
1427
1665
|
|
|
1428
1666
|
function stronglyConnectedComponents(adjacency) {
|