eyeprolog 1.2.19 → 1.2.21

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.19",
6
+ "version": "1.2.21",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/parser.js CHANGED
@@ -436,7 +436,16 @@ class Parser {
436
436
  if (!value || (value !== ' ' && isWhitespaceCode(value.charCodeAt(0)))) {
437
437
  throw new Error(`parse line ${line}: bad character code constant`);
438
438
  }
439
- if (value === '\\') value = this.readEscape(line, { allowContinuation: false });
439
+ if (value === "'") {
440
+ // In the single-quoted-character notation used after 0', an
441
+ // apostrophe is doubled just as it is inside a quoted atom. Thus
442
+ // 0''' is one numeric token denoting character code 39, while the
443
+ // undoubled 0'' is not a complete single quoted character.
444
+ if (this.peek() !== "'") throw new Error(`parse line ${line}: bad character code constant`);
445
+ this.take();
446
+ } else if (value === '\\') {
447
+ value = this.readEscape(line, { allowContinuation: false });
448
+ }
440
449
  const code = value.codePointAt(0);
441
450
  return { type: TOK.NUMBER, text: String(negative ? -code : code), line };
442
451
  }
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(`${formatAnswer(engine, state, variables, current.result.value)}`);
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;
@@ -538,6 +538,35 @@ c4 ?- call((!;1)).
538
538
  }
539
539
  },
540
540
  },
541
+ {
542
+ name: 'apostrophe character-code constants parse in source and number conversion',
543
+ run: () => {
544
+ const source = String.raw`
545
+ ?- N = 0'''.
546
+ N = 39.
547
+ ?- number_chars(N,"0'''").
548
+ N = 39.
549
+ ?- number_chars(N,"0'\\'").
550
+ N = 39.
551
+ ?- number_codes(N,[48,39,39,39]).
552
+ N = 39.
553
+ `;
554
+ const result = publicApi.runQuads(source);
555
+ assertEqual(result.total, 4, 'quad total');
556
+ assertEqual(result.passed, 4, 'quad passed');
557
+ assertEqual(result.stdout, 'quads: 4 run, 4 passed, 0 failed.\n', 'quad report');
558
+
559
+ for (const goal of ["N = 0''", 'number_chars(N,"0\'\'")']) {
560
+ let caught = null;
561
+ try {
562
+ publicApi.run('', { goal });
563
+ } catch (error) {
564
+ caught = error;
565
+ }
566
+ if (caught == null) throw new Error(`${goal} should reject an undoubled apostrophe`);
567
+ }
568
+ },
569
+ },
541
570
  {
542
571
  name: 'number conversion rejects parenthesized numeric terms',
543
572
  run: () => {
@@ -790,6 +819,61 @@ c4 ?- call((!;1)).
790
819
  assertEqual(result.stderr, '', 'stderr');
791
820
  },
792
821
  },
