eyeprolog 1.6.39 → 1.6.41
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/examples/check/dependency-depth-10000.pl +14 -0
- package/examples/check/dependency-preparation-5000.pl +14 -0
- package/examples/deck/README.md +2 -0
- package/examples/deck/dependency-depth-10000.md +107 -0
- package/examples/deck/dependency-preparation-5000.md +106 -0
- package/examples/dependency-depth-10000.pl +10008 -0
- package/examples/dependency-preparation-5000.pl +5008 -0
- package/examples/output/dependency-depth-10000.pl +1 -0
- package/examples/output/dependency-preparation-5000.pl +1 -0
- package/examples/proof/dependency-depth-10000.pl +8 -0
- package/examples/proof/dependency-preparation-5000.pl +7 -0
- package/package.json +1 -1
- package/playground.html +2 -0
- package/src/check-proof.js +30 -3
- package/src/explain.js +36 -4
- package/src/parser.js +14 -5
- package/src/program-analysis.js +80 -22
- package/src/program.js +31 -37
- package/src/result-format.js +24 -3
- package/src/write.js +11 -3
- package/test/README.md +4 -0
- package/test/parallel-tasks.mjs +23 -5
- package/test/regression/cases-regression.mjs +34 -0
- package/test/regression/cases-white-box.mjs +116 -0
- package/test/run-examples.mjs +1 -0
- package/test/run-proof-checking.mjs +2 -1
- package/test/test-support.mjs +23 -15
- package/the-art-of-eyeprolog.md +4 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
chain9998(ready).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
chain4998(ready).
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
chain9998(ready).
|
|
2
|
+
|
|
3
|
+
clause(9999, chain9998(var('X')), chain9999(var('X'))).
|
|
4
|
+
clause(10000, chain9999(ready), !).
|
|
5
|
+
|
|
6
|
+
step(chain9998(ready), rule(9999), ['X' = ready], [chain9999(ready)]).
|
|
7
|
+
step(chain9999(ready), rule(10000), [], "!").
|
|
8
|
+
step(!, builtin, [], []).
|
package/package.json
CHANGED
package/playground.html
CHANGED
package/src/check-proof.js
CHANGED
|
@@ -152,8 +152,19 @@ function isAtom(term, name) {
|
|
|
152
152
|
return term?.type === ATOM && term.name === name;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
// While a proof is checked, each term is spelled once: a step's conclusion is
|
|
156
|
+
// also the use of the step that rests on it, and the coverage, relevance and
|
|
157
|
+
// well-foundedness checks each look it up.
|
|
158
|
+
let keyMemo = null;
|
|
159
|
+
|
|
155
160
|
function key(term) {
|
|
156
|
-
return termToString(term, new Env(), true);
|
|
161
|
+
if (keyMemo == null || term === null || typeof term !== 'object') return termToString(term, new Env(), true);
|
|
162
|
+
let spelled = keyMemo.get(term);
|
|
163
|
+
if (spelled === undefined) {
|
|
164
|
+
spelled = termToString(term, new Env(), true);
|
|
165
|
+
keyMemo.set(term, spelled);
|
|
166
|
+
}
|
|
167
|
+
return spelled;
|
|
157
168
|
}
|
|
158
169
|
|
|
159
170
|
// A term is identical to another when it needs no further instantiation to
|
|
@@ -455,7 +466,17 @@ export function checkProofDocument(program, text, options = {}) {
|
|
|
455
466
|
// its own proofs this way before writing them (SPEC.md, Section 7.1); that the
|
|
456
467
|
// text it writes reads back as the same structures is what checking the
|
|
457
468
|
// packaged documents establishes.
|
|
458
|
-
export function checkProof(program,
|
|
469
|
+
export function checkProof(program, document, options = {}, failures = []) {
|
|
470
|
+
const outer = keyMemo;
|
|
471
|
+
keyMemo ??= new WeakMap();
|
|
472
|
+
try {
|
|
473
|
+
return checkProofWithKeys(program, document, options, failures);
|
|
474
|
+
} finally {
|
|
475
|
+
keyMemo = outer;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function checkProofWithKeys(program, { claims, records = [], steps: allSteps }, options, failures) {
|
|
459
480
|
const writeOptions = {
|
|
460
481
|
doubleQuotes: program?.doubleQuotes ?? 'chars', doubleBar: true, quoted: true,
|
|
461
482
|
operators: [...(program?.operators?.values() ?? [])],
|
|
@@ -472,7 +493,13 @@ export function checkProof(program, { claims, records = [], steps: allSteps }, o
|
|
|
472
493
|
for (const record of records) {
|
|
473
494
|
const number = record.args[0]?.type === NUMBER ? Number(record.args[0].name) : NaN;
|
|
474
495
|
const clause = Number.isInteger(number) ? numbering.get(number) : null;
|
|
475
|
-
if (!clause
|
|
496
|
+
if (!clause) {
|
|
497
|
+
fail('C1', 'clause record differs from source', record);
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
// A record that is the rebuilt record itself restates the clause.
|
|
501
|
+
const rebuilt = clauseRecordTerm(number, clause);
|
|
502
|
+
if (record !== rebuilt && !identical(normalizedClauseRecord(record), normalizedClauseRecord(rebuilt))) {
|
|
476
503
|
fail('C1', 'clause record differs from source', record);
|
|
477
504
|
}
|
|
478
505
|
}
|
package/src/explain.js
CHANGED
|
@@ -1009,6 +1009,7 @@ function resolvedSubstitutions(substitutions, env) {
|
|
|
1009
1009
|
// would displace the real clause and every step citing it would stop
|
|
1010
1010
|
// resolving. Such a declaration is never what a rule(N) or fact(N) cites.
|
|
1011
1011
|
function isUnnumberedDeclaration(clause) {
|
|
1012
|
+
if (clause?.compactBinary === true) return false;
|
|
1012
1013
|
const head = clause?.head;
|
|
1013
1014
|
if (head?.type !== COMPOUND || head.name !== ':-' || head.arity !== 1) return false;
|
|
1014
1015
|
const directive = head.args[0];
|
|
@@ -1016,7 +1017,36 @@ function isUnnumberedDeclaration(clause) {
|
|
|
1016
1017
|
return ['module', 'use_module', 'meta_predicate', 'attribute'].includes(directive.name);
|
|
1017
1018
|
}
|
|
1018
1019
|
|
|
1020
|
+
// A Map from small positive integers, kept in an array.
|
|
1021
|
+
class NumberedTable {
|
|
1022
|
+
constructor() {
|
|
1023
|
+
this.items = [];
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
get(number) {
|
|
1027
|
+
return Number.isInteger(number) && number >= 0 ? this.items[number] : undefined;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
set(number, value) {
|
|
1031
|
+
this.items[number] = value;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// A proof is numbered when it is flattened and again when it is checked, and
|
|
1036
|
+
// both see the same program; the numbering is kept until the program's
|
|
1037
|
+
// clauses change.
|
|
1038
|
+
const numberings = new WeakMap();
|
|
1039
|
+
|
|
1019
1040
|
export function clauseNumbering(program) {
|
|
1041
|
+
const clauses = program.clauses ?? [];
|
|
1042
|
+
const cached = numberings.get(program);
|
|
1043
|
+
if (cached && cached.clauses === clauses && cached.length === clauses.length) return cached.numbering;
|
|
1044
|
+
const numbering = computeClauseNumbering(program);
|
|
1045
|
+
numberings.set(program, { clauses, length: clauses.length, numbering });
|
|
1046
|
+
return numbering;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function computeClauseNumbering(program) {
|
|
1020
1050
|
const spans = new Map();
|
|
1021
1051
|
for (const clause of program.clauses ?? []) {
|
|
1022
1052
|
if (!isProgramClause(clause)) continue;
|
|
@@ -1032,17 +1062,19 @@ export function clauseNumbering(program) {
|
|
|
1032
1062
|
|
|
1033
1063
|
// Keyed by file, then by clause position: one string key per clause cost
|
|
1034
1064
|
// more than the numbering itself on programs with hundreds of thousands of
|
|
1035
|
-
// clauses.
|
|
1065
|
+
// clauses. Positions and numbers count up from 1, so arrays hold them.
|
|
1036
1066
|
const bySource = new Map();
|
|
1037
|
-
const byNumber = new
|
|
1067
|
+
const byNumber = new NumberedTable();
|
|
1038
1068
|
for (const clause of program.clauses ?? []) {
|
|
1039
1069
|
if (!isProgramClause(clause) || isUnnumberedDeclaration(clause)) continue;
|
|
1040
1070
|
const { filename, clause: position } = clause.source;
|
|
1041
1071
|
const number = (offsets.get(filename) ?? 0) + position;
|
|
1042
1072
|
let numbers = bySource.get(filename);
|
|
1043
|
-
if (!numbers) bySource.set(filename, numbers = new
|
|
1073
|
+
if (!numbers) bySource.set(filename, numbers = new NumberedTable());
|
|
1044
1074
|
numbers.set(position, number);
|
|
1045
|
-
|
|
1075
|
+
// The clause itself, not a copy of its head and body: a compact clause
|
|
1076
|
+
// builds those terms only when a proof cites it.
|
|
1077
|
+
byNumber.set(number, clause.compactBinary === true || clause.body != null ? clause : { head: clause.head, body: [] });
|
|
1046
1078
|
}
|
|
1047
1079
|
return { bySource, byNumber };
|
|
1048
1080
|
}
|
package/src/parser.js
CHANGED
|
@@ -1504,8 +1504,8 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1504
1504
|
let operatorState = null;
|
|
1505
1505
|
// With source metadata, this path numbers clauses exactly as the general
|
|
1506
1506
|
// parser does: one number per clause and per counted directive, none for a
|
|
1507
|
-
// query or a quad.
|
|
1508
|
-
//
|
|
1507
|
+
// query or a quad. A compact binary clause is numbered the same way, through
|
|
1508
|
+
// the clause object its builder hands back.
|
|
1509
1509
|
const trackSource = options.sourceMetadata === true;
|
|
1510
1510
|
const sourceFilename = options.filename ?? '<input>';
|
|
1511
1511
|
let clauseNumber = 0;
|
|
@@ -1774,18 +1774,20 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1774
1774
|
if (rule < 0) {
|
|
1775
1775
|
if (!parseBinaryRawRange(text, start, termEnd, rawHead)) return false;
|
|
1776
1776
|
reportFastBinarySingletonWarnings(rawHead, null, line);
|
|
1777
|
-
emitBinary(rawHead.name,
|
|
1777
|
+
const clause = emitBinary(rawHead.name,
|
|
1778
1778
|
rawHead.arg0Type, rawHead.arg0Name, rawHead.arg1Type, rawHead.arg1Name,
|
|
1779
1779
|
null, null, null, null, null);
|
|
1780
|
+
if (clause) numbered(clause, line);
|
|
1780
1781
|
return true;
|
|
1781
1782
|
}
|
|
1782
1783
|
if (findRuleInRange(text, rule + 2, termEnd) >= 0 ||
|
|
1783
1784
|
!parseBinaryRawRange(text, start, rule, rawHead) ||
|
|
1784
1785
|
!parseBinaryRawRange(text, rule + 2, termEnd, rawBody)) return false;
|
|
1785
1786
|
reportFastBinarySingletonWarnings(rawHead, rawBody, line);
|
|
1786
|
-
emitBinary(rawHead.name,
|
|
1787
|
+
const clause = emitBinary(rawHead.name,
|
|
1787
1788
|
rawHead.arg0Type, rawHead.arg0Name, rawHead.arg1Type, rawHead.arg1Name,
|
|
1788
1789
|
rawBody.name, rawBody.arg0Type, rawBody.arg0Name, rawBody.arg1Type, rawBody.arg1Name);
|
|
1790
|
+
if (clause) numbered(clause, line);
|
|
1789
1791
|
return true;
|
|
1790
1792
|
};
|
|
1791
1793
|
|
|
@@ -1851,12 +1853,14 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1851
1853
|
// subsequent source character must pass through the full tokenizer. The
|
|
1852
1854
|
// compact parser deliberately operates on raw source ranges, so using it
|
|
1853
1855
|
// here would silently bypass Convc for otherwise simple clauses.
|
|
1856
|
+
const anonymousBefore = anonymous;
|
|
1854
1857
|
const simple = preparationConversionActive() ? null : parseSimple(text);
|
|
1855
1858
|
if (simple) {
|
|
1856
1859
|
reportClauseSingletonWarnings(simple, options.onWarning, options.filename ?? '<input>', sourceLine);
|
|
1857
1860
|
accept(numbered(simple, sourceLine));
|
|
1858
1861
|
return true;
|
|
1859
1862
|
}
|
|
1863
|
+
anonymous = anonymousBefore;
|
|
1860
1864
|
try {
|
|
1861
1865
|
const nestedWarning = typeof options.onWarning === 'function'
|
|
1862
1866
|
? (warning) => options.onWarning({ ...warning, line: warning.line + sourceLine - 1 })
|
|
@@ -1912,14 +1916,19 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1912
1916
|
contentEnd = trimRangeEnd(source, contentStart, contentEnd);
|
|
1913
1917
|
if (contentStart < contentEnd && source.charCodeAt(contentStart) !== 37) {
|
|
1914
1918
|
if (!chunk && source.charCodeAt(contentEnd - 1) === 46 && !preparationConversionActive()) {
|
|
1915
|
-
|
|
1919
|
+
// A reader that declines a clause may have named some of its
|
|
1920
|
+
// anonymous variables already; the next one starts the count again.
|
|
1921
|
+
const anonymousBefore = anonymous;
|
|
1922
|
+
if (emitFastBinaryRange(source, contentStart, contentEnd, lineNumber)) {
|
|
1916
1923
|
// The program builder accepted a compact binary clause directly.
|
|
1917
1924
|
} else {
|
|
1925
|
+
anonymous = anonymousBefore;
|
|
1918
1926
|
const simple = parseFastRange(source, contentStart, contentEnd);
|
|
1919
1927
|
if (simple) {
|
|
1920
1928
|
reportClauseSingletonWarnings(simple, options.onWarning, options.filename ?? '<input>', lineNumber);
|
|
1921
1929
|
accept(numbered(simple, lineNumber));
|
|
1922
1930
|
} else {
|
|
1931
|
+
anonymous = anonymousBefore;
|
|
1923
1932
|
chunkStartLine = lineNumber;
|
|
1924
1933
|
chunk = source.slice(lineStart, lineEnd) + '\n';
|
|
1925
1934
|
if (!flush()) return null;
|
package/src/program-analysis.js
CHANGED
|
@@ -44,8 +44,12 @@ export function componentHasCut(start, deps, groups) {
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
export function reachableIndexes(start, deps) {
|
|
47
|
+
return reachableFromRoots([start], deps);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function reachableFromRoots(roots, deps) {
|
|
47
51
|
const seen = new Set();
|
|
48
|
-
const stack = [
|
|
52
|
+
const stack = [...roots];
|
|
49
53
|
while (stack.length) {
|
|
50
54
|
const current = stack.pop();
|
|
51
55
|
if (seen.has(current)) continue;
|
|
@@ -55,6 +59,49 @@ export function reachableIndexes(start, deps) {
|
|
|
55
59
|
return seen;
|
|
56
60
|
}
|
|
57
61
|
|
|
62
|
+
// Classify the whole dependency graph once. Repeating a reachability walk for
|
|
63
|
+
// every predicate makes loading a long acyclic chain quadratic, even though
|
|
64
|
+
// recursion and cut propagation are properties of its components and edges.
|
|
65
|
+
export function analyzeDependencyGraph(deps, cutDeps, groups, multiplicityRoots, wfsEdges) {
|
|
66
|
+
const components = stronglyConnectedComponents(deps);
|
|
67
|
+
const componentByIndex = new Int32Array(groups.length);
|
|
68
|
+
const componentCuts = new Uint8Array(components.length);
|
|
69
|
+
const reverse = groups.map(() => []);
|
|
70
|
+
const reverseCuts = groups.map(() => []);
|
|
71
|
+
const cutRoots = [];
|
|
72
|
+
for (let component = 0; component < components.length; component++) {
|
|
73
|
+
for (const index of components[component]) {
|
|
74
|
+
componentByIndex[index] = component;
|
|
75
|
+
const group = groups[index];
|
|
76
|
+
if (!group.hasCut) continue;
|
|
77
|
+
// Preserve the distinction between a cut in a base clause and a cut
|
|
78
|
+
// in a directly recursive clause used by the solver's recursion plans.
|
|
79
|
+
const directRecursive = group.clauses.some((clause) => clauseIsDirectRecursive(clause, group));
|
|
80
|
+
if (group.clauses.some((clause) =>
|
|
81
|
+
(!directRecursive || clauseIsDirectRecursive(clause, group)) && clauseHasCut(clause))) {
|
|
82
|
+
componentCuts[component] = 1;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (let from = 0; from < groups.length; from++) {
|
|
87
|
+
for (const to of deps[from]) reverse[to].push(from);
|
|
88
|
+
for (const to of cutDeps[from]) reverseCuts[to].push(from);
|
|
89
|
+
if (groups[from].hasCut) cutRoots.push(from);
|
|
90
|
+
}
|
|
91
|
+
const wfsRoots = [];
|
|
92
|
+
for (const [from, to] of wfsEdges) {
|
|
93
|
+
if (componentByIndex[from] === componentByIndex[to]) wfsRoots.push(from, to);
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
components,
|
|
97
|
+
componentByIndex,
|
|
98
|
+
componentCuts,
|
|
99
|
+
cutReachable: reachableFromRoots(cutRoots, reverseCuts),
|
|
100
|
+
multiplicitySensitive: reachableFromRoots(multiplicityRoots, deps),
|
|
101
|
+
wfsReachable: reachableFromRoots(wfsRoots, reverse),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
58
105
|
// Returns the set of nodes in `candidates` that can reach `target` by
|
|
59
106
|
// traversing `deps` in reverse. This is equivalent to asking which nodes
|
|
60
107
|
// in the forward-reachable set from `target` also have `target` in their
|
|
@@ -509,36 +556,47 @@ export function stronglyConnectedComponents(adjacency) {
|
|
|
509
556
|
const lowlinks = new Map();
|
|
510
557
|
const components = [];
|
|
511
558
|
|
|
512
|
-
function
|
|
559
|
+
function enter(v) {
|
|
513
560
|
indexes.set(v, index);
|
|
514
561
|
lowlinks.set(v, index);
|
|
515
562
|
index++;
|
|
516
563
|
stack.push(v);
|
|
517
564
|
onStack.add(v);
|
|
518
565
|
|
|
519
|
-
|
|
520
|
-
if (!indexes.has(w)) {
|
|
521
|
-
visit(w);
|
|
522
|
-
lowlinks.set(v, Math.min(lowlinks.get(v), lowlinks.get(w)));
|
|
523
|
-
} else if (onStack.has(w)) {
|
|
524
|
-
lowlinks.set(v, Math.min(lowlinks.get(v), indexes.get(w)));
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
if (lowlinks.get(v) === indexes.get(v)) {
|
|
529
|
-
const component = [];
|
|
530
|
-
while (true) {
|
|
531
|
-
const w = stack.pop();
|
|
532
|
-
onStack.delete(w);
|
|
533
|
-
component.push(w);
|
|
534
|
-
if (w === v) break;
|
|
535
|
-
}
|
|
536
|
-
components.push(component);
|
|
537
|
-
}
|
|
566
|
+
return { node: v, edges: adjacency[v][Symbol.iterator]() };
|
|
538
567
|
}
|
|
539
568
|
|
|
540
569
|
for (let v = 0; v < adjacency.length; v++) {
|
|
541
|
-
if (
|
|
570
|
+
if (indexes.has(v)) continue;
|
|
571
|
+
// Explicit DFS frames keep preparation safe for dependency chains deeper
|
|
572
|
+
// than the JavaScript stack, including large generated taxonomies.
|
|
573
|
+
const frames = [enter(v)];
|
|
574
|
+
while (frames.length > 0) {
|
|
575
|
+
const frame = frames[frames.length - 1];
|
|
576
|
+
const next = frame.edges.next();
|
|
577
|
+
if (!next.done) {
|
|
578
|
+
const w = next.value;
|
|
579
|
+
if (!indexes.has(w)) frames.push(enter(w));
|
|
580
|
+
else if (onStack.has(w)) lowlinks.set(frame.node, Math.min(lowlinks.get(frame.node), indexes.get(w)));
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
const node = frame.node;
|
|
584
|
+
if (lowlinks.get(node) === indexes.get(node)) {
|
|
585
|
+
const component = [];
|
|
586
|
+
let w;
|
|
587
|
+
do {
|
|
588
|
+
w = stack.pop();
|
|
589
|
+
onStack.delete(w);
|
|
590
|
+
component.push(w);
|
|
591
|
+
} while (w !== node);
|
|
592
|
+
components.push(component);
|
|
593
|
+
}
|
|
594
|
+
frames.pop();
|
|
595
|
+
if (frames.length > 0) {
|
|
596
|
+
const parent = frames[frames.length - 1].node;
|
|
597
|
+
lowlinks.set(parent, Math.min(lowlinks.get(parent), lowlinks.get(node)));
|
|
598
|
+
}
|
|
599
|
+
}
|
|
542
600
|
}
|
|
543
601
|
return components;
|
|
544
602
|
}
|
package/src/program.js
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
export { selectClauseCandidates, selectClauseCandidatesForValues, selectGroundClauseCandidates } from './program-indexing.js';
|
|
33
33
|
import {
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
analyzeDependencyGraph, datalogDependencyClauseCount, isFiniteDatalogGroup, isRangeRestrictedFiniteDatalogGroup, isFiniteWfsDatalogGroup, inferStructuralInputPositions, hasStrictListTailRecursion, hasLinearNumericRecursion, isPiAccumulator, isPortableBetweenGenerator, directGoalDependencyKey, collectGoalDependencies, stronglyConnectedComponents, computeNegationStrata,
|
|
36
36
|
} from './program-analysis.js';
|
|
37
37
|
|
|
38
38
|
const RE_DECIMAL_INT = /^\d+$/;
|
|
@@ -434,7 +434,6 @@ export class Program {
|
|
|
434
434
|
const deps = groups.map(() => new Set());
|
|
435
435
|
const cutDeps = groups.map(() => new Set());
|
|
436
436
|
const multiplicitySensitiveRoots = new Set();
|
|
437
|
-
const negativeEdges = [];
|
|
438
437
|
const wfsNegativeEdges = [];
|
|
439
438
|
for (const group of groups) {
|
|
440
439
|
const groupIndex = indexByGroup.get(group);
|
|
@@ -465,7 +464,6 @@ export class Program {
|
|
|
465
464
|
const dependencyIndex = indexByGroup.get(dep);
|
|
466
465
|
deps[groupIndex].add(dependencyIndex);
|
|
467
466
|
if (dependency.negative) {
|
|
468
|
-
negativeEdges.push([groupIndex, dependencyIndex]);
|
|
469
467
|
if (dependency.wfs === true) wfsNegativeEdges.push([groupIndex, dependencyIndex]);
|
|
470
468
|
}
|
|
471
469
|
}
|
|
@@ -480,6 +478,7 @@ export class Program {
|
|
|
480
478
|
cutDeps[groupIndex].add(dependencyIndex);
|
|
481
479
|
}
|
|
482
480
|
}
|
|
481
|
+
const graph = analyzeDependencyGraph(deps, cutDeps, groups, multiplicitySensitiveRoots, wfsNegativeEdges);
|
|
483
482
|
const finiteDatalogCache = new Map();
|
|
484
483
|
const rangeRestrictedDatalogCache = new Map();
|
|
485
484
|
for (const group of groups) {
|
|
@@ -487,19 +486,10 @@ export class Program {
|
|
|
487
486
|
const standardLibraryModule = group.module !== 'user' &&
|
|
488
487
|
this.modules.get(group.module)?.filename?.startsWith('src/lib/');
|
|
489
488
|
group.bundledLibrary = standardLibraryModule === true;
|
|
490
|
-
group.cutReachable =
|
|
491
|
-
const
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
while (stack.length && !recursive) {
|
|
495
|
-
const current = stack.pop();
|
|
496
|
-
if (seen.has(current)) continue;
|
|
497
|
-
seen.add(current);
|
|
498
|
-
for (const next of deps[current]) {
|
|
499
|
-
if (next === start) { recursive = true; break; }
|
|
500
|
-
if (!seen.has(next)) stack.push(next);
|
|
501
|
-
}
|
|
502
|
-
}
|
|
489
|
+
group.cutReachable = graph.cutReachable.has(start);
|
|
490
|
+
const component = graph.componentByIndex[start];
|
|
491
|
+
const componentSize = graph.components[component].length;
|
|
492
|
+
const recursive = componentSize > 1 || deps[start].has(start);
|
|
503
493
|
// Bundled libraries use their written ISO control directly. User modules
|
|
504
494
|
// are analyzed for recursion, but tabling itself is strictly opt-in.
|
|
505
495
|
const plannedRecursive = recursive && !standardLibraryModule;
|
|
@@ -509,18 +499,16 @@ export class Program {
|
|
|
509
499
|
group.tableInputPositions = explicitlyTabled && plannedRecursive
|
|
510
500
|
? inferStructuralInputPositions(group)
|
|
511
501
|
: [];
|
|
512
|
-
group.cutRecursive = plannedRecursive &&
|
|
502
|
+
group.cutRecursive = plannedRecursive && graph.componentCuts[component] === 1;
|
|
513
503
|
const linearNumeric = plannedRecursive && !explicitlyTabled && hasLinearNumericRecursion(group) &&
|
|
514
504
|
(isPiAccumulator(group) || isPortableBetweenGenerator(group));
|
|
515
505
|
group.linearNumeric = linearNumeric;
|
|
516
506
|
group.fastPi = linearNumeric && isPiAccumulator(group);
|
|
517
|
-
const directRecursiveComponent = plannedRecursive &&
|
|
518
|
-
dependency === start || !reachableIndexes(dependency, deps).has(start)
|
|
519
|
-
);
|
|
507
|
+
const directRecursiveComponent = plannedRecursive && componentSize === 1;
|
|
520
508
|
group.listTailRecursive = directRecursiveComponent && !group.cutRecursive &&
|
|
521
509
|
hasStrictListTailRecursion(group);
|
|
522
510
|
const cutTailRecursion = plannedRecursive && hasDirectCutTailRecursion(group);
|
|
523
|
-
const multiplicitySensitive =
|
|
511
|
+
const multiplicitySensitive = graph.multiplicitySensitive.has(start);
|
|
524
512
|
group.cutTailRecursion = cutTailRecursion;
|
|
525
513
|
group.multiplicitySensitive = multiplicitySensitive;
|
|
526
514
|
group.depthFirstRecursive = plannedRecursive && !explicitlyTabled;
|
|
@@ -542,17 +530,10 @@ export class Program {
|
|
|
542
530
|
// not silently change ordinary Prolog programs. A group is WFS-backed when
|
|
543
531
|
// its dependency cone reaches an SCC containing an edge introduced by
|
|
544
532
|
// tnot/1 and the whole cone is range-restricted finite Datalog.
|
|
545
|
-
const wfsCycleNodes = new Set();
|
|
546
|
-
for (const [from, to] of wfsNegativeEdges) {
|
|
547
|
-
if (!reachableIndexes(to, deps).has(from)) continue;
|
|
548
|
-
wfsCycleNodes.add(from);
|
|
549
|
-
wfsCycleNodes.add(to);
|
|
550
|
-
}
|
|
551
533
|
const finiteWfsCache = new Map();
|
|
552
534
|
for (const group of groups) {
|
|
553
535
|
const start = indexByGroup.get(group);
|
|
554
|
-
const
|
|
555
|
-
const reachesWfsCycle = [...wfsCycleNodes].some((index) => reachable.has(index));
|
|
536
|
+
const reachesWfsCycle = graph.wfsReachable.has(start);
|
|
556
537
|
group.wfsDatalog = reachesWfsCycle && isFiniteWfsDatalogGroup(this, group, finiteWfsCache);
|
|
557
538
|
}
|
|
558
539
|
}
|
|
@@ -1288,18 +1269,27 @@ function collectAutoloadClosureDependencies(closure, extraArguments, out, module
|
|
|
1288
1269
|
|
|
1289
1270
|
function expandAutoloadMetaDependencies(program, dependencies, module = 'user') {
|
|
1290
1271
|
const seen = new WeakMap();
|
|
1272
|
+
// Most dependencies name a predicate without closure arguments; look each
|
|
1273
|
+
// predicate up once instead of once per call site.
|
|
1274
|
+
const closureModes = new Map();
|
|
1291
1275
|
for (let i = 0; i < dependencies.length; i++) {
|
|
1292
1276
|
const dependency = dependencies[i];
|
|
1293
1277
|
if (dependency.goal == null) continue;
|
|
1294
1278
|
const caller = dependency.module ?? module;
|
|
1295
1279
|
const visitKey = `${caller}\u0000${dependency.key}`;
|
|
1280
|
+
let modes = closureModes.get(visitKey);
|
|
1281
|
+
if (modes === undefined) {
|
|
1282
|
+
const group = program.findGroup(dependency.name, dependency.arity, caller);
|
|
1283
|
+
modes = (group?.metaArgumentModes ?? []).filter((mode) =>
|
|
1284
|
+
mode.kind === 'closure' && Number.isSafeInteger(mode.extraArguments) && mode.extraArguments >= 0);
|
|
1285
|
+
closureModes.set(visitKey, modes);
|
|
1286
|
+
}
|
|
1287
|
+
if (modes.length === 0) continue;
|
|
1296
1288
|
const visits = seen.get(dependency.goal) ?? new Set();
|
|
1297
1289
|
if (visits.has(visitKey)) continue;
|
|
1298
1290
|
visits.add(visitKey);
|
|
1299
1291
|
seen.set(dependency.goal, visits);
|
|
1300
|
-
const
|
|
1301
|
-
for (const mode of group?.metaArgumentModes ?? []) {
|
|
1302
|
-
if (mode.kind !== 'closure' || !Number.isSafeInteger(mode.extraArguments) || mode.extraArguments < 0) continue;
|
|
1292
|
+
for (const mode of modes) {
|
|
1303
1293
|
collectAutoloadClosureDependencies(dependency.goal?.args[mode.index], mode.extraArguments, dependencies, caller);
|
|
1304
1294
|
}
|
|
1305
1295
|
}
|
|
@@ -1735,13 +1725,17 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
|
|
|
1735
1725
|
if (fast) {
|
|
1736
1726
|
const acceptBinary = (headName, head0Type, head0Name, head1Type, head1Name,
|
|
1737
1727
|
bodyName, body0Type, body0Name, body1Type, body1Name) => {
|
|
1738
|
-
batch.
|
|
1728
|
+
if (batch.length >= PROGRAM_BUILD_BATCH_SIZE) flush();
|
|
1729
|
+
const clause = new CompactBinaryClause(
|
|
1739
1730
|
headName, head0Type, head0Name, head1Type, head1Name,
|
|
1740
1731
|
bodyName, body0Type, body0Name, body1Type, body1Name,
|
|
1741
|
-
)
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1732
|
+
);
|
|
1733
|
+
clause.module = context.module;
|
|
1734
|
+
clause.textUnit = context.textUnit;
|
|
1735
|
+
batch.push(clause);
|
|
1736
|
+
// The parser numbers the clause for its source metadata once it is
|
|
1737
|
+
// returned, so the batch is flushed at the next clause, not here.
|
|
1738
|
+
return clause;
|
|
1745
1739
|
};
|
|
1746
1740
|
const parsed = tryParseClausesFastInto(source, processClause, acceptBinary, options);
|
|
1747
1741
|
if (parsed) flush();
|
package/src/result-format.js
CHANGED
|
@@ -158,11 +158,24 @@ function renameMinted(value, minted) {
|
|
|
158
158
|
if (!minted.has(key)) minted.set(key, minted.size + 1);
|
|
159
159
|
return compound('$stream', [numberTerm(BigInt(minted.get(key)))]);
|
|
160
160
|
}
|
|
161
|
-
return
|
|
161
|
+
return sameOrRebuilt(value, (arg) => renameMinted(arg, minted));
|
|
162
162
|
}
|
|
163
163
|
return value;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
// A compound with `rewrite` applied to its arguments: the compound itself
|
|
167
|
+
// when no argument changes, so ground subterms are shared, not copied.
|
|
168
|
+
function sameOrRebuilt(value, rewrite) {
|
|
169
|
+
let args = null;
|
|
170
|
+
for (let i = 0; i < value.args.length; i++) {
|
|
171
|
+
const arg = value.args[i];
|
|
172
|
+
const rewritten = rewrite(arg);
|
|
173
|
+
if (args == null && rewritten !== arg) args = value.args.slice(0, i);
|
|
174
|
+
if (args != null) args.push(rewritten);
|
|
175
|
+
}
|
|
176
|
+
return args == null ? value : compound(value.name, args);
|
|
177
|
+
}
|
|
178
|
+
|
|
166
179
|
function templateTerm(value, anonymous, minted) {
|
|
167
180
|
if (value.type === VAR) {
|
|
168
181
|
if (value.name !== '_' && !value.name.startsWith('_')) {
|
|
@@ -172,7 +185,7 @@ function templateTerm(value, anonymous, minted) {
|
|
|
172
185
|
if (!anonymous.has(value.name)) anonymous.set(value.name, anonymous.size + 1);
|
|
173
186
|
return compound('anonymous', [numberTerm(BigInt(anonymous.get(value.name)))]);
|
|
174
187
|
}
|
|
175
|
-
if (value.type === COMPOUND) return
|
|
188
|
+
if (value.type === COMPOUND) return sameOrRebuilt(value, (arg) => templateTerm(arg, anonymous, minted));
|
|
176
189
|
return value;
|
|
177
190
|
}
|
|
178
191
|
|
|
@@ -192,8 +205,16 @@ function clauseTerm(number, clause, minted) {
|
|
|
192
205
|
|
|
193
206
|
// The `clause/3` record a proof writes for clause `number`, which a checker
|
|
194
207
|
// rebuilds from the source to compare with the document's (C1).
|
|
208
|
+
// The record is kept with the clause, so a proof checked as it is written
|
|
209
|
+
// compares its records with the very terms the checker rebuilds.
|
|
210
|
+
const clauseRecords = new WeakMap();
|
|
211
|
+
|
|
195
212
|
export function clauseRecordTerm(number, clause) {
|
|
196
|
-
|
|
213
|
+
const cached = clauseRecords.get(clause);
|
|
214
|
+
if (cached?.number === number) return cached.record;
|
|
215
|
+
const record = clauseTerm(number, clause, new Map());
|
|
216
|
+
clauseRecords.set(clause, { number, record });
|
|
217
|
+
return record;
|
|
197
218
|
}
|
|
198
219
|
|
|
199
220
|
// A `clause/3` record with its minted variable names renumbered from the
|
package/src/write.js
CHANGED
|
@@ -250,7 +250,11 @@ function printableReadVariableNames(term, env, explicit) {
|
|
|
250
250
|
const used = new Set(names.values());
|
|
251
251
|
const suffixes = new Map();
|
|
252
252
|
const seenVariables = new Set();
|
|
253
|
-
|
|
253
|
+
// A subterm shared many times over is walked once. Most terms are small
|
|
254
|
+
// trees, where walking a subterm again changes nothing, so the set is only
|
|
255
|
+
// kept once a term turns out to be large.
|
|
256
|
+
let seenTerms = null;
|
|
257
|
+
let walked = 0;
|
|
254
258
|
const stack = [term];
|
|
255
259
|
|
|
256
260
|
while (stack.length) {
|
|
@@ -268,8 +272,12 @@ function printableReadVariableNames(term, env, explicit) {
|
|
|
268
272
|
used.add(candidate);
|
|
269
273
|
continue;
|
|
270
274
|
}
|
|
271
|
-
if (current.type !== COMPOUND
|
|
272
|
-
seenTerms
|
|
275
|
+
if (current.type !== COMPOUND) continue;
|
|
276
|
+
if (seenTerms == null && ++walked > 4096) seenTerms = new Set();
|
|
277
|
+
if (seenTerms != null) {
|
|
278
|
+
if (seenTerms.has(current)) continue;
|
|
279
|
+
seenTerms.add(current);
|
|
280
|
+
}
|
|
273
281
|
for (let i = current.arity - 1; i >= 0; i--) stack.push(current.args[i]);
|
|
274
282
|
}
|
|
275
283
|
|
package/test/README.md
CHANGED
|
@@ -58,4 +58,8 @@ deliberately, and say why, never to make a transient failure disappear.
|
|
|
58
58
|
There is no separate wall-clock benchmark harness: `npm test`'s own elapsed
|
|
59
59
|
time is the project's performance indicator.
|
|
60
60
|
|
|
61
|
+
Example, proof-example, and proof-checking tests print results as they finish,
|
|
62
|
+
so long-running proofs do not hide completed tests behind alphabetical order.
|
|
63
|
+
Every test still compares its results with the same golden files.
|
|
64
|
+
|
|
61
65
|
See the [conformance guide](conformance/README.md) for report maintenance.
|