eyeprolog 1.1.6 → 1.1.7
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 +9 -0
- package/examples/book/README.md +4 -0
- package/examples/book/chapter-40/01-color.pl +10 -0
- package/index.d.ts +28 -2
- package/package.json +1 -1
- package/src/cli.js +16 -5
- package/src/index.js +1 -0
- package/src/parser.js +48 -0
- package/src/program.js +13 -0
- package/src/quads.js +368 -0
- package/src/write.js +1 -0
- package/test/run-regression.mjs +76 -0
- package/the-art-of-eyeprolog.md +52 -1
package/README.md
CHANGED
|
@@ -45,6 +45,15 @@ Programs may declare their default queries with `%% goal:` comments.
|
|
|
45
45
|
Double-quoted text follows the ISO `double_quotes` flag and defaults to a
|
|
46
46
|
proper list of one-character atoms (`chars`), matching Trealla and Scryer.
|
|
47
47
|
|
|
48
|
+
Portable unit tests can be embedded as quads—a query followed by its expected
|
|
49
|
+
top-level answer—and run with `eyeprolog --quads program.pl`:
|
|
50
|
+
|
|
51
|
+
```prolog
|
|
52
|
+
member_test ?- member(X, [prolog, logic]).
|
|
53
|
+
X = prolog
|
|
54
|
+
; X = logic.
|
|
55
|
+
```
|
|
56
|
+
|
|
48
57
|
## ISO modules and definite clause grammars
|
|
49
58
|
|
|
50
59
|
EyeProlog implements ISO/IEC 13211-2 modules and the grammar rules and
|
package/examples/book/README.md
CHANGED
|
@@ -242,3 +242,7 @@ npm run generate
|
|
|
242
242
|
- [03-answer-3.pl](chapter-39/03-answer-3.pl)
|
|
243
243
|
- [04-cost.pl](chapter-39/04-cost.pl)
|
|
244
244
|
- [05-message.pl](chapter-39/05-message.pl)
|
|
245
|
+
|
|
246
|
+
## Chapter 40: Running EyeProlog: command line and corpus
|
|
247
|
+
|
|
248
|
+
- [01-color.pl](chapter-40/01-color.pl) — Embedded quad tests
|
package/index.d.ts
CHANGED
|
@@ -55,6 +55,29 @@ export interface EyePrologClause {
|
|
|
55
55
|
module?: string;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
export interface EyePrologQuad {
|
|
59
|
+
kind: 'quad';
|
|
60
|
+
id: EyePrologTerm | null;
|
|
61
|
+
query: EyePrologTerm;
|
|
62
|
+
answers: EyePrologTerm[];
|
|
63
|
+
module?: string;
|
|
64
|
+
source: { filename: string; line: number };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface EyePrologQuadResult {
|
|
68
|
+
ok: boolean;
|
|
69
|
+
kind?: 'failed' | 'malformed' | 'bad_identifier' | 'unsupported';
|
|
70
|
+
expected?: EyePrologTerm;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface EyePrologQuadRunResult {
|
|
74
|
+
stdout: string;
|
|
75
|
+
total: number;
|
|
76
|
+
passed: number;
|
|
77
|
+
failed: number;
|
|
78
|
+
results: EyePrologQuadResult[];
|
|
79
|
+
}
|
|
80
|
+
|
|
58
81
|
export interface EyePrologPredicateGroup {
|
|
59
82
|
name: string;
|
|
60
83
|
arity: number;
|
|
@@ -94,6 +117,7 @@ export class Program {
|
|
|
94
117
|
groups: Map<string, EyePrologPredicateGroup>;
|
|
95
118
|
modules: Map<string, { name: string; exports: Map<string, unknown>; filename: string }>;
|
|
96
119
|
moduleImports: Map<string, Map<string, string>>;
|
|
120
|
+
quads: EyePrologQuad[];
|
|
97
121
|
doubleQuotes: 'chars' | 'codes' | 'atom';
|
|
98
122
|
negationDependencies: Array<{ from: string; to: string; negative: boolean }>;
|
|
99
123
|
negationStratificationErrors: Array<{ from: string; to: string }>;
|
|
@@ -186,8 +210,8 @@ export function numberTextFromDouble(value: number): string | null;
|
|
|
186
210
|
export function compareNumberText(left: string, right: string): number;
|
|
187
211
|
|
|
188
212
|
export function makeProgram(source: string, options?: EyePrologRunOptions): Program;
|
|
189
|
-
export function parseClauses(source: string, options?: EyePrologRunOptions): EyePrologClause
|
|
190
|
-
export function parseProgramText(source: string, options?: EyePrologRunOptions): EyePrologClause
|
|
213
|
+
export function parseClauses(source: string, options?: EyePrologRunOptions): Array<EyePrologClause | EyePrologQuad>;
|
|
214
|
+
export function parseProgramText(source: string, options?: EyePrologRunOptions): Array<EyePrologClause | EyePrologQuad>;
|
|
191
215
|
export function parseGoalText(source: string, options?: EyePrologRunOptions): EyePrologTerm;
|
|
192
216
|
export function createDefaultRegistry(): BuiltinRegistry;
|
|
193
217
|
export function createEyePrologRegistry(): BuiltinRegistry;
|
|
@@ -208,6 +232,7 @@ export class HaltSignal extends Error {
|
|
|
208
232
|
constructor(code?: number);
|
|
209
233
|
}
|
|
210
234
|
export function run(source: string | Program, options?: EyePrologRunOptions): EyePrologRunResult;
|
|
235
|
+
export function runQuads(source: string | Program, options?: EyePrologRunOptions & { initialize?: boolean }): EyePrologQuadRunResult;
|
|
211
236
|
export function whyProof(program: Program, goal: EyePrologTerm, options?: EyePrologRunOptions): { ok: boolean; text: string };
|
|
212
237
|
export function whyNoProof(goal: EyePrologTerm): string;
|
|
213
238
|
export function explainProof(program: Program, goal: EyePrologTerm, options?: EyePrologRunOptions): { ok: boolean; text: string };
|
|
@@ -269,6 +294,7 @@ declare const eyeprolog: {
|
|
|
269
294
|
eyePrologNativeLibraryIndicators: typeof eyePrologNativeLibraryIndicators;
|
|
270
295
|
eyePrologPortableLibraryIndicators: typeof eyePrologPortableLibraryIndicators;
|
|
271
296
|
run: typeof run;
|
|
297
|
+
runQuads: typeof runQuads;
|
|
272
298
|
whyProof: typeof whyProof;
|
|
273
299
|
whyNoProof: typeof whyNoProof;
|
|
274
300
|
explainProof: typeof explainProof;
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -24,6 +24,7 @@ export async function main(argv) {
|
|
|
24
24
|
const options = {
|
|
25
25
|
files: [],
|
|
26
26
|
proof: false,
|
|
27
|
+
quads: false,
|
|
27
28
|
stats: false,
|
|
28
29
|
version: false,
|
|
29
30
|
warnings: false,
|
|
@@ -42,6 +43,8 @@ export async function main(argv) {
|
|
|
42
43
|
return;
|
|
43
44
|
} else if (!endOptions && (arg === '--proof' || arg === '-p')) {
|
|
44
45
|
options.proof = true;
|
|
46
|
+
} else if (!endOptions && (arg === '--quads' || arg === '-q')) {
|
|
47
|
+
options.quads = true;
|
|
45
48
|
} else if (!endOptions && (arg === '--stats' || arg === '-s')) {
|
|
46
49
|
options.stats = true;
|
|
47
50
|
} else if (!endOptions && (arg === '--version' || arg === '-v')) {
|
|
@@ -55,13 +58,14 @@ export async function main(argv) {
|
|
|
55
58
|
} else if (!endOptions && arg.startsWith('-') && !arg.startsWith('--') && arg.length > 2) {
|
|
56
59
|
const flags = arg.slice(1);
|
|
57
60
|
for (const flag of flags) {
|
|
58
|
-
if (!'
|
|
61
|
+
if (!'hpqsvw'.includes(flag)) throw new Error(`unknown option: ${arg}`);
|
|
59
62
|
}
|
|
60
63
|
if (flags.includes('h')) {
|
|
61
64
|
await usage(process.stdout);
|
|
62
65
|
return;
|
|
63
66
|
}
|
|
64
67
|
if (flags.includes('p')) options.proof = true;
|
|
68
|
+
if (flags.includes('q')) options.quads = true;
|
|
65
69
|
if (flags.includes('s')) options.stats = true;
|
|
66
70
|
if (flags.includes('v')) options.version = true;
|
|
67
71
|
if (flags.includes('w')) options.warnings = true;
|
|
@@ -102,7 +106,7 @@ export async function main(argv) {
|
|
|
102
106
|
}
|
|
103
107
|
}
|
|
104
108
|
|
|
105
|
-
if (options.goals.length === 0) {
|
|
109
|
+
if (options.goals.length === 0 && !options.quads) {
|
|
106
110
|
for (const source of sourceParts) options.goals.push(...goalsFromSource(source.text));
|
|
107
111
|
}
|
|
108
112
|
|
|
@@ -111,12 +115,17 @@ export async function main(argv) {
|
|
|
111
115
|
|
|
112
116
|
if (options.warnings) printWarnings(program);
|
|
113
117
|
|
|
114
|
-
await runDefault(engine, program, options);
|
|
118
|
+
if (!options.quads || options.goals.length > 0) await runDefault(engine, program, options);
|
|
119
|
+
if (options.quads) {
|
|
120
|
+
const result = engine.runQuads(program, { initialize: options.goals.length === 0 });
|
|
121
|
+
process.stdout.write(result.stdout);
|
|
122
|
+
if (result.failed > 0) process.exitCode = 1;
|
|
123
|
+
}
|
|
115
124
|
}
|
|
116
125
|
|
|
117
126
|
async function loadEngine() {
|
|
118
127
|
if (engineModule == null) {
|
|
119
|
-
const [term, parser, program, solver, iso, library, write] = await Promise.all([
|
|
128
|
+
const [term, parser, program, solver, iso, library, write, quads] = await Promise.all([
|
|
120
129
|
import('./term.js'),
|
|
121
130
|
import('./parser.js'),
|
|
122
131
|
import('./program.js'),
|
|
@@ -124,8 +133,9 @@ async function loadEngine() {
|
|
|
124
133
|
import('./iso.js'),
|
|
125
134
|
import('./standard-library.js'),
|
|
126
135
|
import('./write.js'),
|
|
136
|
+
import('./quads.js'),
|
|
127
137
|
]);
|
|
128
|
-
engineModule = { ...term, ...parser, ...program, ...solver, ...iso, ...library, ...write };
|
|
138
|
+
engineModule = { ...term, ...parser, ...program, ...solver, ...iso, ...library, ...write, ...quads };
|
|
129
139
|
}
|
|
130
140
|
return engineModule;
|
|
131
141
|
}
|
|
@@ -207,6 +217,7 @@ Input:
|
|
|
207
217
|
Options:
|
|
208
218
|
-h, --help Show this help text and exit.
|
|
209
219
|
-p, --proof Enable proof explanations.
|
|
220
|
+
-q, --quads Run embedded quad tests and fail if any do not hold.
|
|
210
221
|
-s, --stats Print solver statistics to stderr after execution.
|
|
211
222
|
-v, --version Show the package version and exit.
|
|
212
223
|
-w, --warnings Print non-fatal portability warnings to stderr.
|
package/src/index.js
CHANGED
|
@@ -20,6 +20,7 @@ export {
|
|
|
20
20
|
eyePrologPortableLibraryIndicators,
|
|
21
21
|
} from './standard-library.js';
|
|
22
22
|
export { StreamManager } from './io.js';
|
|
23
|
+
export { runQuads } from './quads.js';
|
|
23
24
|
|
|
24
25
|
import { ATOM, COMPOUND, VAR, Env, copyResolved, termIsGround, termToString } from './term.js';
|
|
25
26
|
import { Program } from './program.js';
|
package/src/parser.js
CHANGED
|
@@ -277,6 +277,14 @@ class Parser {
|
|
|
277
277
|
const line = this.line;
|
|
278
278
|
const ch = this.peek();
|
|
279
279
|
if (!ch) return { type: TOK.EOF, text: '', line };
|
|
280
|
+
if (this.source.startsWith('...', this.pos) && this.peek(3) !== '.') {
|
|
281
|
+
this.pos += 3;
|
|
282
|
+
return { type: TOK.ATOM, text: '...', line };
|
|
283
|
+
}
|
|
284
|
+
if (ch === '?' && this.peek(1) === '-') {
|
|
285
|
+
this.pos += 2;
|
|
286
|
+
return { type: TOK.ATOM, text: '?-', line };
|
|
287
|
+
}
|
|
280
288
|
if (ch === '!') {
|
|
281
289
|
this.take();
|
|
282
290
|
return { type: TOK.ATOM, text: '!', line };
|
|
@@ -591,12 +599,47 @@ class Parser {
|
|
|
591
599
|
}
|
|
592
600
|
throw new Error(`parse line ${this.token.line}: bad term`);
|
|
593
601
|
}
|
|
602
|
+
sourceLineIsIndented(line) {
|
|
603
|
+
let start = 0;
|
|
604
|
+
for (let current = 1; current < line; current++) {
|
|
605
|
+
const newline = this.source.indexOf('\n', start);
|
|
606
|
+
if (newline < 0) return false;
|
|
607
|
+
start = newline + 1;
|
|
608
|
+
}
|
|
609
|
+
return this.source[start] === ' ' || this.source[start] === '\t';
|
|
610
|
+
}
|
|
611
|
+
parseQuad(id, line, accept) {
|
|
612
|
+
const query = this.parseTerm(0, true);
|
|
613
|
+
this.expect(TOK.DOT, '.');
|
|
614
|
+
this.advance();
|
|
615
|
+
|
|
616
|
+
const answers = [];
|
|
617
|
+
while (this.token.type !== TOK.EOF && this.sourceLineIsIndented(this.token.line)) {
|
|
618
|
+
answers.push(this.parseTerm(0, true));
|
|
619
|
+
this.expect(TOK.DOT, '.');
|
|
620
|
+
this.advance();
|
|
621
|
+
}
|
|
622
|
+
if (answers.length === 0) throw new Error(`parse line ${line}: quad requires an indented answer description`);
|
|
623
|
+
|
|
624
|
+
accept({
|
|
625
|
+
kind: 'quad',
|
|
626
|
+
id,
|
|
627
|
+
query,
|
|
628
|
+
answers,
|
|
629
|
+
source: { filename: this.filename, line },
|
|
630
|
+
});
|
|
631
|
+
}
|
|
594
632
|
parseProgram(emit = null) {
|
|
595
633
|
const clauses = emit ? null : [];
|
|
596
634
|
let clauseNumber = 0;
|
|
597
635
|
const accept = emit ?? ((clause) => clauses.push(clause));
|
|
598
636
|
while (this.token.type !== TOK.EOF) {
|
|
599
637
|
const line = this.token.line;
|
|
638
|
+
if (this.operatorTokenName() === '?-') {
|
|
639
|
+
this.advance();
|
|
640
|
+
this.parseQuad(null, line, accept);
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
600
643
|
if (this.token.type === TOK.IF) {
|
|
601
644
|
this.advance();
|
|
602
645
|
const directive = this.parseTerm();
|
|
@@ -622,6 +665,11 @@ class Parser {
|
|
|
622
665
|
continue;
|
|
623
666
|
}
|
|
624
667
|
let head = this.parseTerm(3);
|
|
668
|
+
if (this.operatorTokenName() === '?-') {
|
|
669
|
+
this.advance();
|
|
670
|
+
this.parseQuad(head, line, accept);
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
625
673
|
// ISO/IEC TS 13211-3 grammar rules use -->/2 at the same top-level
|
|
626
674
|
// priority as clauses. The left side may include an unparenthesized
|
|
627
675
|
// semicontext: NonTerminal, Terminals --> Body.
|
package/src/program.js
CHANGED
|
@@ -104,6 +104,7 @@ export class Program {
|
|
|
104
104
|
this.defineOperator(priority, specifier, name);
|
|
105
105
|
}
|
|
106
106
|
this.initializations = [];
|
|
107
|
+
this.quads = [];
|
|
107
108
|
this.prologFlagDirectives = [];
|
|
108
109
|
this.charConversionDirectives = [];
|
|
109
110
|
this.doubleQuotes = options.doubleQuotes ?? 'chars';
|
|
@@ -471,6 +472,12 @@ class ProgramBuilder {
|
|
|
471
472
|
let lastGroup = this.lastGroup;
|
|
472
473
|
|
|
473
474
|
for (const clause of clauses) {
|
|
475
|
+
if (clause?.kind === 'quad') {
|
|
476
|
+
const module = clause.module ?? 'user';
|
|
477
|
+
annotateGoalModule(clause.query, module);
|
|
478
|
+
program.quads.push({ ...clause, module });
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
474
481
|
clause.index = program.clauses.length;
|
|
475
482
|
program.clauses.push(clause);
|
|
476
483
|
|
|
@@ -664,6 +671,12 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
|
|
|
664
671
|
batch.length = 0;
|
|
665
672
|
};
|
|
666
673
|
const accept = (clause) => {
|
|
674
|
+
if (clause?.kind === 'quad') {
|
|
675
|
+
flush();
|
|
676
|
+
clause.module = context.module;
|
|
677
|
+
builder.addClauses([clause]);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
667
680
|
const grammarClause = expandDcgRuleClause(clause, context.module);
|
|
668
681
|
if (grammarClause) clause = grammarClause;
|
|
669
682
|
const moduleDeclaration = moduleDirective(clause);
|
package/src/quads.js
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
// Embedded quad tests: a query followed by one or more answer descriptions.
|
|
2
|
+
// The syntax follows the portable "queries using answer descriptions" format
|
|
3
|
+
// used by Trealla and the ISO Prolog working examples linked from issue #1.
|
|
4
|
+
import {
|
|
5
|
+
ATOM, COMPOUND, VAR, Env, atom, compound, copyResolved, deref,
|
|
6
|
+
flattenConjunction, properListItems, termIsGround,
|
|
7
|
+
unify, variable,
|
|
8
|
+
} from './term.js';
|
|
9
|
+
import { parseGoalText } from './parser.js';
|
|
10
|
+
import { Program } from './program.js';
|
|
11
|
+
import { Solver } from './solver.js';
|
|
12
|
+
import { getEyePrologRegistry } from './standard-library.js';
|
|
13
|
+
import { formatTermForWrite } from './write.js';
|
|
14
|
+
|
|
15
|
+
export function runQuads(source, options = {}) {
|
|
16
|
+
const program = source instanceof Program
|
|
17
|
+
? source
|
|
18
|
+
: Program.parse(source, { ...options, sourceMetadata: true });
|
|
19
|
+
const quads = program.quads ?? [];
|
|
20
|
+
if (quads.length === 0) {
|
|
21
|
+
return { stdout: 'quads: nothing to run.\n', total: 0, passed: 0, failed: 0, results: [] };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (options.initialize !== false) {
|
|
25
|
+
const initializer = new Solver(program, {
|
|
26
|
+
...options,
|
|
27
|
+
registry: options.registry ?? getEyePrologRegistry(),
|
|
28
|
+
ioOptions: { write: () => {} },
|
|
29
|
+
});
|
|
30
|
+
initializer.runInitializations();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const results = [];
|
|
34
|
+
const lines = [];
|
|
35
|
+
for (const quad of quads) {
|
|
36
|
+
const result = checkQuad(program, quad, options);
|
|
37
|
+
results.push(result);
|
|
38
|
+
if (!result.ok) lines.push(formatFailure(program, quad, result));
|
|
39
|
+
}
|
|
40
|
+
const passed = results.filter((result) => result.ok).length;
|
|
41
|
+
const failed = results.length - passed;
|
|
42
|
+
lines.push(`quads: ${results.length} run, ${passed} passed, ${failed} failed.\n`);
|
|
43
|
+
return { stdout: lines.join(''), total: results.length, passed, failed, results };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function checkQuad(program, quad, options) {
|
|
47
|
+
if (quad.id != null && !termIsGround(quad.id, new Env())) {
|
|
48
|
+
return { ok: false, kind: 'bad_identifier', expected: quad.id };
|
|
49
|
+
}
|
|
50
|
+
for (const description of quad.answers) {
|
|
51
|
+
const checked = checkDescription(program, quad, description, options);
|
|
52
|
+
if (!checked.ok) return checked;
|
|
53
|
+
}
|
|
54
|
+
return { ok: true };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function checkDescription(program, quad, description, options) {
|
|
58
|
+
const alternatives = splitOperator(description, '|');
|
|
59
|
+
for (const alternative of alternatives) {
|
|
60
|
+
const malformed = malformedAlternative(quad.query, alternative);
|
|
61
|
+
if (malformed != null) return { ok: false, kind: 'malformed', expected: malformed };
|
|
62
|
+
}
|
|
63
|
+
let unsupported = null;
|
|
64
|
+
for (const alternative of alternatives) {
|
|
65
|
+
const checked = checkAlternative(program, quad, alternative, options);
|
|
66
|
+
if (checked.ok) return checked;
|
|
67
|
+
if (checked.kind === 'unsupported') unsupported ??= checked;
|
|
68
|
+
}
|
|
69
|
+
return unsupported ?? { ok: false, kind: 'failed', expected: description };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function checkAlternative(program, quad, alternative, options) {
|
|
73
|
+
const leaves = splitOperator(alternative, ';').map(describeLeaf);
|
|
74
|
+
if (leaves.some((leaf) => leaf.sto)) return { ok: true };
|
|
75
|
+
const unsupported = leaves.find((leaf) => leaf.unsupported != null)?.unsupported;
|
|
76
|
+
if (unsupported != null) {
|
|
77
|
+
return { ok: false, kind: 'unsupported', expected: unsupported };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const inputSpecs = [...new Set(leaves.filter((leaf) => leaf.input != null).map((leaf) => leaf.input))];
|
|
81
|
+
if (inputSpecs.length > 1) return { ok: false, kind: 'malformed', expected: alternative };
|
|
82
|
+
const input = inputSpecs[0] ?? '';
|
|
83
|
+
if (inputSpecs.length > 0 && leaves.length !== 1) return { ok: false };
|
|
84
|
+
const moreAt = leaves.findIndex((leaf) => leaf.more);
|
|
85
|
+
const describedCount = moreAt < 0 ? leaves.length : moreAt + (leaves[moreAt].hasExpectation ? 1 : 0);
|
|
86
|
+
const maxSolutions = inputSpecs.length > 0
|
|
87
|
+
? 1
|
|
88
|
+
: moreAt < 0 ? describedCount + 1 : Math.max(describedCount, 1);
|
|
89
|
+
const actual = executeQuery(program, quad.query, input, maxSolutions, options);
|
|
90
|
+
|
|
91
|
+
if (inputSpecs.length > 0) {
|
|
92
|
+
const leaf = leaves[0];
|
|
93
|
+
const matches = actual.inputPosition === input.length && matchLeaf(quad.query, leaf, actual, 0);
|
|
94
|
+
return { ok: leaf.unexpected ? !matches : matches };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let position = 0;
|
|
98
|
+
for (const leaf of leaves) {
|
|
99
|
+
if (leaf.more && !leaf.hasExpectation) return { ok: true };
|
|
100
|
+
const matches = matchLeaf(quad.query, leaf, actual, position);
|
|
101
|
+
if (leaf.unexpected ? matches : !matches) return { ok: false };
|
|
102
|
+
if (leaf.more) return { ok: true };
|
|
103
|
+
if (!leaf.unexpected && (leaf.false || leaf.error != null)) {
|
|
104
|
+
return { ok: position === leaves.length - 1 };
|
|
105
|
+
}
|
|
106
|
+
position++;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const ended = position >= actual.solutions.length && actual.error == null;
|
|
110
|
+
return { ok: ended };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function malformedAlternative(query, alternative) {
|
|
114
|
+
const queryNames = new Set(namedVariables(query).map((variable) => variable.name));
|
|
115
|
+
for (const leaf of splitOperator(alternative, ';').map(describeLeaf)) {
|
|
116
|
+
if (leaf.malformed != null) return leaf.malformed;
|
|
117
|
+
const names = new Set();
|
|
118
|
+
for (const binding of leaf.bindings) {
|
|
119
|
+
const name = binding.args[0].name;
|
|
120
|
+
if (!queryNames.has(name) || names.has(name)) return binding;
|
|
121
|
+
names.add(name);
|
|
122
|
+
}
|
|
123
|
+
if (!leaf.sto) {
|
|
124
|
+
for (const binding of leaf.bindings) {
|
|
125
|
+
if (namedVariables(binding.args[1]).some((variable) => names.has(variable.name))) return binding;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function describeLeaf(term) {
|
|
133
|
+
const leaf = {
|
|
134
|
+
bindings: [],
|
|
135
|
+
unexpected: false,
|
|
136
|
+
more: false,
|
|
137
|
+
sto: false,
|
|
138
|
+
false: false,
|
|
139
|
+
truth: false,
|
|
140
|
+
error: null,
|
|
141
|
+
input: null,
|
|
142
|
+
output: null,
|
|
143
|
+
unsupported: null,
|
|
144
|
+
malformed: null,
|
|
145
|
+
hasExpectation: false,
|
|
146
|
+
};
|
|
147
|
+
for (const item of flattenConjunction(term)) {
|
|
148
|
+
if (item.type === ATOM) {
|
|
149
|
+
if (item.name === 'unexpected' || item.name === 'inattendue') leaf.unexpected = true;
|
|
150
|
+
else if (item.name === '...' || item.name === 'ad_infinitum') leaf.more = true;
|
|
151
|
+
else if (item.name === 'sto') leaf.sto = true;
|
|
152
|
+
else if (item.name === 'loops' || item.name === 'waits' || item.name === 'other_answer_sequence') {
|
|
153
|
+
leaf.unsupported ??= item;
|
|
154
|
+
} else if (item.name === 'false') leaf.false = true;
|
|
155
|
+
else if (item.name === 'true') leaf.truth = true;
|
|
156
|
+
else if (isErrorDescription(item)) leaf.error = item;
|
|
157
|
+
else leaf.malformed ??= item;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (item.type === COMPOUND && item.name === '=' && item.arity === 2) {
|
|
161
|
+
if (item.args[0].type !== VAR) leaf.malformed ??= item;
|
|
162
|
+
else leaf.bindings.push(item);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (item.type === COMPOUND && item.name === 'inputs' && item.arity === 1) {
|
|
166
|
+
const text = characterText(item.args[0]);
|
|
167
|
+
if (text == null || leaf.input != null) leaf.malformed ??= item;
|
|
168
|
+
else leaf.input = text;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (item.type === COMPOUND && item.name === 'outputs' && item.arity === 1) {
|
|
172
|
+
const text = characterText(item.args[0]);
|
|
173
|
+
if (text == null || leaf.output != null) leaf.malformed ??= item;
|
|
174
|
+
else leaf.output = text;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (item.type === COMPOUND && item.name === 'peeks' && item.arity === 1) {
|
|
178
|
+
leaf.unsupported ??= item;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (isErrorDescription(item)) leaf.error = item;
|
|
182
|
+
else leaf.malformed ??= item;
|
|
183
|
+
}
|
|
184
|
+
leaf.hasExpectation = leaf.bindings.length > 0 || leaf.truth || leaf.false || leaf.error != null || leaf.output != null;
|
|
185
|
+
if (!leaf.hasExpectation && !leaf.more && !leaf.sto && leaf.unsupported == null) leaf.malformed ??= term;
|
|
186
|
+
if ([leaf.false, leaf.truth, leaf.error != null].filter(Boolean).length > 1) leaf.malformed ??= term;
|
|
187
|
+
return leaf;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function executeQuery(program, query, input, maxSolutions, options) {
|
|
191
|
+
let pendingOutput = '';
|
|
192
|
+
const solver = new Solver(program, {
|
|
193
|
+
...options,
|
|
194
|
+
registry: options.registry ?? getEyePrologRegistry(),
|
|
195
|
+
solutionLimit: Math.max(1, maxSolutions),
|
|
196
|
+
ioOptions: {
|
|
197
|
+
input,
|
|
198
|
+
write: (text) => { pendingOutput += String(text); },
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
const solutions = [];
|
|
202
|
+
let error = null;
|
|
203
|
+
let tailOutput = '';
|
|
204
|
+
try {
|
|
205
|
+
const iterator = solver.solve([query], new Env(), 0);
|
|
206
|
+
while (solutions.length < maxSolutions) {
|
|
207
|
+
pendingOutput = '';
|
|
208
|
+
const result = iterator.next();
|
|
209
|
+
if (result.done) {
|
|
210
|
+
tailOutput += pendingOutput;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
solutions.push({ env: result.value, output: pendingOutput });
|
|
214
|
+
}
|
|
215
|
+
} catch (caught) {
|
|
216
|
+
error = { term: errorTerm(caught), output: pendingOutput };
|
|
217
|
+
}
|
|
218
|
+
const inputPosition = solver.io.resolve('user_input')?.position ?? 0;
|
|
219
|
+
return { solutions, error, tailOutput, inputPosition };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function matchLeaf(query, leaf, actual, position) {
|
|
223
|
+
if (leaf.false) {
|
|
224
|
+
return position >= actual.solutions.length && actual.error == null && outputMatches(leaf.output, actual.tailOutput);
|
|
225
|
+
}
|
|
226
|
+
if (leaf.error != null) {
|
|
227
|
+
return position === actual.solutions.length && actual.error != null &&
|
|
228
|
+
errorMatches(leaf.error, actual.error.term) && outputMatches(leaf.output, actual.error.output);
|
|
229
|
+
}
|
|
230
|
+
const solution = actual.solutions[position];
|
|
231
|
+
if (!solution || !outputMatches(leaf.output, solution.output)) return false;
|
|
232
|
+
return substitutionMatches(query, leaf.bindings, solution.env);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function substitutionMatches(query, bindings, actualEnv) {
|
|
236
|
+
const queryVariables = namedVariables(query);
|
|
237
|
+
const queryNames = new Set(queryVariables.map((variable) => variable.name));
|
|
238
|
+
const expectedEnv = new Env();
|
|
239
|
+
const rebound = new Set();
|
|
240
|
+
for (const binding of bindings) {
|
|
241
|
+
const variable = binding.args[0];
|
|
242
|
+
if (!queryNames.has(variable.name) || rebound.has(variable.name)) return false;
|
|
243
|
+
rebound.add(variable.name);
|
|
244
|
+
if (!unify(variable, binding.args[1], expectedEnv)) return false;
|
|
245
|
+
}
|
|
246
|
+
const expected = compound('$quad_answer', queryVariables.map((variable) => copyResolved(variable, expectedEnv)));
|
|
247
|
+
const actual = compound('$quad_answer', queryVariables.map((variable) => copyResolved(variable, actualEnv)));
|
|
248
|
+
return patternVariant(expected, new Env(), actual, new Env());
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function namedVariables(term) {
|
|
252
|
+
const found = [];
|
|
253
|
+
const seen = new Set();
|
|
254
|
+
const stack = [term];
|
|
255
|
+
while (stack.length) {
|
|
256
|
+
const current = stack.pop();
|
|
257
|
+
if (current.type === VAR) {
|
|
258
|
+
if (!current.name.startsWith('__anon') && !seen.has(current.name)) {
|
|
259
|
+
seen.add(current.name);
|
|
260
|
+
found.push(current);
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
for (let index = current.args.length - 1; index >= 0; index--) stack.push(current.args[index]);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return found;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function patternVariant(pattern, patternEnv, actual, actualEnv, pairs = new Map(), reverse = new Map()) {
|
|
270
|
+
pattern = deref(pattern, patternEnv);
|
|
271
|
+
actual = deref(actual, actualEnv);
|
|
272
|
+
if (pattern.type === ATOM && pattern.name === '...') return true;
|
|
273
|
+
if (pattern.type === VAR || actual.type === VAR) {
|
|
274
|
+
if (pattern.type !== VAR || actual.type !== VAR) return false;
|
|
275
|
+
const paired = pairs.get(pattern.name);
|
|
276
|
+
const reversed = reverse.get(actual.name);
|
|
277
|
+
if (paired != null || reversed != null) return paired === actual.name && reversed === pattern.name;
|
|
278
|
+
pairs.set(pattern.name, actual.name);
|
|
279
|
+
reverse.set(actual.name, pattern.name);
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
if (pattern.type !== actual.type || pattern.name !== actual.name || pattern.arity !== actual.arity) return false;
|
|
283
|
+
for (let index = 0; index < pattern.arity; index++) {
|
|
284
|
+
if (!patternVariant(pattern.args[index], patternEnv, actual.args[index], actualEnv, pairs, reverse)) return false;
|
|
285
|
+
}
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function errorTerm(error) {
|
|
290
|
+
if (error?.name === 'ThrownTerm' && error.term) return compound('$quad_thrown', [error.term]);
|
|
291
|
+
if (error?.name === 'PrologError') {
|
|
292
|
+
let formal;
|
|
293
|
+
try {
|
|
294
|
+
formal = parseGoalText(error.formal);
|
|
295
|
+
} catch (_) {
|
|
296
|
+
formal = atom(error.formal ?? 'system_error');
|
|
297
|
+
}
|
|
298
|
+
if (error.culprit != null) formal = formal.type === COMPOUND
|
|
299
|
+
? compound(formal.name, [...formal.args, error.culprit])
|
|
300
|
+
: compound(formal.name, [error.culprit]);
|
|
301
|
+
return compound('error', [formal, variable('$quad_context')]);
|
|
302
|
+
}
|
|
303
|
+
return compound('error', [atom('system_error'), variable('$quad_context')]);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function errorMatches(expected, actual) {
|
|
307
|
+
if (expected.type === COMPOUND && expected.name === 'throw' && expected.arity === 1 &&
|
|
308
|
+
actual.type === COMPOUND && actual.name === '$quad_thrown' && actual.arity === 1) {
|
|
309
|
+
return patternVariant(expected.args[0], new Env(), actual.args[0], new Env());
|
|
310
|
+
}
|
|
311
|
+
if (actual.type !== COMPOUND || actual.name !== 'error' || actual.arity !== 2) return false;
|
|
312
|
+
if (expected.type === COMPOUND && expected.name === 'error' && expected.arity === 2) {
|
|
313
|
+
return patternVariant(expected, new Env(), actual, new Env());
|
|
314
|
+
}
|
|
315
|
+
return patternVariant(expected, new Env(), actual.args[0], new Env());
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function isErrorDescription(term) {
|
|
319
|
+
if (term.type === ATOM) return ['instantiation_error', 'system_error'].includes(term.name);
|
|
320
|
+
return term.type === COMPOUND && [
|
|
321
|
+
'error', 'throw', 'type_error', 'domain_error', 'existence_error',
|
|
322
|
+
'permission_error', 'evaluation_error', 'representation_error',
|
|
323
|
+
'resource_error', 'syntax_error', 'uninstantiation_error',
|
|
324
|
+
].includes(term.name);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function characterText(term) {
|
|
328
|
+
const items = properListItems(term, new Env());
|
|
329
|
+
if (items == null) return null;
|
|
330
|
+
let text = '';
|
|
331
|
+
for (const item of items) {
|
|
332
|
+
if (item.type === ATOM && Array.from(item.name).length === 1) text += item.name;
|
|
333
|
+
else if (item.type === 'number' && /^\d+$/.test(item.name)) text += String.fromCodePoint(Number(item.name));
|
|
334
|
+
else return null;
|
|
335
|
+
}
|
|
336
|
+
return text;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function outputMatches(expected, actual) {
|
|
340
|
+
return expected == null || expected === actual;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function splitOperator(term, name) {
|
|
344
|
+
if (term.type === COMPOUND && term.name === name && term.arity === 2) {
|
|
345
|
+
return [term.args[0], ...splitOperator(term.args[1], name)];
|
|
346
|
+
}
|
|
347
|
+
return [term];
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function formatFailure(program, quad, result) {
|
|
351
|
+
const source = quad.source ?? { filename: '<input>', line: 1 };
|
|
352
|
+
const label = quad.id == null ? '' : `${formatQuadTerm(program, quad.id)}, `;
|
|
353
|
+
const reason = result.kind === 'malformed' ? 'MALFORMED'
|
|
354
|
+
: result.kind === 'bad_identifier' ? 'BAD_ID'
|
|
355
|
+
: result.kind === 'unsupported' ? 'UNSUPPORTED'
|
|
356
|
+
: 'FAILED';
|
|
357
|
+
const expected = result.expected ?? quad.answers[0];
|
|
358
|
+
return `quads: ${reason} ${label}${source.filename}:${source.line}\n` +
|
|
359
|
+
` ?- ${formatQuadTerm(program, quad.query)}.\n` +
|
|
360
|
+
` expected: ${formatQuadTerm(program, expected)}.\n`;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function formatQuadTerm(program, term) {
|
|
364
|
+
return formatTermForWrite(term, new Env(), {
|
|
365
|
+
quoted: true,
|
|
366
|
+
operators: [...program.operators.values()],
|
|
367
|
+
});
|
|
368
|
+
}
|
package/src/write.js
CHANGED
|
@@ -9,6 +9,7 @@ const graphicAtomCharacters = new Set('!#$&*+-/<=>@^~\\'.split(''));
|
|
|
9
9
|
function atomNeedsQuotes(name) {
|
|
10
10
|
if (!name) return true;
|
|
11
11
|
if (name === '[]' || name === '{}') return false;
|
|
12
|
+
if (name === '...') return false;
|
|
12
13
|
if (name === '\\+' || name === '+' || name === '-' || name === '\\') return true;
|
|
13
14
|
if (/^[a-z][A-Za-z0-9_]*$/.test(name)) return false;
|
|
14
15
|
for (const ch of name) if (!graphicAtomCharacters.has(ch)) return true;
|
package/test/run-regression.mjs
CHANGED
|
@@ -183,6 +183,81 @@ why(
|
|
|
183
183
|
assertNotIncludes(result.stdout, 'no_proof', 'stdout');
|
|
184
184
|
},
|
|
185
185
|
},
|
|
186
|
+
{
|
|
187
|
+
name: 'parser records embedded quads without indexing them as clauses',
|
|
188
|
+
run: () => {
|
|
189
|
+
const source = `p(1).\n\nnamed ?- p(X).\n X = 1.\n\nq(2).\n`;
|
|
190
|
+
const program = Program.parseSources([{ text: source, filename: 'embedded-quads.pl' }]);
|
|
191
|
+
assertEqual(program.clauses.length, 2, 'ordinary clause count');
|
|
192
|
+
assertEqual(program.quads.length, 1, 'quad count');
|
|
193
|
+
assertEqual(program.quads[0].id.name, 'named', 'quad label');
|
|
194
|
+
assertEqual(program.quads[0].source.filename, 'embedded-quads.pl', 'quad filename');
|
|
195
|
+
assertEqual(program.quads[0].source.line, 3, 'quad line');
|
|
196
|
+
assertEqual(Boolean(program.findGroup('p', 1)), true, 'preceding clause indexed');
|
|
197
|
+
assertEqual(Boolean(program.findGroup('q', 1)), true, 'following clause indexed');
|
|
198
|
+
assertEqual(Boolean(program.findGroup('?-', 2)), false, 'quad is inert');
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: 'runQuads checks portable answer descriptions',
|
|
203
|
+
run: () => {
|
|
204
|
+
const source = `p(1).\np(2).\np(3).\n\n` +
|
|
205
|
+
`ordered ?- p(X).\n X = 1 ; X = 2 ; X = 3.\n\n` +
|
|
206
|
+
`?- p(4).\n false.\n\n` +
|
|
207
|
+
`?- X = 1.\n X = 2, unexpected.\n\n` +
|
|
208
|
+
`?- p(X).\n X = 1, ... .\n\n` +
|
|
209
|
+
`?- atom_length(1, L).\n type_error(atom, 1).\n\n` +
|
|
210
|
+
`?- atom_length(1, L).\n error(type_error(atom, 1), _).\n\n` +
|
|
211
|
+
`?- throw(ball).\n throw(ball).\n\n` +
|
|
212
|
+
`?- write(ok), nl.\n outputs("ok\\n"), true.\n\n` +
|
|
213
|
+
`?- get_char(C).\n inputs("a"), C = a.\n\n` +
|
|
214
|
+
`?- get_char(C).\n inputs("ab"), C = a, unexpected.\n\n` +
|
|
215
|
+
`?- X = 1.\n X = 2, unexpected.\n X = 1.\n\n` +
|
|
216
|
+
`?- catch(throw(ball), E, true).\n E = ball | error(system_error, ...).\n`;
|
|
217
|
+
const result = publicApi.runQuads(Program.parseSources([{ text: source, filename: 'quads.pl' }]));
|
|
218
|
+
assertEqual(result.total, 12, 'quad total');
|
|
219
|
+
assertEqual(result.passed, 12, 'quad passed');
|
|
220
|
+
assertEqual(result.failed, 0, 'quad failed');
|
|
221
|
+
assertEqual(result.stdout, 'quads: 12 run, 12 passed, 0 failed.\n', 'quad report');
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
name: 'runQuads rejects malformed answer substitutions',
|
|
226
|
+
run: () => {
|
|
227
|
+
const source = `?- X = f(Y), Y = 1.\n X = f(Y), Y = 1.\n`;
|
|
228
|
+
const result = publicApi.runQuads(Program.parseSources([{ text: source, filename: 'malformed-quad.pl' }]));
|
|
229
|
+
assertEqual(result.total, 1, 'quad total');
|
|
230
|
+
assertEqual(result.failed, 1, 'quad failed');
|
|
231
|
+
assertIncludes(result.stdout, 'quads: MALFORMED malformed-quad.pl:1', 'malformed report');
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: 'runQuads reports annotations that cannot be checked safely',
|
|
236
|
+
run: () => {
|
|
237
|
+
const result = publicApi.runQuads(`?- repeat, fail.\n loops.\n`);
|
|
238
|
+
assertEqual(result.results[0].kind, 'unsupported', 'quad result');
|
|
239
|
+
assertIncludes(result.stdout, 'quads: UNSUPPORTED <input>:1', 'unsupported report');
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: '--quads runs embedded tests and reports failures through exit status',
|
|
244
|
+
run: () => {
|
|
245
|
+
const passing = runCli(['--quads', '-'], {
|
|
246
|
+
input: `p(ok).\n\nsmoke ?- p(X).\n X = ok.\n`,
|
|
247
|
+
});
|
|
248
|
+
assertEqual(passing.status, 0, 'passing quad exit status');
|
|
249
|
+
assertEqual(passing.stdout, 'quads: 1 run, 1 passed, 0 failed.\n', 'passing quad stdout');
|
|
250
|
+
assertEqual(passing.stderr, '', 'passing quad stderr');
|
|
251
|
+
|
|
252
|
+
const failing = runCli(['-q', '-'], {
|
|
253
|
+
input: `p(actual).\n\nsmoke ?- p(X).\n X = expected.\n`,
|
|
254
|
+
});
|
|
255
|
+
assertEqual(failing.status, 1, 'failing quad exit status');
|
|
256
|
+
assertIncludes(failing.stdout, 'quads: FAILED smoke, <stdin>:3', 'failing quad report');
|
|
257
|
+
assertIncludes(failing.stdout, 'quads: 1 run, 0 passed, 1 failed.', 'failing quad summary');
|
|
258
|
+
assertEqual(failing.stderr, '', 'failing quad stderr');
|
|
259
|
+
},
|
|
260
|
+
},
|
|
186
261
|
{
|
|
187
262
|
name: 'seeded random/3 sequence is reproducible',
|
|
188
263
|
run: () => {
|
|
@@ -212,6 +287,7 @@ why(
|
|
|
212
287
|
assertIncludes(result.stdout, 'With no arguments, start a Prolog REPL.', 'stdout');
|
|
213
288
|
assertIncludes(result.stdout, '-g, --goal goal', 'stdout');
|
|
214
289
|
assertIncludes(result.stdout, '-p, --proof', 'stdout');
|
|
290
|
+
assertIncludes(result.stdout, '-q, --quads', 'stdout');
|
|
215
291
|
assertIncludes(result.stdout, '-s, --stats', 'stdout');
|
|
216
292
|
assertIncludes(result.stdout, '-v, --version', 'stdout');
|
|
217
293
|
assertIncludes(result.stdout, '-w, --warnings', 'stdout');
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -6072,13 +6072,14 @@ make the observed question explicit.
|
|
|
6072
6072
|
| --- | --- |
|
|
6073
6073
|
| `-h`, `--help` | Show usage |
|
|
6074
6074
|
| `-p`, `--proof` | Print `why/2` explanations |
|
|
6075
|
+
| `-q`, `--quads` | Run embedded quad tests and fail if any do not hold |
|
|
6075
6076
|
| `-s`, `--stats` | Print solver counters to stderr |
|
|
6076
6077
|
| `-v`, `--version` | Print the package version |
|
|
6077
6078
|
| `-w`, `--warnings` | Print non-fatal portability warnings |
|
|
6078
6079
|
| `-g`, `--goal Goal` | Solve a callable goal; may be repeated; overrides `%% goal:` comments |
|
|
6079
6080
|
| `--` | Treat following arguments as inputs |
|
|
6080
6081
|
|
|
6081
|
-
Short flags may be combined, so `-
|
|
6082
|
+
Short flags may be combined, so `-pqw` is equivalent to `-p -q -w`.
|
|
6082
6083
|
|
|
6083
6084
|
Inputs may be local files, HTTP(S) URLs, or one `-` for stdin. The bare command
|
|
6084
6085
|
`eyeprolog` starts the REPL. When options are present but no input is named,
|
|
@@ -6113,6 +6114,56 @@ they do not corrupt that logical stream. A successful run normally exits with
|
|
|
6113
6114
|
status zero; loading, syntax, option, and other uncaught errors use status `1`. `halt/0-1` can deliberately choose the
|
|
6114
6115
|
process status from inside a program.
|
|
6115
6116
|
|
|
6117
|
+
### Embedded quad tests
|
|
6118
|
+
|
|
6119
|
+
A quad places a query directly before a description of its expected top-level
|
|
6120
|
+
answer. The answer is ordinary Prolog syntax rather than quoted text or a
|
|
6121
|
+
comment, so a small test reads like the interaction it checks:
|
|
6122
|
+
|
|
6123
|
+
```eyeprolog
|
|
6124
|
+
color(red).
|
|
6125
|
+
color(green).
|
|
6126
|
+
|
|
6127
|
+
colors ?- color(X).
|
|
6128
|
+
X = red
|
|
6129
|
+
; X = green.
|
|
6130
|
+
|
|
6131
|
+
?- color(blue).
|
|
6132
|
+
false.
|
|
6133
|
+
```
|
|
6134
|
+
|
|
6135
|
+
Run all quads in a file with `eyeprolog --quads file.pl` or `eyeprolog -q
|
|
6136
|
+
file.pl`. A label such as `colors` is optional. Loading the file normally only
|
|
6137
|
+
records its quads; it does not execute them or add their queries and answers as
|
|
6138
|
+
program clauses. A quad run prints a summary and exits with status `1` when any
|
|
6139
|
+
description fails.
|
|
6140
|
+
|
|
6141
|
+
Answer descriptions support ordered answers separated by `;`, acceptable
|
|
6142
|
+
alternatives separated by `|`, `true`, `false`, standard error descriptions,
|
|
6143
|
+
and the `unexpected` annotation for an answer that must not occur (`inattendue`
|
|
6144
|
+
is its synonym). `...` and `ad_infinitum` accept further answers. Multiple
|
|
6145
|
+
indented descriptions after one query must all hold. `inputs/1` supplies and
|
|
6146
|
+
checks consumed characters; `outputs/1` checks emitted characters. `sto` marks
|
|
6147
|
+
an answer description that this finite-tree implementation skips. The
|
|
6148
|
+
nontermination and advanced stream annotations `loops`, `peeks/1`, and `waits`,
|
|
6149
|
+
and the unordered `other_answer_sequence` annotation, are not executed by the
|
|
6150
|
+
current runner.
|
|
6151
|
+
|
|
6152
|
+
The JavaScript API exposes the same operation without process I/O:
|
|
6153
|
+
|
|
6154
|
+
```js
|
|
6155
|
+
import { Program, runQuads } from 'eyeprolog';
|
|
6156
|
+
|
|
6157
|
+
const program = Program.parse(source);
|
|
6158
|
+
const report = runQuads(program);
|
|
6159
|
+
console.log(report.passed, report.failed, report.stdout);
|
|
6160
|
+
```
|
|
6161
|
+
|
|
6162
|
+
The syntax follows the “queries using answer descriptions” convention used by
|
|
6163
|
+
Trealla and the ISO Prolog working examples. Because answer descriptions are
|
|
6164
|
+
layout-sensitive, indent every description while keeping ordinary clause heads
|
|
6165
|
+
and the next quad query at the left margin.
|
|
6166
|
+
|
|
6116
6167
|
Statistics are comparative evidence, not a score in isolation. Preserve the
|
|
6117
6168
|
program, input, runtime version, selected query, answers, and counters together.
|
|
6118
6169
|
An optimization is acceptable only when the intended answers remain unchanged
|