822
+ {
823
+ name: 'REPL answer prompt distinguishes waiting from computation',
824
+ run: () => {
825
+ const helper = `
826
+ import { spawn } from 'node:child_process';
827
+
828
+ const child = spawn(${JSON.stringify(process.execPath)}, [${JSON.stringify(bin)}], {
829
+ cwd: ${JSON.stringify(packageRoot)},
830
+ stdio: ['pipe', 'pipe', 'pipe'],
831
+ });
832
+ child.stdout.setEncoding('utf8');
833
+ child.stderr.setEncoding('utf8');
834
+ let stdout = '';
835
+ let stderr = '';
836
+ let sawComputingPrompt = false;
837
+ child.stdout.on('data', (text) => {
838
+ stdout += text;
839
+ if (stdout.endsWith('\\n; ')) sawComputingPrompt = true;
840
+ });
841
+ child.stderr.on('data', (text) => { stderr += text; });
842
+
843
+ async function waitFor(predicate, label) {
844
+ const deadline = Date.now() + 5000;
845
+ while (!predicate()) {
846
+ if (Date.now() >= deadline) {
847
+ throw new Error(label + ' timeout; stdout=' + JSON.stringify(stdout) + '; stderr=' + JSON.stringify(stderr));
848
+ }
849
+ await new Promise((resolve) => setTimeout(resolve, 10));
850
+ }
851
+ }
852
+
853
+ child.stdin.write('use_module(library(prologue)).\\n');
854
+ await waitFor(() => stdout.includes(' true.\\n?- '), 'module import');
855
+ child.stdin.write('(N = 0; N = 1; (call_nth(repeat, 100000), N = 2)).\\n');
856
+ await waitFor(() => stdout.endsWith(' N = 0\\n;'), 'waiting prompt');
857
+ child.stdin.write(';\\n');
858
+ await waitFor(() => sawComputingPrompt, 'computing prompt');
859
+ await waitFor(() => stdout.endsWith('; N = 1\\n;'), 'formatted answer');
860
+ child.stdin.write('\\n');
861
+ await waitFor(() => stdout.endsWith(' ... .\\n?- '), 'stopped enumeration');
862
+ child.stdin.write('halt.\\n');
863
+ const status = await new Promise((resolve) => child.once('exit', resolve));
864
+ if (status !== 0) throw new Error('child status ' + status + '; stderr=' + stderr);
865
+ process.stdout.write('waiting;computing;formatting');
866
+ `;
867
+ const result = spawnSync(process.execPath, [
868
+ '--input-type=module',
869
+ '--eval',
870
+ helper,
871
+ ], { cwd: packageRoot, encoding: 'utf8', timeout: 10000 });
872
+ if (result.error) throw result.error;
873
+ assertEqual(result.status, 0, `prompt helper status; stderr=${result.stderr}`);
874
+ assertEqual(result.stdout, 'waiting;computing;formatting', 'prompt state sequence');
875
+ },
876
+ },
793
877
  {
794
878
  name: 'REPL f stops at five-answer boundaries instead of adding five answers',
795
879
  run: () => {
@@ -5776,8 +5776,9 @@ minus token and the following numeric token. A single-line `%...` comment may
5776
5776
  therefore follow `-` directly because `%` cannot continue a graphic token; an
5777
5777
  adjacent bracketed comment in `-/**/1` remains a syntax error under the eager
5778
5778
  token-consumer rule. Decimal fractions and decimal exponents are supported;
5779
- trailing material and non-finite values are rejected. The regression gate
5780
- vendors all 74 numbered cases from Ulrich Neumerkel's contemporary
5779
+ the apostrophe character code is written with a doubled apostrophe as `0'''`
5780
+ and has value 39. Trailing material and non-finite values are rejected. The
5781
+ regression gate vendors all 74 numbered cases from Ulrich Neumerkel's contemporary
5781
5782
  `number_chars/2` comparison, including the Cor.2 error-precedence cases;
5782
5783
  `number_codes/2` shares the same numeric parser and has mirrored coverage for
5783
5784
  the recent numeric-syntax regressions.
@@ -6284,7 +6285,9 @@ When another answer exists in an interactive terminal, press `;`, Space, or
6284
6285
  enumeration, `a` enumerates all remaining answers, and `f` advances to the
6285
6286
  next five-answer boundary (5, 10, 15, ... displayed leaf answers), regardless
6286
6287
  of how many answers were stepped through individually beforehand. `h` displays
6287
- the answer-control help. While a query is actively
6288
+ the answer-control help. The answer prompt is `;` with no trailing space while
6289
+ it waits for input; after an advance command, one space marks active search and
6290
+ a second marks an answer ready for formatting. While a query is actively
6288
6291
  computing, EyeProlog releases readline's terminal signal handling: `Ctrl-C`
6289
6292
  therefore terminates the current EyeProlog process immediately, and on POSIX
6290
6293
  terminals `Ctrl-Z` suspends it in the usual shell-managed way. This remains a