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.
Files changed (75) hide show
  1. package/README.md +4 -0
  2. package/bin/eyelang.js +1 -1
  3. package/conformance-report.md +3 -3
  4. package/docs/guide.md +12 -11
  5. package/docs/language-reference.md +22 -16
  6. package/examples/abstract-interpretation.pl +0 -1
  7. package/examples/bayes-therapy.pl +1 -4
  8. package/examples/binomial-vandermonde.pl +1 -2
  9. package/examples/catalan-convolution.pl +1 -2
  10. package/examples/chart-parser.pl +1 -2
  11. package/examples/collatz-1000.pl +1 -2
  12. package/examples/continued-fraction-sqrt2.pl +1 -2
  13. package/examples/critical-path-schedule.pl +0 -2
  14. package/examples/dijkstra-risk-path.pl +0 -1
  15. package/examples/drone-corridor-planner.pl +0 -1
  16. package/examples/equality-saturation.pl +0 -2
  17. package/examples/family-cousins.pl +0 -3
  18. package/examples/fibonacci.pl +0 -1
  19. package/examples/gd-step-certified.pl +0 -7
  20. package/examples/gps.pl +0 -2
  21. package/examples/hamiltonian-path.pl +0 -1
  22. package/examples/hanoi.pl +0 -1
  23. package/examples/integer-partitions.pl +0 -1
  24. package/examples/intuitionistic-logic-kripke.pl +0 -2
  25. package/examples/knuth-bendix-completion.pl +0 -1
  26. package/examples/language.pl +1 -2
  27. package/examples/matrix-chain-order.pl +1 -2
  28. package/examples/modular-exponentiation.pl +0 -1
  29. package/examples/monkey-bananas.pl +0 -1
  30. package/examples/n-queens-8.pl +0 -1
  31. package/examples/output/chart-parser.pl +1 -1
  32. package/examples/partial-evaluator.pl +0 -1
  33. package/examples/peano-calculus.pl +0 -3
  34. package/examples/pell-equation.pl +1 -2
  35. package/examples/pointer-analysis.pl +0 -2
  36. package/examples/prime-range.pl +0 -1
  37. package/examples/proof/chart-parser.pl +91 -77
  38. package/examples/proof/hanoi.pl +16 -16
  39. package/examples/proof/intuitionistic-logic-kripke.pl +16 -16
  40. package/examples/route-planning.pl +0 -1
  41. package/examples/stirling-bell-numbers.pl +0 -4
  42. package/examples/totient-summatory.pl +1 -3
  43. package/examples/truth-maintenance-system.pl +0 -1
  44. package/examples/type-inference.pl +0 -1
  45. package/examples/web-names.pl +0 -1
  46. package/examples/weighted-interval-scheduling.pl +2 -3
  47. package/examples/wolf-goat-cabbage.pl +0 -1
  48. package/index.d.ts +1 -0
  49. package/package.json +1 -1
  50. package/src/explain.js +3 -1
  51. package/src/program.js +88 -9
  52. package/src/solver.js +95 -46
  53. package/test/conformance/cases/{table/038_table_declaration.pl → automatic-tabling/038_automatic_tabling.pl} +1 -2
  54. package/test/conformance/cases/{table/102_table_declaration.pl → automatic-tabling/102_recursive_inference.pl} +1 -2
  55. package/test/conformance/cases/{table/tabled_left_recursion_path.pl → automatic-tabling/automatic_left_recursion_path.pl} +0 -1
  56. package/test/conformance/cases/{table/table_does_not_change_answers.pl → automatic-tabling/automatic_tabling_preserves_answers.pl} +1 -2
  57. package/test/conformance/cases/{table/extra_table_bound_cycle.pl → automatic-tabling/extra_automatic_bound_cycle.pl} +0 -1
  58. package/test/conformance/cases/{table/extra_table_open_call_fallback.pl → automatic-tabling/extra_automatic_open_call_fallback.pl} +0 -1
  59. package/test/conformance/cases/rules/transitive_rule_three_edges.pl +0 -1
  60. package/test/conformance/expected/{table/tabled_left_recursion_path.pl → automatic-tabling/automatic_left_recursion_path.pl} +1 -0
  61. package/test/conformance/expected-proofs/proofs/{table_recursive.pl → automatic_recursive.pl} +8 -8
  62. package/test/conformance/expected-proofs/proofs/{extra_table_path_bound.pl → extra_automatic_path_bound.pl} +8 -8
  63. package/test/conformance/proofs/proofs/{table_recursive.pl → automatic_recursive.pl} +1 -2
  64. package/test/conformance/proofs/proofs/{extra_table_path_bound.pl → extra_automatic_path_bound.pl} +0 -1
  65. package/test/conformance/warnings/negation/{extra_stratified_negation_with_table_quiet.pl → extra_stratified_negation_with_recursion_quiet.pl} +0 -1
  66. package/test/run-regression.mjs +52 -11
  67. package/test/conformance/cases/declarations/table_is_also_fact.pl +0 -4
  68. package/test/conformance/expected/declarations/table_is_also_fact.pl +0 -1
  69. /package/test/conformance/expected/{table/038_table_declaration.pl → automatic-tabling/038_automatic_tabling.pl} +0 -0
  70. /package/test/conformance/expected/{table/102_table_declaration.pl → automatic-tabling/102_recursive_inference.pl} +0 -0
  71. /package/test/conformance/expected/{table/table_does_not_change_answers.pl → automatic-tabling/automatic_tabling_preserves_answers.pl} +0 -0
  72. /package/test/conformance/expected/{table/extra_table_bound_cycle.pl → automatic-tabling/extra_automatic_bound_cycle.pl} +0 -0
  73. /package/test/conformance/expected/{table/extra_table_open_call_fallback.pl → automatic-tabling/extra_automatic_open_call_fallback.pl} +0 -0
  74. /package/test/conformance/expected-warnings/negation/{extra_stratified_negation_with_table_quiet.pl → extra_stratified_negation_with_recursion_quiet.pl} +0 -0
  75. /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(24))),
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(17))),
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(16))),
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(11)))
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(15))),
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(8)))
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(13)))
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(25))),
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(21))),
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(5)))
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(26))),
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(22))),
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(21))),
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(5)))
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(27))),
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(28))),
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)))))),
@@ -6,7 +6,6 @@
6
6
  % route can be inspected, stored, or used by later rules.
