eyeprolog 1.3.80 → 1.3.82
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/package.json +1 -1
- package/src/cleanup.js +1 -12
- package/src/iso.js +1 -1
- package/src/solver.js +32 -60
- package/test/run-regression.mjs +108 -13
package/package.json
CHANGED
package/src/cleanup.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// therefore need two pieces of host support:
|
|
6
6
|
// * discarded resumeBuiltin iterators must be closed when search is pruned;
|
|
7
7
|
// * a cleanup iterator can report that its protected goal has no alternatives,
|
|
8
|
-
// so the
|
|
8
|
+
// so the solver does not retain an exhausted resume frame.
|
|
9
9
|
import { PrologError } from './errors.js';
|
|
10
10
|
import { deref } from './term.js';
|
|
11
11
|
|
|
@@ -59,10 +59,6 @@ export function installCleanupLifecycle(Solver) {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
};
|
|
62
|
-
|
|
63
|
-
prototype.hasPendingAlternatives = function cleanupAwarePendingAlternatives() {
|
|
64
|
-
return this.solveStacks.some((stack) => stack.some(frameHasPendingAlternative));
|
|
65
|
-
};
|
|
66
62
|
}
|
|
67
63
|
|
|
68
64
|
export function registerCleanupBuiltins(registry) {
|
|
@@ -168,13 +164,6 @@ function closeFrames(frames, suppressErrors) {
|
|
|
168
164
|
if (!suppressErrors && firstError != null) throw firstError;
|
|
169
165
|
}
|
|
170
166
|
|
|
171
|
-
function frameHasPendingAlternative(frame) {
|
|
172
|
-
if (frame?.kind !== 'resumeBuiltin') return true;
|
|
173
|
-
const predicate = frame.iterator?.hasPendingAlternatives;
|
|
174
|
-
if (typeof predicate !== 'function') return true;
|
|
175
|
-
return predicate.call(frame.iterator);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
167
|
function callableTerm(term, env) {
|
|
179
168
|
const value = deref(term, env);
|
|
180
169
|
if (value.type === 'var') throw new PrologError('instantiation_error');
|
package/src/iso.js
CHANGED
|
@@ -2598,7 +2598,7 @@ function callResidueVarsBuiltin({ solver, goal, env }) {
|
|
|
2598
2598
|
const child = solver.cloneForInnerGoal();
|
|
2599
2599
|
let pending = true;
|
|
2600
2600
|
// The wrapper iterator is resumable even for a deterministic Goal. Expose
|
|
2601
|
-
// the child's actual search state so the
|
|
2601
|
+
// the child's actual search state so the solver does not install a phantom
|
|
2602
2602
|
// choicepoint after the final answer.
|
|
2603
2603
|
const iterator = (function* residueSolutions() {
|
|
2604
2604
|
try {
|
package/src/solver.js
CHANGED
|
@@ -421,7 +421,11 @@ export class Solver {
|
|
|
421
421
|
if (this.solutionsSeen >= this.solutionLimit) continue;
|
|
422
422
|
const result = frame.iterator.next();
|
|
423
423
|
if (result.done) continue;
|
|
424
|
-
|
|
424
|
+
// Predicate iterators that can identify their final answer report it
|
|
425
|
+
// before yielding. Do not recreate a choicepoint that the predicate has
|
|
426
|
+
// already exhausted; consumers must not need to hide such a frame later.
|
|
427
|
+
if (iteratorMayHavePendingAlternatives(frame.iterator)) stack.push(frame);
|
|
428
|
+
else frame.iterator.return?.();
|
|
425
429
|
stack.push({
|
|
426
430
|
kind: 'goals',
|
|
427
431
|
goals: frame.goals,
|
|
@@ -620,15 +624,7 @@ export class Solver {
|
|
|
620
624
|
else this.stats.deterministic_builtin_failures++;
|
|
621
625
|
}
|
|
622
626
|
if (firstResult.done) break;
|
|
623
|
-
if (!deterministic)
|
|
624
|
-
stack.push({
|
|
625
|
-
kind: 'resumeBuiltin',
|
|
626
|
-
iterator,
|
|
627
|
-
goals: rest,
|
|
628
|
-
depth: depth + 1,
|
|
629
|
-
active,
|
|
630
|
-
});
|
|
631
|
-
}
|
|
627
|
+
if (!deterministic) pushResumeBuiltinFrame(stack, iterator, rest, depth + 1, active);
|
|
632
628
|
goals = rest;
|
|
633
629
|
env = firstResult.value;
|
|
634
630
|
depth++;
|
|
@@ -660,13 +656,7 @@ export class Solver {
|
|
|
660
656
|
const iterator = datalogAnswerSolutions(this, relation, goal, env);
|
|
661
657
|
const firstResult = iterator.next();
|
|
662
658
|
if (firstResult.done) break;
|
|
663
|
-
stack
|
|
664
|
-
kind: 'resumeBuiltin',
|
|
665
|
-
iterator,
|
|
666
|
-
goals: rest,
|
|
667
|
-
depth: depth + 1,
|
|
668
|
-
active,
|
|
669
|
-
});
|
|
659
|
+
pushResumeBuiltinFrame(stack, iterator, rest, depth + 1, active);
|
|
670
660
|
goals = rest;
|
|
671
661
|
env = firstResult.value;
|
|
672
662
|
depth++;
|
|
@@ -683,13 +673,7 @@ export class Solver {
|
|
|
683
673
|
if (betweenIterator != null) {
|
|
684
674
|
const firstResult = betweenIterator.next();
|
|
685
675
|
if (firstResult.done) break;
|
|
686
|
-
stack
|
|
687
|
-
kind: 'resumeBuiltin',
|
|
688
|
-
iterator: betweenIterator,
|
|
689
|
-
goals: rest,
|
|
690
|
-
depth: depth + 1,
|
|
691
|
-
active,
|
|
692
|
-
});
|
|
676
|
+
pushResumeBuiltinFrame(stack, betweenIterator, rest, depth + 1, active);
|
|
693
677
|
goals = rest;
|
|
694
678
|
env = firstResult.value;
|
|
695
679
|
depth++;
|
|
@@ -700,13 +684,7 @@ export class Solver {
|
|
|
700
684
|
if (memberIterator != null) {
|
|
701
685
|
const firstResult = memberIterator.next();
|
|
702
686
|
if (firstResult.done) break;
|
|
703
|
-
stack
|
|
704
|
-
kind: 'resumeBuiltin',
|
|
705
|
-
iterator: memberIterator,
|
|
706
|
-
goals: rest,
|
|
707
|
-
depth: depth + 1,
|
|
708
|
-
active,
|
|
709
|
-
});
|
|
687
|
+
pushResumeBuiltinFrame(stack, memberIterator, rest, depth + 1, active);
|
|
710
688
|
goals = rest;
|
|
711
689
|
env = firstResult.value;
|
|
712
690
|
depth++;
|
|
@@ -717,13 +695,7 @@ export class Solver {
|
|
|
717
695
|
if (lengthIterator != null) {
|
|
718
696
|
const firstResult = lengthIterator.next();
|
|
719
697
|
if (firstResult.done) break;
|
|
720
|
-
stack
|
|
721
|
-
kind: 'resumeBuiltin',
|
|
722
|
-
iterator: lengthIterator,
|
|
723
|
-
goals: rest,
|
|
724
|
-
depth: depth + 1,
|
|
725
|
-
active,
|
|
726
|
-
});
|
|
698
|
+
pushResumeBuiltinFrame(stack, lengthIterator, rest, depth + 1, active);
|
|
727
699
|
goals = rest;
|
|
728
700
|
env = firstResult.value;
|
|
729
701
|
depth++;
|
|
@@ -734,13 +706,7 @@ export class Solver {
|
|
|
734
706
|
if (ellipsisPlan != null) {
|
|
735
707
|
const firstResult = ellipsisPlan.iterator.next();
|
|
736
708
|
if (firstResult.done) break;
|
|
737
|
-
stack.
|
|
738
|
-
kind: 'resumeBuiltin',
|
|
739
|
-
iterator: ellipsisPlan.iterator,
|
|
740
|
-
goals: ellipsisPlan.rest,
|
|
741
|
-
depth: depth + 1,
|
|
742
|
-
active,
|
|
743
|
-
});
|
|
709
|
+
pushResumeBuiltinFrame(stack, ellipsisPlan.iterator, ellipsisPlan.rest, depth + 1, active);
|
|
744
710
|
goals = ellipsisPlan.rest;
|
|
745
711
|
env = firstResult.value;
|
|
746
712
|
depth++;
|
|
@@ -826,21 +792,12 @@ export class Solver {
|
|
|
826
792
|
}
|
|
827
793
|
|
|
828
794
|
hasPendingAlternatives(excludedStacks = null) {
|
|
829
|
-
//
|
|
830
|
-
//
|
|
831
|
-
//
|
|
832
|
-
//
|
|
833
|
-
//
|
|
834
|
-
return this.solveStacks.some((stack) => !excludedStacks?.has(stack) && stack.
|
|
835
|
-
if (frame.kind === 'resumeBuiltin' &&
|
|
836
|
-
typeof frame.iterator?.hasPendingAlternatives === 'function') {
|
|
837
|
-
return frame.iterator.hasPendingAlternatives();
|
|
838
|
-
}
|
|
839
|
-
if (frame.kind === 'userClause' && headCannotMatch(frame.goal, frame.clause.head, frame.env)) {
|
|
840
|
-
return false;
|
|
841
|
-
}
|
|
842
|
-
return true;
|
|
843
|
-
}));
|
|
795
|
+
// Choicepoints are represented by actual pending frames. Predicate
|
|
796
|
+
// iterators discard an exhausted resume frame before yielding their final
|
|
797
|
+
// answer, so observers never need to reinterpret or conceal stack entries.
|
|
798
|
+
// Control builtins can exclude their caller's pre-existing stacks when
|
|
799
|
+
// inspecting a nested branch.
|
|
800
|
+
return this.solveStacks.some((stack) => !excludedStacks?.has(stack) && stack.length !== 0);
|
|
844
801
|
}
|
|
845
802
|
|
|
846
803
|
fastCountGoal(goal, env) {
|
|
@@ -2872,6 +2829,21 @@ function builtinIsReadyOrAuthoritative(def, solver, goal, env) {
|
|
|
2872
2829
|
return !def.fallbackWhenNotReady;
|
|
2873
2830
|
}
|
|
2874
2831
|
|
|
2832
|
+
function iteratorMayHavePendingAlternatives(iterator) {
|
|
2833
|
+
const predicate = iterator?.hasPendingAlternatives;
|
|
2834
|
+
return typeof predicate !== 'function' || predicate.call(iterator) !== false;
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
function pushResumeBuiltinFrame(stack, iterator, goals, depth, active) {
|
|
2838
|
+
if (!iteratorMayHavePendingAlternatives(iterator)) {
|
|
2839
|
+
// Finish the suspended generator now so its finally blocks release child
|
|
2840
|
+
// solvers, aggregate statistics, and perform predicate-local cleanup.
|
|
2841
|
+
iterator.return?.();
|
|
2842
|
+
return;
|
|
2843
|
+
}
|
|
2844
|
+
stack.push({ kind: 'resumeBuiltin', iterator, goals, depth, active });
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2875
2847
|
function selectReadyDeterministicBuiltin(goals, env, registry) {
|
|
2876
2848
|
for (let i = 0; i < goals.length; i++) {
|
|
2877
2849
|
const goal = goals[i];
|
package/test/run-regression.mjs
CHANGED
|
@@ -111,18 +111,88 @@ export function runRegression(reporter = new TestReporter(), requestedSection =
|
|
|
111
111
|
function regressionCases() {
|
|
112
112
|
return [
|
|
113
113
|
{
|
|
114
|
-
name: 'dif/2
|
|
115
|
-
run: () => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
'
|
|
120
|
-
|
|
121
|
-
'
|
|
122
|
-
|
|
123
|
-
'
|
|
124
|
-
|
|
125
|
-
'
|
|
114
|
+
name: 'dif/2 passes the WG17 finite-tree comparison cases (issue #68)',
|
|
115
|
+
run: () => {
|
|
116
|
+
// Comparison cases from
|
|
117
|
+
// https://www.complang.tuwien.ac.at/ulrich/iso-prolog/dif
|
|
118
|
+
const comparisonCases = [
|
|
119
|
+
['1', true, 'dif(1,2)'],
|
|
120
|
+
['2', false, 'dif(1,Y),Y=1'],
|
|
121
|
+
['3', true, 'dif(1,Y),Y=2'],
|
|
122
|
+
['4', false, 'dif(X,-Y),X= -Y'],
|
|
123
|
+
['5', false, 'dif(X,Y),X=Y'],
|
|
124
|
+
['6', false, 'dif(X,Y),X=Y,X=1'],
|
|
125
|
+
['7', false, 'dif(-X,-Y),X=Y'],
|
|
126
|
+
['8', false, 'dif(-X,-Y),X=Y,X=1'],
|
|
127
|
+
['9', true, 'dif(-X,X)'],
|
|
128
|
+
['10', true, 'dif(-X,Y),X=Y'],
|
|
129
|
+
['11', true, 'X=Y,dif(X-Y,1-2)'],
|
|
130
|
+
['12', true, 'dif(X-Y,1-2),X=Y'],
|
|
131
|
+
['13', true, 'X=Y,Y=1,dif(X-Y,1-2)'],
|
|
132
|
+
['14', true, 'dif(X-Y,1-2),X=Y,Y=1'],
|
|
133
|
+
['15', true, 'dif(X-Y,1-2),X=Y,X=2'],
|
|
134
|
+
['16', true, 'dif(A-C,B-D),C-D=z-z,A-B=1-2'],
|
|
135
|
+
['17', true, 'A-B=1-2,C-D=z-z,dif(A-C,B-D)'],
|
|
136
|
+
['18', true, 'dif(A,[C|B]),A=[[]|_],A=[B]'],
|
|
137
|
+
['19', true, 'dif([E],[/]),E=1'],
|
|
138
|
+
['20', true, 'dif([a],B),B=[_|_],B=[b]'],
|
|
139
|
+
['21', true, 'dif([],A),A=[_]'],
|
|
140
|
+
['22', true, 'A=[_],dif([],A)'],
|
|
141
|
+
// Cases 23-26 are implementation-defined in the comparison. These
|
|
142
|
+
// assertions record EyeProlog's term-copying and collector
|
|
143
|
+
// choices so changes remain deliberate.
|
|
144
|
+
['23', true, 'dif(X,a),copy_term(X,a)'],
|
|
145
|
+
['24', true, 'findall(X,dif(X,a),[a])'],
|
|
146
|
+
['25', true, 'setof(t,dif(X,a),_),X=a'],
|
|
147
|
+
['26', true, 'setof(t,(dif(X,a);dif(X,b)),_),X=a'],
|
|
148
|
+
];
|
|
149
|
+
for (const [id, succeeds, query] of comparisonCases) {
|
|
150
|
+
const result = run(`check :- ${query}.`, { goal: 'check' });
|
|
151
|
+
assertEqual(result.stdout, succeeds ? 'check.\n' : '', `WG17 dif/2 case ${id}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// EyeProlog is a finite-tree processor. The comparison's rational-tree
|
|
155
|
+
// probes must terminate as failures instead of constructing cycles.
|
|
156
|
+
const rationalTreeCases = [
|
|
157
|
+
['t1', '\\+ \\+ -X=X'],
|
|
158
|
+
['t2', '-X=X,-Y=Y,X\\=Y'],
|
|
159
|
+
['t3', '-X=X,dif(X,1)'],
|
|
160
|
+
['t4', '-X=X,-Y=Y,dif(X,Y)'],
|
|
161
|
+
['t5', 'dif(X,Y),-X=X,-Y=Y'],
|
|
162
|
+
['t6', 'A=[[]|A],dif(A,B),B=[[]|A]'],
|
|
163
|
+
['t7', 'dif(-X,X),-Y=Y,X=Y'],
|
|
164
|
+
['t8', '-X=X,dif(X,Y),X=Y'],
|
|
165
|
+
];
|
|
166
|
+
for (const [id, query] of rationalTreeCases) {
|
|
167
|
+
assertEqual(run(`check :- ${query}.`, { goal: 'check' }).stdout, '',
|
|
168
|
+
`WG17 dif/2 ${id} terminates under finite-tree unification`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
for (const [id, query, succeeds] of [
|
|
172
|
+
['o1', '-X=X', false],
|
|
173
|
+
['o2', 'dif(-X,X)', true],
|
|
174
|
+
['o3', 'dif(-X,Y),X=Y', true],
|
|
175
|
+
]) {
|
|
176
|
+
assertEqual(run(`check :- ${query}.`, { goal: 'check' }).stdout,
|
|
177
|
+
succeeds ? 'check.\n' : '', `WG17 dif/2 occurs-check case ${id}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const residuals = runCli([], {
|
|
181
|
+
input:
|
|
182
|
+
'dif(A-C,B-D),C-D=z-z.\n' +
|
|
183
|
+
'dif(A,[C|B]),A=[[]|_],A=[B].\n' +
|
|
184
|
+
'dif(X-Y,1-2),X=Y.\n' +
|
|
185
|
+
'halt.\n',
|
|
186
|
+
});
|
|
187
|
+
assertEqual(residuals.status, 0, 'dif/2 residual display status');
|
|
188
|
+
assertEqual(residuals.stdout,
|
|
189
|
+
'?- C = z, D = z, dif(A - z, B - z).\n' +
|
|
190
|
+
'?- A = [[]], B = [], dif([[]], [C]).\n' +
|
|
191
|
+
'?- X = Y.\n' +
|
|
192
|
+
'?- ',
|
|
193
|
+
'dif/2 residual constraints are retained and discharged');
|
|
194
|
+
assertEqual(residuals.stderr, '', 'dif/2 residual display stderr');
|
|
195
|
+
|
|
126
196
|
assertEqual(run('p(a).', { goal: 'dif(X,a),p(X)' }).stats.completed_goal_lists, 0,
|
|
127
197
|
'scalar-fact fast matching still validates annotated variables');
|
|
128
198
|
assertEqual(run('p(b).', { goal: 'dif(X,a),p(X)' }).stats.completed_goal_lists, 1,
|
|
@@ -1827,7 +1897,7 @@ c4 ?- call((!;1)).
|
|
|
1827
1897
|
},
|
|
1828
1898
|
},
|
|
1829
1899
|
{
|
|
1830
|
-
name: '
|
|
1900
|
+
name: 'predicates discard exhausted choicepoints before the REPL observes them (issue #74)',
|
|
1831
1901
|
run: () => {
|
|
1832
1902
|
const result = runCli([], {
|
|
1833
1903
|
input:
|
|
@@ -1852,6 +1922,31 @@ c4 ?- call((!;1)).
|
|
|
1852
1922
|
'stdout');
|
|
1853
1923
|
assertEqual(result.stderr, '', 'stderr');
|
|
1854
1924
|
|
|
1925
|
+
const finalAnswerHasNoFrames = (source, text, answerCount, module = null) => {
|
|
1926
|
+
const solver = new Solver(Program.parse(source), { registry: getEyePrologRegistry() });
|
|
1927
|
+
const goal = parseGoalText(text);
|
|
1928
|
+
if (module != null) goal.module = module;
|
|
1929
|
+
const solutions = solver.solve([goal], new Env(), 0);
|
|
1930
|
+
for (let answer = 1; answer <= answerCount; answer++) {
|
|
1931
|
+
assertEqual(solutions.next().done, false, `${text} answer ${answer}`);
|
|
1932
|
+
if (answer < answerCount) {
|
|
1933
|
+
assertEqual(solver.solveStacks.some((stack) => stack.length !== 0), true,
|
|
1934
|
+
`${text} retains its genuine earlier choicepoint`);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
assertEqual(solver.solveStacks.every((stack) => stack.length === 0), true,
|
|
1938
|
+
`${text} removes its exhausted predicate frames`);
|
|
1939
|
+
assertEqual(solver.hasPendingAlternatives(), false,
|
|
1940
|
+
`${text} has no reported alternatives`);
|
|
1941
|
+
solutions.return();
|
|
1942
|
+
};
|
|
1943
|
+
|
|
1944
|
+
finalAnswerHasNoFrames('', 'X=1;X=2', 2);
|
|
1945
|
+
finalAnswerHasNoFrames('', 'setof(X,true,Xs)', 1);
|
|
1946
|
+
finalAnswerHasNoFrames('', 'findall(t,false,Xs)', 1);
|
|
1947
|
+
finalAnswerHasNoFrames(':- use_module(library(lists)).', 'member(a,"ba")', 1, 'lists');
|
|
1948
|
+
finalAnswerHasNoFrames(':- use_module(library(lists)).', 'append("a",Xs,Xs0)', 1, 'lists');
|
|
1949
|
+
|
|
1855
1950
|
const grouped = new Solver(Program.parse(''));
|
|
1856
1951
|
const groupedSolutions = grouped.solve([
|
|
1857
1952
|
parseGoalText('setof(X,(Y=1,X=a;Y=2,X=b),Xs)'),
|