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 +1 -1
- package/src/quads.js +14 -6
- package/src/solver.js +9 -2
- package/test/run-regression.mjs +74 -0
- package/the-art-of-eyeprolog.md +11 -3
package/package.json
CHANGED
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 (
|
|
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)
|
|
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
|
}
|
package/test/run-regression.mjs
CHANGED
|
@@ -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: () => {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -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).
|
|
7045
|
-
|
|
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
|