7
7
 
8
8
  materialize(route_to_nantes, 2).
9
- table(path, 2).
10
9
  mode(path, 2, [in, out]).
11
10
 
12
11
  oneway(paris, orleans).
@@ -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).
@@ -10,7 +10,6 @@ materialize(tmsJustification, 3).
10
10
  materialize(tmsInconsistent, 1).
11
11
  materialize(tmsConclusion, 2).
12
12
 
13
- table(supported, 2).
14
13
 
15
14
  % Candidate environments.
16
15
  environment(clear_only, [sensor_clear]).
@@ -9,7 +9,6 @@
9
9
  materialize(type_answer, 2).
10
10
  materialize(type_reason, 2).
11
11
 
12
- table(type_expr, 3).
13
12
 
14
13
  % Tiny expression language: int_lit/1, bool_lit/1, var/1, lam/3, app/2,
15
14
  % add/2, if/3, pair/2, fst/1, and snd/1.
@@ -7,7 +7,6 @@
7
7
  materialize(affiliated_with, 2).
8
8
  materialize(project_contact, 3).
9
9
 
10
- table(parent_organization, 2).
11
10
 
12
11
  % A tiny graph using absolute IRI atoms directly.
13
12
  triple('<https://data.ugent.be/id/josd>', '<https://schema.org/name>', "Jos De Roo").
@@ -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 tabled decisions to report a schedule.
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
@@ -45,6 +45,7 @@ export interface EyelangPredicateGroup {
45
45
  mode: string[] | null;
46
46
  determinism: 'det' | 'semidet' | null;
47
47
  recursive: boolean;
48
+ tableInputPositions: number[];
48
49
  negationStratum: number | null;
49
50
  }
50
51
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyelang",
3
- "version": "4.0.6",
3
+ "version": "4.0.8",
4
4
  "description": "A small reasoning language with ISO Prolog-compatible surface syntax.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
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
- if (!group.tabled && activeVariant(goal, env, active)) return;
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 === 'table') {
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
- if (options.markRecursive !== false) this.markRecursivePredicates();
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 bodyGoals = clause.body.flatMap((goal) => goal.type === COMPOUND && goal.name === ',' && goal.arity === 2 ? flattenConjunction(goal) : [goal]);
123
- for (const goal of bodyGoals) {
124
- if (goal.type !== COMPOUND) continue;
125
- const dep = this.findGroup(goal.name, goal.arity);
126
- if (dep) deps[groupIndex].add(indexByGroup.get(dep));
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 = { computing: false, complete: false, answers: [], answerKeys: new Set() };
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
- entry.computing = true;
152
- stack.push({ kind: 'completeMemo', entry });
153
- pushUserGoalUncachedFrames(stack, this, group, goal, [{ kind: 'memoStore', entry, goal }, ...rest], env, depth, active);
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
- // Memoization only pays off when at least one argument is bound. A fully
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 key = answerArgs.map((arg) => termToString(arg, new Env(), true)).join('\x1f');
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.type !== 'var') hasBound = true;
697
- return value.type === 'var' ? '_' : termToString(value, env, true);
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 10.1: table/2 is a declaration and does not change answers.
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).
@@ -1,6 +1,5 @@
1
- % Reference 11.1: table/2 is the preferred spelling for tabled predicates.
1
+ % Reference 11.1: recursive predicate groups are detected automatically.
2
2
  materialize(reach, 2).
3
- table(reach_any, 2).
4
3
 
5
4
  edge(a, b).
6
5
  edge(b, c).
@@ -1,5 +1,4 @@
1
1
  materialize(answer, 1).
2
- table(reachable, 2).
3
2
  edge(a, b).
4
3
  edge(b, c).
5
4
  reachable(X, Z) :- reachable(X, Y), edge(Y, Z).
@@ -1,6 +1,5 @@
1
- % `table/2` is a search-control declaration and does not change answers.
1
+ % Automatic tabling is a search-control strategy and does not change answers.
2
2
  materialize(path, 2).
3
- table(path, 2).
4
3
  edge(a, b).
5
4
  edge(b, c).
6
5
  path(X, Y) :- edge(X, Y).
@@ -1,5 +1,4 @@
1
1
  materialize(reach, 2).
2
- table(path, 2).
3
2
  edge(a, b).
4
3
  edge(b, a).
5
4
  path(X, Y) :- edge(X, Y).
@@ -1,5 +1,4 @@
1
1
  materialize(answer, 2).
2
- table(edge, 2).
3
2
  edge(a, b).
4
3
  edge(b, c).
5
4
  answer(table_open_call_fallback, pair(X, Y)) :- edge(X, Y).
@@ -1,5 +1,4 @@
1
1
  materialize(path, 2).
2
- table(path, 2).
3
2
  edge(a, b).
4
3
  edge(b, c).
5
4
  edge(c, d).