eyeprolog 1.1.2 → 1.1.4
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 +18 -6
- package/src/repl.js +412 -0
- package/test/run-regression.mjs +92 -3
- package/the-art-of-eyeprolog.md +32 -7
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
|
|
|
@@ -41,9 +48,9 @@ export async function main(argv) {
|
|
|
41
48
|
options.version = true;
|
|
42
49
|
} else if (!endOptions && (arg === '--warnings' || arg === '-w')) {
|
|
43
50
|
options.warnings = true;
|
|
44
|
-
} else if (!endOptions && arg === '--goal') {
|
|
51
|
+
} else if (!endOptions && (arg === '--goal' || arg === '-g')) {
|
|
45
52
|
const goal = argv[++i];
|
|
46
|
-
if (goal == null) throw new Error(
|
|
53
|
+
if (goal == null) throw new Error(`option ${arg} requires a goal`);
|
|
47
54
|
options.goals.push(goal);
|
|
48
55
|
} else if (!endOptions && arg.startsWith('-') && !arg.startsWith('--') && arg.length > 2) {
|
|
49
56
|
const flags = arg.slice(1);
|
|
@@ -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.
|
|
@@ -198,7 +210,7 @@ Options:
|
|
|
198
210
|
-s, --stats Print solver statistics to stderr after execution.
|
|
199
211
|
-v, --version Show the package version and exit.
|
|
200
212
|
-w, --warnings Print non-fatal portability warnings to stderr.
|
|
201
|
-
--goal goal
|
|
213
|
+
-g, --goal goal Solve goal and print its ground answers; may be repeated.
|
|
202
214
|
If omitted, use %% goal: comments from the inputs.
|
|
203
215
|
-- Stop option parsing; following arguments are treated as files.
|
|
204
216
|
`);
|
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,13 @@ 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');
|
|
213
|
+
assertIncludes(result.stdout, '-g, --goal goal', 'stdout');
|
|
212
214
|
assertIncludes(result.stdout, '-p, --proof', 'stdout');
|
|
213
215
|
assertIncludes(result.stdout, '-s, --stats', 'stdout');
|
|
214
216
|
assertIncludes(result.stdout, '-v, --version', 'stdout');
|
|
@@ -218,6 +220,68 @@ why(
|
|
|
218
220
|
assertEqual(result.stderr, '', 'stderr');
|
|
219
221
|
},
|
|
220
222
|
},
|
|
223
|
+
{
|
|
224
|
+
name: 'bare CLI starts a REPL with truth, failure, and bindings',
|
|
225
|
+
run: () => {
|
|
226
|
+
const result = runCli([], { input: 'true.\nfalse.\nX = hello.\nhalt.\n' });
|
|
227
|
+
assertEqual(result.status, 0, 'exit status');
|
|
228
|
+
assertEqual(result.stdout, '?- true.\n?- false.\n?- X = hello.\n?- ', 'stdout');
|
|
229
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
name: 'REPL enumerates and stops answers like the Scryer top level',
|
|
234
|
+
run: () => {
|
|
235
|
+
const result = runCli([], {
|
|
236
|
+
input: '(X = a; X = b).\n;\n(X = one; X = two).\n\nhalt.\n',
|
|
237
|
+
});
|
|
238
|
+
assertEqual(result.status, 0, 'exit status');
|
|
239
|
+
assertEqual(result.stdout, '?- X = a\n; X = b.\n?- X = one\n; ... .\n?- ', 'stdout');
|
|
240
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
name: 'REPL accepts multiline period-terminated queries',
|
|
245
|
+
run: () => {
|
|
246
|
+
const result = runCli([], { input: '(X =\n one).\nhalt.\n' });
|
|
247
|
+
assertEqual(result.status, 0, 'exit status');
|
|
248
|
+
assertEqual(result.stdout, '?- | X = one.\n?- ', 'stdout');
|
|
249
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: 'REPL consult shorthand loads local Prolog files',
|
|
254
|
+
run: () => {
|
|
255
|
+
const filename = path.join(tmp, `repl-consult-${++tmpCounter}.pl`);
|
|
256
|
+
fs.writeFileSync(filename, 'color(red).\ncolor(blue).\n');
|
|
257
|
+
const result = runCli([], {
|
|
258
|
+
input: `[${sourceAtom(filename)}].\ncolor(X).\n;\nhalt.\n`,
|
|
259
|
+
});
|
|
260
|
+
assertEqual(result.status, 0, 'exit status');
|
|
261
|
+
assertEqual(result.stdout, '?- true.\n?- X = red\n; X = blue.\n?- ', 'stdout');
|
|
262
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: 'REPL use_module imports Part 2 library predicates',
|
|
267
|
+
run: () => {
|
|
268
|
+
const result = runCli([], {
|
|
269
|
+
input: 'append(X, Y, [1, 2, 3, 4]).\nuse_module(library(lists)).\nappend(X, Y, [1, 2, 3, 4]).\n\nhalt.\n',
|
|
270
|
+
});
|
|
271
|
+
assertEqual(result.status, 0, 'exit status');
|
|
272
|
+
assertEqual(result.stdout, '?- false.\n?- true.\n?- X = [], Y = [1, 2, 3, 4]\n; ... .\n?- ', 'stdout');
|
|
273
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: 'REPL halt status is returned by the CLI',
|
|
278
|
+
run: () => {
|
|
279
|
+
const result = runCli([], { input: 'halt(7).\n' });
|
|
280
|
+
assertEqual(result.status, 7, 'exit status');
|
|
281
|
+
assertEqual(result.stdout, '?- ', 'stdout');
|
|
282
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
283
|
+
},
|
|
284
|
+
},
|
|
221
285
|
{
|
|
222
286
|
name: 'CLI loads an explicitly imported standard library module',
|
|
223
287
|
run: () => {
|
|
@@ -303,6 +367,31 @@ why(
|
|
|
303
367
|
assertEqual(result.stderr, '', 'stderr');
|
|
304
368
|
},
|
|
305
369
|
},
|
|
370
|
+
{
|
|
371
|
+
name: '-g supplies an explicit CLI goal',
|
|
372
|
+
run: () => {
|
|
373
|
+
const input = [
|
|
374
|
+
'%% goal: answer(metadata, X)',
|
|
375
|
+
'value(metadata, ignored).',
|
|
376
|
+
'value(explicit, selected).',
|
|
377
|
+
'answer(Kind, Value) :- value(Kind, Value).',
|
|
378
|
+
'',
|
|
379
|
+
].join('\n');
|
|
380
|
+
const result = runCli(['-g', 'answer(explicit, X)', '-'], { input });
|
|
381
|
+
assertEqual(result.status, 0, 'exit status');
|
|
382
|
+
assertEqual(result.stdout, 'answer(explicit, selected).\n', 'stdout');
|
|
383
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
name: '-g requires a goal argument',
|
|
388
|
+
run: () => {
|
|
389
|
+
const result = runCli(['-g']);
|
|
390
|
+
assertEqual(result.status, 1, 'exit status');
|
|
391
|
+
assertEqual(result.stdout, '', 'stdout');
|
|
392
|
+
assertEqual(result.stderr, 'eyeprolog: option -g requires a goal\n', 'stderr');
|
|
393
|
+
},
|
|
394
|
+
},
|
|
306
395
|
|
|
307
396
|
{
|
|
308
397
|
name: '--proof enables query explanations',
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -6015,19 +6015,44 @@ 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
|
|
6024
|
-
selects what to solve. Supply `--goal` followed by a callable Prolog goal:
|
|
6049
|
+
selects what to solve. Supply `-g` or `--goal` followed by a callable Prolog goal:
|
|
6025
6050
|
|
|
6026
6051
|
```sh
|
|
6027
6052
|
eyeprolog --goal 'ancestor(ada, Who)' examples/ancestor.pl
|
|
6028
6053
|
```
|
|
6029
6054
|
|
|
6030
|
-
Repeat `--goal` to request several result relations in one run. EyeProlog prints
|
|
6055
|
+
Repeat `-g` or `--goal` to request several result relations in one run. EyeProlog prints
|
|
6031
6056
|
their ground answers in the order the goals were supplied.
|
|
6032
6057
|
|
|
6033
6058
|
For a self-running example, place the host goal in an ordinary comment:
|
|
@@ -6036,8 +6061,8 @@ For a self-running example, place the host goal in an ordinary comment:
|
|
|
6036
6061
|
%% goal: ancestor(ada, Who)
|
|
6037
6062
|
```
|
|
6038
6063
|
|
|
6039
|
-
When no `--goal` option is present, the CLI reads these comments from all
|
|
6040
|
-
sources and runs them in source order. An explicit
|
|
6064
|
+
When no `-g` or `--goal` option is present, the CLI reads these comments from all
|
|
6065
|
+
input sources and runs them in source order. An explicit goal option overrides them.
|
|
6041
6066
|
Because `%% goal:` is a comment rather than a Prolog directive, another ISO
|
|
6042
6067
|
processor may ignore it and the program remains portable Prolog text. External
|
|
6043
6068
|
goals are still preferable when a script, shell history, or API call should
|
|
@@ -6050,14 +6075,14 @@ make the observed question explicit.
|
|
|
6050
6075
|
| `-s`, `--stats` | Print solver counters to stderr |
|
|
6051
6076
|
| `-v`, `--version` | Print the package version |
|
|
6052
6077
|
| `-w`, `--warnings` | Print non-fatal portability warnings |
|
|
6053
|
-
| `--goal Goal` | Solve a callable goal; may be repeated; overrides `%% goal:` comments |
|
|
6078
|
+
| `-g`, `--goal Goal` | Solve a callable goal; may be repeated; overrides `%% goal:` comments |
|
|
6054
6079
|
| `--` | Treat following arguments as inputs |
|
|
6055
6080
|
|
|
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.
|