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 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
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.1.2",
6
+ "version": "1.1.4",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
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 usage(process.stdout);
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('option --goal requires a goal');
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 Solve goal and print its ground answers; may be repeated.
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
+ }
@@ -204,11 +204,13 @@ why(
204
204
  },
205
205
  },
206
206
  {
207
- name: 'help with no arguments',
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',
@@ -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 input
6040
- sources and runs them in source order. An explicit `--goal` overrides them.
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` prints help. When options are present but no input is named, stdin is
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.