eyeprolog 1.5.88 → 1.5.89

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.88",
6
+ "version": "1.5.89",
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
@@ -162,6 +162,21 @@ function checkAlternative(program, quad, alternative, options, context, unordere
162
162
  // matchLeaf accepts it once this disproof gate has been cleared.
163
163
  if (requiresSto && actual.nstoObserved) return { ok: false };
164
164
 
165
+ // The reverse of the check above: execution that actually triggered
166
+ // occurs-check produced an implementation-dependent outcome (ISO 7.3.3
167
+ // Note 2 -- succeed, loop, or fail are all conforming), even when none of
168
+ // this alternative's own leaves say so. A leaf making a positive claim
169
+ // about that outcome without an `sto` tag would silently misrepresent an
170
+ // implementation choice as a portably decided one (see issue #111's
171
+ // `\+ \+ -X=X` follow-up: a bare `false.` happens to match here, but only
172
+ // because this engine always occurs-checks -- a non-occurs-checking engine
173
+ // would see `true` instead). An `unexpected` leaf is a negative claim
174
+ // instead, and already has its own STO carve-out below
175
+ // (`stoPermitsUnexpected`, issue #60), so leave those alone here.
176
+ if (!requiresSto && actual.stoObserved && !leaves.some((leaf) => leaf.unexpected)) {
177
+ return { ok: false, alternative };
178
+ }
179
+
165
180
  if (hasInputSpec) {
166
181
  const leaf = leaves[0];
167
182
  const exactConsumption = actual.inputPosition === input.length;
@@ -1010,15 +1025,22 @@ const FAILURE_LABELS = {
1010
1025
  function formatAlternativeContext(program, description, alternative) {
1011
1026
  const parts = splitOperator(description, '|');
1012
1027
  if (parts.length <= 1 || !parts.includes(alternative)) return null;
1028
+ // Every detail line under a `quads: ...` header shares one 3-space report
1029
+ // margin (the same margin the plain `?- ...` and `expected: ...` lines
1030
+ // already use) -- reproduce the answer-description syntax's own layout
1031
+ // (a bare 3-space indent for the first alternative, a leading `| `/`; `
1032
+ // marker for a later alternative/leaf) *inside* that margin, so a marker
1033
+ // character lines up under the `?` above it rather than at column 0.
1034
+ const margin = ' ';
1013
1035
  const lines = [];
1014
1036
  parts.forEach((part, index) => {
1015
- const prefix = index === 0 ? ' ' : '| ';
1037
+ const marker = index === 0 ? ' ' : '| ';
1016
1038
  if (part !== alternative) {
1017
- lines.push(`${prefix}...`);
1039
+ lines.push(`${margin}${marker}...`);
1018
1040
  return;
1019
1041
  }
1020
1042
  splitOperator(part, ';').forEach((leaf, leafIndex) => {
1021
- lines.push(`${leafIndex === 0 ? prefix : '; '}${formatQuadTerm(program, leaf)}`);
1043
+ lines.push(`${margin}${leafIndex === 0 ? marker : '; '}${formatQuadTerm(program, leaf)}`);
1022
1044
  });
1023
1045
  });
1024
1046
  // A trailing `...` glued straight to a full stop reads as four dots; the
@@ -2586,13 +2586,16 @@ c4 ?- call((!;1)).
2586
2586
  assertEqual(result.failed, 1, 'quad failed');
2587
2587
  // Laid out the same way the answer-description syntax itself lays
2588
2588
  // out alternatives (a leading `|`) and leaves (a leading `;`), per
2589
- // Ulrich's own proposed shape in that follow-up comment.
2589
+ // Ulrich's own proposed shape in that follow-up comment, inside the
2590
+ // same 3-space report margin every other detail line under a
2591
+ // `quads: ...` header uses -- so each `|`/`;` marker lines up under
2592
+ // the `?` of the `?- ...` line above it, not at column 0.
2590
2593
  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',
2594
+ ' ...\n' +
2595
+ ' | sto, Xs = [E | Xs]\n' +
2596
+ ' ; Xs = [_A | _B], Ys = [E | Ys]\n' +
2597
+ ' ; ..., ad_infinitum\n' +
2598
+ ' | ... .\n',
2596
2599
  'sibling-elided alternative context');
2597
2600
  assertIncludes(result.stdout, ' expected: Ys = [E | Ys].\n', 'still-precise offending sub-term');
2598
2601
  },
@@ -2688,6 +2691,33 @@ c4 ?- call((!;1)).
2688
2691
  assertIncludes(nsto.stdout, 'quads: FAILED 34, <input>:3', 'NSTO diagnostic');
2689
2692
  },
2690
2693
  },
2694
+ {
2695
+ name: 'quad sto is required, not merely tolerated, once execution observes occurs-check (issue #111 follow-up)',
2696
+ run: () => {
2697
+ // https://github.com/eyereasoner/eyeprolog/issues/111#issuecomment-5645863132:
2698
+ // `\+ \+ -X=X` fails only because this engine always occurs-checks
2699
+ // plain `=`/2 (ISO 7.3.3 Note 2 leaves succeed/loop/fail all
2700
+ // conforming); a non-occurs-checking engine sees `-X=X` succeed
2701
+ // (creating a cyclic X), so `\+ \+` sees `true` instead. A bare
2702
+ // `false.` -- with no `sto` tag -- misrepresents that as a portably
2703
+ // decided outcome, so it must be rejected even though it happens to
2704
+ // match this engine's own behavior; only the honestly `sto`-tagged
2705
+ // claim may pass.
2706
+ const bare = publicApi.runQuads('?- \\+ \\+ -X=X.\n false.\n');
2707
+ assertEqual(bare.passed, 0, 'undeclared sto claim rejected');
2708
+ assertEqual(bare.failed, 1, 'undeclared sto claim rejected');
2709
+
2710
+ const declared = publicApi.runQuads('?- \\+ \\+ -X=X.\n sto, false.\n');
2711
+ assertEqual(declared.passed, 1, 'declared sto claim accepted');
2712
+ assertEqual(declared.failed, 0, 'declared sto claim accepted');
2713
+
2714
+ // An `unexpected` leaf is a negative claim with its own, separate STO
2715
+ // carve-out (issue #60, tested above); this new rule must not touch it.
2716
+ const unexpected = publicApi.runQuads('?- \\+ \\+ -X=X.\n false, unexpected.\n');
2717
+ assertEqual(unexpected.passed, 0, 'wrong negative claim still rejected on its own terms');
2718
+ assertEqual(unexpected.failed, 1, 'wrong negative claim still rejected on its own terms');
2719
+ },
2720
+ },
2691
2721
  {
2692
2722
  name: 'quads peeks/1 supplies one unconsumed look-ahead character (issue #62)',
2693
2723
  run: () => {