eyeprolog 1.2.1 → 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.1",
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;
@@ -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).
@@ -1206,6 +1206,44 @@ c4 ?- call((!;1)).
1206
1206
  assertIncludes(result.stderr, 'implementation-specific directive use_module/1', 'stderr');
1207
1207
  },
1208
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
+ },
1209
1247
  {
1210
1248
  name: 'term input keeps dotted operators intact and uses program operators',
1211
1249
  run: () => {
@@ -5751,9 +5751,11 @@ the beginning. A peek does not mark the stream as past-end.
5751
5751
  | `write_canonical(+Term)`, `write_canonical(+Stream,+Term)` | Writes quoted canonical functor notation while ignoring operators and without interpreting *$VAR/1*. |
5752
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,...])*. |
5753
5753
 
5754
- Term input uses the program's current operator table and applies installed
5755
- character conversions outside quoted text when the `char_conversion` flag is
5756
- `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
5757
5759
  predicates do not append a period or newline; call `write/1`, then `write('.')`
5758
5760
  and `nl/0` when emitting a complete source term manually.
5759
5761