eyeprolog 1.2.35 → 1.2.37

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.37",
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
  }
@@ -614,6 +617,7 @@ function formatAnswer(engine, state, variables, env) {
614
617
  // current operator atoms in argument and list-element positions without
615
618
  // quotes, just as writeq/1 already prints them.
616
619
  operatorAtomsAsArgs: true,
620
+ dottedGraphicAtoms: true,
617
621
  doubleQuotes: state.solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
618
622
  })}`);
619
623
  }
@@ -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;
package/src/write.js CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  } from './term.js';
6
6
 
7
7
  const graphicAtomCharacters = new Set('!#$&*+-/<=>?@^~\\'.split(''));
8
+ const dottedGraphicAtomCharacters = new Set([...graphicAtomCharacters, '.']);
8
9
  const compactInfixOperators = new Set([':', '..']);
9
10
 
10
11
  function quotedControlEscape(ch) {
@@ -48,6 +49,11 @@ function writeAtom(name) {
48
49
  return atomNeedsQuotes(name) ? quoteAtom(name) : name;
49
50
  }
50
51
 
52
+ function isDottedGraphicAtom(name) {
53
+ return name.includes('.') && [...name].some((ch) => ch !== '.') && !name.startsWith('/*') &&
54
+ [...name].every((ch) => dottedGraphicAtomCharacters.has(ch));
55
+ }
56
+
51
57
  function legacyVariableToIso(name) {
52
58
  if (name === '?') return '_';
53
59
  const tail = name.slice(1);
@@ -172,6 +178,10 @@ function format(term, env, options, table, maxPriority = 1200, context = 'term')
172
178
  if (resolved.type === STRING) return writeString(resolved.name);
173
179
  if (resolved.type === ATOM) {
174
180
  if (!options.quoted) return resolved.name;
181
+ // Top-level bindings are already delimited by their answer punctuation.
182
+ // Keep valid dotted graphic tokens readable there without weakening the
183
+ // ISO writeq/1 policy tested by WG17 #308.
184
+ if (options.dottedGraphicAtoms && isDottedGraphicAtom(resolved.name)) return resolved.name;
175
185
  // ISO 6.3.3.1 gives functional arguments and list elements a special
176
186
  // `arg` production: an atom that is a current operator is valid there
177
187
  // without quoting. Keep lexical exceptions such as `|` quoted.
@@ -269,6 +279,7 @@ export function formatTermForWrite(term, env = new Env(), options = {}) {
269
279
  variableNames: printableReadVariableNames(term, env, explicitVariableNames),
270
280
  compact: options.compact === true,
271
281
  operatorAtomsAsArgs: options.operatorAtomsAsArgs === true,
282
+ dottedGraphicAtoms: options.dottedGraphicAtoms === true,
272
283
  };
273
284
  const maxPriority = Number.isInteger(options.maxPriority)
274
285
  ? Math.max(0, Math.min(1200, options.maxPriority))
@@ -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,44 @@ 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
+ assertNotIncludes(repl.stdout, "T = './*.'", 'REPL dotted graphic atom has no spurious quotes');
769
+ assertIncludes(repl.stdout, 'T = ok.', 'REPL following read answer');
770
+ assertIncludes(repl.stdout, 'error(syntax_error(read_term), eyeprolog)', 'REPL syntax error');
771
+ assertEqual(repl.stderr, '', 'REPL stderr');
772
+ },
773
+ },
736
774
  {
737
775
  name: 'question mark is a graphic character and writeq keeps graphic atoms unquoted',
738
776
  run: () => {
@@ -2781,20 +2819,22 @@ open(X) :- candidate(X), \\+ closed(X).
2781
2819
  const goal = parseGoalText('trial(Chars)');
2782
2820
  let count = 0;
2783
2821
  for (const _ of solver.solve([goal], new Env(), 0)) {
2784
- if (++count === 500000) break;
2822
+ if (++count === 50000) break;
2785
2823
  }
2786
- if (count !== 500000) throw new Error('unexpected answer count: ' + count);
2824
+ if (count !== 50000) throw new Error('unexpected answer count: ' + count);
2787
2825
  process.stdout.write(String(count));
2788
2826
  `;
2789
2827
  const result = spawnSync(process.execPath, [
2790
- '--max-old-space-size=64',
2828
+ // A smaller heap preserves the original false-exhaustion signal
2829
+ // without requiring half a million transient conversion attempts.
2830
+ '--max-old-space-size=32',
2791
2831
  '--input-type=module',
2792
2832
  '--eval',
2793
2833
  script,
2794
- ], { cwd: packageRoot, encoding: 'utf8', timeout: 45000 });
2834
+ ], { cwd: packageRoot, encoding: 'utf8', timeout: 10000 });
2795
2835
  if (result.error) throw result.error;
2796
2836
  assertEqual(result.status, 0, `bounded-heap child status; stderr=${result.stderr}`);
2797
- assertEqual(result.stdout, '500000', 'distinct number syntax attempts');
2837
+ assertEqual(result.stdout, '50000', 'distinct number syntax attempts');
2798
2838
  },
2799
2839
  },
2800
2840
  {
@@ -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