eyeprolog 1.2.18 → 1.2.20
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/index.d.ts +3 -0
- package/package.json +1 -1
- package/src/platform.js +42 -0
- package/src/repl.js +17 -5
- package/src/solver.js +16 -0
- package/test/run-regression.mjs +84 -0
- package/the-art-of-eyeprolog.md +32 -17
package/index.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface EyePrologRunOptions {
|
|
|
12
12
|
explain?: boolean;
|
|
13
13
|
maxDepth?: number;
|
|
14
14
|
maxInferences?: number;
|
|
15
|
+
/** Soft JavaScript heap ceiling in bytes; exhaustion raises resource_error(memory). */
|
|
16
|
+
maxMemoryBytes?: number;
|
|
15
17
|
solutionLimit?: number;
|
|
16
18
|
registry?: BuiltinRegistry;
|
|
17
19
|
sourceMetadata?: boolean;
|
|
@@ -171,6 +173,7 @@ export class Solver {
|
|
|
171
173
|
maxInferences: number;
|
|
172
174
|
inferences: number;
|
|
173
175
|
inferenceLimitExceeded: boolean;
|
|
176
|
+
maxMemoryBytes: number;
|
|
174
177
|
solutionLimit: number;
|
|
175
178
|
solutionsSeen: number;
|
|
176
179
|
active: unknown[];
|
package/package.json
CHANGED
package/src/platform.js
CHANGED
|
@@ -6,10 +6,12 @@ const isNode = typeof process !== 'undefined' && Boolean(process.versions?.node)
|
|
|
6
6
|
let fs = null;
|
|
7
7
|
let path = null;
|
|
8
8
|
let BufferCtor = null;
|
|
9
|
+
let v8 = null;
|
|
9
10
|
|
|
10
11
|
if (isNode) {
|
|
11
12
|
({ default: fs } = await import('node:fs'));
|
|
12
13
|
({ default: path } = await import('node:path'));
|
|
14
|
+
({ default: v8 } = await import('node:v8'));
|
|
13
15
|
BufferCtor = globalThis.Buffer ?? null;
|
|
14
16
|
}
|
|
15
17
|
|
|
@@ -18,3 +20,43 @@ export { fs, path, BufferCtor, isNode };
|
|
|
18
20
|
export function currentWorkingDirectory() {
|
|
19
21
|
return isNode && typeof process.cwd === 'function' ? process.cwd() : '/';
|
|
20
22
|
}
|
|
23
|
+
|
|
24
|
+
export function usedHeapSize() {
|
|
25
|
+
if (isNode && typeof process.memoryUsage === 'function') {
|
|
26
|
+
return process.memoryUsage().heapUsed;
|
|
27
|
+
}
|
|
28
|
+
const memory = globalThis.performance?.memory;
|
|
29
|
+
return Number.isFinite(memory?.usedJSHeapSize) ? memory.usedJSHeapSize : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function softHeapLimit() {
|
|
33
|
+
let limit = null;
|
|
34
|
+
if (isNode) {
|
|
35
|
+
limit = v8?.getHeapStatistics?.().heap_size_limit ?? null;
|
|
36
|
+
const configuredOldSpace = configuredOldSpaceBytes();
|
|
37
|
+
if (configuredOldSpace != null) limit = Math.min(limit ?? Infinity, configuredOldSpace);
|
|
38
|
+
} else {
|
|
39
|
+
const memory = globalThis.performance?.memory;
|
|
40
|
+
if (Number.isFinite(memory?.jsHeapSizeLimit)) limit = memory.jsHeapSizeLimit;
|
|
41
|
+
}
|
|
42
|
+
// Leave ample room for the generator stack to unwind and for the top level
|
|
43
|
+
// to construct and print resource_error(memory). Fatal V8 OOMs cannot be
|
|
44
|
+
// caught after the heap limit itself has been reached.
|
|
45
|
+
return Number.isFinite(limit) && limit > 0 ? Math.floor(limit * 0.75) : Infinity;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function configuredOldSpaceBytes() {
|
|
49
|
+
if (!isNode) return null;
|
|
50
|
+
const argumentsText = [
|
|
51
|
+
...(process.execArgv ?? []),
|
|
52
|
+
...(String(process.env?.NODE_OPTIONS ?? '').match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? []),
|
|
53
|
+
];
|
|
54
|
+
for (let index = 0; index < argumentsText.length; index++) {
|
|
55
|
+
const argument = argumentsText[index];
|
|
56
|
+
const match = /^--max[-_]old[-_]space[-_]size(?:=(\d+))?$/.exec(argument);
|
|
57
|
+
if (!match) continue;
|
|
58
|
+
const megabytes = match[1] ?? argumentsText[index + 1];
|
|
59
|
+
if (/^\d+$/.test(megabytes ?? '')) return Number(megabytes) * 1024 * 1024;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
package/src/repl.js
CHANGED
|
@@ -488,8 +488,15 @@ async function solveQuery(engine, state, goal, reader, output) {
|
|
|
488
488
|
let automatic = 0;
|
|
489
489
|
let answersShown = 0;
|
|
490
490
|
let firstAnswer = true;
|
|
491
|
+
let formattingAfterAdvance = false;
|
|
491
492
|
while (!current.result.done) {
|
|
492
493
|
const next = pullSolution(solver, solutions, reader);
|
|
494
|
+
// The control prompt has no trailing space while it waits for input. The
|
|
495
|
+
// first space appears as soon as the user requests another solution and
|
|
496
|
+
// remains visible while pullSolution() computes; the second appears only
|
|
497
|
+
// when the requested leaf answer is ready to format.
|
|
498
|
+
if (formattingAfterAdvance) output.write(' ');
|
|
499
|
+
formattingAfterAdvance = false;
|
|
493
500
|
output.write(current.output);
|
|
494
501
|
output.write(`${firstAnswer ? ' ' : ''}${formatAnswer(engine, state, variables, current.result.value)}`);
|
|
495
502
|
answersShown++;
|
|
@@ -501,14 +508,15 @@ async function solveQuery(engine, state, goal, reader, output) {
|
|
|
501
508
|
|
|
502
509
|
if (automatic > 0 || automatic === Infinity) {
|
|
503
510
|
if (automatic !== Infinity) automatic--;
|
|
504
|
-
output.write('\n;
|
|
511
|
+
output.write('\n; ');
|
|
512
|
+
formattingAfterAdvance = true;
|
|
505
513
|
} else {
|
|
506
514
|
while (true) {
|
|
507
|
-
const controlLine = await reader.readControl('\n;
|
|
515
|
+
const controlLine = await reader.readControl('\n;');
|
|
508
516
|
if (controlLine == null || controlLine === '' || controlLine === '\r' || controlLine === '\n' ||
|
|
509
517
|
controlLine.trimStart().startsWith('.')) {
|
|
510
518
|
if (typeof solutions.return === 'function') solutions.return();
|
|
511
|
-
output.write('... .\n');
|
|
519
|
+
output.write(' ... .\n');
|
|
512
520
|
return null;
|
|
513
521
|
}
|
|
514
522
|
const control = controlLine === ' ' ? ' ' : controlLine.trimStart()[0];
|
|
@@ -527,18 +535,22 @@ async function solveQuery(engine, state, goal, reader, output) {
|
|
|
527
535
|
break;
|
|
528
536
|
}
|
|
529
537
|
if (control === 'w' || control === 'p') {
|
|
530
|
-
output.write(
|
|
538
|
+
output.write(` ${formatAnswer(engine, state, variables, current.result.value)}`);
|
|
531
539
|
continue;
|
|
532
540
|
}
|
|
533
541
|
if (control === 'h') {
|
|
534
542
|
output.write(ANSWER_HELP);
|
|
535
543
|
continue;
|
|
536
544
|
}
|
|
537
|
-
output.write('Action? ');
|
|
545
|
+
output.write(' Action? ');
|
|
538
546
|
}
|
|
547
|
+
output.write(' ');
|
|
548
|
+
formattingAfterAdvance = true;
|
|
539
549
|
}
|
|
540
550
|
|
|
541
551
|
if (next.error) {
|
|
552
|
+
if (formattingAfterAdvance) output.write(' ');
|
|
553
|
+
formattingAfterAdvance = false;
|
|
542
554
|
output.write(next.output);
|
|
543
555
|
if (next.error?.name === 'HaltSignal') return { halted: true, code: next.error.code };
|
|
544
556
|
throw next.error;
|
package/src/solver.js
CHANGED
|
@@ -9,6 +9,7 @@ import { getEyePrologRegistry } from './standard-library.js';
|
|
|
9
9
|
import { selectClauseCandidates, selectClauseCandidatesForValues, selectGroundClauseCandidates } from './program.js';
|
|
10
10
|
import { StreamManager } from './io.js';
|
|
11
11
|
import { clpzStateConsistent } from './clpz.js';
|
|
12
|
+
import { softHeapLimit, usedHeapSize } from './platform.js';
|
|
12
13
|
|
|
13
14
|
let freshCounter = 0;
|
|
14
15
|
|
|
@@ -47,6 +48,8 @@ export class Solver {
|
|
|
47
48
|
this.maxInferences = options.maxInferences ?? Infinity;
|
|
48
49
|
this.inferences = 0;
|
|
49
50
|
this.inferenceLimitExceeded = false;
|
|
51
|
+
this.maxMemoryBytes = options.maxMemoryBytes ?? softHeapLimit();
|
|
52
|
+
this.nextMemoryCheck = 0;
|
|
50
53
|
// Do not impose an implicit answer cap. Infinite and very large searches are
|
|
51
54
|
// part of normal Prolog semantics; callers that need a resource bound can
|
|
52
55
|
// still supply solutionLimit explicitly.
|
|
@@ -115,6 +118,7 @@ export class Solver {
|
|
|
115
118
|
registry: this.registry,
|
|
116
119
|
maxDepth: this.maxDepth,
|
|
117
120
|
maxInferences: this.maxInferences,
|
|
121
|
+
maxMemoryBytes: this.maxMemoryBytes,
|
|
118
122
|
solutionLimit,
|
|
119
123
|
isoStrict: this.isoStrict,
|
|
120
124
|
prologFlags: this.prologFlags,
|
|
@@ -177,6 +181,7 @@ export class Solver {
|
|
|
177
181
|
this.solveStacks.push(stack);
|
|
178
182
|
while (stack.length) {
|
|
179
183
|
this.inferences++;
|
|
184
|
+
this.checkMemoryLimit();
|
|
180
185
|
if (this.inferences > this.maxInferences) {
|
|
181
186
|
this.inferenceLimitExceeded = true;
|
|
182
187
|
break;
|
|
@@ -228,6 +233,7 @@ export class Solver {
|
|
|
228
233
|
|
|
229
234
|
while (true) {
|
|
230
235
|
this.inferences++;
|
|
236
|
+
this.checkMemoryLimit();
|
|
231
237
|
if (this.inferences > this.maxInferences) {
|
|
232
238
|
this.inferenceLimitExceeded = true;
|
|
233
239
|
stack.length = 0;
|
|
@@ -411,6 +417,16 @@ export class Solver {
|
|
|
411
417
|
return activeVariantIn(goal, env, this.active);
|
|
412
418
|
}
|
|
413
419
|
|
|
420
|
+
checkMemoryLimit() {
|
|
421
|
+
if (this.inferences < this.nextMemoryCheck) return;
|
|
422
|
+
this.nextMemoryCheck = this.inferences + 256;
|
|
423
|
+
if (!Number.isFinite(this.maxMemoryBytes)) return;
|
|
424
|
+
const used = usedHeapSize();
|
|
425
|
+
if (used != null && used >= this.maxMemoryBytes) {
|
|
426
|
+
throw new PrologError('resource_error(memory)');
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
414
430
|
*solveUserGoal(goal, rest, env, depth) {
|
|
415
431
|
this.stats.solve_one_goal_calls++;
|
|
416
432
|
if (depth > this.maxDepth) {
|
package/test/run-regression.mjs
CHANGED
|
@@ -790,6 +790,61 @@ c4 ?- call((!;1)).
|
|
|
790
790
|
assertEqual(result.stderr, '', 'stderr');
|
|
791
791
|
},
|
|
792
792
|
},
|
|
793
|
+
{
|
|
794
|
+
name: 'REPL answer prompt distinguishes waiting from computation',
|
|
795
|
+
run: () => {
|
|
796
|
+
const helper = `
|
|
797
|
+
import { spawn } from 'node:child_process';
|
|
798
|
+
|
|
799
|
+
const child = spawn(${JSON.stringify(process.execPath)}, [${JSON.stringify(bin)}], {
|
|
800
|
+
cwd: ${JSON.stringify(packageRoot)},
|
|
801
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
802
|
+
});
|
|
803
|
+
child.stdout.setEncoding('utf8');
|
|
804
|
+
child.stderr.setEncoding('utf8');
|
|
805
|
+
let stdout = '';
|
|
806
|
+
let stderr = '';
|
|
807
|
+
let sawComputingPrompt = false;
|
|
808
|
+
child.stdout.on('data', (text) => {
|
|
809
|
+
stdout += text;
|
|
810
|
+
if (stdout.endsWith('\\n; ')) sawComputingPrompt = true;
|
|
811
|
+
});
|
|
812
|
+
child.stderr.on('data', (text) => { stderr += text; });
|
|
813
|
+
|
|
814
|
+
async function waitFor(predicate, label) {
|
|
815
|
+
const deadline = Date.now() + 5000;
|
|
816
|
+
while (!predicate()) {
|
|
817
|
+
if (Date.now() >= deadline) {
|
|
818
|
+
throw new Error(label + ' timeout; stdout=' + JSON.stringify(stdout) + '; stderr=' + JSON.stringify(stderr));
|
|
819
|
+
}
|
|
820
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
child.stdin.write('use_module(library(prologue)).\\n');
|
|
825
|
+
await waitFor(() => stdout.includes(' true.\\n?- '), 'module import');
|
|
826
|
+
child.stdin.write('(N = 0; N = 1; (call_nth(repeat, 100000), N = 2)).\\n');
|
|
827
|
+
await waitFor(() => stdout.endsWith(' N = 0\\n;'), 'waiting prompt');
|
|
828
|
+
child.stdin.write(';\\n');
|
|
829
|
+
await waitFor(() => sawComputingPrompt, 'computing prompt');
|
|
830
|
+
await waitFor(() => stdout.endsWith('; N = 1\\n;'), 'formatted answer');
|
|
831
|
+
child.stdin.write('\\n');
|
|
832
|
+
await waitFor(() => stdout.endsWith(' ... .\\n?- '), 'stopped enumeration');
|
|
833
|
+
child.stdin.write('halt.\\n');
|
|
834
|
+
const status = await new Promise((resolve) => child.once('exit', resolve));
|
|
835
|
+
if (status !== 0) throw new Error('child status ' + status + '; stderr=' + stderr);
|
|
836
|
+
process.stdout.write('waiting;computing;formatting');
|
|
837
|
+
`;
|
|
838
|
+
const result = spawnSync(process.execPath, [
|
|
839
|
+
'--input-type=module',
|
|
840
|
+
'--eval',
|
|
841
|
+
helper,
|
|
842
|
+
], { cwd: packageRoot, encoding: 'utf8', timeout: 10000 });
|
|
843
|
+
if (result.error) throw result.error;
|
|
844
|
+
assertEqual(result.status, 0, `prompt helper status; stderr=${result.stderr}`);
|
|
845
|
+
assertEqual(result.stdout, 'waiting;computing;formatting', 'prompt state sequence');
|
|
846
|
+
},
|
|
847
|
+
},
|
|
793
848
|
{
|
|
794
849
|
name: 'REPL f stops at five-answer boundaries instead of adding five answers',
|
|
795
850
|
run: () => {
|
|
@@ -2324,6 +2379,35 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
2324
2379
|
}
|
|
2325
2380
|
},
|
|
2326
2381
|
},
|
|
2382
|
+
{
|
|
2383
|
+
name: 'list allocation heap pressure becomes resource_error(memory)',
|
|
2384
|
+
run: () => {
|
|
2385
|
+
const engineUrl = new URL('../src/index.js', import.meta.url).href;
|
|
2386
|
+
const programText = ':- use_module(library(prologue)).\n';
|
|
2387
|
+
const goalText = 'length(_, I), I > 9, N is 2^I, \\+ \\+ length(_, N)';
|
|
2388
|
+
const script = `
|
|
2389
|
+
import { Program, Solver, Env, parseGoalText, getEyePrologRegistry } from ${JSON.stringify(engineUrl)};
|
|
2390
|
+
const program = Program.parse(${JSON.stringify(programText)});
|
|
2391
|
+
const solver = new Solver(program, { registry: getEyePrologRegistry() });
|
|
2392
|
+
const goal = parseGoalText(${JSON.stringify(goalText)}, {
|
|
2393
|
+
operatorDefinitions: [...program.operators.values()],
|
|
2394
|
+
});
|
|
2395
|
+
let caught = null;
|
|
2396
|
+
try { [...solver.solve([goal], new Env(), 0)]; } catch (error) { caught = error; }
|
|
2397
|
+
if (caught?.formal !== 'resource_error(memory)') throw caught ?? new Error('no resource error');
|
|
2398
|
+
process.stdout.write(caught.formal);
|
|
2399
|
+
`;
|
|
2400
|
+
const result = spawnSync(process.execPath, [
|
|
2401
|
+
'--max-old-space-size=64',
|
|
2402
|
+
'--input-type=module',
|
|
2403
|
+
'--eval',
|
|
2404
|
+
script,
|
|
2405
|
+
], { cwd: packageRoot, encoding: 'utf8', timeout: 30000 });
|
|
2406
|
+
if (result.error) throw result.error;
|
|
2407
|
+
assertEqual(result.status, 0, `bounded-heap child status; stderr=${result.stderr}`);
|
|
2408
|
+
assertEqual(result.stdout, 'resource_error(memory)', 'heap pressure resource error');
|
|
2409
|
+
},
|
|
2410
|
+
},
|
|
2327
2411
|
{
|
|
2328
2412
|
name: 'solver honors solution limits',
|
|
2329
2413
|
run: () => {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -1725,6 +1725,8 @@ const result = run(reasoningSource(record), {
|
|
|
1725
1725
|
goal: `thermal_alert(${record.sensor})`,
|
|
1726
1726
|
proof: true,
|
|
1727
1727
|
maxDepth: 10_000,
|
|
1728
|
+
maxInferences: 100_000,
|
|
1729
|
+
maxMemoryBytes: 256 * 1024 * 1024,
|
|
1728
1730
|
solutionLimit: 10
|
|
1729
1731
|
});
|
|
1730
1732
|
|
|
@@ -1774,10 +1776,10 @@ prints numeric work counters; those counters describe this run rather than an
|
|
|
1774
1776
|
additional logical answer.
|
|
1775
1777
|
|
|
1776
1778
|
`run/2` accepts source text or an already parsed `Program`. Its options include
|
|
1777
|
-
`proof` (with `why` and `explain` as aliases), `maxDepth`, `
|
|
1778
|
-
custom `registry`, and `strictNegation` or
|
|
1779
|
-
`stdout`, the solver's numeric `stats`, and a
|
|
1780
|
-
write to the process streams.
|
|
1779
|
+
`proof` (with `why` and `explain` as aliases), `maxDepth`, `maxInferences`,
|
|
1780
|
+
`maxMemoryBytes`, `solutionLimit`, a custom `registry`, and `strictNegation` or
|
|
1781
|
+
`analyzeNegation`. It returns `stdout`, the solver's numeric `stats`, and a
|
|
1782
|
+
nullable `haltCode`; it does not write to the process streams.
|
|
1781
1783
|
|
|
1782
1784
|
For applications that inspect or prepare a theory before running it, use
|
|
1783
1785
|
`Program` directly:
|
|
@@ -1800,18 +1802,24 @@ console.log(path?.recursive, path?.tabled, path?.tableInputPositions);
|
|
|
1800
1802
|
|
|
1801
1803
|
const solver = new Solver(program, {
|
|
1802
1804
|
maxDepth: 50_000,
|
|
1805
|
+
maxInferences: 1_000_000,
|
|
1806
|
+
maxMemoryBytes: 256 * 1024 * 1024,
|
|
1803
1807
|
solutionLimit: 100_000
|
|
1804
1808
|
});
|
|
1805
1809
|
```
|
|
1806
1810
|
|
|
1807
|
-
The limits are safety ceilings, not logical declarations. Reaching
|
|
1808
|
-
truncate search; it does not prove that no
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1811
|
+
The limits are safety ceilings, not logical declarations. Reaching the depth,
|
|
1812
|
+
inference, or solution ceiling may truncate search; it does not prove that no
|
|
1813
|
+
further answer exists. Reaching `maxMemoryBytes` instead raises
|
|
1814
|
+
`resource_error(memory)`, because continuing until the JavaScript engine's hard
|
|
1815
|
+
heap limit would let the host abort before Prolog could report an exception. At
|
|
1816
|
+
the `Solver` API boundary, `solutionLimit` is opt-in: if it is omitted, ordinary
|
|
1817
|
+
solving and child searches that inherit the solver limit do not stop after a
|
|
1818
|
+
fixed number of solutions. This matters for re-executable goals such as
|
|
1819
|
+
`repeat/0` and for library relations such as `call_nth/2`; an implementation
|
|
1820
|
+
safety threshold must not turn a still re-executable search into logical
|
|
1821
|
+
failure. Embedders that need a finite answer budget should pass `solutionLimit`
|
|
1822
|
+
explicitly.
|
|
1815
1823
|
|
|
1816
1824
|
Variable term order is deliberately scoped rather than stored as a permanent
|
|
1817
1825
|
property of a variable. ISO 13211-1 section 7.2.1 leaves the order of two
|
|
@@ -1822,10 +1830,15 @@ sorting step of `setof/3` share one ranking for the duration of that single
|
|
|
1822
1830
|
sorted-list operation. No process-global variable registry or creation ordinal
|
|
1823
1831
|
is retained or exposed through later comparisons.
|
|
1824
1832
|
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
`resource_error(memory)`
|
|
1828
|
-
|
|
1833
|
+
EyeProlog periodically checks detectable JavaScript heap use and keeps a quarter
|
|
1834
|
+
of the host heap ceiling in reserve so the solver can unwind and report
|
|
1835
|
+
`resource_error(memory)` before a fatal host out-of-memory abort. Embedders may
|
|
1836
|
+
replace that automatically derived soft ceiling with `maxMemoryBytes`; setting
|
|
1837
|
+
it to `Infinity` disables the proactive check. Environments that do not expose
|
|
1838
|
+
heap use cannot provide the proactive check. Host capacity failures that V8
|
|
1839
|
+
reports as `Map maximum size exceeded` or `Set maximum size exceeded` are also
|
|
1840
|
+
normalized at the solver boundary instead of leaking a JavaScript `RangeError`.
|
|
1841
|
+
ISO 13211-1 leaves the resource atom implementation dependent. EyeProlog uses
|
|
1829
1842
|
`memory` for a finite host allocation/capacity ceiling and reserves the
|
|
1830
1843
|
`finite_memory` spelling for the distinct convention where no finite amount of
|
|
1831
1844
|
memory could complete the computation.
|
|
@@ -6271,7 +6284,9 @@ When another answer exists in an interactive terminal, press `;`, Space, or
|
|
|
6271
6284
|
enumeration, `a` enumerates all remaining answers, and `f` advances to the
|
|
6272
6285
|
next five-answer boundary (5, 10, 15, ... displayed leaf answers), regardless
|
|
6273
6286
|
of how many answers were stepped through individually beforehand. `h` displays
|
|
6274
|
-
the answer-control help.
|
|
6287
|
+
the answer-control help. The answer prompt is `;` with no trailing space while
|
|
6288
|
+
it waits for input; after an advance command, one space marks active search and
|
|
6289
|
+
a second marks an answer ready for formatting. While a query is actively
|
|
6275
6290
|
computing, EyeProlog releases readline's terminal signal handling: `Ctrl-C`
|
|
6276
6291
|
therefore terminates the current EyeProlog process immediately, and on POSIX
|
|
6277
6292
|
terminals `Ctrl-Z` suspends it in the usual shell-managed way. This remains a
|