eyeprolog 1.1.2 → 1.1.3
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 -0
- package/package.json +1 -1
- package/src/cli.js +15 -3
- package/src/repl.js +412 -0
- package/test/run-regression.mjs +66 -3
- package/the-art-of-eyeprolog.md +27 -2
package/README.md
CHANGED
|
@@ -25,6 +25,18 @@ EyeProlog requires Node.js 18 or newer.
|
|
|
25
25
|
|
|
26
26
|
```sh
|
|
27
27
|
npm install --global eyeprolog
|
|
28
|
+
eyeprolog
|
|
29
|
+
?- use_module(library(lists)).
|
|
30
|
+
true.
|
|
31
|
+
?- member(X, [prolog, logic]).
|
|
32
|
+
X = prolog
|
|
33
|
+
; X = logic.
|
|
34
|
+
?- halt.
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
For a non-interactive run:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
28
40
|
printf 'human(socrates).\nmortal(X) :- human(X).\n' |
|
|
29
41
|
eyeprolog --proof --goal 'mortal(socrates)' -
|
|
30
42
|
```
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -10,7 +10,14 @@ let explanationModule = null;
|
|
|
10
10
|
|
|
11
11
|
export async function main(argv) {
|
|
12
12
|
if (argv.length === 0) {
|
|
13
|
-
await
|
|
13
|
+
const engine = await loadEngine();
|
|
14
|
+
const { runRepl } = await import('./repl.js');
|
|
15
|
+
const exitCode = await runRepl(engine, {
|
|
16
|
+
input: process.stdin,
|
|
17
|
+
output: process.stdout,
|
|
18
|
+
errorOutput: process.stderr,
|
|
19
|
+
});
|
|
20
|
+
if (exitCode !== 0) process.exitCode = exitCode;
|
|
14
21
|
return;
|
|
15
22
|
}
|
|
16
23
|
|
|
@@ -109,15 +116,16 @@ export async function main(argv) {
|
|
|
109
116
|
|
|
110
117
|
async function loadEngine() {
|
|
111
118
|
if (engineModule == null) {
|
|
112
|
-
const [term, parser, program, solver, iso, library] = await Promise.all([
|
|
119
|
+
const [term, parser, program, solver, iso, library, write] = await Promise.all([
|
|
113
120
|
import('./term.js'),
|
|
114
121
|
import('./parser.js'),
|
|
115
122
|
import('./program.js'),
|
|
116
123
|
import('./solver.js'),
|
|
117
124
|
import('./iso.js'),
|
|
118
125
|
import('./standard-library.js'),
|
|
126
|
+
import('./write.js'),
|
|
119
127
|
]);
|
|
120
|
-
engineModule = { ...term, ...parser, ...program, ...solver, ...iso, ...library };
|
|
128
|
+
engineModule = { ...term, ...parser, ...program, ...solver, ...iso, ...library, ...write };
|
|
121
129
|
}
|
|
122
130
|
return engineModule;
|
|
123
131
|
}
|
|
@@ -186,8 +194,12 @@ async function usage(stream) {
|
|
|
186
194
|
stream.write(`eyeprolog ${await packageVersion()}
|
|
187
195
|
|
|
188
196
|
Usage:
|
|
197
|
+
eyeprolog
|
|
189
198
|
eyeprolog [options] [file-or-url.pl|- ...]
|
|
190
199
|
|
|
200
|
+
Interactive:
|
|
201
|
+
With no arguments, start a Prolog REPL. Use eyeprolog -h for help.
|
|
202
|
+
|
|
191
203
|
Input:
|
|
192
204
|
file-or-url.pl Read an EyeProlog program from a local file or http(s) URL.
|
|
193
205
|
- Read an EyeProlog program from standard input.
|
package/src/repl.js
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
// Interactive top level for the eyeprolog command.
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
5
|
+
|
|
6
|
+
const ANSWER_HELP = `
|
|
7
|
+
SPACE, "n" or ";": next solution, if any
|
|
8
|
+
RETURN or ".": stop enumeration
|
|
9
|
+
"a": enumerate all solutions
|
|
10
|
+
"f": enumerate the next 5 solutions
|
|
11
|
+
"h": display this help message
|
|
12
|
+
"w": write terms without depth limit
|
|
13
|
+
"p": print terms with depth limit
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
export async function runRepl(engine, options = {}) {
|
|
17
|
+
const input = options.input ?? process.stdin;
|
|
18
|
+
const output = options.output ?? process.stdout;
|
|
19
|
+
const errorOutput = options.errorOutput ?? process.stderr;
|
|
20
|
+
const reader = new LineReader(input, output);
|
|
21
|
+
const sources = [];
|
|
22
|
+
let state = makeState(engine, sources, output);
|
|
23
|
+
let exitCode = 0;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
state.solver.runInitializations();
|
|
27
|
+
while (true) {
|
|
28
|
+
const text = await readQuery(reader);
|
|
29
|
+
if (text == null) break;
|
|
30
|
+
if (!text.trim()) continue;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const goal = parseGoal(engine, state, text);
|
|
34
|
+
if (isUseModuleGoal(goal)) {
|
|
35
|
+
sources.push({ text: `:- ${text}.\n`, filename: '<repl>' });
|
|
36
|
+
state = makeState(engine, sources, output);
|
|
37
|
+
state.solver.runInitializations();
|
|
38
|
+
output.write(' true.\n');
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const consultFiles = consultDesignations(engine, goal);
|
|
42
|
+
if (consultFiles != null) {
|
|
43
|
+
for (const filename of consultFiles) sources.push(await readSource(filename));
|
|
44
|
+
state = makeState(engine, sources, output);
|
|
45
|
+
state.solver.runInitializations();
|
|
46
|
+
output.write(' true.\n');
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const result = await solveQuery(engine, state, goal, reader, output);
|
|
51
|
+
if (result?.halted) {
|
|
52
|
+
exitCode = result.code;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (error?.name === 'HaltSignal') {
|
|
57
|
+
exitCode = error.code;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
output.write(` ${formatError(error)}\n`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
errorOutput.write(`eyeprolog: ${error?.message ?? String(error)}\n`);
|
|
65
|
+
exitCode = 1;
|
|
66
|
+
} finally {
|
|
67
|
+
reader.close();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return exitCode;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
class LineReader {
|
|
74
|
+
constructor(input, output) {
|
|
75
|
+
this.input = input;
|
|
76
|
+
this.output = output;
|
|
77
|
+
this.terminal = Boolean(input.isTTY && output.isTTY && typeof input.setRawMode === 'function');
|
|
78
|
+
this.history = [];
|
|
79
|
+
this.currentPrompt = '?- ';
|
|
80
|
+
this.open();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
open() {
|
|
84
|
+
this.readline = createInterface({
|
|
85
|
+
input: this.input,
|
|
86
|
+
output: this.output,
|
|
87
|
+
terminal: Boolean(this.input.isTTY && this.output.isTTY),
|
|
88
|
+
prompt: this.currentPrompt,
|
|
89
|
+
});
|
|
90
|
+
if (this.terminal && this.history.length > 0) {
|
|
91
|
+
this.readline.history.push(...this.history);
|
|
92
|
+
}
|
|
93
|
+
this.lines = this.readline[Symbol.asyncIterator]();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async read(prompt) {
|
|
97
|
+
this.currentPrompt = prompt;
|
|
98
|
+
this.readline.setPrompt(prompt);
|
|
99
|
+
this.output.write(prompt);
|
|
100
|
+
const result = await this.lines.next();
|
|
101
|
+
return result.done ? null : result.value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async readControl(prompt) {
|
|
105
|
+
if (!this.terminal) return this.read(prompt);
|
|
106
|
+
this.output.write(prompt);
|
|
107
|
+
this.history = [...this.readline.history];
|
|
108
|
+
this.currentPrompt = '?- ';
|
|
109
|
+
this.readline.close();
|
|
110
|
+
this.readline = null;
|
|
111
|
+
this.lines = null;
|
|
112
|
+
this.input.setRawMode(true);
|
|
113
|
+
this.input.resume();
|
|
114
|
+
|
|
115
|
+
const control = await new Promise((resolve, reject) => {
|
|
116
|
+
const cleanup = () => {
|
|
117
|
+
this.input.off('data', onData);
|
|
118
|
+
this.input.off('error', onError);
|
|
119
|
+
};
|
|
120
|
+
const onData = (data) => {
|
|
121
|
+
cleanup();
|
|
122
|
+
const text = String(data);
|
|
123
|
+
resolve(text === '\x04' ? null : text[0] ?? null);
|
|
124
|
+
};
|
|
125
|
+
const onError = (error) => {
|
|
126
|
+
cleanup();
|
|
127
|
+
reject(error);
|
|
128
|
+
};
|
|
129
|
+
this.input.once('data', onData);
|
|
130
|
+
this.input.once('error', onError);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
this.input.setRawMode(false);
|
|
134
|
+
this.open();
|
|
135
|
+
return control;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
close() {
|
|
139
|
+
if (this.input.isRaw) this.input.setRawMode(false);
|
|
140
|
+
this.readline?.close();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function makeState(engine, sources, output) {
|
|
145
|
+
const program = engine.Program.parseSources(sources);
|
|
146
|
+
const solver = new engine.Solver(program, {
|
|
147
|
+
registry: engine.getEyePrologRegistry(),
|
|
148
|
+
ioOptions: { write: (text) => output.write(String(text)) },
|
|
149
|
+
});
|
|
150
|
+
return { program: solver.program, solver };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function readQuery(reader) {
|
|
154
|
+
let source = '';
|
|
155
|
+
let prompt = '?- ';
|
|
156
|
+
while (true) {
|
|
157
|
+
const line = await reader.read(prompt);
|
|
158
|
+
if (line == null) return source.trim() ? source : null;
|
|
159
|
+
source += `${line}\n`;
|
|
160
|
+
const end = terminalFullStop(source);
|
|
161
|
+
if (end >= 0) return source.slice(0, end);
|
|
162
|
+
prompt = '| ';
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function terminalFullStop(source) {
|
|
167
|
+
let quote = null;
|
|
168
|
+
let lineComment = false;
|
|
169
|
+
let blockComment = false;
|
|
170
|
+
let escaped = false;
|
|
171
|
+
let depth = 0;
|
|
172
|
+
|
|
173
|
+
for (let i = 0; i < source.length; i++) {
|
|
174
|
+
const ch = source[i];
|
|
175
|
+
const next = source[i + 1];
|
|
176
|
+
if (lineComment) {
|
|
177
|
+
if (ch === '\n') lineComment = false;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (blockComment) {
|
|
181
|
+
if (ch === '*' && next === '/') {
|
|
182
|
+
blockComment = false;
|
|
183
|
+
i++;
|
|
184
|
+
}
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (quote != null) {
|
|
188
|
+
if (escaped) {
|
|
189
|
+
escaped = false;
|
|
190
|
+
} else if (ch === '\\') {
|
|
191
|
+
escaped = true;
|
|
192
|
+
} else if (ch === quote) {
|
|
193
|
+
if (next === quote) i++;
|
|
194
|
+
else quote = null;
|
|
195
|
+
}
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (ch === '%') {
|
|
199
|
+
lineComment = true;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (ch === '/' && next === '*') {
|
|
203
|
+
blockComment = true;
|
|
204
|
+
i++;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
208
|
+
quote = ch;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if ('([{'.includes(ch)) depth++;
|
|
212
|
+
else if (')]}'.includes(ch)) depth = Math.max(0, depth - 1);
|
|
213
|
+
else if (ch === '.' && depth === 0 && onlyLayoutAndComments(source.slice(i + 1))) return i;
|
|
214
|
+
}
|
|
215
|
+
return -1;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function onlyLayoutAndComments(source) {
|
|
219
|
+
return source.replace(/\s|%[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\//g, '').length === 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function parseGoal(engine, state, text) {
|
|
223
|
+
const goal = engine.parseGoalText(text, {
|
|
224
|
+
doubleQuotes: state.solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
|
|
225
|
+
operatorDefinitions: [...state.program.operators.values()],
|
|
226
|
+
});
|
|
227
|
+
if (goal.type === 'var') throw new engine.PrologError('instantiation_error');
|
|
228
|
+
if (goal.type !== 'atom' && goal.type !== 'compound') {
|
|
229
|
+
throw new engine.PrologError('type_error(callable)', goal);
|
|
230
|
+
}
|
|
231
|
+
return goal;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function isUseModuleGoal(goal) {
|
|
235
|
+
return goal.type === 'compound' && goal.name === 'use_module' && [1, 2].includes(goal.arity);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function consultDesignations(engine, goal) {
|
|
239
|
+
if (goal.type === 'atom' && goal.name === '[]') return [];
|
|
240
|
+
if (goal.type !== 'compound' || goal.name !== '.' || goal.arity !== 2) return null;
|
|
241
|
+
const items = engine.properListItems(goal, new engine.Env());
|
|
242
|
+
if (items == null) return null;
|
|
243
|
+
return items.map((item) => {
|
|
244
|
+
if (item.type === 'var') throw new engine.PrologError('instantiation_error');
|
|
245
|
+
if (item.type !== 'atom') throw new engine.PrologError('type_error(atom)', item);
|
|
246
|
+
return item.name;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function readSource(designation) {
|
|
251
|
+
let filename = path.resolve(designation);
|
|
252
|
+
try {
|
|
253
|
+
await fs.access(filename);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (path.extname(filename)) throw error;
|
|
256
|
+
filename += '.pl';
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
text: await fs.readFile(filename, 'utf8'),
|
|
260
|
+
filename: path.basename(filename),
|
|
261
|
+
baseDir: path.dirname(filename),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function solveQuery(engine, state, goal, reader, output) {
|
|
266
|
+
const variables = queryVariables(goal);
|
|
267
|
+
const solver = state.solver;
|
|
268
|
+
solver.solutionsSeen = 0;
|
|
269
|
+
const solutions = solver.solve([goal], new engine.Env(), 0);
|
|
270
|
+
let current = pullSolution(solver, solutions);
|
|
271
|
+
if (current.error) {
|
|
272
|
+
if (current.error?.name === 'HaltSignal') return { halted: true, code: current.error.code };
|
|
273
|
+
throw current.error;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (current.result.done) {
|
|
277
|
+
output.write(' false.\n');
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
let automatic = 0;
|
|
282
|
+
let firstAnswer = true;
|
|
283
|
+
while (!current.result.done) {
|
|
284
|
+
const next = pullSolution(solver, solutions);
|
|
285
|
+
output.write(current.output);
|
|
286
|
+
output.write(`${firstAnswer ? ' ' : ''}${formatAnswer(engine, state, variables, current.result.value)}`);
|
|
287
|
+
firstAnswer = false;
|
|
288
|
+
if (!next.error && next.result.done) {
|
|
289
|
+
output.write('.\n');
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (automatic > 0 || automatic === Infinity) {
|
|
294
|
+
if (automatic !== Infinity) automatic--;
|
|
295
|
+
output.write('\n; ');
|
|
296
|
+
} else {
|
|
297
|
+
while (true) {
|
|
298
|
+
const controlLine = await reader.readControl('\n; ');
|
|
299
|
+
if (controlLine == null || controlLine === '' || controlLine === '\r' || controlLine === '\n' ||
|
|
300
|
+
controlLine.trimStart().startsWith('.')) {
|
|
301
|
+
if (typeof solutions.return === 'function') solutions.return();
|
|
302
|
+
output.write('... .\n');
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
const control = controlLine === ' ' ? ' ' : controlLine.trimStart()[0];
|
|
306
|
+
if (control === ';' || control === 'n' || control === ' ') break;
|
|
307
|
+
if (control === 'a') {
|
|
308
|
+
automatic = Infinity;
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
if (control === 'f') {
|
|
312
|
+
automatic = 4;
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
if (control === 'w' || control === 'p') {
|
|
316
|
+
output.write(`${formatAnswer(engine, state, variables, current.result.value)}`);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (control === 'h') {
|
|
320
|
+
output.write(ANSWER_HELP);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
output.write('Action? ');
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (next.error) {
|
|
328
|
+
output.write(next.output);
|
|
329
|
+
if (next.error?.name === 'HaltSignal') return { halted: true, code: next.error.code };
|
|
330
|
+
throw next.error;
|
|
331
|
+
}
|
|
332
|
+
current = next;
|
|
333
|
+
}
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function pullSolution(solver, solutions) {
|
|
338
|
+
const stream = solver.io.resolve('user_output');
|
|
339
|
+
const originalWrite = stream?.write;
|
|
340
|
+
let captured = '';
|
|
341
|
+
if (stream) stream.write = (text) => { captured += String(text); };
|
|
342
|
+
try {
|
|
343
|
+
return { result: solutions.next(), output: captured };
|
|
344
|
+
} catch (error) {
|
|
345
|
+
return { error, output: captured };
|
|
346
|
+
} finally {
|
|
347
|
+
if (stream) stream.write = originalWrite;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function queryVariables(goal) {
|
|
352
|
+
const variables = [];
|
|
353
|
+
const seen = new Set();
|
|
354
|
+
const stack = [goal];
|
|
355
|
+
while (stack.length) {
|
|
356
|
+
const term = stack.pop();
|
|
357
|
+
if (term.type === 'var') {
|
|
358
|
+
if (!term.name.startsWith('__anon') && !seen.has(term.name)) {
|
|
359
|
+
seen.add(term.name);
|
|
360
|
+
variables.push(term);
|
|
361
|
+
}
|
|
362
|
+
} else {
|
|
363
|
+
for (let i = term.args.length - 1; i >= 0; i--) stack.push(term.args[i]);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return variables;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function formatAnswer(engine, state, variables, env) {
|
|
370
|
+
const bindings = [];
|
|
371
|
+
const names = new Map(variables.map((variable) => [variable.name, variable.name]));
|
|
372
|
+
let generated = 0;
|
|
373
|
+
|
|
374
|
+
for (const variable of variables) collectUnboundVariables(engine, variable, env, names, () => `_${letterName(generated++)}`);
|
|
375
|
+
for (const variable of variables) {
|
|
376
|
+
const value = engine.deref(variable, env);
|
|
377
|
+
if (value.type === 'var' && value.name === variable.name) continue;
|
|
378
|
+
bindings.push(`${variable.name} = ${engine.formatTermForWrite(value, env, {
|
|
379
|
+
quoted: true,
|
|
380
|
+
operators: [...state.program.operators.values()],
|
|
381
|
+
variableNames: names,
|
|
382
|
+
})}`);
|
|
383
|
+
}
|
|
384
|
+
return bindings.length === 0 ? 'true' : bindings.join(', ');
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function collectUnboundVariables(engine, term, env, names, nextName) {
|
|
388
|
+
const stack = [term];
|
|
389
|
+
const seen = new Set();
|
|
390
|
+
while (stack.length) {
|
|
391
|
+
const current = engine.deref(stack.pop(), env);
|
|
392
|
+
if (current.type === 'var') {
|
|
393
|
+
if (!seen.has(current.name)) {
|
|
394
|
+
seen.add(current.name);
|
|
395
|
+
if (!names.has(current.name)) names.set(current.name, nextName());
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
for (let i = current.args.length - 1; i >= 0; i--) stack.push(current.args[i]);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function letterName(index) {
|
|
404
|
+
const letter = String.fromCharCode(65 + (index % 26));
|
|
405
|
+
const suffix = Math.floor(index / 26);
|
|
406
|
+
return suffix === 0 ? letter : `${letter}${suffix}`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function formatError(error) {
|
|
410
|
+
const message = error?.message ?? String(error);
|
|
411
|
+
return message.endsWith('.') ? message : `${message}.`;
|
|
412
|
+
}
|
package/test/run-regression.mjs
CHANGED
|
@@ -204,11 +204,12 @@ why(
|
|
|
204
204
|
},
|
|
205
205
|
},
|
|
206
206
|
{
|
|
207
|
-
name: '
|
|
207
|
+
name: '-h shows CLI help',
|
|
208
208
|
run: () => {
|
|
209
|
-
const result = runCli([]);
|
|
209
|
+
const result = runCli(['-h']);
|
|
210
210
|
assertEqual(result.status, 0, 'exit status');
|
|
211
|
-
assertIncludes(result.stdout, 'Usage:\n eyeprolog [options] [file-or-url.pl|- ...]', 'stdout');
|
|
211
|
+
assertIncludes(result.stdout, 'Usage:\n eyeprolog\n eyeprolog [options] [file-or-url.pl|- ...]', 'stdout');
|
|
212
|
+
assertIncludes(result.stdout, 'With no arguments, start a Prolog REPL.', 'stdout');
|
|
212
213
|
assertIncludes(result.stdout, '-p, --proof', 'stdout');
|
|
213
214
|
assertIncludes(result.stdout, '-s, --stats', 'stdout');
|
|
214
215
|
assertIncludes(result.stdout, '-v, --version', 'stdout');
|
|
@@ -218,6 +219,68 @@ why(
|
|
|
218
219
|
assertEqual(result.stderr, '', 'stderr');
|
|
219
220
|
},
|
|
220
221
|
},
|
|
222
|
+
{
|
|
223
|
+
name: 'bare CLI starts a REPL with truth, failure, and bindings',
|
|
224
|
+
run: () => {
|
|
225
|
+
const result = runCli([], { input: 'true.\nfalse.\nX = hello.\nhalt.\n' });
|
|
226
|
+
assertEqual(result.status, 0, 'exit status');
|
|
227
|
+
assertEqual(result.stdout, '?- true.\n?- false.\n?- X = hello.\n?- ', 'stdout');
|
|
228
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
name: 'REPL enumerates and stops answers like the Scryer top level',
|
|
233
|
+
run: () => {
|
|
234
|
+
const result = runCli([], {
|
|
235
|
+
input: '(X = a; X = b).\n;\n(X = one; X = two).\n\nhalt.\n',
|
|
236
|
+
});
|
|
237
|
+
assertEqual(result.status, 0, 'exit status');
|
|
238
|
+
assertEqual(result.stdout, '?- X = a\n; X = b.\n?- X = one\n; ... .\n?- ', 'stdout');
|
|
239
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: 'REPL accepts multiline period-terminated queries',
|
|
244
|
+
run: () => {
|
|
245
|
+
const result = runCli([], { input: '(X =\n one).\nhalt.\n' });
|
|
246
|
+
assertEqual(result.status, 0, 'exit status');
|
|
247
|
+
assertEqual(result.stdout, '?- | X = one.\n?- ', 'stdout');
|
|
248
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
name: 'REPL consult shorthand loads local Prolog files',
|
|
253
|
+
run: () => {
|
|
254
|
+
const filename = path.join(tmp, `repl-consult-${++tmpCounter}.pl`);
|
|
255
|
+
fs.writeFileSync(filename, 'color(red).\ncolor(blue).\n');
|
|
256
|
+
const result = runCli([], {
|
|
257
|
+
input: `[${sourceAtom(filename)}].\ncolor(X).\n;\nhalt.\n`,
|
|
258
|
+
});
|
|
259
|
+
assertEqual(result.status, 0, 'exit status');
|
|
260
|
+
assertEqual(result.stdout, '?- true.\n?- X = red\n; X = blue.\n?- ', 'stdout');
|
|
261
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: 'REPL use_module imports Part 2 library predicates',
|
|
266
|
+
run: () => {
|
|
267
|
+
const result = runCli([], {
|
|
268
|
+
input: 'append(X, Y, [1, 2, 3, 4]).\nuse_module(library(lists)).\nappend(X, Y, [1, 2, 3, 4]).\n\nhalt.\n',
|
|
269
|
+
});
|
|
270
|
+
assertEqual(result.status, 0, 'exit status');
|
|
271
|
+
assertEqual(result.stdout, '?- false.\n?- true.\n?- X = [], Y = [1, 2, 3, 4]\n; ... .\n?- ', 'stdout');
|
|
272
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
name: 'REPL halt status is returned by the CLI',
|
|
277
|
+
run: () => {
|
|
278
|
+
const result = runCli([], { input: 'halt(7).\n' });
|
|
279
|
+
assertEqual(result.status, 7, 'exit status');
|
|
280
|
+
assertEqual(result.stdout, '?- ', 'stdout');
|
|
281
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
282
|
+
},
|
|
283
|
+
},
|
|
221
284
|
{
|
|
222
285
|
name: 'CLI loads an explicitly imported standard library module',
|
|
223
286
|
run: () => {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -6015,9 +6015,34 @@ search behavior.
|
|
|
6015
6015
|
</figure>
|
|
6016
6016
|
|
|
6017
6017
|
```text
|
|
6018
|
+
eyeprolog
|
|
6018
6019
|
eyeprolog [options] [file-or-url.pl|- ...]
|
|
6019
6020
|
```
|
|
6020
6021
|
|
|
6022
|
+
### Interactive queries
|
|
6023
|
+
|
|
6024
|
+
Run `eyeprolog` without arguments to enter the interactive top level. Queries
|
|
6025
|
+
may span lines and end with a full stop, as in Scryer Prolog:
|
|
6026
|
+
|
|
6027
|
+
```text
|
|
6028
|
+
?- use_module(library(lists)).
|
|
6029
|
+
true.
|
|
6030
|
+
?- member(X, [prolog, logic]).
|
|
6031
|
+
X = prolog
|
|
6032
|
+
; X = logic.
|
|
6033
|
+
?- halt.
|
|
6034
|
+
```
|
|
6035
|
+
|
|
6036
|
+
When another answer exists in an interactive terminal, press `;`, Space, or
|
|
6037
|
+
`n` to ask for it immediately; no Return is needed. Return or `.` stops
|
|
6038
|
+
enumeration, `a` enumerates all remaining answers, `f` enumerates the next
|
|
6039
|
+
five, and `h` displays the answer-control help. A
|
|
6040
|
+
period-terminated query with no solutions prints `false.`; a solution without
|
|
6041
|
+
visible variable bindings prints `true.`. Use `[file].` or `['file.pl'].` to
|
|
6042
|
+
consult local source, and `halt.` or `halt(Status).` to leave the top level.
|
|
6043
|
+
Up and Down recall queries from the current session. Explicit `eyeprolog -h`
|
|
6044
|
+
displays command-line help.
|
|
6045
|
+
|
|
6021
6046
|
### Selecting goals
|
|
6022
6047
|
|
|
6023
6048
|
A Prolog source file states facts, rules, and ISO directives; the command line
|
|
@@ -6056,8 +6081,8 @@ make the observed question explicit.
|
|
|
6056
6081
|
Short flags may be combined, so `-pw` is equivalent to `-p -w`.
|
|
6057
6082
|
|
|
6058
6083
|
Inputs may be local files, HTTP(S) URLs, or one `-` for stdin. The bare command
|
|
6059
|
-
`eyeprolog`
|
|
6060
|
-
used; writing `-` explicitly is clearer in scripts. Multiple sources are
|
|
6084
|
+
`eyeprolog` starts the REPL. When options are present but no input is named,
|
|
6085
|
+
stdin is used; writing `-` explicitly is clearer in scripts. Multiple sources are
|
|
6061
6086
|
parsed as one program, so facts, rules, and directives can be separated across
|
|
6062
6087
|
files. A relative `include/1` inside a local file resolves from that file's
|
|
6063
6088
|
directory.
|