eyeprolog 1.2.39 → 1.2.41

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.39",
6
+ "version": "1.2.41",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/iso.js CHANGED
@@ -1074,8 +1074,16 @@ function* nlBuiltin({ solver, goal, env }) {
1074
1074
  yield env;
1075
1075
  }
1076
1076
 
1077
- function* termTextCandidates(stream) {
1077
+ function activeCharConverter(solver) {
1078
+ if (solver.prologFlags.get('char_conversion')?.value?.name !== 'on' || solver.charConversions.size === 0) {
1079
+ return null;
1080
+ }
1081
+ return (character) => solver.charConversions.get(character) ?? character;
1082
+ }
1083
+
1084
+ function* termTextCandidates(stream, solver) {
1078
1085
  const source = String(stream.content);
1086
+ const convert = activeCharConverter(solver);
1079
1087
  let quote = null, lineComment = false, blockComment = false;
1080
1088
  for (let i = stream.position; i < source.length; i++) {
1081
1089
  const ch = source[i], next = source[i + 1];
@@ -1105,15 +1113,25 @@ function* termTextCandidates(stream) {
1105
1113
  continue;
1106
1114
  }
1107
1115
  if (ch === "'" || ch === '"') { quote = ch; continue; }
1108
- if (ch === '.' && isTerminatingFullStop(source, i)) {
1116
+ if (isTerminatingFullStop(source, i, convert)) {
1109
1117
  yield { text: source.slice(stream.position, i + 1), end: i + 1 };
1110
1118
  }
1111
1119
  }
1112
1120
  }
1113
1121
  function hasNonLayoutRemainder(source, start) {
1114
- return source.slice(start)
1115
- .replace(/[\u0009-\u000d\u0020]+|%[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\//g, '')
1116
- .length > 0;
1122
+ return lastNonLayoutIndex(source, start) >= start;
1123
+ }
1124
+ function lastNonLayoutIndex(source, start = 0) {
1125
+ const ignored = /[\u0009-\u000d\u0020]+|%[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\//g;
1126
+ ignored.lastIndex = start;
1127
+ let cursor = start;
1128
+ let last = -1;
1129
+ for (let match = ignored.exec(source); match != null; match = ignored.exec(source)) {
1130
+ if (match.index > cursor) last = match.index - 1;
1131
+ cursor = match.index + match[0].length;
1132
+ }
1133
+ if (cursor < source.length) last = source.length - 1;
1134
+ return last;
1117
1135
  }
1118
1136
  function convertedTermText(text, solver) {
1119
1137
  if (solver.prologFlags.get('char_conversion')?.value?.name !== 'on' || solver.charConversions.size === 0) return text;
@@ -1207,7 +1225,7 @@ function readTermFromStream(stream, solver) {
1207
1225
  let requestedInteractiveTerm = false;
1208
1226
  while (true) {
1209
1227
  let sawCandidate = false;
1210
- for (const candidate of termTextCandidates(stream)) {
1228
+ for (const candidate of termTextCandidates(stream, solver)) {
1211
1229
  sawCandidate = true;
1212
1230
  if (candidate.lexicalError) {
1213
1231
  stream.position = candidate.end;
package/src/parser.js CHANGED
@@ -378,10 +378,6 @@ class Parser {
378
378
  const line = this.line;
379
379
  const ch = this.peek();
380
380
  if (!ch) return { type: TOK.EOF, text: '', line };
381
- if (this.source.startsWith('...', this.pos) && this.peek(3) !== '.') {
382
- this.pos += 3;
383
- return { type: TOK.ATOM, text: '...', line };
384
- }
385
381
  if (ch === '?' && this.peek(1) === '-' &&
386
382
  !(isGraphicAtomCode(this.peek(2).charCodeAt(0)) && !this.terminatingFullStop(this.pos + 2))) {
387
383
  this.pos += 2;
package/src/repl.js CHANGED
@@ -3,7 +3,7 @@ import fs from 'node:fs/promises';
3
3
  import { readSync } from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { createInterface } from 'node:readline';
6
- import { formalErrorTerm, isCompleteReadTermText } from './iso.js';
6
+ import { formalErrorTerm } from './iso.js';
7
7
  import {
8
8
  characterCodeConstantEnd, continuesGraphicToken, isTerminatingFullStop, quotedEscapeEnd,
9
9
  } from './syntax-scan.js';
@@ -171,8 +171,8 @@ class LineReader {
171
171
  const line = this.readTerminalLineSync();
172
172
  if (line == null) return source.trim() ? source : null;
173
173
  source += `${line}\n`;
174
- const end = terminalFullStop(source);
175
- if (end >= 0 && acceptsReadTermBoundary(source, end, solver)) {
174
+ const end = terminalFullStop(source, solver);
175
+ if (end >= 0) {
176
176
  return source.slice(0, end + 1) + '\n';
177
177
  }
178
178
  prompt = '| ';
@@ -300,7 +300,7 @@ async function prepareInteractiveTermInput(state, goal, reader) {
300
300
  // fallback for piped/non-TTY REPL tests and scripted input.
301
301
  if (reader.canReadTermSynchronously()) return;
302
302
  const stream = interactiveTermInputStream(state, goal);
303
- if (stream == null || terminalFullStop(String(stream.content).slice(stream.position)) >= 0) return;
303
+ if (stream == null || terminalFullStop(String(stream.content).slice(stream.position), state.solver) >= 0) return;
304
304
 
305
305
  const text = await readInteractiveTerm(reader, state.solver);
306
306
  if (text == null) return;
@@ -343,24 +343,23 @@ async function readInteractiveTerm(reader, solver = null) {
343
343
  const line = await reader.read(prompt);
344
344
  if (line == null) return source.trim() ? source : null;
345
345
  source += `${line}\n`;
346
- const end = terminalFullStop(source);
347
- if (end >= 0 && acceptsReadTermBoundary(source, end, solver)) {
346
+ const end = terminalFullStop(source, solver);
347
+ if (end >= 0) {
348
348
  return source.slice(0, end + 1) + '\n';
349
349
  }
350
350
  prompt = '| ';
351
351
  }
352
352
  }
353
353
 
354
- function acceptsReadTermBoundary(source, end, solver) {
355
- // A dot after a graphic character has two possible readings. Stop at once
356
- // when the candidate is already a complete term (for example `./*.`);
357
- // otherwise keep reading so a later end char can make it part of an atom
358
- // (for example the first dot in `!,*.\n.`).
359
- return !continuesGraphicToken(source, end) || solver == null ||
360
- isCompleteReadTermText(source.slice(0, end + 1), solver);
354
+ function activeCharConverter(solver) {
355
+ if (solver?.prologFlags.get('char_conversion')?.value?.name !== 'on' || solver.charConversions.size === 0) {
356
+ return null;
357
+ }
358
+ return (character) => solver.charConversions.get(character) ?? character;
361
359
  }
362
360
 
363
- function terminalFullStop(source) {
361
+ function terminalFullStop(source, solver = null) {
362
+ const convert = activeCharConverter(solver);
364
363
  let quote = null;
365
364
  let lineComment = false;
366
365
  let blockComment = false;
@@ -415,7 +414,7 @@ function terminalFullStop(source) {
415
414
  }
416
415
  if ('([{'.includes(ch)) depth++;
417
416
  else if (')]}'.includes(ch)) depth = Math.max(0, depth - 1);
418
- else if (ch === '.' && depth === 0 && isTerminatingFullStop(source, i) &&
417
+ else if (depth === 0 && isTerminatingFullStop(source, i, convert) &&
419
418
  onlyLayoutAndComments(source.slice(i + 1))) return i;
420
419
  }
421
420
  return -1;
@@ -503,11 +502,15 @@ async function solveQuery(engine, state, goal, reader, output) {
503
502
  if (formattingAfterAdvance) output.write(' ');
504
503
  formattingAfterAdvance = false;
505
504
  output.write(current.output);
506
- output.write(`${firstAnswer ? ' ' : ''}${formatAnswer(engine, state, variables, current.result.value)}`);
505
+ const answer = formatAnswer(engine, state, variables, current.result.value);
506
+ output.write(`${firstAnswer ? ' ' : ''}${answer}`);
507
507
  answersShown++;
508
508
  firstAnswer = false;
509
509
  if (!next.error && next.result.done) {
510
- output.write('.\n');
510
+ // A terminal full stop cannot immediately follow a graphic token: the
511
+ // scanner would absorb it into that token. Insert layout so the printed
512
+ // answer remains valid Prolog text (issue #44).
513
+ output.write(`${continuesGraphicToken(answer, answer.length) ? ' ' : ''}.\n`);
511
514
  return null;
512
515
  }
513
516
 
@@ -4,21 +4,49 @@
4
4
 
5
5
  const graphicTokenCharacters = new Set('#$&*+-./<=>?@^~\\:');
6
6
 
7
- export function continuesGraphicToken(source, index) {
8
- return index > 0 && graphicTokenCharacters.has(source[index - 1]);
7
+ export function continuesGraphicToken(source, index, convert = null) {
8
+ if (index <= 0) return false;
9
+ const rawPrevious = source[index - 1];
10
+ const previous = convert == null ? rawPrevious : convert(rawPrevious);
11
+ // Most full stops follow a non-graphic token. Reject those in O(1) before
12
+ // doing the rarer character-code/comment disambiguation below; otherwise a
13
+ // large source with many term-ending dots degenerates into repeated backward
14
+ // scans (notably multi-megabyte generated data files).
15
+ if (!graphicTokenCharacters.has(previous)) return false;
16
+
17
+ // A graphic-looking character can be the payload or closing escape of a
18
+ // character-code constant rather than a graphic token. For example, the
19
+ // backslash immediately before the full stop in `0'\x41\.` belongs to
20
+ // the number token, so that full stop still terminates the term.
21
+ const apostrophe = source.lastIndexOf("'", index - 1);
22
+ if (apostrophe >= 0 && characterCodeConstantEnd(source, apostrophe) === index - 1) return false;
23
+ // The slash that closes a bracketed comment is layout, not the tail of a
24
+ // graphic token. Distinguish it from spellings such as `//*.*/`, where the
25
+ // apparent /* is itself embedded in a graphic token and therefore never
26
+ // opens a comment.
27
+ if (source[index - 1] === '/' && source[index - 2] === '*') {
28
+ for (let open = source.lastIndexOf('/*', index - 3); open >= 0;
29
+ open = source.lastIndexOf('/*', open - 1)) {
30
+ if (open === 0 || !graphicTokenCharacters.has(source[open - 1])) return false;
31
+ }
32
+ }
33
+ return true;
9
34
  }
10
35
 
11
- export function isTerminatingFullStop(source, index) {
12
- if (source[index] !== '.') return false;
13
- const next = source[index + 1] ?? '';
14
- // At a line boundary, after a single-line comment marker, or at end of
15
- // input, the dot is the read-term end char. Before horizontal layout it is
16
- // instead part of an already-started graphic token: `./*. .` is the atom
17
- // `./*.` followed by its separate end char. A directly following /* also
18
- // stays in the current graphic token; bracketed comments are recognized
19
- // only when /* begins a token.
36
+ export function isTerminatingFullStop(source, index, convert = null) {
37
+ const current = convert == null ? source[index] : convert(source[index]);
38
+ if (current !== '.') return false;
39
+ const rawNext = source[index + 1] ?? '';
40
+ const next = convert == null ? rawNext : convert(rawNext);
41
+ // A full stop cannot terminate a term when it can still extend the graphic
42
+ // token immediately before it. This remains true at a line boundary and at
43
+ // the current end of interactive input: `*.\n` is the graphic token `*.`
44
+ // followed by layout, so read/1 must keep waiting for a separate end char.
45
+ // Conversely `!.\n` terminates because ! is a solo token, not a graphic
46
+ // token character accepted by continuesGraphicToken().
47
+ if (continuesGraphicToken(source, index, convert)) return false;
20
48
  if (next === '' || next === '%' || next === '\n' || next === '\r') return true;
21
- if (/^[\u0009\u000b\u000c\u0020]$/.test(next)) return !continuesGraphicToken(source, index);
49
+ if (/^[\u0009\u000b\u000c\u0020]$/.test(next)) return true;
22
50
  return false;
23
51
  }
24
52
 
package/src/write.js CHANGED
@@ -4,8 +4,8 @@ import {
4
4
  Env, deref, isCons, isEmptyList,
5
5
  } from './term.js';
6
6
 
7
- const graphicAtomCharacters = new Set('!#$&*+-/<=>?@^~\\'.split(''));
8
- const dottedGraphicAtomCharacters = new Set([...graphicAtomCharacters, '.']);
7
+ const graphicAtomCharacters = new Set('!#$&*+-./<=>?@^~\\'.split(''));
8
+ const dottedGraphicAtomCharacters = graphicAtomCharacters;
9
9
  const compactInfixOperators = new Set([':', '..']);
10
10
 
11
11
  function quotedControlEscape(ch) {
@@ -28,7 +28,11 @@ function quotedControlEscape(ch) {
28
28
  function atomNeedsQuotes(name) {
29
29
  if (!name) return true;
30
30
  if (name === '[]' || name === '{}') return false;
31
- if (name === '...') return false;
31
+ // A lone full stop is the end token, not a graphic atom. Longer
32
+ // graphic tokens may contain dots and are valid unquoted writeq/1 output
33
+ // (WG17 #371-373: ./*, .*, ...*). Only a token beginning with /* would
34
+ // be read as a bracketed comment and therefore still requires quoting.
35
+ if (name === '.') return true;
32
36
  if (name.startsWith('/*')) return true;
33
37
  if (/^[a-z][A-Za-z0-9_]*$/.test(name)) return false;
34
38
  for (const ch of name) if (!graphicAtomCharacters.has(ch)) return true;
@@ -1 +1 @@
1
- parse line 1: bad term
1
+ parse line 1: expected ., got :-.
@@ -1 +1 @@
1
- parse line 1: expected ., got .
1
+ parse line 1: expected ., got ..
@@ -671,7 +671,7 @@
671
671
  "type": "success",
672
672
  "stages": [
673
673
  {
674
- "output": "'//*.*/'",
674
+ "output": "//*.*/",
675
675
  "variables": "[]"
676
676
  }
677
677
  ]
@@ -4302,7 +4302,7 @@
4302
4302
  "variables": "[]"
4303
4303
  },
4304
4304
  {
4305
- "output": ">('>.'(a),b)",
4305
+ "output": ">(>.(a),b)",
4306
4306
  "variables": "[]"
4307
4307
  }
4308
4308
  ]
@@ -4322,7 +4322,7 @@
4322
4322
  "variables": "[]"
4323
4323
  },
4324
4324
  {
4325
- "output": "=('>.'(a),b)",
4325
+ "output": "=(>.(a),b)",
4326
4326
  "variables": "[]"
4327
4327
  }
4328
4328
  ]
@@ -4342,7 +4342,7 @@
4342
4342
  "variables": "[]"
4343
4343
  },
4344
4344
  {
4345
- "output": "','('>.'(a),b)",
4345
+ "output": "','(>.(a),b)",
4346
4346
  "variables": "[]"
4347
4347
  }
4348
4348
  ]
@@ -4362,7 +4362,7 @@
4362
4362
  "variables": "[]"
4363
4363
  },
4364
4364
  {
4365
- "output": "'>.'(a)",
4365
+ "output": ">.(a)",
4366
4366
  "variables": "[]"
4367
4367
  }
4368
4368
  ]
@@ -5022,7 +5022,7 @@
5022
5022
  "type": "success",
5023
5023
  "stages": [
5024
5024
  {
5025
- "output": "'.+'",
5025
+ "output": ".+",
5026
5026
  "variables": "[]"
5027
5027
  }
5028
5028
  ]
@@ -5273,7 +5273,7 @@
5273
5273
  "variables": "[]"
5274
5274
  },
5275
5275
  {
5276
- "output": "'.>'('.>'(a))",
5276
+ "output": ".>(.>(a))",
5277
5277
  "variables": "[]"
5278
5278
  }
5279
5279
  ]
@@ -102,6 +102,16 @@ export function runRegression(reporter = new TestReporter()) {
102
102
 
103
103
  function regressionCases() {
104
104
  return [
105
+ {
106
+ name: 'large source scanning avoids quadratic full-stop lookback',
107
+ run: () => {
108
+ const result = runCli(['examples/path-discovery.pl'], { timeout: 10000 });
109
+ if (result.error) throw new Error(`path-discovery timed out or failed to launch: ${result.error.message}`);
110
+ assertEqual(result.status, 0, `path-discovery status; stderr=${result.stderr}`);
111
+ assertIncludes(result.stdout, "airroute('Ostend-Bruges International Airport', 'Václav Havel Airport Prague'",
112
+ 'path-discovery result');
113
+ },
114
+ },
105
115
  {
106
116
  name: '--proof rule fact explanation output',
107
117
  run: () => runWhy({
@@ -733,24 +743,77 @@ c4 ?- call((!;1)).
733
743
  assertIncludes(upstreamResult.stdout, '46 true.', 'WG17 #367 output');
734
744
  },
735
745
  },
746
+ {
747
+ name: 'top level separates terminal full stop from graphic answers (issue #44)',
748
+ run: () => {
749
+ const result = runCli([], { input: 'X = .* .\nhalt.\n' });
750
+ assertEqual(result.status, 0, 'issue #44 exit status');
751
+ assertIncludes(result.stdout, 'X = .* .\n', 'graphic binding is separated from terminal full stop');
752
+ assertNotIncludes(result.stdout, 'X = .*.\n', 'terminal full stop is not absorbed into graphic atom');
753
+ assertEqual(result.stderr, '', 'issue #44 stderr');
754
+ },
755
+ },
756
+ {
757
+ name: 'writeq leaves ISO dotted graphic atoms unquoted (WG17 #371-373)',
758
+ run: () => {
759
+ const result = runCli([], {
760
+ input: 'writeq(./*).\nwriteq(.*).\nwriteq(...*).\nhalt.\n',
761
+ });
762
+ assertEqual(result.status, 0, 'dotted graphic writeq exit status');
763
+ assertIncludes(result.stdout, ' ./* true.\n', 'writeq ./* is unquoted');
764
+ assertIncludes(result.stdout, ' .* true.\n', 'writeq .* is unquoted');
765
+ assertIncludes(result.stdout, ' ...* true.\n', 'writeq ...* is unquoted');
766
+ assertNotIncludes(result.stdout, "'./*'", 'writeq ./* has no quotes');
767
+ assertNotIncludes(result.stdout, "'.*'", 'writeq .* has no quotes');
768
+ assertNotIncludes(result.stdout, "'...*'", 'writeq ...* has no quotes');
769
+ },
770
+ },
736
771
  {
737
772
  name: 'readers distinguish graphic tokens, comments, and full stops (issue #41)',
738
773
  run: () => {
739
- const parsed = parseProgramText('./*.');
774
+ const parsed = parseProgramText('./* .');
740
775
  assertEqual(parsed.length, 1, 'graphic atom clause count');
741
776
  assertEqual(parsed[0].head.name, './*', 'comment opener stays inside graphic atom');
742
777
 
778
+ // A dot immediately after a graphic token belongs to that maximal
779
+ // token. A separate end char is therefore required even at a line
780
+ // boundary; this is the waiting behavior called out in WG17 #370-373.
781
+ const waitProgram = Program.parse('');
782
+ const waitSolver = new Solver(waitProgram, {
783
+ registry: getEyePrologRegistry(),
784
+ ioOptions: { input: '*.\n' },
785
+ });
786
+ const waitStream = waitSolver.io.resolve('user_input');
787
+ let refillRequests = 0;
788
+ waitStream.interactiveReadTerm = () => {
789
+ refillRequests++;
790
+ return '.\n';
791
+ };
792
+ const waitGoal = parseGoalText('read(T)', {
793
+ operatorDefinitions: [...waitProgram.operators.values()],
794
+ });
795
+ const waitAnswers = [...waitSolver.solve([waitGoal], new Env(), 0)];
796
+ assertEqual(waitAnswers.length, 1, 'graphic token read answer after refill');
797
+ assertEqual(refillRequests, 1, 'graphic token boundary waits for a separate end char');
798
+ assertEqual(copyResolved(waitGoal.args[0], waitAnswers[0]).name, '*.', 'maximal graphic token after refill');
799
+
743
800
  const read = runEyeProlog('', {
744
801
  goal: 'read(T)',
745
- ioOptions: { input: './*.' },
802
+ ioOptions: { input: './*. .' },
803
+ });
804
+ assertEqual(read.stdout, "read(./*.).\n", 'dotted graphic atom writeq readback');
805
+
806
+ const ellipsisGraphic = runEyeProlog('', {
807
+ goal: 'read(T)',
808
+ ioOptions: { input: '...*\n.\n' },
746
809
  });
747
- assertEqual(read.stdout, "read('./*').\n", 'graphic atom writeq readback');
810
+ assertEqual(ellipsisGraphic.stdout, "read(...*).\n", 'ellipsis prefix remains inside maximal graphic atom');
748
811
 
749
812
  const consecutive = runEyeProlog('answer(A, B) :- read(A), read(B).\n', {
750
813
  goal: 'answer(A, B)',
751
814
  ioOptions: { input: './*. .\nok.\n' },
752
815
  });
753
- assertEqual(consecutive.stdout, "answer('./*.', ok).\n", 'following read starts after the complete term');
816
+ assertEqual(consecutive.stdout, "answer(./*., ok).\n", 'following read starts after the complete term');
754
817
 
755
818
  // A possible full stop can fail to complete the term while still
756
819
  // extending a current graphic operator into an ordinary atom. The
@@ -777,6 +840,37 @@ c4 ?- call((!;1)).
777
840
  assertEqual(term.args[1].name, `${name}.`, `graphic operator atom for ${name}`);
778
841
  }
779
842
 
843
+ // Unlike an interactive reader waiting at a line boundary, a buffered
844
+ // stream can see later non-layout input. Its first dot therefore
845
+ // remains in the maximal graphic token, making the adjacent ! invalid
846
+ // rather than prematurely returning the shorter atom.
847
+ for (const name of [...graphicOperators, '?', '#', '@', './*', '//*']) {
848
+ let bufferedError = null;
849
+ try {
850
+ runEyeProlog('', {
851
+ goal: 'read_term(T, [])',
852
+ ioOptions: { input: `${name}.\n!\n.` },
853
+ });
854
+ } catch (caught) {
855
+ bufferedError = caught;
856
+ }
857
+ assertEqual(
858
+ bufferedError?.message,
859
+ 'error(syntax_error(read_term))',
860
+ `buffered graphic atom boundary for ${name}`,
861
+ );
862
+ }
863
+
864
+ const bufferedPath = path.join(tmp, 'graphic-atom-boundary.pl');
865
+ fs.writeFileSync(bufferedPath, '*.\n!\n.');
866
+ const namedStream = runEyeProlog([
867
+ `caught(ok) :- open(${sourceAtom(bufferedPath)}, read, S),`,
868
+ ' catch(read_term(S, _, []), error(syntax_error(read_term), _), true),',
869
+ ' close(S).',
870
+ '',
871
+ ].join('\n'), { goal: 'caught(ok)' });
872
+ assertEqual(namedStream.stdout, 'caught(ok).\n', 'named buffered stream syntax error');
873
+
780
874
  let error = null;
781
875
  try {
782
876
  runEyeProlog('', { goal: 'read(T)', ioOptions: { input: '!.!.' } });
@@ -789,7 +883,7 @@ c4 ?- call((!;1)).
789
883
  input: 'read(T).\n./*. .\nread(T).\nok.\nread(T).\n!.!.\nhalt.\n',
790
884
  });
791
885
  assertEqual(repl.status, 0, 'REPL exit status');
792
- assertIncludes(repl.stdout, 'T = ./*..', 'REPL dotted graphic atom answer');
886
+ assertIncludes(repl.stdout, 'T = ./*. .', 'REPL dotted graphic atom answer');
793
887
  assertNotIncludes(repl.stdout, "T = './*.'", 'REPL dotted graphic atom has no spurious quotes');
794
888
  assertIncludes(repl.stdout, 'T = ok.', 'REPL following read answer');
795
889
  assertIncludes(repl.stdout, 'error(syntax_error(read_term), eyeprolog)', 'REPL syntax error');
@@ -1531,10 +1625,10 @@ c4 ?- call((!;1)).
1531
1625
  if (available.status !== 0) return;
1532
1626
  const command = `${shellQuote(process.execPath)} ${shellQuote(bin)}`;
1533
1627
  const scriptCommand =
1534
- `{ printf 'read(X), read(Y).\n'; sleep 0.15; ` +
1535
- `printf 'foo.\n'; sleep 0.15; printf 'bar.\n'; sleep 0.15; ` +
1536
- `printf 'read(Z).\n'; sleep 0.15; printf '\\004'; sleep 0.15; ` +
1537
- `printf 'true.\n'; sleep 0.15; printf 'halt.\n'; } | ` +
1628
+ `{ printf 'read(X), read(Y).\n'; sleep 0.3; ` +
1629
+ `printf 'foo.\n'; sleep 0.3; printf 'bar.\n'; sleep 0.3; ` +
1630
+ `printf 'read(Z).\n'; sleep 0.3; printf '\\004'; sleep 0.3; ` +
1631
+ `printf 'true.\n'; sleep 0.3; printf 'halt.\n'; } | ` +
1538
1632
  `script -qefc ${shellQuote(command)} /dev/null`;
1539
1633
  const result = spawnSync('sh', ['-c', scriptCommand], {
1540
1634
  cwd: packageRoot,
@@ -4338,6 +4432,7 @@ function runCli(args, options = {}) {
4338
4432
  encoding: 'utf8',
4339
4433
  env: options.env ? { ...process.env, ...options.env } : process.env,
4340
4434
  input: options.input ?? undefined,
4435
+ timeout: options.timeout ?? undefined,
4341
4436
  });
4342
4437
  }
4343
4438
 
@@ -5253,10 +5253,12 @@ write_event(Path, Event) :-
5253
5253
 
5254
5254
  The period is essential when another Prolog processor will read the result as
5255
5255
  a term. `write/1-2` uses readable conventional syntax, `writeq/1-2` quotes
5256
- where needed, and `write_canonical/1-2` exposes canonical structure. ISO term
5257
- output uses only the separator characters needed by the syntax, so functional
5258
- arguments and list elements are emitted compactly; for example `writeq([a,b])`
5259
- outputs `[a,b]`.
5256
+ where needed, and `write_canonical/1-2` exposes canonical structure. Dotted
5257
+ graphic atoms do not need quotes merely because they contain a period:
5258
+ `writeq(./*)`, `writeq(.*)`, and `writeq(...*)` output `./*`, `.*`, and `...*`
5259
+ respectively. ISO term output uses only the separator characters needed by the
5260
+ syntax, so functional arguments and list elements are emitted compactly; for
5261
+ example `writeq([a,b])` outputs `[a,b]`.
5260
5262
  `write_term/2-3` supports `quoted/1`, `ignore_ops/1`, `numbervars/1`, and
5261
5263
  `variable_names/1`.
5262
5264
 
@@ -5377,10 +5379,12 @@ quote an atom whose name itself contains a colon. Unquoted angle-bracket IRIs
5377
5379
  are not syntax.
5378
5380
 
5379
5381
  A `/*` sequence opens a block comment only when it begins a token; inside a
5380
- maximal graphic token the slash and star remain atom characters. A period ends
5381
- a term only when it is recognized as the terminating full stop. Consequently,
5382
- `./*.` at the end of a line reads the atom `./*`, whereas `./*. .` reads the
5383
- atom `./*.` and consumes the second period as the terminator.
5382
+ maximal graphic token the slash and star remain atom characters. Graphic tokens
5383
+ are formed maximally before a period can be recognized as the terminating full
5384
+ stop. Consequently, interactive input `*.` or `./*.` is not yet a complete term:
5385
+ the period is part of the graphic atom and the reader waits for a separate
5386
+ terminating full stop. Thus `./*. .` reads the atom `./*.` and consumes the
5387
+ second period as the terminator.
5384
5388
 
5385
5389
  In the grammar below, `{ x }` means zero or more repetitions of `x`, `[ x ]`
5386
5390
  means that `x` is optional, and parentheses group alternatives. These marks
@@ -6344,8 +6348,10 @@ period-terminated query with no solutions prints `false.`; a solution without
6344
6348
  visible variable bindings prints `true.`. Answer substitutions are rendered as
6345
6349
  valid Prolog syntax under the current operator table: when a bound value would
6346
6350
  not be a valid right operand of the displayed `=/2`, EyeProlog adds parentheses,
6347
- for example `T = (a = b).` rather than the invalid `T = a = b.`. Use `[file].`
6348
- or `['file.pl'].` to
6351
+ for example `T = (a = b).` rather than the invalid `T = a = b.`. When an answer
6352
+ ends in a graphic token, the top level inserts layout before its terminating
6353
+ full stop so the two tokens cannot merge; for example `?- X = .* .` displays
6354
+ `X = .* .`, not `X = .*.`. Use `[file].` or `['file.pl'].` to
6349
6355
  consult local source, and `halt.` or `halt(Status).` to leave the top level.
6350
6356
  When `read/1-2` or `read_term/2-3` actually reaches interactive
6351
6357
  `user_input`, the top level requests the next full-stop-terminated Prolog term