eyeprolog 1.3.35 → 1.3.36
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/index.d.ts +14 -2
- package/package.json +1 -1
- package/src/cli.js +1 -0
- package/src/quads.js +208 -27
- package/test/run-regression.mjs +47 -0
- package/the-art-of-eyeprolog.md +19 -10
package/index.d.ts
CHANGED
|
@@ -77,8 +77,9 @@ export interface EyePrologQuad {
|
|
|
77
77
|
|
|
78
78
|
export interface EyePrologQuadResult {
|
|
79
79
|
ok: boolean;
|
|
80
|
-
kind?: 'failed' | 'malformed' | 'bad_identifier' | 'unsupported';
|
|
80
|
+
kind?: 'failed' | 'malformed' | 'bad_identifier' | 'unsupported' | 'undecided';
|
|
81
81
|
expected?: EyePrologTerm;
|
|
82
|
+
reason?: string;
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
export interface EyePrologQuadRunResult {
|
|
@@ -86,9 +87,20 @@ export interface EyePrologQuadRunResult {
|
|
|
86
87
|
total: number;
|
|
87
88
|
passed: number;
|
|
88
89
|
failed: number;
|
|
90
|
+
undecided: number;
|
|
89
91
|
results: EyePrologQuadResult[];
|
|
90
92
|
}
|
|
91
93
|
|
|
94
|
+
export interface EyePrologQuadRunOptions extends EyePrologRunOptions {
|
|
95
|
+
initialize?: boolean;
|
|
96
|
+
/** Search budget for ordinary quad descriptions before reporting an undecided result. */
|
|
97
|
+
quadMaxInferences?: number;
|
|
98
|
+
/** Depth bound used when a quad explicitly expects loops. */
|
|
99
|
+
loopMaxDepth?: number;
|
|
100
|
+
/** Inference bound used when a quad explicitly expects loops. */
|
|
101
|
+
loopMaxInferences?: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
92
104
|
export interface EyePrologPredicateGroup {
|
|
93
105
|
name: string;
|
|
94
106
|
arity: number;
|
|
@@ -271,7 +283,7 @@ export class HaltSignal extends Error {
|
|
|
271
283
|
constructor(code?: number);
|
|
272
284
|
}
|
|
273
285
|
export function run(source: string | Program, options?: EyePrologRunOptions): EyePrologRunResult;
|
|
274
|
-
export function runQuads(source: string | Program, options?:
|
|
286
|
+
export function runQuads(source: string | Program, options?: EyePrologQuadRunOptions): EyePrologQuadRunResult;
|
|
275
287
|
export function whyProof(program: Program, goal: EyePrologTerm, options?: EyePrologRunOptions): { ok: boolean; text: string };
|
|
276
288
|
export function whyNoProof(goal: EyePrologTerm): string;
|
|
277
289
|
export function explainProof(program: Program, goal: EyePrologTerm, options?: EyePrologRunOptions): { ok: boolean; text: string };
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -168,6 +168,7 @@ export async function main(argv) {
|
|
|
168
168
|
const result = engine.runQuads(program, { initialize: options.goals.length === 0 });
|
|
169
169
|
process.stdout.write(result.stdout);
|
|
170
170
|
if (result.failed > 0) process.exitCode = 1;
|
|
171
|
+
else if (result.undecided > 0) process.exitCode = 2;
|
|
171
172
|
}
|
|
172
173
|
}
|
|
173
174
|
|
package/src/quads.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// used by Trealla and the ISO Prolog working examples linked from issue #1.
|
|
4
4
|
import {
|
|
5
5
|
ATOM, COMPOUND, VAR, Env, atom, compound, copyResolved, deref,
|
|
6
|
-
flattenConjunction, properListItems, termIsGround,
|
|
6
|
+
flattenConjunction, listFromItems, properListItems, termIsGround,
|
|
7
7
|
unify, variable,
|
|
8
8
|
} from './term.js';
|
|
9
9
|
import { parseGoalText } from './parser.js';
|
|
@@ -12,13 +12,16 @@ import { Solver } from './solver.js';
|
|
|
12
12
|
import { getEyePrologRegistry } from './standard-library.js';
|
|
13
13
|
import { formatTermForWrite } from './write.js';
|
|
14
14
|
|
|
15
|
+
const DEFAULT_QUAD_MAX_INFERENCES = 100000;
|
|
16
|
+
const DEFAULT_LOOP_MAX_INFERENCES = 10000;
|
|
17
|
+
|
|
15
18
|
export function runQuads(source, options = {}) {
|
|
16
19
|
const program = source instanceof Program
|
|
17
20
|
? source
|
|
18
21
|
: Program.parse(source, { ...options, sourceMetadata: true });
|
|
19
22
|
const quads = program.quads ?? [];
|
|
20
23
|
if (quads.length === 0) {
|
|
21
|
-
return { stdout: 'quads: nothing to run.\n', total: 0, passed: 0, failed: 0, results: [] };
|
|
24
|
+
return { stdout: 'quads: nothing to run.\n', total: 0, passed: 0, failed: 0, undecided: 0, results: [] };
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
if (options.initialize !== false) {
|
|
@@ -43,9 +46,11 @@ export function runQuads(source, options = {}) {
|
|
|
43
46
|
}
|
|
44
47
|
}
|
|
45
48
|
const passed = results.filter((result) => result.ok).length;
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
const undecided = results.filter((result) => result.kind === 'undecided').length;
|
|
50
|
+
const failed = results.length - passed - undecided;
|
|
51
|
+
const undecidedSummary = undecided === 0 ? '' : `, ${undecided} undecided`;
|
|
52
|
+
lines.push(`quads: ${results.length} run, ${passed} passed, ${failed} failed${undecidedSummary}.\n`);
|
|
53
|
+
return { stdout: lines.join(''), total: results.length, passed, failed, undecided, results };
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
function checkQuadDescription(program, quad, description, options) {
|
|
@@ -66,12 +71,14 @@ function checkDescription(program, quad, description, options) {
|
|
|
66
71
|
if (malformed != null) return { ok: false, kind: 'malformed', expected: malformed };
|
|
67
72
|
}
|
|
68
73
|
let unsupported = null;
|
|
74
|
+
let undecided = null;
|
|
69
75
|
for (const alternative of ordered) {
|
|
70
76
|
const checked = checkAlternative(program, quad, alternative, options);
|
|
71
77
|
if (checked.ok) return checked;
|
|
72
78
|
if (checked.kind === 'unsupported') unsupported ??= checked;
|
|
79
|
+
if (checked.kind === 'undecided') undecided ??= checked;
|
|
73
80
|
}
|
|
74
|
-
return unsupported ?? { ok: false, kind: 'failed', expected: description };
|
|
81
|
+
return unsupported ?? undecided ?? { ok: false, kind: 'failed', expected: description };
|
|
75
82
|
}
|
|
76
83
|
|
|
77
84
|
function checkAlternative(program, quad, alternative, options) {
|
|
@@ -98,15 +105,21 @@ function checkAlternative(program, quad, alternative, options) {
|
|
|
98
105
|
|
|
99
106
|
if (inputSpecs.length > 0) {
|
|
100
107
|
const leaf = leaves[0];
|
|
101
|
-
const matches = actual.inputPosition === input.length && matchLeaf(quad.query, leaf, actual, 0);
|
|
108
|
+
const matches = actual.inputPosition === input.length && matchLeaf(program, quad.query, leaf, actual, 0);
|
|
109
|
+
if (actual.undecided && !matches) return undecidedResult(actual, alternative);
|
|
102
110
|
return { ok: leaf.unexpected ? !matches : matches };
|
|
103
111
|
}
|
|
104
112
|
|
|
105
113
|
let position = 0;
|
|
106
114
|
for (const leaf of leaves) {
|
|
107
115
|
if (leaf.more && !leaf.hasExpectation) return { ok: true };
|
|
108
|
-
const matches = matchLeaf(quad.query, leaf, actual, position);
|
|
109
|
-
if (leaf.unexpected ? matches : !matches)
|
|
116
|
+
const matches = matchLeaf(program, quad.query, leaf, actual, position);
|
|
117
|
+
if (leaf.unexpected ? matches : !matches) {
|
|
118
|
+
if (actual.undecided && leafNeedsMoreSearch(leaf, actual, position)) {
|
|
119
|
+
return undecidedResult(actual, alternative);
|
|
120
|
+
}
|
|
121
|
+
return { ok: false };
|
|
122
|
+
}
|
|
110
123
|
// An unexpected error description is a negative assertion about that
|
|
111
124
|
// particular error pattern. A different exception may be present and is
|
|
112
125
|
// checked by other descriptions; do not make the final 'no error' test
|
|
@@ -114,12 +127,16 @@ function checkAlternative(program, quad, alternative, options) {
|
|
|
114
127
|
if (leaf.unexpected && leaf.error != null) return { ok: true };
|
|
115
128
|
if (leaf.more) return { ok: true };
|
|
116
129
|
if (!leaf.unexpected && (leaf.false || leaf.error != null)) {
|
|
130
|
+
if (actual.undecided) return undecidedResult(actual, alternative);
|
|
117
131
|
return { ok: position === leaves.length - 1 };
|
|
118
132
|
}
|
|
119
133
|
position++;
|
|
120
134
|
}
|
|
121
135
|
|
|
122
|
-
const ended = position >= actual.solutions.length && actual.error == null;
|
|
136
|
+
const ended = position >= actual.solutions.length && actual.error == null && !actual.undecided;
|
|
137
|
+
if (!ended && actual.undecided && position >= actual.solutions.length && actual.error == null) {
|
|
138
|
+
return undecidedResult(actual, alternative);
|
|
139
|
+
}
|
|
123
140
|
return { ok: ended };
|
|
124
141
|
}
|
|
125
142
|
|
|
@@ -184,9 +201,8 @@ function describeLeaf(term) {
|
|
|
184
201
|
continue;
|
|
185
202
|
}
|
|
186
203
|
if (item.type === COMPOUND && item.name === 'outputs' && item.arity === 1) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
else leaf.output = text;
|
|
204
|
+
if (leaf.output != null) leaf.malformed ??= item;
|
|
205
|
+
else leaf.output = item.args[0];
|
|
190
206
|
continue;
|
|
191
207
|
}
|
|
192
208
|
if (item.type === COMPOUND && item.name === 'peeks' && item.arity === 1) {
|
|
@@ -208,7 +224,9 @@ function executeQuery(program, query, input, maxSolutions, options) {
|
|
|
208
224
|
...options,
|
|
209
225
|
registry: options.registry ?? getEyePrologRegistry(),
|
|
210
226
|
maxDepth: options.detectLoops ? (options.loopMaxDepth ?? 1000) : options.maxDepth,
|
|
211
|
-
maxInferences: options.detectLoops
|
|
227
|
+
maxInferences: options.detectLoops
|
|
228
|
+
? (options.loopMaxInferences ?? DEFAULT_LOOP_MAX_INFERENCES)
|
|
229
|
+
: (options.quadMaxInferences ?? options.maxInferences ?? DEFAULT_QUAD_MAX_INFERENCES),
|
|
212
230
|
// The solver's counter also observes completed nested searches (for
|
|
213
231
|
// example each arm of a DCG disjunction). Bound the public iterator here
|
|
214
232
|
// instead of letting those internal completions consume the quad's answer
|
|
@@ -242,29 +260,37 @@ function executeQuery(program, query, input, maxSolutions, options) {
|
|
|
242
260
|
error = { term: errorTerm(caught), output: pendingOutput };
|
|
243
261
|
}
|
|
244
262
|
const inputPosition = solver.io.resolve('user_input')?.position ?? 0;
|
|
263
|
+
const bounded = solver.depthLimitExceeded || solver.inferenceLimitExceeded;
|
|
264
|
+
const undecided = !options.detectLoops && (solver.recursionCycleDetected || bounded);
|
|
245
265
|
return {
|
|
246
266
|
solutions,
|
|
247
267
|
error,
|
|
248
268
|
tailOutput,
|
|
249
269
|
inputPosition,
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
loops:
|
|
270
|
+
// A loops expectation explicitly asks for bounded nontermination evidence.
|
|
271
|
+
// Other descriptions treat the same exhausted search budget as undecided:
|
|
272
|
+
// a timeout cannot establish finite failure or an exact answer sequence.
|
|
273
|
+
loops: options.detectLoops && (solver.recursionCycleDetected || bounded),
|
|
274
|
+
undecided,
|
|
275
|
+
undecidedReason: solver.inferenceLimitExceeded ? 'inference limit reached'
|
|
276
|
+
: solver.depthLimitExceeded ? 'depth limit reached'
|
|
277
|
+
: solver.recursionCycleDetected ? 'recursion cycle encountered'
|
|
278
|
+
: null,
|
|
254
279
|
};
|
|
255
280
|
}
|
|
256
281
|
|
|
257
|
-
function matchLeaf(query, leaf, actual, position) {
|
|
282
|
+
function matchLeaf(program, query, leaf, actual, position) {
|
|
258
283
|
if (leaf.loops) return actual.loops;
|
|
259
284
|
if (leaf.false) {
|
|
260
|
-
return position >= actual.solutions.length && actual.error == null &&
|
|
285
|
+
return position >= actual.solutions.length && actual.error == null && !actual.undecided &&
|
|
286
|
+
outputMatches(program, leaf.output, actual.tailOutput);
|
|
261
287
|
}
|
|
262
288
|
if (leaf.error != null) {
|
|
263
289
|
return position === actual.solutions.length && actual.error != null &&
|
|
264
|
-
errorMatches(query, leaf.error, actual.error.term) && outputMatches(leaf.output, actual.error.output);
|
|
290
|
+
errorMatches(query, leaf.error, actual.error.term) && outputMatches(program, leaf.output, actual.error.output);
|
|
265
291
|
}
|
|
266
292
|
const solution = actual.solutions[position];
|
|
267
|
-
if (!solution || !outputMatches(leaf.output, solution.output)) return false;
|
|
293
|
+
if (!solution || !outputMatches(program, leaf.output, solution.output)) return false;
|
|
268
294
|
return substitutionMatches(query, leaf.bindings, solution.env);
|
|
269
295
|
}
|
|
270
296
|
|
|
@@ -391,10 +417,162 @@ function characterText(term) {
|
|
|
391
417
|
return text;
|
|
392
418
|
}
|
|
393
419
|
|
|
394
|
-
function outputMatches(expected, actual) {
|
|
395
|
-
|
|
420
|
+
function outputMatches(program, expected, actual) {
|
|
421
|
+
if (expected == null) return true;
|
|
422
|
+
|
|
423
|
+
// Preserve the original exact character-list/code-list shorthand first.
|
|
424
|
+
const exactText = characterText(expected);
|
|
425
|
+
if (exactText != null && exactText === actual) return true;
|
|
426
|
+
|
|
427
|
+
// Trealla's portable quad convention also permits the captured character
|
|
428
|
+
// sequence to unify directly with outputs/1's argument before trying it as a
|
|
429
|
+
// DCG body. This makes outputs(Cs) and partially instantiated character lists
|
|
430
|
+
// useful without weakening the DCG interpretation.
|
|
431
|
+
const actualList = listFromItems(Array.from(actual, (character) => atom(character)));
|
|
432
|
+
if (unify(expected, actualList, new Env())) return true;
|
|
433
|
+
|
|
434
|
+
return outputDcgMatches(program, expected, actual);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function outputDcgMatches(program, body, actual) {
|
|
438
|
+
const characters = Array.from(actual);
|
|
439
|
+
for (const state of matchOutputDcg(program, body, characters, 0, new Env())) {
|
|
440
|
+
if (state.position === characters.length) return true;
|
|
441
|
+
}
|
|
442
|
+
return false;
|
|
396
443
|
}
|
|
397
444
|
|
|
445
|
+
function* matchOutputDcg(program, body, characters, position, env) {
|
|
446
|
+
body = deref(body, env);
|
|
447
|
+
|
|
448
|
+
if (body.type === ATOM && (body.name === '...' || body.name === 'ad_infinitum')) {
|
|
449
|
+
for (let next = position; next <= characters.length; next++) {
|
|
450
|
+
yield { position: next, env: env.clone() };
|
|
451
|
+
}
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (body.type === ATOM && body.name === '[]') {
|
|
456
|
+
yield { position, env };
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (body.type === COMPOUND && body.name === '.' && body.arity === 2) {
|
|
461
|
+
const items = properListItems(body, env);
|
|
462
|
+
if (items == null) return;
|
|
463
|
+
let states = [{ position, env }];
|
|
464
|
+
for (const item of items) {
|
|
465
|
+
const nextStates = [];
|
|
466
|
+
for (const state of states) {
|
|
467
|
+
if (state.position >= characters.length) continue;
|
|
468
|
+
const expected = outputTerminalTerm(item, state.env);
|
|
469
|
+
if (expected == null) continue;
|
|
470
|
+
const nextEnv = state.env.clone();
|
|
471
|
+
if (unify(expected, atom(characters[state.position]), nextEnv)) {
|
|
472
|
+
nextStates.push({ position: state.position + 1, env: nextEnv });
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
states = nextStates;
|
|
476
|
+
if (states.length === 0) return;
|
|
477
|
+
}
|
|
478
|
+
yield* states;
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (body.type === COMPOUND && body.name === ',' && body.arity === 2) {
|
|
483
|
+
for (const left of matchOutputDcg(program, body.args[0], characters, position, env)) {
|
|
484
|
+
yield* matchOutputDcg(program, body.args[1], characters, left.position, left.env);
|
|
485
|
+
}
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (body.type === COMPOUND && [';', '|'].includes(body.name) && body.arity === 2) {
|
|
490
|
+
yield* matchOutputDcg(program, body.args[0], characters, position, env.clone());
|
|
491
|
+
yield* matchOutputDcg(program, body.args[1], characters, position, env.clone());
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if ((body.type === ATOM && ['!', '{}'].includes(body.name)) ||
|
|
496
|
+
(body.type === COMPOUND && body.name === '{}' && body.arity === 1 &&
|
|
497
|
+
body.args[0].type === ATOM && body.args[0].name === 'true')) {
|
|
498
|
+
yield { position, env };
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (body.type === COMPOUND && body.name === '{}' && body.arity === 1 &&
|
|
503
|
+
body.args[0].type === ATOM && body.args[0].name === 'fail') return;
|
|
504
|
+
|
|
505
|
+
// A nonterminal in an outputs/1 DCG body is interpreted against the same
|
|
506
|
+
// program as the query. Ask its expanded /2 relation how much of the
|
|
507
|
+
// remaining captured character list it consumes. This covers user-defined
|
|
508
|
+
// DCGs while the structural cases above handle terminals and ... without
|
|
509
|
+
// requiring a harness-only library import.
|
|
510
|
+
if (body.type === ATOM || body.type === COMPOUND) {
|
|
511
|
+
yield* matchOutputNonterminal(program, body, characters, position, env);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
let outputDcgFresh = 0;
|
|
516
|
+
function* matchOutputNonterminal(program, body, characters, position, env) {
|
|
517
|
+
const input = listFromItems(characters.slice(position).map((character) => atom(character)));
|
|
518
|
+
const output = variable(`\u0000quad-output:${++outputDcgFresh}`);
|
|
519
|
+
let goal;
|
|
520
|
+
if (body.type === ATOM) goal = compound(body.name, [input, output]);
|
|
521
|
+
else goal = compound(body.name, [...body.args, input, output]);
|
|
522
|
+
goal.module = body.module ?? 'user';
|
|
523
|
+
|
|
524
|
+
const solver = new Solver(program, {
|
|
525
|
+
registry: getEyePrologRegistry(),
|
|
526
|
+
maxInferences: DEFAULT_QUAD_MAX_INFERENCES,
|
|
527
|
+
solutionLimit: characters.length + 2,
|
|
528
|
+
ioOptions: { write: () => {} },
|
|
529
|
+
});
|
|
530
|
+
try {
|
|
531
|
+
for (const solutionEnv of solver.solve([goal], env.clone(), 0)) {
|
|
532
|
+
const tail = characterText(deref(output, solutionEnv));
|
|
533
|
+
if (tail == null) continue;
|
|
534
|
+
const remaining = characters.slice(position).join('');
|
|
535
|
+
if (!remaining.endsWith(tail)) continue;
|
|
536
|
+
yield {
|
|
537
|
+
position: characters.length - Array.from(tail).length,
|
|
538
|
+
env: solutionEnv,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
} catch (_) {
|
|
542
|
+
// A DCG body that raises while being used as an output matcher simply does
|
|
543
|
+
// not match, mirroring catch(phrase(Expected, Cs), _, fail).
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function outputTerminalTerm(term, env) {
|
|
548
|
+
term = deref(term, env);
|
|
549
|
+
if (term.type === 'number' && /^\d+$/.test(term.name)) {
|
|
550
|
+
const code = Number(term.name);
|
|
551
|
+
if (!Number.isSafeInteger(code) || code < 0 || code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) {
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
return atom(String.fromCodePoint(code));
|
|
555
|
+
}
|
|
556
|
+
return term;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function leafNeedsMoreSearch(leaf, actual, position) {
|
|
560
|
+
if (!actual.undecided) return false;
|
|
561
|
+
if (leaf.false) return true;
|
|
562
|
+
if (leaf.error != null) return actual.error == null && position >= actual.solutions.length;
|
|
563
|
+
return position >= actual.solutions.length;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function undecidedResult(actual, expected) {
|
|
567
|
+
return {
|
|
568
|
+
ok: false,
|
|
569
|
+
kind: 'undecided',
|
|
570
|
+
expected,
|
|
571
|
+
reason: actual.undecidedReason ?? 'search budget exhausted',
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
|
|
398
576
|
function splitOperator(term, name) {
|
|
399
577
|
if (term.type === COMPOUND && term.name === name && term.arity === 2) {
|
|
400
578
|
return [term.args[0], ...splitOperator(term.args[1], name)];
|
|
@@ -408,11 +586,14 @@ function formatFailure(program, quad, result, description = quad.answers[0]) {
|
|
|
408
586
|
const reason = result.kind === 'malformed' ? 'MALFORMED'
|
|
409
587
|
: result.kind === 'bad_identifier' ? 'BAD_ID'
|
|
410
588
|
: result.kind === 'unsupported' ? 'UNSUPPORTED'
|
|
411
|
-
: '
|
|
589
|
+
: result.kind === 'undecided' ? 'UNDECIDED'
|
|
590
|
+
: 'FAILED';
|
|
412
591
|
const expected = result.expected ?? description;
|
|
592
|
+
const detail = result.kind === 'undecided'
|
|
593
|
+
? ` undecided: ${result.reason}.\n`
|
|
594
|
+
: ` expected: ${formatQuadTerm(program, expected)}.\n`;
|
|
413
595
|
return `quads: ${reason} ${label}${source.filename}:${source.line}\n` +
|
|
414
|
-
` ?- ${formatQuadTerm(program, quad.query)}.\n` +
|
|
415
|
-
` expected: ${formatQuadTerm(program, expected)}.\n`;
|
|
596
|
+
` ?- ${formatQuadTerm(program, quad.query)}.\n` + detail;
|
|
416
597
|
}
|
|
417
598
|
|
|
418
599
|
function formatQuadTerm(program, term) {
|
package/test/run-regression.mjs
CHANGED
|
@@ -1333,6 +1333,53 @@ c4 ?- call((!;1)).
|
|
|
1333
1333
|
assertEqual(result.stdout, 'quads: 1 run, 1 passed, 0 failed.\n', 'quad report');
|
|
1334
1334
|
},
|
|
1335
1335
|
},
|
|
1336
|
+
{
|
|
1337
|
+
name: 'quad search-budget exhaustion is undecided rather than loops or failure (issue #58)',
|
|
1338
|
+
run: () => {
|
|
1339
|
+
const result = runCli(['--quads', '-'], {
|
|
1340
|
+
input:
|
|
1341
|
+
'24,passes/too_expensive\n' +
|
|
1342
|
+
'?- N is 10^9, between(1,N,I), I = 1.\n' +
|
|
1343
|
+
' N = ..., I = 1\n' +
|
|
1344
|
+
'; false.\n',
|
|
1345
|
+
timeout: 5000,
|
|
1346
|
+
});
|
|
1347
|
+
if (result.error) throw result.error;
|
|
1348
|
+
assertEqual(result.status, 2, 'undecided exit status');
|
|
1349
|
+
assertIncludes(result.stdout,
|
|
1350
|
+
'quads: UNDECIDED 24, passes / too_expensive, <stdin>:1',
|
|
1351
|
+
'undecided diagnostic');
|
|
1352
|
+
assertIncludes(result.stdout, 'undecided: inference limit reached.', 'undecided reason');
|
|
1353
|
+
assertIncludes(result.stdout,
|
|
1354
|
+
'quads: 1 run, 0 passed, 0 failed, 1 undecided.',
|
|
1355
|
+
'undecided summary');
|
|
1356
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
1357
|
+
},
|
|
1358
|
+
},
|
|
1359
|
+
{
|
|
1360
|
+
name: 'outputs/1 accepts DCG bodies over captured characters (issue #59)',
|
|
1361
|
+
run: () => {
|
|
1362
|
+
const source = [
|
|
1363
|
+
'pair --> "_", "A".',
|
|
1364
|
+
'22',
|
|
1365
|
+
"?- write('_A').",
|
|
1366
|
+
' outputs("_A").',
|
|
1367
|
+
' outputs("_"), unexpected.',
|
|
1368
|
+
' outputs(("_","A")).',
|
|
1369
|
+
' outputs(("_",...,"A")).',
|
|
1370
|
+
' outputs(("_",...,"B")), unexpected.',
|
|
1371
|
+
' outputs(("_",[_],[_])), unexpected.',
|
|
1372
|
+
' outputs(pair).',
|
|
1373
|
+
'',
|
|
1374
|
+
].join('\n');
|
|
1375
|
+
const result = publicApi.runQuads(source);
|
|
1376
|
+
assertEqual(result.total, 7, 'quad total');
|
|
1377
|
+
assertEqual(result.passed, 7, 'quad passed');
|
|
1378
|
+
assertEqual(result.failed, 0, 'quad failed');
|
|
1379
|
+
assertEqual(result.undecided, 0, 'quad undecided');
|
|
1380
|
+
assertEqual(result.stdout, 'quads: 7 run, 7 passed, 0 failed.\n', 'quad report');
|
|
1381
|
+
},
|
|
1382
|
+
},
|
|
1336
1383
|
{
|
|
1337
1384
|
name: '--quads runs embedded tests and reports failures through exit status',
|
|
1338
1385
|
run: () => {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -6820,9 +6820,11 @@ requires that argument to be ground; a non-ground label is reported as a quad
|
|
|
6820
6820
|
failure rather than aborting source parsing. Loading the file normally only
|
|
6821
6821
|
records its quads; it does not execute them or add their queries and answers as
|
|
6822
6822
|
program clauses. A quad run prints a summary and exits with status `1` when any
|
|
6823
|
-
description fails.
|
|
6824
|
-
|
|
6825
|
-
|
|
6823
|
+
description fails. If no description fails but a bounded search cannot decide
|
|
6824
|
+
an exact answer sequence, the case is reported separately as `UNDECIDED` and
|
|
6825
|
+
the CLI exits with status `2`. Quad mode imports `library(prologue)` as a
|
|
6826
|
+
compatibility prelude because the ISO Prolog working-example files use those
|
|
6827
|
+
predicates as system predicates without an explicit module directive.
|
|
6826
6828
|
|
|
6827
6829
|
Unless the source explicitly selects another `unknown` flag, quad execution
|
|
6828
6830
|
uses `unknown=error`, so an undefined predicate is reported rather than being
|
|
@@ -6839,12 +6841,19 @@ variable in the renamed exception term. `...` and `ad_infinitum` accept further
|
|
|
6839
6841
|
indented descriptions after one query are independent checks: each re-runs the
|
|
6840
6842
|
query, each is counted in the `quads:` summary, and a failing description does
|
|
6841
6843
|
not suppress later descriptions for that query. `inputs/1` supplies and checks
|
|
6842
|
-
consumed characters
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6844
|
+
consumed characters. `outputs/1` checks characters emitted while reaching the
|
|
6845
|
+
described answer or error, including output produced before a later exception.
|
|
6846
|
+
Its argument may be an exact character list/string or a DCG body: terminal
|
|
6847
|
+
sequences, conjunction/disjunction, `...`/`ad_infinitum` sequence wildcards,
|
|
6848
|
+
and user-defined DCG nonterminals are matched against the captured characters.
|
|
6849
|
+
`sto` marks an answer description that this finite-tree implementation skips.
|
|
6850
|
+
`loops` explicitly asks for bounded nontermination evidence and accepts direct
|
|
6851
|
+
active-variant cycle evidence from EyeProlog's normal recursion guard, with the
|
|
6852
|
+
loop depth/inference bounds as a fallback. Ordinary quad descriptions also have
|
|
6853
|
+
a finite inference budget (100000 by default); exhausting it does **not** mean
|
|
6854
|
+
`loops` or `false`, but produces an `UNDECIDED` result. The JavaScript API may
|
|
6855
|
+
override this with `quadMaxInferences`, while `loopMaxDepth` and
|
|
6856
|
+
`loopMaxInferences` control the explicit `loops` probe. The advanced stream
|
|
6848
6857
|
annotations `peeks/1` and `waits`, and the unordered `other_answer_sequence`
|
|
6849
6858
|
annotation, are not executed by the current runner.
|
|
6850
6859
|
|
|
@@ -6855,7 +6864,7 @@ import { Program, runQuads } from 'eyeprolog';
|
|
|
6855
6864
|
|
|
6856
6865
|
const program = Program.parse(source);
|
|
6857
6866
|
const report = runQuads(program);
|
|
6858
|
-
console.log(report.passed, report.failed, report.stdout);
|
|
6867
|
+
console.log(report.passed, report.failed, report.undecided, report.stdout);
|
|
6859
6868
|
```
|
|
6860
6869
|
|
|
6861
6870
|
The syntax follows the “queries using answer descriptions” convention used by
|