eyelang 4.0.7 → 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/docs/guide.md +1 -1
- package/docs/language-reference.md +14 -10
- package/examples/output/chart-parser.pl +1 -1
- package/examples/proof/chart-parser.pl +20 -6
- package/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/program.js +71 -7
- package/src/solver.js +95 -46
- package/test/conformance/expected/automatic-tabling/automatic_left_recursion_path.pl +1 -0
- package/test/run-regression.mjs +41 -0
package/docs/guide.md
CHANGED
|
@@ -604,7 +604,7 @@ path(a, Y).
|
|
|
604
604
|
status(Case, accepted).
|
|
605
605
|
```
|
|
606
606
|
|
|
607
|
-
Ground facts use a fast path that avoids freshening and copying a rule body. Recursive-predicate detection uses an explicit work stack, which keeps large predicate chains safer in the browser. eyelang automatically tables recursive
|
|
607
|
+
Ground facts use a fast path that avoids freshening and copying a rule body. Recursive-predicate detection uses an explicit work stack, which keeps large predicate chains safer in the browser. eyelang automatically tables positive recursive groups, including dependencies reached through supported meta-goals and directly materialized relations. Cyclic calls iterate to an answer fixed point before replay. Recursive components containing a negative dependency retain guarded ordinary resolution because positive fixed-point tabling is not a semantics for unstratified negation. The engine also infers common structurally decreasing input positions; calls with an unbound structural input and fully open calls retain ordinary resolution rather than trying to materialize a potentially infinite relation. Authors do not need a search-control declaration.
|
|
608
608
|
|
|
609
609
|
Predicates can also carry advisory mode and determinism declarations for documentation and host tooling:
|
|
610
610
|
|
|
@@ -639,16 +639,20 @@ Declarations are written as ordinary facts, but the host treats them specially.
|
|
|
639
639
|
eyelang automatically combines ordinary goal-directed resolution with tabled
|
|
640
640
|
resolution. Predicate dependency cycles are detected when a program is loaded,
|
|
641
641
|
including dependencies inside conjunctions, negation, `once/1`, `forall/2`, and
|
|
642
|
-
aggregation goals.
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
642
|
+
aggregation goals. Positive recursive predicate groups, including directly
|
|
643
|
+
materialized relations, are tabled automatically. Cyclic calls are evaluated to
|
|
644
|
+
an answer fixed point, so a table is complete before its answers are replayed.
|
|
645
|
+
Recursive components containing a negative dependency retain guarded ordinary
|
|
646
|
+
resolution because positive least-fixed-point tabling does not define
|
|
647
|
+
unstratified negation. Other groups use ordinary depth-first resolution and
|
|
648
|
+
indexed fact lookup. Authors do not select an execution strategy in source code.
|
|
649
|
+
|
|
650
|
+
For a tabled call with a ground input argument, answers may be cached and reused
|
|
651
|
+
within a solver run. The engine infers common structurally decreasing input
|
|
652
|
+
positions from recursive clause heads. Calls whose inferred structural input is
|
|
653
|
+
not ground, and fully open calls, continue with ordinary resolution so that an
|
|
654
|
+
infinite relation is not forced into a table. This is a search-control strategy
|
|
655
|
+
and does not change the logical meaning of a program.
|
|
652
656
|
|
|
653
657
|
### 11.2 Default-output materialization
|
|
654
658
|
|
|
@@ -3,4 +3,4 @@ chart_parser_answer(parsed, ambiguous_pp).
|
|
|
3
3
|
chart_parser_answer(parse_count, count(command, 1)).
|
|
4
4
|
chart_parser_answer(parse_count, count(ambiguous_pp, 1)).
|
|
5
5
|
chart_parser_answer(noun_phrase_count, count(command, 2)).
|
|
6
|
-
chart_parser_answer(noun_phrase_count, count(ambiguous_pp,
|
|
6
|
+
chart_parser_answer(noun_phrase_count, count(ambiguous_pp, 4)).
|
|
@@ -421,7 +421,21 @@ why(
|
|
|
421
421
|
chart_parser_answer(parse_count, count(ambiguous_pp, 1)).
|
|
422
422
|
why(
|
|
423
423
|
chart_parser_answer(parse_count, count(ambiguous_pp, 1)),
|
|
424
|
-
|
|
424
|
+
proof(
|
|
425
|
+
goal(chart_parser_answer(parse_count, count(ambiguous_pp, 1))),
|
|
426
|
+
by(rule("chart-parser.pl", clause(33))),
|
|
427
|
+
bindings([binding("Sentence", ambiguous_pp), binding("Count", 1), binding("Length", 8)]),
|
|
428
|
+
uses([
|
|
429
|
+
proof(
|
|
430
|
+
goal(sentence(ambiguous_pp, 8)),
|
|
431
|
+
by(fact("chart-parser.pl", clause(3)))
|
|
432
|
+
),
|
|
433
|
+
proof(
|
|
434
|
+
goal(countall(span(ambiguous_pp, s, 0, 8), 1)),
|
|
435
|
+
by(builtin(countall, 2))
|
|
436
|
+
)
|
|
437
|
+
])
|
|
438
|
+
)
|
|
425
439
|
).
|
|
426
440
|
|
|
427
441
|
chart_parser_answer(noun_phrase_count, count(command, 2)).
|
|
@@ -448,20 +462,20 @@ why(
|
|
|
448
462
|
)
|
|
449
463
|
).
|
|
450
464
|
|
|
451
|
-
chart_parser_answer(noun_phrase_count, count(ambiguous_pp,
|
|
465
|
+
chart_parser_answer(noun_phrase_count, count(ambiguous_pp, 4)).
|
|
452
466
|
why(
|
|
453
|
-
chart_parser_answer(noun_phrase_count, count(ambiguous_pp,
|
|
467
|
+
chart_parser_answer(noun_phrase_count, count(ambiguous_pp, 4)),
|
|
454
468
|
proof(
|
|
455
|
-
goal(chart_parser_answer(noun_phrase_count, count(ambiguous_pp,
|
|
469
|
+
goal(chart_parser_answer(noun_phrase_count, count(ambiguous_pp, 4))),
|
|
456
470
|
by(rule("chart-parser.pl", clause(34))),
|
|
457
|
-
bindings([binding("Sentence", ambiguous_pp), binding("Count",
|
|
471
|
+
bindings([binding("Sentence", ambiguous_pp), binding("Count", 4), binding("Length", 8)]),
|
|
458
472
|
uses([
|
|
459
473
|
proof(
|
|
460
474
|
goal(sentence(ambiguous_pp, 8)),
|
|
461
475
|
by(fact("chart-parser.pl", clause(3)))
|
|
462
476
|
),
|
|
463
477
|
proof(
|
|
464
|
-
goal(countall(span(ambiguous_pp, np, _start, _end),
|
|
478
|
+
goal(countall(span(ambiguous_pp, np, _start, _end), 4)),
|
|
465
479
|
by(builtin(countall, 2))
|
|
466
480
|
),
|
|
467
481
|
proof(
|
package/index.d.ts
CHANGED
package/package.json
CHANGED
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
|
};
|
|
@@ -117,13 +118,18 @@ export class Program {
|
|
|
117
118
|
const groups = [...this.groups.values()];
|
|
118
119
|
const indexByGroup = new Map(groups.map((group, i) => [group, i]));
|
|
119
120
|
const deps = groups.map(() => new Set());
|
|
121
|
+
const negativeEdges = [];
|
|
120
122
|
for (const group of groups) {
|
|
121
123
|
const groupIndex = indexByGroup.get(group);
|
|
122
124
|
for (const clause of group.clauses) {
|
|
123
125
|
for (const goal of clause.body) {
|
|
124
126
|
for (const dependency of collectGoalDependencies(goal, false)) {
|
|
125
127
|
const dep = this.groups.get(dependency.key);
|
|
126
|
-
if (dep)
|
|
128
|
+
if (dep) {
|
|
129
|
+
const dependencyIndex = indexByGroup.get(dep);
|
|
130
|
+
deps[groupIndex].add(dependencyIndex);
|
|
131
|
+
if (dependency.negative) negativeEdges.push([groupIndex, dependencyIndex]);
|
|
132
|
+
}
|
|
127
133
|
}
|
|
128
134
|
}
|
|
129
135
|
}
|
|
@@ -143,13 +149,14 @@ export class Program {
|
|
|
143
149
|
}
|
|
144
150
|
}
|
|
145
151
|
group.recursive = recursive;
|
|
152
|
+
group.tableInputPositions = recursive
|
|
153
|
+
? inferStructuralInputPositions(group)
|
|
154
|
+
: [];
|
|
146
155
|
// Recursive predicates are proved with tabling automatically, keeping
|
|
147
|
-
// search control inside the engine.
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
// Recursive helpers demanded by an output predicate are safe candidates.
|
|
152
|
-
group.tabled = recursive && !this.materializedGroups.has(`${group.name}/${group.arity}`);
|
|
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);
|
|
153
160
|
}
|
|
154
161
|
}
|
|
155
162
|
|
|
@@ -275,8 +282,65 @@ export class Program {
|
|
|
275
282
|
}
|
|
276
283
|
}
|
|
277
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
|
+
}
|
|
278
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
|
+
}
|
|
279
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
|
+
}
|
|
280
344
|
|
|
281
345
|
function termHasNoVariables(term) {
|
|
282
346
|
if (!term || term.type === 'var') return false;
|
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);
|
package/test/run-regression.mjs
CHANGED
|
@@ -757,6 +757,44 @@ function whiteBoxCases() {
|
|
|
757
757
|
assertEqual(group.tabled, true, 'path/2 tabled automatically');
|
|
758
758
|
},
|
|
759
759
|
},
|
|
760
|
+
{
|
|
761
|
+
name: 'directly materialized recursive groups are tabled automatically',
|
|
762
|
+
run: () => {
|
|
763
|
+
const program = Program.parse('materialize(path, 2).\nedge(a, b).\npath(X, Y) :- edge(X, Y).\npath(X, Z) :- edge(X, Y), path(Y, Z).\n');
|
|
764
|
+
const group = program.findGroup('path', 2);
|
|
765
|
+
assertEqual(group.tabled, true, 'materialized path/2 tabled automatically');
|
|
766
|
+
},
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
name: 'cycles through negation retain guarded resolution',
|
|
770
|
+
run: () => {
|
|
771
|
+
const program = Program.parse('p(X) :- not(q(X)).\nq(X) :- p(X).\n');
|
|
772
|
+
assertEqual(program.findGroup('p', 1).recursive, true, 'p/1 recursive');
|
|
773
|
+
assertEqual(program.findGroup('q', 1).recursive, true, 'q/1 recursive');
|
|
774
|
+
assertEqual(program.findGroup('p', 1).tabled, false, 'p/1 not positively tabled');
|
|
775
|
+
assertEqual(program.findGroup('q', 1).tabled, false, 'q/1 not positively tabled');
|
|
776
|
+
},
|
|
777
|
+
},
|
|
778
|
+
{
|
|
779
|
+
name: 'cyclic tabling reaches a complete fixed point',
|
|
780
|
+
run: () => {
|
|
781
|
+
const result = run(Program.parse(`
|
|
782
|
+
materialize(path, 2).
|
|
783
|
+
edge(a, b).
|
|
784
|
+
edge(b, c).
|
|
785
|
+
edge(c, d).
|
|
786
|
+
edge(d, a).
|
|
787
|
+
path(X, Y) :- edge(X, Y).
|
|
788
|
+
path(X, Z) :- edge(X, Y), path(Y, Z).
|
|
789
|
+
`));
|
|
790
|
+
const answers = result.stdout.trim().split('\n');
|
|
791
|
+
assertEqual(answers.length, 16, 'four-node cycle transitive closure size');
|
|
792
|
+
for (const node of ['a', 'b', 'c', 'd']) {
|
|
793
|
+
assertIncludes(result.stdout, `path(${node}, ${node}).\n`, `${node} reaches itself`);
|
|
794
|
+
}
|
|
795
|
+
assertEqual(result.stats.table_fixpoint_rounds > 1, true, 'cyclic table required multiple rounds');
|
|
796
|
+
},
|
|
797
|
+
},
|
|
760
798
|
{
|
|
761
799
|
name: 'ordinary declaration-like facts do not table predicates',
|
|
762
800
|
run: () => {
|
|
@@ -823,6 +861,8 @@ function whiteBoxCases() {
|
|
|
823
861
|
assertEqual(Boolean(group), true, 'no_diagonal_attack/3 group exists');
|
|
824
862
|
assertEqual(group.tabled, true, 'no_diagonal_attack/3 tabled');
|
|
825
863
|
assertEqual(group.recursive, true, 'no_diagonal_attack/3 recursive');
|
|
864
|
+
assertEqual(group.tableInputPositions.join(','), '2', 'diagonal scan uses the remaining rows as input');
|
|
865
|
+
assertEqual(program.findGroup('perm', 2).tableInputPositions.join(','), '1', 'permutation output stays untabled while open');
|
|
826
866
|
},
|
|
827
867
|
},
|
|
828
868
|
{
|
|
@@ -834,6 +874,7 @@ function whiteBoxCases() {
|
|
|
834
874
|
assertEqual(Boolean(group), true, 'collatz/2 group exists');
|
|
835
875
|
assertEqual(group.tabled, true, 'collatz/2 tabled');
|
|
836
876
|
assertEqual(group.recursive, true, 'collatz/2 recursive');
|
|
877
|
+
assertEqual(group.tableInputPositions.join(','), '0', 'collatz uses its numeric seed as input');
|
|
837
878
|
},
|
|
838
879
|
},
|
|
839
880
|
{
|