eyeprolog 1.3.38 → 1.3.40

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/README.md CHANGED
@@ -84,7 +84,18 @@ use any normal Prolog term syntax; EyeProlog only requires it to be ground when
84
84
  the quad is checked. A non-ground label is a quad failure, not a source syntax
85
85
  error, and later quads still run. When a query has multiple indented answer
86
86
  descriptions, each description is checked and counted independently, so one
87
- failed expectation does not prevent the later ones from running.
87
+ failed expectation does not prevent the later ones from running. Following the
88
+ Trealla quad convention, `sto` declares that the query is subject to occurs-check.
89
+ EyeProlog now uses an occurs-check event observed during the query's ordinary
90
+ execution as positive STO evidence, and rejects `sto` when a finite execution
91
+ completes without such an event (for example `?- true. sto.`). If execution is
92
+ cut short by a search/resource boundary, the STO claim remains conservatively
93
+ unverified rather than being guessed.
94
+ For nontermination expectations, `loops` is kept distinct from resource
95
+ exhaustion: structural loop evidence may satisfy `loops`, while a bounded
96
+ search that cannot establish the requested answer sequence is reported as
97
+ `UNDECIDED`. In CLI quad mode, failures use exit status `1`; if there are no
98
+ failures but at least one undecided quad, the exit status is `2`.
88
99
 
89
100
  ## Tabling and well-founded negation
90
101
 
@@ -248,3 +248,4 @@ npm run generate
248
248
  ## Chapter 40: Running EyeProlog: command line and corpus
249
249
 
250
250
  - [01-color.pl](chapter-40/01-color.pl) — Embedded quad tests
