eyeprolog 1.3.77 → 1.3.78

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.77",
6
+ "version": "1.3.78",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/iso.js CHANGED
@@ -65,9 +65,9 @@ export const isoBuiltins = {
65
65
  registry.add('=..', 2, univBuiltin, { deterministic: true });
66
66
  registry.add('copy_term', 2, copyTermBuiltin, { deterministic: true });
67
67
  registry.add('term_variables', 2, termVariablesBuiltin, { deterministic: true });
68
- registry.add('findall', 3, findallBuiltin);
69
- registry.add('bagof', 3, bagofBuiltin);
70
- registry.add('setof', 3, setofBuiltin);
68
+ registry.add('findall', 3, findallBuiltin, { deterministic: true });
69
+ registry.add('bagof', 3, bagofBuiltin, { deterministicWhen: allSolutionsHasAtMostOneGroup });
70
+ registry.add('setof', 3, setofBuiltin, { deterministicWhen: allSolutionsHasAtMostOneGroup });
71
71
  registry.add('clause', 2, clauseBuiltin, {
72
72
  shouldUse: ({ solver }) => solver.program.findGroup('clause', 2) == null,
73
73
  });
@@ -2367,6 +2367,16 @@ function allSolutionsBuiltin(asSet) {
2367
2367
  const bagofBuiltin = allSolutionsBuiltin(false);
2368
2368
  const setofBuiltin = allSolutionsBuiltin(true);
2369
2369
 
2370
+ function allSolutionsHasAtMostOneGroup({ goal, env }) {
2371
+ try {
2372
+ const { iterated, quantified } = bagGoalParts(goal.args[1], env);
2373
+ return freeVariables(iterated, goal.args[0], quantified, env).length === 0;
2374
+ } catch (_) {
2375
+ // Leave error selection and reporting to the builtin itself.
2376
+ return false;
2377
+ }
2378
+ }
2379
+
2370
2380
  function callable(term, env) {
2371
2381
  term = deref(term, env);
2372
2382
  if (term.type === VAR) throw new PrologError('instantiation_error');
@@ -2823,35 +2833,57 @@ function* negationBuiltin({ solver, goal, env }) {
2823
2833
  for (const _ of solver.cloneForInnerGoal(1).solve([invoked], env.clone(), 0)) return;
2824
2834
  yield env;
2825
2835
  }
2826
- function* solveControlBranch(solver, goal, env) {
2836
+ function* solveControlBranch(solver, goal, env, observePending = null) {
2837
+ const existingStacks = new Set(solver.solveStacks);
2827
2838
  for (const answer of solver.solve([callable(goal, env)], env, 0)) {
2828
2839
  // A branch answer is internal to its enclosing control construct. The
2829
2840
  // surrounding solve will count the completed control goal after the
2830
2841
  // builtin yields it. Leaving both counts in place makes a bounded search
2831
2842
  // such as once/1 or negation stop before it can observe the branch answer.
2832
2843
  if (solver.solutionsSeen > 0) solver.solutionsSeen--;
2844
+ observePending?.(solver.hasPendingAlternatives(existingStacks));
2833
2845
  yield answer;
2834
2846
  }
2835
2847
  }
2836
- function* disjunctionBuiltin({ solver, goal, env }) {
2848
+ function disjunctionBuiltin(context) {
2849
+ const state = { pending: true };
2850
+ const iterator = disjunctionSolutions(context, state);
2851
+ iterator.hasPendingAlternatives = () => state.pending;
2852
+ return iterator;
2853
+ }
2854
+ function* disjunctionSolutions({ solver, goal, env }, state) {
2837
2855
  const left = deref(goal.args[0], env);
2838
2856
  if (left.type === COMPOUND && left.name === '->' && left.arity === 2) {
2839
2857
  for (const conditionEnv of solver.cloneForInnerGoal(1).solve([callable(left.args[0], env)], env.clone(), 0)) {
2840
- yield* solveControlBranch(solver, left.args[1], conditionEnv);
2858
+ yield* solveControlBranch(solver, left.args[1], conditionEnv,
2859
+ (pending) => { state.pending = pending; });
2860
+ state.pending = false;
2841
2861
  return;
2842
2862
  }
2843
- yield* solveControlBranch(solver, goal.args[1], env.clone());
2863
+ yield* solveControlBranch(solver, goal.args[1], env.clone(),
2864
+ (pending) => { state.pending = pending; });
2865
+ state.pending = false;
2844
2866
  return;
2845
2867
  }
2846
2868
  const marker = solver.active[solver.active.length - 1] ?? null;
2847
2869
  const markerCutEpoch = marker?.cutEpoch ?? 0;
2848
2870
  const solverCutEpoch = solver.cutEpoch;
2849
- yield* solveControlBranch(solver, goal.args[0], env.clone());
2871
+ yield* solveControlBranch(solver, goal.args[0], env.clone(), (pending) => {
2872
+ const cutThisScope = marker == null
2873
+ ? solver.cutEpoch !== solverCutEpoch
2874
+ : (marker.cutEpoch ?? 0) !== markerCutEpoch;
2875
+ state.pending = pending || !cutThisScope;
2876
+ });
2850
2877
  const cutThisScope = marker == null
2851
2878
  ? solver.cutEpoch !== solverCutEpoch
2852
2879
  : (marker.cutEpoch ?? 0) !== markerCutEpoch;
2853
- if (cutThisScope) return;
2854
- yield* solveControlBranch(solver, goal.args[1], env.clone());
2880
+ if (cutThisScope) {
2881
+ state.pending = false;
2882
+ return;
2883
+ }
2884
+ yield* solveControlBranch(solver, goal.args[1], env.clone(),
2885
+ (pending) => { state.pending = pending; });
2886
+ state.pending = false;
2855
2887
  }
2856
2888
  function* ifThenBuiltin({ solver, goal, env }) {
2857
2889
  for (const conditionEnv of solver.cloneForInnerGoal(1).solve([callable(goal.args[0], env)], env.clone(), 0)) {
@@ -2875,6 +2907,7 @@ export class BuiltinRegistry {
2875
2907
  arity,
2876
2908
  handler,
2877
2909
  deterministic: options.deterministic ?? false,
2910
+ deterministicWhen: options.deterministicWhen ?? null,
2878
2911
  ready: options.ready ?? null,
2879
2912
  fallbackWhenNotReady: options.fallbackWhenNotReady ?? false,
2880
2913
  shouldUse: options.shouldUse ?? null,
package/src/solver.js CHANGED
@@ -607,14 +607,16 @@ export class Solver {
607
607
  const def = callable ? this.registry.get(goal.name, goal.arity) : null;
608
608
  this.active = active;
609
609
  if (def && builtinIsReadyOrAuthoritative(def, this, goal, env)) {
610
+ const deterministic = def.deterministic ||
611
+ def.deterministicWhen?.({ solver: this, goal, env }) === true;
610
612
  const iterator = def.handler({ solver: this, goal, env });
611
613
  const firstResult = iterator.next();
612
- if (def.deterministic) {
614
+ if (deterministic) {
613
615
  if (!firstResult.done) this.stats.deterministic_builtin_successes++;
614
616
  else this.stats.deterministic_builtin_failures++;
615
617
  }
616
618
  if (firstResult.done) break;
617
- if (!def.deterministic) {
619
+ if (!deterministic) {
618
620
  stack.push({
619
621
  kind: 'resumeBuiltin',
620
622
  iterator,
@@ -819,11 +821,22 @@ export class Solver {
819
821
  }
820
822
  }
821
823
 
822
- hasPendingAlternatives() {
823
- // When solve() is suspended at an answer, active solve stacks contain only
824
- // unexplored work. The demand-driven REPL uses this without speculatively
825
- // pulling the next answer.
826
- return this.solveStacks.some((stack) => stack.length !== 0);
824
+ hasPendingAlternatives(excludedStacks = null) {
825
+ // When solve() is suspended at an answer, active solve stacks contain
826
+ // unexplored work or an iterator frame that can prove it just yielded its
827
+ // final answer. The demand-driven REPL uses this without speculatively
828
+ // pulling the next answer. Control builtins can exclude their caller's
829
+ // pre-existing stacks when inspecting a nested branch.
830
+ return this.solveStacks.some((stack) => !excludedStacks?.has(stack) && stack.some((frame) => {
831
+ if (frame.kind === 'resumeBuiltin' &&
832
+ typeof frame.iterator?.hasPendingAlternatives === 'function') {
833
+ return frame.iterator.hasPendingAlternatives();
834
+ }
835
+ if (frame.kind === 'userClause' && headCannotMatch(frame.goal, frame.clause.head, frame.env)) {
836
+ return false;
837
+ }
838
+ return true;
839
+ }));
827
840
  }
828
841
 
829
842
  fastCountGoal(goal, env) {
@@ -1440,7 +1453,10 @@ function bundledBetweenIterator(solver, group, goal, env) {
1440
1453
  // the generated value in the caller repeatedly revisits all earlier frames.
1441
1454
  // Enumerate the canonical bundled relation directly while leaving user
1442
1455
  // definitions and non-EyeProlog registries on the ordinary Prolog path.
1443
- return bundledBetweenSolutions(solver, goal, env);
1456
+ const state = { pending: true };
1457
+ const iterator = bundledBetweenSolutions(solver, goal, env, state);
1458
+ iterator.hasPendingAlternatives = () => state.pending;
1459
+ return iterator;
1444
1460
  }
1445
1461
 
1446
1462
  function requireBetweenInteger(term, env) {
@@ -1452,7 +1468,7 @@ function requireBetweenInteger(term, env) {
1452
1468
  return BigInt(value.name);
1453
1469
  }
1454
1470
 
1455
- function* bundledBetweenSolutions(solver, goal, env) {
1471
+ function* bundledBetweenSolutions(solver, goal, env, state) {
1456
1472
  const lower = requireBetweenInteger(goal.args[0], env);
1457
1473
  const upper = requireBetweenInteger(goal.args[1], env);
1458
1474
  const requested = deref(goal.args[2], env);
@@ -1462,15 +1478,23 @@ function* bundledBetweenSolutions(solver, goal, env) {
1462
1478
  throw new PrologError('type_error(integer)', requested);
1463
1479
  }
1464
1480
  const value = BigInt(requested.name);
1465
- if (value >= lower && value <= upper) yield env;
1481
+ if (value >= lower && value <= upper) {
1482
+ state.pending = false;
1483
+ yield env;
1484
+ }
1485
+ state.pending = false;
1466
1486
  return;
1467
1487
  }
1468
1488
 
1469
1489
  for (let value = lower; value <= upper; value++) {
1470
1490
  const next = env.clone();
1471
1491
  solver.stats.unify_calls++;
1472
- if (unify(goal.args[2], numberTerm(value), next)) yield next;
1492
+ if (unify(goal.args[2], numberTerm(value), next)) {
1493
+ state.pending = value < upper;
1494
+ yield next;
1495
+ }
1473
1496
  }
1497
+ state.pending = false;
1474
1498
  }
1475
1499
 
1476
1500
  function bundledMemberIterator(solver, group, goal, env) {
@@ -1487,17 +1511,33 @@ function bundledMemberIterator(solver, group, goal, env) {
1487
1511
  let cursor = deref(goal.args[1], env);
1488
1512
  while (isCons(cursor)) cursor = deref(cursor.args[1], env);
1489
1513
  if (!isEmptyList(cursor)) return null;
1490
- return bundledMemberSolutions(solver, goal, env);
1514
+ const state = { pending: true };
1515
+ const iterator = bundledMemberSolutions(solver, goal, env, state);
1516
+ iterator.hasPendingAlternatives = () => state.pending;
1517
+ return iterator;
1491
1518
  }
1492
1519
 
1493
- function* bundledMemberSolutions(solver, goal, env) {
1520
+ function* bundledMemberSolutions(solver, goal, env, state) {
1494
1521
  let cursor = deref(goal.args[1], env);
1495
1522
  while (isCons(cursor)) {
1523
+ const item = cursor.args[0];
1524
+ cursor = deref(cursor.args[1], env);
1496
1525
  const next = env.clone();
1497
1526
  solver.stats.unify_calls++;
1498
- if (unify(goal.args[0], cursor.args[0], next)) yield next;
1527
+ if (unify(goal.args[0], item, next)) {
1528
+ state.pending = bundledMemberMayMatch(goal.args[0], cursor, env);
1529
+ yield next;
1530
+ }
1531
+ }
1532
+ state.pending = false;
1533
+ }
1534
+
1535
+ function bundledMemberMayMatch(target, cursor, env) {
1536
+ while (isCons(cursor)) {
1537
+ if (!goalHeadTermsCannotMatch(target, cursor.args[0], env)) return true;
1499
1538
  cursor = deref(cursor.args[1], env);
1500
1539
  }
1540
+ return false;
1501
1541
  }
1502
1542
 
1503
1543
  function bundledEllipsisPlan(solver, group, goal, rest, env) {
@@ -2849,11 +2889,22 @@ function headCannotMatch(goal, head, env) {
2849
2889
  if (goal.type !== COMPOUND || head.type !== COMPOUND) return false;
2850
2890
  if (goal.name !== head.name || goal.arity !== head.arity) return true;
2851
2891
  for (let i = 0; i < goal.arity; i++) {
2852
- const a = goal.args[i];
2853
- const b = head.args[i];
2854
- // Keep this only as a cheap scalar rejection. unify() remains authoritative.
2855
- const da = derefForLocal(a, env);
2856
- if (isScalarTerm(da) && isScalarTerm(b) && !sameScalarTerm(da, b)) return true;
2892
+ if (goalHeadTermsCannotMatch(goal.args[i], head.args[i], env)) return true;
2893
+ }
2894
+ return false;
2895
+ }
2896
+
2897
+ function goalHeadTermsCannotMatch(goalTerm, headTerm, env) {
2898
+ const pending = [[goalTerm, headTerm]];
2899
+ while (pending.length > 0) {
2900
+ const [goalItem, headItem] = pending.pop();
2901
+ const actual = derefForLocal(goalItem, env);
2902
+ if (actual.type === VAR || headItem.type === VAR) continue;
2903
+ if (actual.type !== headItem.type || actual.name !== headItem.name || actual.arity !== headItem.arity) return true;
2904
+ if (actual.type !== COMPOUND) continue;
2905
+ for (let index = 0; index < actual.arity; index++) {
2906
+ pending.push([actual.args[index], headItem.args[index]]);
2907
+ }
2857
2908
  }
2858
2909
  return false;
2859
2910
  }
@@ -1826,6 +1826,41 @@ c4 ?- call((!;1)).
1826
1826
  assertEqual(result.stderr, '', 'stderr');
1827
1827
  },
1828
1828
  },
1829
+ {
1830
+ name: 'REPL omits exhausted choicepoints without probing future branches (issue #74)',
1831
+ run: () => {
1832
+ const result = runCli([], {
1833
+ input:
1834
+ 'X=1;X=2.\n;\n' +
1835
+ 'setof(X,true,Xs).\n' +
1836
+ 'findall(t,false,Xs).\n' +
1837
+ 'use_module(library(lists)).\n' +
1838
+ 'member(a,"ba").\n' +
1839
+ 'append("a",Xs,Xs0).\n' +
1840
+ 'halt.\n',
1841
+ });
1842
+ assertEqual(result.status, 0, 'exit status');
1843
+ assertEqual(result.stdout,
1844
+ '?- X = 1\n' +
1845
+ '; X = 2.\n' +
1846
+ '?- Xs = [_A].\n' +
1847
+ '?- Xs = [].\n' +
1848
+ '?- true.\n' +
1849
+ '?- true.\n' +
1850
+ '?- Xs0 = [a | Xs].\n' +
1851
+ '?- ',
1852
+ 'stdout');
1853
+ assertEqual(result.stderr, '', 'stderr');
1854
+
1855
+ const grouped = new Solver(Program.parse(''));
1856
+ const groupedSolutions = grouped.solve([
1857
+ parseGoalText('setof(X,(Y=1,X=a;Y=2,X=b),Xs)'),
1858
+ ], new Env(), 0);
1859
+ assertEqual(groupedSolutions.next().done, false, 'first grouped setof/3 answer');
1860
+ assertEqual(grouped.hasPendingAlternatives(), true, 'genuine grouped setof/3 choicepoint');
1861
+ groupedSolutions.return();
1862
+ },
1863
+ },
1829
1864
  {
1830
1865
  name: 'REPL hides aliases to fresh throw variables while preserving query aliases',
1831
1866
  run: () => {