eyeprolog 1.3.93 → 1.3.95

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.3.93",
6
+ "version": "1.3.95",
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
@@ -152,17 +152,25 @@ function checkAlternative(program, quad, alternative, options, context) {
152
152
  // portably outlaw the implementation's chosen STO outcome. This is the
153
153
  // case behind issue #60's `false, unexpected` example.
154
154
  const stoPermitsUnexpected = context.declaresSto && actual.stoObserved && leaf.unexpected && !leaf.sto;
155
- if (!stoPermitsUnexpected && (leaf.unexpected ? matches : !matches)) {
155
+ if (leaf.unexpected) {
156
+ // `unexpected` is a negative assertion about the leaf at this answer
157
+ // position. Once the observed answer differs, the assertion is proved;
158
+ // it does not consume that answer or require the query to end afterward.
159
+ // Likewise, an STO-declared query may accept an implementation-dependent
160
+ // answer at this point without imposing any later answer-sequence check.
161
+ if (stoPermitsUnexpected) return { ok: true };
162
+ if (matches) return { ok: false };
163
+ if (actual.undecided && leafNeedsMoreSearch(leaf, actual, position)) {
164
+ return undecidedResult(actual, alternative);
165
+ }
166
+ return { ok: true };
167
+ }
168
+ if (!matches) {
156
169
  if (actual.undecided && leafNeedsMoreSearch(leaf, actual, position)) {
157
170
  return undecidedResult(actual, alternative);
158
171
  }
159
172
  return { ok: false };
160
173
  }
161
- // An unexpected error description is a negative assertion about that
162
- // particular error pattern. A different exception may be present and is
163
- // checked by other descriptions; do not make the final 'no error' test
164
- // turn a successful negative match back into a failure.
165
- if (leaf.unexpected && leaf.error != null) return { ok: true };
166
174
  if (leaf.more) return { ok: true };
167
175
  if (!leaf.unexpected && (leaf.false || leaf.loops || leaf.error != null)) {
168
176
  if (actual.undecided) return undecidedResult(actual, alternative);
package/src/solver.js CHANGED
@@ -524,14 +524,21 @@ export class Solver {
524
524
  if (this.solutionsSeen >= this.solutionLimit) break;
525
525
 
526
526
  const pendingAttributeGoals = env.takePendingAttributeGoals?.() ?? [];
527
- if (pendingAttributeGoals.length > 0) goals = [...pendingAttributeGoals, ...goals];
527
+ if (pendingAttributeGoals.length > 0) {
528
+ // Goals produced by verify_attributes/3 are invoked by the attribute
529
+ // mechanism, not textually spliced into the caller. Give every
530
+ // awakened goal the same opaque cut boundary as call/1 so a delayed
531
+ // cut cannot prune choices that were made before the suspension.
532
+ const awakened = pendingAttributeGoals.map((pending) => compound('call', [pending]));
533
+ goals = [...awakened, ...goals];
534
+ }
528
535
 
529
536
  const readyDelays = env.takeReadyDelays();
530
537
  if (readyDelays.length > 0) {
531
538
  const awakened = readyDelays.map(({ goal, module }) => {
532
539
  const delayed = copyResolved(goal, env);
533
540
  qualifyTerm(delayed, module);
534
- return delayed;
541
+ return compound('call', [delayed]);
535
542
  });
536
543
  goals = [...awakened, ...goals];
537
544
  }
@@ -951,6 +951,64 @@ why(
951
951
  assertEqual(result.stdout, 'quads: 13 run, 13 passed, 0 failed.\n', 'quad report');
952
952
  },
953
953
  },
954
+ {
955
+ name: 'runQuads treats unexpected as a negative assertion on the next leaf (issue #83)',
956
+ run: () => {
957
+ const source = `:- use_module(library(prologue)).
958
+ ` +
959
+ `40
960
+ ` +
961
+ `?- member(X,"abc").
962
+ ` +
963
+ ` X = a
964
+ ` +
965
+ `; X = b
966
+ ` +
967
+ `; X = c.
968
+ ` +
969
+ ` X = a
970
+ ` +
971
+ `; X = c, unexpected.
972
+
973
+ ` +
974
+ `41
975
+ ` +
976
+ `?- member(X, Xs).
977
+ ` +
978
+ ` Xs = [X|_A]
979
+ ` +
980
+ `; ... .
981
+ ` +
982
+ ` Xs = [X|_A]
983
+ ` +
984
+ `; Xs = [_A,X|_B]
985
+ ` +
986
+ `; Xs = [_A,_B,X|_C]
987
+ ` +
988
+ `; ... .
989
+ ` +
990
+ ` Xs = [X|_A]
991
+ ` +
992
+ `; Xs = [], unexpected.
993
+ `;
994
+ const result = publicApi.runQuads(Program.parseSources([{ text: source, filename: 'unexpected-quad.pl' }]));
995
+ assertEqual(result.total, 5, 'quad total');
996
+ assertEqual(result.passed, 5, 'quad passed');
997
+ assertEqual(result.failed, 0, 'quad failed');
998
+ assertEqual(result.stdout, 'quads: 5 run, 5 passed, 0 failed.\n', 'quad report');
999
+
1000
+ const forbidden = publicApi.runQuads(`:- use_module(library(prologue)).
1001
+ ` +
1002
+ `?- member(X,[a,c]).
1003
+ ` +
1004
+ ` X = a
1005
+ ` +
1006
+ `; X = c, unexpected.
1007
+ `);
1008
+ assertEqual(forbidden.total, 1, 'forbidden next answer total');
1009
+ assertEqual(forbidden.failed, 1, 'matching unexpected next answer fails');
1010
+ },
1011
+ },
954
1012
  {
955
1013
  name: 'runQuads distinguishes query variables from renamed throw variables',
956
1014
  run: () => {
@@ -1990,6 +2048,22 @@ c4 ?- call((!;1)).
1990
2048
  assertEqual(result.stdout, 'wake(ready, awake).\n', 'freeze answer');
1991
2049
  },
1992
2050
  },
2051
+ {
2052
+ name: 'Prologue freeze keeps awakened cuts inside the delayed call (issue #81)',
2053
+ run: () => {
2054
+ const result = publicApi.runQuads(`:- use_module(library(prologue)).
2055
+ 38
2056
+ ?- call( ( (Y=1;Y=2), freeze(X,!), X = c ) ); Y = 3.
2057
+ Y = 1, X = c
2058
+ ; Y = 2, X = c
2059
+ ; Y = 3.
2060
+ `);
2061
+ assertEqual(result.total, 1, 'quad total');
2062
+ assertEqual(result.passed, 1, 'quad passed');
2063
+ assertEqual(result.failed, 0, 'quad failed');
2064
+ assertEqual(result.stdout, 'quads: 1 run, 1 passed, 0 failed.\n', 'quad report');
2065
+ },
2066
+ },
1993
2067
  {
1994
2068
  name: 'runQuads covers the remaining finite Prologue examples and arities',
1995
2069
  run: () => {
@@ -6283,7 +6283,11 @@ private helpers and same-named predicates in different modules separate.
6283
6283
  `freeze(?Term,:Goal)` runs `Goal` immediately when `Term` is already nonvariable;
6284
6284
  otherwise it delays the goal until `Term` becomes nonvariable. Suspensions are
6285
6285
  kept in the logical environment, so bindings and backtracking remain isolated
6286
- between solution branches.
6286
+ between solution branches. When a suspension wakes, its goal is meta-invoked
6287
+ with its own cut scope: a `!` inside the delayed goal may commit choices made by
6288
+ that invocation, but it cannot prune alternatives that were created before the
6289
+ `freeze/2` call. Thus `call(((Y=1;Y=2),freeze(X,!),X=c));Y=3` retains the three
6290
+ answers `Y=1`, `Y=2`, and `Y=3`.
6287
6291
 
6288
6292
  `dif(?Left,?Right)` posts a delayed finite-tree disequality when its arguments
6289
6293
  can still unify. Its residual store is normalized by logical implication:
@@ -7041,8 +7045,12 @@ accepted as a negative answer.
7041
7045
  Answer descriptions support ordered answers separated by `;`, acceptable
7042
7046
  alternatives separated by `|`, `true`, `false`, standard error descriptions,
7043
7047
  and the `unexpected` annotation for an answer that must not occur (`inattendue`
7044
- is its synonym). Variables named in the query keep their identity inside answer
7045
- descriptions; variables introduced only by a description are fresh. For example,
7048
+ is its synonym). In an ordered answer sequence, `unexpected` is a negative
7049
+ assertion about the answer at that position: once the next observed answer does
7050
+ not match the annotated leaf, that description succeeds and does not require the
7051
+ query to have no further answers. Variables named in the query keep their
7052
+ identity inside answer descriptions; variables introduced only by a description
7053
+ are fresh. For example,
7046
7054
  a query `throw(g(X))` is described by `throw(g(_X))`, while
7047
7055
  `throw(g(X)), unexpected` verifies that ISO `throw/1` did not retain the query
7048
7056
  variable in the renamed exception term. `...` and `ad_infinitum` accept further answers. The `maybe` annotation describes