eyeprolog 1.3.80 → 1.3.81

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 CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.3.80",
6
+ "version": "1.3.81",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
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 REPL does not need speculative look-ahead to suppress a choicepoint.
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 top level does not print a phantom
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
- stack.push(frame);
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.push({
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.push({
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.push({
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.push({
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.push({
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
- // When solve() is suspended at an answer, active solve stacks contain
830
- // unexplored work or an iterator frame that can prove it just yielded its
831
- // final answer. The demand-driven REPL uses this without speculatively
832
- // pulling the next answer. Control builtins can exclude their caller's
833
- // pre-existing stacks when inspecting a nested branch.
834
- return this.solveStacks.some((stack) => !excludedStacks?.has(stack) && stack.some((frame) => {
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];
@@ -1827,7 +1827,7 @@ c4 ?- call((!;1)).
1827
1827
  },
1828
1828
  },
1829
1829
  {
1830
- name: 'REPL omits exhausted choicepoints without probing future branches (issue #74)',
1830
+ name: 'predicates discard exhausted choicepoints before the REPL observes them (issue #74)',
1831
1831
  run: () => {
1832
1832
  const result = runCli([], {
1833
1833
  input:
@@ -1852,6 +1852,31 @@ c4 ?- call((!;1)).
1852
1852
  'stdout');
1853
1853
  assertEqual(result.stderr, '', 'stderr');
1854
1854
 
1855
+ const finalAnswerHasNoFrames = (source, text, answerCount, module = null) => {
1856
+ const solver = new Solver(Program.parse(source), { registry: getEyePrologRegistry() });
1857
+ const goal = parseGoalText(text);
1858
+ if (module != null) goal.module = module;
1859
+ const solutions = solver.solve([goal], new Env(), 0);
1860
+ for (let answer = 1; answer <= answerCount; answer++) {
1861
+ assertEqual(solutions.next().done, false, `${text} answer ${answer}`);
1862
+ if (answer < answerCount) {
1863
+ assertEqual(solver.solveStacks.some((stack) => stack.length !== 0), true,
1864
+ `${text} retains its genuine earlier choicepoint`);
1865
+ }
1866
+ }
1867
+ assertEqual(solver.solveStacks.every((stack) => stack.length === 0), true,
1868
+ `${text} removes its exhausted predicate frames`);
1869
+ assertEqual(solver.hasPendingAlternatives(), false,
1870
+ `${text} has no reported alternatives`);
1871
+ solutions.return();
1872
+ };
1873
+
1874
+ finalAnswerHasNoFrames('', 'X=1;X=2', 2);
1875
+ finalAnswerHasNoFrames('', 'setof(X,true,Xs)', 1);
1876
+ finalAnswerHasNoFrames('', 'findall(t,false,Xs)', 1);
1877
+ finalAnswerHasNoFrames(':- use_module(library(lists)).', 'member(a,"ba")', 1, 'lists');
1878
+ finalAnswerHasNoFrames(':- use_module(library(lists)).', 'append("a",Xs,Xs0)', 1, 'lists');
1879
+
1855
1880
  const grouped = new Solver(Program.parse(''));
1856
1881
  const groupedSolutions = grouped.solve([
1857
1882
  parseGoalText('setof(X,(Y=1,X=a;Y=2,X=b),Xs)'),