eyeprolog 1.1.27 → 1.1.29

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.1.27",
6
+ "version": "1.1.29",
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
@@ -1783,7 +1783,7 @@ function* phraseBuiltin({ solver, goal, env }) {
1783
1783
  solver.absorbStatsFrom(child);
1784
1784
  }
1785
1785
  }
1786
- function formalErrorTerm(error) {
1786
+ export function formalErrorTerm(error) {
1787
1787
  const context = error.contextTerm ?? atom('eyeprolog');
1788
1788
  if (error.formalTerm != null) return compound('error', [error.formalTerm, context]);
1789
1789
  const parse = (text) => {
package/src/repl.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { createInterface } from 'node:readline';
5
+ import { formalErrorTerm } from './iso.js';
5
6
 
6
7
  const ANSWER_HELP = `
7
8
  SPACE, "n" or ";": next solution, if any
@@ -163,11 +164,33 @@ async function readQuery(reader) {
163
164
  }
164
165
  }
165
166
 
167
+ function quotedEscapeEnd(source, index) {
168
+ const escaped = source[index + 1] ?? '';
169
+ if (!escaped) return index;
170
+
171
+ // ISO 6.4.2.1 octal and hexadecimal escapes are terminated by a
172
+ // backslash. Consume that terminator as part of the escape so the REPL
173
+ // scanner does not mistake it for an escape of the following quote.
174
+ if (escaped === 'x') {
175
+ let cursor = index + 2;
176
+ while (/^[0-9A-Fa-f]$/.test(source[cursor] ?? '')) cursor++;
177
+ return source[cursor] === '\\' ? cursor : Math.max(index + 1, cursor - 1);
178
+ }
179
+ if (/^[0-7]$/.test(escaped)) {
180
+ let cursor = index + 1;
181
+ while (/^[0-7]$/.test(source[cursor] ?? '')) cursor++;
182
+ return source[cursor] === '\\' ? cursor : Math.max(index + 1, cursor - 1);
183
+ }
184
+
185
+ // Meta escapes, symbolic control escapes, and continuation escapes consume
186
+ // one character after the backslash. The parser performs validity checks.
187
+ return index + 1;
188
+ }
189
+
166
190
  function terminalFullStop(source) {
167
191
  let quote = null;
168
192
  let lineComment = false;
169
193
  let blockComment = false;
170
- let escaped = false;
171
194
  let depth = 0;
172
195
 
173
196
  for (let i = 0; i < source.length; i++) {
@@ -185,10 +208,8 @@ function terminalFullStop(source) {
185
208
  continue;
186
209
  }
187
210
  if (quote != null) {
188
- if (escaped) {
189
- escaped = false;
190
- } else if (ch === '\\') {
191
- escaped = true;
211
+ if (ch === '\\') {
212
+ i = quotedEscapeEnd(source, i);
192
213
  } else if (ch === quote) {
193
214
  if (next === quote) i++;
194
215
  else quote = null;
@@ -425,46 +446,17 @@ function formatError(engine, state, error) {
425
446
  const env = new engine.Env();
426
447
  const variableNames = new Map();
427
448
  let generated = 0;
428
- if (error.formalTerm != null) {
429
- collectUnboundVariables(engine, error.formalTerm, env, variableNames, () => `_${letterName(generated++)}`);
430
- if (error.contextTerm != null) {
431
- collectUnboundVariables(engine, error.contextTerm, env, variableNames, () => `_${letterName(generated++)}`);
432
- }
433
- const formal = engine.formatTermForWrite(error.formalTerm, env, {
434
- quoted: true,
435
- operators: [...state.program.operators.values()],
436
- variableNames,
437
- });
438
- if (error.contextTerm != null) {
439
- const context = engine.formatTermForWrite(error.contextTerm, env, {
440
- quoted: true,
441
- operators: [...state.program.operators.values()],
442
- variableNames,
443
- });
444
- return `error(${formal}, ${context}).`;
445
- }
446
- return `error(${formal}).`;
447
- }
448
- if (error.culprit != null) {
449
- collectUnboundVariables(engine, error.culprit, env, variableNames, () => `_${letterName(generated++)}`);
450
- }
451
- if (error.contextTerm != null) {
452
- collectUnboundVariables(engine, error.contextTerm, env, variableNames, () => `_${letterName(generated++)}`);
453
- }
454
- const culprit = error.culprit == null ? '' : `, ${engine.formatTermForWrite(error.culprit, env, {
449
+ // ISO 7.12.1 represents processor errors as error(ErrorTerm, ImpDef).
450
+ // Reuse the same conversion as catch/3 so uncaught errors at the top
451
+ // level cannot lose the implementation-defined context or misplace a
452
+ // culprit as the second argument of error/2.
453
+ const term = formalErrorTerm(error);
454
+ collectUnboundVariables(engine, term, env, variableNames, () => `_${letterName(generated++)}`);
455
+ return `${engine.formatTermForWrite(term, env, {
455
456
  quoted: true,
456
457
  operators: [...state.program.operators.values()],
457
458
  variableNames,
458
- })}`;
459
- if (error.contextTerm != null) {
460
- const context = engine.formatTermForWrite(error.contextTerm, env, {
461
- quoted: true,
462
- operators: [...state.program.operators.values()],
463
- variableNames,
464
- });
465
- return `error(${error.formal}${culprit}, ${context}).`;
466
- }
467
- return `error(${error.formal}${culprit}).`;
459
+ })}.`;
468
460
  }
469
461
  const message = error?.message ?? String(error);
470
462
  return message.endsWith('.') ? message : `${message}.`;
package/src/write.js CHANGED
@@ -22,8 +22,13 @@ function quoteAtom(name) {
22
22
  for (const ch of name) {
23
23
  if (ch === "'") out += "''";
24
24
  else if (ch === '\\') out += '\\\\';
25
- else if (ch === '\n') out += '\\n';
25
+ else if (ch === '\x07') out += '\\a';
26
+ else if (ch === '\b') out += '\\b';
27
+ else if (ch === '\r') out += '\\r';
28
+ else if (ch === '\f') out += '\\f';
26
29
  else if (ch === '\t') out += '\\t';
30
+ else if (ch === '\n') out += '\\n';
31
+ else if (ch === '\v') out += '\\v';
27
32
  else out += ch;
28
33
  }
29
34
  return out + "'";
@@ -533,8 +533,23 @@ c4 ?- call((!;1)).
533
533
  });
534
534
  assertEqual(result.status, 0, 'exit status');
535
535
  assertEqual(result.stdout,
536
- '?- error(type_error(list), [1, [], _A | 2]).\n' +
537
- '?- error(type_error(list), [1, [], _A | 2]).\n' +
536
+ '?- error(type_error(list, [1, [], _A | 2]), eyeprolog).\n' +
537
+ '?- error(type_error(list, [1, [], _A | 2]), eyeprolog).\n' +
538
+ '?- ',
539
+ 'stdout');
540
+ assertEqual(result.stderr, '', 'stderr');
541
+ },
542
+ },
543
+ {
544
+ name: 'REPL uncaught ISO errors retain the error/2 implementation context',
545
+ run: () => {
546
+ const result = runCli([], {
547
+ input: '_ is _.\ncatch(_ is _, error(Error, Imp_def), true).\nhalt.\n',
548
+ });
549
+ assertEqual(result.status, 0, 'exit status');
550
+ assertEqual(result.stdout,
551
+ '?- error(instantiation_error, eyeprolog).\n' +
552
+ '?- Error = instantiation_error, Imp_def = eyeprolog.\n' +
538
553
  '?- ',
539
554
  'stdout');
540
555
  assertEqual(result.stderr, '', 'stderr');
@@ -602,6 +617,22 @@ c4 ?- call((!;1)).
602
617
  assertEqual(result.stderr, '', 'stderr');
603
618
  },
604
619
  },
620
+ {
621
+ name: 'REPL recognizes ISO octal and hexadecimal escapes in quoted atoms',
622
+ run: () => {
623
+ const result = runCli([], {
624
+ input: "writeq('\\7\\').\nwriteq('\\x7\\').\nwriteq('\\a').\nhalt.\n",
625
+ });
626
+ assertEqual(result.status, 0, 'exit status');
627
+ assertEqual(result.stdout,
628
+ "?- '\\a' true.\n" +
629
+ "?- '\\a' true.\n" +
630
+ "?- '\\a' true.\n" +
631
+ '?- ',
632
+ 'stdout');
633
+ assertEqual(result.stderr, '', 'stderr');
634
+ },
635
+ },
605
636
  {
606
637
  name: 'REPL accepts multiline period-terminated queries',
607
638
  run: () => {
@@ -5268,8 +5268,11 @@ city('München').
5268
5268
  message("café").
5269
5269
  ```
5270
5270
 
5271
- Inside a quoted atom, a single quote is doubled: `'don''t'`. Double-quoted lists support
5272
- the common escapes `\n`, `\t`, `\"`, and `\\`. Whitespace is insignificant
5271
+ Inside a quoted atom, a single quote is doubled: `'don''t'`. Quoted characters
5272
+ support the ISO symbolic control escapes such as `\a`, `\n`, and `\t`, and numeric
5273
+ octal or hexadecimal escapes are terminated by a backslash; for example, `'\7\'`
5274
+ and `'\x7\'` both denote the alert character. Double-quoted lists use the same
5275
+ quoted-character escapes. Whitespace is insignificant
5273
5276
  between tokens, and a `%` comment continues to the end of its line. Doubling
5274
5277
  the active delimiter is also accepted inside either quoted form, so `""`
5275
5278
  inside double-quoted notation denotes one literal double quote character.
@@ -5803,11 +5806,14 @@ instances whose message contains the corresponding Prolog error term.
5803
5806
  `error(Formal,eyeprolog)` term. `throw/1` copies its ball before unwinding: bound
5804
5807
  parts are preserved, repeated variables remain shared within the copied ball,
5805
5808
  and unbound variables are fresh with respect to the protected goal and catcher.
5806
- Catchable error terms follow the same variable-freshening rule. At the interactive
5807
- top level, variables that occur in an uncaught ISO error are rendered with fresh
5808
- answer names such as `_A` rather than reusing query-variable spellings such as
5809
- `X` or `Xx`; this keeps the displayed error consistent with the copied exception
5810
- term. An unmatched ball or error continues outward.
5809
+ Catchable error terms follow the same variable-freshening rule. The interactive
5810
+ top level uses the same ISO error envelope for uncaught processor errors, so an
5811
+ ordinary runtime error is displayed as `error(Formal,eyeprolog)` rather than
5812
+ dropping the implementation-defined second argument. Variables that occur in an
5813
+ uncaught ISO error are rendered with fresh answer names such as `_A` rather than
5814
+ reusing query-variable spellings such as `X` or `Xx`; this keeps the displayed
5815
+ error consistent with the copied exception term. An unmatched ball or error
5816
+ continues outward.
5811
5817
 
5812
5818
  Streams belong to one solver run and are shared by nested calls, exceptions,
5813
5819
  and solution collectors. `user_input` and `user_output` are always present.