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 +7 -1
- 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 +11 -1
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
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,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`
|
|
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
|