eyelang 4.0.6 → 4.0.8
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 +4 -0
- package/bin/eyelang.js +1 -1
- package/conformance-report.md +3 -3
- package/docs/guide.md +12 -11
- package/docs/language-reference.md +22 -16
- package/examples/abstract-interpretation.pl +0 -1
- package/examples/bayes-therapy.pl +1 -4
- package/examples/binomial-vandermonde.pl +1 -2
- package/examples/catalan-convolution.pl +1 -2
- package/examples/chart-parser.pl +1 -2
- package/examples/collatz-1000.pl +1 -2
- package/examples/continued-fraction-sqrt2.pl +1 -2
- package/examples/critical-path-schedule.pl +0 -2
- package/examples/dijkstra-risk-path.pl +0 -1
- package/examples/drone-corridor-planner.pl +0 -1
- package/examples/equality-saturation.pl +0 -2
- package/examples/family-cousins.pl +0 -3
- package/examples/fibonacci.pl +0 -1
- package/examples/gd-step-certified.pl +0 -7
- package/examples/gps.pl +0 -2
- package/examples/hamiltonian-path.pl +0 -1
- package/examples/hanoi.pl +0 -1
- package/examples/integer-partitions.pl +0 -1
- package/examples/intuitionistic-logic-kripke.pl +0 -2
- package/examples/knuth-bendix-completion.pl +0 -1
- package/examples/language.pl +1 -2
- package/examples/matrix-chain-order.pl +1 -2
- package/examples/modular-exponentiation.pl +0 -1
- package/examples/monkey-bananas.pl +0 -1
- package/examples/n-queens-8.pl +0 -1
- package/examples/output/chart-parser.pl +1 -1
- package/examples/partial-evaluator.pl +0 -1
- package/examples/peano-calculus.pl +0 -3
- package/examples/pell-equation.pl +1 -2
- package/examples/pointer-analysis.pl +0 -2
- package/examples/prime-range.pl +0 -1
- package/examples/proof/chart-parser.pl +91 -77
- package/examples/proof/hanoi.pl +16 -16
- package/examples/proof/intuitionistic-logic-kripke.pl +16 -16
- package/examples/route-planning.pl +0 -1
- package/examples/stirling-bell-numbers.pl +0 -4
- package/examples/totient-summatory.pl +1 -3
- package/examples/truth-maintenance-system.pl +0 -1
- package/examples/type-inference.pl +0 -1
- package/examples/web-names.pl +0 -1
- package/examples/weighted-interval-scheduling.pl +2 -3
- package/examples/wolf-goat-cabbage.pl +0 -1
- package/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/explain.js +3 -1
- package/src/program.js +88 -9
- package/src/solver.js +95 -46
- package/test/conformance/cases/{table/038_table_declaration.pl → automatic-tabling/038_automatic_tabling.pl} +1 -2
- package/test/conformance/cases/{table/102_table_declaration.pl → automatic-tabling/102_recursive_inference.pl} +1 -2
- package/test/conformance/cases/{table/tabled_left_recursion_path.pl → automatic-tabling/automatic_left_recursion_path.pl} +0 -1
- package/test/conformance/cases/{table/table_does_not_change_answers.pl → automatic-tabling/automatic_tabling_preserves_answers.pl} +1 -2
- package/test/conformance/cases/{table/extra_table_bound_cycle.pl → automatic-tabling/extra_automatic_bound_cycle.pl} +0 -1
- package/test/conformance/cases/{table/extra_table_open_call_fallback.pl → automatic-tabling/extra_automatic_open_call_fallback.pl} +0 -1
- package/test/conformance/cases/rules/transitive_rule_three_edges.pl +0 -1
- package/test/conformance/expected/{table/tabled_left_recursion_path.pl → automatic-tabling/automatic_left_recursion_path.pl} +1 -0
- package/test/conformance/expected-proofs/proofs/{table_recursive.pl → automatic_recursive.pl} +8 -8
- package/test/conformance/expected-proofs/proofs/{extra_table_path_bound.pl → extra_automatic_path_bound.pl} +8 -8
- package/test/conformance/proofs/proofs/{table_recursive.pl → automatic_recursive.pl} +1 -2
- package/test/conformance/proofs/proofs/{extra_table_path_bound.pl → extra_automatic_path_bound.pl} +0 -1
- package/test/conformance/warnings/negation/{extra_stratified_negation_with_table_quiet.pl → extra_stratified_negation_with_recursion_quiet.pl} +0 -1
- package/test/run-regression.mjs +52 -11
- package/test/conformance/cases/declarations/table_is_also_fact.pl +0 -4
- package/test/conformance/expected/declarations/table_is_also_fact.pl +0 -1
- /package/test/conformance/expected/{table/038_table_declaration.pl → automatic-tabling/038_automatic_tabling.pl} +0 -0
- /package/test/conformance/expected/{table/102_table_declaration.pl → automatic-tabling/102_recursive_inference.pl} +0 -0
- /package/test/conformance/expected/{table/table_does_not_change_answers.pl → automatic-tabling/automatic_tabling_preserves_answers.pl} +0 -0
- /package/test/conformance/expected/{table/extra_table_bound_cycle.pl → automatic-tabling/extra_automatic_bound_cycle.pl} +0 -0
- /package/test/conformance/expected/{table/extra_table_open_call_fallback.pl → automatic-tabling/extra_automatic_open_call_fallback.pl} +0 -0
- /package/test/conformance/expected-warnings/negation/{extra_stratified_negation_with_table_quiet.pl → extra_stratified_negation_with_recursion_quiet.pl} +0 -0
- /package/test/conformance/expected-warnings/negation/{extra_stratified_negation_with_table_quiet.txt → extra_stratified_negation_with_recursion_quiet.txt} +0 -0
|
@@ -3,30 +3,30 @@ why(
|
|
|
3
3
|
intuitionistic_truth(monotone_p_reaches_both, both, atom(p)),
|
|
4
4
|
proof(
|
|
5
5
|
goal(intuitionistic_truth(monotone_p_reaches_both, both, atom(p))),
|
|
6
|
-
by(rule("intuitionistic-logic-kripke.pl", clause(
|
|
6
|
+
by(rule("intuitionistic-logic-kripke.pl", clause(22))),
|
|
7
7
|
uses([
|
|
8
8
|
proof(
|
|
9
9
|
goal(forces(both, atom(p))),
|
|
10
|
-
by(rule("intuitionistic-logic-kripke.pl", clause(
|
|
10
|
+
by(rule("intuitionistic-logic-kripke.pl", clause(15))),
|
|
11
11
|
bindings([binding("World", both), binding("Prop", p), binding("Someworld", left)]),
|
|
12
12
|
uses([
|
|
13
13
|
proof(
|
|
14
14
|
goal(leq(left, both)),
|
|
15
|
-
by(rule("intuitionistic-logic-kripke.pl", clause(
|
|
15
|
+
by(rule("intuitionistic-logic-kripke.pl", clause(14))),
|
|
16
16
|
bindings([binding("From", left), binding("To", both), binding("Mid", both)]),
|
|
17
17
|
uses([
|
|
18
18
|
proof(
|
|
19
19
|
goal(step(left, both)),
|
|
20
|
-
by(fact("intuitionistic-logic-kripke.pl", clause(
|
|
20
|
+
by(fact("intuitionistic-logic-kripke.pl", clause(9)))
|
|
21
21
|
),
|
|
22
22
|
proof(
|
|
23
23
|
goal(leq(both, both)),
|
|
24
|
-
by(rule("intuitionistic-logic-kripke.pl", clause(
|
|
24
|
+
by(rule("intuitionistic-logic-kripke.pl", clause(13))),
|
|
25
25
|
bindings([binding("World", both)]),
|
|
26
26
|
uses([
|
|
27
27
|
proof(
|
|
28
28
|
goal(world(both)),
|
|
29
|
-
by(fact("intuitionistic-logic-kripke.pl", clause(
|
|
29
|
+
by(fact("intuitionistic-logic-kripke.pl", clause(6)))
|
|
30
30
|
)
|
|
31
31
|
])
|
|
32
32
|
)
|
|
@@ -34,7 +34,7 @@ why(
|
|
|
34
34
|
),
|
|
35
35
|
proof(
|
|
36
36
|
goal(base(left, p)),
|
|
37
|
-
by(fact("intuitionistic-logic-kripke.pl", clause(
|
|
37
|
+
by(fact("intuitionistic-logic-kripke.pl", clause(11)))
|
|
38
38
|
)
|
|
39
39
|
])
|
|
40
40
|
)
|
|
@@ -47,16 +47,16 @@ why(
|
|
|
47
47
|
intuitionistic_truth(constructive_case_analysis, root, implies(atom(p), or(atom(p), atom(q)))),
|
|
48
48
|
proof(
|
|
49
49
|
goal(intuitionistic_truth(constructive_case_analysis, root, implies(atom(p), or(atom(p), atom(q))))),
|
|
50
|
-
by(rule("intuitionistic-logic-kripke.pl", clause(
|
|
50
|
+
by(rule("intuitionistic-logic-kripke.pl", clause(23))),
|
|
51
51
|
uses([
|
|
52
52
|
proof(
|
|
53
53
|
goal(forces(root, implies(atom(p), or(atom(p), atom(q))))),
|
|
54
|
-
by(rule("intuitionistic-logic-kripke.pl", clause(
|
|
54
|
+
by(rule("intuitionistic-logic-kripke.pl", clause(19))),
|
|
55
55
|
bindings([binding("World", root), binding("Left", atom(p)), binding("Right", or(atom(p), atom(q)))]),
|
|
56
56
|
uses([
|
|
57
57
|
proof(
|
|
58
58
|
goal(world(root)),
|
|
59
|
-
by(fact("intuitionistic-logic-kripke.pl", clause(
|
|
59
|
+
by(fact("intuitionistic-logic-kripke.pl", clause(3)))
|
|
60
60
|
),
|
|
61
61
|
proof(
|
|
62
62
|
goal(not(bad_implication(root, atom(p), or(atom(p), atom(q))))),
|
|
@@ -73,21 +73,21 @@ why(
|
|
|
73
73
|
intuitionistic_truth(double_negated_branch_information, root, neg(neg(or(atom(p), atom(q))))),
|
|
74
74
|
proof(
|
|
75
75
|
goal(intuitionistic_truth(double_negated_branch_information, root, neg(neg(or(atom(p), atom(q)))))),
|
|
76
|
-
by(rule("intuitionistic-logic-kripke.pl", clause(
|
|
76
|
+
by(rule("intuitionistic-logic-kripke.pl", clause(24))),
|
|
77
77
|
uses([
|
|
78
78
|
proof(
|
|
79
79
|
goal(forces(root, neg(neg(or(atom(p), atom(q)))))),
|
|
80
|
-
by(rule("intuitionistic-logic-kripke.pl", clause(
|
|
80
|
+
by(rule("intuitionistic-logic-kripke.pl", clause(20))),
|
|
81
81
|
bindings([binding("World", root), binding("Formula", neg(or(atom(p), atom(q))))]),
|
|
82
82
|
uses([
|
|
83
83
|
proof(
|
|
84
84
|
goal(forces(root, implies(neg(or(atom(p), atom(q))), bottom))),
|
|
85
|
-
by(rule("intuitionistic-logic-kripke.pl", clause(
|
|
85
|
+
by(rule("intuitionistic-logic-kripke.pl", clause(19))),
|
|
86
86
|
bindings([binding("World", root), binding("Left", neg(or(atom(p), atom(q)))), binding("Right", bottom)]),
|
|
87
87
|
uses([
|
|
88
88
|
proof(
|
|
89
89
|
goal(world(root)),
|
|
90
|
-
by(fact("intuitionistic-logic-kripke.pl", clause(
|
|
90
|
+
by(fact("intuitionistic-logic-kripke.pl", clause(3)))
|
|
91
91
|
),
|
|
92
92
|
proof(
|
|
93
93
|
goal(not(bad_implication(root, neg(or(atom(p), atom(q))), bottom))),
|
|
@@ -106,7 +106,7 @@ why(
|
|
|
106
106
|
intuitionistic_countermodel(root_does_not_decide_branch, root, or(atom(p), atom(q))),
|
|
107
107
|
proof(
|
|
108
108
|
goal(intuitionistic_countermodel(root_does_not_decide_branch, root, or(atom(p), atom(q)))),
|
|
109
|
-
by(rule("intuitionistic-logic-kripke.pl", clause(
|
|
109
|
+
by(rule("intuitionistic-logic-kripke.pl", clause(25))),
|
|
110
110
|
uses([
|
|
111
111
|
proof(
|
|
112
112
|
goal(not(forces(root, or(atom(p), atom(q))))),
|
|
@@ -121,7 +121,7 @@ why(
|
|
|
121
121
|
intuitionistic_countermodel(excluded_middle_not_forced, root, or(atom(p), neg(atom(p)))),
|
|
122
122
|
proof(
|
|
123
123
|
goal(intuitionistic_countermodel(excluded_middle_not_forced, root, or(atom(p), neg(atom(p))))),
|
|
124
|
-
by(rule("intuitionistic-logic-kripke.pl", clause(
|
|
124
|
+
by(rule("intuitionistic-logic-kripke.pl", clause(26))),
|
|
125
125
|
uses([
|
|
126
126
|
proof(
|
|
127
127
|
goal(not(forces(root, or(atom(p), neg(atom(p)))))),
|
|
@@ -7,10 +7,6 @@
|
|
|
7
7
|
% The table declarations memoize the smaller helper relations used by both formulas.
|
|
8
8
|
materialize(stirling_bell_answer, 2).
|
|
9
9
|
|
|
10
|
-
table(factorial, 2).
|
|
11
|
-
table(binomial, 3).
|
|
12
|
-
table(stirling2, 3).
|
|
13
|
-
table(bell, 2).
|
|
14
10
|
|
|
15
11
|
factorial(0, 1).
|
|
16
12
|
factorial(N, Value) :-
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
% Euler totients and coprimality by tabled Euclidean gcd.
|
|
1
|
+
% Euler totients and coprimality by automatically tabled Euclidean gcd.
|
|
2
2
|
%
|
|
3
3
|
% phi(N) is modeled directly as the count of integers K in 1..N with gcd(N,K)=1.
|
|
4
4
|
% The summatory query reuses many gcd/totient subgoals, so memoization keeps the
|
|
5
5
|
% example responsive while preserving the relational presentation.
|
|
6
6
|
materialize(totient_answer, 2).
|
|
7
7
|
|
|
8
|
-
table(gcd, 3).
|
|
9
|
-
table(totient, 2).
|
|
10
8
|
|
|
11
9
|
% Euclid's algorithm is expressed recursively over remainders.
|
|
12
10
|
gcd(A, 0, A) :- ge(A, 0).
|
package/examples/web-names.pl
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
% Weighted interval scheduling via tabled dynamic programming.
|
|
1
|
+
% Weighted interval scheduling via automatically tabled dynamic programming.
|
|
2
2
|
%
|
|
3
3
|
% Intervals are ordered by finish time. best_from(I,Best) compares the two
|
|
4
4
|
% choices at position I: skip it, or take it and jump to the next compatible
|
|
5
|
-
% interval. chosen_from/2 then walks the
|
|
5
|
+
% interval. chosen_from/2 then walks the cached decisions to report a schedule.
|
|
6
6
|
materialize(weighted_interval_answer, 2).
|
|
7
7
|
|
|
8
|
-
table(best_from, 2).
|
|
9
8
|
|
|
10
9
|
last_interval(8).
|
|
11
10
|
sentinel(9).
|
|
@@ -10,7 +10,6 @@ materialize(solution, 2).
|
|
|
10
10
|
materialize(solved, 2).
|
|
11
11
|
|
|
12
12
|
% Program structure: facts set up the scenario, and rules derive the materialized conclusions.
|
|
13
|
-
table(solve, 4).
|
|
14
13
|
|
|
15
14
|
% Derivation rules: each rule below contributes one logical step toward the displayed results.
|
|
16
15
|
solution(Moves) :-
|
package/index.d.ts
CHANGED
package/package.json
CHANGED
package/src/explain.js
CHANGED
|
@@ -69,7 +69,9 @@ function* proveGoalAll(program, goal, env, depth, maxDepth, registry, active) {
|
|
|
69
69
|
|
|
70
70
|
const group = program.findGroup(goal.name, goal.arity);
|
|
71
71
|
if (!group) return;
|
|
72
|
-
|
|
72
|
+
// Explanation replay does not use the solver's answer tables, so its cycle
|
|
73
|
+
// guard applies even when normal execution tables this predicate.
|
|
74
|
+
if (activeVariant(goal, env, active)) return;
|
|
73
75
|
|
|
74
76
|
const candidates = selectClauseCandidates(group, goal, env);
|
|
75
77
|
for (const pass of [candidates.primary, candidates.fallback]) {
|
package/src/program.js
CHANGED
|
@@ -44,6 +44,7 @@ export class Program {
|
|
|
44
44
|
mode: null,
|
|
45
45
|
determinism: null,
|
|
46
46
|
recursive: false,
|
|
47
|
+
tableInputPositions: [],
|
|
47
48
|
scalarFactsOnly: true,
|
|
48
49
|
negationStratum: null,
|
|
49
50
|
};
|
|
@@ -84,9 +85,7 @@ export class Program {
|
|
|
84
85
|
const indicator = declarationIndicator(h.args[0], h.args[1]);
|
|
85
86
|
if (!indicator) continue;
|
|
86
87
|
const group = this.groups.get(indicator.key);
|
|
87
|
-
if (h.name === '
|
|
88
|
-
if (group) group.tabled = true;
|
|
89
|
-
} else if (h.name === 'materialize') {
|
|
88
|
+
if (h.name === 'materialize') {
|
|
90
89
|
this.hasMaterialize = true;
|
|
91
90
|
this.materializedGroups.add(indicator.key);
|
|
92
91
|
} else if ((h.name === 'det' || h.name === 'semidet') && group) {
|
|
@@ -105,7 +104,10 @@ export class Program {
|
|
|
105
104
|
}
|
|
106
105
|
}
|
|
107
106
|
}
|
|
108
|
-
|
|
107
|
+
// Hybrid planning is part of normal execution, so dependency analysis is
|
|
108
|
+
// always performed. `markRecursive` is retained as a compatible parse
|
|
109
|
+
// option but no longer disables the engine's automatic table decisions.
|
|
110
|
+
this.markRecursivePredicates();
|
|
109
111
|
if (options.analyzeNegation === true || options.strictNegation === true) this.analyzeNegationStratification();
|
|
110
112
|
if (options.strictNegation === true) this.assertStratifiedNegation();
|
|
111
113
|
}
|
|
@@ -116,14 +118,19 @@ export class Program {
|
|
|
116
118
|
const groups = [...this.groups.values()];
|
|
117
119
|
const indexByGroup = new Map(groups.map((group, i) => [group, i]));
|
|
118
120
|
const deps = groups.map(() => new Set());
|
|
121
|
+
const negativeEdges = [];
|
|
119
122
|
for (const group of groups) {
|
|
120
123
|
const groupIndex = indexByGroup.get(group);
|
|
121
124
|
for (const clause of group.clauses) {
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
for (const goal of clause.body) {
|
|
126
|
+
for (const dependency of collectGoalDependencies(goal, false)) {
|
|
127
|
+
const dep = this.groups.get(dependency.key);
|
|
128
|
+
if (dep) {
|
|
129
|
+
const dependencyIndex = indexByGroup.get(dep);
|
|
130
|
+
deps[groupIndex].add(dependencyIndex);
|
|
131
|
+
if (dependency.negative) negativeEdges.push([groupIndex, dependencyIndex]);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
127
134
|
}
|
|
128
135
|
}
|
|
129
136
|
}
|
|
@@ -142,6 +149,14 @@ export class Program {
|
|
|
142
149
|
}
|
|
143
150
|
}
|
|
144
151
|
group.recursive = recursive;
|
|
152
|
+
group.tableInputPositions = recursive
|
|
153
|
+
? inferStructuralInputPositions(group)
|
|
154
|
+
: [];
|
|
155
|
+
// Recursive predicates are proved with tabling automatically, keeping
|
|
156
|
+
// search control inside the engine. Cycles through negation retain
|
|
157
|
+
// guarded resolution because positive least-fixed-point tabling is not
|
|
158
|
+
// sound for an unstratified negative component.
|
|
159
|
+
group.tabled = recursive && !componentHasNegativeEdge(start, deps, negativeEdges);
|
|
145
160
|
}
|
|
146
161
|
}
|
|
147
162
|
|
|
@@ -267,8 +282,65 @@ export class Program {
|
|
|
267
282
|
}
|
|
268
283
|
}
|
|
269
284
|
|
|
285
|
+
function componentHasNegativeEdge(start, deps, negativeEdges) {
|
|
286
|
+
const forward = reachableIndexes(start, deps);
|
|
287
|
+
const component = new Set([...forward].filter((index) => reachableIndexes(index, deps).has(start)));
|
|
288
|
+
return negativeEdges.some(([from, to]) => component.has(from) && component.has(to));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function reachableIndexes(start, deps) {
|
|
292
|
+
const seen = new Set();
|
|
293
|
+
const stack = [start];
|
|
294
|
+
while (stack.length) {
|
|
295
|
+
const current = stack.pop();
|
|
296
|
+
if (seen.has(current)) continue;
|
|
297
|
+
seen.add(current);
|
|
298
|
+
for (const next of deps[current]) if (!seen.has(next)) stack.push(next);
|
|
299
|
+
}
|
|
300
|
+
return seen;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function inferStructuralInputPositions(group) {
|
|
304
|
+
const patternedPositions = new Set();
|
|
305
|
+
const linkedInputPositions = new Set();
|
|
306
|
+
for (const clause of group.clauses) {
|
|
307
|
+
const recursiveGoals = clause.body.filter((goal) =>
|
|
308
|
+
goal.type === COMPOUND && goal.name === group.name && goal.arity === group.arity
|
|
309
|
+
);
|
|
310
|
+
if (recursiveGoals.length === 0) continue;
|
|
311
|
+
const clauseChangedPositions = new Set();
|
|
312
|
+
for (let index = 0; index < clause.head.args.length; index++) {
|
|
313
|
+
if (clause.head.args[index].type !== 'var') patternedPositions.add(index);
|
|
314
|
+
if (recursiveGoals.some((goal) => !sameClauseTerm(clause.head.args[index], goal.args[index]))) {
|
|
315
|
+
clauseChangedPositions.add(index);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
for (let index = 0; index < clause.head.args.length; index++) {
|
|
319
|
+
const headArg = clause.head.args[index];
|
|
320
|
+
if (headArg.type !== 'var' || !clauseChangedPositions.has(index)) continue;
|
|
321
|
+
if (clause.head.args.some((pattern, patternIndex) =>
|
|
322
|
+
patternIndex !== index && pattern.type !== 'var' && termContainsVariable(pattern, headArg.name)
|
|
323
|
+
)) linkedInputPositions.add(index);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (linkedInputPositions.size > 0) {
|
|
327
|
+
return [[...linkedInputPositions].sort((left, right) => left - right)[0]];
|
|
328
|
+
}
|
|
329
|
+
if (patternedPositions.size > 0) {
|
|
330
|
+
return [[...patternedPositions].sort((left, right) => left - right)[0]];
|
|
331
|
+
}
|
|
332
|
+
return Array.from({ length: group.arity }, (_, index) => index);
|
|
333
|
+
}
|
|
270
334
|
|
|
335
|
+
function termContainsVariable(term, name) {
|
|
336
|
+
if (term.type === 'var') return term.name === name;
|
|
337
|
+
return term.args.some((arg) => termContainsVariable(arg, name));
|
|
338
|
+
}
|
|
271
339
|
|
|
340
|
+
function sameClauseTerm(left, right) {
|
|
341
|
+
if (left.type !== right.type || left.name !== right.name || left.args.length !== right.args.length) return false;
|
|
342
|
+
return left.args.every((arg, index) => sameClauseTerm(arg, right.args[index]));
|
|
343
|
+
}
|
|
272
344
|
|
|
273
345
|
function termHasNoVariables(term) {
|
|
274
346
|
if (!term || term.type === 'var') return false;
|
|
@@ -295,6 +367,13 @@ function collectGoalDependencies(goal, negated) {
|
|
|
295
367
|
...collectGoalDependencies(goal.args[1], negated),
|
|
296
368
|
];
|
|
297
369
|
}
|
|
370
|
+
if ((goal.name === 'findall' || goal.name === 'sumall') && goal.arity === 3) {
|
|
371
|
+
return collectGoalDependencies(goal.args[1], negated);
|
|
372
|
+
}
|
|
373
|
+
if (goal.name === 'countall' && goal.arity === 2) return collectGoalDependencies(goal.args[0], negated);
|
|
374
|
+
if ((goal.name === 'aggregate_min' || goal.name === 'aggregate_max') && goal.arity === 5) {
|
|
375
|
+
return collectGoalDependencies(goal.args[2], negated);
|
|
376
|
+
}
|
|
298
377
|
return [{ key: `${goal.name}/${goal.arity}`, negative: negated }];
|
|
299
378
|
}
|
|
300
379
|
|
package/src/solver.js
CHANGED
|
@@ -19,6 +19,7 @@ export class Solver {
|
|
|
19
19
|
this.solutionsSeen = 0;
|
|
20
20
|
this.active = [];
|
|
21
21
|
this.memo = new Map();
|
|
22
|
+
this.tableCoordinator = null;
|
|
22
23
|
this.groundChainSuccess = new Set();
|
|
23
24
|
this.stats = {
|
|
24
25
|
completed_goal_lists: 0,
|
|
@@ -29,6 +30,7 @@ export class Solver {
|
|
|
29
30
|
max_goal_count: 0,
|
|
30
31
|
deterministic_builtin_successes: 0,
|
|
31
32
|
deterministic_builtin_failures: 0,
|
|
33
|
+
table_fixpoint_rounds: 0,
|
|
32
34
|
};
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -58,6 +60,21 @@ export class Solver {
|
|
|
58
60
|
const stack = [{ kind: 'goals', goals, env, depth, active: savedActive.slice() }];
|
|
59
61
|
while (stack.length) {
|
|
60
62
|
const frame = stack.pop();
|
|
63
|
+
if (frame.kind === 'completeTableFixpointRound') {
|
|
64
|
+
frame.entry.computing = false;
|
|
65
|
+
const answerCount = frame.entry.answers.length;
|
|
66
|
+
if (this.tableCoordinator?.cycleSeen && answerCount > frame.answerCountBefore) {
|
|
67
|
+
scheduleTableFixpointRound(stack, this, frame);
|
|
68
|
+
} else {
|
|
69
|
+
for (const entry of this.tableCoordinator?.entries ?? [frame.entry]) {
|
|
70
|
+
entry.computing = false;
|
|
71
|
+
entry.complete = true;
|
|
72
|
+
}
|
|
73
|
+
this.tableCoordinator = null;
|
|
74
|
+
pushMemoAnswerFrames(stack, frame.entry, frame.goal, frame.rest, frame.env, frame.depth, frame.active, this);
|
|
75
|
+
}
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
61
78
|
if (frame.kind === 'completeMemo') {
|
|
62
79
|
frame.entry.computing = false;
|
|
63
80
|
frame.entry.complete = true;
|
|
@@ -91,6 +108,7 @@ export class Solver {
|
|
|
91
108
|
}
|
|
92
109
|
if (first?.kind === 'memoStore') {
|
|
93
110
|
rememberMemoAnswer(first.entry, first.goal, env);
|
|
111
|
+
if (goals.length === 1) break;
|
|
94
112
|
goals = goals.slice(1);
|
|
95
113
|
continue;
|
|
96
114
|
}
|
|
@@ -135,24 +153,35 @@ export class Solver {
|
|
|
135
153
|
if (!group) break;
|
|
136
154
|
|
|
137
155
|
if (group.tabled) {
|
|
138
|
-
const key = memoKey(goal, env);
|
|
156
|
+
const key = memoKey(goal, env, group);
|
|
139
157
|
if (key.hasBound) {
|
|
140
158
|
const mapKey = `${goal.name}/${goal.arity}:${key.text}`;
|
|
141
159
|
let entry = this.memo.get(mapKey);
|
|
142
160
|
if (!entry) {
|
|
143
|
-
entry =
|
|
161
|
+
entry = makeMemoEntry();
|
|
144
162
|
this.memo.set(mapKey, entry);
|
|
145
163
|
}
|
|
164
|
+
if (this.tableCoordinator) this.tableCoordinator.entries.add(entry);
|
|
146
165
|
if (entry.complete) {
|
|
147
166
|
pushMemoAnswerFrames(stack, entry, goal, rest, env, depth, active, this);
|
|
148
167
|
break;
|
|
149
168
|
}
|
|
150
169
|
if (!entry.computing) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
170
|
+
if (!this.tableCoordinator) {
|
|
171
|
+
this.tableCoordinator = { entry, cycleSeen: false, entries: new Set([entry]) };
|
|
172
|
+
scheduleTableFixpointRound(stack, this, { entry, group, goal, rest, env, depth, active });
|
|
173
|
+
} else {
|
|
174
|
+
entry.computing = true;
|
|
175
|
+
stack.push({ kind: 'completeMemo', entry });
|
|
176
|
+
pushUserGoalUncachedFrames(stack, this, group, goal, [{ kind: 'memoStore', entry, goal }, ...rest], env, depth, active);
|
|
177
|
+
}
|
|
154
178
|
break;
|
|
155
179
|
}
|
|
180
|
+
if (this.tableCoordinator && activeVariantIn(goal, env, active)) {
|
|
181
|
+
this.tableCoordinator.cycleSeen = true;
|
|
182
|
+
}
|
|
183
|
+
pushMemoAnswerFrames(stack, entry, goal, rest, env, depth, active, this);
|
|
184
|
+
break;
|
|
156
185
|
}
|
|
157
186
|
}
|
|
158
187
|
|
|
@@ -184,43 +213,7 @@ export class Solver {
|
|
|
184
213
|
}
|
|
185
214
|
|
|
186
215
|
*solveMemoizedGoal(group, goal, rest, env, depth) {
|
|
187
|
-
|
|
188
|
-
// open call would require storing the entire relation and can mask useful
|
|
189
|
-
// goal-ordering behavior, so it falls back to the normal path.
|
|
190
|
-
const key = memoKey(goal, env);
|
|
191
|
-
if (!key.hasBound) {
|
|
192
|
-
yield* this.solveUserGoalUncached(group, goal, rest, env, depth);
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
const mapKey = `${goal.name}/${goal.arity}:${key.text}`;
|
|
196
|
-
let entry = this.memo.get(mapKey);
|
|
197
|
-
if (!entry) {
|
|
198
|
-
entry = { computing: false, complete: false, answers: [] };
|
|
199
|
-
this.memo.set(mapKey, entry);
|
|
200
|
-
}
|
|
201
|
-
if (!entry.complete && !entry.computing) {
|
|
202
|
-
entry.computing = true;
|
|
203
|
-
const collector = this.cloneForInnerGoal();
|
|
204
|
-
for (const answerEnv of collector.solveUserGoalUncached(group, goal, [], env.clone(), depth)) {
|
|
205
|
-
entry.answers.push(goal.args.map((arg) => importResolved(arg, answerEnv)));
|
|
206
|
-
}
|
|
207
|
-
entry.computing = false;
|
|
208
|
-
entry.complete = true;
|
|
209
|
-
}
|
|
210
|
-
if (!entry.complete && entry.computing) {
|
|
211
|
-
yield* this.solveUserGoalUncached(group, goal, rest, env, depth);
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
for (const answerArgs of entry.answers) {
|
|
215
|
-
const next = env.clone();
|
|
216
|
-
let ok = true;
|
|
217
|
-
for (let i = 0; i < goal.arity; i++) {
|
|
218
|
-
this.stats.unify_calls++;
|
|
219
|
-
if (!unify(goal.args[i], answerArgs[i], next)) { ok = false; break; }
|
|
220
|
-
}
|
|
221
|
-
if (ok) yield* this.solve(rest, next, depth + 1);
|
|
222
|
-
if (this.solutionsSeen >= this.solutionLimit) return;
|
|
223
|
-
}
|
|
216
|
+
yield* this.solve([goal, ...rest], env, depth);
|
|
224
217
|
}
|
|
225
218
|
|
|
226
219
|
*solveUserGoalUncached(group, goal, rest, env, depth) {
|
|
@@ -273,6 +266,42 @@ export class Solver {
|
|
|
273
266
|
|
|
274
267
|
}
|
|
275
268
|
|
|
269
|
+
function makeMemoEntry() {
|
|
270
|
+
return { computing: false, complete: false, answers: [], answerKeys: new Set() };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function scheduleTableFixpointRound(stack, solver, frame) {
|
|
274
|
+
solver.stats.table_fixpoint_rounds++;
|
|
275
|
+
solver.tableCoordinator.cycleSeen = false;
|
|
276
|
+
for (const entry of solver.tableCoordinator.entries) {
|
|
277
|
+
entry.computing = false;
|
|
278
|
+
entry.complete = false;
|
|
279
|
+
}
|
|
280
|
+
frame.entry.computing = true;
|
|
281
|
+
const nextFrame = {
|
|
282
|
+
kind: 'completeTableFixpointRound',
|
|
283
|
+
entry: frame.entry,
|
|
284
|
+
group: frame.group,
|
|
285
|
+
goal: frame.goal,
|
|
286
|
+
rest: frame.rest,
|
|
287
|
+
env: frame.env,
|
|
288
|
+
depth: frame.depth,
|
|
289
|
+
active: frame.active,
|
|
290
|
+
answerCountBefore: frame.entry.answers.length,
|
|
291
|
+
};
|
|
292
|
+
stack.push(nextFrame);
|
|
293
|
+
pushUserGoalUncachedFrames(
|
|
294
|
+
stack,
|
|
295
|
+
solver,
|
|
296
|
+
frame.group,
|
|
297
|
+
frame.goal,
|
|
298
|
+
[{ kind: 'memoStore', entry: frame.entry, goal: frame.goal }],
|
|
299
|
+
frame.env,
|
|
300
|
+
frame.depth,
|
|
301
|
+
frame.active,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
276
305
|
|
|
277
306
|
function pushMemoAnswerFrames(stack, entry, goal, rest, env, depth, active, solver) {
|
|
278
307
|
for (let answerIndex = entry.answers.length - 1; answerIndex >= 0; answerIndex--) {
|
|
@@ -639,7 +668,8 @@ function rememberGroundChainSuccess(solver, seen) {
|
|
|
639
668
|
|
|
640
669
|
function rememberMemoAnswer(entry, goal, env) {
|
|
641
670
|
const answerArgs = goal.args.map((arg) => importResolved(arg, env));
|
|
642
|
-
const
|
|
671
|
+
const variables = new Map();
|
|
672
|
+
const key = answerArgs.map((arg) => canonicalTermKey(arg, new Env(), variables)).join('\x1f');
|
|
643
673
|
if (entry.answerKeys.has(key)) return;
|
|
644
674
|
entry.answerKeys.add(key);
|
|
645
675
|
entry.answers.push(answerArgs);
|
|
@@ -689,16 +719,35 @@ function derefForLocal(term, env) {
|
|
|
689
719
|
return current;
|
|
690
720
|
}
|
|
691
721
|
|
|
692
|
-
function memoKey(goal, env) {
|
|
722
|
+
function memoKey(goal, env, group = null) {
|
|
693
723
|
let hasBound = false;
|
|
724
|
+
const variables = new Map();
|
|
725
|
+
const required = group?.tableInputPositions ?? [];
|
|
694
726
|
const parts = goal.args.map((arg) => {
|
|
695
727
|
const value = derefForLocal(arg, env);
|
|
696
|
-
if (value
|
|
697
|
-
return value.type === 'var' ? '_' :
|
|
728
|
+
if (termIsGround(value, env)) hasBound = true;
|
|
729
|
+
return value.type === 'var' ? '_' : canonicalTermKey(value, env, variables);
|
|
698
730
|
});
|
|
731
|
+
if (required.length > 0) {
|
|
732
|
+
hasBound = required.some((index) => termIsGround(goal.args[index], env));
|
|
733
|
+
}
|
|
699
734
|
return { hasBound, text: parts.join('|') };
|
|
700
735
|
}
|
|
701
736
|
|
|
737
|
+
function canonicalTermKey(term, env, variables) {
|
|
738
|
+
const value = derefForLocal(term, env);
|
|
739
|
+
if (value.type === 'var') {
|
|
740
|
+
let id = variables.get(value.name);
|
|
741
|
+
if (id == null) {
|
|
742
|
+
id = variables.size;
|
|
743
|
+
variables.set(value.name, id);
|
|
744
|
+
}
|
|
745
|
+
return `var:${id}`;
|
|
746
|
+
}
|
|
747
|
+
if (!value.args?.length) return `${value.type}:${value.name}`;
|
|
748
|
+
return `${value.type}:${value.name}(${value.args.map((arg) => canonicalTermKey(arg, env, variables)).join(',')})`;
|
|
749
|
+
}
|
|
750
|
+
|
|
702
751
|
function importResolved(term, env) {
|
|
703
752
|
const { copyResolved } = termModuleCache;
|
|
704
753
|
return copyResolved(term, env);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
% Reference
|
|
1
|
+
% Reference 11.1: recursive predicates are tabled automatically without changing answers.
|
|
2
2
|
materialize(reach, 2).
|
|
3
|
-
table(reach_any, 2).
|
|
4
3
|
edge(a, b).
|
|
5
4
|
edge(b, c).
|
|
6
5
|
reach_any(X, Y) :- edge(X, Y).
|