eyeprolog 1.2.0 → 1.2.2

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.0",
6
+ "version": "1.2.2",
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
@@ -1063,16 +1063,36 @@ function isTerminatingFullStop(source, index) {
1063
1063
  return true;
1064
1064
  }
1065
1065
 
1066
+ function quotedEscapeEnd(source, index) {
1067
+ const escaped = source[index + 1] ?? '';
1068
+ if (!escaped) return index;
1069
+
1070
+ // ISO 6.4.2.1 numeric escapes include a terminating backslash. Consume
1071
+ // that delimiter as part of the quoted character so stream scanning does
1072
+ // not mistake it for an escape of the quote which follows.
1073
+ if (escaped === 'x') {
1074
+ let cursor = index + 2;
1075
+ while (/^[0-9A-Fa-f]$/.test(source[cursor] ?? '')) cursor++;
1076
+ return source[cursor] === '\\' ? cursor : Math.max(index + 1, cursor - 1);
1077
+ }
1078
+ if (/^[0-7]$/.test(escaped)) {
1079
+ let cursor = index + 1;
1080
+ while (/^[0-7]$/.test(source[cursor] ?? '')) cursor++;
1081
+ return source[cursor] === '\\' ? cursor : Math.max(index + 1, cursor - 1);
1082
+ }
1083
+
1084
+ return index + 1;
1085
+ }
1086
+
1066
1087
  function* termTextCandidates(stream) {
1067
1088
  const source = String(stream.content);
1068
- let quote = null, escaped = false, lineComment = false, blockComment = false;
1089
+ let quote = null, lineComment = false, blockComment = false;
1069
1090
  for (let i = stream.position; i < source.length; i++) {
1070
1091
  const ch = source[i], next = source[i + 1];
1071
1092
  if (lineComment) { if (ch === '\n') lineComment = false; continue; }
1072
1093
  if (blockComment) { if (ch === '*' && next === '/') { blockComment = false; i++; } continue; }
1073
1094
  if (quote) {
1074
- if (escaped) escaped = false;
1075
- else if (ch === '\\') escaped = true;
1095
+ if (ch === '\\') i = quotedEscapeEnd(source, i);
1076
1096
  else if (ch === quote && next === quote) i++;
1077
1097
  else if (ch === quote) quote = null;
1078
1098
  continue;
@@ -1087,13 +1107,21 @@ function* termTextCandidates(stream) {
1087
1107
  }
1088
1108
  function convertedTermText(text, solver) {
1089
1109
  if (solver.prologFlags.get('char_conversion')?.value?.name !== 'on' || solver.charConversions.size === 0) return text;
1090
- let result = '', quote = null, escaped = false;
1091
- for (const ch of text) {
1110
+ let result = '', quote = null;
1111
+ for (let i = 0; i < text.length; i++) {
1112
+ const ch = text[i], next = text[i + 1];
1092
1113
  if (quote) {
1093
1114
  result += ch;
1094
- if (escaped) escaped = false;
1095
- else if (ch === '\\') escaped = true;
1096
- else if (ch === quote) quote = null;
1115
+ if (ch === '\\') {
1116
+ const end = quotedEscapeEnd(text, i);
1117
+ if (end > i) result += text.slice(i + 1, end + 1);
1118
+ i = end;
1119
+ } else if (ch === quote && next === quote) {
1120
+ result += next;
1121
+ i++;
1122
+ } else if (ch === quote) {
1123
+ quote = null;
1124
+ }
1097
1125
  } else if (ch === "'" || ch === '"') {
1098
1126
  quote = ch;
1099
1127
  result += ch;
package/src/parser.js CHANGED
@@ -663,8 +663,7 @@ class Parser {
663
663
  }
664
664
  return this.source[start] === ' ' || this.source[start] === '\t';
665
665
  }
666
- parseQuad(id, line, accept) {
667
- const query = this.parseTerm(0, true);
666
+ parseQuadAnswers(id, query, line, accept) {
668
667
  this.expect(TOK.DOT, '.');
669
668
  this.advance();
670
669
 
@@ -684,28 +683,38 @@ class Parser {
684
683
  source: { filename: this.filename, line },
685
684
  });
686
685
  }
686
+ parseQuad(id, line, accept) {
687
+ const query = this.parseTerm(0, true);
688
+ this.parseQuadAnswers(id, query, line, accept);
689
+ }
690
+ parseQuadTerm(term, line, accept) {
691
+ if (term.type !== COMPOUND || term.name !== '?-' || ![1, 2].includes(term.arity)) return false;
692
+ const id = term.arity === 2 ? term.args[0] : null;
693
+ const query = term.args[term.arity - 1];
694
+ this.parseQuadAnswers(id, query, line, accept);
695
+ return true;
696
+ }
687
697
  parseProgram(emit = null) {
688
698
  const clauses = emit ? null : [];
689
699
  let clauseNumber = 0;
690
700
  const accept = emit ?? ((clause) => clauses.push(clause));
691
701
  while (this.token.type !== TOK.EOF) {
692
702
  const line = this.token.line;
693
- if (this.operatorTokenName() === '?-') {
694
- if (this.strictIso) {
695
- // In Part 1, ?- is the predefined 1200 fx operator. A strict-core
696
- // source therefore reads it as an ordinary term rather than giving
697
- // it EyeProlog's top-level quad meaning.
698
- const head = this.parseTerm(0, true);
699
- this.expect(TOK.DOT, '.');
703
+ // In the normal EyeProlog profile, canonical functional ?-/1 and
704
+ // ?-/2 notation denotes the same quad marker as the corresponding
705
+ // operator notation. Keep the existing operator-form query parsing so
706
+ // a query containing comma remains wholly to the right of `?-`, while
707
+ // functional notation is parsed as a term and then decomposed by arity.
708
+ if (this.operatorTokenName() === '?-' && !this.strictIso) {
709
+ if (this.peek() === '(') {
710
+ const quadTerm = this.parseTerm(0, true);
711
+ if (!this.parseQuadTerm(quadTerm, line, accept)) {
712
+ throw new Error(`parse line ${line}: bad quad term`);
713
+ }
714
+ } else {
700
715
  this.advance();
701
- const clause = { head, body: [] };
702
- clauseNumber++;
703
- if (this.sourceMetadata) clause.source = { filename: this.filename, line, clause: clauseNumber };
704
- accept(clause);
705
- continue;
716
+ this.parseQuad(null, line, accept);
706
717
  }
707
- this.advance();
708
- this.parseQuad(null, line, accept);
709
718
  continue;
710
719
  }
711
720
  if (this.token.type === TOK.IF) {
@@ -740,6 +749,13 @@ class Parser {
740
749
  continue;
741
750
  }
742
751
  let head = this.parseTerm(3);
752
+ // Both a quad label and a TS 13211-3 semicontext may contain an
753
+ // unparenthesized comma before their priority-1200 operator. Assemble
754
+ // that left operand before deciding whether the marker is ?- or -->.
755
+ if (this.token.type === TOK.COMMA) {
756
+ this.advance();
757
+ head = compound(',', [head, this.parseTerm(3)]);
758
+ }
743
759
  if (this.operatorTokenName() === '?-') {
744
760
  if (this.strictIso) {
745
761
  // There is no predefined infix ?-/2 in strict core mode. If a
@@ -761,13 +777,6 @@ class Parser {
761
777
  this.parseQuad(head, line, accept);
762
778
  continue;
763
779
  }
764
- // ISO/IEC TS 13211-3 grammar rules use -->/2 at the same top-level
765
- // priority as clauses. The left side may include an unparenthesized
766
- // semicontext: NonTerminal, Terminals --> Body.
767
- if (this.token.type === TOK.COMMA) {
768
- this.advance();
769
- head = compound(',', [head, this.parseTerm(3)]);
770
- }
771
780
  if (this.operatorTokenName() === '-->') {
772
781
  this.advance();
773
782
  const grammarBody = this.parseTerm(0, true);
@@ -65,3 +65,23 @@ default_streams(ok) :-
65
65
  standard_write(ok) :-
66
66
  write(io_marker),
67
67
  nl.
68
+
69
+ %% goal: numeric_escape_term_input(ok)
70
+
71
+ numeric_escape_term_input(ok) :-
72
+ open('/tmp/eyeprolog-iso-read-escapes.txt', write, Output, []),
73
+ put_code(Output, 39), put_code(Output, 92), put_code(Output, 55),
74
+ put_code(Output, 92), put_code(Output, 39), put_code(Output, 46), nl(Output),
75
+ put_code(Output, 39), put_code(Output, 92), put_code(Output, 120), put_code(Output, 55),
76
+ put_code(Output, 92), put_code(Output, 39), put_code(Output, 46), nl(Output),
77
+ close(Output),
78
+ current_input(Old),
79
+ open('/tmp/eyeprolog-iso-read-escapes.txt', read, Input, []),
80
+ set_input(Input),
81
+ read(A),
82
+ read_term(B, []),
83
+ set_input(Old),
84
+ close(Input),
85
+ A == '\a',
86
+ B == '\a'.
87
+
@@ -4,3 +4,4 @@ read_term_metadata(ok).
4
4
  default_streams(ok).
5
5
  io_marker
6
6
  standard_write(ok).
7
+ numeric_escape_term_input(ok).
@@ -221,6 +221,43 @@ why(
221
221
  assertEqual(program.quads[1].query.args[1].name, '.', 'dot atom');
222
222
  },
223
223
  },
224
+ {
225
+ name: 'quad parser treats operator and functional ?- notation equivalently',
226
+ run: () => {
227
+ const labelled = Program.parse(
228
+ `0,passes
229
+ ?- X = 1.
230
+ X = 1.
231
+ `,
232
+ );
233
+ assertEqual(labelled.quads.length, 1, 'labelled quad count');
234
+ assertEqual(labelled.clauses.length, 0, 'labelled quad clause count');
235
+ assertEqual(labelled.quads[0].id.name, ',', 'comma label functor');
236
+ assertEqual(labelled.quads[0].query.name, '=', 'labelled quad query');
237
+ const labelledReport = publicApi.runQuads(labelled);
238
+ assertEqual(labelledReport.stdout, 'quads: 1 run, 1 passed, 0 failed.\n', 'labelled quad report');
239
+
240
+ const functional = Program.parse(
241
+ `?-(','(0,passes),=(X,1)).
242
+ X = 1.
243
+ `,
244
+ );
245
+ assertEqual(functional.quads.length, 1, 'functional quad count');
246
+ assertEqual(functional.clauses.length, 0, 'functional quad clause count');
247
+ assertEqual(functional.quads[0].id.name, ',', 'functional comma label functor');
248
+ assertEqual(functional.quads[0].query.name, '=', 'functional quad query');
249
+ assertEqual(termToString(functional.quads[0].id), termToString(labelled.quads[0].id),
250
+ 'operator and functional labels are equivalent');
251
+ assertEqual(termToString(functional.quads[0].query), termToString(labelled.quads[0].query),
252
+ 'operator and functional queries are equivalent');
253
+ const functionalReport = publicApi.runQuads(functional);
254
+ assertEqual(functionalReport.stdout, 'quads: 1 run, 1 passed, 0 failed.\n', 'functional quad report');
255
+
256
+ const strict = Program.parse(`?-(','(0,passes),=(X,1)).\n`, { isoStrict: true });
257
+ assertEqual(strict.quads.length, 0, 'strict mode has no quads');
258
+ assertEqual(strict.clauses.length, 1, 'strict functional ?-/2 remains an ordinary term');
259
+ },
260
+ },
224
261
  {
225
262
  name: 'runQuads checks portable answer descriptions',
226
263
  run: () => {
@@ -1169,6 +1206,44 @@ c4 ?- call((!;1)).
1169
1206
  assertIncludes(result.stderr, 'implementation-specific directive use_module/1', 'stderr');
1170
1207
  },
1171
1208
  },
1209
+ {
1210
+ name: 'term input accepts ISO numeric escapes through read predicates',
1211
+ run: () => {
1212
+ const escapePath = path.join(tmp, `read-escapes-${++tmpCounter}.term`);
1213
+ // Two raw read-terms: '\7\'. and '\x7\'. Build the file from
1214
+ // character codes so this regression tests stream parsing rather than
1215
+ // the parser which reads this JavaScript fixture.
1216
+ fs.writeFileSync(escapePath, String.fromCharCode(
1217
+ 39, 92, 55, 92, 39, 46, 10,
1218
+ 39, 92, 120, 55, 92, 39, 46, 10,
1219
+ ));
1220
+ const source = [
1221
+ `read_escapes(A, B) :-`,
1222
+ ` current_input(Old),`,
1223
+ ` open(${sourceAtom(escapePath)}, read, Input, []),`,
1224
+ ` set_input(Input),`,
1225
+ ` read(A),`,
1226
+ ` read_term(B, []),`,
1227
+ ` set_input(Old),`,
1228
+ ` close(Input).`,
1229
+ '',
1230
+ ].join('\n');
1231
+ assertEqual(
1232
+ run(source, { goal: 'read_escapes(A, B)' }).stdout,
1233
+ "read_escapes('\\a', '\\a').\n",
1234
+ 'read/1 and read_term/2 numeric escapes',
1235
+ );
1236
+
1237
+ assertEqual(
1238
+ run('answer(T) :- read(T).\n', {
1239
+ goal: 'answer(T)',
1240
+ ioOptions: { input: "'\\7\\'." },
1241
+ }).stdout,
1242
+ "answer('\\a').\n",
1243
+ 'read/1 user_input numeric escape',
1244
+ );
1245
+ },
1246
+ },
1172
1247
  {
1173
1248
  name: 'term input keeps dotted operators intact and uses program operators',
1174
1249
  run: () => {
@@ -5146,7 +5146,11 @@ contains `?-` at priority 1200 with specifier `fx`, so
5146
5146
  an optional label before the query marker (`Label ?- Query.`), so while quad
5147
5147
  syntax is supported it additionally exposes `?-` at priority 1200 with
5148
5148
  specifier `xfx` as an implementation-specific operator. Consequently
5149
- `current_op(Priority, Specifier, ?-)` enumerates both definitions.
5149
+ `current_op(Priority, Specifier, ?-)` enumerates both definitions. At top level in the normal EyeProlog profile, the quad marker is recognized
5150
+ after term parsing, so equivalent syntax stays equivalent: `Label ?- Query.`
5151
+ and canonical `?-(Label, Query).` denote the same labelled quad when followed
5152
+ by its indented answer descriptions. In `--iso-strict` mode this quad
5153
+ interpretation is disabled, and `?-/2` remains ordinary Prolog term syntax.
5150
5154
 
5151
5155
  Run [`iso-dynamic-database.pl`](https://github.com/eyereasoner/eyeprolog/blob/main/examples/iso-dynamic-database.pl)
5152
5156
  for an explicitly stateful queue and
@@ -5747,9 +5751,11 @@ the beginning. A peek does not mark the stream as past-end.
5747
5751
  | `write_canonical(+Term)`, `write_canonical(+Stream,+Term)` | Writes quoted canonical functor notation while ignoring operators and without interpreting *$VAR/1*. |
5748
5752
  | `write_term(+Term,+Options)`, `write_term(+Stream,+Term,+Options)` | Writes with *quoted(true or false)*, *ignore_ops(true or false)*, *numbervars(true or false)*, and *variable_names([Name=Variable,...])*. |
5749
5753
 
5750
- Term input uses the program's current operator table and applies installed
5751
- character conversions outside quoted text when the `char_conversion` flag is
5752
- `on`. `variable_names/1` and `singletons/1` omit anonymous variables. Output
5754
+ Term input uses the program's current operator table and the same ISO quoted-character
5755
+ syntax as source text, including backslash-terminated octal and hexadecimal
5756
+ escapes such as `'\7\'` and `'\x7\'`. This applies equally to `read/1-2` and
5757
+ `read_term/2-3`. Installed character conversions are applied outside quoted text
5758
+ when the `char_conversion` flag is `on`. `variable_names/1` and `singletons/1` omit anonymous variables. Output
5753
5759
  predicates do not append a period or newline; call `write/1`, then `write('.')`
5754
5760
  and `nl/0` when emitting a complete source term manually.
5755
5761
 
@@ -6361,7 +6367,10 @@ console.log(report.passed, report.failed, report.stdout);
6361
6367
  The syntax follows the “queries using answer descriptions” convention used by
6362
6368
  Trealla and the ISO Prolog working examples. Because answer descriptions are
6363
6369
  layout-sensitive, indent every description while keeping ordinary clause heads
6364
- and the next quad query at the left margin.
6370
+ and the next quad query at the left margin. A comma may be part of a quad label,
6371
+ including across layout before `?-`. Canonical functional notation is
6372
+ semantically equivalent: `?-(Label, Query).` followed by the same indented
6373
+ answer descriptions creates the same labelled quad as `Label ?- Query.`.
6365
6374
 
6366
6375
  Statistics are comparative evidence, not a score in isolation. Preserve the
6367
6376
  program, input, runtime version, selected query, answers, and counters together.