eyeprolog 1.3.38 → 1.3.39

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,13 @@ 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.
88
94
 
89
95
  ## Tabling and well-founded negation
90
96
 
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.39",
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,7 +6896,17 @@ 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.
6899
+ Following Trealla's quad convention, `sto` declares that the query is subject
6900
+ to occurs-check; the answer portion of an `sto`-annotated leaf remains
6901
+ implementation-dependent and is not compared. EyeProlog can nevertheless check
6902
+ some of the declaration without a second execution: the normal finite-tree
6903
+ unifier records a concrete occurs-check event as positive STO evidence. A
6904
+ naturally completed finite execution with no such event disproves `sto` (so
6905
+ `?- true. sto.` fails), while a search/resource boundary leaves the declaration
6906
+ conservatively unchecked. When the same quad declares STO and an occurs-check
6907
+ event is observed, an unannotated `unexpected` leaf does not reject an outcome
6908
+ that is implementation-dependent precisely because the query is STO. This is
6909
+ partial STO detection, not a decision procedure for the full STO/NSTO property.
6900
6910
  `loops` explicitly asks for bounded nontermination evidence and accepts direct
6901
6911
  active-variant cycle evidence from EyeProlog's normal recursion guard, with the
6902
6912
  loop depth/inference bounds as a fallback. Ordinary quad descriptions also have