eyeprolog 1.2.37 → 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.37",
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
 
@@ -606,7 +625,9 @@ class Parser {
606
625
  this.advance();
607
626
  return atom('{}');
608
627
  }
609
- const term = this.parseTerm(0, true);
628
+ // As with a parenthesized term, a current operator atom may be the entire
629
+ // curly-bracket content: `{*}` denotes {}(*), not an incomplete infix use.
630
+ const term = this.parseTerm(0, true, true, true);
610
631
  this.expect(TOK.RBRACE, '}');
611
632
  this.advance();
612
633
  return compound('{}', [term]);
@@ -732,7 +753,7 @@ class Parser {
732
753
  // an argument delimiter there is no operand for prefix syntax, so keep
733
754
  // the operator as an atom instead of reporting a misleading bad-term
734
755
  // error.
735
- if ([TOK.COMMA, TOK.RPAREN, TOK.RBRACKET, TOK.BAR, TOK.DOT].includes(this.token.type)) {
756
+ if ([TOK.COMMA, TOK.RPAREN, TOK.RBRACKET, TOK.RBRACE, TOK.BAR, TOK.DOT].includes(this.token.type)) {
736
757
  if (!allowOperatorAtom && this.token.type !== TOK.DOT) {
737
758
  throw new Error(`parse line ${this.token.line}: operator atom ${op} requires argument context or parentheses`);
738
759
  }
@@ -1032,7 +1053,7 @@ export function parseClauses(source, options = {}) {
1032
1053
  const parserOptions = ownsParserFlagState
1033
1054
  ? { ...options, parserFlagState: { doubleQuotes: initialDoubleQuotes } }
1034
1055
  : options;
1035
- if (options.sourceMetadata === false) {
1056
+ if (options.sourceMetadata === false && options.readTermEnd == null) {
1036
1057
  const clauses = parseClausesFastNoSource(source, null, null, parserOptions);
1037
1058
  if (clauses) return clauses;
1038
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;
package/src/write.js CHANGED
@@ -227,7 +227,9 @@ function format(term, env, options, table, maxPriority = 1200, context = 'term')
227
227
  }
228
228
 
229
229
  if (!options.ignoreOps && resolved.name === '{}' && resolved.arity === 1) {
230
- return `{${format(resolved.args[0], env, options, table, 1200)}}`;
230
+ // A current operator atom is valid as the complete curly-bracket content,
231
+ // just as it is in a functional argument or list element.
232
+ return `{${format(resolved.args[0], env, options, table, 1200, 'argument')}}`;
231
233
  }
232
234
 
233
235
  if (!options.ignoreOps) {
@@ -9,6 +9,7 @@ answer(Binary, Octal, Hex, Character, Escaped, Curly, EmptyCurly, IntegerPart, F
9
9
  =(Character, 0'\n),
10
10
  =(Escaped, 'A\x42\\101\'),
11
11
  =(Curly, {pair(a, b)}),
12
+ {*} = {}(*),
12
13
  =(EmptyCurly, {}),
13
14
  IntegerPart is float_integer_part(-3.75),
14
15
  FractionalPart is float_fractional_part(-3.75).
@@ -38,7 +38,7 @@ import {
38
38
  variantTerms,
39
39
  parseProgramText,
40
40
  } from '../src/index.js';
41
- import { parseGoalText, parseNumberTokenText } from '../src/parser.js';
41
+ import { ISO_OPERATOR_DEFINITIONS, parseGoalText, parseNumberTokenText } from '../src/parser.js';
42
42
  import { compareTerms } from '../src/term.js';
43
43
  import { formatTermForWrite } from '../src/write.js';
44
44
  import { selectClauseCandidates } from '../src/program.js';
@@ -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
  {
@@ -782,6 +816,38 @@ c4 ?- call((!;1)).
782
816
  assertNotIncludes(result.stdout, "'?-'", 'writeq(?-) has no quotes');
783
817
  },
784
818
  },
819
+ {
820
+ name: 'curly brackets accept ISO and custom operator atoms (issue #41)',
821
+ run: () => {
822
+ const operatorNames = [...new Set(ISO_OPERATOR_DEFINITIONS.map(([, , name]) => name))]
823
+ .filter((name) => name !== ',');
824
+ for (const name of operatorNames) {
825
+ const holder = parseGoalText(`holder({${name}})`);
826
+ const curly = holder.args[0];
827
+ assertEqual(curly.name, '{}', `curly functor for ${name}`);
828
+ assertEqual(curly.args[0].name, name, `curly operator atom ${name}`);
829
+ }
830
+
831
+ const customProgram = parseProgramText([
832
+ ':- op(100, fx, pre).',
833
+ ':- op(100, xf, post).',
834
+ ':- op(100, xfx, infix).',
835
+ 'custom({pre}, {post}, {infix}).',
836
+ '',
837
+ ].join('\n'));
838
+ const custom = customProgram.find((clause) => clause.head.name === 'custom').head;
839
+ assertEqual(
840
+ custom.args.map((curly) => curly.args[0].name).join(','),
841
+ 'pre,post,infix',
842
+ 'custom prefix, postfix, and infix operator atoms',
843
+ );
844
+
845
+ const repl = runCli([], { input: 'read(T).\n{*}.\nhalt.\n' });
846
+ assertEqual(repl.status, 0, 'curly operator REPL status');
847
+ assertIncludes(repl.stdout, 'T = {*}.', 'curly operator REPL answer');
848
+ assertNotIncludes(repl.stdout, '{(*)}', 'curly operator has no unnecessary parentheses');
849
+ },
850
+ },
785
851
  {
786
852
  name: 'normal parser rejects non-conforming bare operator operands',
787
853
  run: () => {
@@ -5197,12 +5197,14 @@ parsing of subsequent text, place them before their first use. ISO argument
5197
5197
  syntax also permits an atom that is currently an operator to appear directly
5198
5198
  as a functional argument or list element, so forms such as
5199
5199
  `current_op(Priority, Specifier, :-)` and `[:-,-]` are valid without quoting
5200
- or parenthesizing those operator atoms. Term output observes the same `arg`
5201
- rule: with `quoted(true)`, an operator atom is not quoted merely because it is
5202
- an operator when it occurs as a functional argument or list element. Thus
5203
- `writeq([:-,-])` emits `[:-,-]`, and `writeq(f(;,'|',';;'))` emits
5204
- `f(;,'|',';;')`; the bar stays quoted because ISO treats the unquoted `|`
5205
- token as a list separator rather than an atom. The ISO initial operator table also
5200
+ or parenthesizing those operator atoms. A current operator atom may likewise
5201
+ be the complete content of parentheses or curly brackets: `(+)` denotes the
5202
+ atom `+`, and `{*}` denotes the curly term `{}(*)`. Term output observes the
5203
+ same context rules: with `quoted(true)`, an operator atom is not quoted merely
5204
+ because it occurs as a functional argument, list element, or sole curly-bracket
5205
+ content. Thus `writeq({*})` emits `{*}`, `writeq([:-,-])` emits `[:-,-]`, and
5206
+ `writeq(f(;,'|',';;'))` emits `f(;,'|',';;')`; the bar stays quoted because
5207
+ ISO treats the unquoted `|` token as a list separator rather than an atom. The ISO initial operator table also
5206
5208
  contains `?-` at priority 1200 with specifier `fx`, so
5207
5209
  `current_op(1200, fx, ?-)` succeeds. EyeProlog's embedded quad syntax permits
5208
5210
  an optional label before the query marker (`Label ?- Query.`), so while quad