eyeprolog 1.2.5 → 1.2.6

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 CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.2.5",
6
+ "version": "1.2.6",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/repl.js CHANGED
@@ -24,7 +24,7 @@ export async function runRepl(engine, options = {}) {
24
24
  let exitCode = 0;
25
25
 
26
26
  try {
27
- state.solver.runInitializations();
27
+ runWithTerminalSignals(reader, () => state.solver.runInitializations());
28
28
  while (true) {
29
29
  const text = await readQuery(reader);
30
30
  if (text == null) break;
@@ -35,7 +35,7 @@ export async function runRepl(engine, options = {}) {
35
35
  if (!options.isoStrict && isUseModuleGoal(goal)) {
36
36
  sources.push({ text: `:- ${text}.\n`, filename: '<repl>' });
37
37
  state = makeState(engine, sources, output, options, state);
38
- state.solver.runInitializations();
38
+ runWithTerminalSignals(reader, () => state.solver.runInitializations());
39
39
  output.write(' true.\n');
40
40
  continue;
41
41
  }
@@ -43,7 +43,7 @@ export async function runRepl(engine, options = {}) {
43
43
  if (consultFiles != null) {
44
44
  for (const filename of consultFiles) sources.push(await readSource(filename));
45
45
  state = makeState(engine, sources, output, options, state);
46
- state.solver.runInitializations();
46
+ runWithTerminalSignals(reader, () => state.solver.runInitializations());
47
47
  output.write(' true.\n');
48
48
  continue;
49
49
  }
@@ -142,12 +142,39 @@ class LineReader {
142
142
  return control;
143
143
  }
144
144
 
145
+ suspendForComputation() {
146
+ if (!this.terminal || !this.readline) return false;
147
+ // Node readline installs terminal signal handling while the interface is
148
+ // open. During a synchronous Prolog search that prevents the terminal's
149
+ // normal SIGINT/SIGTSTP actions from taking effect until JavaScript yields.
150
+ // Close readline while the solver is running so Ctrl-C can terminate and
151
+ // Ctrl-Z can suspend an otherwise non-terminating computation immediately.
152
+ this.history = [...this.readline.history];
153
+ this.readline.close();
154
+ this.readline = null;
155
+ this.lines = null;
156
+ return true;
157
+ }
158
+
159
+ resumeAfterComputation(suspended) {
160
+ if (suspended && !this.readline) this.open();
161
+ }
162
+
145
163
  close() {
146
164
  if (this.input.isRaw) this.input.setRawMode(false);
147
165
  this.readline?.close();
148
166
  }
149
167
  }
150
168
 
169
+ function runWithTerminalSignals(reader, operation) {
170
+ const suspended = reader.suspendForComputation();
171
+ try {
172
+ return operation();
173
+ } finally {
174
+ reader.resumeAfterComputation(suspended);
175
+ }
176
+ }
177
+
151
178
  function makeState(engine, sources, output, options = {}, previousState = null) {
152
179
  const strictIso = options.isoStrict === true;
153
180
  const program = engine.Program.parseSources(sources, { strictIso, sourceMetadata: strictIso });
@@ -369,7 +396,7 @@ async function solveQuery(engine, state, goal, reader, output) {
369
396
  const solver = state.solver;
370
397
  solver.solutionsSeen = 0;
371
398
  const solutions = solver.solve([goal], new engine.Env(), 0);
372
- let current = pullSolution(solver, solutions);
399
+ let current = pullSolution(solver, solutions, reader);
373
400
  if (current.error) {
374
401
  if (current.error?.name === 'HaltSignal') return { halted: true, code: current.error.code };
375
402
  throw current.error;
@@ -383,7 +410,7 @@ async function solveQuery(engine, state, goal, reader, output) {
383
410
  let automatic = 0;
384
411
  let firstAnswer = true;
385
412
  while (!current.result.done) {
386
- const next = pullSolution(solver, solutions);
413
+ const next = pullSolution(solver, solutions, reader);
387
414
  output.write(current.output);
388
415
  output.write(`${firstAnswer ? ' ' : ''}${formatAnswer(engine, state, variables, current.result.value)}`);
389
416
  firstAnswer = false;
@@ -436,16 +463,18 @@ async function solveQuery(engine, state, goal, reader, output) {
436
463
  return null;
437
464
  }
438
465
 
439
- function pullSolution(solver, solutions) {
466
+ function pullSolution(solver, solutions, reader) {
440
467
  const stream = solver.io.resolve('user_output');
441
468
  const originalWrite = stream?.write;
442
469
  let captured = '';
443
470
  if (stream) stream.write = (text) => { captured += String(text); };
471
+ const suspended = reader.suspendForComputation();
444
472
  try {
445
473
  return { result: solutions.next(), output: captured };
446
474
  } catch (error) {
447
475
  return { error, output: captured };
448
476
  } finally {
477
+ reader.resumeAfterComputation(suspended);
449
478
  if (stream) stream.write = originalWrite;
450
479
  }
451
480
  }
@@ -719,6 +719,27 @@ c4 ?- call((!;1)).
719
719
  assertEqual(result.stderr, '', 'stderr');
720
720
  },
721
721
  },
722
+ {
723
+ name: 'REPL releases terminal signals while a query computes',
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 'repeat, fail.\n'; sleep 0.2; printf '\\003'; } | ` +
732
+ `script -qefc ${shellQuote(command)} /dev/null`;
733
+ const result = spawnSync('sh', ['-c', scriptCommand], {
734
+ cwd: packageRoot,
735
+ encoding: 'utf8',
736
+ timeout: 3000,
737
+ });
738
+ assertEqual(result.error?.code, undefined, 'terminal interrupt timeout');
739
+ assertEqual(result.status, 130, 'SIGINT exit status');
740
+ assertIncludes(result.stdout, '?- repeat, fail.', 'terminal query echo');
741
+ },
742
+ },
722
743
  {
723
744
  name: 'REPL accepts multiline period-terminated queries',
724
745
  run: () => {
@@ -3109,6 +3130,10 @@ function between(text, startMarker, endMarker) {
3109
3130
  return text.slice(contentStart, end);
3110
3131
  }
3111
3132
 
3133
+ function shellQuote(value) {
3134
+ return `'${String(value).replaceAll("'", "'\"'\"'")}'`;
3135
+ }
3136
+
3112
3137
  function runCli(args, options = {}) {
3113
3138
  return spawnSync(process.execPath, [bin, ...args], {
3114
3139
  cwd: packageRoot,
@@ -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. A
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