eyeprolog 1.3.33 → 1.3.34

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.33",
6
+ "version": "1.3.34",
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
@@ -1939,7 +1939,13 @@ function validateControlCallable(term, culprit, env) {
1939
1939
  if (current.type !== COMPOUND || ![',', ';', '->'].includes(current.name) || current.arity !== 2) continue;
1940
1940
  for (let index = current.arity - 1; index >= 0; index--) {
1941
1941
  const argument = deref(current.args[index], env);
1942
- if (argument.type === VAR) throw new PrologError('instantiation_error');
1942
+ // A variable nested in a control construct is not an error at call/1
1943
+ // entry: an earlier goal may instantiate it before execution reaches
1944
+ // that position. If it is still unbound when selected, the solver then
1945
+ // raises instantiation_error at that point, after any preceding effects.
1946
+ // Non-variable non-callables are different: ISO call/1 validates those
1947
+ // eagerly and reports the whole control term as the culprit.
1948
+ if (argument.type === VAR) continue;
1943
1949
  if (argument.type !== ATOM && argument.type !== COMPOUND) {
1944
1950
  throw new PrologError('type_error(callable)', culprit);
1945
1951
  }
package/src/quads.js CHANGED
@@ -247,7 +247,10 @@ function executeQuery(program, query, input, maxSolutions, options) {
247
247
  error,
248
248
  tailOutput,
249
249
  inputPosition,
250
- loops: solver.depthLimitExceeded || solver.inferenceLimitExceeded,
250
+ // Accept direct structural evidence from EyeProlog's recursion guard as
251
+ // well as bounded-search evidence. A detected active-variant cycle is
252
+ // stronger evidence than merely reaching a timeout/inference ceiling.
253
+ loops: solver.recursionCycleDetected || solver.depthLimitExceeded || solver.inferenceLimitExceeded,
251
254
  };
252
255
  }
253
256
 
package/src/solver.js CHANGED
@@ -63,6 +63,10 @@ export class Solver {
63
63
  // limit accounting. time/1 snapshots this counter around the measured goal.
64
64
  this.inferenceObservation = options.inferenceObservation ?? { value: 0 };
65
65
  this.inferenceLimitExceeded = false;
66
+ // Set when the normal-profile recursion guard detects re-entry of an
67
+ // already active variant on the current search path. Quad `loops` checks
68
+ // use this structural evidence in addition to bounded resource probes.
69
+ this.recursionCycleDetected = false;
66
70
  this.maxMemoryBytes = options.maxMemoryBytes ?? softHeapLimit();
67
71
  this.memoryRecovery = options.memoryRecovery ?? {
68
72
  active: false,
@@ -261,6 +265,7 @@ export class Solver {
261
265
  if (!child || child === this || !child.stats) return;
262
266
  this.depthLimitExceeded ||= child.depthLimitExceeded;
263
267
  this.inferenceLimitExceeded ||= child.inferenceLimitExceeded;
268
+ this.recursionCycleDetected ||= child.recursionCycleDetected;
264
269
  for (const [key, value] of Object.entries(child.stats)) {
265
270
  if (key === 'max_depth' || key === 'max_goal_count') {
266
271
  this.stats[key] = Math.max(this.stats[key] ?? 0, value ?? 0);
@@ -845,7 +850,10 @@ export class Solver {
845
850
  }
846
851
 
847
852
  *solveUserGoalUncached(group, goal, rest, env, depth) {
848
- if (group.recursive && !group.cutRecursive && !group.linearNumeric && this.activeVariant(goal, env)) return;
853
+ if (group.recursive && !group.cutRecursive && !group.linearNumeric && this.activeVariant(goal, env)) {
854
+ this.recursionCycleDetected = true;
855
+ return;
856
+ }
849
857
  // Program indexes provide candidate clauses, but every candidate is still
850
858
  // freshened and unified below. The index is a performance hint, not a
851
859
  // semantic shortcut.
@@ -1047,7 +1055,10 @@ function pushMemoAnswerFrames(stack, entry, goal, rest, env, depth, active, solv
1047
1055
  }
1048
1056
 
1049
1057
  function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth, active) {
1050
- if (group.recursive && !group.cutRecursive && !group.linearNumeric && activeVariantIn(goal, env, active)) return;
1058
+ if (group.recursive && !group.cutRecursive && !group.linearNumeric && activeVariantIn(goal, env, active)) {
1059
+ solver.recursionCycleDetected = true;
1060
+ return;
1061
+ }
1051
1062
  if (group.fastPi && pushFastPiFrames(stack, goal, rest, env, depth, active)) return;
1052
1063
  if (tryPushGroundScalarRuleFrame(stack, solver, group, goal, rest, env, depth, active)) return;
1053
1064
  if (tryPushGroundChainFrames(stack, solver, group, goal, rest, env, depth, active)) return;
@@ -1981,7 +1992,10 @@ function tryPushScalarFactRunFrames(stack, solver, goals, env, depth, active) {
1981
1992
  }
1982
1993
 
1983
1994
  const goal = runGoals[state.index];
1984
- if (activeMightContain(goal, active) && activeVariantIn(goal, envWithLocal(env, state.names, state.values), active)) continue;
1995
+ if (activeMightContain(goal, active) && activeVariantIn(goal, envWithLocal(env, state.names, state.values), active)) {
1996
+ solver.recursionCycleDetected = true;
1997
+ continue;
1998
+ }
1985
1999
  solver.stats.solve_one_goal_calls++;
1986
2000
  const candidates = selectScalarFactCandidates(groups[state.index], goal, env, state.names, state.values);
1987
2001
  const nextStates = [];
@@ -2035,7 +2049,10 @@ function* scalarFactRunSolutions(solver, goals, groups, env, depth, active) {
2035
2049
  }
2036
2050
 
2037
2051
  const goal = goals[state.index];
2038
- if (activeMightContain(goal, active) && activeVariantIn(goal, envWithLocal(env, state.names, state.values), active)) continue;
2052
+ if (activeMightContain(goal, active) && activeVariantIn(goal, envWithLocal(env, state.names, state.values), active)) {
2053
+ solver.recursionCycleDetected = true;
2054
+ continue;
2055
+ }
2039
2056
  solver.stats.solve_one_goal_calls++;
2040
2057
  const candidates = selectScalarFactCandidates(groups[state.index], goal, env, state.names, state.values);
2041
2058
  const nextStates = [];
@@ -2205,7 +2222,11 @@ function tryPushCompactBinaryChainFrames(stack, solver, group, goal, rest, env,
2205
2222
  if (solver.solutionsSeen >= solver.solutionLimit) return true;
2206
2223
  solver.stats.max_depth = Math.max(solver.stats.max_depth, currentDepth);
2207
2224
  const seenSet = seen[secondType];
2208
- if (!seenSet || seenSet.has(secondName)) return true;
2225
+ if (!seenSet) return true;
2226
+ if (seenSet.has(secondName)) {
2227
+ solver.recursionCycleDetected = true;
2228
+ return true;
2229
+ }
2209
2230
  if (cache[secondType].has(secondName)) {
2210
2231
  rememberCompactChainSuccess(cache, seen);
2211
2232
  stack.push({ kind: 'goals', goals: rest, env, depth: depth + 1, active });
@@ -2326,8 +2347,14 @@ function tryPushGroundChainFrames(stack, solver, group, goal, rest, env, depth,
2326
2347
  if (solver.solutionsSeen >= solver.solutionLimit) return true;
2327
2348
  solver.stats.max_depth = Math.max(solver.stats.max_depth, currentDepth);
2328
2349
  const key = groundChainKey(currentGoal);
2329
- if (seen.has(key)) return true;
2330
- if (activeVariantIn(currentGoal, currentEnv, active)) return true;
2350
+ if (seen.has(key)) {
2351
+ solver.recursionCycleDetected = true;
2352
+ return true;
2353
+ }
2354
+ if (activeVariantIn(currentGoal, currentEnv, active)) {
2355
+ solver.recursionCycleDetected = true;
2356
+ return true;
2357
+ }
2331
2358
  if (solver.groundChainSuccess.has(key)) {
2332
2359
  rememberGroundChainSuccess(solver, seen);
2333
2360
  stack.push({ kind: 'goals', goals: rest, env: baseEnv, depth: depth + 1, active });
@@ -1317,6 +1317,22 @@ c4 ?- call((!;1)).
1317
1317
  assertEqual(result.stdout, 'quads: 1 run, 1 passed, 0 failed.\n', 'quad report');
1318
1318
  },
1319
1319
  },
1320
+ {
1321
+ name: 'runQuads preserves output before a delayed call/1 instantiation error (issue #57)',
1322
+ run: () => {
1323
+ const result = publicApi.runQuads(`16, "7.8.3.4#9"\n?- call((write(3), X)).\n outputs("3"), instantiation_error.\n`);
1324
+ assertEqual(result.passed, 1, 'quad passed');
1325
+ assertEqual(result.stdout, 'quads: 1 run, 1 passed, 0 failed.\n', 'quad report');
1326
+ },
1327
+ },
1328
+ {
1329
+ name: 'runQuads recognizes recursion-guard cycle evidence as loops (issue #58)',
1330
+ run: () => {
1331
+ const result = publicApi.runQuads(`inf :- inf, inf.\n\n23\n?- inf.\n loops.\n`);
1332
+ assertEqual(result.passed, 1, 'quad passed');
1333
+ assertEqual(result.stdout, 'quads: 1 run, 1 passed, 0 failed.\n', 'quad report');
1334
+ },
1335
+ },
1320
1336
  {
1321
1337
  name: '--quads runs embedded tests and reports failures through exit status',
1322
1338
  run: () => {
@@ -6831,9 +6831,12 @@ variable in the renamed exception term. `...` and `ad_infinitum` accept further
6831
6831
  indented descriptions after one query are independent checks: each re-runs the
6832
6832
  query, each is counted in the `quads:` summary, and a failing description does
6833
6833
  not suppress later descriptions for that query. `inputs/1` supplies and checks
6834
- consumed characters; `outputs/1` checks emitted characters. `sto` marks
6835
- an answer description that this finite-tree implementation skips. `loops` is
6836
- checked with a deterministic solver-depth budget. The advanced stream
6834
+ consumed characters; `outputs/1` checks characters emitted while reaching
6835
+ the described answer or error, including output produced before a later
6836
+ exception. `sto` marks an answer description that this finite-tree
6837
+ implementation skips. `loops` accepts direct active-variant cycle evidence from
6838
+ EyeProlog's normal recursion guard, with bounded depth/inference exhaustion as a
6839
+ fallback for loops that have no such structural witness. The advanced stream
6837
6840
  annotations `peeks/1` and `waits`, and the unordered `other_answer_sequence`
6838
6841
  annotation, are not executed by the current runner.
6839
6842