eyeprolog 1.2.6 → 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 +70 -4
- package/test/run-regression.mjs +47 -0
- package/the-art-of-eyeprolog.md +18 -12
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,7 +21,7 @@ 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 {
|
|
@@ -34,7 +35,7 @@ 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 = makeState(engine, sources, output, options, state, reader);
|
|
38
39
|
runWithTerminalSignals(reader, () => state.solver.runInitializations());
|
|
39
40
|
output.write(' true.\n');
|
|
40
41
|
continue;
|
|
@@ -42,7 +43,7 @@ export async function runRepl(engine, options = {}) {
|
|
|
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 = makeState(engine, sources, output, options, state, reader);
|
|
46
47
|
runWithTerminalSignals(reader, () => state.solver.runInitializations());
|
|
47
48
|
output.write(' true.\n');
|
|
48
49
|
continue;
|
|
@@ -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,6 +145,57 @@ 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
|
+
|
|
145
199
|
suspendForComputation() {
|
|
146
200
|
if (!this.terminal || !this.readline) return false;
|
|
147
201
|
// Node readline installs terminal signal handling while the interface is
|
|
@@ -175,7 +229,7 @@ function runWithTerminalSignals(reader, operation) {
|
|
|
175
229
|
}
|
|
176
230
|
}
|
|
177
231
|
|
|
178
|
-
function makeState(engine, sources, output, options = {}, previousState = null) {
|
|
232
|
+
function makeState(engine, sources, output, options = {}, previousState = null, reader = null) {
|
|
179
233
|
const strictIso = options.isoStrict === true;
|
|
180
234
|
const program = engine.Program.parseSources(sources, { strictIso, sourceMetadata: strictIso });
|
|
181
235
|
const solver = new engine.Solver(program, {
|
|
@@ -183,6 +237,14 @@ function makeState(engine, sources, output, options = {}, previousState = null)
|
|
|
183
237
|
isoStrict: strictIso,
|
|
184
238
|
ioOptions: { write: (text) => output.write(String(text)) },
|
|
185
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
|
+
}
|
|
186
248
|
const flagOverrides = new Map(previousState?.flagOverrides ?? []);
|
|
187
249
|
for (const [name, value] of flagOverrides) {
|
|
188
250
|
const definition = solver.prologFlags.get(name);
|
|
@@ -218,6 +280,10 @@ async function readQuery(reader) {
|
|
|
218
280
|
}
|
|
219
281
|
|
|
220
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;
|
|
221
287
|
const stream = interactiveTermInputStream(state, goal);
|
|
222
288
|
if (stream == null || terminalFullStop(String(stream.content).slice(stream.position)) >= 0) return;
|
|
223
289
|
|
package/test/run-regression.mjs
CHANGED
|
@@ -719,6 +719,53 @@ 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
|
+
},
|
|
722
769
|
{
|
|
723
770
|
name: 'REPL releases terminal signals while a query computes',
|
|
724
771
|
run: () => {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -6228,21 +6228,27 @@ not be a valid right operand of the displayed `=/2`, EyeProlog adds parentheses,
|
|
|
6228
6228
|
for example `T = (a = b).` rather than the invalid `T = a = b.`. Use `[file].`
|
|
6229
6229
|
or `['file.pl'].` to
|
|
6230
6230
|
consult local source, and `halt.` or `halt(Status).` to leave the top level.
|
|
6231
|
-
|
|
6232
|
-
next full-stop-terminated Prolog term
|
|
6233
|
-
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:
|
|
6234
6236
|
|
|
6235
6237
|
```text
|
|
6236
|
-
?- read(X).
|
|
6238
|
+
?- read(X), read(Y).
|
|
6237
6239
|
|: hello.
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
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.
|
|
6246
6252
|
|
|
6247
6253
|
### Selecting goals
|
|
6248
6254
|
|