eyeprolog 1.2.5 → 1.2.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/package.json +1 -1
- package/src/iso.js +40 -19
- package/src/repl.js +105 -10
- package/test/run-regression.mjs +72 -0
- package/the-art-of-eyeprolog.md +23 -13
package/package.json
CHANGED
package/src/iso.js
CHANGED
|
@@ -1135,28 +1135,49 @@ function convertedTermText(text, solver) {
|
|
|
1135
1135
|
return result;
|
|
1136
1136
|
}
|
|
1137
1137
|
function readTermFromStream(stream, solver) {
|
|
1138
|
-
let
|
|
1139
|
-
|
|
1140
|
-
sawCandidate =
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1138
|
+
let requestedInteractiveTerm = false;
|
|
1139
|
+
while (true) {
|
|
1140
|
+
let sawCandidate = false;
|
|
1141
|
+
for (const candidate of termTextCandidates(stream)) {
|
|
1142
|
+
sawCandidate = true;
|
|
1143
|
+
try {
|
|
1144
|
+
const operatorState = createParserOperatorState(solver.program.operators.values(), false);
|
|
1145
|
+
const clauses = parseClauses(convertedTermText(candidate.text, solver), {
|
|
1146
|
+
sourceMetadata: false,
|
|
1147
|
+
operatorState,
|
|
1148
|
+
doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
|
|
1149
|
+
});
|
|
1150
|
+
if (clauses.length !== 1 || clauses[0].body.length) throw new Error('bad term');
|
|
1151
|
+
stream.position = candidate.end;
|
|
1152
|
+
return clauses[0].head;
|
|
1153
|
+
} catch (_) {
|
|
1154
|
+
// A dot inside a graphic operator, such as =.., is only a possible
|
|
1155
|
+
// terminator. Keep scanning until a complete term parses.
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// The interactive top level may attach a synchronous reader to the
|
|
1160
|
+
// standard user_input stream. Ask it for one complete read-term only when
|
|
1161
|
+
// this read operation actually reaches the end of buffered input. This is
|
|
1162
|
+
// deliberately a stream hook, not goal-shape recognition, so conjunctions
|
|
1163
|
+
// and reads reached through user predicates behave the same as read/1.
|
|
1164
|
+
if (!sawCandidate && !requestedInteractiveTerm &&
|
|
1165
|
+
typeof stream.interactiveReadTerm === 'function') {
|
|
1166
|
+
requestedInteractiveTerm = true;
|
|
1167
|
+
const text = stream.interactiveReadTerm();
|
|
1168
|
+
if (text != null) {
|
|
1169
|
+
stream.content += String(text);
|
|
1170
|
+
stream.pastEnd = false;
|
|
1171
|
+
continue;
|
|
1172
|
+
}
|
|
1154
1173
|
}
|
|
1174
|
+
|
|
1175
|
+
stream.position = String(stream.content).length;
|
|
1176
|
+
if (!sawCandidate) return atom('end_of_file');
|
|
1177
|
+
throw new PrologError('syntax_error(read_term)');
|
|
1155
1178
|
}
|
|
1156
|
-
stream.position = String(stream.content).length;
|
|
1157
|
-
if (!sawCandidate) return atom('end_of_file');
|
|
1158
|
-
throw new PrologError('syntax_error(read_term)');
|
|
1159
1179
|
}
|
|
1180
|
+
|
|
1160
1181
|
function* readBuiltin({ solver, goal, env }) {
|
|
1161
1182
|
const stream = inputStreamFor(solver, goal, env);
|
|
1162
1183
|
if (stream.type !== 'text') throw new PrologError('permission_error(input, binary_stream)', streamHandle(stream.id));
|
package/src/repl.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Interactive top level for the eyeprolog command.
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
|
+
import { readSync } from 'node:fs';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { createInterface } from 'node:readline';
|
|
5
6
|
import { formalErrorTerm } from './iso.js';
|
|
@@ -20,11 +21,11 @@ export async function runRepl(engine, options = {}) {
|
|
|
20
21
|
const errorOutput = options.errorOutput ?? process.stderr;
|
|
21
22
|
const reader = new LineReader(input, output);
|
|
22
23
|
const sources = [];
|
|
23
|
-
let state = makeState(engine, sources, output, options);
|
|
24
|
+
let state = makeState(engine, sources, output, options, null, reader);
|
|
24
25
|
let exitCode = 0;
|
|
25
26
|
|
|
26
27
|
try {
|
|
27
|
-
state.solver.runInitializations();
|
|
28
|
+
runWithTerminalSignals(reader, () => state.solver.runInitializations());
|
|
28
29
|
while (true) {
|
|
29
30
|
const text = await readQuery(reader);
|
|
30
31
|
if (text == null) break;
|
|
@@ -34,16 +35,16 @@ export async function runRepl(engine, options = {}) {
|
|
|
34
35
|
const goal = parseGoal(engine, state, text);
|
|
35
36
|
if (!options.isoStrict && isUseModuleGoal(goal)) {
|
|
36
37
|
sources.push({ text: `:- ${text}.\n`, filename: '<repl>' });
|
|
37
|
-
state = makeState(engine, sources, output, options, state);
|
|
38
|
-
state.solver.runInitializations();
|
|
38
|
+
state = makeState(engine, sources, output, options, state, reader);
|
|
39
|
+
runWithTerminalSignals(reader, () => state.solver.runInitializations());
|
|
39
40
|
output.write(' true.\n');
|
|
40
41
|
continue;
|
|
41
42
|
}
|
|
42
43
|
const consultFiles = options.isoStrict ? null : consultDesignations(engine, goal);
|
|
43
44
|
if (consultFiles != null) {
|
|
44
45
|
for (const filename of consultFiles) sources.push(await readSource(filename));
|
|
45
|
-
state = makeState(engine, sources, output, options, state);
|
|
46
|
-
state.solver.runInitializations();
|
|
46
|
+
state = makeState(engine, sources, output, options, state, reader);
|
|
47
|
+
runWithTerminalSignals(reader, () => state.solver.runInitializations());
|
|
47
48
|
output.write(' true.\n');
|
|
48
49
|
continue;
|
|
49
50
|
}
|
|
@@ -78,6 +79,8 @@ export async function runRepl(engine, options = {}) {
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
class LineReader {
|
|
82
|
+
static syncWait = new Int32Array(new SharedArrayBuffer(4));
|
|
83
|
+
|
|
81
84
|
constructor(input, output) {
|
|
82
85
|
this.input = input;
|
|
83
86
|
this.output = output;
|
|
@@ -142,13 +145,91 @@ class LineReader {
|
|
|
142
145
|
return control;
|
|
143
146
|
}
|
|
144
147
|
|
|
148
|
+
canReadTermSynchronously() {
|
|
149
|
+
return this.terminal && Number.isInteger(this.input.fd);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
readInteractiveTermSync() {
|
|
153
|
+
if (!this.canReadTermSynchronously()) return null;
|
|
154
|
+
let source = '';
|
|
155
|
+
let prompt = '|: ';
|
|
156
|
+
while (true) {
|
|
157
|
+
this.output.write(prompt);
|
|
158
|
+
const line = this.readTerminalLineSync();
|
|
159
|
+
if (line == null) return source.trim() ? source : null;
|
|
160
|
+
source += `${line}\n`;
|
|
161
|
+
const end = terminalFullStop(source);
|
|
162
|
+
if (end >= 0) return source.slice(0, end + 1) + '\n';
|
|
163
|
+
prompt = '| ';
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
readTerminalLineSync() {
|
|
168
|
+
const byte = Buffer.allocUnsafe(1);
|
|
169
|
+
const bytes = [];
|
|
170
|
+
while (true) {
|
|
171
|
+
let count;
|
|
172
|
+
try {
|
|
173
|
+
count = readSync(this.input.fd, byte, 0, 1, null);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
// Node keeps terminal fds non-blocking. Once readline is suspended,
|
|
176
|
+
// a synchronous read can therefore report EAGAIN while waiting for
|
|
177
|
+
// the user. Sleep briefly and retry; terminal signals still retain
|
|
178
|
+
// their native action because no readline signal handler is installed.
|
|
179
|
+
if (error?.code === 'EAGAIN' || error?.code === 'EWOULDBLOCK') {
|
|
180
|
+
Atomics.wait(LineReader.syncWait, 0, 0, 10);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
// In canonical terminal mode Ctrl-D on an empty line makes read(2)
|
|
186
|
+
// return zero bytes. Scope that EOF to the current Prolog read rather
|
|
187
|
+
// than closing the outer readline iterator / top-level loop.
|
|
188
|
+
if (count === 0) {
|
|
189
|
+
return bytes.length === 0 ? null : Buffer.from(bytes).toString('utf8');
|
|
190
|
+
}
|
|
191
|
+
if (byte[0] === 10) {
|
|
192
|
+
if (bytes.at(-1) === 13) bytes.pop();
|
|
193
|
+
return Buffer.from(bytes).toString('utf8');
|
|
194
|
+
}
|
|
195
|
+
bytes.push(byte[0]);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
suspendForComputation() {
|
|
200
|
+
if (!this.terminal || !this.readline) return false;
|
|
201
|
+
// Node readline installs terminal signal handling while the interface is
|
|
202
|
+
// open. During a synchronous Prolog search that prevents the terminal's
|
|
203
|
+
// normal SIGINT/SIGTSTP actions from taking effect until JavaScript yields.
|
|
204
|
+
// Close readline while the solver is running so Ctrl-C can terminate and
|
|
205
|
+
// Ctrl-Z can suspend an otherwise non-terminating computation immediately.
|
|
206
|
+
this.history = [...this.readline.history];
|
|
207
|
+
this.readline.close();
|
|
208
|
+
this.readline = null;
|
|
209
|
+
this.lines = null;
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
resumeAfterComputation(suspended) {
|
|
214
|
+
if (suspended && !this.readline) this.open();
|
|
215
|
+
}
|
|
216
|
+
|
|
145
217
|
close() {
|
|
146
218
|
if (this.input.isRaw) this.input.setRawMode(false);
|
|
147
219
|
this.readline?.close();
|
|
148
220
|
}
|
|
149
221
|
}
|
|
150
222
|
|
|
151
|
-
function
|
|
223
|
+
function runWithTerminalSignals(reader, operation) {
|
|
224
|
+
const suspended = reader.suspendForComputation();
|
|
225
|
+
try {
|
|
226
|
+
return operation();
|
|
227
|
+
} finally {
|
|
228
|
+
reader.resumeAfterComputation(suspended);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function makeState(engine, sources, output, options = {}, previousState = null, reader = null) {
|
|
152
233
|
const strictIso = options.isoStrict === true;
|
|
153
234
|
const program = engine.Program.parseSources(sources, { strictIso, sourceMetadata: strictIso });
|
|
154
235
|
const solver = new engine.Solver(program, {
|
|
@@ -156,6 +237,14 @@ function makeState(engine, sources, output, options = {}, previousState = null)
|
|
|
156
237
|
isoStrict: strictIso,
|
|
157
238
|
ioOptions: { write: (text) => output.write(String(text)) },
|
|
158
239
|
});
|
|
240
|
+
const userInput = solver.io.resolve('user_input');
|
|
241
|
+
if (userInput && reader?.canReadTermSynchronously()) {
|
|
242
|
+
// The solver is synchronous. While pullSolution() has readline suspended,
|
|
243
|
+
// let ISO term input request a complete terminal term exactly when read/1-2
|
|
244
|
+
// or read_term/2-3 actually executes. This also works inside conjunctions
|
|
245
|
+
// and user predicates instead of only when read/* is the whole REPL goal.
|
|
246
|
+
userInput.interactiveReadTerm = () => reader.readInteractiveTermSync();
|
|
247
|
+
}
|
|
159
248
|
const flagOverrides = new Map(previousState?.flagOverrides ?? []);
|
|
160
249
|
for (const [name, value] of flagOverrides) {
|
|
161
250
|
const definition = solver.prologFlags.get(name);
|
|
@@ -191,6 +280,10 @@ async function readQuery(reader) {
|
|
|
191
280
|
}
|
|
192
281
|
|
|
193
282
|
async function prepareInteractiveTermInput(state, goal, reader) {
|
|
283
|
+
// Real terminals are serviced on demand from readTermFromStream() while the
|
|
284
|
+
// synchronous solver is running. Keep the older async preloader only as a
|
|
285
|
+
// fallback for piped/non-TTY REPL tests and scripted input.
|
|
286
|
+
if (reader.canReadTermSynchronously()) return;
|
|
194
287
|
const stream = interactiveTermInputStream(state, goal);
|
|
195
288
|
if (stream == null || terminalFullStop(String(stream.content).slice(stream.position)) >= 0) return;
|
|
196
289
|
|
|
@@ -369,7 +462,7 @@ async function solveQuery(engine, state, goal, reader, output) {
|
|
|
369
462
|
const solver = state.solver;
|
|
370
463
|
solver.solutionsSeen = 0;
|
|
371
464
|
const solutions = solver.solve([goal], new engine.Env(), 0);
|
|
372
|
-
let current = pullSolution(solver, solutions);
|
|
465
|
+
let current = pullSolution(solver, solutions, reader);
|
|
373
466
|
if (current.error) {
|
|
374
467
|
if (current.error?.name === 'HaltSignal') return { halted: true, code: current.error.code };
|
|
375
468
|
throw current.error;
|
|
@@ -383,7 +476,7 @@ async function solveQuery(engine, state, goal, reader, output) {
|
|
|
383
476
|
let automatic = 0;
|
|
384
477
|
let firstAnswer = true;
|
|
385
478
|
while (!current.result.done) {
|
|
386
|
-
const next = pullSolution(solver, solutions);
|
|
479
|
+
const next = pullSolution(solver, solutions, reader);
|
|
387
480
|
output.write(current.output);
|
|
388
481
|
output.write(`${firstAnswer ? ' ' : ''}${formatAnswer(engine, state, variables, current.result.value)}`);
|
|
389
482
|
firstAnswer = false;
|
|
@@ -436,16 +529,18 @@ async function solveQuery(engine, state, goal, reader, output) {
|
|
|
436
529
|
return null;
|
|
437
530
|
}
|
|
438
531
|
|
|
439
|
-
function pullSolution(solver, solutions) {
|
|
532
|
+
function pullSolution(solver, solutions, reader) {
|
|
440
533
|
const stream = solver.io.resolve('user_output');
|
|
441
534
|
const originalWrite = stream?.write;
|
|
442
535
|
let captured = '';
|
|
443
536
|
if (stream) stream.write = (text) => { captured += String(text); };
|
|
537
|
+
const suspended = reader.suspendForComputation();
|
|
444
538
|
try {
|
|
445
539
|
return { result: solutions.next(), output: captured };
|
|
446
540
|
} catch (error) {
|
|
447
541
|
return { error, output: captured };
|
|
448
542
|
} finally {
|
|
543
|
+
reader.resumeAfterComputation(suspended);
|
|
449
544
|
if (stream) stream.write = originalWrite;
|
|
450
545
|
}
|
|
451
546
|
}
|
package/test/run-regression.mjs
CHANGED
|
@@ -719,6 +719,74 @@ c4 ?- call((!;1)).
|
|
|
719
719
|
assertEqual(result.stderr, '', 'stderr');
|
|
720
720
|
},
|
|
721
721
|
},
|
|
722
|
+
{
|
|
723
|
+
name: 'REPL term input is on demand in conjunctions and Ctrl-D does not exit the top level',
|
|
724
|
+
run: () => {
|
|
725
|
+
if (process.platform === 'win32') return;
|
|
726
|
+
const available = spawnSync('sh', ['-c',
|
|
727
|
+
'command -v script >/dev/null 2>&1 && script --version 2>/dev/null | grep -qi util-linux']);
|
|
728
|
+
if (available.status !== 0) return;
|
|
729
|
+
const command = `${shellQuote(process.execPath)} ${shellQuote(bin)}`;
|
|
730
|
+
const scriptCommand =
|
|
731
|
+
`{ printf 'read(X), read(Y).\n'; sleep 0.15; ` +
|
|
732
|
+
`printf 'foo.\n'; sleep 0.15; printf 'bar.\n'; sleep 0.15; ` +
|
|
733
|
+
`printf 'read(Z).\n'; sleep 0.15; printf '\\004'; sleep 0.15; ` +
|
|
734
|
+
`printf 'true.\n'; sleep 0.15; printf 'halt.\n'; } | ` +
|
|
735
|
+
`script -qefc ${shellQuote(command)} /dev/null`;
|
|
736
|
+
const result = spawnSync('sh', ['-c', scriptCommand], {
|
|
737
|
+
cwd: packageRoot,
|
|
738
|
+
encoding: 'utf8',
|
|
739
|
+
timeout: 5000,
|
|
740
|
+
});
|
|
741
|
+
assertEqual(result.error?.code, undefined, 'interactive read timeout');
|
|
742
|
+
assertEqual(result.status, 0, 'exit status');
|
|
743
|
+
assertIncludes(result.stdout, 'X = foo, Y = bar.', 'conjunction reads');
|
|
744
|
+
assertIncludes(result.stdout, 'Z = end_of_file.', 'Ctrl-D read result');
|
|
745
|
+
assertIncludes(result.stdout, '?- true.', 'top level resumes after Ctrl-D');
|
|
746
|
+
assertIncludes(result.stdout, ' true.', 'post-EOF query executes');
|
|
747
|
+
},
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
name: 'interactive user_input hook serves reads reached through user predicates',
|
|
751
|
+
run: () => {
|
|
752
|
+
const program = Program.parse('pair(A, B) :- read(A), read(B).\n');
|
|
753
|
+
const solver = new Solver(program, { registry: getEyePrologRegistry() });
|
|
754
|
+
const stream = solver.io.resolve('user_input');
|
|
755
|
+
const pending = ['left.\n', 'right.\n'];
|
|
756
|
+
let requests = 0;
|
|
757
|
+
stream.interactiveReadTerm = () => {
|
|
758
|
+
requests++;
|
|
759
|
+
return pending.shift() ?? null;
|
|
760
|
+
};
|
|
761
|
+
const goal = parseGoalText('pair(X, Y)');
|
|
762
|
+
const answers = [...solver.solve([goal], new Env(), 0)];
|
|
763
|
+
assertEqual(answers.length, 1, 'answer count');
|
|
764
|
+
assertEqual(termToString(copyResolved(goal.args[0], answers[0])), 'left', 'first read');
|
|
765
|
+
assertEqual(termToString(copyResolved(goal.args[1], answers[0])), 'right', 'second read');
|
|
766
|
+
assertEqual(requests, 2, 'on-demand read count');
|
|
767
|
+
},
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
name: 'REPL releases terminal signals while a query computes',
|
|
771
|
+
run: () => {
|
|
772
|
+
if (process.platform === 'win32') return;
|
|
773
|
+
const available = spawnSync('sh', ['-c',
|
|
774
|
+
'command -v script >/dev/null 2>&1 && script --version 2>/dev/null | grep -qi util-linux']);
|
|
775
|
+
if (available.status !== 0) return;
|
|
776
|
+
const command = `${shellQuote(process.execPath)} ${shellQuote(bin)}`;
|
|
777
|
+
const scriptCommand =
|
|
778
|
+
`{ printf 'repeat, fail.\n'; sleep 0.2; printf '\\003'; } | ` +
|
|
779
|
+
`script -qefc ${shellQuote(command)} /dev/null`;
|
|
780
|
+
const result = spawnSync('sh', ['-c', scriptCommand], {
|
|
781
|
+
cwd: packageRoot,
|
|
782
|
+
encoding: 'utf8',
|
|
783
|
+
timeout: 3000,
|
|
784
|
+
});
|
|
785
|
+
assertEqual(result.error?.code, undefined, 'terminal interrupt timeout');
|
|
786
|
+
assertEqual(result.status, 130, 'SIGINT exit status');
|
|
787
|
+
assertIncludes(result.stdout, '?- repeat, fail.', 'terminal query echo');
|
|
788
|
+
},
|
|
789
|
+
},
|
|
722
790
|
{
|
|
723
791
|
name: 'REPL accepts multiline period-terminated queries',
|
|
724
792
|
run: () => {
|
|
@@ -3109,6 +3177,10 @@ function between(text, startMarker, endMarker) {
|
|
|
3109
3177
|
return text.slice(contentStart, end);
|
|
3110
3178
|
}
|
|
3111
3179
|
|
|
3180
|
+
function shellQuote(value) {
|
|
3181
|
+
return `'${String(value).replaceAll("'", "'\"'\"'")}'`;
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3112
3184
|
function runCli(args, options = {}) {
|
|
3113
3185
|
return spawnSync(process.execPath, [bin, ...args], {
|
|
3114
3186
|
cwd: packageRoot,
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -6216,7 +6216,11 @@ may span lines and end with a full stop, as in Scryer Prolog:
|
|
|
6216
6216
|
When another answer exists in an interactive terminal, press `;`, Space, or
|
|
6217
6217
|
`n` to ask for it immediately; no Return is needed. Return or `.` stops
|
|
6218
6218
|
enumeration, `a` enumerates all remaining answers, `f` enumerates the next
|
|
6219
|
-
five, and `h` displays the answer-control help.
|
|
6219
|
+
five, and `h` displays the answer-control help. While a query is actively
|
|
6220
|
+
computing, EyeProlog releases readline's terminal signal handling: `Ctrl-C`
|
|
6221
|
+
therefore terminates the current EyeProlog process immediately, and on POSIX
|
|
6222
|
+
terminals `Ctrl-Z` suspends it in the usual shell-managed way. This remains a
|
|
6223
|
+
host top-level convention rather than an ISO/IEC 13211-1 language feature. A
|
|
6220
6224
|
period-terminated query with no solutions prints `false.`; a solution without
|
|
6221
6225
|
visible variable bindings prints `true.`. Answer substitutions are rendered as
|
|
6222
6226
|
valid Prolog syntax under the current operator table: when a bound value would
|
|
@@ -6224,21 +6228,27 @@ not be a valid right operand of the displayed `=/2`, EyeProlog adds parentheses,
|
|
|
6224
6228
|
for example `T = (a = b).` rather than the invalid `T = a = b.`. Use `[file].`
|
|
6225
6229
|
or `['file.pl'].` to
|
|
6226
6230
|
consult local source, and `halt.` or `halt(Status).` to leave the top level.
|
|
6227
|
-
|
|
6228
|
-
next full-stop-terminated Prolog term
|
|
6229
|
-
treating the
|
|
6231
|
+
When `read/1-2` or `read_term/2-3` actually reaches interactive
|
|
6232
|
+
`user_input`, the top level requests the next full-stop-terminated Prolog term
|
|
6233
|
+
with a `|: ` input prompt instead of treating the terminal stream as already
|
|
6234
|
+
exhausted. The request is made at execution time, so multiple reads in one goal
|
|
6235
|
+
and reads reached through user predicates work independently. For example:
|
|
6230
6236
|
|
|
6231
6237
|
```text
|
|
6232
|
-
?- read(X).
|
|
6238
|
+
?- read(X), read(Y).
|
|
6233
6239
|
|: hello.
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6240
|
+
|: world.
|
|
6241
|
+
X = hello, Y = world.
|
|
6242
|
+
```
|
|
6243
|
+
|
|
6244
|
+
Typing `Ctrl-D` at an empty `|: ` prompt makes that Prolog read return
|
|
6245
|
+
`end_of_file`; it does not close the surrounding EyeProlog top-level loop, so a
|
|
6246
|
+
new `?- ` query can still be entered afterwards. The top-level prompts and this
|
|
6247
|
+
terminal EOF convention are host-interface behavior rather than part of
|
|
6248
|
+
ISO/IEC 13211-1; terms supplied to the reads are parsed by the same ISO
|
|
6249
|
+
term-input machinery as `read/1-2` and `read_term/2-3` on other text streams.
|
|
6250
|
+
Up and Down recall queries from the current session. Explicit `eyeprolog -h`
|
|
6251
|
+
displays command-line help.
|
|
6242
6252
|
|
|
6243
6253
|
### Selecting goals
|
|
6244
6254
|
|