eyeprolog 1.2.35 → 1.2.36

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.35",
6
+ "version": "1.2.36",
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
@@ -9,7 +9,9 @@ import { sameNumberValue } from './number-value.js';
9
9
  import { createParserOperatorState, parseClauses, parseGoalText, parseNumberTokenText } from './parser.js';
10
10
  import { formatTermForWrite } from './write.js';
11
11
  import { emptyTerminalSequence, expandDcgBody, isListOrPartialList, validateDcgEmbeddedGoals } from './dcg.js';
12
- import { characterCodeConstantEnd, quotedEscapeEnd } from './syntax-scan.js';
12
+ import {
13
+ characterCodeConstantEnd, continuesGraphicToken, isTerminatingFullStop, quotedEscapeEnd,
14
+ } from './syntax-scan.js';
13
15
 
14
16
  let isoFresh = 0;
15
17
 
@@ -1072,20 +1074,6 @@ function* nlBuiltin({ solver, goal, env }) {
1072
1074
  yield env;
1073
1075
  }
1074
1076
 
1075
- function isTerminatingFullStop(source, index) {
1076
- const previous = source[index - 1] ?? '';
1077
- const next = source[index + 1] ?? '';
1078
- if (previous === '.' || next === '.') return false;
1079
- if (/\d/.test(previous) && /\d/.test(next)) return false;
1080
- if (/[A-Za-z0-9_]/.test(previous) && /[A-Za-z0-9_]/.test(next)) return false;
1081
- return true;
1082
- }
1083
-
1084
- const termGraphicCharacters = new Set('#$&*+-./<=>?@^~\\:');
1085
- function continuesGraphicToken(source, index) {
1086
- return index > 0 && termGraphicCharacters.has(source[index - 1]);
1087
- }
1088
-
1089
1077
  function* termTextCandidates(stream) {
1090
1078
  const source = String(stream.content);
1091
1079
  let quote = null, lineComment = false, blockComment = false;
package/src/parser.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Tokenizer and recursive-descent parser for the EyeProlog source language.
2
2
  // It preserves the compact Prolog-like syntax while producing Term objects for the solver.
3
3
  import { ATOM, COMPOUND, atom, compound, cons, emptyList, numberTerm, variable } from './term.js';
4
+ import { isTerminatingFullStop } from './syntax-scan.js';
4
5
 
5
6
  const TOK = {
6
7
  EOF: 'eof', ATOM: 'atom', VAR: 'var', STRING: 'string', NUMBER: 'number',
@@ -369,12 +370,11 @@ class Parser {
369
370
  this.pos += 2;
370
371
  return { type: TOK.ATOM, text: '?-', line };
371
372
  }
372
- if (ch === '.' && this.peek(1) &&
373
- !isWhitespaceCode(this.peek(1).charCodeAt(0)) &&
374
- this.peek(1) !== '%' && !(this.peek(1) === '/' && this.peek(2) === '*')) {
373
+ if (ch === '.' && !isTerminatingFullStop(this.source, this.pos)) {
375
374
  const start = this.pos;
376
375
  this.take();
377
- while (isGraphicAtomCode(this.peek().charCodeAt(0))) this.take();
376
+ while (isGraphicAtomCode(this.peek().charCodeAt(0)) &&
377
+ !isTerminatingFullStop(this.source, this.pos)) this.take();
378
378
  return { type: TOK.ATOM, text: this.source.slice(start, this.pos), line };
379
379
  }
380
380
  if (ch === '!') {
@@ -543,7 +543,8 @@ class Parser {
543
543
  if (isGraphicAtomCode(ch.charCodeAt(0))) {
544
544
  const start = this.pos;
545
545
  this.take();
546
- while (isGraphicAtomCode(this.peek().charCodeAt(0))) this.take();
546
+ while (isGraphicAtomCode(this.peek().charCodeAt(0)) &&
547
+ !isTerminatingFullStop(this.source, this.pos)) this.take();
547
548
  return { type: TOK.ATOM, text: this.source.slice(start, this.pos), line };
548
549
  }
549
550
 
package/src/repl.js CHANGED
@@ -4,7 +4,9 @@ import { readSync } from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { createInterface } from 'node:readline';
6
6
  import { formalErrorTerm } from './iso.js';
7
- import { characterCodeConstantEnd, quotedEscapeEnd } from './syntax-scan.js';
7
+ import {
8
+ characterCodeConstantEnd, continuesGraphicToken, isTerminatingFullStop, quotedEscapeEnd,
9
+ } from './syntax-scan.js';
8
10
 
9
11
  const ANSWER_HELP = `
10
12
  SPACE, "n" or ";": next solution, if any
@@ -389,7 +391,7 @@ function terminalFullStop(source) {
389
391
  lineComment = true;
390
392
  continue;
391
393
  }
392
- if (ch === '/' && next === '*') {
394
+ if (ch === '/' && next === '*' && !continuesGraphicToken(source, i)) {
393
395
  blockComment = true;
394
396
  i++;
395
397
  continue;
@@ -400,7 +402,8 @@ function terminalFullStop(source) {
400
402
  }
401
403
  if ('([{'.includes(ch)) depth++;
402
404
  else if (')]}'.includes(ch)) depth = Math.max(0, depth - 1);
403
- else if (ch === '.' && depth === 0 && onlyLayoutAndComments(source.slice(i + 1))) return i;
405
+ else if (ch === '.' && depth === 0 && isTerminatingFullStop(source, i) &&
406
+ onlyLayoutAndComments(source.slice(i + 1))) return i;
404
407
  }
405
408
  return -1;
406
409
  }
@@ -2,6 +2,26 @@
2
2
  // readers. These locate token boundaries only; parser.js remains responsible
3
3
  // for accepting or rejecting the token itself.
4
4
 
5
+ const graphicTokenCharacters = new Set('#$&*+-./<=>?@^~\\:');
6
+
7
+ export function continuesGraphicToken(source, index) {
8
+ return index > 0 && graphicTokenCharacters.has(source[index - 1]);
9
+ }
10
+
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.
20
+ if (next === '' || next === '%' || next === '\n' || next === '\r') return true;
21
+ if (/^[\u0009\u000b\u000c\u0020]$/.test(next)) return !continuesGraphicToken(source, index);
22
+ return false;
23
+ }
24
+
5
25
  export function quotedEscapeEnd(source, index) {
6
26
  const escaped = source[index + 1] ?? '';
7
27
  if (!escaped) return index;
@@ -5,7 +5,7 @@ text_roundtrip(Term, Peek, Code, Mode, Alias) :-
5
5
  open('/tmp/eyeprolog-iso-text.txt', write, Output, [alias(iso_text_output), type(text)]),
6
6
  writeq(iso_text_output, sample(42)),
7
7
  put_char(iso_text_output, '.'),
8
- put_char(iso_text_output, 'Z'),
8
+ put_char(iso_text_output, ' '),
9
9
  close(Output),
10
10
  open('/tmp/eyeprolog-iso-text.txt', read, Input, [alias(iso_text_input), eof_action(eof_code)]),
11
11
  stream_property(Input, mode(Mode)),
@@ -1,4 +1,4 @@
1
- text_roundtrip(sample(42), 'Z', 90, read, iso_text_input).
1
+ text_roundtrip(sample(42), ' ', 32, read, iso_text_input).
2
2
  binary_roundtrip(65, 65, -1).
3
3
  read_term_metadata(ok).
4
4
  default_streams(ok).
@@ -1 +1 @@
1
- parse line 1: expected ., got ..
1
+ parse line 1: expected ., got .
@@ -733,6 +733,43 @@ c4 ?- call((!;1)).
733
733
  assertIncludes(upstreamResult.stdout, '46 true.', 'WG17 #367 output');
734
734
  },
735
735
  },
736
+ {
737
+ name: 'readers distinguish graphic tokens, comments, and full stops (issue #41)',
738
+ run: () => {
739
+ const parsed = parseProgramText('./*.');
740
+ assertEqual(parsed.length, 1, 'graphic atom clause count');
741
+ assertEqual(parsed[0].head.name, './*', 'comment opener stays inside graphic atom');
742
+
743
+ const read = runEyeProlog('', {
744
+ goal: 'read(T)',
745
+ ioOptions: { input: './*.' },
746
+ });
747
+ assertEqual(read.stdout, "read('./*').\n", 'graphic atom writeq readback');
748
+
749
+ const consecutive = runEyeProlog('answer(A, B) :- read(A), read(B).\n', {
750
+ goal: 'answer(A, B)',
751
+ ioOptions: { input: './*. .\nok.\n' },
752
+ });
753
+ assertEqual(consecutive.stdout, "answer('./*.', ok).\n", 'following read starts after the complete term');
754
+
755
+ let error = null;
756
+ try {
757
+ runEyeProlog('', { goal: 'read(T)', ioOptions: { input: '!.!.' } });
758
+ } catch (caught) {
759
+ error = caught;
760
+ }
761
+ assertEqual(error?.message, 'error(syntax_error(read_term))', 'solo-token sequence rejection');
762
+
763
+ const repl = runCli([], {
764
+ input: 'read(T).\n./*. .\nread(T).\nok.\nread(T).\n!.!.\nhalt.\n',
765
+ });
766
+ assertEqual(repl.status, 0, 'REPL exit status');
767
+ assertIncludes(repl.stdout, "T = './*.'.", 'REPL dotted graphic atom answer');
768
+ assertIncludes(repl.stdout, 'T = ok.', 'REPL following read answer');
769
+ assertIncludes(repl.stdout, 'error(syntax_error(read_term), eyeprolog)', 'REPL syntax error');
770
+ assertEqual(repl.stderr, '', 'REPL stderr');
771
+ },
772
+ },
736
773
  {
737
774
  name: 'question mark is a graphic character and writeq keeps graphic atoms unquoted',
738
775
  run: () => {
@@ -2781,20 +2818,22 @@ open(X) :- candidate(X), \\+ closed(X).
2781
2818
  const goal = parseGoalText('trial(Chars)');
2782
2819
  let count = 0;
2783
2820
  for (const _ of solver.solve([goal], new Env(), 0)) {
2784
- if (++count === 500000) break;
2821
+ if (++count === 50000) break;
2785
2822
  }
2786
- if (count !== 500000) throw new Error('unexpected answer count: ' + count);
2823
+ if (count !== 50000) throw new Error('unexpected answer count: ' + count);
2787
2824
  process.stdout.write(String(count));
2788
2825
  `;
2789
2826
  const result = spawnSync(process.execPath, [
2790
- '--max-old-space-size=64',
2827
+ // A smaller heap preserves the original false-exhaustion signal
2828
+ // without requiring half a million transient conversion attempts.
2829
+ '--max-old-space-size=32',
2791
2830
  '--input-type=module',
2792
2831
  '--eval',
2793
2832
  script,
2794
- ], { cwd: packageRoot, encoding: 'utf8', timeout: 45000 });
2833
+ ], { cwd: packageRoot, encoding: 'utf8', timeout: 10000 });
2795
2834
  if (result.error) throw result.error;
2796
2835
  assertEqual(result.status, 0, `bounded-heap child status; stderr=${result.stderr}`);
2797
- assertEqual(result.stdout, '500000', 'distinct number syntax attempts');
2836
+ assertEqual(result.stdout, '50000', 'distinct number syntax attempts');
2798
2837
  },
2799
2838
  },
2800
2839
  {
@@ -5369,9 +5369,16 @@ comment continues to the end of its line. Doubling the active delimiter is
5369
5369
  also accepted inside either quoted form, so `""` inside double-quoted notation
5370
5370
  denotes one literal double quote character.
5371
5371
 
5372
- Graphic atoms may contain `#$&*+-/<=>@^~\;`. A colon is the Part 2 module
5373
- qualification operator in `Module:Goal`; quote an atom whose name itself
5374
- contains a colon. Unquoted angle-bracket IRIs are not syntax.
5372
+ Graphic tokens use the characters `#$&*+-./<=>?@^~\`; `!` and `;` are solo
5373
+ atoms. A colon is the Part 2 module qualification operator in `Module:Goal`;
5374
+ quote an atom whose name itself contains a colon. Unquoted angle-bracket IRIs
5375
+ are not syntax.
5376
+
5377
+ A `/*` sequence opens a block comment only when it begins a token; inside a
5378
+ maximal graphic token the slash and star remain atom characters. A period ends
5379
+ a term only when it is recognized as the terminating full stop. Consequently,
5380
+ `./*.` at the end of a line reads the atom `./*`, whereas `./*. .` reads the
5381
+ atom `./*.` and consumes the second period as the terminator.
5375
5382
 
5376
5383
  In the grammar below, `{ x }` means zero or more repetitions of `x`, `[ x ]`
5377
5384
  means that `x` is optional, and parentheses group alternatives. These marks