eyeprolog 1.5.86 → 1.5.88

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.5.86",
6
+ "version": "1.5.88",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/quads.js CHANGED
@@ -82,7 +82,7 @@ function checkDescription(program, quad, description, options, context) {
82
82
  if (part.type === ATOM && part.name === 'other_answer_sequence') {
83
83
  const previous = alternatives.at(-1);
84
84
  if (previous == null || unordered.has(previous)) {
85
- return { ok: false, kind: 'malformed', expected: part };
85
+ return { ok: false, kind: 'malformed', expected: part, alternative: previous ?? undefined };
86
86
  }
87
87
  unordered.add(previous);
88
88
  } else alternatives.push(part);
@@ -93,7 +93,13 @@ function checkDescription(program, quad, description, options, context) {
93
93
  Number(alternativeDescribesLoop(right)) - Number(alternativeDescribesLoop(left)));
94
94
  for (const alternative of ordered) {
95
95
  const malformed = malformedAlternative(quad.query, alternative);
96
- if (malformed != null) return { ok: false, kind: 'malformed', expected: malformed };
96
+ // Carry the whole offending `|`-alternative alongside the narrow
97
+ // sub-term the check actually tripped on: with several alternatives (the
98
+ // norm for `sto`-annotated queries -- see issue #112's follow-up),
99
+ // formatFailure shows this alternative in full and elides its siblings
100
+ // instead of leaving a reader to guess which branch a bare sub-term like
101
+ // `_B = [E|_B]` came from.
102
+ if (malformed != null) return { ok: false, kind: 'malformed', expected: malformed, alternative };
97
103
  }
98
104
  let unsupported = null;
99
105
  let undecided = null;
@@ -111,19 +117,19 @@ function checkAlternative(program, quad, alternative, options, context, unordere
111
117
  const requiresSto = leaves.some((leaf) => leaf.sto);
112
118
  const unsupported = leaves.find((leaf) => leaf.unsupported != null)?.unsupported;
113
119
  if (unsupported != null) {
114
- return { ok: false, kind: 'unsupported', expected: unsupported };
120
+ return { ok: false, kind: 'unsupported', expected: unsupported, alternative };
115
121
  }
116
122
 
117
123
  // A permutation describes a complete sequence, not an arbitrary prefix or
118
124
  // a negative assertion. Do not silently weaken those annotations.
119
125
  if (unordered && leaves.some((leaf) => leaf.more || leaf.unexpected || leaf.sto ||
120
126
  leaf.waits || leaf.input != null || leaf.peek != null)) {
121
- return { ok: false, kind: 'unsupported', expected: alternative };
127
+ return { ok: false, kind: 'unsupported', expected: alternative, alternative };
122
128
  }
123
129
 
124
130
  const ioLeaves = leaves.filter((leaf) => leaf.input != null || leaf.peek != null);
125
131
  if (ioLeaves.length > 1 || (ioLeaves.length > 0 && leaves.length !== 1)) {
126
- return { ok: false, kind: 'malformed', expected: alternative };
132
+ return { ok: false, kind: 'malformed', expected: alternative, alternative };
127
133
  }
128
134
  const hasInputSpec = ioLeaves.length === 1;
129
135
  const inputLeaf = ioLeaves[0] ?? null;
@@ -986,6 +992,43 @@ const FAILURE_LABELS = {
986
992
  undecided: 'UNDECIDED',
987
993
  };
988
994
 
995
+ // With several `|`-separated alternatives -- the norm for `sto`-annotated
996
+ // queries -- a bare offending sub-term like `_B = [E|_B]` gives no sense of
997
+ // which alternative it came from. Show that whole alternative in place, laid
998
+ // out the same way the answer-description syntax itself lays out alternatives
999
+ // and leaves (a leading `|` per alternative after the first, a leading `;`
1000
+ // per leaf after an alternative's first), and stand in for sibling
1001
+ // alternatives with a bare `...` rather than reproducing them (they were not
1002
+ // the problem) or dropping them silently (a reader can no longer tell how
1003
+ // many alternatives, or which one, this was) -- see issue #112's follow-up
1004
+ // (https://github.com/eyereasoner/eyeprolog/issues/112#issuecomment-5645403025).
1005
+ // Comments are not retained past parsing, so unlike a hand-written answer
1006
+ // description this reconstruction never has any to show. Returns null when
1007
+ // the description has only one alternative to begin with: there is then
1008
+ // nothing to elide, and the existing plain rendering already shows the whole
1009
+ // thing.
1010
+ function formatAlternativeContext(program, description, alternative) {
1011
+ const parts = splitOperator(description, '|');
1012
+ if (parts.length <= 1 || !parts.includes(alternative)) return null;
1013
+ const lines = [];
1014
+ parts.forEach((part, index) => {
1015
+ const prefix = index === 0 ? ' ' : '| ';
1016
+ if (part !== alternative) {
1017
+ lines.push(`${prefix}...`);
1018
+ return;
1019
+ }
1020
+ splitOperator(part, ';').forEach((leaf, leafIndex) => {
1021
+ lines.push(`${leafIndex === 0 ? prefix : '; '}${formatQuadTerm(program, leaf)}`);
1022
+ });
1023
+ });
1024
+ // A trailing `...` glued straight to a full stop reads as four dots; the
1025
+ // answer-description syntax itself always writes a space before the period
1026
+ // in that position (compare `..., ad_infinitum` with `... .`), so match it.
1027
+ const last = lines.length - 1;
1028
+ lines[last] = lines[last].endsWith('...') ? `${lines[last]} .` : `${lines[last]}.`;
1029
+ return lines.join('\n');
1030
+ }
1031
+
989
1032
  function formatFailure(program, quad, result, description = quad.answers[0]) {
990
1033
  const source = quad.source ?? { filename: '<input>', line: 1 };
991
1034
  // Point at the failing answer description's own line rather than always the
@@ -995,9 +1038,21 @@ function formatFailure(program, quad, result, description = quad.answers[0]) {
995
1038
  const label = quad.id == null ? '' : `${formatQuadTerm(program, quad.id)}, `;
996
1039
  const reason = FAILURE_LABELS[result.kind] ?? 'FAILED';
997
1040
  const expected = result.expected ?? description;
1041
+ const context = result.alternative != null
1042
+ ? formatAlternativeContext(program, description, result.alternative)
1043
+ : null;
998
1044
  const detail = result.kind === 'undecided'
999
1045
  ? ` undecided: ${result.reason}.\n`
1000
- : ` expected: ${formatQuadTerm(program, expected)}.\n`;
1046
+ // When the offending sub-term already *is* the whole alternative (an
1047
+ // `expected:` line would just repeat the context lines), the context
1048
+ // alone is the full, non-redundant report. formatAlternativeContext
1049
+ // already lays out and terminates its own lines, so it needs no further
1050
+ // wrapping here (unlike the plain single-line `expected:` case below).
1051
+ : context != null && expected === result.alternative
1052
+ ? `${context}\n`
1053
+ : context != null
1054
+ ? `${context}\n expected: ${formatQuadTerm(program, expected)}.\n`
1055
+ : ` expected: ${formatQuadTerm(program, expected)}.\n`;
1001
1056
  return `quads: ${reason} ${label}${source.filename}:${line}\n` +
1002
1057
  ` ?- ${formatQuadTerm(program, quad.query)}.\n` + detail;
1003
1058
  }
@@ -2552,6 +2552,49 @@ c4 ?- call((!;1)).
2552
2552
  assertEqual(result.total, 1, 'quad total');
2553
2553
  assertEqual(result.failed, 1, 'quad failed');
2554
2554
  assertIncludes(result.stdout, 'quads: MALFORMED malformed-quad.pl:2', 'malformed report');
2555
+ // A single `|`-alternative already is the whole answer description --
2556
+ // there is nothing to elide, so the plain, unelided report is
2557
+ // unchanged (contrast the multi-alternative case just below).
2558
+ assertNotIncludes(result.stdout, ' | ', 'single-alternative report stays unelided');
2559
+ },
2560
+ },
2561
+ {
2562
+ name: 'runQuads shows a malformed sub-term in the context of its full sibling-elided alternative (issue #112)',
2563
+ run: () => {
2564
+ // Adapted from the upstream Prologue `select(E, Xs, Xs)` quad
2565
+ // (https://github.com/eyereasoner/eyeprolog/issues/112#issuecomment-5645403025):
2566
+ // a query-unrelated `Ys` stands in for the legitimate forward-reference
2567
+ // idiom so this stays malformed regardless of that fix. With three
2568
+ // `|`-alternatives, a bare `expected: Ys = [E|Ys].` gives no sense of
2569
+ // which one it came from; the report should instead show that whole
2570
+ // alternative in place and stand in for its siblings with `...`.
2571
+ const source = `:- use_module(library(prologue)).
2572
+ ?- select(E, Xs, Xs).
2573
+ sto,
2574
+ loops
2575
+ | sto,
2576
+ Xs = [E|Xs]
2577
+ ; Xs = [_A|_B], Ys = [E|Ys]
2578
+ ; ..., ad_infinitum
2579
+ | sto,
2580
+ Xs = [E,E|_A]
2581
+ ; Xs = [_A,E,E|_B]
2582
+ ; ..., ad_infinitum.
2583
+ `;
2584
+ const result = publicApi.runQuads(Program.parseSources([{ text: source, filename: 'select-quad.pl' }]));
2585
+ assertEqual(result.total, 1, 'quad total');
2586
+ assertEqual(result.failed, 1, 'quad failed');
2587
+ // Laid out the same way the answer-description syntax itself lays
2588
+ // out alternatives (a leading `|`) and leaves (a leading `;`), per
2589
+ // Ulrich's own proposed shape in that follow-up comment.
2590
+ assertIncludes(result.stdout,
2591
+ ' ...\n' +
2592
+ '| sto, Xs = [E | Xs]\n' +
2593
+ '; Xs = [_A | _B], Ys = [E | Ys]\n' +
2594
+ '; ..., ad_infinitum\n' +
2595
+ '| ... .\n',
2596
+ 'sibling-elided alternative context');
2597
+ assertIncludes(result.stdout, ' expected: Ys = [E | Ys].\n', 'still-precise offending sub-term');
2555
2598
  },
2556
2599
  },
2557
2600
  {