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 +12 -1
- package/examples/book/README.md +1 -0
- package/examples/book/chapter-40/02-program.pl +5 -0
- package/package.json +1 -1
- package/src/quads.js +48 -9
- package/src/solver.js +7 -0
- package/test/run-regression.mjs +27 -0
- package/the-art-of-eyeprolog.md +61 -10
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
|
|
package/examples/book/README.md
CHANGED
package/package.json
CHANGED
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/test/run-regression.mjs
CHANGED
|
@@ -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: () => {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -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
|
-
|
|
6900
|
-
`
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
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
|
|