251
+ - [02-program.pl](chapter-40/02-program.pl)
@@ -0,0 +1,5 @@
1
+ % From The Art of EyeProlog, Chapter 40.
2
+ inf :- inf, inf.
3
+
4
+ ?- inf.
5
+ loops.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.3.38",
6
+ "version": "1.3.40",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/quads.js CHANGED
@@ -36,11 +36,15 @@ export function runQuads(source, options = {}) {
36
36
  const results = [];
37
37
  const lines = [];
38
38
  for (const quad of quads) {
39
+ // `sto` declares a property of the query, not merely of the leaf in which
40
+ // the annotation happens to be written. Preserve that context while each
41
+ // answer description is still checked independently.
42
+ const context = { declaresSto: quad.answers.some(descriptionDeclaresSto) };
39
43
  // Every indented answer description is an independent portable quad test.
40
44
  // Re-run the query for each description so a failed expectation does not
41
45
  // prevent later expectations for the same query from being checked.
42
46
  for (const description of quad.answers) {
43
- const result = checkQuadDescription(program, quad, description, options);
47
+ const result = checkQuadDescription(program, quad, description, options, context);
44
48
  results.push(result);
45
49
  if (!result.ok) lines.push(formatFailure(program, quad, result, description));
46
50
  }
@@ -53,14 +57,14 @@ export function runQuads(source, options = {}) {
53
57
  return { stdout: lines.join(''), total: results.length, passed, failed, undecided, results };
54
58
  }
55
59
 
56
- function checkQuadDescription(program, quad, description, options) {
60
+ function checkQuadDescription(program, quad, description, options, context) {
57
61
  if (quad.id != null && !termIsGround(quad.id, new Env())) {
58
62
  return { ok: false, kind: 'bad_identifier', expected: quad.id };
59
63
  }
60
- return checkDescription(program, quad, description, options);
64
+ return checkDescription(program, quad, description, options, context);
61
65
  }
62
66
 
63
- function checkDescription(program, quad, description, options) {
67
+ function checkDescription(program, quad, description, options, context) {
64
68
  const alternatives = splitOperator(description, '|');
65
69
  // Probe an explicitly accepted nontermination outcome before alternatives
66
70
  // that would run the same query without a bound.
@@ -73,7 +77,7 @@ function checkDescription(program, quad, description, options) {
73
77
  let unsupported = null;
74
78
  let undecided = null;
75
79
  for (const alternative of ordered) {
76
- const checked = checkAlternative(program, quad, alternative, options);
80
+ const checked = checkAlternative(program, quad, alternative, options, context);
77
81
  if (checked.ok) return checked;
78
82
  if (checked.kind === 'unsupported') unsupported ??= checked;
79
83
  if (checked.kind === 'undecided') undecided ??= checked;
@@ -81,9 +85,9 @@ function checkDescription(program, quad, description, options) {
81
85
  return unsupported ?? undecided ?? { ok: false, kind: 'failed', expected: description };
82
86
  }
83
87
 
84
- function checkAlternative(program, quad, alternative, options) {
88
+ function checkAlternative(program, quad, alternative, options, context) {
85
89
  const leaves = splitOperator(alternative, ';').map(describeLeaf);
86
- if (leaves.some((leaf) => leaf.sto)) return { ok: true };
90
+ const requiresSto = leaves.some((leaf) => leaf.sto);
87
91
  const unsupported = leaves.find((leaf) => leaf.unsupported != null)?.unsupported;
88
92
  if (unsupported != null) {
89
93
  return { ok: false, kind: 'unsupported', expected: unsupported };
@@ -103,6 +107,18 @@ function checkAlternative(program, quad, alternative, options) {
103
107
  detectLoops: leaves.some((leaf) => leaf.loops),
104
108
  });
105
109
 
110
+ if (requiresSto) {
111
+ // Trealla currently treats the answer part of an `sto`-annotated leaf as
112
+ // implementation-dependent and skips it. EyeProlog can strengthen that
113
+ // conservatively: an observed occurs-check event proves the STO claim, and
114
+ // a naturally completed finite execution without one disproves it. When
115
+ // execution was cut short by a search/resource boundary, leave the claim
116
+ // unchecked rather than pretending to have proved NSTO.
117
+ if (actual.stoObserved) return { ok: true };
118
+ if (actual.nstoObserved) return { ok: false };
119
+ return { ok: true };
120
+ }
121
+
106
122
  if (inputSpecs.length > 0) {
107
123
  const leaf = leaves[0];
108
124
  const matches = actual.inputPosition === input.length && matchLeaf(program, quad.query, leaf, actual, 0);
@@ -114,7 +130,12 @@ function checkAlternative(program, quad, alternative, options) {
114
130
  for (const leaf of leaves) {
115
131
  if (leaf.more && !leaf.hasExpectation) return { ok: true };
116
132
  const matches = matchLeaf(program, quad.query, leaf, actual, position);
117
- if (leaf.unexpected ? matches : !matches) {
133
+ // Once a quad explicitly declares the query STO and this execution has
134
+ // observed an occurs-check event, an unannotated `unexpected` leaf cannot
135
+ // portably outlaw the implementation's chosen STO outcome. This is the
136
+ // case behind issue #60's `false, unexpected` example.
137
+ const stoPermitsUnexpected = context.declaresSto && actual.stoObserved && leaf.unexpected && !leaf.sto;
138
+ if (!stoPermitsUnexpected && (leaf.unexpected ? matches : !matches)) {
118
139
  if (actual.undecided && leafNeedsMoreSearch(leaf, actual, position)) {
119
140
  return undecidedResult(actual, alternative);
120
141
  }
@@ -245,19 +266,28 @@ function executeQuery(program, query, input, maxSolutions, options) {
245
266
  const solutions = [];
246
267
  let error = null;
247
268
  let tailOutput = '';
269
+ let complete = false;
270
+ let resourceInterrupted = false;
271
+ let iterator = null;
248
272
  try {
249
- const iterator = solver.solve([query], new Env(), 0);
273
+ iterator = solver.solve([query], new Env(), 0);
250
274
  while (solutions.length < maxSolutions) {
251
275
  pendingOutput = '';
252
276
  const result = iterator.next();
253
277
  if (result.done) {
254
278
  tailOutput += pendingOutput;
279
+ complete = true;
255
280
  break;
256
281
  }
257
282
  solutions.push({ env: result.value, output: pendingOutput });
258
283
  }
259
284
  } catch (caught) {
260
285
  error = { term: errorTerm(caught), output: pendingOutput };
286
+ resourceInterrupted = caught?.name === 'PrologError' &&
287
+ String(caught.formal ?? '').startsWith('resource_error(');
288
+ complete = true;
289
+ } finally {
290
+ if (!complete) iterator?.return?.();
261
291
  }
262
292
  const inputPosition = solver.io.resolve('user_input')?.position ?? 0;
263
293
  const bounded = solver.depthLimitExceeded || solver.inferenceLimitExceeded;
@@ -267,6 +297,10 @@ function executeQuery(program, query, input, maxSolutions, options) {
267
297
  error,
268
298
  tailOutput,
269
299
  inputPosition,
300
+ complete,
301
+ stoObserved: solver.occursCheckObserved,
302
+ nstoObserved: complete && !solver.occursCheckObserved && !solver.recursionCycleDetected &&
303
+ !bounded && !resourceInterrupted,
270
304
  // A loops expectation explicitly asks for bounded nontermination evidence.
271
305
  // Other descriptions treat the same exhausted search budget as undecided:
272
306
  // a timeout cannot establish finite failure or an exact answer sequence.
@@ -294,6 +328,11 @@ function matchLeaf(program, query, leaf, actual, position) {
294
328
  return substitutionMatches(query, leaf.bindings, solution.env);
295
329
  }
296
330
 
331
+ function descriptionDeclaresSto(description) {
332
+ return splitOperator(description, '|').some((alternative) =>
333
+ splitOperator(alternative, ';').some((term) => describeLeaf(term).sto));
334
+ }
335
+
297
336
  function alternativeDescribesLoop(alternative) {
298
337
  return splitOperator(alternative, ';').some((term) => describeLeaf(term).loops);
299
338
  }
package/src/solver.js CHANGED
@@ -88,7 +88,13 @@ export class Solver {
88
88
  if (!ISO_CORE_FLAG_NAMES.has(name)) this.prologFlags.delete(name);
89
89
  }
90
90
  }
91
+ // Record a concrete occurs-check event even when the configured action is
92
+ // finite-tree failure rather than an exception. Quad `sto` checks can then
93
+ // use the query's real execution as evidence without running it a second
94
+ // time (which could repeat side effects).
95
+ this.occursCheckObserved = false;
91
96
  this.occursCheckHandler = (left, right, env) => {
97
+ this.occursCheckObserved = true;
92
98
  if (this.prologFlags.get('occurs_check')?.value?.name === 'error') {
93
99
  raiseOccursCheckError(left, right, env);
94
100
  }
@@ -266,6 +272,7 @@ export class Solver {
266
272
  this.depthLimitExceeded ||= child.depthLimitExceeded;
267
273
  this.inferenceLimitExceeded ||= child.inferenceLimitExceeded;
268
274
  this.recursionCycleDetected ||= child.recursionCycleDetected;
275
+ this.occursCheckObserved ||= child.occursCheckObserved;
269
276
  for (const [key, value] of Object.entries(child.stats)) {
270
277
  if (key === 'max_depth' || key === 'max_goal_count') {
271
278
  this.stats[key] = Math.max(this.stats[key] ?? 0, value ?? 0);
@@ -1356,6 +1356,33 @@ c4 ?- call((!;1)).
1356
1356
  assertEqual(result.stderr, '', 'stderr');
1357
1357
  },
1358
1358
  },
1359
+ {
1360
+ name: 'quad sto uses observed occurs-check evidence instead of unconditional acceptance (issue #60)',
1361
+ run: () => {
1362
+ const sto = publicApi.runQuads(String.raw`33
1363
+ ?- X = s(X).
1364
+ X = ..., unexpected.
1365
+ false, unexpected.
1366
+ sto, false
1367
+ | sto, true.
1368
+ `);
1369
+ assertEqual(sto.total, 3, 'STO description total');
1370
+ assertEqual(sto.passed, 3, 'STO descriptions passed');
1371
+ assertEqual(sto.failed, 0, 'STO descriptions failed');
1372
+ assertEqual(sto.undecided, 0, 'STO descriptions undecided');
1373
+ assertEqual(sto.stdout, 'quads: 3 run, 3 passed, 0 failed.\n', 'STO report');
1374
+
1375
+ const nsto = publicApi.runQuads(String.raw`34
1376
+ ?- true.
1377
+ sto.
1378
+ `);
1379
+ assertEqual(nsto.total, 1, 'NSTO description total');
1380
+ assertEqual(nsto.passed, 0, 'NSTO description passed');
1381
+ assertEqual(nsto.failed, 1, 'NSTO description failed');
1382
+ assertEqual(nsto.undecided, 0, 'NSTO description undecided');
1383
+ assertIncludes(nsto.stdout, 'quads: FAILED 34, <input>:1', 'NSTO diagnostic');
1384
+ },
1385
+ },
1359
1386
  {
1360
1387
  name: 'outputs/1 accepts DCG bodies over captured characters (issue #59)',
1361
1388
  run: () => {
@@ -6896,16 +6896,67 @@ described answer or error, including output produced before a later exception.
6896
6896
  Its argument may be an exact character list/string or a DCG body: terminal
6897
6897
  sequences, conjunction/disjunction, `...`/`ad_infinitum` sequence wildcards,
6898
6898
  and user-defined DCG nonterminals are matched against the captured characters.
6899
- `sto` marks an answer description that this finite-tree implementation skips.
6900
- `loops` explicitly asks for bounded nontermination evidence and accepts direct
6901
- active-variant cycle evidence from EyeProlog's normal recursion guard, with the
6902
- loop depth/inference bounds as a fallback. Ordinary quad descriptions also have
6903
- a finite inference budget (100000 by default); exhausting it does **not** mean
6904
- `loops` or `false`, but produces an `UNDECIDED` result. The JavaScript API may
6905
- override this with `quadMaxInferences`, while `loopMaxDepth` and
6906
- `loopMaxInferences` control the explicit `loops` probe. The advanced stream
6907
- annotations `peeks/1` and `waits`, and the unordered `other_answer_sequence`
6908
- annotation, are not executed by the current runner.
6899
+ The advanced stream annotations `peeks/1` and `waits`, and the unordered
6900
+ `other_answer_sequence` annotation, are not executed by the current runner.
6901
+
6902
+ #### STO, loops, and undecided quad results
6903
+
6904
+ Following Trealla's quad convention, `sto` declares that a query is subject to
6905
+ occurs-check. EyeProlog checks this conservatively rather than attempting a
6906
+ complete STO/NSTO decision procedure. During the query's ordinary execution,
6907
+ the finite-tree unifier records a concrete occurs-check event as positive STO
6908
+ evidence; the query is not run a second time merely to probe STO-ness. A finite
6909
+ execution that completes naturally without such an event disproves `sto`, while
6910
+ a search or resource boundary leaves the declaration conservatively unchecked.
6911
+ The answer portion of an `sto`-annotated leaf remains implementation-dependent
6912
+ and is therefore not compared.
6913
+
6914
+ For example, the cyclic binding in the first query provides positive STO
6915
+ evidence, whereas the second query is finite and cannot be STO:
6916
+
6917
+ ```eyeprolog
6918
+ ?- X = s(X).
6919
+ X = ..., unexpected.
6920
+ false, unexpected.
6921
+ sto, false
6922
+ | sto, true.
6923
+
6924
+ ?- true.
6925
+ sto. % fails: no STO evidence
6926
+ ```
6927
+
6928
+ When a quad declares STO and the execution observes an occurs-check event, an
6929
+ unannotated `unexpected` leaf does not reject the implementation-dependent
6930
+ finite-tree outcome. This is partial STO detection only: EyeProlog makes a
6931
+ definite statement where execution provides definite evidence and otherwise
6932
+ does not guess.
6933
+
6934
+ `loops` is kept distinct from merely exhausting the quad runner's resources.
6935
+ EyeProlog accepts structural nontermination evidence such as an active-variant
6936
+ recursion cycle, with the loop depth/inference bounds as a bounded fallback:
6937
+
6938
+ ```eyeprolog
6939
+ inf :- inf, inf.
6940
+
6941
+ ?- inf.
6942
+ loops.
6943
+ ```
6944
+
6945
+ Ordinary answer descriptions also have a finite inference budget (100000 by
6946
+ default). Exhausting that budget does **not** establish `loops` and does not
6947
+ turn an unfinished search into `false`; instead the description is reported as
6948
+ `UNDECIDED`, for example:
6949
+
6950
+ ```text
6951
+ quads: UNDECIDED expensive_case, program.pl:12
6952
+ undecided: inference limit reached.
6953
+ ```
6954
+
6955
+ Thus quad execution has three useful outcomes: passed, failed, and undecided.
6956
+ When there are no failures but at least one undecided description, the CLI exits
6957
+ with status `2`. The JavaScript API may override the ordinary search budget with
6958
+ `quadMaxInferences`; `loopMaxDepth` and `loopMaxInferences` control the explicit
6959
+ `loops` probe.
6909
6960
 
6910
6961
  The JavaScript API exposes the same operation without process I/O:
6911
6962