eyeprolog 1.2.34 → 1.2.35

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.2.34",
6
+ "version": "1.2.35",
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
@@ -2029,35 +2029,39 @@ function* negationBuiltin({ solver, goal, env }) {
2029
2029
  for (const _ of solver.cloneForInnerGoal(1).solve([callable(goal.args[0], env)], env.clone(), 0)) return;
2030
2030
  yield env;
2031
2031
  }
2032
+ function* solveControlBranch(solver, goal, env) {
2033
+ for (const answer of solver.solve([callable(goal, env)], env, 0)) {
2034
+ // A branch answer is internal to its enclosing control construct. The
2035
+ // surrounding solve will count the completed control goal after the
2036
+ // builtin yields it. Leaving both counts in place makes a bounded search
2037
+ // such as once/1 or negation stop before it can observe the branch answer.
2038
+ if (solver.solutionsSeen > 0) solver.solutionsSeen--;
2039
+ yield answer;
2040
+ }
2041
+ }
2032
2042
  function* disjunctionBuiltin({ solver, goal, env }) {
2033
2043
  const left = deref(goal.args[0], env);
2034
2044
  if (left.type === COMPOUND && left.name === '->' && left.arity === 2) {
2035
2045
  for (const conditionEnv of solver.cloneForInnerGoal(1).solve([callable(left.args[0], env)], env.clone(), 0)) {
2036
- yield* solver.solve([callable(left.args[1], conditionEnv)], conditionEnv, 0);
2046
+ yield* solveControlBranch(solver, left.args[1], conditionEnv);
2037
2047
  return;
2038
2048
  }
2039
- yield* solver.solve([callable(goal.args[1], env)], env.clone(), 0);
2049
+ yield* solveControlBranch(solver, goal.args[1], env.clone());
2040
2050
  return;
2041
2051
  }
2042
2052
  const marker = solver.active[solver.active.length - 1] ?? null;
2043
2053
  const markerCutEpoch = marker?.cutEpoch ?? 0;
2044
2054
  const solverCutEpoch = solver.cutEpoch;
2045
- yield* solver.solve([callable(goal.args[0], env)], env.clone(), 0);
2055
+ yield* solveControlBranch(solver, goal.args[0], env.clone());
2046
2056
  const cutThisScope = marker == null
2047
2057
  ? solver.cutEpoch !== solverCutEpoch
2048
2058
  : (marker.cutEpoch ?? 0) !== markerCutEpoch;
2049
2059
  if (cutThisScope) return;
2050
- yield* solver.solve([callable(goal.args[1], env)], env.clone(), 0);
2060
+ yield* solveControlBranch(solver, goal.args[1], env.clone());
2051
2061
  }
2052
2062
  function* ifThenBuiltin({ solver, goal, env }) {
2053
2063
  for (const conditionEnv of solver.cloneForInnerGoal(1).solve([callable(goal.args[0], env)], env.clone(), 0)) {
2054
- for (const consequentEnv of solver.solve([callable(goal.args[1], conditionEnv)], conditionEnv, 0)) {
2055
- // The consequent is an internal part of the current solution, not a
2056
- // completed top-level solution. Keep a surrounding bounded search (for
2057
- // example nested ISO once-as-if-then) from consuming its limit early.
2058
- if (solver.solutionsSeen > 0) solver.solutionsSeen--;
2059
- yield consequentEnv;
2060
- }
2064
+ yield* solveControlBranch(solver, goal.args[1], conditionEnv);
2061
2065
  return;
2062
2066
  }
2063
2067
  }
@@ -477,6 +477,32 @@ c4 ?- call((!;1)).
477
477
  assertEqual(result.stdout, 'quads: 22 run, 22 passed, 0 failed.\n', 'quad report');
478
478
  },
479
479
  },
480
+ {
481
+ name: 'negation observes disjunction through direct and call/1 execution',
482
+ run: () => {
483
+ const reported = publicApi.runQuads(String.raw`?- \+ (true ; true).
484
+ false.
485
+
486
+ ?- call(\+ (true ; true)).
487
+ false.
488
+ `);
489
+ assertEqual(reported.total, 2, 'reported query count');
490
+ assertEqual(reported.passed, 2, 'reported queries pass');
491
+
492
+ const program = Program.parse('');
493
+ const answerCount = (text) => {
494
+ const solver = new Solver(program, { registry: getEyePrologRegistry() });
495
+ return [...solver.solve([parseGoalText(text)], new Env(), 0)].length;
496
+ };
497
+
498
+ assertEqual(answerCount(String.raw`\+ (true ; true)`), 0, 'direct successful disjunction is negated');
499
+ assertEqual(answerCount(String.raw`call(\+ (true ; true))`), 0, 'called negation fails');
500
+ assertEqual(answerCount(String.raw`\+ (true ; fail)`), 0, 'successful left branch is observed');
501
+ assertEqual(answerCount(String.raw`\+ (fail ; true)`), 0, 'successful right branch is observed');
502
+ assertEqual(answerCount(String.raw`\+ (fail ; fail)`), 1, 'failed disjunction is negated');
503
+ assertEqual(answerCount('once((true ; true))'), 1, 'once keeps the first disjunction answer');
504
+ },
505
+ },
480
506
  {
481
507
  name: 'runQuads passes the complete vendored ISO phrase quad corpus',
482
508
  run: () => {
@@ -1844,7 +1844,10 @@ normalized at the solver boundary instead of leaking a JavaScript `RangeError`.
1844
1844
  ISO 13211-1 leaves the resource atom implementation dependent. EyeProlog uses
1845
1845
  `memory` for a finite host allocation/capacity ceiling and reserves the
1846
1846
  `finite_memory` spelling for the distinct convention where no finite amount of
1847
- memory could complete the computation.
1847
+ memory could complete the computation. After a recoverable memory error, the
1848
+ solver keeps a bounded recovery window while the failed search unwinds so the
1849
+ host can collect released query terms. The same solver can then run later
1850
+ queries; this recovery does not resume the query that exhausted its limit.
1848
1851
 
1849
1852
  The iterative solver keeps active-call frames only where they are semantically
1850
1853
  needed for cut scope or recursive variant guards. Bundled-library helpers whose
@@ -1853,9 +1856,15 @@ guard therefore do not copy a growing active-call sequence at every step.
1853
1856
  Under the normal EyeProlog registry, the bundled Prologue `length/2` also has a
1854
1857
  scoped iterative execution path: named lists are counted or constructed without
1855
1858
  recursive interpreter frames, and an anonymous list is not materialized because
1856
- its binding cannot be observed. The ordinary clauses remain the authoritative
1857
- module definition and are used unchanged by the ISO-only registry and whenever
1858
- delays or finite-domain constraints require their normal wake-up points.
1859
+ its binding cannot be observed. A newly constructed fixed-length suffix starts
1860
+ as a lazy compact skeleton and expands one ordinary `./2` cell at a time when
1861
+ unification, another list predicate, or answer readback inspects it. This is a
1862
+ storage optimization, not a distinct Prolog term or list semantics. Embedders
1863
+ that inspect the JavaScript term model can recognize this representation with
1864
+ `CompactListTerm`, `isCompactList`, and `compactListLength`, or construct one
1865
+ with `compactVariableList`. The ordinary clauses remain the authoritative module
1866
+ definition and are used unchanged by the ISO-only registry and whenever delays
1867
+ or finite-domain constraints require their normal wake-up points.
1859
1868
 
1860
1869
  ### Implementation boundary
1861
1870