eyeprolog 1.2.38 → 1.2.39

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.38",
6
+ "version": "1.2.39",
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
@@ -1178,6 +1178,31 @@ function scopeReadTerm(term) {
1178
1178
  return { term: copy(term), variables };
1179
1179
  }
1180
1180
 
1181
+ function parseReadTermText(text, solver) {
1182
+ const converted = convertedTermText(text, solver);
1183
+ const operatorState = createParserOperatorState(solver.program.operators.values(), false);
1184
+ const clauses = parseClauses(converted, {
1185
+ sourceMetadata: false,
1186
+ operatorState,
1187
+ isoStrict: solver.isoStrict,
1188
+ doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
1189
+ // The stream scanner supplies one candidate ending at this full stop.
1190
+ // Earlier ambiguous dots must remain available to maximal graphic tokens.
1191
+ readTermEnd: converted.length - 1,
1192
+ });
1193
+ if (clauses.length !== 1 || clauses[0].body.length) throw new Error('bad term');
1194
+ return clauses[0].head;
1195
+ }
1196
+
1197
+ export function isCompleteReadTermText(text, solver) {
1198
+ try {
1199
+ parseReadTermText(text, solver);
1200
+ return true;
1201
+ } catch (_) {
1202
+ return false;
1203
+ }
1204
+ }
1205
+
1181
1206
  function readTermFromStream(stream, solver) {
1182
1207
  let requestedInteractiveTerm = false;
1183
1208
  while (true) {
@@ -1189,16 +1214,9 @@ function readTermFromStream(stream, solver) {
1189
1214
  throw new PrologError('syntax_error(read_term)');
1190
1215
  }
1191
1216
  try {
1192
- const operatorState = createParserOperatorState(solver.program.operators.values(), false);
1193
- const clauses = parseClauses(convertedTermText(candidate.text, solver), {
1194
- sourceMetadata: false,
1195
- operatorState,
1196
- isoStrict: solver.isoStrict,
1197
- doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
1198
- });
1199
- if (clauses.length !== 1 || clauses[0].body.length) throw new Error('bad term');
1217
+ const term = parseReadTermText(candidate.text, solver);
1200
1218
  stream.position = candidate.end;
1201
- return scopeReadTerm(clauses[0].head);
1219
+ return scopeReadTerm(term);
1202
1220
  } catch (_) {
1203
1221
  // A dot inside a graphic operator, such as =.., is only a possible
1204
1222
  // terminator. Keep scanning until a complete term parses.
package/src/parser.js CHANGED
@@ -1,7 +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
+ import { continuesGraphicToken, isTerminatingFullStop } from './syntax-scan.js';
5
5
 
6
6
  const TOK = {
7
7
  EOF: 'eof', ATOM: 'atom', VAR: 'var', STRING: 'string', NUMBER: 'number',
@@ -196,9 +196,25 @@ class Parser {
196
196
  this.infixOperators = operatorState.infixOperators;
197
197
  this.prefixOperators = operatorState.prefixOperators;
198
198
  this.postfixOperators = operatorState.postfixOperators;
199
+ this.readTermEnd = Number.isInteger(options.readTermEnd) ? options.readTermEnd : null;
199
200
  this.previousToken = null;
200
201
  this.token = this.nextToken();
201
202
  }
203
+ terminatingFullStop(index = this.pos) {
204
+ // read/1 tries each possible full stop in turn. While parsing a later
205
+ // candidate, a preceding dot after a graphic character belongs to that
206
+ // maximal graphic token; the candidate's final dot is the end char. Keep
207
+ // ordinary program parsing context-free by enabling this only for a
208
+ // designated read-term candidate.
209
+ if (this.readTermEnd != null) {
210
+ if (index === this.readTermEnd) return true;
211
+ if (index < this.readTermEnd && continuesGraphicToken(this.source, index)) {
212
+ const next = this.source[index + 1] ?? '';
213
+ if (next === '%' || /^[\u0009-\u000d\u0020]$/.test(next)) return false;
214
+ }
215
+ }
216
+ return isTerminatingFullStop(this.source, index);
217
+ }
202
218
  defineOperator(priority, specifier, name) {
203
219
  defineParserOperator(this, priority, specifier, name);
204
220
  }
@@ -366,15 +382,16 @@ class Parser {
366
382
  this.pos += 3;
367
383
  return { type: TOK.ATOM, text: '...', line };
368
384
  }
369
- if (ch === '?' && this.peek(1) === '-') {
385
+ if (ch === '?' && this.peek(1) === '-' &&
386
+ !(isGraphicAtomCode(this.peek(2).charCodeAt(0)) && !this.terminatingFullStop(this.pos + 2))) {
370
387
  this.pos += 2;
371
388
  return { type: TOK.ATOM, text: '?-', line };
372
389
  }
373
- if (ch === '.' && !isTerminatingFullStop(this.source, this.pos)) {
390
+ if (ch === '.' && !this.terminatingFullStop()) {
374
391
  const start = this.pos;
375
392
  this.take();
376
393
  while (isGraphicAtomCode(this.peek().charCodeAt(0)) &&
377
- !isTerminatingFullStop(this.source, this.pos)) this.take();
394
+ !this.terminatingFullStop()) this.take();
378
395
  return { type: TOK.ATOM, text: this.source.slice(start, this.pos), line };
379
396
  }
380
397
  if (ch === '!') {
@@ -394,11 +411,13 @@ class Parser {
394
411
  this.take();
395
412
  return { type: punct[ch], text: ch, line, precededByLayout };
396
413
  }
397
- if (ch === ':' && this.peek(1) === '-') {
414
+ if (ch === ':' && this.peek(1) === '-' &&
415
+ !(isGraphicAtomCode(this.peek(2).charCodeAt(0)) && !this.terminatingFullStop(this.pos + 2))) {
398
416
  this.pos += 2;
399
417
  return { type: TOK.IF, text: ':-', line };
400
418
  }
401
- if (ch === ':') {
419
+ if (ch === ':' &&
420
+ !(isGraphicAtomCode(this.peek(1).charCodeAt(0)) && !this.terminatingFullStop(this.pos + 1))) {
402
421
  this.take();
403
422
  return { type: TOK.ATOM, text: ':', line };
404
423
  }
@@ -544,7 +563,7 @@ class Parser {
544
563
  const start = this.pos;
545
564
  this.take();
546
565
  while (isGraphicAtomCode(this.peek().charCodeAt(0)) &&
547
- !isTerminatingFullStop(this.source, this.pos)) this.take();
566
+ !this.terminatingFullStop()) this.take();
548
567
  return { type: TOK.ATOM, text: this.source.slice(start, this.pos), line };
549
568
  }
550
569
 
@@ -1034,7 +1053,7 @@ export function parseClauses(source, options = {}) {
1034
1053
  const parserOptions = ownsParserFlagState
1035
1054
  ? { ...options, parserFlagState: { doubleQuotes: initialDoubleQuotes } }
1036
1055
  : options;
1037
- if (options.sourceMetadata === false) {
1056
+ if (options.sourceMetadata === false && options.readTermEnd == null) {
1038
1057
  const clauses = parseClausesFastNoSource(source, null, null, parserOptions);
1039
1058
  if (clauses) return clauses;
1040
1059
  if (ownsParserFlagState) parserOptions.parserFlagState.doubleQuotes = initialDoubleQuotes;
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 } from './iso.js';
6
+ import { formalErrorTerm, isCompleteReadTermText } from './iso.js';
7
7
  import {
8
8
  characterCodeConstantEnd, continuesGraphicToken, isTerminatingFullStop, quotedEscapeEnd,
9
9
  } from './syntax-scan.js';
@@ -162,7 +162,7 @@ class LineReader {
162
162
  return this.terminal && Number.isInteger(this.input.fd);
163
163
  }
164
164
 
165
- readInteractiveTermSync() {
165
+ readInteractiveTermSync(solver = null) {
166
166
  if (!this.canReadTermSynchronously()) return null;
167
167
  let source = '';
168
168
  let prompt = '|: ';
@@ -172,7 +172,9 @@ class LineReader {
172
172
  if (line == null) return source.trim() ? source : null;
173
173
  source += `${line}\n`;
174
174
  const end = terminalFullStop(source);
175
- if (end >= 0) return source.slice(0, end + 1) + '\n';
175
+ if (end >= 0 && acceptsReadTermBoundary(source, end, solver)) {
176
+ return source.slice(0, end + 1) + '\n';
177
+ }
176
178
  prompt = '| ';
177
179
  }
178
180
  }
@@ -256,7 +258,7 @@ function makeState(engine, sources, output, options = {}, previousState = null,
256
258
  // let ISO term input request a complete terminal term exactly when read/1-2
257
259
  // or read_term/2-3 actually executes. This also works inside conjunctions
258
260
  // and user predicates instead of only when read/* is the whole REPL goal.
259
- userInput.interactiveReadTerm = () => reader.readInteractiveTermSync();
261
+ userInput.interactiveReadTerm = () => reader.readInteractiveTermSync(solver);
260
262
  }
261
263
  const flagOverrides = new Map(previousState?.flagOverrides ?? []);
262
264
  for (const [name, value] of flagOverrides) {
@@ -300,7 +302,7 @@ async function prepareInteractiveTermInput(state, goal, reader) {
300
302
  const stream = interactiveTermInputStream(state, goal);
301
303
  if (stream == null || terminalFullStop(String(stream.content).slice(stream.position)) >= 0) return;
302
304
 
303
- const text = await readInteractiveTerm(reader);
305
+ const text = await readInteractiveTerm(reader, state.solver);
304
306
  if (text == null) return;
305
307
  stream.content += text;
306
308
  stream.pastEnd = false;
@@ -334,7 +336,7 @@ function explicitInputReference(term) {
334
336
  return null;
335
337
  }
336
338
 
337
- async function readInteractiveTerm(reader) {
339
+ async function readInteractiveTerm(reader, solver = null) {
338
340
  let source = '';
339
341
  let prompt = '|: ';
340
342
  while (true) {
@@ -342,11 +344,22 @@ async function readInteractiveTerm(reader) {
342
344
  if (line == null) return source.trim() ? source : null;
343
345
  source += `${line}\n`;
344
346
  const end = terminalFullStop(source);
345
- if (end >= 0) return source.slice(0, end + 1) + '\n';
347
+ if (end >= 0 && acceptsReadTermBoundary(source, end, solver)) {
348
+ return source.slice(0, end + 1) + '\n';
349
+ }
346
350
  prompt = '| ';
347
351
  }
348
352
  }
349
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);
361
+ }
362
+
350
363
  function terminalFullStop(source) {
351
364
  let quote = null;
352
365
  let lineComment = false;
@@ -752,6 +752,31 @@ c4 ?- call((!;1)).
752
752
  });
753
753
  assertEqual(consecutive.stdout, "answer('./*.', ok).\n", 'following read starts after the complete term');
754
754
 
755
+ // A possible full stop can fail to complete the term while still
756
+ // extending a current graphic operator into an ordinary atom. The
757
+ // later standalone full stop then completes the read term. Exercise
758
+ // every predefined graphic operator, including the tokenizer's
759
+ // special :- and ?- cases, so they cannot diverge from * again.
760
+ const graphicOperator = /^[#$&*+\-./<=>?@^~\\:]+$/;
761
+ const graphicOperators = [...new Set(ISO_OPERATOR_DEFINITIONS.map(([, , name]) => name))]
762
+ .filter((name) => graphicOperator.test(name));
763
+ for (const name of graphicOperators) {
764
+ const program = Program.parse('');
765
+ const solver = new Solver(program, {
766
+ registry: getEyePrologRegistry(),
767
+ ioOptions: { input: `!,${name}.\n.\n` },
768
+ });
769
+ const readGoal = parseGoalText('read(T)', {
770
+ operatorDefinitions: [...program.operators.values()],
771
+ });
772
+ const answers = [...solver.solve([readGoal], new Env(), 0)];
773
+ assertEqual(answers.length, 1, `graphic operator read answer for ${name}`);
774
+ const term = copyResolved(readGoal.args[0], answers[0]);
775
+ assertEqual(term.name, ',', `graphic operator conjunction for ${name}`);
776
+ assertEqual(term.args[0].name, '!', `graphic operator left operand for ${name}`);
777
+ assertEqual(term.args[1].name, `${name}.`, `graphic operator atom for ${name}`);
778
+ }
779
+
755
780
  let error = null;
756
781
  try {
757
782
  runEyeProlog('', { goal: 'read(T)', ioOptions: { input: '!.!.' } });
@@ -769,6 +794,15 @@ c4 ?- call((!;1)).
769
794
  assertIncludes(repl.stdout, 'T = ok.', 'REPL following read answer');
770
795
  assertIncludes(repl.stdout, 'error(syntax_error(read_term), eyeprolog)', 'REPL syntax error');
771
796
  assertEqual(repl.stderr, '', 'REPL stderr');
797
+
798
+ const continuedGraphic = runCli([], {
799
+ input: 'read(T).\n!,*.\n.\nread(T).\na\n.\nhalt.\n',
800
+ });
801
+ assertEqual(continuedGraphic.status, 0, 'continued graphic operator REPL status');
802
+ assertIncludes(continuedGraphic.stdout, 'T = (!, *.).', 'continued graphic operator answer');
803
+ assertIncludes(continuedGraphic.stdout, 'T = a.', 'read after continued graphic operator');
804
+ assertNotIncludes(continuedGraphic.stdout, 'syntax_error', 'continued graphic operator syntax');
805
+ assertEqual(continuedGraphic.stderr, '', 'continued graphic operator REPL stderr');
772
806
  },
773
807
  },
774
808
  {